Implement the 004-edit-combatant feature that adds the possibility to change a combatants name

This commit is contained in:
Lukas
2026-03-04 10:05:13 +01:00
parent aed234de7b
commit a9df826fef
16 changed files with 854 additions and 5 deletions

View File

@@ -0,0 +1,163 @@
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";
// --- 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");
expect(isDomainError(result)).toBe(true);
if (isDomainError(result)) {
expect(result.code).toBe("combatant-not-found");
}
});
it("empty name returns invalid-name error", () => {
const e = enc([Alice, Bob]);
const result = editCombatant(e, combatantId("Alice"), "");
expect(isDomainError(result)).toBe(true);
if (isDomainError(result)) {
expect(result.code).toBe("invalid-name");
}
});
it("whitespace-only name returns invalid-name error", () => {
const e = enc([Alice, Bob]);
const result = editCombatant(e, combatantId("Alice"), " ");
expect(isDomainError(result)).toBe(true);
if (isDomainError(result)) {
expect(result.code).toBe("invalid-name");
}
});
it("empty encounter returns combatant-not-found for any id", () => {
const e = enc([]);
const result = editCombatant(e, combatantId("any"), "Name");
expect(isDomainError(result)).toBe(true);
if (isDomainError(result)) {
expect(result.code).toBe("combatant-not-found");
}
});
});
});