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>
This commit is contained in:
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { addCombatant } from "../add-combatant.js";
|
||||
import type { Combatant, Encounter } from "../types.js";
|
||||
import { combatantId, isDomainError } from "../types.js";
|
||||
import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
@@ -112,20 +113,14 @@ describe("addCombatant", () => {
|
||||
const e = enc([A, B]);
|
||||
const result = addCombatant(e, combatantId("x"), "");
|
||||
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-name");
|
||||
}
|
||||
expectDomainError(result, "invalid-name");
|
||||
});
|
||||
|
||||
it("scenario 6: whitespace-only name returns error", () => {
|
||||
const e = enc([A, B]);
|
||||
const result = addCombatant(e, combatantId("x"), " ");
|
||||
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-name");
|
||||
}
|
||||
expectDomainError(result, "invalid-name");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,12 +141,10 @@ describe("addCombatant", () => {
|
||||
for (const e of scenarios) {
|
||||
const result = successResult(e, "new", "New");
|
||||
const { combatants, activeIndex } = result.encounter;
|
||||
if (combatants.length > 0) {
|
||||
expect(activeIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(activeIndex).toBeLessThan(combatants.length);
|
||||
} else {
|
||||
expect(activeIndex).toBe(0);
|
||||
}
|
||||
// After adding a combatant, list is always non-empty
|
||||
expect(combatants.length).toBeGreaterThan(0);
|
||||
expect(activeIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(activeIndex).toBeLessThan(combatants.length);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -188,7 +181,7 @@ describe("addCombatant", () => {
|
||||
it("INV-7: new combatant is always appended at the end", () => {
|
||||
const e = enc([A, B]);
|
||||
const { encounter } = successResult(e, "C", "C");
|
||||
expect(encounter.combatants[encounter.combatants.length - 1]).toEqual({
|
||||
expect(encounter.combatants.at(-1)).toEqual({
|
||||
id: combatantId("C"),
|
||||
name: "C",
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { adjustHp } from "../adjust-hp.js";
|
||||
import type { Combatant, Encounter } from "../types.js";
|
||||
import { combatantId, isDomainError } from "../types.js";
|
||||
import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
function makeCombatant(
|
||||
name: string,
|
||||
@@ -101,37 +102,25 @@ describe("adjustHp", () => {
|
||||
it("returns error for nonexistent combatant", () => {
|
||||
const e = enc([makeCombatant("A", { maxHp: 20, currentHp: 10 })]);
|
||||
const result = adjustHp(e, combatantId("Z"), -1);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("combatant-not-found");
|
||||
}
|
||||
expectDomainError(result, "combatant-not-found");
|
||||
});
|
||||
|
||||
it("returns error when combatant has no HP tracking", () => {
|
||||
const e = enc([makeCombatant("A")]);
|
||||
const result = adjustHp(e, combatantId("A"), -1);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("no-hp-tracking");
|
||||
}
|
||||
expectDomainError(result, "no-hp-tracking");
|
||||
});
|
||||
|
||||
it("returns error for zero delta", () => {
|
||||
const e = enc([makeCombatant("A", { maxHp: 20, currentHp: 10 })]);
|
||||
const result = adjustHp(e, combatantId("A"), 0);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("zero-delta");
|
||||
}
|
||||
expectDomainError(result, "zero-delta");
|
||||
});
|
||||
|
||||
it("returns error for non-integer delta", () => {
|
||||
const e = enc([makeCombatant("A", { maxHp: 20, currentHp: 10 })]);
|
||||
const result = adjustHp(e, combatantId("A"), 1.5);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-delta");
|
||||
}
|
||||
expectDomainError(result, "invalid-delta");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createEncounter,
|
||||
type Encounter,
|
||||
} from "../types.js";
|
||||
import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
@@ -150,10 +151,7 @@ describe("advanceTurn", () => {
|
||||
};
|
||||
const result = advanceTurn(enc);
|
||||
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-encounter");
|
||||
}
|
||||
expectDomainError(result, "invalid-encounter");
|
||||
});
|
||||
|
||||
it("scenario 8: three advances on [A,B,C] completes a full round cycle", () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createPlayerCharacter } from "../create-player-character.js";
|
||||
import type { PlayerCharacter } from "../player-character-types.js";
|
||||
import { playerCharacterId } from "../player-character-types.js";
|
||||
import { isDomainError } from "../types.js";
|
||||
import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
const id = playerCharacterId("pc-1");
|
||||
|
||||
@@ -80,10 +81,7 @@ describe("createPlayerCharacter", () => {
|
||||
|
||||
it("rejects empty name", () => {
|
||||
const result = createPlayerCharacter([], id, "", 10, 50, "blue", "sword");
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-name");
|
||||
}
|
||||
expectDomainError(result, "invalid-name");
|
||||
});
|
||||
|
||||
it("rejects whitespace-only name", () => {
|
||||
@@ -96,10 +94,7 @@ describe("createPlayerCharacter", () => {
|
||||
"blue",
|
||||
"sword",
|
||||
);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-name");
|
||||
}
|
||||
expectDomainError(result, "invalid-name");
|
||||
});
|
||||
|
||||
it("rejects negative AC", () => {
|
||||
@@ -112,10 +107,7 @@ describe("createPlayerCharacter", () => {
|
||||
"blue",
|
||||
"sword",
|
||||
);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-ac");
|
||||
}
|
||||
expectDomainError(result, "invalid-ac");
|
||||
});
|
||||
|
||||
it("rejects non-integer AC", () => {
|
||||
@@ -128,10 +120,7 @@ describe("createPlayerCharacter", () => {
|
||||
"blue",
|
||||
"sword",
|
||||
);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-ac");
|
||||
}
|
||||
expectDomainError(result, "invalid-ac");
|
||||
});
|
||||
|
||||
it("allows AC of 0", () => {
|
||||
@@ -149,10 +138,7 @@ describe("createPlayerCharacter", () => {
|
||||
"blue",
|
||||
"sword",
|
||||
);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-max-hp");
|
||||
}
|
||||
expectDomainError(result, "invalid-max-hp");
|
||||
});
|
||||
|
||||
it("rejects negative maxHp", () => {
|
||||
@@ -165,10 +151,7 @@ describe("createPlayerCharacter", () => {
|
||||
"blue",
|
||||
"sword",
|
||||
);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-max-hp");
|
||||
}
|
||||
expectDomainError(result, "invalid-max-hp");
|
||||
});
|
||||
|
||||
it("rejects non-integer maxHp", () => {
|
||||
@@ -181,10 +164,7 @@ describe("createPlayerCharacter", () => {
|
||||
"blue",
|
||||
"sword",
|
||||
);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-max-hp");
|
||||
}
|
||||
expectDomainError(result, "invalid-max-hp");
|
||||
});
|
||||
|
||||
it("rejects invalid color", () => {
|
||||
@@ -197,10 +177,7 @@ describe("createPlayerCharacter", () => {
|
||||
"neon",
|
||||
"sword",
|
||||
);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-color");
|
||||
}
|
||||
expectDomainError(result, "invalid-color");
|
||||
});
|
||||
|
||||
it("rejects invalid icon", () => {
|
||||
@@ -213,10 +190,7 @@ describe("createPlayerCharacter", () => {
|
||||
"blue",
|
||||
"banana",
|
||||
);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-icon");
|
||||
}
|
||||
expectDomainError(result, "invalid-icon");
|
||||
});
|
||||
|
||||
it("allows undefined color", () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { deletePlayerCharacter } from "../delete-player-character.js";
|
||||
import type { PlayerCharacter } from "../player-character-types.js";
|
||||
import { playerCharacterId } from "../player-character-types.js";
|
||||
import { isDomainError } from "../types.js";
|
||||
import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
const id1 = playerCharacterId("pc-1");
|
||||
const id2 = playerCharacterId("pc-2");
|
||||
@@ -28,10 +29,7 @@ describe("deletePlayerCharacter", () => {
|
||||
|
||||
it("returns error for not-found id", () => {
|
||||
const result = deletePlayerCharacter([makePC()], id2);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("player-character-not-found");
|
||||
}
|
||||
expectDomainError(result, "player-character-not-found");
|
||||
});
|
||||
|
||||
it("emits PlayerCharacterDeleted event", () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 ---
|
||||
|
||||
@@ -124,40 +125,28 @@ describe("editCombatant", () => {
|
||||
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");
|
||||
}
|
||||
expectDomainError(result, "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");
|
||||
}
|
||||
expectDomainError(result, "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");
|
||||
}
|
||||
expectDomainError(result, "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");
|
||||
}
|
||||
expectDomainError(result, "combatant-not-found");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { editPlayerCharacter } from "../edit-player-character.js";
|
||||
import type { PlayerCharacter } from "../player-character-types.js";
|
||||
import { playerCharacterId } from "../player-character-types.js";
|
||||
import { isDomainError } from "../types.js";
|
||||
import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
const id = playerCharacterId("pc-1");
|
||||
|
||||
@@ -42,50 +43,32 @@ describe("editPlayerCharacter", () => {
|
||||
playerCharacterId("pc-999"),
|
||||
{ name: "Nope" },
|
||||
);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("player-character-not-found");
|
||||
}
|
||||
expectDomainError(result, "player-character-not-found");
|
||||
});
|
||||
|
||||
it("rejects empty name", () => {
|
||||
const result = editPlayerCharacter([makePC()], id, { name: "" });
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-name");
|
||||
}
|
||||
expectDomainError(result, "invalid-name");
|
||||
});
|
||||
|
||||
it("rejects invalid AC", () => {
|
||||
const result = editPlayerCharacter([makePC()], id, { ac: -1 });
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-ac");
|
||||
}
|
||||
expectDomainError(result, "invalid-ac");
|
||||
});
|
||||
|
||||
it("rejects invalid maxHp", () => {
|
||||
const result = editPlayerCharacter([makePC()], id, { maxHp: 0 });
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-max-hp");
|
||||
}
|
||||
expectDomainError(result, "invalid-max-hp");
|
||||
});
|
||||
|
||||
it("rejects invalid color", () => {
|
||||
const result = editPlayerCharacter([makePC()], id, { color: "neon" });
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-color");
|
||||
}
|
||||
expectDomainError(result, "invalid-color");
|
||||
});
|
||||
|
||||
it("rejects invalid icon", () => {
|
||||
const result = editPlayerCharacter([makePC()], id, { icon: "banana" });
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-icon");
|
||||
}
|
||||
expectDomainError(result, "invalid-icon");
|
||||
});
|
||||
|
||||
it("returns error when no fields changed", () => {
|
||||
@@ -94,10 +77,7 @@ describe("editPlayerCharacter", () => {
|
||||
name: pc.name,
|
||||
ac: pc.ac,
|
||||
});
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("no-changes");
|
||||
}
|
||||
expectDomainError(result, "no-changes");
|
||||
});
|
||||
|
||||
it("emits exactly one event on success", () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ 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";
|
||||
import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
@@ -92,10 +93,7 @@ describe("removeCombatant", () => {
|
||||
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");
|
||||
}
|
||||
expectDomainError(result, "combatant-not-found");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type Encounter,
|
||||
isDomainError,
|
||||
} from "../types.js";
|
||||
import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
@@ -83,10 +84,7 @@ describe("retreatTurn", () => {
|
||||
const enc = encounter([A, B, C], 0, 1);
|
||||
const result = retreatTurn(enc);
|
||||
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("no-previous-turn");
|
||||
}
|
||||
expectDomainError(result, "no-previous-turn");
|
||||
});
|
||||
|
||||
it("scenario 4: single-combatant retreat — wraps to same combatant, decrements round", () => {
|
||||
@@ -117,10 +115,7 @@ describe("retreatTurn", () => {
|
||||
};
|
||||
const result = retreatTurn(enc);
|
||||
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-encounter");
|
||||
}
|
||||
expectDomainError(result, "invalid-encounter");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { rollInitiative } from "../roll-initiative.js";
|
||||
import { isDomainError } from "../types.js";
|
||||
import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
describe("rollInitiative", () => {
|
||||
describe("valid rolls", () => {
|
||||
@@ -32,18 +33,12 @@ describe("rollInitiative", () => {
|
||||
describe("invalid dice rolls", () => {
|
||||
it("rejects 0", () => {
|
||||
const result = rollInitiative(0, 5);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-dice-roll");
|
||||
}
|
||||
expectDomainError(result, "invalid-dice-roll");
|
||||
});
|
||||
|
||||
it("rejects 21", () => {
|
||||
const result = rollInitiative(21, 5);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-dice-roll");
|
||||
}
|
||||
expectDomainError(result, "invalid-dice-roll");
|
||||
});
|
||||
|
||||
it("rejects non-integer (3.5)", () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { setAc } from "../set-ac.js";
|
||||
import type { Combatant, Encounter } from "../types.js";
|
||||
import { combatantId, isDomainError } from "../types.js";
|
||||
import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
function makeCombatant(name: string, ac?: number): Combatant {
|
||||
return ac === undefined
|
||||
@@ -67,30 +68,21 @@ describe("setAc", () => {
|
||||
const e = enc([makeCombatant("A")]);
|
||||
const result = setAc(e, combatantId("nonexistent"), 10);
|
||||
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("combatant-not-found");
|
||||
}
|
||||
expectDomainError(result, "combatant-not-found");
|
||||
});
|
||||
|
||||
it("returns error for negative AC", () => {
|
||||
const e = enc([makeCombatant("A")]);
|
||||
const result = setAc(e, combatantId("A"), -1);
|
||||
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-ac");
|
||||
}
|
||||
expectDomainError(result, "invalid-ac");
|
||||
});
|
||||
|
||||
it("returns error for non-integer AC", () => {
|
||||
const e = enc([makeCombatant("A")]);
|
||||
const result = setAc(e, combatantId("A"), 3.5);
|
||||
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-ac");
|
||||
}
|
||||
expectDomainError(result, "invalid-ac");
|
||||
});
|
||||
|
||||
it("returns error for NaN", () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { setHp } from "../set-hp.js";
|
||||
import type { Combatant, Encounter } from "../types.js";
|
||||
import { combatantId, isDomainError } from "../types.js";
|
||||
import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
function makeCombatant(
|
||||
name: string,
|
||||
@@ -10,9 +11,9 @@ function makeCombatant(
|
||||
return {
|
||||
id: combatantId(name),
|
||||
name,
|
||||
...(opts?.maxHp !== undefined
|
||||
? { maxHp: opts.maxHp, currentHp: opts.currentHp ?? opts.maxHp }
|
||||
: {}),
|
||||
...(opts?.maxHp === undefined
|
||||
? {}
|
||||
: { maxHp: opts.maxHp, currentHp: opts.currentHp ?? opts.maxHp }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -116,37 +117,25 @@ describe("setHp", () => {
|
||||
it("returns error for nonexistent combatant", () => {
|
||||
const e = enc([makeCombatant("A")]);
|
||||
const result = setHp(e, combatantId("Z"), 10);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("combatant-not-found");
|
||||
}
|
||||
expectDomainError(result, "combatant-not-found");
|
||||
});
|
||||
|
||||
it("rejects maxHp of 0", () => {
|
||||
const e = enc([makeCombatant("A")]);
|
||||
const result = setHp(e, combatantId("A"), 0);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-max-hp");
|
||||
}
|
||||
expectDomainError(result, "invalid-max-hp");
|
||||
});
|
||||
|
||||
it("rejects negative maxHp", () => {
|
||||
const e = enc([makeCombatant("A")]);
|
||||
const result = setHp(e, combatantId("A"), -5);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-max-hp");
|
||||
}
|
||||
expectDomainError(result, "invalid-max-hp");
|
||||
});
|
||||
|
||||
it("rejects non-integer maxHp", () => {
|
||||
const e = enc([makeCombatant("A")]);
|
||||
const result = setHp(e, combatantId("A"), 3.5);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-max-hp");
|
||||
}
|
||||
expectDomainError(result, "invalid-max-hp");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { setInitiative } from "../set-initiative.js";
|
||||
import type { Combatant, Encounter } from "../types.js";
|
||||
import { combatantId, isDomainError } from "../types.js";
|
||||
import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
@@ -73,10 +74,7 @@ describe("setInitiative", () => {
|
||||
const e = enc([A, B], 0);
|
||||
const result = setInitiative(e, combatantId("A"), 3.5);
|
||||
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("invalid-initiative");
|
||||
}
|
||||
expectDomainError(result, "invalid-initiative");
|
||||
});
|
||||
|
||||
it("AS-3b: reject NaN", () => {
|
||||
@@ -109,10 +107,7 @@ describe("setInitiative", () => {
|
||||
const e = enc([A, B], 0);
|
||||
const result = setInitiative(e, combatantId("nonexistent"), 10);
|
||||
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("combatant-not-found");
|
||||
}
|
||||
expectDomainError(result, "combatant-not-found");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
9
packages/domain/src/__tests__/test-helpers.ts
Normal file
9
packages/domain/src/__tests__/test-helpers.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { expect } from "vitest";
|
||||
import { type DomainError, isDomainError } from "../types.js";
|
||||
|
||||
export function expectDomainError(result: unknown, code: string): DomainError {
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (!isDomainError(result)) throw new Error("unreachable");
|
||||
expect(result.code).toBe(code);
|
||||
return result;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { toggleConcentration } from "../toggle-concentration.js";
|
||||
import type { Combatant, Encounter } from "../types.js";
|
||||
import { combatantId, isDomainError } from "../types.js";
|
||||
import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
function makeCombatant(name: string, isConcentrating?: boolean): Combatant {
|
||||
return isConcentrating
|
||||
@@ -46,10 +47,7 @@ describe("toggleConcentration", () => {
|
||||
const e = enc([makeCombatant("A")]);
|
||||
const result = toggleConcentration(e, combatantId("missing"));
|
||||
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("combatant-not-found");
|
||||
}
|
||||
expectDomainError(result, "combatant-not-found");
|
||||
});
|
||||
|
||||
it("does not mutate input encounter", () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CONDITION_DEFINITIONS } from "../conditions.js";
|
||||
import { toggleCondition } from "../toggle-condition.js";
|
||||
import type { Combatant, Encounter } from "../types.js";
|
||||
import { combatantId, isDomainError } from "../types.js";
|
||||
import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
function makeCombatant(
|
||||
name: string,
|
||||
@@ -77,20 +78,14 @@ describe("toggleCondition", () => {
|
||||
"flying" as ConditionId,
|
||||
);
|
||||
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("unknown-condition");
|
||||
}
|
||||
expectDomainError(result, "unknown-condition");
|
||||
});
|
||||
|
||||
it("returns error for nonexistent combatant", () => {
|
||||
const e = enc([makeCombatant("A")]);
|
||||
const result = toggleCondition(e, combatantId("missing"), "blinded");
|
||||
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
if (isDomainError(result)) {
|
||||
expect(result.code).toBe("combatant-not-found");
|
||||
}
|
||||
expectDomainError(result, "combatant-not-found");
|
||||
});
|
||||
|
||||
it("does not mutate input encounter", () => {
|
||||
|
||||
Reference in New Issue
Block a user