Implement the 004-edit-combatant feature that adds the possibility to change a combatants name
This commit is contained in:
163
packages/domain/src/__tests__/edit-combatant.test.ts
Normal file
163
packages/domain/src/__tests__/edit-combatant.test.ts
Normal 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");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user