Add test coverage for all 17 application layer use cases
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 <noreply@anthropic.com>
This commit is contained in:
54
packages/application/src/__tests__/helpers.ts
Normal file
54
packages/application/src/__tests__/helpers.ts
Normal file
@@ -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<T>(value: T | null): T {
|
||||||
|
if (value === null) throw new Error("Expected store.saved to be non-null");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expectSuccess<T>(
|
||||||
|
result: T,
|
||||||
|
): asserts result is Exclude<T, { kind: "domain-error" }> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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>): 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
388
packages/application/src/__tests__/use-cases.test.ts
Normal file
388
packages/application/src/__tests__/use-cases.test.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user