Files
initiative/apps/web/src/hooks/__tests__/use-bulk-import.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

169 lines
4.8 KiB
TypeScript

// @vitest-environment jsdom
import { act, renderHook } from "@testing-library/react";
import type { ReactNode } from "react";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { createTestAdapters } from "../../__tests__/adapters/in-memory-adapters.js";
import { AllProviders } from "../../__tests__/test-providers.js";
import { useBulkImport } from "../use-bulk-import.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(),
})),
});
});
const adapters = createTestAdapters();
adapters.bestiaryIndex = {
...adapters.bestiaryIndex,
getAllSourceCodes: () => ["MM", "VGM", "XGE"],
getDefaultFetchUrl: (code: string, baseUrl?: string) =>
`${baseUrl}${code}.json`,
};
function wrapper({ children }: { children: ReactNode }) {
return <AllProviders adapters={adapters}>{children}</AllProviders>;
}
/** Flush microtasks so the internal async IIFE inside startImport settles. */
function flushMicrotasks(): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
describe("useBulkImport", () => {
it("starts in idle state with all counters at 0", () => {
const { result } = renderHook(() => useBulkImport(), { wrapper });
expect(result.current.state).toEqual({
status: "idle",
total: 0,
completed: 0,
failed: 0,
});
});
it("reset returns to idle state", async () => {
const { result } = renderHook(() => useBulkImport(), { wrapper });
const isSourceCached = vi.fn().mockResolvedValue(true);
const fetchAndCacheSource = vi.fn();
const refreshCache = vi.fn();
await act(async () => {
result.current.startImport(
"https://example.com/",
fetchAndCacheSource,
isSourceCached,
refreshCache,
);
await flushMicrotasks();
});
act(() => result.current.reset());
expect(result.current.state.status).toBe("idle");
});
it("goes straight to complete when all sources are cached", async () => {
const { result } = renderHook(() => useBulkImport(), { wrapper });
const isSourceCached = vi.fn().mockResolvedValue(true);
const fetchAndCacheSource = vi.fn();
const refreshCache = vi.fn();
await act(async () => {
result.current.startImport(
"https://example.com/",
fetchAndCacheSource,
isSourceCached,
refreshCache,
);
await flushMicrotasks();
});
expect(result.current.state.status).toBe("complete");
expect(result.current.state.completed).toBe(3);
expect(fetchAndCacheSource).not.toHaveBeenCalled();
});
it("fetches uncached sources and completes", async () => {
const { result } = renderHook(() => useBulkImport(), { wrapper });
const isSourceCached = vi.fn().mockResolvedValue(false);
const fetchAndCacheSource = vi.fn().mockResolvedValue(undefined);
const refreshCache = vi.fn().mockResolvedValue(undefined);
await act(async () => {
result.current.startImport(
"https://example.com/",
fetchAndCacheSource,
isSourceCached,
refreshCache,
);
await flushMicrotasks();
});
expect(result.current.state.status).toBe("complete");
expect(result.current.state.completed).toBe(3);
expect(result.current.state.failed).toBe(0);
expect(fetchAndCacheSource).toHaveBeenCalledTimes(3);
expect(refreshCache).toHaveBeenCalled();
});
it("reports partial-failure when some sources fail", async () => {
const { result } = renderHook(() => useBulkImport(), { wrapper });
const isSourceCached = vi.fn().mockResolvedValue(false);
const fetchAndCacheSource = vi
.fn()
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(new Error("fail"))
.mockResolvedValueOnce(undefined);
const refreshCache = vi.fn().mockResolvedValue(undefined);
await act(async () => {
result.current.startImport(
"https://example.com/",
fetchAndCacheSource,
isSourceCached,
refreshCache,
);
await flushMicrotasks();
});
expect(result.current.state.status).toBe("partial-failure");
expect(result.current.state.completed).toBe(2);
expect(result.current.state.failed).toBe(1);
expect(refreshCache).toHaveBeenCalled();
});
it("calls refreshCache after all batches complete", async () => {
const { result } = renderHook(() => useBulkImport(), { wrapper });
const isSourceCached = vi.fn().mockResolvedValue(false);
const fetchAndCacheSource = vi.fn().mockResolvedValue(undefined);
const refreshCache = vi.fn().mockResolvedValue(undefined);
await act(async () => {
result.current.startImport(
"https://example.com/",
fetchAndCacheSource,
isSourceCached,
refreshCache,
);
await flushMicrotasks();
});
expect(refreshCache).toHaveBeenCalledOnce();
});
});