// @vitest-environment jsdom import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { MemoryRouter } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, expect, it, vi } from "vitest"; import type { PluginUiContribution } from "@/api/plugins"; import { pluginsApi } from "@/api/plugins"; import { queryKeys } from "@/lib/queryKeys"; import { PluginLauncherOutlet, PluginLauncherProvider } from "./launchers"; import { _resetPluginModuleLoader, PluginSlotOutlet, registerPluginReactComponent } from "./slots"; vi.mock("@/context/CompanyContext", () => ({ useCompany: () => ({ selectedCompany: null }) })); const contribution: PluginUiContribution = { pluginId: "fixture", pluginKey: "fixture.sidebar", displayName: "Fixture", version: "1", uiEntryFile: "index.js", slots: [ { type: "sidebar", id: "nav", displayName: "Nav", exportName: "SidebarItem" }, { type: "sidebarPanel", id: "panel", displayName: "Panel", exportName: "SidebarPanel" }, ], launchers: [{ id: "open", displayName: "Fixture launcher", placementZone: "sidebar", action: { type: "navigate", target: "/fixture" }, }], }; const context = { companyId: "company-1", companyPrefix: "PAP" }; let root: Root; let container: HTMLDivElement; let client: QueryClient; async function render(errorBehavior?: "inline" | "hidden") { await act(async () => root.render( , )); } beforeEach(() => { Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); // Module and session fetches stay pending; components are registered directly. vi.stubGlobal("__paperclipPluginBridge__", {}); vi.stubGlobal("fetch", vi.fn(() => new Promise(() => {}))); client = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } }); container = document.createElement("div"); document.body.append(container); root = createRoot(container); vi.spyOn(pluginsApi, "listUiContributions").mockRejectedValue(new TypeError("Failed to fetch")); }); afterEach(async () => { await act(async () => root.unmount()); container.remove(); client.clear(); _resetPluginModuleLoader(); vi.unstubAllGlobals(); vi.restoreAllMocks(); }); it("shows inline errors by default when contributions fail to load", async () => { await render(); await vi.waitFor(() => expect(container.textContent).toContain("unavailable")); expect(container.textContent).toContain("Plugin extensions unavailable: Failed to fetch"); expect(container.textContent).toContain("Plugin launchers unavailable: Failed to fetch"); }); it("hides errors and keeps the last loaded slots and launchers when errorBehavior is hidden", async () => { registerPluginReactComponent("fixture.sidebar", "SidebarItem", () => Fixture sidebar item); registerPluginReactComponent("fixture.sidebar", "SidebarPanel", () => Fixture sidebar panel); client.setQueryData(queryKeys.plugins.uiContributions, [contribution]); await render("hidden"); expect(container.textContent).toContain("Fixture sidebar item"); expect(container.textContent).toContain("Fixture sidebar panel"); await act(async () => { await client.refetchQueries({ queryKey: queryKeys.plugins.uiContributions }); // React Query notifies observers on a timer; let the error render land. await new Promise((resolve) => setTimeout(resolve, 0)); }); expect(client.getQueryState(queryKeys.plugins.uiContributions)?.status).toBe("error"); expect(container.textContent).not.toContain("unavailable"); expect(container.textContent).toContain("Fixture sidebar item"); expect(container.textContent).toContain("Fixture sidebar panel"); expect(container.textContent).toContain("Fixture launcher"); });