// @vitest-environment jsdom import { flushSync } from "react-dom"; import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { AppsSidebar } from "./AppsSidebar"; import { AppsSidebar as ProductionAppsSidebar } from "./AppsSidebar.production"; import { contextualSidebarStyles } from "./contextual-sidebar-styles"; const sidebarNavItemMock = vi.hoisted(() => vi.fn()); const route = vi.hoisted(() => ({ pathname: "/SLA/apps" })); const mockToolsApi = vi.hoisted(() => ({ listActionRequests: vi.fn(), })); vi.mock("@/lib/router", () => ({ useLocation: () => route, Link: ({ children, to, onClick, className, }: { children: React.ReactNode; to: string; onClick?: () => void; className?: string; }) => ( {children} ), })); vi.mock("@/context/CompanyContext", () => ({ useCompany: () => ({ selectedCompanyId: "company-1", selectedCompany: { id: "company-1", name: "Paperclip" }, }), })); vi.mock("@/context/SidebarContext", () => ({ useSidebar: () => ({ isMobile: false, setSidebarOpen: vi.fn(), }), })); vi.mock("@/api/tools", () => ({ toolsApi: mockToolsApi, })); vi.mock("./SidebarNavItem", () => ({ SidebarNavItem: (props: { to: string; label: string; end?: boolean; liveCount?: number; badge?: number; }) => { sidebarNavItemMock(props); return
{props.label}
; }, })); vi.mock("./SidebarNavItem.production", () => ({ SidebarNavItem: (props: { to: string; label: string; end?: boolean; liveCount?: number; badge?: number; }) => { sidebarNavItemMock(props); return
{props.label}
; }, })); // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; // React 19 does not export a usable `act` in this vitest/jsdom setup; use a // flushSync-based helper (PAP-12371 gotcha). async function act(callback: () => void | Promise) { let result: void | Promise = undefined; flushSync(() => { result = callback(); }); await result; } async function flushReact() { for (let i = 0; i < 5; i += 1) { await act(async () => { await Promise.resolve(); await new Promise((resolve) => window.setTimeout(resolve, 0)); }); } } describe("AppsSidebar", () => { let container: HTMLDivElement; beforeEach(() => { route.pathname = "/SLA/apps"; container = document.createElement("div"); document.body.appendChild(container); mockToolsApi.listActionRequests.mockResolvedValue({ actionRequests: [] }); }); afterEach(() => { container.remove(); document.body.innerHTML = ""; vi.clearAllMocks(); }); it.each([AppsSidebar, ProductionAppsSidebar])("shows chat navigation in place of browse and review", async (Sidebar) => { route.pathname = "/SLA/apps/chat/endpoint-a/conversations"; const root = createRoot(container); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); await act(async () => root.render()); await flushReact(); expect(container.textContent).not.toMatch(/Browse|Review/); for (const tab of ["settings", "access", "conversations", "activity"]) { expect(container.querySelector(`[data-to="/apps/chat/endpoint-a/${tab}"]`)).not.toBeNull(); } await act(async () => root.unmount()); queryClient.clear(); }); it("renders the consolidated connector and review doors without a redundant contextual heading", async () => { const root = createRoot(container); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); await act(async () => { root.render( , ); }); await flushReact(); expect(container.textContent).not.toContain("Apps"); expect(container.querySelector('nav[aria-label="Connectors"]')).not.toBeNull(); expect(container.textContent).not.toContain("Developer"); expect(container.textContent).not.toContain("Advanced setup for developers"); expect(container.textContent).not.toContain("Most teams"); expect(container.textContent).not.toMatch(/you (?:won'?t|will not) need this/i); // Paste-config discovery now lives inside the custom connector row; // assert both advanced setup items remain absent at the item level below. expect(sidebarNavItemMock).toHaveBeenCalledWith( expect.objectContaining({ to: "/apps", label: "Browse", end: true }), ); expect(sidebarNavItemMock).toHaveBeenCalledWith( expect.objectContaining({ to: "/apps/review", label: "Review" }), ); expect(sidebarNavItemMock).not.toHaveBeenCalledWith( expect.objectContaining({ label: "Connections" }), ); expect(sidebarNavItemMock).not.toHaveBeenCalledWith( expect.objectContaining({ label: "Needs attention" }), ); expect(sidebarNavItemMock).not.toHaveBeenCalledWith( expect.objectContaining({ label: "Run your own" }), ); expect(sidebarNavItemMock).not.toHaveBeenCalledWith( expect.objectContaining({ label: "Paste a config" }), ); expect(sidebarNavItemMock).not.toHaveBeenCalledWith( expect.objectContaining({ label: "Applications" }), ); expect(sidebarNavItemMock).not.toHaveBeenCalledWith( expect.objectContaining({ to: "/apps/advanced/gateways", label: "Gateways" }), ); expect(sidebarNavItemMock).not.toHaveBeenCalledWith( expect.objectContaining({ to: "/apps/advanced/profiles", label: "Profiles" }), ); expect(sidebarNavItemMock).not.toHaveBeenCalledWith(expect.objectContaining({ label: "Rules" })); expect(sidebarNavItemMock).not.toHaveBeenCalledWith(expect.objectContaining({ label: "Health" })); expect(sidebarNavItemMock).not.toHaveBeenCalledWith( expect.objectContaining({ to: "/apps/advanced/audit" }), ); expect(container.querySelector('[data-slot="contextual-sidebar-nav"]')?.className).toBe( contextualSidebarStyles.nav, ); expect( Array.from(container.querySelectorAll('[data-slot="contextual-sidebar-group"]')).every( (group) => group.className === contextualSidebarStyles.group, ), ).toBe(true); await act(async () => { root.unmount(); }); }); it("uses Connectors terminology throughout the classic contextual sidebar", async () => { const root = createRoot(container); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); await act(async () => { root.render( , ); }); await flushReact(); expect(container.textContent).toContain("Connectors"); expect(container.textContent).not.toContain("Apps"); expect(sidebarNavItemMock).toHaveBeenCalledWith( expect.objectContaining({ to: "/apps", label: "Browse", end: true }), ); await act(async () => { root.unmount(); }); }); });