From 53c580ce5fde89d4ef348bf091da8074c5a66aee Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Mon, 27 Jul 2026 15:25:44 -0400 Subject: [PATCH] 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) --- .../app/components/size-chip.test.tsx | 40 +++++++++++++++++++ apps/fabro-web/app/routes/runs.tsx | 11 ++--- 2 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 apps/fabro-web/app/components/size-chip.test.tsx diff --git a/apps/fabro-web/app/components/size-chip.test.tsx b/apps/fabro-web/app/components/size-chip.test.tsx new file mode 100644 index 000000000..b355f5db1 --- /dev/null +++ b/apps/fabro-web/app/components/size-chip.test.tsx @@ -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(); + }); + + expect(JSON.stringify(renderer!.toJSON())).toContain("M"); + }); + + test("appends the cost to the tooltip", () => { + expect(tooltipLabel()) + .toBe("Size M · $12.34"); + }); + + test("omits the cost when the run has no billing yet", () => { + expect(tooltipLabel()).toBe("Size M"); + expect(tooltipLabel()).toBe("Size M"); + }); + + test("calls out the tiers that warrant attention", () => { + expect(tooltipLabel()) + .toBe("Size L (risky) · $150.00"); + expect(tooltipLabel()).toBe("Size XL (unhealthy)"); + }); +}); diff --git a/apps/fabro-web/app/routes/runs.tsx b/apps/fabro-web/app/routes/runs.tsx index 7da0719ea..fe21e36c8 100644 --- a/apps/fabro-web/app/routes/runs.tsx +++ b/apps/fabro-web/app/routes/runs.tsx @@ -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 `
` 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 (
@@ -416,9 +417,9 @@ function PrCardFooter({ pr, actions }: { pr: RunItem; actions?: string[] }) { ))}
)} - {pr.elapsed != null && ( - - {pr.elapsed} + {pr.size != null && ( + + )}