// @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"; vi.mock("../../adapters/bestiary-cache.js", () => ({ getCachedSources: vi.fn(), clearSource: vi.fn(), clearAll: vi.fn(), })); import * as bestiaryCache from "../../adapters/bestiary-cache.js"; import { SourceManager } from "../source-manager"; const mockGetCachedSources = vi.mocked(bestiaryCache.getCachedSources); const mockClearSource = vi.mocked(bestiaryCache.clearSource); const mockClearAll = vi.mocked(bestiaryCache.clearAll); afterEach(() => { cleanup(); vi.clearAllMocks(); }); describe("SourceManager", () => { it("shows 'No cached sources' empty state when no sources", async () => { mockGetCachedSources.mockResolvedValue([]); render(); await waitFor(() => { expect(screen.getByText("No cached sources")).toBeInTheDocument(); }); }); it("lists cached sources with display name and creature count", async () => { mockGetCachedSources.mockResolvedValue([ { sourceCode: "mm", displayName: "Monster Manual", creatureCount: 300, cachedAt: Date.now(), }, { sourceCode: "vgm", displayName: "Volo's Guide", creatureCount: 100, cachedAt: Date.now(), }, ]); render(); 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 calls cache clear and onCacheCleared", async () => { const user = userEvent.setup(); const onCacheCleared = vi.fn(); mockGetCachedSources .mockResolvedValueOnce([ { sourceCode: "mm", displayName: "Monster Manual", creatureCount: 300, cachedAt: Date.now(), }, ]) .mockResolvedValue([]); mockClearAll.mockResolvedValue(undefined); render(); await waitFor(() => { expect(screen.getByText("Monster Manual")).toBeInTheDocument(); }); await user.click(screen.getByRole("button", { name: "Clear All" })); await waitFor(() => { expect(mockClearAll).toHaveBeenCalled(); }); expect(onCacheCleared).toHaveBeenCalled(); }); it("individual source delete button calls clear for that source", async () => { const user = userEvent.setup(); const onCacheCleared = vi.fn(); mockGetCachedSources .mockResolvedValueOnce([ { sourceCode: "mm", displayName: "Monster Manual", creatureCount: 300, cachedAt: Date.now(), }, { sourceCode: "vgm", displayName: "Volo's Guide", creatureCount: 100, cachedAt: Date.now(), }, ]) .mockResolvedValue([ { sourceCode: "vgm", displayName: "Volo's Guide", creatureCount: 100, cachedAt: Date.now(), }, ]); mockClearSource.mockResolvedValue(undefined); render(); await waitFor(() => { expect(screen.getByText("Monster Manual")).toBeInTheDocument(); }); await user.click( screen.getByRole("button", { name: "Remove Monster Manual" }), ); await waitFor(() => { expect(mockClearSource).toHaveBeenCalledWith("mm"); }); expect(onCacheCleared).toHaveBeenCalled(); }); });