Files
initiative/apps/web/src/components/__tests__/bulk-import-prompt.test.tsx
Lukas e62c49434c
All checks were successful
CI / check (push) Successful in 2m21s
CI / build-image (push) Successful in 24s
Add Pathfinder 2e game system mode
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>
2026-04-07 01:26:22 +02:00

167 lines
4.7 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 { RulesEditionProvider } from "../../contexts/rules-edition-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(
<RulesEditionProvider>
<AdapterProvider adapters={adapters}>
<BulkImportPrompt />
</AdapterProvider>
</RulesEditionProvider>,
);
}
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();
});
});