Add test coverage for 5 components: HpAdjustPopover, ConditionPicker, CombatantRow, ActionBar, SourceManager
Adds aria-label attributes to HP placeholder and source delete buttons for both accessibility and testability. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
|
||||
88
apps/web/src/components/__tests__/action-bar.test.tsx
Normal file
88
apps/web/src/components/__tests__/action-bar.test.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
// @vitest-environment jsdom
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ActionBar } from "../action-bar";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const defaultProps = {
|
||||
onAddCombatant: vi.fn(),
|
||||
onAddFromBestiary: vi.fn(),
|
||||
bestiarySearch: () => [],
|
||||
bestiaryLoaded: false,
|
||||
};
|
||||
|
||||
function renderBar(overrides: Partial<Parameters<typeof ActionBar>[0]> = {}) {
|
||||
const props = { ...defaultProps, ...overrides };
|
||||
return render(<ActionBar {...props} />);
|
||||
}
|
||||
|
||||
describe("ActionBar", () => {
|
||||
it("renders input with placeholder '+ Add combatants'", () => {
|
||||
renderBar();
|
||||
expect(screen.getByPlaceholderText("+ Add combatants")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("submitting with a name calls onAddCombatant", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onAddCombatant = vi.fn();
|
||||
renderBar({ onAddCombatant });
|
||||
const input = screen.getByPlaceholderText("+ Add combatants");
|
||||
await user.type(input, "Goblin");
|
||||
// The Add button appears when name >= 2 chars and no suggestions
|
||||
const addButton = screen.getByRole("button", { name: "Add" });
|
||||
await user.click(addButton);
|
||||
expect(onAddCombatant).toHaveBeenCalledWith("Goblin", undefined);
|
||||
});
|
||||
|
||||
it("submitting with empty name does nothing", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onAddCombatant = vi.fn();
|
||||
renderBar({ onAddCombatant });
|
||||
// Submit the form directly (Enter on empty input)
|
||||
const input = screen.getByPlaceholderText("+ Add combatants");
|
||||
await user.type(input, "{Enter}");
|
||||
expect(onAddCombatant).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows custom fields (Init, AC, MaxHP) when name >= 2 chars and no bestiary suggestions", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderBar();
|
||||
const input = screen.getByPlaceholderText("+ Add combatants");
|
||||
await user.type(input, "Go");
|
||||
expect(screen.getByPlaceholderText("Init")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("AC")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("MaxHP")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows Add button when name >= 2 chars and no suggestions", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderBar();
|
||||
const input = screen.getByPlaceholderText("+ Add combatants");
|
||||
await user.type(input, "Go");
|
||||
expect(screen.getByRole("button", { name: "Add" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows roll all initiative button when showRollAllInitiative is true", () => {
|
||||
const onRollAllInitiative = vi.fn();
|
||||
renderBar({ showRollAllInitiative: true, onRollAllInitiative });
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Roll all initiative" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("roll all initiative button is disabled when rollAllInitiativeDisabled is true", () => {
|
||||
const onRollAllInitiative = vi.fn();
|
||||
renderBar({
|
||||
showRollAllInitiative: true,
|
||||
onRollAllInitiative,
|
||||
rollAllInitiativeDisabled: true,
|
||||
});
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Roll all initiative" }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
});
|
||||
164
apps/web/src/components/__tests__/combatant-row.test.tsx
Normal file
164
apps/web/src/components/__tests__/combatant-row.test.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
// @vitest-environment jsdom
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
import { combatantId } 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 { CombatantRow } from "../combatant-row";
|
||||
import { PLAYER_COLOR_HEX } from "../player-icon-map";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const defaultProps = {
|
||||
onRename: vi.fn(),
|
||||
onSetInitiative: vi.fn(),
|
||||
onRemove: vi.fn(),
|
||||
onSetHp: vi.fn(),
|
||||
onAdjustHp: vi.fn(),
|
||||
onSetAc: vi.fn(),
|
||||
onToggleCondition: vi.fn(),
|
||||
onToggleConcentration: vi.fn(),
|
||||
};
|
||||
|
||||
function renderRow(
|
||||
overrides: Partial<{
|
||||
combatant: Parameters<typeof CombatantRow>[0]["combatant"];
|
||||
isActive: boolean;
|
||||
onRollInitiative: (id: ReturnType<typeof combatantId>) => void;
|
||||
onRemove: (id: ReturnType<typeof combatantId>) => void;
|
||||
onShowStatBlock: () => void;
|
||||
}> = {},
|
||||
) {
|
||||
const combatant = overrides.combatant ?? {
|
||||
id: combatantId("1"),
|
||||
name: "Goblin",
|
||||
initiative: 15,
|
||||
maxHp: 10,
|
||||
currentHp: 10,
|
||||
ac: 13,
|
||||
};
|
||||
const props = {
|
||||
...defaultProps,
|
||||
combatant,
|
||||
isActive: overrides.isActive ?? false,
|
||||
onRollInitiative: overrides.onRollInitiative,
|
||||
onShowStatBlock: overrides.onShowStatBlock,
|
||||
onRemove: overrides.onRemove ?? defaultProps.onRemove,
|
||||
};
|
||||
return render(<CombatantRow {...props} />);
|
||||
}
|
||||
|
||||
describe("CombatantRow", () => {
|
||||
it("renders combatant name", () => {
|
||||
renderRow();
|
||||
expect(screen.getByText("Goblin")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders initiative value", () => {
|
||||
renderRow();
|
||||
expect(screen.getByText("15")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders current HP", () => {
|
||||
renderRow({
|
||||
combatant: {
|
||||
id: combatantId("1"),
|
||||
name: "Goblin",
|
||||
maxHp: 10,
|
||||
currentHp: 7,
|
||||
},
|
||||
});
|
||||
expect(screen.getByText("7")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("active combatant gets active border styling", () => {
|
||||
const { container } = renderRow({ isActive: true });
|
||||
const row = container.firstElementChild;
|
||||
expect(row?.className).toContain("border-l-accent");
|
||||
});
|
||||
|
||||
it("unconscious combatant (currentHp === 0) gets dimmed styling", () => {
|
||||
renderRow({
|
||||
combatant: {
|
||||
id: combatantId("1"),
|
||||
name: "Goblin",
|
||||
maxHp: 10,
|
||||
currentHp: 0,
|
||||
},
|
||||
});
|
||||
// The name area should have opacity-50
|
||||
const nameEl = screen.getByText("Goblin");
|
||||
const nameContainer = nameEl.closest(".opacity-50");
|
||||
expect(nameContainer).not.toBeNull();
|
||||
});
|
||||
|
||||
it("shows '--' for current HP when no maxHp is set", () => {
|
||||
renderRow({
|
||||
combatant: {
|
||||
id: combatantId("1"),
|
||||
name: "Goblin",
|
||||
},
|
||||
});
|
||||
expect(screen.getByLabelText("No HP set")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows concentration icon when isConcentrating is true", () => {
|
||||
renderRow({
|
||||
combatant: {
|
||||
id: combatantId("1"),
|
||||
name: "Goblin",
|
||||
isConcentrating: true,
|
||||
},
|
||||
});
|
||||
const concButton = screen.getByRole("button", {
|
||||
name: "Toggle concentration",
|
||||
});
|
||||
expect(concButton.className).toContain("text-purple-400");
|
||||
});
|
||||
|
||||
it("shows player character icon and color when set", () => {
|
||||
const { container } = renderRow({
|
||||
combatant: {
|
||||
id: combatantId("1"),
|
||||
name: "Aragorn",
|
||||
color: "red",
|
||||
icon: "sword",
|
||||
},
|
||||
});
|
||||
// The icon should be rendered with the player color
|
||||
const svgIcon = container.querySelector("svg[style]");
|
||||
expect(svgIcon).not.toBeNull();
|
||||
expect(svgIcon).toHaveStyle({ color: PLAYER_COLOR_HEX.red });
|
||||
});
|
||||
|
||||
it("remove button calls onRemove after confirmation", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRemove = vi.fn();
|
||||
renderRow({ onRemove });
|
||||
const removeBtn = screen.getByRole("button", {
|
||||
name: "Remove combatant",
|
||||
});
|
||||
// First click enters confirm state
|
||||
await user.click(removeBtn);
|
||||
// Second click confirms
|
||||
const confirmBtn = screen.getByRole("button", {
|
||||
name: "Confirm remove combatant",
|
||||
});
|
||||
await user.click(confirmBtn);
|
||||
expect(onRemove).toHaveBeenCalledWith(combatantId("1"));
|
||||
});
|
||||
|
||||
it("shows d20 roll button when initiative is undefined and onRollInitiative is provided", () => {
|
||||
renderRow({
|
||||
combatant: {
|
||||
id: combatantId("1"),
|
||||
name: "Goblin",
|
||||
},
|
||||
onRollInitiative: vi.fn(),
|
||||
});
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Roll initiative" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
63
apps/web/src/components/__tests__/condition-picker.test.tsx
Normal file
63
apps/web/src/components/__tests__/condition-picker.test.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
// @vitest-environment jsdom
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
import { CONDITION_DEFINITIONS, 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 { ConditionPicker } from "../condition-picker";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
function renderPicker(
|
||||
overrides: Partial<{
|
||||
activeConditions: readonly ConditionId[];
|
||||
onToggle: (conditionId: ConditionId) => void;
|
||||
onClose: () => void;
|
||||
}> = {},
|
||||
) {
|
||||
const onToggle = overrides.onToggle ?? vi.fn();
|
||||
const onClose = overrides.onClose ?? vi.fn();
|
||||
const result = render(
|
||||
<ConditionPicker
|
||||
activeConditions={overrides.activeConditions ?? []}
|
||||
onToggle={onToggle}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
);
|
||||
return { ...result, onToggle, onClose };
|
||||
}
|
||||
|
||||
describe("ConditionPicker", () => {
|
||||
it("renders all condition definitions from domain", () => {
|
||||
renderPicker();
|
||||
for (const def of CONDITION_DEFINITIONS) {
|
||||
expect(screen.getByText(def.label)).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it("active conditions are visually distinguished", () => {
|
||||
renderPicker({ activeConditions: ["blinded"] });
|
||||
const blindedButton = screen.getByText("Blinded").closest("button");
|
||||
expect(blindedButton?.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: ["charmed"] });
|
||||
const label = screen.getByText("Charmed");
|
||||
expect(label.className).toContain("text-foreground");
|
||||
});
|
||||
});
|
||||
115
apps/web/src/components/__tests__/hp-adjust-popover.test.tsx
Normal file
115
apps/web/src/components/__tests__/hp-adjust-popover.test.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
// @vitest-environment jsdom
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { HpAdjustPopover } from "../hp-adjust-popover";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
function renderPopover(
|
||||
overrides: Partial<{
|
||||
onAdjust: (delta: number) => void;
|
||||
onClose: () => void;
|
||||
}> = {},
|
||||
) {
|
||||
const onAdjust = overrides.onAdjust ?? vi.fn();
|
||||
const onClose = overrides.onClose ?? vi.fn();
|
||||
const result = render(
|
||||
<HpAdjustPopover onAdjust={onAdjust} onClose={onClose} />,
|
||||
);
|
||||
return { ...result, onAdjust, onClose };
|
||||
}
|
||||
|
||||
describe("HpAdjustPopover", () => {
|
||||
it("renders input with placeholder 'HP'", () => {
|
||||
renderPopover();
|
||||
expect(screen.getByPlaceholderText("HP")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("damage and heal buttons are disabled when input is empty", () => {
|
||||
renderPopover();
|
||||
expect(screen.getByRole("button", { name: "Apply damage" })).toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Apply healing" }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it("damage and heal buttons are disabled when input is '0'", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPopover();
|
||||
await user.type(screen.getByPlaceholderText("HP"), "0");
|
||||
expect(screen.getByRole("button", { name: "Apply damage" })).toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Apply healing" }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it("typing a valid number enables both buttons", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPopover();
|
||||
await user.type(screen.getByPlaceholderText("HP"), "5");
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Apply damage" }),
|
||||
).not.toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Apply healing" }),
|
||||
).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it("clicking damage button calls onAdjust with negative value and onClose", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { onAdjust, onClose } = renderPopover();
|
||||
await user.type(screen.getByPlaceholderText("HP"), "7");
|
||||
await user.click(screen.getByRole("button", { name: "Apply damage" }));
|
||||
expect(onAdjust).toHaveBeenCalledWith(-7);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clicking heal button calls onAdjust with positive value and onClose", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { onAdjust, onClose } = renderPopover();
|
||||
await user.type(screen.getByPlaceholderText("HP"), "3");
|
||||
await user.click(screen.getByRole("button", { name: "Apply healing" }));
|
||||
expect(onAdjust).toHaveBeenCalledWith(3);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Enter key applies damage (negative)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { onAdjust, onClose } = renderPopover();
|
||||
const input = screen.getByPlaceholderText("HP");
|
||||
await user.type(input, "4");
|
||||
await user.keyboard("{Enter}");
|
||||
expect(onAdjust).toHaveBeenCalledWith(-4);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Shift+Enter applies healing (positive)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { onAdjust, onClose } = renderPopover();
|
||||
const input = screen.getByPlaceholderText("HP");
|
||||
await user.type(input, "6");
|
||||
await user.keyboard("{Shift>}{Enter}{/Shift}");
|
||||
expect(onAdjust).toHaveBeenCalledWith(6);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Escape key calls onClose", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { onClose } = renderPopover();
|
||||
const input = screen.getByPlaceholderText("HP");
|
||||
await user.type(input, "2");
|
||||
await user.keyboard("{Escape}");
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("only accepts digit characters in input", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPopover();
|
||||
const input = screen.getByPlaceholderText("HP");
|
||||
await user.type(input, "12abc34");
|
||||
expect(input).toHaveValue("1234");
|
||||
});
|
||||
});
|
||||
127
apps/web/src/components/__tests__/source-manager.test.tsx
Normal file
127
apps/web/src/components/__tests__/source-manager.test.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
// @vitest-environment jsdom
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("../../adapters/bestiary-cache.js", () => ({
|
||||
getCachedSources: vi.fn(),
|
||||
clearSource: vi.fn(),
|
||||
clearAll: vi.fn(),
|
||||
}));
|
||||
|
||||
import * as bestiaryCache from "../../adapters/bestiary-cache.js";
|
||||
import { SourceManager } from "../source-manager";
|
||||
|
||||
const mockGetCachedSources = vi.mocked(bestiaryCache.getCachedSources);
|
||||
const mockClearSource = vi.mocked(bestiaryCache.clearSource);
|
||||
const mockClearAll = vi.mocked(bestiaryCache.clearAll);
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("SourceManager", () => {
|
||||
it("shows 'No cached sources' empty state when no sources", async () => {
|
||||
mockGetCachedSources.mockResolvedValue([]);
|
||||
render(<SourceManager onCacheCleared={vi.fn()} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("No cached sources")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("lists cached sources with display name and creature count", async () => {
|
||||
mockGetCachedSources.mockResolvedValue([
|
||||
{
|
||||
sourceCode: "mm",
|
||||
displayName: "Monster Manual",
|
||||
creatureCount: 300,
|
||||
cachedAt: Date.now(),
|
||||
},
|
||||
{
|
||||
sourceCode: "vgm",
|
||||
displayName: "Volo's Guide",
|
||||
creatureCount: 100,
|
||||
cachedAt: Date.now(),
|
||||
},
|
||||
]);
|
||||
render(<SourceManager onCacheCleared={vi.fn()} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Monster Manual")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("300 creatures")).toBeInTheDocument();
|
||||
expect(screen.getByText("Volo's Guide")).toBeInTheDocument();
|
||||
expect(screen.getByText("100 creatures")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Clear All button calls cache clear and onCacheCleared", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCacheCleared = vi.fn();
|
||||
mockGetCachedSources
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
sourceCode: "mm",
|
||||
displayName: "Monster Manual",
|
||||
creatureCount: 300,
|
||||
cachedAt: Date.now(),
|
||||
},
|
||||
])
|
||||
.mockResolvedValue([]);
|
||||
mockClearAll.mockResolvedValue(undefined);
|
||||
render(<SourceManager onCacheCleared={onCacheCleared} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Monster Manual")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Clear All" }));
|
||||
await waitFor(() => {
|
||||
expect(mockClearAll).toHaveBeenCalled();
|
||||
});
|
||||
expect(onCacheCleared).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("individual source delete button calls clear for that source", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCacheCleared = vi.fn();
|
||||
mockGetCachedSources
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
sourceCode: "mm",
|
||||
displayName: "Monster Manual",
|
||||
creatureCount: 300,
|
||||
cachedAt: Date.now(),
|
||||
},
|
||||
{
|
||||
sourceCode: "vgm",
|
||||
displayName: "Volo's Guide",
|
||||
creatureCount: 100,
|
||||
cachedAt: Date.now(),
|
||||
},
|
||||
])
|
||||
.mockResolvedValue([
|
||||
{
|
||||
sourceCode: "vgm",
|
||||
displayName: "Volo's Guide",
|
||||
creatureCount: 100,
|
||||
cachedAt: Date.now(),
|
||||
},
|
||||
]);
|
||||
mockClearSource.mockResolvedValue(undefined);
|
||||
|
||||
render(<SourceManager onCacheCleared={onCacheCleared} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Monster Manual")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "Remove Monster Manual" }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockClearSource).toHaveBeenCalledWith("mm");
|
||||
});
|
||||
expect(onCacheCleared).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -231,6 +231,8 @@ function ClickableHp({
|
||||
"inline-block h-7 w-[4ch] text-center text-muted-foreground text-sm tabular-nums leading-7",
|
||||
dimmed && "opacity-50",
|
||||
)}
|
||||
role="status"
|
||||
aria-label="No HP set"
|
||||
>
|
||||
--
|
||||
</span>
|
||||
|
||||
@@ -88,6 +88,7 @@ export function SourceManager({
|
||||
type="button"
|
||||
onClick={() => handleClearSource(source.sourceCode)}
|
||||
className="rounded-md p-1 text-muted-foreground transition-colors hover:bg-hover-destructive-bg hover:text-hover-destructive"
|
||||
aria-label={`Remove ${source.displayName}`}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
@@ -78,6 +78,9 @@ importers:
|
||||
'@testing-library/react':
|
||||
specifier: ^16.3.2
|
||||
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@testing-library/user-event':
|
||||
specifier: ^14.6.1
|
||||
version: 14.6.1(@testing-library/dom@10.4.1)
|
||||
'@types/react':
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.14
|
||||
@@ -1027,6 +1030,12 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@testing-library/user-event@14.6.1':
|
||||
resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
peerDependencies:
|
||||
'@testing-library/dom': '>=7.21.4'
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||
|
||||
@@ -3031,6 +3040,10 @@ snapshots:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||
|
||||
'@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
|
||||
dependencies:
|
||||
'@testing-library/dom': 10.4.1
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
Reference in New Issue
Block a user