// @vitest-environment jsdom import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Issue, IssueStatus } from "@paperclipai/shared"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { KanbanBoard, resolveKanbanTargetStatus } from "./KanbanBoard"; vi.mock("@/lib/router", () => ({ Link: ({ children, to, disableIssueQuicklook: _disableIssueQuicklook, ...props }: React.AnchorHTMLAttributes & { to: string; disableIssueQuicklook?: boolean; }) => ( {children} ), })); // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; function createIssue(index: number, status: IssueStatus): Issue { return { id: `issue-${status}-${index}`, identifier: `PAP-${index}`, companyId: "company-1", projectId: null, projectWorkspaceId: null, goalId: null, parentId: null, title: `Issue ${index}`, description: null, status, workMode: "standard", priority: "medium", assigneeAgentId: index === 1 ? "agent-1" : null, assigneeUserId: null, createdByAgentId: null, createdByUserId: null, issueNumber: index, requestDepth: 0, billingCode: null, assigneeAdapterOverrides: null, executionWorkspaceId: null, executionWorkspacePreference: null, executionWorkspaceSettings: null, checkoutRunId: null, executionRunId: null, executionAgentNameKey: null, executionLockedAt: null, startedAt: null, completedAt: null, cancelledAt: null, hiddenAt: null, createdAt: new Date("2026-05-05T00:00:00.000Z"), updatedAt: new Date("2026-05-05T00:00:00.000Z"), labels: [], labelIds: [], myLastTouchAt: null, lastExternalCommentAt: null, lastActivityAt: null, isUnreadForMe: false, }; } function createIssues(count: number, status: IssueStatus): Issue[] { return Array.from({ length: count }, (_, index) => createIssue(index + 1, status)); } function renderBoard( props: Partial> & { issues: Issue[] }, ) { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); const render = (nextProps: Partial> & { issues: Issue[] }) => { act(() => { root.render( , ); }); }; render(props); return { container, root, render }; } describe("KanbanBoard", () => { beforeEach(() => { document.body.innerHTML = ""; }); afterEach(() => { document.body.innerHTML = ""; }); it("limits visible cards and reveals more cards per column", () => { const { container } = renderBoard({ issues: createIssues(60, "todo"), compactCards: true, initialVisibleCount: 50, revealIncrement: 50, }); expect(container.textContent).toContain("Showing 50 of 60"); expect(container.textContent).toContain("Show 10 more"); expect(container.textContent).toContain("Issue 50"); expect(container.textContent).not.toContain("Issue 51"); const showMoreButton = Array.from(container.querySelectorAll("button")).find((button) => button.textContent?.includes("Show 10 more"), ); expect(showMoreButton).toBeTruthy(); act(() => { showMoreButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(container.textContent).toContain("Issue 60"); expect(container.textContent).not.toContain("Show 10 more"); }); it("resets visible counts when the column page size changes", () => { const issues = createIssues(60, "todo"); const { container, render } = renderBoard({ issues, initialVisibleCount: 50, revealIncrement: 50, }); const showMoreButton = Array.from(container.querySelectorAll("button")).find((button) => button.textContent?.includes("Show 10 more"), ); expect(showMoreButton).toBeTruthy(); act(() => { showMoreButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(container.textContent).toContain("Issue 60"); render({ issues, initialVisibleCount: 10, revealIncrement: 10, }); expect(container.textContent).toContain("Showing 10 of 60"); expect(container.textContent).toContain("Show 10 more"); expect(container.textContent).toContain("Issue 10"); expect(container.textContent).not.toContain("Issue 11"); }); it("renders collapsed statuses as rails without cards", () => { const { container } = renderBoard({ issues: createIssues(3, "done"), collapsedStatuses: ["done"], }); expect(container.textContent).toContain("Done"); expect(container.textContent).toContain("3"); expect(container.textContent).not.toContain("Issue 1"); }); it("keeps core issue signals in compact cards", () => { const { container } = renderBoard({ issues: createIssues(1, "todo"), compactCards: true, }); expect(container.textContent).toContain("PAP-1"); expect(container.textContent).toContain("Issue 1"); expect(container.textContent).toContain("Codex"); expect(container.textContent).toContain("Live"); }); it("resolves drop targets from status rails and cards", () => { const issues = [ createIssue(1, "todo"), createIssue(2, "blocked"), ]; expect(resolveKanbanTargetStatus("done", issues)).toBe("done"); expect(resolveKanbanTargetStatus("issue-blocked-2", issues)).toBe("blocked"); expect(resolveKanbanTargetStatus("missing", issues)).toBeNull(); }); });