import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { useCombatStore } from "../game/combatStore"; import { activateDungeonEncounterDefinitions, resetDungeonEncounters } from "../game/dungeonEncounterRuntime"; import { requireDungeonDefinition } from "../game/dungeonRegistry"; import { usePartyStore } from "../game/partyStore"; import { useGameStore } from "../game/store"; import { PartyFrames } from "./PartyFrames"; // Exercise the client HUD against live stores rather than their initial SSR snapshots. vi.mock("../game/partyStore", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, usePartyStore: Object.assign( (select: (state: ReturnType) => unknown) => select(actual.usePartyStore.getState()), actual.usePartyStore, ) }; }); vi.mock("../game/combatStore", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useCombatStore: Object.assign( (select: (state: ReturnType) => unknown) => select(actual.useCombatStore.getState()), actual.useCombatStore, ) }; }); vi.mock("../game/store", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useGameStore: Object.assign( (select: (state: ReturnType) => unknown) => select(actual.useGameStore.getState()), actual.useGameStore, ) }; }); const dungeon = requireDungeonDefinition("stormwind-stockade"); const render = () => renderToStaticMarkup(createElement(PartyFrames)); function killMember() { const member = usePartyStore.getState().members[0]; useCombatStore.getState().commitCompanionActors([{ ...member, health: 0, status: "down" }]); } describe("party resurrection UI", () => { beforeEach(() => { useGameStore.getState().activateDungeon("stormwind-stockade"); activateDungeonEncounterDefinitions(dungeon.id, dungeon.encounters ?? []); resetDungeonEncounters("dungeon", dungeon.id); useCombatStore.getState().initializeCharacter({ classId: "priest", level: 16 }); useCombatStore.setState({ mobs: {} }); usePartyStore.getState().initializeParty("rez-ui", 16, useCombatStore.getState().health); useCombatStore.getState().synchronizeActors(); }); it("hides the control when all NPCs are alive, even if only the player is dead", () => { expect(render()).not.toContain("Rez dead members"); useCombatStore.getState().damagePlayer(useCombatStore.getState().maxHealth * 10); expect(render()).not.toContain("Rez dead members"); }); it("places the button and labeled destination selector below every party frame", () => { killMember(); const markup = render(); expect(markup).toContain("Rez dead members"); expect(markup).toContain("Resurrect at"); expect(markup).toContain("Dungeon entrance"); expect(markup.indexOf("party-resurrection")).toBeGreaterThan(markup.lastIndexOf("party-frame__identity")); expect(markup).not.toMatch(/class="party-rez-button" disabled/); }); it("shows why resurrection is disabled during a living boss encounter", () => { killMember(); useCombatStore.getState().registerMob("boss", { name: "Boss", boss: true, maxHealth: 100 }); useCombatStore.getState().damageMob("boss", 1, 100); expect(render()).toMatch(/class="party-rez-button" disabled=""/); expect(render()).toContain("Unavailable during boss combat"); }); it("offers killed boss destinations but not bosses that are still alive", () => { killMember(); const boss = dungeon.bosses[0]; expect(render()).not.toContain(`value="boss:${boss.id}"`); useCombatStore.getState().registerMob(boss.id, { name: boss.name, boss: true, maxHealth: 1 }); useCombatStore.getState().damageMob(boss.id, 1, 100); expect(render()).toContain(`value="boss:${boss.id}"`); }); it("does not offer free resurrection in Manastorm", () => { killMember(); useGameStore.setState({ gameMode: "manastorm" }); expect(render()).not.toContain("Rez dead members"); }); });