From 16aa089b91bb8b323a28c5bbda2ba59c8fde9e5b Mon Sep 17 00:00:00 2001 From: Ashish Huddar Date: Mon, 13 Apr 2026 23:46:48 +0530 Subject: [PATCH] fix(web): link passing CI check chips to their check run URL Passing CI check chips in the session kanban card were rendered as plain elements, so clicking them did nothing even though check.url was available in DashboardCICheck. Wrap each chip in an tag when check.url is present, matching the pattern already used by CICheckList in CIBadge.tsx. Stop click propagation so the chip click does not bubble up to the card's navigation handler. Also fix a pre-existing broken test from #1218 that still expected the old terminal link href without the #session-terminal-section anchor, which was blocking the test suite. Fixes #1209 --- .../web/src/__tests__/components.test.tsx | 64 ++++++++++++++++++- packages/web/src/components/SessionCard.tsx | 34 +++++++--- 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/packages/web/src/__tests__/components.test.tsx b/packages/web/src/__tests__/components.test.tsx index 982d467d4..706d73ac4 100644 --- a/packages/web/src/__tests__/components.test.tsx +++ b/packages/web/src/__tests__/components.test.tsx @@ -208,7 +208,7 @@ describe("SessionCard", () => { const session = makeSession({ id: "backend-5" }); render(); const link = screen.getByText("terminal"); - expect(link).toHaveAttribute("href", "/sessions/backend-5"); + expect(link).toHaveAttribute("href", "/sessions/backend-5#session-terminal-section"); }); it("shows restore button when agent has exited", () => { @@ -269,6 +269,68 @@ describe("SessionCard", () => { expect(onMerge).toHaveBeenCalledWith(42); }); + it("renders passing CI check chips as hyperlinks when url is present", () => { + const pr = makePR({ + state: "open", + ciStatus: "passing", + ciChecks: [ + { name: "lint-and-type-checks", status: "passed", url: "https://github.com/owner/repo/runs/111" }, + { name: "tests", status: "passed", url: "https://github.com/owner/repo/runs/222" }, + { name: "no-url-check", status: "passed" }, + ], + reviewDecision: "approved", + mergeability: { + mergeable: true, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: [], + }, + }); + const session = makeSession({ status: "mergeable", activity: "idle", pr }); + render(); + + const lintLink = screen.getByRole("link", { name: /lint-and-type-checks/ }); + expect(lintLink).toHaveAttribute("href", "https://github.com/owner/repo/runs/111"); + expect(lintLink).toHaveAttribute("target", "_blank"); + expect(lintLink).toHaveAttribute("rel", "noopener noreferrer"); + + const testsLink = screen.getByRole("link", { name: /^tests$/ }); + expect(testsLink).toHaveAttribute("href", "https://github.com/owner/repo/runs/222"); + + // Check without url should still render as plain text, not a link + expect(screen.queryByRole("link", { name: /no-url-check/ })).not.toBeInTheDocument(); + expect(screen.getByText("no-url-check")).toBeInTheDocument(); + }); + + it("stops propagation when clicking a passing CI chip link", () => { + const onClick = vi.fn(); + const pr = makePR({ + state: "open", + ciStatus: "passing", + ciChecks: [ + { name: "build", status: "passed", url: "https://github.com/owner/repo/runs/333" }, + ], + reviewDecision: "approved", + mergeability: { + mergeable: true, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: [], + }, + }); + const session = makeSession({ status: "mergeable", activity: "idle", pr }); + render( +
+ +
, + ); + const link = screen.getByRole("link", { name: /build/ }); + fireEvent.click(link); + expect(onClick).not.toHaveBeenCalled(); + }); + it("shows CI failing alert", () => { const pr = makePR({ state: "open", diff --git a/packages/web/src/components/SessionCard.tsx b/packages/web/src/components/SessionCard.tsx index dface1c17..04181e43b 100644 --- a/packages/web/src/components/SessionCard.tsx +++ b/packages/web/src/components/SessionCard.tsx @@ -571,14 +571,32 @@ function SessionCardView({ session, onSend, onKill, onMerge, onRestore }: Sessio {visiblePassingChecks.length > 0 && (
- {visiblePassingChecks.map((check) => ( - - - {check.name} - - ))} + {visiblePassingChecks.map((check) => { + const chipContent = ( + <> + + {check.name} + + ); + return check.url ? ( + e.stopPropagation()} + > + {chipContent} + + ) : ( + + {chipContent} + + ); + })}
)}