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>
164 lines
4.6 KiB
TypeScript
164 lines
4.6 KiB
TypeScript
// @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 { createTestAdapters } from "../../__tests__/adapters/in-memory-adapters.js";
|
|
import { AdapterProvider } from "../../contexts/adapter-context.js";
|
|
import { BulkImportPrompt } from "../bulk-import-prompt.js";
|
|
|
|
const THREE_SOURCES_REGEX = /3 sources/;
|
|
const GITHUB_URL_REGEX = /raw\.githubusercontent/;
|
|
const LOADING_PROGRESS_REGEX = /Loading sources\.\.\. 4\/10/;
|
|
const SEVEN_OF_TEN_REGEX = /7\/10 sources/;
|
|
const THREE_FAILED_REGEX = /3 failed/;
|
|
|
|
afterEach(cleanup);
|
|
|
|
const mockFetchAndCacheSource = vi.fn();
|
|
const mockIsSourceCached = vi.fn().mockResolvedValue(false);
|
|
const mockRefreshCache = vi.fn();
|
|
const mockStartImport = vi.fn();
|
|
const mockReset = vi.fn();
|
|
const mockDismissPanel = vi.fn();
|
|
|
|
let mockImportState = {
|
|
status: "idle" as string,
|
|
total: 0,
|
|
completed: 0,
|
|
failed: 0,
|
|
};
|
|
|
|
// Uses context mocks because the bulk import state machine (idle → loading →
|
|
// complete → partial-failure) is impractical to drive through user interactions
|
|
// without real network calls. Consider migrating if adapter injection expands
|
|
// to cover these state transitions.
|
|
vi.mock("../../contexts/bestiary-context.js", () => ({
|
|
useBestiaryContext: () => ({
|
|
fetchAndCacheSource: mockFetchAndCacheSource,
|
|
isSourceCached: mockIsSourceCached,
|
|
refreshCache: mockRefreshCache,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("../../contexts/bulk-import-context.js", () => ({
|
|
useBulkImportContext: () => ({
|
|
state: mockImportState,
|
|
startImport: mockStartImport,
|
|
reset: mockReset,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("../../contexts/side-panel-context.js", () => ({
|
|
useSidePanelContext: () => ({
|
|
dismissPanel: mockDismissPanel,
|
|
}),
|
|
}));
|
|
|
|
function createAdaptersWithSources() {
|
|
const adapters = createTestAdapters();
|
|
adapters.bestiaryIndex = {
|
|
...adapters.bestiaryIndex,
|
|
getAllSourceCodes: () => ["MM", "VGM", "XGE"],
|
|
};
|
|
return adapters;
|
|
}
|
|
|
|
function renderWithAdapters() {
|
|
const adapters = createAdaptersWithSources();
|
|
return render(
|
|
<AdapterProvider adapters={adapters}>
|
|
<BulkImportPrompt />
|
|
</AdapterProvider>,
|
|
);
|
|
}
|
|
|
|
describe("BulkImportPrompt", () => {
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
mockImportState = { status: "idle", total: 0, completed: 0, failed: 0 };
|
|
});
|
|
|
|
it("idle: shows base URL input, source count, Load All button", () => {
|
|
renderWithAdapters();
|
|
expect(screen.getByText(THREE_SOURCES_REGEX)).toBeInTheDocument();
|
|
expect(screen.getByDisplayValue(GITHUB_URL_REGEX)).toBeInTheDocument();
|
|
expect(
|
|
screen.getByRole("button", { name: "Load All" }),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("idle: clearing URL disables the button", async () => {
|
|
const user = userEvent.setup();
|
|
renderWithAdapters();
|
|
|
|
const input = screen.getByDisplayValue(GITHUB_URL_REGEX);
|
|
await user.clear(input);
|
|
expect(screen.getByRole("button", { name: "Load All" })).toBeDisabled();
|
|
});
|
|
|
|
it("idle: clicking Load All calls startImport with URL", async () => {
|
|
const user = userEvent.setup();
|
|
renderWithAdapters();
|
|
|
|
await user.click(screen.getByRole("button", { name: "Load All" }));
|
|
expect(mockStartImport).toHaveBeenCalledWith(
|
|
expect.stringContaining("raw.githubusercontent"),
|
|
mockFetchAndCacheSource,
|
|
mockIsSourceCached,
|
|
mockRefreshCache,
|
|
);
|
|
});
|
|
|
|
it("loading: shows progress text and progress bar", () => {
|
|
mockImportState = {
|
|
status: "loading",
|
|
total: 10,
|
|
completed: 3,
|
|
failed: 1,
|
|
};
|
|
renderWithAdapters();
|
|
expect(screen.getByText(LOADING_PROGRESS_REGEX)).toBeInTheDocument();
|
|
});
|
|
|
|
it("complete: shows success message and Done button", () => {
|
|
mockImportState = {
|
|
status: "complete",
|
|
total: 10,
|
|
completed: 10,
|
|
failed: 0,
|
|
};
|
|
renderWithAdapters();
|
|
expect(screen.getByText("All sources loaded")).toBeInTheDocument();
|
|
expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("complete: Done calls dismissPanel and reset", async () => {
|
|
mockImportState = {
|
|
status: "complete",
|
|
total: 10,
|
|
completed: 10,
|
|
failed: 0,
|
|
};
|
|
const user = userEvent.setup();
|
|
renderWithAdapters();
|
|
|
|
await user.click(screen.getByRole("button", { name: "Done" }));
|
|
expect(mockDismissPanel).toHaveBeenCalled();
|
|
expect(mockReset).toHaveBeenCalled();
|
|
});
|
|
|
|
it("partial-failure: shows loaded/failed counts", () => {
|
|
mockImportState = {
|
|
status: "partial-failure",
|
|
total: 10,
|
|
completed: 7,
|
|
failed: 3,
|
|
};
|
|
renderWithAdapters();
|
|
expect(screen.getByText(SEVEN_OF_TEN_REGEX)).toBeInTheDocument();
|
|
expect(screen.getByText(THREE_FAILED_REGEX)).toBeInTheDocument();
|
|
});
|
|
});
|