226 lines
7.0 KiB
TypeScript
226 lines
7.0 KiB
TypeScript
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 { render } from "vitest-browser-react";
|
|
|
|
import { TaxonomyManager } from "../../src/components/TaxonomyManager";
|
|
|
|
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 (
|
|
<Toasty>
|
|
<QueryClientProvider client={qc}>{children}</QueryClientProvider>
|
|
</Toasty>
|
|
);
|
|
}
|
|
|
|
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(<TaxonomyManager taxonomyName="categories" />, {
|
|
wrapper: Wrapper,
|
|
});
|
|
|
|
await expect.element(screen.getByRole("heading", { name: "Categories" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows list of terms with labels", async () => {
|
|
const screen = await render(<TaxonomyManager taxonomyName="categories" />, {
|
|
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(<TaxonomyManager taxonomyName="categories" />, {
|
|
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(<TaxonomyManager taxonomyName="categories" />, {
|
|
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(<TaxonomyManager taxonomyName="categories" />, {
|
|
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(<TaxonomyManager taxonomyName="categories" />, {
|
|
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(<TaxonomyManager taxonomyName="categories" />, {
|
|
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(<TaxonomyManager taxonomyName="categories" />, {
|
|
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(<TaxonomyManager taxonomyName="categories" />, {
|
|
wrapper: Wrapper,
|
|
});
|
|
|
|
await expect.element(screen.getByText(NO_CATEGORIES_REGEX)).toBeInTheDocument();
|
|
});
|
|
});
|