Implements PF2e as an alternative game system alongside D&D 5e/5.5e. Settings modal "Game System" selector switches conditions, bestiary, stat block layout, and initiative calculation between systems. - Valued conditions with increment/decrement UX (Clumsy 2, Frightened 3) - 2,502 PF2e creatures from bundled search index (77 sources) - PF2e stat block: level, traits, Perception, Fort/Ref/Will, ability mods - Perception-based initiative rolling - System-scoped source cache (D&D and PF2e sources don't collide) - Backwards-compatible condition rehydration (ConditionId[] → ConditionEntry[]) - Difficulty indicator hidden in PF2e mode (excluded from MVP) Closes dostulata/initiative#19 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
import "@testing-library/jest-dom/vitest";
|
|
|
|
import {
|
|
type ConditionEntry,
|
|
type ConditionId,
|
|
getConditionsForEdition,
|
|
} from "@initiative/domain";
|
|
import { cleanup, render, screen } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { createRef, type RefObject } from "react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { RulesEditionProvider } from "../../contexts/index.js";
|
|
import { ConditionPicker } from "../condition-picker";
|
|
|
|
afterEach(cleanup);
|
|
|
|
function renderPicker(
|
|
overrides: Partial<{
|
|
activeConditions: readonly ConditionEntry[];
|
|
onToggle: (conditionId: ConditionId) => void;
|
|
onSetValue: (conditionId: ConditionId, value: number) => void;
|
|
onClose: () => void;
|
|
}> = {},
|
|
) {
|
|
const onToggle = overrides.onToggle ?? vi.fn();
|
|
const onSetValue = overrides.onSetValue ?? vi.fn();
|
|
const onClose = overrides.onClose ?? vi.fn();
|
|
const anchorRef = createRef<HTMLElement>() as RefObject<HTMLElement>;
|
|
const anchor = document.createElement("div");
|
|
document.body.appendChild(anchor);
|
|
(anchorRef as { current: HTMLElement }).current = anchor;
|
|
const result = render(
|
|
<RulesEditionProvider>
|
|
<ConditionPicker
|
|
anchorRef={anchorRef}
|
|
activeConditions={overrides.activeConditions ?? []}
|
|
onToggle={onToggle}
|
|
onSetValue={onSetValue}
|
|
onClose={onClose}
|
|
/>
|
|
</RulesEditionProvider>,
|
|
);
|
|
return { ...result, onToggle, onSetValue, onClose };
|
|
}
|
|
|
|
describe("ConditionPicker", () => {
|
|
it("renders edition-specific conditions from domain", () => {
|
|
renderPicker();
|
|
const editionConditions = getConditionsForEdition("5.5e");
|
|
for (const def of editionConditions) {
|
|
expect(screen.getByText(def.label)).toBeInTheDocument();
|
|
}
|
|
});
|
|
|
|
it("active conditions are visually distinguished", () => {
|
|
renderPicker({ activeConditions: [{ id: "blinded" }] });
|
|
const row = screen.getByText("Blinded").closest("div[class]");
|
|
expect(row?.className).toContain("bg-card/50");
|
|
});
|
|
|
|
it("clicking a condition calls onToggle with that condition's ID", async () => {
|
|
const user = userEvent.setup();
|
|
const { onToggle } = renderPicker();
|
|
await user.click(screen.getByText("Poisoned"));
|
|
expect(onToggle).toHaveBeenCalledWith("poisoned");
|
|
});
|
|
|
|
it("non-active conditions render with muted styling", () => {
|
|
renderPicker({ activeConditions: [] });
|
|
const label = screen.getByText("Charmed");
|
|
expect(label.className).toContain("text-muted-foreground");
|
|
});
|
|
|
|
it("active condition labels use foreground color", () => {
|
|
renderPicker({ activeConditions: [{ id: "charmed" }] });
|
|
const label = screen.getByText("Charmed");
|
|
expect(label.className).toContain("text-foreground");
|
|
});
|
|
});
|