Files
initiative/packages/domain/src/__tests__/create-player-character.test.ts
Lukas 91703ddebc
All checks were successful
CI / check (push) Successful in 45s
CI / build-image (push) Successful in 18s
Add player character management feature
Persistent player character templates (name, AC, HP, color, icon) with
full CRUD, bestiary-style search to add PCs to encounters with pre-filled
stats, and color/icon visual distinction in combatant rows. Also stops
the stat block panel from auto-opening when adding a creature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 18:11:08 +01:00

228 lines
4.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
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";
const id = playerCharacterId("pc-1");
function success(
characters: readonly PlayerCharacter[],
name: string,
ac: number,
maxHp: number,
color = "blue",
icon = "sword",
) {
const result = createPlayerCharacter(
characters,
id,
name,
ac,
maxHp,
color,
icon,
);
if (isDomainError(result)) {
throw new Error(`Expected success, got error: ${result.message}`);
}
return result;
}
describe("createPlayerCharacter", () => {
it("creates a valid player character", () => {
const { characters, events } = success(
[],
"Aragorn",
16,
120,
"green",
"shield",
);
expect(characters).toHaveLength(1);
expect(characters[0]).toEqual({
id,
name: "Aragorn",
ac: 16,
maxHp: 120,
color: "green",
icon: "shield",
});
expect(events).toEqual([
{
type: "PlayerCharacterCreated",
playerCharacterId: id,
name: "Aragorn",
},
]);
});
it("trims whitespace from name", () => {
const { characters } = success([], " Gandalf ", 12, 80);
expect(characters[0].name).toBe("Gandalf");
});
it("appends to existing characters", () => {
const existing: PlayerCharacter = {
id: playerCharacterId("pc-0"),
name: "Legolas",
ac: 14,
maxHp: 90,
color: "green",
icon: "eye",
};
const { characters } = success([existing], "Gimli", 18, 100, "red", "axe");
expect(characters).toHaveLength(2);
expect(characters[0]).toEqual(existing);
expect(characters[1].name).toBe("Gimli");
});
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");
}
});
it("rejects whitespace-only name", () => {
const result = createPlayerCharacter(
[],
id,
" ",
10,
50,
"blue",
"sword",
);
expect(isDomainError(result)).toBe(true);
if (isDomainError(result)) {
expect(result.code).toBe("invalid-name");
}
});
it("rejects negative AC", () => {
const result = createPlayerCharacter(
[],
id,
"Test",
-1,
50,
"blue",
"sword",
);
expect(isDomainError(result)).toBe(true);
if (isDomainError(result)) {
expect(result.code).toBe("invalid-ac");
}
});
it("rejects non-integer AC", () => {
const result = createPlayerCharacter(
[],
id,
"Test",
10.5,
50,
"blue",
"sword",
);
expect(isDomainError(result)).toBe(true);
if (isDomainError(result)) {
expect(result.code).toBe("invalid-ac");
}
});
it("allows AC of 0", () => {
const { characters } = success([], "Test", 0, 50);
expect(characters[0].ac).toBe(0);
});
it("rejects maxHp of 0", () => {
const result = createPlayerCharacter(
[],
id,
"Test",
10,
0,
"blue",
"sword",
);
expect(isDomainError(result)).toBe(true);
if (isDomainError(result)) {
expect(result.code).toBe("invalid-max-hp");
}
});
it("rejects negative maxHp", () => {
const result = createPlayerCharacter(
[],
id,
"Test",
10,
-5,
"blue",
"sword",
);
expect(isDomainError(result)).toBe(true);
if (isDomainError(result)) {
expect(result.code).toBe("invalid-max-hp");
}
});
it("rejects non-integer maxHp", () => {
const result = createPlayerCharacter(
[],
id,
"Test",
10,
50.5,
"blue",
"sword",
);
expect(isDomainError(result)).toBe(true);
if (isDomainError(result)) {
expect(result.code).toBe("invalid-max-hp");
}
});
it("rejects invalid color", () => {
const result = createPlayerCharacter(
[],
id,
"Test",
10,
50,
"neon",
"sword",
);
expect(isDomainError(result)).toBe(true);
if (isDomainError(result)) {
expect(result.code).toBe("invalid-color");
}
});
it("rejects invalid icon", () => {
const result = createPlayerCharacter(
[],
id,
"Test",
10,
50,
"blue",
"banana",
);
expect(isDomainError(result)).toBe(true);
if (isDomainError(result)) {
expect(result.code).toBe("invalid-icon");
}
});
it("emits exactly one event on success", () => {
const { events } = success([], "Test", 10, 50);
expect(events).toHaveLength(1);
expect(events[0].type).toBe("PlayerCharacterCreated");
});
});