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 { 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"; interface TestTaxonomy { id: string; name: string; label: string; labelSingular?: string; hierarchical: boolean; collections: string[]; } interface TestTerm { id: string; name: string; slug: string; label: string; parentId?: string | null; children: TestTerm[]; } const tagsTaxonomy: TestTaxonomy = { id: "tax_tags", name: "tags", label: "Tags", labelSingular: "Tag", hierarchical: false, collections: ["products"], }; const categoriesTaxonomy: TestTaxonomy = { id: "tax_categories", name: "categories", label: "Categories", labelSingular: "Category", hierarchical: true, collections: ["products"], }; const alphaTerm = makeTerm("term_alpha", "Alpha"); const betaTerm = makeTerm("term_beta", "Beta"); function makeTerm(id: string, label: string): TestTerm { return { id, name: label.toLowerCase(), slug: label.toLowerCase(), label, parentId: null, children: [], }; } function dataResponse(data: unknown) { return Promise.resolve( new Response(JSON.stringify({ data }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); } function mockApiFetch({ taxonomies = [tagsTaxonomy], terms = [alphaTerm, betaTerm], entryTerms = [], }: { taxonomies?: TestTaxonomy[]; terms?: TestTerm[]; entryTerms?: TestTerm[]; } = {}) { vi.mocked(apiFetch).mockImplementation((url: string | URL | Request, init?: RequestInit) => { const urlString = typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url; const method = init?.method ?? "GET"; if (method === "GET" && urlString === "/_emdash/api/taxonomies") { return dataResponse({ taxonomies }); } if (method === "GET" && urlString === "/_emdash/api/taxonomies/tags/terms") { return dataResponse({ terms }); } if (method === "GET" && urlString === "/_emdash/api/taxonomies/categories/terms") { return dataResponse({ terms }); } if (method === "GET" && urlString === "/_emdash/api/content/products/entry_1/terms/tags") { return dataResponse({ terms: entryTerms }); } return dataResponse({}); }); } function Wrapper({ children }: { children: React.ReactNode }) { const queryClient = React.useMemo( () => new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, }), [], ); return ( {children} ); } describe("TaxonomySidebar", () => { beforeEach(() => { vi.clearAllMocks(); mockApiFetch(); }); it("shows existing flat taxonomy terms when the tag picker receives focus", async () => { const screen = await render(, { wrapper: Wrapper }); await expect.element(screen.getByLabelText("Add Tags")).toBeInTheDocument(); expect(screen.getByRole("button", { name: /^Alpha$/ }).query()).toBeNull(); await screen.getByLabelText("Add Tags").click(); await expect.element(screen.getByRole("button", { name: /^Alpha$/ })).toBeInTheDocument(); await expect.element(screen.getByRole("button", { name: /^Beta$/ })).toBeInTheDocument(); }); it("filters flat taxonomy terms while preserving the create option for new input", async () => { const screen = await render(, { wrapper: Wrapper }); const input = screen.getByLabelText("Add Tags"); await input.fill("Alp"); await expect.element(screen.getByRole("button", { name: /^Alpha$/ })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /^Beta$/ }).query()).toBeNull(); await expect.element(screen.getByText('Create "Alp"')).toBeInTheDocument(); }); it("does not suggest terms already assigned to the entry", async () => { mockApiFetch({ entryTerms: [alphaTerm] }); const screen = await render(, { wrapper: Wrapper, }); await expect.element(screen.getByLabelText("Remove Alpha")).toBeInTheDocument(); await screen.getByLabelText("Add Tags").click(); expect(screen.getByRole("button", { name: /^Alpha$/ }).query()).toBeNull(); await expect.element(screen.getByRole("button", { name: /^Beta$/ })).toBeInTheDocument(); }); it("keeps the create prompt available when no flat taxonomy terms exist", async () => { mockApiFetch({ terms: [] }); const screen = await render(, { wrapper: Wrapper }); const input = screen.getByLabelText("Add Tags"); await input.click(); expect(screen.getByText('Create "Gamma"').query()).toBeNull(); await input.fill("Gamma"); await expect.element(screen.getByText('Create "Gamma"')).toBeInTheDocument(); }); it("continues to render hierarchical taxonomies as a checkbox tree", async () => { mockApiFetch({ taxonomies: [categoriesTaxonomy], terms: [alphaTerm] }); const screen = await render(, { wrapper: Wrapper }); await expect.element(screen.getByText("Categories")).toBeInTheDocument(); await expect.element(screen.getByText("Alpha")).toBeInTheDocument(); expect(screen.getByLabelText("Add Categories").query()).toBeNull(); }); });