// @vitest-environment jsdom import { act } from "react"; import type { ComponentProps, ReactNode } from "react"; import { createRoot } from "react-dom/client"; import type { ExecutionWorkspace, Issue } from "@paperclipai/shared"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ProjectWorkspaceSummary } from "../lib/project-workspaces-tab"; import { ProjectWorkspaceSummaryCard } from "./ProjectWorkspaceSummaryCard"; vi.mock("@/lib/router", () => ({ Link: ({ children, to, ...props }: ComponentProps<"a"> & { to: string }) => {children}, })); vi.mock("./IssuesQuicklook", () => ({ IssuesQuicklook: ({ children }: { children: ReactNode }) => <>{children}, })); // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; function createIssue(overrides: Partial = {}): Issue { return { id: overrides.id ?? "issue-1", companyId: overrides.companyId ?? "company-1", projectId: overrides.projectId ?? "project-1", projectWorkspaceId: overrides.projectWorkspaceId ?? null, goalId: overrides.goalId ?? null, parentId: overrides.parentId ?? null, title: overrides.title ?? "Issue", description: overrides.description ?? null, status: overrides.status ?? "todo", priority: overrides.priority ?? "medium", assigneeAgentId: overrides.assigneeAgentId ?? null, assigneeUserId: overrides.assigneeUserId ?? null, checkoutRunId: overrides.checkoutRunId ?? null, executionRunId: overrides.executionRunId ?? null, executionAgentNameKey: overrides.executionAgentNameKey ?? null, executionLockedAt: overrides.executionLockedAt ?? null, createdByAgentId: overrides.createdByAgentId ?? null, createdByUserId: overrides.createdByUserId ?? null, issueNumber: overrides.issueNumber ?? 1, identifier: overrides.identifier ?? "PAP-1", requestDepth: overrides.requestDepth ?? 0, billingCode: overrides.billingCode ?? null, assigneeAdapterOverrides: overrides.assigneeAdapterOverrides ?? null, executionWorkspaceId: overrides.executionWorkspaceId ?? null, executionWorkspacePreference: overrides.executionWorkspacePreference ?? null, executionWorkspaceSettings: overrides.executionWorkspaceSettings ?? null, startedAt: overrides.startedAt ?? null, completedAt: overrides.completedAt ?? null, cancelledAt: overrides.cancelledAt ?? null, hiddenAt: overrides.hiddenAt ?? null, createdAt: overrides.createdAt ?? new Date("2026-04-12T00:00:00Z"), updatedAt: overrides.updatedAt ?? new Date("2026-04-12T00:00:00Z"), } as Issue; } function createSummary(overrides: Partial = {}): ProjectWorkspaceSummary { return { key: overrides.key ?? "execution:workspace-1", kind: overrides.kind ?? "execution_workspace", workspaceId: overrides.workspaceId ?? "workspace-1", workspaceName: overrides.workspaceName ?? "PAP-989-multi-user-implementation", cwd: overrides.cwd ?? "/worktrees/PAP-989-multi-user-implementation", branchName: overrides.branchName ?? "PAP-989-multi-user-implementation", lastUpdatedAt: overrides.lastUpdatedAt ?? new Date("2026-04-12T00:00:00Z"), projectWorkspaceId: overrides.projectWorkspaceId ?? "project-workspace-1", executionWorkspaceId: overrides.executionWorkspaceId ?? "workspace-1", executionWorkspaceStatus: overrides.executionWorkspaceStatus ?? "active", serviceCount: overrides.serviceCount ?? 2, runningServiceCount: overrides.runningServiceCount ?? 0, primaryServiceUrl: overrides.primaryServiceUrl ?? "http://127.0.0.1:62474", primaryServiceUrlRunning: overrides.primaryServiceUrlRunning ?? false, hasRuntimeConfig: overrides.hasRuntimeConfig ?? true, issues: overrides.issues ?? [ createIssue({ id: "issue-1", identifier: "PAP-1364" }), createIssue({ id: "issue-2", identifier: "PAP-1367" }), createIssue({ id: "issue-3", identifier: "PAP-1362" }), createIssue({ id: "issue-4", identifier: "PAP-1363" }), createIssue({ id: "issue-5", identifier: "PAP-1340" }), ], }; } describe("ProjectWorkspaceSummaryCard", () => { let container: HTMLDivElement; let writeClipboard: ReturnType; beforeEach(() => { container = document.createElement("div"); document.body.appendChild(container); writeClipboard = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText: writeClipboard }, }); Object.defineProperty(window, "isSecureContext", { configurable: true, value: true, }); }); afterEach(() => { document.body.innerHTML = ""; }); it("renders a stacked mobile-friendly summary with metadata labels and compact issue pills", () => { const root = createRoot(container); act(() => { root.render( {}} onCloseWorkspace={() => {}} />, ); }); expect(container.textContent).toContain("Execution workspace"); expect(container.textContent).toContain("Branch"); expect(container.textContent).toContain("Path"); expect(container.textContent).toContain("Service"); expect(container.textContent).toContain("Linked issues"); expect(container.textContent).toContain("Start services"); expect(container.textContent).toContain("Close workspace"); expect(container.textContent).toContain("+1 more"); const actions = container.querySelector('[data-testid="workspace-summary-actions"]'); expect(actions?.className).toContain("flex-col"); const card = container.firstElementChild; expect(card?.className).toContain("rounded-lg"); expect(card?.className).toContain("border"); act(() => { root.unmount(); }); }); it("uses project workspace routes and omits close controls for project workspaces", () => { const runtimeSpy = vi.fn(); const closeSpy = vi.fn(); const root = createRoot(container); act(() => { root.render( , ); }); const titleLink = container.querySelector("a[href='/projects/paperclip-app/workspaces/workspace-1']"); expect(titleLink).not.toBeNull(); expect(container.textContent).not.toContain("Close workspace"); expect(container.textContent).not.toContain("Start services"); act(() => { root.unmount(); }); }); it("shows retry close for cleanup failures", () => { const root = createRoot(container); act(() => { root.render( {}} onCloseWorkspace={() => {}} />, ); }); expect(container.textContent).toContain("Retry close"); act(() => { root.unmount(); }); }); it("copies branch and path from both text and icon controls with feedback", async () => { const root = createRoot(container); const summary = createSummary({ branchName: "PAP-1552-workspace-polish", cwd: "/Users/dotta/paperclip/.worktrees/PAP-1552-workspace-polish", }); await act(async () => { root.render( {}} onCloseWorkspace={() => {}} />, ); }); const branchTextButton = Array.from(container.querySelectorAll("button")) .find((button) => button.textContent === summary.branchName); const pathTextButton = container.querySelector(`button[title="${summary.cwd}"]`); const branchIconButton = container.querySelector('button[aria-label="Copy branch"]'); const pathIconButton = container.querySelector('button[aria-label="Copy path"]'); expect(branchTextButton).not.toBeNull(); expect(pathTextButton).not.toBeNull(); expect(branchIconButton).not.toBeNull(); expect(pathIconButton).not.toBeNull(); await act(async () => { branchTextButton!.click(); }); expect(writeClipboard).toHaveBeenLastCalledWith(summary.branchName); expect(branchTextButton?.nextElementSibling?.className).toContain("opacity-100"); await act(async () => { pathTextButton!.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(writeClipboard).toHaveBeenLastCalledWith(summary.cwd); expect(pathTextButton?.nextElementSibling?.className).toContain("opacity-100"); await act(async () => { branchIconButton!.dispatchEvent(new MouseEvent("click", { bubbles: true })); pathIconButton!.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(writeClipboard).toHaveBeenCalledWith(summary.branchName); expect(writeClipboard).toHaveBeenCalledWith(summary.cwd); act(() => { root.unmount(); }); }); it("colors live service urls green", () => { const root = createRoot(container); act(() => { root.render( {}} onCloseWorkspace={() => {}} />, ); }); const serviceLink = container.querySelector("a[href='http://127.0.0.1:62475']"); expect(serviceLink?.className).toContain("text-emerald"); act(() => { root.unmount(); }); }); });