import { Toasty } from "@cloudflare/kumo";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import * as React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { TaxonomyManager } from "../../src/components/TaxonomyManager";
import { TaxonomySidebar } from "../../src/components/TaxonomySidebar";
import { render } from "../utils/render.tsx";
vi.mock("../../src/lib/api/client.js", async () => {
const actual = await vi.importActual("../../src/lib/api/client.js");
return {
...actual,
apiFetch: vi.fn(),
};
});
import { apiFetch } from "../../src/lib/api/client.js";
const categoriesTaxonomy = {
id: "tax_categories",
name: "categories",
label: "Categories",
labelSingular: "Category",
hierarchical: true,
collections: ["posts"],
};
const terms = [
{ id: "1", name: "tech", slug: "tech", label: "Technology", parentId: null, children: [] },
];
function dataResponse(data: unknown) {
return Promise.resolve(
new Response(JSON.stringify({ data }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
}
/** Mirrors the endpoint: counts unless the caller opts out. */
function mockApiFetch() {
vi.mocked(apiFetch).mockImplementation((url: string | URL | Request, init?: RequestInit) => {
const urlString = typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url;
const { pathname, searchParams } = new URL(urlString, "http://localhost");
const method = init?.method ?? "GET";
if (method === "GET" && pathname === "/_emdash/api/taxonomies") {
return dataResponse({ taxonomies: [categoriesTaxonomy] });
}
if (method === "GET" && pathname === "/_emdash/api/taxonomies/categories/terms") {
const withCounts = searchParams.get("includeCounts") !== "false";
return dataResponse({
terms: terms.map((term) => (withCounts ? { ...term, count: 5 } : term)),
});
}
return dataResponse({});
});
}
function makeWrapper() {
// staleTime mirrors App.tsx: inside that window a mounting consumer is served
// the cached list without refetching.
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: 1000 * 60 },
mutations: { retry: false },
},
});
return function Wrapper({ children }: { children: React.ReactNode }) {
return (
{children}
);
};
}
/** The editor sidebar renders first; the settings page mounts afterwards, as it
* would when the user navigates to it in the same SPA session. */
function EditorThenSettings() {
const [settingsOpen, setSettingsOpen] = React.useState(false);
return (
<>
{settingsOpen ? : null}
>
);
}
describe("taxonomy term cache", () => {
beforeEach(() => {
vi.clearAllMocks();
mockApiFetch();
});
it("shows real counts in the manager after the editor cached a count-free list", async () => {
const screen = await render(, { wrapper: makeWrapper() });
await expect.element(screen.getByText("Technology")).toBeInTheDocument();
await screen.getByRole("button", { name: "Open settings" }).click();
await expect.element(screen.getByRole("heading", { name: "Categories" })).toBeInTheDocument();
await expect.element(screen.getByText("5", { exact: true })).toBeInTheDocument();
});
});