// @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();
});
});