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:
87
apps/web/src/components/__tests__/condition-tags.test.tsx
Normal file
87
apps/web/src/components/__tests__/condition-tags.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
173
apps/web/src/components/__tests__/create-player-modal.test.tsx
Normal file
173
apps/web/src/components/__tests__/create-player-modal.test.tsx
Normal 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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -30,12 +30,12 @@ export default defineConfig({
|
|||||||
branches: 55,
|
branches: 55,
|
||||||
},
|
},
|
||||||
"apps/web/src/components": {
|
"apps/web/src/components": {
|
||||||
lines: 52,
|
lines: 59,
|
||||||
branches: 49,
|
branches: 55,
|
||||||
},
|
},
|
||||||
"apps/web/src/components/ui": {
|
"apps/web/src/components/ui": {
|
||||||
lines: 83,
|
lines: 86,
|
||||||
branches: 74,
|
branches: 83,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user