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 { Widgets } from "../../src/components/Widgets"; vi.mock("../../src/lib/api", async () => { const actual = await vi.importActual("../../src/lib/api"); return { ...actual, fetchWidgetAreas: vi.fn(), fetchWidgetComponents: vi.fn(), fetchMenus: vi.fn().mockResolvedValue([]), createWidgetArea: vi.fn().mockResolvedValue({}), deleteWidgetArea: vi.fn().mockResolvedValue(undefined), deleteWidget: vi.fn().mockResolvedValue(undefined), updateWidget: vi.fn().mockResolvedValue({}), reorderWidgets: vi.fn().mockResolvedValue(undefined), }; }); import * as api from "../../src/lib/api"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const DELETE_WIDGET_AREA_MSG_REGEX = /This will delete the widget area and all its widgets/; const ADD_WIDGET_AREA_REGEX = /Add Widget Area/; function mockDefaults() { vi.mocked(api.fetchWidgetAreas).mockResolvedValue([ { id: "a1", name: "sidebar", label: "Sidebar", description: "Main sidebar", widgets: [ { id: "w1", type: "content", title: "Recent Posts", sort_order: 0 }, { id: "w2", type: "menu", title: "Quick Links", sort_order: 1 }, ], }, ]); vi.mocked(api.fetchWidgetComponents).mockResolvedValue([ { id: "recent-posts", label: "Recent Posts Widget", description: "Shows recent posts", props: {}, }, ]); } function Wrapper({ children }: { children: React.ReactNode }) { const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, }); return ( {children} ); } describe("Widgets", () => { beforeEach(() => { vi.clearAllMocks(); mockDefaults(); }); it("displays widget areas with labels", async () => { const screen = await render(, { wrapper: Wrapper }); await expect.element(screen.getByRole("heading", { name: "Sidebar" })).toBeInTheDocument(); await expect.element(screen.getByText("Main sidebar")).toBeInTheDocument(); }); it("shows widgets within each area", async () => { const screen = await render(, { wrapper: Wrapper }); // Widget titles are rendered in elements. Use exact match to avoid // matching the "Recent Posts Widget" component in the available widgets list. await expect.element(screen.getByText("Quick Links")).toBeInTheDocument(); // Verify widget type badges await expect.element(screen.getByText("(content)")).toBeInTheDocument(); await expect.element(screen.getByText("(menu)")).toBeInTheDocument(); }); it("create area button opens dialog with name/label/description form", async () => { const screen = await render(, { wrapper: Wrapper }); await screen.getByRole("button", { name: ADD_WIDGET_AREA_REGEX }).click(); await expect .element(screen.getByRole("heading", { name: "Create Widget Area" })) .toBeInTheDocument(); await expect.element(screen.getByLabelText("Name")).toBeInTheDocument(); await expect.element(screen.getByLabelText("Label")).toBeInTheDocument(); await expect.element(screen.getByLabelText("Description")).toBeInTheDocument(); }); it("delete area shows confirmation dialog", async () => { const screen = await render(, { wrapper: Wrapper }); await expect.element(screen.getByRole("heading", { name: "Sidebar" })).toBeInTheDocument(); // The area header has a delete button next to the area label. // The WidgetAreaPanel header is a .p-4.border-b div with the label and a button. // Find the button inside the panel header (the div that contains the heading "Sidebar") const sidebarHeading = document.querySelector("h3"); expect(sidebarHeading).not.toBeNull(); // The delete button is a sibling of the div containing h3, within the .border-b parent const headerContainer = sidebarHeading!.closest(".border-b"); expect(headerContainer).not.toBeNull(); const deleteBtn = headerContainer!.querySelector("button"); expect(deleteBtn).not.toBeNull(); (deleteBtn as HTMLButtonElement).click(); await expect .element(screen.getByRole("heading", { name: "Delete Widget Area?" })) .toBeInTheDocument(); await expect.element(screen.getByText(DELETE_WIDGET_AREA_MSG_REGEX)).toBeInTheDocument(); }); it("widget expand/collapse toggles editor form", async () => { const screen = await render(, { wrapper: Wrapper }); await expect.element(screen.getByText("Quick Links")).toBeInTheDocument(); // Initially collapsed — editor form should not be visible expect(screen.getByText("Save").query()).toBeNull(); // Click the widget title area to expand (it's a