131 lines
4.7 KiB
TypeScript
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"]);
|
|
});
|
|
});
|