Files
initiative/packages/domain/src/__tests__/edit-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

153 lines
4.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { editCombatant } from "../edit-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 Alice = makeCombatant("Alice");
const Bob = makeCombatant("Bob");
function enc(
combatants: Combatant[],
activeIndex = 0,
roundNumber = 1,
): Encounter {
return { combatants, activeIndex, roundNumber };
}
function successResult(encounter: Encounter, id: string, newName: string) {
const result = editCombatant(encounter, combatantId(id), newName);
if (isDomainError(result)) {
throw new Error(`Expected success, got error: ${result.message}`);
}
return result;
}
// --- Acceptance Scenarios (T004) ---
describe("editCombatant", () => {
describe("acceptance scenarios", () => {
it("scenario 1: rename succeeds with correct event containing combatantId, oldName, newName", () => {
const e = enc([Alice, Bob]);
const { encounter, events } = successResult(e, "Bob", "Robert");
expect(encounter.combatants[1]).toEqual({
id: combatantId("Bob"),
name: "Robert",
});
expect(events).toEqual([
{
type: "CombatantUpdated",
combatantId: combatantId("Bob"),
oldName: "Bob",
newName: "Robert",
},
]);
});
it("scenario 2: activeIndex and roundNumber preserved when renaming the active combatant", () => {
const e = enc([Alice, Bob], 1, 3);
const { encounter } = successResult(e, "Bob", "Robert");
expect(encounter.activeIndex).toBe(1);
expect(encounter.roundNumber).toBe(3);
expect(encounter.combatants[1].name).toBe("Robert");
});
it("scenario 3: combatant list order preserved", () => {
const Cael = makeCombatant("Cael");
const e = enc([Alice, Bob, Cael]);
const { encounter } = successResult(e, "Bob", "Robert");
expect(encounter.combatants.map((c) => c.name)).toEqual([
"Alice",
"Robert",
"Cael",
]);
});
it("scenario 4: renaming to same name still emits event", () => {
const e = enc([Alice, Bob]);
const { encounter, events } = successResult(e, "Bob", "Bob");
expect(encounter.combatants[1].name).toBe("Bob");
expect(events).toHaveLength(1);
expect(events[0]).toEqual({
type: "CombatantUpdated",
combatantId: combatantId("Bob"),
oldName: "Bob",
newName: "Bob",
});
});
});
// --- Invariant Tests (T005) ---
describe("invariants", () => {
it("INV-1: determinism — same inputs produce same outputs", () => {
const e = enc([Alice, Bob], 1, 3);
const result1 = editCombatant(e, combatantId("Alice"), "Aria");
const result2 = editCombatant(e, combatantId("Alice"), "Aria");
expect(result1).toEqual(result2);
});
it("INV-2: exactly one event emitted on success", () => {
const e = enc([Alice, Bob]);
const { events } = successResult(e, "Alice", "Aria");
expect(events).toHaveLength(1);
expect(events[0].type).toBe("CombatantUpdated");
});
it("INV-3: original encounter is not mutated", () => {
const e = enc([Alice, Bob], 0, 1);
const originalCombatants = [...e.combatants];
const originalActiveIndex = e.activeIndex;
const originalRoundNumber = e.roundNumber;
successResult(e, "Alice", "Aria");
expect(e.combatants).toEqual(originalCombatants);
expect(e.activeIndex).toBe(originalActiveIndex);
expect(e.roundNumber).toBe(originalRoundNumber);
});
});
// --- Error Scenarios (T011) ---
describe("error scenarios", () => {
it("non-existent id returns combatant-not-found error", () => {
const e = enc([Alice, Bob]);
const result = editCombatant(e, combatantId("nonexistent"), "NewName");
expectDomainError(result, "combatant-not-found");
});
it("empty name returns invalid-name error", () => {
const e = enc([Alice, Bob]);
const result = editCombatant(e, combatantId("Alice"), "");
expectDomainError(result, "invalid-name");
});
it("whitespace-only name returns invalid-name error", () => {
const e = enc([Alice, Bob]);
const result = editCombatant(e, combatantId("Alice"), " ");
expectDomainError(result, "invalid-name");
});
it("empty encounter returns combatant-not-found for any id", () => {
const e = enc([]);
const result = editCombatant(e, combatantId("any"), "Name");
expectDomainError(result, "combatant-not-found");
});
});
});