From 5a262c66cdbd94bbad34dbdcc0a07f4dcc824af6 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 14 Mar 2026 15:31:35 +0100 Subject: [PATCH] Add test coverage for all 17 application layer use cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests verify the get→call→save wiring and error propagation for each use case. The 15 formulaic use cases share a test file; rollInitiative and rollAllInitiative have dedicated suites covering their multi-step logic (creature lookup, modifier calculation, iteration, early return). Co-Authored-By: Claude Opus 4.6 --- packages/application/src/__tests__/helpers.ts | 54 +++ .../roll-all-initiative-use-case.test.ts | 195 +++++++++ .../roll-initiative-use-case.test.ts | 155 +++++++ .../src/__tests__/use-cases.test.ts | 388 ++++++++++++++++++ 4 files changed, 792 insertions(+) create mode 100644 packages/application/src/__tests__/helpers.ts create mode 100644 packages/application/src/__tests__/roll-all-initiative-use-case.test.ts create mode 100644 packages/application/src/__tests__/roll-initiative-use-case.test.ts create mode 100644 packages/application/src/__tests__/use-cases.test.ts diff --git a/packages/application/src/__tests__/helpers.ts b/packages/application/src/__tests__/helpers.ts new file mode 100644 index 0000000..2ea5ed6 --- /dev/null +++ b/packages/application/src/__tests__/helpers.ts @@ -0,0 +1,54 @@ +import type { Encounter, PlayerCharacter } from "@initiative/domain"; +import { isDomainError } from "@initiative/domain"; +import type { EncounterStore, PlayerCharacterStore } from "../ports.js"; + +export function requireSaved(value: T | null): T { + if (value === null) throw new Error("Expected store.saved to be non-null"); + return value; +} + +export function expectSuccess( + result: T, +): asserts result is Exclude { + if (isDomainError(result)) { + throw new Error(`Expected success, got domain error: ${result.message}`); + } +} + +export function expectError(result: unknown): asserts result is { + kind: "domain-error"; + code: string; + message: string; +} { + if (!isDomainError(result)) { + throw new Error("Expected domain error"); + } +} + +export function stubEncounterStore( + initial: Encounter, +): EncounterStore & { saved: Encounter | null } { + const stub = { + saved: null as Encounter | null, + get: () => initial, + save: (e: Encounter) => { + stub.saved = e; + stub.get = () => e; + }, + }; + return stub; +} + +export function stubPlayerCharacterStore( + initial: readonly PlayerCharacter[], +): PlayerCharacterStore & { saved: readonly PlayerCharacter[] | null } { + const stub = { + saved: null as readonly PlayerCharacter[] | null, + getAll: () => [...initial], + save: (characters: PlayerCharacter[]) => { + stub.saved = characters; + stub.getAll = () => [...characters]; + }, + }; + return stub; +} diff --git a/packages/application/src/__tests__/roll-all-initiative-use-case.test.ts b/packages/application/src/__tests__/roll-all-initiative-use-case.test.ts new file mode 100644 index 0000000..cd49b89 --- /dev/null +++ b/packages/application/src/__tests__/roll-all-initiative-use-case.test.ts @@ -0,0 +1,195 @@ +import { + type Creature, + combatantId, + createEncounter, + creatureId, + isDomainError, +} from "@initiative/domain"; +import { describe, expect, it } from "vitest"; +import { rollAllInitiativeUseCase } from "../roll-all-initiative-use-case.js"; +import { + expectError, + expectSuccess, + requireSaved, + stubEncounterStore, +} from "./helpers.js"; + +const CREATURE_A = creatureId("creature-a"); +const CREATURE_B = creatureId("creature-b"); + +function makeCreature(id: string, dex = 14): Creature { + return { + id: creatureId(id), + name: `Creature ${id}`, + source: "mm", + sourceDisplayName: "Monster Manual", + size: "Medium", + type: "humanoid", + alignment: "neutral", + ac: 12, + hp: { average: 10, formula: "2d8+2" }, + speed: "30 ft.", + abilities: { str: 10, dex, con: 10, int: 10, wis: 10, cha: 10 }, + cr: "1", + initiativeProficiency: 0, + proficiencyBonus: 2, + passive: 10, + }; +} + +function encounterWithCombatants( + combatants: Array<{ + name: string; + creatureId?: string; + initiative?: number; + }>, +) { + const result = createEncounter( + combatants.map((c) => ({ + id: combatantId(c.name), + name: c.name, + creatureId: c.creatureId ? creatureId(c.creatureId) : undefined, + initiative: c.initiative, + })), + ); + if (isDomainError(result)) throw new Error("Setup failed"); + return result; +} + +describe("rollAllInitiativeUseCase", () => { + it("skips combatants without creatureId", () => { + const enc = encounterWithCombatants([ + { name: "Fighter" }, + { name: "Goblin", creatureId: "creature-a" }, + ]); + const store = stubEncounterStore(enc); + const creature = makeCreature("creature-a"); + + const result = rollAllInitiativeUseCase( + store, + () => 10, + (id) => (id === CREATURE_A ? creature : undefined), + ); + + expectSuccess(result); + expect(result.events.length).toBeGreaterThan(0); + const saved = requireSaved(store.saved); + const fighter = saved.combatants.find((c) => c.name === "Fighter"); + const goblin = saved.combatants.find((c) => c.name === "Goblin"); + expect(fighter?.initiative).toBeUndefined(); + expect(goblin?.initiative).toBeDefined(); + }); + + it("skips combatants that already have initiative", () => { + const enc = encounterWithCombatants([ + { name: "Goblin", creatureId: "creature-a", initiative: 15 }, + ]); + const store = stubEncounterStore(enc); + + const result = rollAllInitiativeUseCase( + store, + () => 10, + () => makeCreature("creature-a"), + ); + + expectSuccess(result); + expect(result.events).toHaveLength(0); + expect(requireSaved(store.saved).combatants[0].initiative).toBe(15); + }); + + it("counts skippedNoSource when creature lookup returns undefined", () => { + const enc = encounterWithCombatants([ + { name: "Unknown", creatureId: "missing" }, + ]); + const store = stubEncounterStore(enc); + + const result = rollAllInitiativeUseCase( + store, + () => 10, + () => undefined, + ); + + expectSuccess(result); + expect(result.skippedNoSource).toBe(1); + expect(result.events).toHaveLength(0); + }); + + it("accumulates events from multiple setInitiative calls", () => { + const enc = encounterWithCombatants([ + { name: "A", creatureId: "creature-a" }, + { name: "B", creatureId: "creature-b" }, + ]); + const store = stubEncounterStore(enc); + const creatureA = makeCreature("creature-a"); + const creatureB = makeCreature("creature-b"); + + const result = rollAllInitiativeUseCase( + store, + () => 10, + (id) => { + if (id === CREATURE_A) return creatureA; + if (id === CREATURE_B) return creatureB; + return undefined; + }, + ); + + expectSuccess(result); + expect(result.events).toHaveLength(2); + }); + + it("returns early with domain error on invalid dice roll", () => { + const enc = encounterWithCombatants([ + { name: "A", creatureId: "creature-a" }, + { name: "B", creatureId: "creature-b" }, + ]); + const store = stubEncounterStore(enc); + + // rollDice returns 0 (invalid — must be 1–20), triggers early return + const result = rollAllInitiativeUseCase( + store, + () => 0, + (id) => { + if (id === CREATURE_A) return makeCreature("creature-a"); + if (id === CREATURE_B) return makeCreature("creature-b"); + return undefined; + }, + ); + + expectError(result); + expect(result.code).toBe("invalid-dice-roll"); + // Store should NOT have been saved since the loop aborted + expect(store.saved).toBeNull(); + }); + + it("saves encounter once at the end", () => { + const enc = encounterWithCombatants([ + { name: "A", creatureId: "creature-a" }, + { name: "B", creatureId: "creature-b" }, + ]); + const store = stubEncounterStore(enc); + const creatureA = makeCreature("creature-a"); + const creatureB = makeCreature("creature-b"); + + let saveCount = 0; + const originalSave = store.save.bind(store); + store.save = (e) => { + saveCount++; + originalSave(e); + }; + + rollAllInitiativeUseCase( + store, + () => 10, + (id) => { + if (id === CREATURE_A) return creatureA; + if (id === CREATURE_B) return creatureB; + return undefined; + }, + ); + + expect(saveCount).toBe(1); + const saved = requireSaved(store.saved); + expect(saved.combatants[0].initiative).toBeDefined(); + expect(saved.combatants[1].initiative).toBeDefined(); + }); +}); diff --git a/packages/application/src/__tests__/roll-initiative-use-case.test.ts b/packages/application/src/__tests__/roll-initiative-use-case.test.ts new file mode 100644 index 0000000..5a27a0d --- /dev/null +++ b/packages/application/src/__tests__/roll-initiative-use-case.test.ts @@ -0,0 +1,155 @@ +import { + type Creature, + type CreatureId, + combatantId, + createEncounter, + creatureId, + isDomainError, +} from "@initiative/domain"; +import { describe, expect, it } from "vitest"; +import { addCombatantUseCase } from "../add-combatant-use-case.js"; +import { rollInitiativeUseCase } from "../roll-initiative-use-case.js"; +import { expectError, requireSaved, stubEncounterStore } from "./helpers.js"; + +const GOBLIN_ID = creatureId("goblin"); + +function makeCreature(overrides?: Partial): Creature { + return { + id: GOBLIN_ID, + name: "Goblin", + source: "mm", + sourceDisplayName: "Monster Manual", + size: "Small", + type: "humanoid", + alignment: "neutral evil", + ac: 15, + hp: { average: 7, formula: "2d6" }, + speed: "30 ft.", + abilities: { str: 8, dex: 14, con: 10, int: 10, wis: 8, cha: 8 }, + cr: "1/4", + initiativeProficiency: 0, + proficiencyBonus: 2, + passive: 9, + ...overrides, + }; +} + +function encounterWithCreatureLink(name: string, creature: CreatureId) { + const enc = createEncounter([]); + if (isDomainError(enc)) throw new Error("Setup failed"); + const id = combatantId(name); + const store = stubEncounterStore(enc); + addCombatantUseCase(store, id, name); + const saved = requireSaved(store.saved); + const result = createEncounter( + saved.combatants.map((c) => + c.id === id ? { ...c, creatureId: creature } : c, + ), + saved.activeIndex, + saved.roundNumber, + ); + if (isDomainError(result)) throw new Error("Setup failed"); + return result; +} + +describe("rollInitiativeUseCase", () => { + it("returns domain error when combatant not found", () => { + const enc = createEncounter([]); + if (isDomainError(enc)) throw new Error("Setup failed"); + const store = stubEncounterStore(enc); + + const result = rollInitiativeUseCase( + store, + combatantId("unknown"), + 10, + () => undefined, + ); + + expectError(result); + expect(result.code).toBe("combatant-not-found"); + expect(store.saved).toBeNull(); + }); + + it("returns domain error when combatant has no creature link", () => { + const enc = createEncounter([]); + if (isDomainError(enc)) throw new Error("Setup failed"); + const store1 = stubEncounterStore(enc); + addCombatantUseCase(store1, combatantId("Fighter"), "Fighter"); + + const store = stubEncounterStore(requireSaved(store1.saved)); + const result = rollInitiativeUseCase( + store, + combatantId("Fighter"), + 10, + () => undefined, + ); + + expectError(result); + expect(result.code).toBe("no-creature-link"); + expect(store.saved).toBeNull(); + }); + + it("returns domain error when creature not found in getter", () => { + const enc = encounterWithCreatureLink("Goblin", GOBLIN_ID); + const store = stubEncounterStore(enc); + + const result = rollInitiativeUseCase( + store, + combatantId("Goblin"), + 10, + () => undefined, + ); + + expectError(result); + expect(result.code).toBe("creature-not-found"); + expect(store.saved).toBeNull(); + }); + + it("calculates initiative from creature and saves", () => { + const creature = makeCreature(); + const enc = encounterWithCreatureLink("Goblin", GOBLIN_ID); + const store = stubEncounterStore(enc); + + // Dex 14 -> modifier +2, CR 1/4 -> PB 2, initiativeProficiency 0 + // So initiative modifier = 2 + 0*2 = 2 + // Roll 10 + modifier 2 = 12 + const result = rollInitiativeUseCase( + store, + combatantId("Goblin"), + 10, + (id) => (id === GOBLIN_ID ? creature : undefined), + ); + + expect(isDomainError(result)).toBe(false); + expect(requireSaved(store.saved).combatants[0].initiative).toBe(12); + }); + + it("applies initiative proficiency bonus correctly", () => { + // CR 5 -> PB 3, dex 16 -> mod +3, initiativeProficiency 1 + // modifier = 3 + 1*3 = 6, roll 8 + 6 = 14 + const creature = makeCreature({ + abilities: { + str: 10, + dex: 16, + con: 10, + int: 10, + wis: 10, + cha: 10, + }, + cr: "5", + initiativeProficiency: 1, + }); + const enc = encounterWithCreatureLink("Monster", GOBLIN_ID); + const store = stubEncounterStore(enc); + + const result = rollInitiativeUseCase( + store, + combatantId("Monster"), + 8, + (id) => (id === GOBLIN_ID ? creature : undefined), + ); + + expect(isDomainError(result)).toBe(false); + expect(requireSaved(store.saved).combatants[0].initiative).toBe(14); + }); +}); diff --git a/packages/application/src/__tests__/use-cases.test.ts b/packages/application/src/__tests__/use-cases.test.ts new file mode 100644 index 0000000..aff7e88 --- /dev/null +++ b/packages/application/src/__tests__/use-cases.test.ts @@ -0,0 +1,388 @@ +import { + type ConditionId, + combatantId, + createEncounter, + isDomainError, + playerCharacterId, +} from "@initiative/domain"; +import { describe, expect, it } from "vitest"; +import { addCombatantUseCase } from "../add-combatant-use-case.js"; +import { adjustHpUseCase } from "../adjust-hp-use-case.js"; +import { advanceTurnUseCase } from "../advance-turn-use-case.js"; +import { clearEncounterUseCase } from "../clear-encounter-use-case.js"; +import { createPlayerCharacterUseCase } from "../create-player-character-use-case.js"; +import { deletePlayerCharacterUseCase } from "../delete-player-character-use-case.js"; +import { editCombatantUseCase } from "../edit-combatant-use-case.js"; +import { editPlayerCharacterUseCase } from "../edit-player-character-use-case.js"; +import { removeCombatantUseCase } from "../remove-combatant-use-case.js"; +import { retreatTurnUseCase } from "../retreat-turn-use-case.js"; +import { setAcUseCase } from "../set-ac-use-case.js"; +import { setHpUseCase } from "../set-hp-use-case.js"; +import { setInitiativeUseCase } from "../set-initiative-use-case.js"; +import { toggleConcentrationUseCase } from "../toggle-concentration-use-case.js"; +import { toggleConditionUseCase } from "../toggle-condition-use-case.js"; +import { + requireSaved, + stubEncounterStore, + stubPlayerCharacterStore, +} from "./helpers.js"; + +const ID_A = combatantId("a"); + +function emptyEncounter() { + const result = createEncounter([]); + if (isDomainError(result)) throw new Error("Test setup failed"); + return result; +} + +function encounterWith(...names: string[]) { + let enc = emptyEncounter(); + for (const name of names) { + const id = combatantId(name); + const store = stubEncounterStore(enc); + const result = addCombatantUseCase(store, id, name); + if (isDomainError(result)) throw new Error(`Setup failed: ${name}`); + enc = requireSaved(store.saved); + } + return enc; +} + +function encounterWithHp(name: string, maxHp: number) { + const enc = encounterWith(name); + const store = stubEncounterStore(enc); + const id = combatantId(name); + setHpUseCase(store, id, maxHp); + return requireSaved(store.saved); +} + +function createPc(name: string) { + const store = stubPlayerCharacterStore([]); + const id = playerCharacterId("pc-1"); + createPlayerCharacterUseCase(store, id, name, 15, 40, undefined, undefined); + return { id, characters: requireSaved(store.saved) }; +} + +describe("addCombatantUseCase", () => { + it("adds a combatant and saves", () => { + const store = stubEncounterStore(emptyEncounter()); + const result = addCombatantUseCase(store, ID_A, "Goblin"); + + expect(isDomainError(result)).toBe(false); + const saved = requireSaved(store.saved); + expect(saved.combatants).toHaveLength(1); + expect(saved.combatants[0].name).toBe("Goblin"); + }); + + it("returns domain error for empty name", () => { + const store = stubEncounterStore(emptyEncounter()); + const result = addCombatantUseCase(store, ID_A, ""); + + expect(isDomainError(result)).toBe(true); + expect(store.saved).toBeNull(); + }); +}); + +describe("adjustHpUseCase", () => { + it("adjusts HP and saves", () => { + const enc = encounterWithHp("Goblin", 10); + const store = stubEncounterStore(enc); + const result = adjustHpUseCase(store, combatantId("Goblin"), -3); + + expect(isDomainError(result)).toBe(false); + const saved = requireSaved(store.saved); + expect(saved.combatants[0].currentHp).toBe(7); + }); + + it("returns domain error for unknown combatant", () => { + const store = stubEncounterStore(emptyEncounter()); + const result = adjustHpUseCase(store, ID_A, -5); + + expect(isDomainError(result)).toBe(true); + expect(store.saved).toBeNull(); + }); +}); + +describe("advanceTurnUseCase", () => { + it("advances turn and saves", () => { + const enc = encounterWith("A", "B"); + const store = stubEncounterStore(enc); + const result = advanceTurnUseCase(store); + + expect(isDomainError(result)).toBe(false); + const saved = requireSaved(store.saved); + expect(saved.activeIndex).toBe(1); + }); + + it("returns domain error on empty encounter", () => { + const store = stubEncounterStore(emptyEncounter()); + const result = advanceTurnUseCase(store); + + expect(isDomainError(result)).toBe(true); + expect(store.saved).toBeNull(); + }); +}); + +describe("clearEncounterUseCase", () => { + it("clears encounter and saves", () => { + const enc = encounterWith("Goblin"); + const store = stubEncounterStore(enc); + const result = clearEncounterUseCase(store); + + expect(isDomainError(result)).toBe(false); + const saved = requireSaved(store.saved); + expect(saved.combatants).toHaveLength(0); + }); +}); + +describe("editCombatantUseCase", () => { + it("edits combatant name and saves", () => { + const enc = encounterWith("Goblin"); + const store = stubEncounterStore(enc); + const result = editCombatantUseCase( + store, + combatantId("Goblin"), + "Hobgoblin", + ); + + expect(isDomainError(result)).toBe(false); + const saved = requireSaved(store.saved); + expect(saved.combatants[0].name).toBe("Hobgoblin"); + }); + + it("returns domain error for unknown combatant", () => { + const store = stubEncounterStore(emptyEncounter()); + const result = editCombatantUseCase(store, ID_A, "X"); + + expect(isDomainError(result)).toBe(true); + expect(store.saved).toBeNull(); + }); +}); + +describe("removeCombatantUseCase", () => { + it("removes combatant and saves", () => { + const enc = encounterWith("Goblin"); + const store = stubEncounterStore(enc); + const result = removeCombatantUseCase(store, combatantId("Goblin")); + + expect(isDomainError(result)).toBe(false); + const saved = requireSaved(store.saved); + expect(saved.combatants).toHaveLength(0); + }); + + it("returns domain error for unknown combatant", () => { + const store = stubEncounterStore(emptyEncounter()); + const result = removeCombatantUseCase(store, ID_A); + + expect(isDomainError(result)).toBe(true); + expect(store.saved).toBeNull(); + }); +}); + +describe("retreatTurnUseCase", () => { + it("retreats turn and saves", () => { + const enc = encounterWith("A", "B"); + const store1 = stubEncounterStore(enc); + advanceTurnUseCase(store1); + const store = stubEncounterStore(requireSaved(store1.saved)); + const result = retreatTurnUseCase(store); + + expect(isDomainError(result)).toBe(false); + expect(store.saved).not.toBeNull(); + }); + + it("returns domain error on empty encounter", () => { + const store = stubEncounterStore(emptyEncounter()); + const result = retreatTurnUseCase(store); + + expect(isDomainError(result)).toBe(true); + expect(store.saved).toBeNull(); + }); +}); + +describe("setAcUseCase", () => { + it("sets AC and saves", () => { + const enc = encounterWith("Goblin"); + const store = stubEncounterStore(enc); + const result = setAcUseCase(store, combatantId("Goblin"), 15); + + expect(isDomainError(result)).toBe(false); + expect(requireSaved(store.saved).combatants[0].ac).toBe(15); + }); + + it("returns domain error for unknown combatant", () => { + const store = stubEncounterStore(emptyEncounter()); + const result = setAcUseCase(store, ID_A, 15); + + expect(isDomainError(result)).toBe(true); + expect(store.saved).toBeNull(); + }); +}); + +describe("setHpUseCase", () => { + it("sets max HP and saves", () => { + const enc = encounterWith("Goblin"); + const store = stubEncounterStore(enc); + const result = setHpUseCase(store, combatantId("Goblin"), 20); + + expect(isDomainError(result)).toBe(false); + expect(requireSaved(store.saved).combatants[0].maxHp).toBe(20); + }); + + it("returns domain error for unknown combatant", () => { + const store = stubEncounterStore(emptyEncounter()); + const result = setHpUseCase(store, ID_A, 20); + + expect(isDomainError(result)).toBe(true); + expect(store.saved).toBeNull(); + }); +}); + +describe("setInitiativeUseCase", () => { + it("sets initiative and saves", () => { + const enc = encounterWith("Goblin"); + const store = stubEncounterStore(enc); + const result = setInitiativeUseCase(store, combatantId("Goblin"), 15); + + expect(isDomainError(result)).toBe(false); + expect(requireSaved(store.saved).combatants[0].initiative).toBe(15); + }); + + it("returns domain error for unknown combatant", () => { + const store = stubEncounterStore(emptyEncounter()); + const result = setInitiativeUseCase(store, ID_A, 15); + + expect(isDomainError(result)).toBe(true); + expect(store.saved).toBeNull(); + }); +}); + +describe("toggleConcentrationUseCase", () => { + it("toggles concentration and saves", () => { + const enc = encounterWith("Wizard"); + const store = stubEncounterStore(enc); + const result = toggleConcentrationUseCase(store, combatantId("Wizard")); + + expect(isDomainError(result)).toBe(false); + expect(requireSaved(store.saved).combatants[0].isConcentrating).toBe(true); + }); + + it("returns domain error for unknown combatant", () => { + const store = stubEncounterStore(emptyEncounter()); + const result = toggleConcentrationUseCase(store, ID_A); + + expect(isDomainError(result)).toBe(true); + expect(store.saved).toBeNull(); + }); +}); + +describe("toggleConditionUseCase", () => { + it("toggles condition and saves", () => { + const enc = encounterWith("Goblin"); + const store = stubEncounterStore(enc); + const result = toggleConditionUseCase( + store, + combatantId("Goblin"), + "blinded" as ConditionId, + ); + + expect(isDomainError(result)).toBe(false); + expect(requireSaved(store.saved).combatants[0].conditions).toContain( + "blinded", + ); + }); + + it("returns domain error for unknown combatant", () => { + const store = stubEncounterStore(emptyEncounter()); + const result = toggleConditionUseCase( + store, + ID_A, + "blinded" as ConditionId, + ); + + expect(isDomainError(result)).toBe(true); + expect(store.saved).toBeNull(); + }); +}); + +describe("createPlayerCharacterUseCase", () => { + it("creates a player character and saves", () => { + const store = stubPlayerCharacterStore([]); + const id = playerCharacterId("pc-1"); + const result = createPlayerCharacterUseCase( + store, + id, + "Gandalf", + 15, + 40, + undefined, + undefined, + ); + + expect(isDomainError(result)).toBe(false); + expect(requireSaved(store.saved)).toHaveLength(1); + }); + + it("returns domain error for invalid input", () => { + const store = stubPlayerCharacterStore([]); + const id = playerCharacterId("pc-1"); + const result = createPlayerCharacterUseCase( + store, + id, + "", + 15, + 40, + undefined, + undefined, + ); + + expect(isDomainError(result)).toBe(true); + expect(store.saved).toBeNull(); + }); +}); + +describe("deletePlayerCharacterUseCase", () => { + it("deletes a player character and saves", () => { + const { id, characters } = createPc("Gandalf"); + const store = stubPlayerCharacterStore(characters); + const result = deletePlayerCharacterUseCase(store, id); + + expect(isDomainError(result)).toBe(false); + expect(requireSaved(store.saved)).toHaveLength(0); + }); + + it("returns domain error for unknown character", () => { + const store = stubPlayerCharacterStore([]); + const result = deletePlayerCharacterUseCase( + store, + playerCharacterId("unknown"), + ); + + expect(isDomainError(result)).toBe(true); + expect(store.saved).toBeNull(); + }); +}); + +describe("editPlayerCharacterUseCase", () => { + it("edits a player character and saves", () => { + const { id, characters } = createPc("Gandalf"); + const store = stubPlayerCharacterStore(characters); + const result = editPlayerCharacterUseCase(store, id, { + name: "Gandalf the White", + }); + + expect(isDomainError(result)).toBe(false); + expect(requireSaved(store.saved)[0].name).toBe("Gandalf the White"); + }); + + it("returns domain error for unknown character", () => { + const store = stubPlayerCharacterStore([]); + const result = editPlayerCharacterUseCase( + store, + playerCharacterId("unknown"), + { name: "X" }, + ); + + expect(isDomainError(result)).toBe(true); + expect(store.saved).toBeNull(); + }); +});