Swap the board card's elapsed time for a size chip

The board cards showed wall-clock duration in the footer's bottom-right
corner. Replace it with the same SizeChip the list view and run detail
header use, so the cost signal is consistent across all three views.

The chip inherits the tooltip, which names the tier and adds the cost
once a run has terminal billing.

Add SizeChip tests pinning the tooltip label for each tier.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-07-27 15:25:44 -04:00
parent 991f160a0b
commit 53c580ce5f
No known key found for this signature in database
2 changed files with 46 additions and 5 deletions

View file

@ -0,0 +1,40 @@
import { describe, expect, test } from "bun:test";
import TestRenderer, { act } from "react-test-renderer";
import { SizeChip } from "./size-chip";
import { Tooltip } from "./ui";
function tooltipLabel(element: React.ReactElement): string {
let renderer: TestRenderer.ReactTestRenderer | undefined;
act(() => {
renderer = TestRenderer.create(element);
});
return renderer!.root.findByType(Tooltip).props.label as string;
}
describe("SizeChip", () => {
test("renders the size letter", () => {
let renderer: TestRenderer.ReactTestRenderer | undefined;
act(() => {
renderer = TestRenderer.create(<SizeChip size="M" />);
});
expect(JSON.stringify(renderer!.toJSON())).toContain("M");
});
test("appends the cost to the tooltip", () => {
expect(tooltipLabel(<SizeChip size="M" totalUsdMicros={12_340_000} />))
.toBe("Size M · $12.34");
});
test("omits the cost when the run has no billing yet", () => {
expect(tooltipLabel(<SizeChip size="M" />)).toBe("Size M");
expect(tooltipLabel(<SizeChip size="M" totalUsdMicros={null} />)).toBe("Size M");
});
test("calls out the tiers that warrant attention", () => {
expect(tooltipLabel(<SizeChip size="L" totalUsdMicros={150_000_000} />))
.toBe("Size L (risky) · $150.00");
expect(tooltipLabel(<SizeChip size="XL" />)).toBe("Size XL (unhealthy)");
});
});

View file

@ -26,6 +26,7 @@ import { ciConfig, columnForRun, columnStatusDisplay, columnStatuses, deriveCiSt
import type { CiStatus, CheckRun, CheckStatus, RunItem } from "../data/runs";
import { EmptyState } from "../components/state";
import { PullRequestChip } from "../components/pull-request-chip";
import { SizeChip } from "../components/size-chip";
import {
summarizeBatchLifecycleAction,
} from "../components/runs-list/batch-lifecycle";
@ -345,7 +346,7 @@ function PrCard({
// All inline footer metadata on PrCard belongs in this one row. Adding a new
// piece as a sibling `<div>` below the card body recreates a recurring bug
// where stats stack onto separate lines instead of sitting next to elapsed/actions.
// where stats stack onto separate lines instead of sitting next to size/actions.
function PrCardFooter({ pr, actions }: { pr: RunItem; actions?: string[] }) {
const hasActions = actions != null && actions.length > 0;
const hasStats =
@ -354,7 +355,7 @@ function PrCardFooter({ pr, actions }: { pr: RunItem; actions?: string[] }) {
(pr.additions != null && pr.additions !== 0) ||
(pr.deletions != null && pr.deletions !== 0);
if (!hasStats && !hasActions && pr.elapsed == null) return null;
if (!hasStats && !hasActions && pr.size == null) return null;
return (
<div className="mt-3 flex items-center gap-3 font-mono text-xs">
@ -416,9 +417,9 @@ function PrCardFooter({ pr, actions }: { pr: RunItem; actions?: string[] }) {
))}
</div>
)}
{pr.elapsed != null && (
<span className={`text-fg-muted ${hasActions ? "" : "ml-auto"}`}>
{pr.elapsed}
{pr.size != null && (
<span className={`inline-flex ${hasActions ? "" : "ml-auto"}`}>
<SizeChip size={pr.size} totalUsdMicros={pr.totalUsdMicros} />
</span>
)}
</div>