// @vitest-environment happy-dom import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import React, { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { MemoryRouter } from "react-router"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { TooltipProvider } from "./ui/tooltip"; import { WorkspaceAppCard } from "./workspace-app-card"; vi.mock("../../../core/dist/client/use-action.js", () => ({ useActionMutation: () => ({ mutate: vi.fn(), isPending: false, }), useActionQuery: () => ({ data: { ownerEmail: "owner@example.com", orgId: null, visibility: "private", role: "owner", shares: [], }, isLoading: false, isError: false, error: null, refetch: vi.fn(async () => undefined), }), })); vi.mock("../../../core/dist/client/i18n.js", () => ({ useT: () => (key: string, values?: Record) => String(values?.defaultValue ?? key), })); vi.mock("@agent-native/core/client/hooks", () => ({ useActionMutation: () => ({ mutate: vi.fn(), isPending: false }), useActionQuery: () => ({ data: { resources: [], counts: {} }, isLoading: false, isError: false, error: null, refetch: vi.fn(), }), })); vi.mock("@agent-native/core/client/i18n", () => ({ useFormatters: () => ({ formatDate: (value: string) => value, }), useT: () => (key: string, values?: Record) => String(values?.defaultValue ?? key), })); const frameState = vi.hoisted(() => ({ inBuilderFrame: false })); vi.mock("@agent-native/core/client/host", () => ({ isInBuilderFrame: () => frameState.inBuilderFrame, })); describe("WorkspaceAppCard", () => { let container: HTMLDivElement; let root: Root; let queryClient: QueryClient; beforeEach(() => { frameState.inBuilderFrame = false; vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); }); afterEach(() => { act(() => root.unmount()); queryClient.clear(); container.remove(); vi.unstubAllGlobals(); }); it("opens ready apps inline and keeps new-tab opening in the chevron menu", async () => { await act(async () => { root.render( , ); }); const openButton = container.querySelector( ".app-open-actions__primary", ); expect(openButton).not.toBeNull(); expect(openButton?.textContent).toContain("Open app"); expect(container.querySelector('a[href="/analytics"]')).toBeNull(); expect( container.querySelector( 'button[aria-label="Open options for Analytics"]', ), ).not.toBeNull(); expect(document.body.textContent).not.toContain("Open in new tab"); expect(document.body.textContent).not.toContain("Add app"); await act(async () => { container .querySelector( 'button[aria-label="Open options for Analytics"]', ) ?.dispatchEvent( new MouseEvent("pointerdown", { bubbles: true, button: 0 }), ); }); const newTabItem = Array.from( document.querySelectorAll('[role="menuitem"]'), ).find((item) => item.textContent?.includes("Open in new tab")); expect(newTabItem).not.toBeUndefined(); expect(newTabItem?.getAttribute("href")).toBe("/analytics"); expect(newTabItem?.getAttribute("target")).toBe("_blank"); const openMenu = document.querySelector('[role="menu"]'); expect(openMenu?.className).toContain("w-48"); expect(openMenu?.className).toContain("min-w-max"); expect(openMenu?.className).toContain("bg-popover"); expect(openMenu?.className).toContain("shadow-md"); expect(newTabItem?.querySelector("svg")).not.toBeNull(); const settingsButton = container.querySelector( 'button[aria-label="Settings for Analytics"]', ); expect(settingsButton).not.toBeNull(); expect(settingsButton?.className).toContain("size-7"); expect( container.querySelector( 'button[aria-label="More actions for Analytics"]', ), ).toBeNull(); expect( container.querySelector( 'button[aria-label="View agent resources for Analytics"]', ), ).toBeNull(); }); it("opens mounted workspace apps at their published URL", async () => { frameState.inBuilderFrame = true; const originalParent = window.parent; const originalTop = window.top; const topWindow = { location: { href: "" } } as unknown as Window; Object.defineProperty(window, "parent", { configurable: true, value: {}, }); Object.defineProperty(window, "top", { configurable: true, value: topWindow, }); try { await act(async () => { root.render( , ); }); const openButton = container.querySelector( ".app-open-actions__primary", ); expect(openButton).not.toBeNull(); expect(openButton?.textContent).toContain("Open app"); expect( container.querySelector( 'a[href="https://agent-workspace.builder.io/feedback-leaderboard/leaderboard"]', ), ).toBeNull(); await act(async () => openButton?.click()); expect(topWindow.location.href).toBe( "https://agent-workspace.builder.io/feedback-leaderboard/leaderboard", ); } finally { Object.defineProperty(window, "parent", { configurable: true, value: originalParent, }); Object.defineProperty(window, "top", { configurable: true, value: originalTop, }); } }); it("opens Share from the app settings menu instead of the card actions", async () => { const animationFrameCallbacks: FrameRequestCallback[] = []; vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { animationFrameCallbacks.push(callback); return animationFrameCallbacks.length; }); await act(async () => { root.render( , ); }); expect(container.querySelector('button[aria-label="Share"]')).toBeNull(); const settingsButton = container.querySelector( 'button[aria-label="Settings for Analytics"]', ); expect(settingsButton).not.toBeNull(); await act(async () => { settingsButton?.dispatchEvent( new MouseEvent("pointerdown", { bubbles: true, button: 0 }), ); }); const shareItem = Array.from( document.querySelectorAll('[role="menuitem"]'), ).find((item) => item.textContent?.trim() === "Share"); expect(shareItem).not.toBeUndefined(); await act(async () => shareItem?.click()); expect(container.querySelector('button[aria-label="Share"]')).toBeNull(); expect(document.querySelector('[role="menu"]')).toBeNull(); expect(animationFrameCallbacks).toHaveLength(1); await act(async () => animationFrameCallbacks.shift()?.(0)); expect( container.querySelector('button[aria-label="Share"]'), ).not.toBeNull(); expect( document.body.querySelector("[data-agent-native-share-overlay]"), ).not.toBeNull(); const shareButton = container.querySelector( 'button[aria-label="Share"]', ); expect(shareButton).not.toBeNull(); await act(async () => shareButton?.click()); expect(animationFrameCallbacks).toHaveLength(1); await act(async () => animationFrameCallbacks.shift()?.(0)); expect(document.activeElement).toBe(settingsButton); }); it("keeps pinning in the app open menu", async () => { const onTogglePinned = vi.fn(); await act(async () => { root.render( , ); }); const optionsButton = container.querySelector( 'button[aria-label="Open options for Analytics"]', ); expect(optionsButton).not.toBeNull(); await act(async () => { optionsButton?.dispatchEvent( new MouseEvent("pointerdown", { bubbles: true, button: 0 }), ); }); const pinItem = Array.from( document.querySelectorAll('[role="menuitem"]'), ).find((item) => item.textContent?.includes("Unpin this app")); expect(pinItem).not.toBeUndefined(); await act(async () => pinItem?.click()); expect(onTogglePinned).toHaveBeenCalledOnce(); }); it("labels per-app context as agent resources while preserving workspace scope", async () => { await act(async () => { root.render( , ); }); const settingsButton = container.querySelector( 'button[aria-label="Settings for Analytics"]', ); await act(async () => { settingsButton?.dispatchEvent( new MouseEvent("pointerdown", { bubbles: true, button: 0 }), ); }); const resourcesItem = Array.from( document.querySelectorAll('[role="menuitem"]'), ).find((item) => item.textContent?.includes("Agent resources")); expect(resourcesItem).not.toBeUndefined(); const settingsMenu = document.querySelector('[role="menu"]'); expect(settingsMenu?.className).toContain("w-48"); expect(settingsMenu?.className).toContain("bg-popover"); expect(settingsMenu?.className).toContain("shadow-md"); expect(document.querySelectorAll('[role="menuitem"] svg').length).toBe(5); await act(async () => resourcesItem?.click()); expect(document.body.textContent).toContain("Analytics agent resources"); expect(document.body.textContent).toContain( "Workspace-scope agent resources are inherited at runtime.", ); expect(document.body.textContent).toContain( "All-app agent resources live once at workspace scope", ); }); it("opens pending Builder apps in a new tab", async () => { await act(async () => { root.render( , ); }); const appLink = container.querySelector( 'a[href="https://builder.example.com/projects/new-app"]', ); expect(appLink).not.toBeNull(); expect(appLink?.getAttribute("href")).toBe( "https://builder.example.com/projects/new-app", ); expect(appLink?.getAttribute("target")).toBe("_blank"); expect(appLink?.getAttribute("rel")).toBe("noreferrer"); expect(container.textContent).toContain("Open in Builder"); expect(container.textContent).not.toContain("pending-branch-id"); expect(container.querySelector(".app-open-actions")).toBeNull(); expect( container.querySelector('button[aria-label="Open options for New app"]'), ).toBeNull(); }); it("shows ownership metadata in the settings menu", async () => { await act(async () => { root.render( , ); }); const settingsButton = container.querySelector( 'button[aria-label="Settings for Analytics"]', ); expect(settingsButton).not.toBeNull(); await act(async () => { settingsButton?.dispatchEvent( new MouseEvent("pointerdown", { bubbles: true, button: 0 }), ); }); expect(document.body.textContent).toContain("creator@example.com"); expect(document.body.textContent).toContain("owner@example.com"); expect(document.body.textContent).toContain("Growth, Operations"); const renderedLabels = Array.from( document.querySelectorAll("span"), (span) => span.textContent, ); expect(renderedLabels).not.toContain("agents.dashboardMetadataCreated"); expect(renderedLabels).not.toContain("agents.notTracked"); }); it("hides metadata rows without tracked values", async () => { await act(async () => { root.render( , ); }); const settingsButton = container.querySelector( 'button[aria-label="Settings for Analytics"]', ); expect(settingsButton).not.toBeNull(); await act(async () => { settingsButton?.dispatchEvent( new MouseEvent("pointerdown", { bubbles: true, button: 0 }), ); }); const renderedLabels = Array.from( document.querySelectorAll("span"), (span) => span.textContent, ); expect(renderedLabels).not.toContain("agents.notTracked"); expect(renderedLabels).not.toContain("agents.dashboardMetadataCreated"); expect(renderedLabels).not.toContain("dispatch.pages.appMetadataOwner"); expect(renderedLabels).not.toContain("dispatch.pages.appMetadataTeams"); }); });