Add tests for ConditionTags and CreatePlayerModal

ConditionTags: rendering, remove callback, add picker callback.
CreatePlayerModal: create/edit modes, form validation (name, AC, HP,
level), error display and clearing, onSave/onClose callbacks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-29 12:35:35 +02:00
parent 2971d32f45
commit 2bc22369ce
3 changed files with 264 additions and 4 deletions

View File

@@ -0,0 +1,87 @@
// @vitest-environment jsdom
import type { ConditionId } from "@initiative/domain";
import { cleanup, render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ConditionTags } from "../condition-tags.js";
vi.mock("../../contexts/rules-edition-context.js", () => ({
useRulesEditionContext: () => ({ edition: "5.5e" }),
}));
afterEach(cleanup);
describe("ConditionTags", () => {
it("renders nothing when conditions is undefined", () => {
const { container } = render(
<ConditionTags
conditions={undefined}
onRemove={() => {}}
onOpenPicker={() => {}}
/>,
);
// Only the add button should be present
expect(container.querySelectorAll("button")).toHaveLength(1);
});
it("renders a button per condition", () => {
const conditions: ConditionId[] = ["blinded", "prone"];
render(
<ConditionTags
conditions={conditions}
onRemove={() => {}}
onOpenPicker={() => {}}
/>,
);
expect(
screen.getByRole("button", { name: "Remove Blinded" }),
).toBeDefined();
expect(screen.getByRole("button", { name: "Remove Prone" })).toBeDefined();
});
it("calls onRemove with condition id when clicked", async () => {
const onRemove = vi.fn();
render(
<ConditionTags
conditions={["blinded"] as ConditionId[]}
onRemove={onRemove}
onOpenPicker={() => {}}
/>,
);
await userEvent.click(
screen.getByRole("button", { name: "Remove Blinded" }),
);
expect(onRemove).toHaveBeenCalledWith("blinded");
});
it("calls onOpenPicker when add button is clicked", async () => {
const onOpenPicker = vi.fn();
render(
<ConditionTags
conditions={[]}
onRemove={() => {}}
onOpenPicker={onOpenPicker}
/>,
);
await userEvent.click(
screen.getByRole("button", { name: "Add condition" }),
);
expect(onOpenPicker).toHaveBeenCalledOnce();
});
it("renders empty conditions array without errors", () => {
render(
<ConditionTags
conditions={[]}
onRemove={() => {}}
onOpenPicker={() => {}}
/>,
);
// Only add button
expect(screen.getByRole("button", { name: "Add condition" })).toBeDefined();
});
});

View File

@@ -0,0 +1,173 @@
// @vitest-environment jsdom
import type { PlayerCharacter } from "@initiative/domain";
import { playerCharacterId } from "@initiative/domain";
import { cleanup, render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { CreatePlayerModal } from "../create-player-modal.js";
beforeAll(() => {
HTMLDialogElement.prototype.showModal =
HTMLDialogElement.prototype.showModal ||
function showModal(this: HTMLDialogElement) {
this.setAttribute("open", "");
};
HTMLDialogElement.prototype.close =
HTMLDialogElement.prototype.close ||
function close(this: HTMLDialogElement) {
this.removeAttribute("open");
};
});
afterEach(cleanup);
function renderModal(
overrides: Partial<Parameters<typeof CreatePlayerModal>[0]> = {},
) {
const defaults = {
open: true,
onClose: vi.fn(),
onSave: vi.fn(),
};
const props = { ...defaults, ...overrides };
return { ...render(<CreatePlayerModal {...props} />), ...props };
}
describe("CreatePlayerModal", () => {
it("renders create form with defaults", () => {
renderModal();
expect(screen.getByText("Create Player")).toBeDefined();
expect(screen.getByLabelText("Name")).toBeDefined();
expect(screen.getByLabelText("AC")).toBeDefined();
expect(screen.getByLabelText("Max HP")).toBeDefined();
expect(screen.getByLabelText("Level")).toBeDefined();
expect(screen.getByRole("button", { name: "Create" })).toBeDefined();
});
it("renders edit form when playerCharacter is provided", () => {
const pc: PlayerCharacter = {
id: playerCharacterId("pc-1"),
name: "Gandalf",
ac: 15,
maxHp: 40,
color: "blue",
icon: "wand",
level: 10,
};
renderModal({ playerCharacter: pc });
expect(screen.getByText("Edit Player")).toBeDefined();
expect(screen.getByLabelText("Name")).toHaveProperty("value", "Gandalf");
expect(screen.getByLabelText("AC")).toHaveProperty("value", "15");
expect(screen.getByLabelText("Max HP")).toHaveProperty("value", "40");
expect(screen.getByLabelText("Level")).toHaveProperty("value", "10");
expect(screen.getByRole("button", { name: "Save" })).toBeDefined();
});
it("calls onSave with valid data", async () => {
const { onSave, onClose } = renderModal();
const user = userEvent.setup();
await user.type(screen.getByLabelText("Name"), "Aria");
await user.clear(screen.getByLabelText("AC"));
await user.type(screen.getByLabelText("AC"), "16");
await user.clear(screen.getByLabelText("Max HP"));
await user.type(screen.getByLabelText("Max HP"), "30");
await user.type(screen.getByLabelText("Level"), "5");
await user.click(screen.getByRole("button", { name: "Create" }));
expect(onSave).toHaveBeenCalledWith(
"Aria",
16,
30,
undefined,
undefined,
5,
);
expect(onClose).toHaveBeenCalled();
});
it("shows error for empty name", async () => {
const { onSave } = renderModal();
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: "Create" }));
expect(screen.getByText("Name is required")).toBeDefined();
expect(onSave).not.toHaveBeenCalled();
});
it("shows error for invalid AC", async () => {
const { onSave } = renderModal();
const user = userEvent.setup();
await user.type(screen.getByLabelText("Name"), "Test");
await user.clear(screen.getByLabelText("AC"));
await user.type(screen.getByLabelText("AC"), "abc");
await user.click(screen.getByRole("button", { name: "Create" }));
expect(screen.getByText("AC must be a non-negative number")).toBeDefined();
expect(onSave).not.toHaveBeenCalled();
});
it("shows error for invalid Max HP", async () => {
const { onSave } = renderModal();
const user = userEvent.setup();
await user.type(screen.getByLabelText("Name"), "Test");
await user.clear(screen.getByLabelText("Max HP"));
await user.type(screen.getByLabelText("Max HP"), "0");
await user.click(screen.getByRole("button", { name: "Create" }));
expect(screen.getByText("Max HP must be at least 1")).toBeDefined();
expect(onSave).not.toHaveBeenCalled();
});
it("shows error for invalid level", async () => {
const { onSave } = renderModal();
const user = userEvent.setup();
await user.type(screen.getByLabelText("Name"), "Test");
await user.type(screen.getByLabelText("Level"), "25");
await user.click(screen.getByRole("button", { name: "Create" }));
expect(screen.getByText("Level must be between 1 and 20")).toBeDefined();
expect(onSave).not.toHaveBeenCalled();
});
it("clears error when name is edited", async () => {
renderModal();
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: "Create" }));
expect(screen.getByText("Name is required")).toBeDefined();
await user.type(screen.getByLabelText("Name"), "A");
expect(screen.queryByText("Name is required")).toBeNull();
});
it("calls onClose when cancel is clicked", async () => {
const { onClose } = renderModal();
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(onClose).toHaveBeenCalledOnce();
});
it("omits level when field is empty", async () => {
const { onSave } = renderModal();
const user = userEvent.setup();
await user.type(screen.getByLabelText("Name"), "Aria");
await user.click(screen.getByRole("button", { name: "Create" }));
expect(onSave).toHaveBeenCalledWith(
"Aria",
10,
10,
undefined,
undefined,
undefined,
);
});
});

View File

@@ -30,12 +30,12 @@ export default defineConfig({
branches: 55,
},
"apps/web/src/components": {
lines: 52,
branches: 49,
lines: 59,
branches: 55,
},
"apps/web/src/components/ui": {
lines: 83,
branches: 74,
lines: 86,
branches: 83,
},
},
},