Files
initiative/apps/web/src/components/__tests__/bulk-import-prompt.test.tsx
Lukas 2c643cc98b
All checks were successful
CI / check (push) Successful in 2m13s
CI / build-image (push) Has been skipped
Introduce adapter injection and migrate test suite
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>
2026-04-01 23:55:45 +02:00

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();
});
});