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 { render } from "../utils/render.tsx"; const taxonomyResponse = JSON.stringify({ data: { taxonomies: [ { id: "t1", name: "categories", label: "Categories", labelSingular: "Category", hierarchical: true, collections: ["posts"], }, ], }, }); const termsResponse = JSON.stringify({ data: { terms: [ { id: "1", name: "tech", slug: "tech", label: "Technology", parentId: null, children: [], count: 5, }, { id: "2", name: "science", slug: "science", label: "Science", parentId: null, children: [], count: 3, }, ], }, }); 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"; function mockApiFetch(overrideTerms?: string) { vi.mocked(apiFetch).mockImplementation((url: string, init?: RequestInit) => { const urlStr = typeof url === "string" ? url : ""; if (urlStr.includes("/terms") && (!init || !init.method || init.method === "GET")) { return Promise.resolve( new Response(overrideTerms ?? termsResponse, { status: 200, headers: { "Content-Type": "application/json" }, }), ); } if (urlStr.includes("/taxonomies") && (!init || !init.method || init.method === "GET")) { return Promise.resolve( new Response(taxonomyResponse, { status: 200, headers: { "Content-Type": "application/json" }, }), ); } return Promise.resolve( new Response(JSON.stringify({ data: { success: true } }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); }); } function Wrapper({ children }: { children: React.ReactNode }) { const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, }); return ( {children} ); } const ADD_CATEGORY_BUTTON_REGEX = /Add Category/; const ADD_CATEGORY_HEADING_REGEX = /Add Category/; const EDIT_CATEGORY_HEADING_REGEX = /Edit Category/; const PARENT_SELECTOR_REGEX = /Parent/; const NO_CATEGORIES_REGEX = /No categories yet/; const DELETE_CATEGORY_HEADING_REGEX = /Delete Category/i; const DELETE_TECHNOLOGY_DESC_REGEX = /permanently delete "Technology"/; describe("TaxonomyManager", () => { beforeEach(() => { vi.clearAllMocks(); mockApiFetch(); }); it("displays taxonomy name as heading", async () => { const screen = await render(, { wrapper: Wrapper, }); await expect.element(screen.getByRole("heading", { name: "Categories" })).toBeInTheDocument(); }); it("shows list of terms with labels", async () => { const screen = await render(, { wrapper: Wrapper, }); // Use locators that target the specific label spans (font-medium class) await expect.element(screen.getByText("Technology", { exact: true })).toBeInTheDocument(); // "Science" also appears in "(science)" slug, so target the font-medium span await expect.element(screen.getByText("(science)")).toBeInTheDocument(); }); it("shows term slugs in parentheses", async () => { const screen = await render(, { wrapper: Wrapper, }); await expect.element(screen.getByText("(tech)")).toBeInTheDocument(); await expect.element(screen.getByText("(science)")).toBeInTheDocument(); }); it("add button opens create dialog", async () => { const screen = await render(, { wrapper: Wrapper, }); // Wait for content to load, then click the button await expect.element(screen.getByRole("heading", { name: "Categories" })).toBeInTheDocument(); await screen.getByRole("button", { name: ADD_CATEGORY_BUTTON_REGEX }).click(); // Verify the dialog heading opened await expect .element(screen.getByRole("heading", { name: ADD_CATEGORY_HEADING_REGEX })) .toBeInTheDocument(); }); it("create dialog has name, slug, and description inputs", async () => { const screen = await render(, { wrapper: Wrapper, }); await expect.element(screen.getByRole("heading", { name: "Categories" })).toBeInTheDocument(); await screen.getByRole("button", { name: ADD_CATEGORY_BUTTON_REGEX }).click(); await expect.element(screen.getByLabelText("Name")).toBeInTheDocument(); await expect.element(screen.getByLabelText("Slug")).toBeInTheDocument(); // The InputArea uses "Description (optional)" as label await expect.element(screen.getByText("Description (optional)")).toBeInTheDocument(); }); it("shows parent selector for hierarchical taxonomies", async () => { const screen = await render(, { wrapper: Wrapper, }); await expect.element(screen.getByRole("heading", { name: "Categories" })).toBeInTheDocument(); await screen.getByRole("button", { name: ADD_CATEGORY_BUTTON_REGEX }).click(); await expect.element(screen.getByLabelText(PARENT_SELECTOR_REGEX)).toBeInTheDocument(); }); it("edit button opens dialog", async () => { const screen = await render(, { wrapper: Wrapper, }); await expect.element(screen.getByText("Technology", { exact: true })).toBeInTheDocument(); await screen.getByRole("button", { name: "Edit Technology" }).click(); // Should open the edit dialog with "Edit Category" heading await expect .element(screen.getByRole("heading", { name: EDIT_CATEGORY_HEADING_REGEX })) .toBeInTheDocument(); }); it("delete button opens confirm dialog", async () => { const screen = await render(, { wrapper: Wrapper, }); await expect.element(screen.getByText("Technology", { exact: true })).toBeInTheDocument(); await screen.getByRole("button", { name: "Delete Technology" }).click(); // Should open a ConfirmDialog (not window.confirm) await expect .element(screen.getByRole("heading", { name: DELETE_CATEGORY_HEADING_REGEX })) .toBeInTheDocument(); await expect.element(screen.getByText(DELETE_TECHNOLOGY_DESC_REGEX)).toBeInTheDocument(); await expect.element(screen.getByRole("button", { name: "Delete" })).toBeInTheDocument(); await expect.element(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument(); }); it("shows empty state when no terms", async () => { mockApiFetch(JSON.stringify({ data: { terms: [] } })); const screen = await render(, { wrapper: Wrapper, }); await expect.element(screen.getByText(NO_CATEGORIES_REGEX)).toBeInTheDocument(); }); });