// @vitest-environment jsdom import { act } from "react"; import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { PluginPage } from "./PluginPage"; const mockPluginsApi = vi.hoisted(() => ({ listUiContributions: vi.fn(), })); const mockSetBreadcrumbs = vi.hoisted(() => vi.fn()); const mockParams = vi.hoisted(() => ({ companyPrefix: "PAP" as string | undefined, pluginId: undefined as string | undefined, pluginRoutePath: undefined as string | undefined, "*": undefined as string | undefined, })); vi.mock("@/api/plugins", () => ({ pluginsApi: mockPluginsApi, })); vi.mock("@/context/BreadcrumbContext", () => ({ useBreadcrumbs: () => ({ setBreadcrumbs: mockSetBreadcrumbs, }), })); vi.mock("@/context/CompanyContext", () => ({ useCompany: () => ({ companies: [{ id: "company-1", name: "Paperclip", issuePrefix: "PAP" }], selectedCompanyId: "company-1", }), })); vi.mock("@/lib/router", () => ({ Link: ({ to, children }: { to: string; children: React.ReactNode }) => {children}, Navigate: () => null, useParams: () => mockParams, })); vi.mock("@/plugins/slots", async () => { const actual = await vi.importActual("@/plugins/slots"); return { resolveRouteSidebarSlot: actual.resolveRouteSidebarSlot, PluginSlotMount: ({ slot }: { slot: { displayName: string } }) => (
{slot.displayName}
), }; }); // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; async function flushReact() { await act(async () => { await Promise.resolve(); await new Promise((resolve) => window.setTimeout(resolve, 0)); }); } function pageContribution(overrides: Partial<{ slots: unknown[] }> = {}) { return { pluginId: "plugin-wiki", pluginKey: "paperclipai.plugin-llm-wiki", displayName: "LLM Wiki", version: "0.1.0", uiEntryFile: "ui.js", slots: [ { type: "page", id: "wiki-page", displayName: "Wiki", exportName: "WikiPage", routePath: "wiki", }, ], launchers: [], ...overrides, }; } async function renderPage(container: HTMLDivElement) { const root = createRoot(container); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); await act(async () => { root.render( , ); }); await flushReact(); await flushReact(); return root; } describe("PluginPage", () => { let container: HTMLDivElement; beforeEach(() => { container = document.createElement("div"); document.body.appendChild(container); mockParams.companyPrefix = "PAP"; mockParams.pluginId = undefined; mockParams.pluginRoutePath = undefined; mockParams["*"] = undefined; }); afterEach(() => { container.remove(); document.body.innerHTML = ""; vi.clearAllMocks(); }); it("renders the breadcrumb and Back button on a legacy plugin route (no routeSidebar)", async () => { mockParams.pluginRoutePath = "wiki"; mockPluginsApi.listUiContributions.mockResolvedValue([pageContribution()]); const root = await renderPage(container); expect(mockSetBreadcrumbs).toHaveBeenCalledWith([ { label: "Plugins", href: "/instance/settings/plugins" }, { label: "LLM Wiki" }, ]); expect(container.textContent).toContain("Back"); expect(container.querySelector('a[href="/PAP/dashboard"]')).not.toBeNull(); await act(async () => { root.unmount(); }); }); it("uses a route title and hides the Back button when a routeSidebar matches the active route", async () => { mockParams.pluginRoutePath = "wiki"; mockPluginsApi.listUiContributions.mockResolvedValue([ pageContribution({ slots: [ { type: "page", id: "wiki-page", displayName: "Wiki", exportName: "WikiPage", routePath: "wiki", }, { type: "routeSidebar", id: "wiki-sidebar", displayName: "Wiki Sidebar", exportName: "WikiRouteSidebar", routePath: "wiki", }, ], }), ]); const root = await renderPage(container); expect(mockSetBreadcrumbs).toHaveBeenCalledWith([{ label: "Wiki" }]); expect(container.textContent).not.toContain("Back"); expect(container.querySelector('a[href="/PAP/dashboard"]')).toBeNull(); // Page slot itself still renders. expect(container.querySelector('[data-testid="plugin-slot-mount"]')?.textContent).toBe("Wiki"); await act(async () => { root.unmount(); }); }); it("uses the selected plugin page path as the route-sidebar title", async () => { mockParams.pluginRoutePath = "wiki"; mockParams["*"] = "page/templates%3A%3Aindex.md"; mockPluginsApi.listUiContributions.mockResolvedValue([ pageContribution({ slots: [ { type: "page", id: "wiki-page", displayName: "Wiki", exportName: "WikiPage", routePath: "wiki", }, { type: "routeSidebar", id: "wiki-sidebar", displayName: "Wiki Sidebar", exportName: "WikiRouteSidebar", routePath: "wiki", }, ], }), ]); const root = await renderPage(container); expect(mockSetBreadcrumbs).toHaveBeenCalledWith([{ label: "index" }]); await act(async () => { root.unmount(); }); }); });