Files
initiative/packages/domain/src/__tests__/remove-combatant.test.ts
Lukas 36768d3aa1 Upgrade Biome to 2.4.7 and enable 54 additional lint rules
Add rules covering bug prevention (noLeakedRender, noFloatingPromises,
noImportCycles, noReactForwardRef), security (noScriptUrl, noAlert),
performance (noAwaitInLoops, useTopLevelRegex), and code style
(noNestedTernary, useGlobalThis, useNullishCoalescing, useSortedClasses,
plus ~40 more). Fix all violations: extract top-level regex constants,
guard React && renders with boolean coercion, refactor nested ternaries,
replace window with globalThis, sort Tailwind classes, and introduce
expectDomainError test helper to eliminate conditional expects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 14:25:09 +01:00

141 lines
4.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { removeCombatant } from "../remove-combatant.js";
import type { Combatant, Encounter } from "../types.js";
import { combatantId, isDomainError } from "../types.js";
import { expectDomainError } from "./test-helpers.js";
// --- Helpers ---
function makeCombatant(name: string): Combatant {
return { id: combatantId(name), name };
}
const A = makeCombatant("A");
const B = makeCombatant("B");
const C = makeCombatant("C");
const D = makeCombatant("D");
function enc(
combatants: Combatant[],
activeIndex = 0,
roundNumber = 1,
): Encounter {
return { combatants, activeIndex, roundNumber };
}
function successResult(encounter: Encounter, id: string) {
const result = removeCombatant(encounter, combatantId(id));
if (isDomainError(result)) {
throw new Error(`Expected success, got error: ${result.message}`);
}
return result;
}
// --- Acceptance Scenarios ---
describe("removeCombatant", () => {
describe("acceptance scenarios", () => {
it("AS-1: remove combatant after active — activeIndex unchanged", () => {
// [A*, B, C] remove C → [A*, B], activeIndex stays 0
const e = enc([A, B, C], 0, 2);
const { encounter, events } = successResult(e, "C");
expect(encounter.combatants).toEqual([A, B]);
expect(encounter.activeIndex).toBe(0);
expect(encounter.roundNumber).toBe(2);
expect(events).toEqual([
{
type: "CombatantRemoved",
combatantId: combatantId("C"),
name: "C",
},
]);
});
it("AS-2: remove combatant before active — activeIndex decrements", () => {
// [A, B, C*] remove A → [B, C*], activeIndex 2→1
const e = enc([A, B, C], 2, 3);
const { encounter } = successResult(e, "A");
expect(encounter.combatants).toEqual([B, C]);
expect(encounter.activeIndex).toBe(1);
expect(encounter.roundNumber).toBe(3);
});
it("AS-3: remove active combatant mid-list — next slides in", () => {
// [A, B*, C, D] remove B → [A, C*, D], activeIndex stays 1
const e = enc([A, B, C, D], 1, 1);
const { encounter } = successResult(e, "B");
expect(encounter.combatants).toEqual([A, C, D]);
expect(encounter.activeIndex).toBe(1);
});
it("AS-4: remove active combatant at end — wraps to 0", () => {
// [A, B, C*] remove C → [A, B], activeIndex wraps to 0
const e = enc([A, B, C], 2, 1);
const { encounter } = successResult(e, "C");
expect(encounter.combatants).toEqual([A, B]);
expect(encounter.activeIndex).toBe(0);
});
it("AS-5: remove only combatant — empty list, activeIndex 0", () => {
const e = enc([A], 0, 5);
const { encounter } = successResult(e, "A");
expect(encounter.combatants).toEqual([]);
expect(encounter.activeIndex).toBe(0);
expect(encounter.roundNumber).toBe(5);
});
it("AS-6: ID not found — returns DomainError", () => {
const e = enc([A, B], 0, 1);
const result = removeCombatant(e, combatantId("nonexistent"));
expectDomainError(result, "combatant-not-found");
});
});
describe("invariants", () => {
it("event shape includes combatantId and name", () => {
const e = enc([A, B], 0, 1);
const { events } = successResult(e, "B");
expect(events).toHaveLength(1);
expect(events[0]).toEqual({
type: "CombatantRemoved",
combatantId: combatantId("B"),
name: "B",
});
});
it("roundNumber never changes on removal", () => {
const e = enc([A, B, C], 1, 7);
const { encounter } = successResult(e, "A");
expect(encounter.roundNumber).toBe(7);
});
it("determinism — same input produces same output", () => {
const e = enc([A, B, C], 1, 3);
const result1 = removeCombatant(e, combatantId("B"));
const result2 = removeCombatant(e, combatantId("B"));
expect(result1).toEqual(result2);
});
it("every success emits exactly one CombatantRemoved event", () => {
const scenarios: [Encounter, string][] = [
[enc([A]), "A"],
[enc([A, B], 1), "A"],
[enc([A, B, C], 2, 5), "C"],
];
for (const [e, id] of scenarios) {
const { events } = successResult(e, id);
expect(events).toHaveLength(1);
expect(events[0].type).toBe("CombatantRemoved");
}
});
});
});