import { describe, expect, it } from "vitest"; import { actionKeyForEvent, assignDesktopAbility, bindDesktopSlot, defaultUiLayout, desktopSlotForKey, exportUiPreferences, importUiPreferences, normalizeActionKey, normalizeUiPreferences, resetDesktopAbilities, resolveDpsMeterDisplay, HUD_ELEMENTS, resolveHudScreen, activateDesktopAbilityLoadout, assignedActionBarAbilityIds, } from "./uiLayout"; describe("personal UI layouts", () => { it("populates fresh PC bars without changing shared keys, geometry, templates, or mobile controls", () => { const ui = normalizeUiPreferences(null); ui.current.pc.wowBars = true; ui.current.pc.barCount = 2; ui.current.pc.bars[1].columns = 3; ui.current.pc = bindDesktopSlot(ui.current.pc, 1, 0, "Ctrl+KeyQ"); ui.slots.pc[0] = { name: "Raid", layout: ui.current.pc }; const defaults = Array.from({ length: 14 }, (_, i) => `spell-${i}`); const activated = activateDesktopAbilityLoadout(ui, "new:priest:", defaults, defaults); expect(activated.current.pc.bars[0].slots[0]).toEqual({ abilityId: "spell-0", key: "Digit1" }); expect(activated.current.pc.bars[1].slots[0]).toEqual({ abilityId: "spell-12", key: "Ctrl+KeyQ" }); expect(activated.current.pc.bars[1].columns).toBe(3); expect(activated.current.pc.barCount).toBe(2); expect(activated.current.pc.wowBars).toBe(true); expect(activated.current.mobile).toEqual(ui.current.mobile); expect(activated.slots).toEqual(ui.slots); expect(ui.current.pc.bars[0].slots[0].abilityId).toBeNull(); }); it("restores separate character and ordered-class loadouts, including intentionally empty bars", () => { let ui = activateDesktopAbilityLoadout(normalizeUiPreferences(null), "one:priest:", ["heal"], ["heal"]); ui.desktopAbilityLoadouts["one:priest:"] = Array(60).fill(null); ui = activateDesktopAbilityLoadout(ui, "two:priest:", ["heal"], ["heal"]); expect(ui.current.pc.bars[0].slots[0].abilityId).toBe("heal"); ui = activateDesktopAbilityLoadout(ui, "one:priest:", ["heal"], ["heal"]); expect(ui.current.pc.bars.every((bar) => bar.slots.every((slot) => slot.abilityId === null))).toBe(true); ui = activateDesktopAbilityLoadout(ui, "one:rom-priest:rom-warrior", ["secondary-attack"], ["secondary-attack"]); expect(ui.current.pc.bars[0].slots[0].abilityId).toBe("secondary-attack"); expect(importUiPreferences(exportUiPreferences(ui))).toEqual(ui); }); it("adopts compatible legacy assignments once and repairs bars holding only another class's spells", () => { const ui = normalizeUiPreferences(null); ui.current.pc = assignDesktopAbility(ui.current.pc, 0, 2, "custom-heal"); const migrated = activateDesktopAbilityLoadout(ui, "one:priest:", ["heal"], ["heal", "custom-heal"]); expect(migrated.current.pc.bars[0].slots[0].abilityId).toBeNull(); expect(migrated.current.pc.bars[0].slots[2].abilityId).toBe("custom-heal"); const next = activateDesktopAbilityLoadout(migrated, "two:priest:", ["heal"], ["heal", "custom-heal"]); expect(next.current.pc.bars[0].slots[0].abilityId).toBe("heal"); expect(next.current.pc.bars[0].slots[2].abilityId).toBeNull(); const repaired = activateDesktopAbilityLoadout(ui, "new:mage:", ["fireball"], ["fireball"]); expect(repaired.current.pc.bars[0].slots[0].abilityId).toBe("fireball"); const oldExport = JSON.parse(exportUiPreferences(ui)); delete oldExport.desktopAbilityLoadouts; expect(importUiPreferences(JSON.stringify(oldExport)).desktopAbilityLoadouts).toEqual({}); }); it("checks the selected layout across modifier layers and active PC bars", () => { const layout = defaultUiLayout(); layout.bars[0].slots[0].abilityId = "pc-heal"; layout.bars[1].slots[0].abilityId = "inactive-heal"; const bindings = { "primary:face-bottom": "controller-heal", "tertiary:r2": "modifier-heal" }; expect([...assignedActionBarAbilityIds(layout, bindings, false)]).toEqual(["controller-heal", "modifier-heal"]); expect([...assignedActionBarAbilityIds(layout, bindings, true)]).toEqual(["pc-heal"]); layout.barCount = 2; expect(assignedActionBarAbilityIds(layout, bindings, true).has("inactive-heal")).toBe(true); }); it("assigns every element one display, saves routing, and keeps single-display widgets available", () => { const preferences = normalizeUiPreferences(null); const layout = preferences.current.mobile; expect(resolveHudScreen("party", layout, true)).toBe("top"); for (const id of ["actions", "player", "cast", "experience"] as const) expect(resolveHudScreen(id, layout, true)).toBe("bottom"); for (const id of Object.keys(HUD_ELEMENTS) as (keyof typeof HUD_ELEMENTS)[]) { layout.elements[id].screen = "bottom"; expect(resolveHudScreen(id, layout, true)).toBe("bottom"); expect(resolveHudScreen(id, layout, false)).toBe("top"); if (id === "dps") { expect(resolveDpsMeterDisplay(layout, false)).toBe("main"); expect(resolveDpsMeterDisplay(layout, true)).toBe("companion"); } layout.elements[id].screen = "top"; expect(resolveHudScreen(id, layout, true)).toBe("top"); layout.elements[id].screen = "hidden"; expect(resolveHudScreen(id, layout, true)).toBe("hidden"); } expect(importUiPreferences(exportUiPreferences(preferences))).toEqual(preferences); }); it("saves and clears abilities on inactive WoW bars without changing keys, templates, or mobile controls", () => { const ui = normalizeUiPreferences(null); ui.slots.pc[0] = { name: "Original", layout: ui.current.pc }; const original = exportUiPreferences(ui); ui.current.pc = assignDesktopAbility(ui.current.pc, 4, 11, "test-heal"); const saved = importUiPreferences(exportUiPreferences(ui)); expect(saved.current.pc.bars[4].slots[11]).toEqual({ abilityId: "test-heal", key: null }); expect(saved.current.pc.bars[0]).toEqual(JSON.parse(original).current.pc.bars[0]); expect(saved.current.mobile).toEqual(ui.current.mobile); expect(saved.slots.pc[0]?.layout.bars[4].slots[11].abilityId).toBeNull(); expect(assignDesktopAbility(saved.current.pc, 4, 11, null).bars[4].slots[11].abilityId).toBeNull(); }); it("restores one or all WoW ability bars with the correct offset and retains custom keys and geometry", () => { let layout = defaultUiLayout(); layout.wowBars = true; layout.barCount = 2; layout.bars[1].columns = 3; layout = bindDesktopSlot(layout, 1, 0, "Ctrl+KeyQ"); layout = assignDesktopAbility(assignDesktopAbility(layout, 0, 0, "custom-one"), 1, 0, "custom-two"); const defaults = Array.from({ length: 14 }, (_, i) => `spell-${i}`); const bar = resetDesktopAbilities(layout, defaults, 1); expect(bar.bars[0]).toEqual(layout.bars[0]); expect(bar.bars[1].slots[0]).toEqual({ abilityId: "spell-12", key: "Ctrl+KeyQ" }); expect(bar.bars[1].slots[2].abilityId).toBeNull(); const all = resetDesktopAbilities(bar, defaults); expect(all.bars[0].slots[0]).toEqual({ abilityId: "spell-0", key: "Digit1" }); expect(all.bars[1].columns).toBe(3); expect(all.wowBars).toBe(true); expect(all.barCount).toBe(2); }); it("defaults DPS to PC lower-left flow and the Thor companion, with saved overrides", () => { const ui = normalizeUiPreferences(null); expect(ui.current.pc.elements.dps).toEqual({ visible: true, position: null, width: null }); expect(resolveDpsMeterDisplay(ui.current.pc, false)).toBe("main"); expect(resolveDpsMeterDisplay(ui.current.mobile, true)).toBe("companion"); expect(resolveDpsMeterDisplay(ui.current.pc, true)).toBe("companion"); ui.current.mobile.dpsMeterDisplay = "main"; ui.current.mobile.elements.dps = { visible: false, position: { x: .6, y: .7 }, width: 300 }; expect(importUiPreferences(exportUiPreferences(ui))).toEqual(ui); expect(resolveDpsMeterDisplay(ui.current.mobile, true)).toBe("main"); }); it("imports older UI templates without a DPS meter", () => { const exported = JSON.parse(exportUiPreferences(normalizeUiPreferences(null))); for (const device of ["pc", "mobile"]) { delete exported.current[device].elements.dps; delete exported.current[device].dpsMeterDisplay; } expect(importUiPreferences(JSON.stringify(exported)).current.pc.elements.dps.visible).toBe(true); exported.current.pc.dpsMeterDisplay = "invalid"; expect(() => importUiPreferences(JSON.stringify(exported))).toThrow("DPS meter display"); }); it("migrates old accounts to independent Mobile and PC layouts with exactly six empty slots", () => { const ui = normalizeUiPreferences(undefined); expect(ui.slots.mobile).toEqual([null, null, null]); expect(ui.slots.pc).toEqual([null, null, null]); ui.current.pc.elements.party.visible = false; expect(ui.current.mobile.elements.party.visible).toBe(true); expect(ui.current.pc.bars).toHaveLength(5); expect(ui.current.pc.bars.every((bar) => bar.slots.length === 12)).toBe(true); }); it("round trips named slots, positions, hidden elements, geometry, assignments, and key chords", () => { const ui = normalizeUiPreferences(null); const layout = ui.current.pc; layout.wowBars = true; layout.barCount = 5; layout.elements.party = { visible: false, position: { x: .32, y: .45 }, width: 200 }; layout.bars[4].columns = 3; layout.bars[4].slots[11] = { abilityId: "test-heal", key: "Ctrl+Shift+KeyQ" }; ui.slots.pc[2] = { name: "Raid night", layout }; ui.slots.mobile[0] = { name: "Travel", layout: defaultUiLayout() }; expect(importUiPreferences(exportUiPreferences(ui))).toEqual(ui); }); it("reassigns conflicts even from hidden bars, accepts modifiers and clears a binding", () => { let layout = defaultUiLayout(); layout.wowBars = true; layout.barCount = 5; layout = bindDesktopSlot(layout, 4, 11, "Digit1"); expect(layout.bars[0].slots[0].key).toBeNull(); expect(desktopSlotForKey(layout, "Digit1")).toBe(layout.bars[4].slots[11]); layout.elements["bar-5"].visible = false; expect(desktopSlotForKey(layout, "Digit1")).not.toBeNull(); layout.barCount = 4; expect(desktopSlotForKey(layout, "Digit1")).toBeNull(); layout = bindDesktopSlot(layout, 4, 11, null); expect(layout.bars[4].slots[11].key).toBeNull(); expect(actionKeyForEvent({ code: "KeyQ", ctrlKey: true, shiftKey: true, altKey: false, metaKey: false })).toBe("Ctrl+Shift+KeyQ"); expect(normalizeActionKey("Escape")).toBe("Escape"); expect(normalizeActionKey("ShiftLeft")).toBeNull(); }); it.each([ ["unsupported versions", (data: any) => { data.version = 99; }], ["extra slots", (data: any) => { data.slots.mobile.push(null); }], ["invalid coordinates", (data: any) => { data.current.pc.elements.party.position = { x: "bad", y: 0 }; }], ["invalid bar geometry", (data: any) => { data.current.pc.bars[0].columns = 7; }], ["invalid keys", (data: any) => { data.current.pc.bars[0].slots[0].key = "NotAKey"; }], ["duplicate chords", (data: any) => { data.current.pc.bars[0].slots[1].key = "Digit1"; }], ["missing buttons", (data: any) => { data.current.pc.bars[0].slots.pop(); }], ["invalid character bars", (data: any) => { data.desktopAbilityLoadouts = { character: ["heal"] }; }], ])("rejects %s without mutating the original preferences", (_, mutate) => { const ui = normalizeUiPreferences(null); const original = exportUiPreferences(ui); const data = JSON.parse(original); mutate(data); expect(() => importUiPreferences(JSON.stringify(data))).toThrow(); expect(exportUiPreferences(ui)).toBe(original); }); it("rejects malformed and oversized imports", () => { expect(() => importUiPreferences("{")).toThrow("valid JSON"); expect(() => importUiPreferences(" ".repeat(256001))).toThrow("256 KB"); }); });