import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it, vi } from "vitest"; import { SpellbookPanel, spellbookAbilitiesForDisplay, type SpellbookAbilityView, } from "./SpellbookPanel"; const barState = vi.hoisted(() => ({ pcIds: [] as (string | null)[], controllerIds: [] as string[], wowBars: true })); vi.mock("../game/uiLayoutStore", async (importOriginal) => { const actual = await importOriginal(); const { normalizeUiPreferences } = await import("../game/uiLayout"); return { ...actual, currentUiDevice: () => "pc", useUiLayoutStore: () => { const preferences = normalizeUiPreferences(null); preferences.current.pc.wowBars = barState.wowBars; preferences.current.pc.bars[0].slots.forEach((slot, index) => { slot.abilityId = barState.pcIds[index] ?? null; }); return { preferences, draft: null }; } }; }); vi.mock("../game/combatStore", () => ({ useCombatStore: (selector: (state: unknown) => unknown) => selector({ actionBindings: Object.fromEntries(barState.controllerIds.map((id, index) => [`layer:${index}`, id])), }) })); const abilities: readonly SpellbookAbilityView[] = [ { id: "known-one", name: "Known One", description: "A starting ability.", level: 1, color: "#ffffff", sigil: "K1", known: true, }, { id: "known-two", name: "Known Two", description: "Another starting ability.", level: 1, color: "#ffffff", sigil: "K2", known: true, }, { id: "future-two", name: "Future Two", description: "The next unlock.", level: 2, color: "#ffffff", sigil: "F2", known: true, }, { id: "future-four", name: "Future Four", description: "A later unlock.", level: 4, color: "#ffffff", sigil: "F4", known: true, }, ]; describe("spellbook ability progression", () => { it("highlights only usable spells missing from the active layout and updates after assignment", () => { barState.wowBars = true; barState.pcIds = [null, null, null, null, "known-one"]; barState.controllerIds = ["known-two"]; try { const render = () => renderToStaticMarkup(createElement(SpellbookPanel, { abilities: [...abilities, { ...abilities[1], id: "passive", passive: true }], playerLevel: 1, onActivate: () => undefined, open: true, onClose: () => undefined, })); let markup = render(); expect(markup).toContain("Known Two, unlocked, missing from action bars"); expect(markup).not.toContain("Known One, unlocked, missing from action bars"); expect(markup.match(/is-missing-from-bars/g)).toHaveLength(1); barState.pcIds[11] = "known-two"; markup = render(); expect(markup).not.toContain("is-missing-from-bars"); barState.wowBars = false; markup = render(); expect(markup).toContain("Known One, unlocked, missing from action bars"); expect(markup).not.toContain("Known Two, unlocked, missing from action bars"); expect(markup.match(/is-missing-from-bars/g)).toHaveLength(1); } finally { barState.pcIds = []; barState.controllerIds = []; barState.wowBars = true; } }); it("renders future abilities as inspectable locked buttons with level requirements", () => { const markup = renderToStaticMarkup(createElement(SpellbookPanel, { abilities, playerLevel: 1, onActivate: () => undefined, onClose: () => undefined, open: true, })); expect(markup).toContain("2 unlocked abilities and 2 locked abilities"); expect(markup).toContain("Future Two, unlocks at level 2"); expect(markup).toContain("Future Four, unlocks at level 4"); expect(markup).toContain("spellbook-ability--locked"); expect(markup).toContain("aria-disabled=\"true\""); expect(markup).toContain("Unlocks at level 2"); expect(markup).toContain("Unlocks at level 4"); }); it("can limit the list to unlocked abilities", () => { expect(spellbookAbilitiesForDisplay(abilities, 1, true).map((ability) => ability.id)) .toEqual(["known-one", "known-two"]); const markup = renderToStaticMarkup(createElement(SpellbookPanel, { abilities, playerLevel: 1, onActivate: () => undefined, onClose: () => undefined, open: true, })); expect(markup).toContain("Unlocked only"); expect(markup).toContain("aria-pressed=\"false\""); }); it("sorts passive abilities after active abilities", () => { const sorted = spellbookAbilitiesForDisplay([ { ...abilities[0], id: "passive", name: "Passive", passive: true }, { ...abilities[2], id: "active", name: "Active" }, ], 1, false); expect(sorted.map((ability) => ability.id)).toEqual(["active", "passive"]); }); });