Files
healer-man/src/ui/spellbookProgression.test.ts
T
2026-09-14 22:12:49 -04:00

131 lines
4.7 KiB
TypeScript

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<typeof import("../game/uiLayoutStore")>();
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"]);
});
});