// @vitest-environment jsdom import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { Agent, Issue, IssueTreeControlPreview, IssueTreeHold } from "@paperclipai/shared"; import { act, type ButtonHTMLAttributes, type ReactNode } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { IssueDetail } from "./IssueDetail"; const mockIssuesApi = vi.hoisted(() => ({ get: vi.fn(), list: vi.fn(), listComments: vi.fn(), listAttachments: vi.fn(), listFeedbackVotes: vi.fn(), markRead: vi.fn(), update: vi.fn(), previewTreeControl: vi.fn(), getTreeControlState: vi.fn(), listTreeHolds: vi.fn(), createTreeHold: vi.fn(), releaseTreeHold: vi.fn(), archiveFromInbox: vi.fn(), addComment: vi.fn(), cancelComment: vi.fn(), upsertFeedbackVote: vi.fn(), uploadAttachment: vi.fn(), deleteAttachment: vi.fn(), upsertDocument: vi.fn(), })); const mockActivityApi = vi.hoisted(() => ({ forIssue: vi.fn(), runsForIssue: vi.fn(), })); const mockHeartbeatsApi = vi.hoisted(() => ({ liveRunsForIssue: vi.fn(), activeRunForIssue: vi.fn(), cancel: vi.fn(), })); const mockAgentsApi = vi.hoisted(() => ({ list: vi.fn(), })); const mockAccessApi = vi.hoisted(() => ({ getCurrentBoardAccess: vi.fn(), listUserDirectory: vi.fn(), })); const mockAuthApi = vi.hoisted(() => ({ getSession: vi.fn(), })); const mockProjectsApi = vi.hoisted(() => ({ list: vi.fn(), })); const mockInstanceSettingsApi = vi.hoisted(() => ({ getGeneral: vi.fn(), })); const mockNavigate = vi.hoisted(() => vi.fn()); const mockOpenPanel = vi.hoisted(() => vi.fn()); const mockClosePanel = vi.hoisted(() => vi.fn()); const mockSetBreadcrumbs = vi.hoisted(() => vi.fn()); const mockSetMobileToolbar = vi.hoisted(() => vi.fn()); const mockPushToast = vi.hoisted(() => vi.fn()); const mockIssuesListRender = vi.hoisted(() => vi.fn()); const mockIssueChatThreadRender = vi.hoisted(() => vi.fn()); vi.mock("../api/issues", () => ({ issuesApi: mockIssuesApi, })); vi.mock("../api/activity", () => ({ activityApi: mockActivityApi, })); vi.mock("../api/heartbeats", () => ({ heartbeatsApi: mockHeartbeatsApi, })); vi.mock("../api/approvals", () => ({ approvalsApi: { approve: vi.fn(), reject: vi.fn(), }, })); vi.mock("../api/agents", () => ({ agentsApi: mockAgentsApi, })); vi.mock("../api/access", () => ({ accessApi: mockAccessApi, })); vi.mock("../api/auth", () => ({ authApi: mockAuthApi, })); vi.mock("../api/projects", () => ({ projectsApi: mockProjectsApi, })); vi.mock("../api/instanceSettings", () => ({ instanceSettingsApi: mockInstanceSettingsApi, })); vi.mock("@/lib/router", () => ({ Link: ({ children, to }: { children?: ReactNode; to: string }) => {children}, useLocation: () => ({ pathname: "/issues/PAP-1", search: "", hash: "", state: null }), useNavigate: () => mockNavigate, useNavigationType: () => "PUSH", useParams: () => ({ issueId: "PAP-1" }), })); vi.mock("../context/CompanyContext", () => ({ useCompany: () => ({ companies: [{ id: "company-1", name: "Paperclip", issuePrefix: "PAP", status: "active" }], selectedCompanyId: "company-1", selectedCompany: { id: "company-1", name: "Paperclip", issuePrefix: "PAP", status: "active" }, selectionSource: "manual", loading: false, error: null, setSelectedCompanyId: vi.fn(), reloadCompanies: vi.fn(), createCompany: vi.fn(), }), })); vi.mock("../context/DialogContext", () => ({ useDialog: () => ({ openNewIssue: vi.fn(), }), useDialogActions: () => ({ openNewIssue: vi.fn(), }), })); vi.mock("../context/PanelContext", () => ({ usePanel: () => ({ openPanel: mockOpenPanel, closePanel: mockClosePanel, panelVisible: true, setPanelVisible: vi.fn(), }), })); vi.mock("../context/SidebarContext", () => ({ useSidebar: () => ({ isMobile: false, }), })); vi.mock("../context/BreadcrumbContext", () => ({ useBreadcrumbs: () => ({ setBreadcrumbs: mockSetBreadcrumbs, setMobileToolbar: mockSetMobileToolbar, }), })); vi.mock("../context/ToastContext", () => ({ useToastActions: () => ({ pushToast: mockPushToast, }), })); vi.mock("../hooks/useProjectOrder", () => ({ useProjectOrder: ({ projects }: { projects: unknown[] }) => ({ orderedProjects: projects, }), })); vi.mock("@/plugins/slots", () => ({ PluginSlotMount: () => null, PluginSlotOutlet: () => null, usePluginSlots: () => ({ slots: [], isLoading: false, errorMessage: null }), })); vi.mock("@/plugins/launchers", () => ({ PluginLauncherOutlet: () => null, })); vi.mock("../components/InlineEditor", () => ({ InlineEditor: ({ value, placeholder }: { value?: string; placeholder?: string }) => (
{value || placeholder}
), })); vi.mock("../components/IssueChatThread", () => ({ IssueChatThread: (props: { onWorkModeChange?: (workMode: string) => void; issueWorkMode?: string; onStopRun?: (runId: string) => Promise; stopRunLabel?: string; stoppingRunLabel?: string; }) => { mockIssueChatThreadRender(props); return (
Chat thread {props.onStopRun ? ( ) : null}
); }, })); vi.mock("../components/IssueDocumentsSection", () => ({ IssueDocumentsSection: () =>
Documents
, })); vi.mock("../components/IssuesList", () => ({ IssuesList: (props: { issueBadgeById?: Map }) => { mockIssuesListRender(props); return (
Sub-issues {Array.from(props.issueBadgeById?.entries() ?? []).map(([issueId, label]) => ( {issueId}:{label} ))}
); }, })); vi.mock("../components/IssueProperties", () => ({ IssueProperties: () =>
Properties
, })); vi.mock("../components/IssueRunLedger", () => ({ IssueRunLedger: () =>
Runs
, })); vi.mock("../components/IssueWorkspaceCard", () => ({ IssueWorkspaceCard: () =>
Workspace
, })); vi.mock("../components/ImageGalleryModal", () => ({ ImageGalleryModal: () => null, })); vi.mock("../components/ScrollToBottom", () => ({ ScrollToBottom: () => null, })); vi.mock("../components/StatusIcon", () => ({ StatusIcon: ({ status, blockerAttention }: { status: string; blockerAttention?: Issue["blockerAttention"] }) => ( {status} ), })); vi.mock("../components/PriorityIcon", () => ({ PriorityIcon: ({ priority }: { priority: string }) => {priority}, })); vi.mock("../components/ApprovalCard", () => ({ ApprovalCard: () =>
Approval
, })); vi.mock("../components/Identity", () => ({ Identity: () => Identity, })); vi.mock("@/components/ui/button", () => ({ Button: ({ children, disabled, onClick, type = "button", variant: _variant, size: _size, asChild: _asChild, ...props }: ButtonHTMLAttributes & { variant?: string; size?: string; asChild?: boolean }) => ( ), })); vi.mock("@/components/ui/separator", () => ({ Separator: () =>
, })); vi.mock("@/components/ui/popover", () => ({ Popover: ({ children }: { children?: ReactNode }) => <>{children}, PopoverTrigger: ({ children }: { children?: ReactNode }) => <>{children}, PopoverContent: ({ children }: { children?: ReactNode }) =>
{children}
, })); vi.mock("@/components/ui/dialog", () => ({ Dialog: ({ children, open }: { children?: ReactNode; open?: boolean }) => (open ?
{children}
: null), DialogContent: ({ children, className }: { children?: ReactNode; className?: string }) => (
{children}
), DialogDescription: ({ children, className }: { children?: ReactNode; className?: string }) =>

{children}

, DialogFooter: ({ children, className }: { children?: ReactNode; className?: string }) =>
{children}
, DialogHeader: ({ children, className }: { children?: ReactNode; className?: string }) =>
{children}
, DialogTitle: ({ children, className }: { children?: ReactNode; className?: string }) =>

{children}

, })); vi.mock("@/components/ui/sheet", () => ({ Sheet: ({ children, open }: { children?: ReactNode; open?: boolean }) => (open ?
{children}
: null), SheetContent: ({ children }: { children?: ReactNode }) =>
{children}
, SheetHeader: ({ children }: { children?: ReactNode }) =>
{children}
, SheetTitle: ({ children }: { children?: ReactNode }) =>

{children}

, })); vi.mock("@/components/ui/scroll-area", () => ({ ScrollArea: ({ children }: { children?: ReactNode }) =>
{children}
, })); vi.mock("@/components/ui/skeleton", () => ({ Skeleton: () =>
, })); vi.mock("@/components/ui/tabs", () => ({ Tabs: ({ children }: { children?: ReactNode }) =>
{children}
, TabsContent: ({ children }: { children?: ReactNode }) =>
{children}
, TabsList: ({ children }: { children?: ReactNode }) =>
{children}
, TabsTrigger: ({ children }: { children?: ReactNode }) => , })); vi.mock("@/components/ui/textarea", () => ({ Textarea: (props: React.TextareaHTMLAttributes) =>