import { describe, expect, it } from "vitest"; import { generateTeam } from "./generate"; import { translateRotation } from "./rotation"; import { teamSchema, type Team } from "./types"; const build = { id: "mav", characterId: "mav", characterKey: "mavuika", characterName: "Mavuika", characterImageUrl: "", level: 90, maxLevel: 90, constellation: 0, talents: [9, 9, 9], weaponId: "weapon", weaponKey: "favoniusgreatsword", weaponName: "Weapon", weaponLevel: 90, weaponMaxLevel: 90, refinement: 1, artifactMode: "4pc", artifactOneId: "set", artifactOneKey: "noblesseoblige", artifactOneName: "Set", artifactTwoId: "", artifactTwoKey: "", artifactTwoName: "", sands: "atk%", goblet: "pyro%", circlet: "cr", rolls: { hp: 0, atk: 0, def: 0, em: 0, er: 0, "hp%": 0, "atk%": 0, "def%": 0, cr: 0, cd: 0 }, } satisfies Team["builds"][number]; const team: Team = { id: "team", name: "Team", builds: [build, { ...build, id: "xil", characterId: "xil", characterKey: "xilonen", characterName: "Xilonen" }] }; const withRotation = (text: string, repeat = false): Team => ({ ...team, rotation: { enabled: true, text, repeat } }); describe("rotation translator", () => { it("translates the requested example", () => { expect(translateRotation(withRotation("Mavuika E > Xilonen E N2"))).toEqual({ code: "active mavuika;\n\nmavuika skill;\nxilonen skill, attack:2;" }); }); it("preserves combined actions and repeated group order", () => { expect(translateRotation(withRotation("mavuika Q 3(N2CD) J")).code).toContain("mavuika burst, attack:2, charge, dash, attack:2, charge, dash, attack:2, charge, dash, jump;"); }); it("accepts Skirk's compact repeat in the fourth rotation step", () => { const skirk = { ...build, id: "skirk", characterId: "skirk", characterKey: "skirk", characterName: "Skirk" }; const rotation = { ...withRotation("Mavuika E > Xilonen E N2 > Mavuika Q > Skirk E 2N2D Q"), builds: [...team.builds, skirk] }; expect(translateRotation(rotation).error).toBeUndefined(); expect(translateRotation(rotation).code).toContain("skirk skill, attack:2, dash, attack:2, dash, burst;"); }); it.each([ ["2N2D", "2(N2D)"], ["3N2CD Q", "3(N2CD) Q"], ["2n2d", "2(N2D)"], ["2hE J", "2(hE) J"], ["2(3N1CJ D)", "2(3(N1CJ) D)"], ])("interprets compact combo %s as %s", (compact, grouped) => { expect(translateRotation(withRotation(`Mavuika ${compact}`))).toEqual(translateRotation(withRotation(`Mavuika ${grouped}`))); }); it.each(["0N2D", "2000N2D", "2N0D", "2.5N2D", "2N2DX", "2N2001D"])("rejects invalid compact combo %s", (combo) => { expect(translateRotation(withRotation(`Mavuika ${combo}`)).error).toBeTruthy(); }); it("supports tap/hold, waits, case insensitive keys and newlines", () => { expect(translateRotation(withRotation("MAVUIKA tE W(60) hE\nxilonen n" )).code).toBe("active mavuika;\n\nmavuika skill[hold=0];\nwait(60);\nmavuika skill[hold=1];\nxilonen attack;"); }); it("supports nested repetitions and loops", () => { expect(translateRotation(withRotation("Mavuika 2(2(N)D)", true)).code).toBe("active mavuika;\n\nwhile 1 {\n mavuika attack, attack, dash, attack, attack, dash;\n}"); }); it("matches multiword names before shorter names", () => { const named: Team = { ...withRotation("Kaedehara Kazuha E"), builds: [{ ...build, characterKey: "kazuha", characterName: "Kaedehara Kazuha" }] }; expect(translateRotation(named).code).toContain("kazuha skill;"); }); it.each(["", "Mavuika", "Mavuika N0", "Mavuika 0(N)", "Mavuika 2()", "Mavuika 2(N", "Mavuika N)", "Mavuika W(0)", "Mavuika W(-1)", "Mavuika Z", "Mavuika E >", "Nahida E", "MavuikaExtra E", "Mavuika 2000(N2)", "Mavuika N2000 > Xilonen E"])("rejects invalid rotation %s", (text) => { expect(translateRotation(withRotation(text)).error).toBeTruthy(); expect(translateRotation(withRotation(text)).code).toBe(""); }); it("enforces text and nesting limits", () => { expect(translateRotation(withRotation("Mavuika " + "N ".repeat(5000))).error).toContain("10,000"); expect(translateRotation(withRotation("Mavuika " + "1(".repeat(40) + "N" + ")".repeat(40))).error).toContain("nested"); }); it("accepts exactly 2000 expanded actions", () => { expect(translateRotation(withRotation("Mavuika N2000")).error).toBeUndefined(); }); it("excludes disabled rotation without validating its text", () => { const disabled: Team = { ...withRotation("invalid"), rotation: { enabled: false, text: "invalid", repeat: true } }; expect(translateRotation(disabled)).toEqual({ code: "" }); expect(generateTeam(disabled)).toBe(generateTeam(team)); }); it("appends valid rotation and keeps build preview for invalid input", () => { expect(generateTeam(withRotation("Mavuika E"))).toBe(generateTeam(team) + "\n\n# Rotation\nactive mavuika;\n\nmavuika skill;"); expect(generateTeam(withRotation("Mavuika Z"))).toBe(generateTeam(team)); }); it("rejects removed characters", () => { expect(translateRotation({ ...withRotation("Xilonen E"), builds: [build] }).error).toContain("Unknown character"); }); it("loads old drafts and round trips independent settings", () => { expect(teamSchema.parse(team).rotation).toBeUndefined(); const saved = teamSchema.parse(JSON.parse(JSON.stringify(withRotation("Mavuika E", true)))); expect(saved.rotation).toEqual({ enabled: true, text: "Mavuika E", repeat: true }); expect(teamSchema.parse({ ...team, rotation: {} }).rotation).toEqual({ enabled: false, text: "", repeat: false }); expect(team.rotation).toBeUndefined(); }); });