Replace direct adapter/persistence imports with context-based injection (AdapterContext + useAdapters) so tests use in-memory implementations instead of vi.mock. Migrate component tests from context mocking to AllProviders with real hooks. Extract export/import logic from ActionBar into useEncounterExportImport hook. Add bestiary-cache and bestiary-index-adapter test suites. Raise adapter coverage thresholds (68→80 lines, 56→62 branches). 77 test files, 891 tests, all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
142 lines
3.8 KiB
TypeScript
142 lines
3.8 KiB
TypeScript
// @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 type { ReactNode } from "react";
|
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
import { createTestAdapters } from "../../__tests__/adapters/in-memory-adapters.js";
|
|
import { AllProviders } from "../../__tests__/test-providers.js";
|
|
import type { CachedSourceInfo } from "../../adapters/ports.js";
|
|
import { SourceManager } from "../source-manager.js";
|
|
|
|
beforeAll(() => {
|
|
Object.defineProperty(globalThis, "matchMedia", {
|
|
writable: true,
|
|
value: vi.fn().mockImplementation((query: string) => ({
|
|
matches: false,
|
|
media: query,
|
|
onchange: null,
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
})),
|
|
});
|
|
});
|
|
|
|
afterEach(cleanup);
|
|
|
|
function renderWithSources(sources: CachedSourceInfo[] = []) {
|
|
const adapters = createTestAdapters();
|
|
// Wire getCachedSources to return the provided sources initially,
|
|
// then empty after clear operations
|
|
let currentSources = [...sources];
|
|
adapters.bestiaryCache = {
|
|
...adapters.bestiaryCache,
|
|
getCachedSources: () => Promise.resolve(currentSources),
|
|
clearSource(sourceCode) {
|
|
currentSources = currentSources.filter(
|
|
(s) => s.sourceCode !== sourceCode,
|
|
);
|
|
return Promise.resolve();
|
|
},
|
|
clearAll() {
|
|
currentSources = [];
|
|
return Promise.resolve();
|
|
},
|
|
};
|
|
|
|
render(<SourceManager />, {
|
|
wrapper: ({ children }: { children: ReactNode }) => (
|
|
<AllProviders adapters={adapters}>{children}</AllProviders>
|
|
),
|
|
});
|
|
}
|
|
|
|
describe("SourceManager", () => {
|
|
it("shows 'No cached sources' empty state when no sources", async () => {
|
|
void renderWithSources([]);
|
|
await waitFor(() => {
|
|
expect(screen.getByText("No cached sources")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("lists cached sources with display name and creature count", async () => {
|
|
void renderWithSources([
|
|
{
|
|
sourceCode: "mm",
|
|
displayName: "Monster Manual",
|
|
creatureCount: 300,
|
|
cachedAt: Date.now(),
|
|
},
|
|
{
|
|
sourceCode: "vgm",
|
|
displayName: "Volo's Guide",
|
|
creatureCount: 100,
|
|
cachedAt: Date.now(),
|
|
},
|
|
]);
|
|
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 removes all sources", async () => {
|
|
const user = userEvent.setup();
|
|
void renderWithSources([
|
|
{
|
|
sourceCode: "mm",
|
|
displayName: "Monster Manual",
|
|
creatureCount: 300,
|
|
cachedAt: Date.now(),
|
|
},
|
|
]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Monster Manual")).toBeInTheDocument();
|
|
});
|
|
|
|
await user.click(screen.getByRole("button", { name: "Clear All" }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("No cached sources")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("individual source delete button removes that source", async () => {
|
|
const user = userEvent.setup();
|
|
void renderWithSources([
|
|
{
|
|
sourceCode: "mm",
|
|
displayName: "Monster Manual",
|
|
creatureCount: 300,
|
|
cachedAt: Date.now(),
|
|
},
|
|
{
|
|
sourceCode: "vgm",
|
|
displayName: "Volo's Guide",
|
|
creatureCount: 100,
|
|
cachedAt: Date.now(),
|
|
},
|
|
]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Monster Manual")).toBeInTheDocument();
|
|
});
|
|
|
|
await user.click(
|
|
screen.getByRole("button", { name: "Remove Monster Manual" }),
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText("Monster Manual")).not.toBeInTheDocument();
|
|
});
|
|
expect(screen.getByText("Volo's Guide")).toBeInTheDocument();
|
|
});
|
|
});
|