143 lines
4.1 KiB
TypeScript
143 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";
|
|
|
|
// --- 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"));
|
|
|
|
expect(isDomainError(result)).toBe(true);
|
|
if (isDomainError(result)) {
|
|
expect(result.code).toBe("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");
|
|
}
|
|
});
|
|
});
|
|
});
|