Adds missing tests for undoUseCase, redoUseCase, and setTempHpUseCase, bringing application layer coverage from ~81% to 97%. Removes autoUpdate from coverage thresholds and sets floors to actual values so they enforce a real minimum. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
472 lines
14 KiB
TypeScript
472 lines
14 KiB
TypeScript
import {
|
|
type ConditionId,
|
|
combatantId,
|
|
createEncounter,
|
|
EMPTY_UNDO_REDO_STATE,
|
|
isDomainError,
|
|
playerCharacterId,
|
|
pushUndo,
|
|
} 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 { redoUseCase } from "../redo-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 { setTempHpUseCase } from "../set-temp-hp-use-case.js";
|
|
import { toggleConcentrationUseCase } from "../toggle-concentration-use-case.js";
|
|
import { toggleConditionUseCase } from "../toggle-condition-use-case.js";
|
|
import { undoUseCase } from "../undo-use-case.js";
|
|
import {
|
|
requireSaved,
|
|
stubEncounterStore,
|
|
stubPlayerCharacterStore,
|
|
stubUndoRedoStore,
|
|
} 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();
|
|
});
|
|
});
|
|
|
|
describe("setTempHpUseCase", () => {
|
|
it("sets temp HP and saves", () => {
|
|
const enc = encounterWithHp("Goblin", 10);
|
|
const store = stubEncounterStore(enc);
|
|
const result = setTempHpUseCase(store, combatantId("Goblin"), 5);
|
|
|
|
expect(isDomainError(result)).toBe(false);
|
|
expect(requireSaved(store.saved).combatants[0].tempHp).toBe(5);
|
|
});
|
|
|
|
it("returns domain error for unknown combatant", () => {
|
|
const store = stubEncounterStore(emptyEncounter());
|
|
const result = setTempHpUseCase(store, ID_A, 5);
|
|
|
|
expect(isDomainError(result)).toBe(true);
|
|
expect(store.saved).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("undoUseCase", () => {
|
|
it("restores previous encounter and saves both stores", () => {
|
|
const previous = encounterWith("A");
|
|
const current = encounterWith("A", "B");
|
|
const undoRedoState = pushUndo(EMPTY_UNDO_REDO_STATE, previous);
|
|
const encounterStore = stubEncounterStore(current);
|
|
const undoRedoStore = stubUndoRedoStore(undoRedoState);
|
|
|
|
const result = undoUseCase(encounterStore, undoRedoStore);
|
|
|
|
expect(isDomainError(result)).toBe(false);
|
|
expect(requireSaved(encounterStore.saved).combatants).toHaveLength(1);
|
|
expect(undoRedoStore.saved).not.toBeNull();
|
|
});
|
|
|
|
it("returns domain error when nothing to undo", () => {
|
|
const encounterStore = stubEncounterStore(emptyEncounter());
|
|
const undoRedoStore = stubUndoRedoStore();
|
|
|
|
const result = undoUseCase(encounterStore, undoRedoStore);
|
|
|
|
expect(isDomainError(result)).toBe(true);
|
|
expect(encounterStore.saved).toBeNull();
|
|
expect(undoRedoStore.saved).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("redoUseCase", () => {
|
|
it("restores next encounter and saves both stores", () => {
|
|
const previous = encounterWith("A");
|
|
const current = encounterWith("A", "B");
|
|
// Simulate: undo pushed current to redoStack
|
|
const undoRedoState = {
|
|
undoStack: [],
|
|
redoStack: [current],
|
|
};
|
|
const encounterStore = stubEncounterStore(previous);
|
|
const undoRedoStore = stubUndoRedoStore(undoRedoState);
|
|
|
|
const result = redoUseCase(encounterStore, undoRedoStore);
|
|
|
|
expect(isDomainError(result)).toBe(false);
|
|
expect(requireSaved(encounterStore.saved).combatants).toHaveLength(2);
|
|
expect(undoRedoStore.saved).not.toBeNull();
|
|
});
|
|
|
|
it("returns domain error when nothing to redo", () => {
|
|
const encounterStore = stubEncounterStore(emptyEncounter());
|
|
const undoRedoStore = stubUndoRedoStore();
|
|
|
|
const result = redoUseCase(encounterStore, undoRedoStore);
|
|
|
|
expect(isDomainError(result)).toBe(true);
|
|
expect(encounterStore.saved).toBeNull();
|
|
expect(undoRedoStore.saved).toBeNull();
|
|
});
|
|
});
|