// @vitest-environment jsdom import "@testing-library/jest-dom/vitest"; import { cleanup, render, screen, waitFor } 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 { SourceFetchPrompt } from "../source-fetch-prompt.js"; const MONSTER_MANUAL_REGEX = /Monster Manual/; afterEach(cleanup); const mockFetchAndCacheSource = vi.fn(); const mockUploadAndCacheSource = vi.fn(); // Uses context mock because fetchAndCacheSource/uploadAndCacheSource involve // real fetch() calls. The test controls success/failure to verify the // component's loading and error UI, not the fetching logic itself. vi.mock("../../contexts/bestiary-context.js", () => ({ useBestiaryContext: () => ({ fetchAndCacheSource: mockFetchAndCacheSource, uploadAndCacheSource: mockUploadAndCacheSource, }), })); function renderPrompt(sourceCode = "MM") { const onSourceLoaded = vi.fn(); const adapters = createTestAdapters(); adapters.bestiaryIndex = { ...adapters.bestiaryIndex, getDefaultFetchUrl: (code: string) => `https://example.com/bestiary/${code}.json`, getSourceDisplayName: (code: string) => code === "MM" ? "Monster Manual" : code, }; const result = render( , ); return { ...result, onSourceLoaded }; } describe("SourceFetchPrompt", () => { afterEach(() => { vi.clearAllMocks(); }); it("renders source name, URL input, Load and Upload buttons", () => { renderPrompt(); expect(screen.getByText(MONSTER_MANUAL_REGEX)).toBeInTheDocument(); expect( screen.getByDisplayValue("https://example.com/bestiary/MM.json"), ).toBeInTheDocument(); expect(screen.getByText("Load")).toBeInTheDocument(); expect(screen.getByText("Upload file")).toBeInTheDocument(); }); it("Load calls fetchAndCacheSource and onSourceLoaded on success", async () => { mockFetchAndCacheSource.mockResolvedValueOnce({ skippedNames: [] }); const user = userEvent.setup(); const { onSourceLoaded } = renderPrompt(); await user.click(screen.getByText("Load")); await waitFor(() => { expect(mockFetchAndCacheSource).toHaveBeenCalledWith( "MM", "https://example.com/bestiary/MM.json", ); expect(onSourceLoaded).toHaveBeenCalled(); }); }); it("fetch error shows error message", async () => { mockFetchAndCacheSource.mockRejectedValueOnce(new Error("Network error")); const user = userEvent.setup(); renderPrompt(); await user.click(screen.getByText("Load")); await waitFor(() => { expect(screen.getByText("Network error")).toBeInTheDocument(); }); }); it("upload file calls uploadAndCacheSource and onSourceLoaded", async () => { mockUploadAndCacheSource.mockResolvedValueOnce(undefined); const user = userEvent.setup(); const { onSourceLoaded } = renderPrompt(); const file = new File(['{"monster":[]}'], "bestiary-mm.json", { type: "application/json", }); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; await user.upload(fileInput, file); await waitFor(() => { expect(mockUploadAndCacheSource).toHaveBeenCalledWith("MM", { monster: [], }); expect(onSourceLoaded).toHaveBeenCalled(); }); }); it("upload error shows error message", async () => { mockUploadAndCacheSource.mockRejectedValueOnce(new Error("Invalid format")); const user = userEvent.setup(); renderPrompt(); const file = new File(['{"bad": true}'], "bad.json", { type: "application/json", }); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; await user.upload(fileInput, file); await waitFor(() => { expect(screen.getByText("Invalid format")).toBeInTheDocument(); }); }); });