// @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 type { ReactNode } from "react"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { createTestAdapters } from "../../__tests__/adapters/in-memory-adapters.js"; import { AllProviders } from "../../__tests__/test-providers.js"; import type { CachedSourceInfo } from "../../adapters/ports.js"; import { SourceManager } from "../source-manager.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(), })), }); }); afterEach(cleanup); function renderWithSources(sources: CachedSourceInfo[] = []) { const adapters = createTestAdapters(); // Wire getCachedSources to return the provided sources initially, // then empty after clear operations let currentSources = [...sources]; adapters.bestiaryCache = { ...adapters.bestiaryCache, getCachedSources: () => Promise.resolve(currentSources), clearSource(sourceCode) { currentSources = currentSources.filter( (s) => s.sourceCode !== sourceCode, ); return Promise.resolve(); }, clearAll() { currentSources = []; return Promise.resolve(); }, }; render(, { wrapper: ({ children }: { children: ReactNode }) => ( {children} ), }); } describe("SourceManager", () => { it("shows 'No cached sources' empty state when no sources", async () => { void renderWithSources([]); await waitFor(() => { expect(screen.getByText("No cached sources")).toBeInTheDocument(); }); }); it("lists cached sources with display name and creature count", async () => { void renderWithSources([ { sourceCode: "mm", displayName: "Monster Manual", creatureCount: 300, cachedAt: Date.now(), }, { sourceCode: "vgm", displayName: "Volo's Guide", creatureCount: 100, cachedAt: Date.now(), }, ]); await waitFor(() => { expect(screen.getByText("Monster Manual")).toBeInTheDocument(); }); expect(screen.getByText("300 creatures")).toBeInTheDocument(); expect(screen.getByText("Volo's Guide")).toBeInTheDocument(); expect(screen.getByText("100 creatures")).toBeInTheDocument(); }); it("Clear All button removes all sources", async () => { const user = userEvent.setup(); void renderWithSources([ { sourceCode: "mm", displayName: "Monster Manual", creatureCount: 300, cachedAt: Date.now(), }, ]); await waitFor(() => { expect(screen.getByText("Monster Manual")).toBeInTheDocument(); }); await user.click(screen.getByRole("button", { name: "Clear All" })); await waitFor(() => { expect(screen.getByText("No cached sources")).toBeInTheDocument(); }); }); it("individual source delete button removes that source", async () => { const user = userEvent.setup(); void renderWithSources([ { sourceCode: "mm", displayName: "Monster Manual", creatureCount: 300, cachedAt: Date.now(), }, { sourceCode: "vgm", displayName: "Volo's Guide", creatureCount: 100, cachedAt: Date.now(), }, ]); await waitFor(() => { expect(screen.getByText("Monster Manual")).toBeInTheDocument(); }); await user.click( screen.getByRole("button", { name: "Remove Monster Manual" }), ); await waitFor(() => { expect(screen.queryByText("Monster Manual")).not.toBeInTheDocument(); }); expect(screen.getByText("Volo's Guide")).toBeInTheDocument(); }); });