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 <span> elements, so clicking them did nothing even though
check.url was available in DashboardCICheck. Wrap each chip in an
<a> 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
This commit is contained in:
Ashish Huddar 2026-04-13 23:46:48 +05:30
parent f22d2d9ccc
commit 16aa089b91
2 changed files with 89 additions and 9 deletions

View File

@ -208,7 +208,7 @@ describe("SessionCard", () => {
const session = makeSession({ id: "backend-5" });
render(<SessionCard session={session} />);
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(<SessionCard session={session} />);
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(
<div onClick={onClick}>
<SessionCard session={session} />
</div>,
);
const link = screen.getByRole("link", { name: /build/ });
fireEvent.click(link);
expect(onClick).not.toHaveBeenCalled();
});
it("shows CI failing alert", () => {
const pr = makePR({
state: "open",

View File

@ -571,14 +571,32 @@ function SessionCardView({ session, onSend, onKill, onMerge, onRestore }: Sessio
{visiblePassingChecks.length > 0 && (
<div className="card__ci">
{visiblePassingChecks.map((check) => (
<span key={check.name} className="ci-chip ci-chip--pass">
<svg width="8" height="8" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 6 9 17l-5-5" />
</svg>
{check.name}
</span>
))}
{visiblePassingChecks.map((check) => {
const chipContent = (
<>
<svg width="8" height="8" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 6 9 17l-5-5" />
</svg>
{check.name}
</>
);
return check.url ? (
<a
key={check.name}
href={check.url}
target="_blank"
rel="noopener noreferrer"
className="ci-chip ci-chip--pass"
onClick={(e) => e.stopPropagation()}
>
{chipContent}
</a>
) : (
<span key={check.name} className="ci-chip ci-chip--pass">
{chipContent}
</span>
);
})}
</div>
)}