{ "name": "coverage-report", "type": "registry:ui", "title": "Coverage Report", "description": "Coverage per file, ranked by uncovered lines rather than by percentage. A 400-line file at 92% has 32 untested lines; a 12-line file at 60% has 5 — and percentage ranks the small one first.", "category": "Build & Perf", "dependencies": [ "lucide-react@^1.39.0" ], "registryDependencies": [ "badge", "lib-motion", "lib-styles", "lib-utils" ], "files": [ { "path": "components/ui/coverage-report.tsx", "type": "registry:ui", "content": "'use client'\n\nimport { useMemo, useState, type ComponentProps, type ReactNode } from 'react'\nimport { ChevronDown, ChevronRight } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { enterFade, growIn } from '@/lib/motion'\nimport { focusRing, interactive, radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * Test coverage per file, with the uncovered lines named.\n *\n * Sorted by uncovered lines rather than by percentage. A 400-line file at 92%\n * has 32 untested lines; a 12-line file at 60% has 5. Percentage ranks the\n * small file first and that is exactly backwards for someone deciding what to\n * test next.\n *\n * Four metrics are kept apart. Statement coverage flatters: a file can hit\n * every statement and miss half its branches, which is where the bugs are.\n * Branch coverage is the one shown in the bar.\n *\n * The threshold is drawn as a line on each bar, so \"below target\" is visible\n * per row without reading numbers.\n */\nexport type CoverageFile = {\n path: string\n statements: number\n branches: number\n functions: number\n lines: number\n /** Line numbers with no coverage. */\n uncovered?: number[]\n}\n\nfunction tone(value: number, threshold: number) {\n if (value >= threshold) return 'var(--green)'\n if (value >= threshold - 0.15) return 'var(--amber)'\n return 'var(--destructive)'\n}\n\nfunction CoverageReport({\n files,\n threshold = 0.8,\n metric = 'branches',\n thresholdLabel = 'target',\n uncoveredLabel = 'uncovered lines',\n totalLabel = 'Total',\n metricLabels = {\n statements: 'Stmts',\n branches: 'Branch',\n functions: 'Funcs',\n lines: 'Lines',\n },\n className,\n ...props\n}: Omit, 'children'> & {\n files: CoverageFile[]\n /** Ratio 0–1. Drawn as a line on every bar. */\n threshold?: number\n /** Which metric the bar shows. Branches by default — statements flatter. */\n metric?: 'statements' | 'branches' | 'functions' | 'lines'\n thresholdLabel?: ReactNode\n uncoveredLabel?: ReactNode\n totalLabel?: ReactNode\n metricLabels?: Record<'statements' | 'branches' | 'functions' | 'lines', ReactNode>\n}) {\n const [open, setOpen] = useState(null)\n\n // Ranked by untested lines, not by percentage — that is the actionable order.\n const rows = useMemo(\n () =>\n [...files].sort(\n (a, b) => (b.uncovered?.length ?? 0) - (a.uncovered?.length ?? 0) || a[metric] - b[metric],\n ),\n [files, metric],\n )\n\n const totals = useMemo(() => {\n if (files.length === 0) return null\n const sum = (key: keyof Omit) =>\n files.reduce((total, file) => total + file[key], 0) / files.length\n return {\n statements: sum('statements'),\n branches: sum('branches'),\n functions: sum('functions'),\n lines: sum('lines'),\n }\n }, [files])\n\n const pct = (value: number) => `${Math.round(value * 100)}%`\n\n return (\n \n {totals && (\n
\n {totalLabel}\n {(['statements', 'branches', 'functions', 'lines'] as const).map((key) => (\n \n {metricLabels[key]} \n {pct(totals[key])}\n \n ))}\n \n {thresholdLabel} {pct(threshold)}\n \n
\n )}\n\n
    \n {rows.map((file) => {\n const value = file[metric]\n const expanded = open === file.path\n const missing = file.uncovered ?? []\n\n return (\n
  • \n setOpen(expanded ? null : file.path)}\n className={cn(\n 'flex w-full items-center gap-3 p-3 text-start',\n missing.length > 0 && interactive,\n focusRing,\n )}\n >\n {missing.length > 0 ? (\n expanded ? (\n \n ) : (\n \n )\n ) : (\n \n )}\n\n {file.path}\n\n {missing.length > 0 && (\n = threshold ? 'neutral' : 'amber'}>\n {missing.length}\n \n )}\n\n \n\n \n {pct(value)}\n \n \n\n {expanded && missing.length > 0 && (\n
    \n

    {uncoveredLabel}

    \n

    {missing.join(', ')}

    \n
    \n )}\n
  • \n )\n })}\n
\n \n )\n}\n\nexport { CoverageReport }\n" } ] }