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>
153 lines
4.4 KiB
TypeScript
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");
|
|
});
|
|
});
|
|
});
|