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>
169 lines
4.8 KiB
TypeScript
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();
|
|
});
|
|
});
|