import { describe, it, expect, vi, beforeAll, afterAll } from "vitest"; import { render } from "@testing-library/react"; import type { Task } from "@fusion/core"; import { TaskCard } from "../TaskCard"; import { loadAllAppCss } from "../../test/cssFixture"; vi.mock("lucide-react", () => ({ Link: () => , GitBranch: () => , Clock: () => , Pencil: () => , Layers: () => , ChevronDown: () => , Folder: () => , GitPullRequest: () => , CircleDot: () => , Target: () => , Bot: () => , Trash2: () => , RotateCw: () => , Zap: () => , AlertTriangle: () => , })); vi.mock("../../hooks/useTaskDiffStats", () => ({ useTaskDiffStats: () => ({ stats: null, loading: false }), })); vi.mock("../../hooks/useBadgeWebSocket", () => ({ useBadgeWebSocket: () => ({ badgeUpdates: new Map(), isConnected: true, subscribeToBadge: vi.fn(), unsubscribeFromBadge: vi.fn(), }), })); vi.mock("../../hooks/useBatchBadgeFetch", () => ({ getFreshBatchData: vi.fn(() => null), })); vi.mock("../../api", () => ({ fetchTaskDetail: vi.fn(), uploadAttachment: vi.fn(), fetchMission: vi.fn(), fetchAgent: vi.fn(), })); vi.mock("../../hooks/useConfirm", () => ({ useConfirm: () => ({ confirm: vi.fn(async () => true) }), })); const noop = () => {}; function makeTask(): Task { return { id: "FN-4598", title: "Alignment test", description: "", column: "in-progress", steps: [], dependencies: [], sourceType: "github_import", issueInfo: { owner: "runfusion", repo: "fusion", number: 315, title: "Imported issue", url: "https://github.com/runfusion/fusion/issues/315", }, githubTracking: { enabled: true, issue: { owner: "runfusion", repo: "fusion", number: 316, url: "https://github.com/runfusion/fusion/issues/316", createdAt: new Date().toISOString(), }, }, retrySummary: { total: 2 }, columnMovedAt: new Date(Date.now() - 60_000).toISOString(), } as Task; } describe("FN-4598 TaskCard footer chip alignment", () => { let styleEl: HTMLStyleElement; beforeAll(() => { styleEl = document.createElement("style"); styleEl.textContent = loadAllAppCss(); document.head.appendChild(styleEl); }); afterAll(() => { styleEl.remove(); }); it("keeps footer chips and inner spans vertically centered", () => { const { container } = render( , ); const selectors = [ ".card-retry-badge", ".card-time-indicator", ".card-github-tracking-chip", ] as const; for (const selector of selectors) { const chip = container.querySelector(selector) as HTMLElement; expect(chip).toBeTruthy(); const chipStyle = getComputedStyle(chip); expect(chipStyle.display).toBe("inline-flex"); expect(chipStyle.alignItems).toBe("center"); expect(chipStyle.lineHeight).toBe("1"); const textSpan = chip.querySelector("span") as HTMLElement; expect(textSpan).toBeTruthy(); const textStyle = getComputedStyle(textSpan); expect(textStyle.display).toBe("inline-flex"); expect(textStyle.alignItems).toBe("center"); expect(textStyle.lineHeight).toBe("1"); expect(textStyle.transform).toMatch(/translateY\(1px\)|matrix\(1,\s*0,\s*0,\s*1,\s*0,\s*1\)/); } }); it("keeps github, retry, and timer as a single right-aligned cluster with token gap", () => { const { container } = render( , ); const footerRow = container.querySelector(".card-footer-row") as HTMLElement; const rightCluster = footerRow.querySelector(":scope > .card-footer-row-right") as HTMLElement; const githubChip = rightCluster.querySelector(":scope > .card-github-tracking-chip") as HTMLElement; const retryChip = rightCluster.querySelector(":scope > .card-retry-badge") as HTMLElement; const timerChip = rightCluster.querySelector(":scope > .card-time-indicator") as HTMLElement; expect(footerRow).toBeTruthy(); expect(rightCluster).toBeTruthy(); expect(githubChip).toBeTruthy(); expect(retryChip).toBeTruthy(); expect(timerChip).toBeTruthy(); expect(getComputedStyle(rightCluster).marginLeft).toBe("auto"); const footerStyle = getComputedStyle(footerRow); expect(footerStyle.gap).toBe("var(--space-sm)"); }); it("FN-5099 keeps retry in the right cluster when source provenance is present in chipFarRight layouts", () => { const { container } = render( , ); const footerRow = container.querySelector(".card-footer-row") as HTMLElement; const sourceChip = footerRow.querySelector(":scope > .card-source-provenance") as HTMLElement; const rightCluster = footerRow.querySelector(":scope > .card-footer-row-right") as HTMLElement; const retryChip = rightCluster.querySelector(":scope > .card-retry-badge") as HTMLElement; const githubChip = rightCluster.querySelector(":scope > .card-github-tracking-chip") as HTMLElement; const timerChip = rightCluster.querySelector(":scope > .card-time-indicator") as HTMLElement; expect(footerRow).toBeTruthy(); expect(sourceChip).toBeTruthy(); expect(rightCluster).toBeTruthy(); expect(retryChip).toBeTruthy(); expect(githubChip).toBeTruthy(); expect(timerChip).toBeTruthy(); expect(getComputedStyle(sourceChip).marginLeft).not.toBe("auto"); expect(getComputedStyle(rightCluster).marginLeft).toBe("auto"); expect(Array.from(rightCluster.children).map((node) => (node as HTMLElement).className)).toEqual([ "card-github-tracking-chip card-github-tracking-link", expect.stringContaining("card-retry-badge"), "card-time-indicator", ]); }); });