Files
initiative/packages/application/src/__tests__/roll-initiative-use-case.test.ts
Lukas 5a262c66cd 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>
2026-03-14 15:31:35 +01:00

156 lines
4.1 KiB
TypeScript

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);
});
});