Add test coverage for 3 hooks: useEncounter, usePlayerCharacters, useSidePanelState
29 tests covering state transitions, persistence sync, domain error propagation, bestiary/PC add flows, and panel state machine logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
217
apps/web/src/hooks/__tests__/use-encounter.test.ts
Normal file
217
apps/web/src/hooks/__tests__/use-encounter.test.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
// @vitest-environment jsdom
|
||||
import type { BestiaryIndexEntry, PlayerCharacter } from "@initiative/domain";
|
||||
import { combatantId, creatureId, playerCharacterId } from "@initiative/domain";
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useEncounter } from "../use-encounter.js";
|
||||
|
||||
vi.mock("../../persistence/encounter-storage.js", () => ({
|
||||
loadEncounter: vi.fn().mockReturnValue(null),
|
||||
saveEncounter: vi.fn(),
|
||||
}));
|
||||
|
||||
const { loadEncounter: mockLoad, saveEncounter: mockSave } =
|
||||
await vi.importMock<typeof import("../../persistence/encounter-storage.js")>(
|
||||
"../../persistence/encounter-storage.js",
|
||||
);
|
||||
|
||||
describe("useEncounter", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockLoad.mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("initializes with empty encounter when persistence returns null", () => {
|
||||
const { result } = renderHook(() => useEncounter());
|
||||
|
||||
expect(result.current.encounter.combatants).toEqual([]);
|
||||
expect(result.current.encounter.activeIndex).toBe(0);
|
||||
expect(result.current.encounter.roundNumber).toBe(1);
|
||||
expect(result.current.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it("initializes from stored encounter", () => {
|
||||
const stored = {
|
||||
combatants: [{ id: combatantId("c-1"), name: "Goblin" }],
|
||||
activeIndex: 0,
|
||||
roundNumber: 2,
|
||||
};
|
||||
mockLoad.mockReturnValue(stored);
|
||||
|
||||
const { result } = renderHook(() => useEncounter());
|
||||
|
||||
expect(result.current.encounter.combatants).toHaveLength(1);
|
||||
expect(result.current.encounter.roundNumber).toBe(2);
|
||||
expect(result.current.isEmpty).toBe(false);
|
||||
});
|
||||
|
||||
it("addCombatant adds a combatant with incremental IDs and persists", () => {
|
||||
const { result } = renderHook(() => useEncounter());
|
||||
|
||||
act(() => result.current.addCombatant("Goblin"));
|
||||
act(() => result.current.addCombatant("Orc"));
|
||||
|
||||
expect(result.current.encounter.combatants).toHaveLength(2);
|
||||
expect(result.current.encounter.combatants[0].name).toBe("Goblin");
|
||||
expect(result.current.encounter.combatants[1].name).toBe("Orc");
|
||||
expect(result.current.isEmpty).toBe(false);
|
||||
expect(mockSave).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removeCombatant removes a combatant and persists", () => {
|
||||
const { result } = renderHook(() => useEncounter());
|
||||
|
||||
act(() => result.current.addCombatant("Goblin"));
|
||||
const id = result.current.encounter.combatants[0].id;
|
||||
|
||||
act(() => result.current.removeCombatant(id));
|
||||
|
||||
expect(result.current.encounter.combatants).toHaveLength(0);
|
||||
expect(result.current.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it("advanceTurn and retreatTurn update encounter state", () => {
|
||||
const { result } = renderHook(() => useEncounter());
|
||||
|
||||
act(() => result.current.addCombatant("Goblin"));
|
||||
act(() => result.current.addCombatant("Orc"));
|
||||
|
||||
const initialActive = result.current.encounter.activeIndex;
|
||||
|
||||
act(() => result.current.advanceTurn());
|
||||
expect(result.current.encounter.activeIndex).not.toBe(initialActive);
|
||||
|
||||
act(() => result.current.retreatTurn());
|
||||
expect(result.current.encounter.activeIndex).toBe(initialActive);
|
||||
});
|
||||
|
||||
it("clearEncounter resets to empty and resets ID counter", () => {
|
||||
const { result } = renderHook(() => useEncounter());
|
||||
|
||||
act(() => result.current.addCombatant("Goblin"));
|
||||
act(() => result.current.clearEncounter());
|
||||
|
||||
expect(result.current.encounter.combatants).toHaveLength(0);
|
||||
expect(result.current.isEmpty).toBe(true);
|
||||
|
||||
// After clear, IDs restart from c-1
|
||||
act(() => result.current.addCombatant("Orc"));
|
||||
expect(result.current.encounter.combatants[0].id).toBe("c-1");
|
||||
});
|
||||
|
||||
it("addCombatant with opts applies initiative, ac, maxHp", () => {
|
||||
const { result } = renderHook(() => useEncounter());
|
||||
|
||||
act(() =>
|
||||
result.current.addCombatant("Goblin", {
|
||||
initiative: 15,
|
||||
ac: 13,
|
||||
maxHp: 7,
|
||||
}),
|
||||
);
|
||||
|
||||
const goblin = result.current.encounter.combatants[0];
|
||||
expect(goblin.initiative).toBe(15);
|
||||
expect(goblin.ac).toBe(13);
|
||||
expect(goblin.maxHp).toBe(7);
|
||||
expect(goblin.currentHp).toBe(7);
|
||||
});
|
||||
|
||||
it("derived flags: hasCreatureCombatants and canRollAllInitiative", () => {
|
||||
const { result } = renderHook(() => useEncounter());
|
||||
|
||||
// No creatures yet
|
||||
expect(result.current.hasCreatureCombatants).toBe(false);
|
||||
expect(result.current.canRollAllInitiative).toBe(false);
|
||||
|
||||
// Add from bestiary to get a creature combatant
|
||||
const entry: BestiaryIndexEntry = {
|
||||
name: "Goblin",
|
||||
source: "MM",
|
||||
ac: 15,
|
||||
hp: 7,
|
||||
dex: 14,
|
||||
cr: "1/4",
|
||||
initiativeProficiency: 0,
|
||||
size: "Small",
|
||||
type: "humanoid",
|
||||
};
|
||||
|
||||
act(() => result.current.addFromBestiary(entry));
|
||||
|
||||
expect(result.current.hasCreatureCombatants).toBe(true);
|
||||
expect(result.current.canRollAllInitiative).toBe(true);
|
||||
});
|
||||
|
||||
it("addFromBestiary adds combatant with HP, AC, creatureId", () => {
|
||||
const { result } = renderHook(() => useEncounter());
|
||||
|
||||
const entry: BestiaryIndexEntry = {
|
||||
name: "Goblin",
|
||||
source: "MM",
|
||||
ac: 15,
|
||||
hp: 7,
|
||||
dex: 14,
|
||||
cr: "1/4",
|
||||
initiativeProficiency: 0,
|
||||
size: "Small",
|
||||
type: "humanoid",
|
||||
};
|
||||
|
||||
act(() => result.current.addFromBestiary(entry));
|
||||
|
||||
const combatant = result.current.encounter.combatants[0];
|
||||
expect(combatant.name).toBe("Goblin");
|
||||
expect(combatant.maxHp).toBe(7);
|
||||
expect(combatant.currentHp).toBe(7);
|
||||
expect(combatant.ac).toBe(15);
|
||||
expect(combatant.creatureId).toBe(creatureId("mm:goblin"));
|
||||
});
|
||||
|
||||
it("addFromBestiary auto-numbers duplicate names", () => {
|
||||
const { result } = renderHook(() => useEncounter());
|
||||
|
||||
const entry: BestiaryIndexEntry = {
|
||||
name: "Goblin",
|
||||
source: "MM",
|
||||
ac: 15,
|
||||
hp: 7,
|
||||
dex: 14,
|
||||
cr: "1/4",
|
||||
initiativeProficiency: 0,
|
||||
size: "Small",
|
||||
type: "humanoid",
|
||||
};
|
||||
|
||||
act(() => result.current.addFromBestiary(entry));
|
||||
act(() => result.current.addFromBestiary(entry));
|
||||
|
||||
const names = result.current.encounter.combatants.map((c) => c.name);
|
||||
expect(names).toContain("Goblin 1");
|
||||
expect(names).toContain("Goblin 2");
|
||||
});
|
||||
|
||||
it("addFromPlayerCharacter adds combatant with HP, AC, color, icon", () => {
|
||||
const { result } = renderHook(() => useEncounter());
|
||||
|
||||
const pc: PlayerCharacter = {
|
||||
id: playerCharacterId("pc-1"),
|
||||
name: "Aria",
|
||||
ac: 16,
|
||||
maxHp: 30,
|
||||
color: "blue",
|
||||
icon: "sword",
|
||||
};
|
||||
|
||||
act(() => result.current.addFromPlayerCharacter(pc));
|
||||
|
||||
const combatant = result.current.encounter.combatants[0];
|
||||
expect(combatant.name).toBe("Aria");
|
||||
expect(combatant.maxHp).toBe(30);
|
||||
expect(combatant.currentHp).toBe(30);
|
||||
expect(combatant.ac).toBe(16);
|
||||
expect(combatant.color).toBe("blue");
|
||||
expect(combatant.icon).toBe("sword");
|
||||
expect(combatant.playerCharacterId).toBe(playerCharacterId("pc-1"));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user