import type { Component } from 'solid-js' import type { CoverageFile, CoverageReport, RunPhase } from '~/components/types' import { createMemo, createSignal, For, Show } from 'solid-js' export interface CoverageProps { report: CoverageReport | null phase: RunPhase enabled: boolean dir: string } function getCoverageColorClass(pct: number): string { if (pct >= 80) return 'text-emerald-400' if (pct >= 50) return 'text-amber-400' return 'text-red-400' } function getCoverageBgClass(pct: number): string { if (pct >= 80) return 'bg-emerald-500' if (pct >= 50) return 'bg-amber-500' return 'bg-red-500' } function getCoverageBorderClass(pct: number): string { if (pct >= 80) return 'border-emerald-500/20' if (pct >= 50) return 'border-amber-500/20' return 'border-red-500/20' } function clampPct(pct: number): number { return Math.min(pct, 100) } function normalizeMetricPct(covered: number, total: number, pct: number): number { if (total <= 0) return 100 if (covered <= 0) return 0 return Number.isFinite(pct) ? pct : 0 } function computePct(covered: number, total: number): number { if (total <= 0) return 100 if (covered <= 0) return 0 return Number(((covered / total) * 100).toFixed(1)) } export function formatCoveragePct(covered: number, total: number, pct: number): string { if (total <= 0) return '—' if (covered <= 0) return '0.0%' return `${pct.toFixed(1)}%` } interface FolderNode { name: string files: CoverageFile[] folders: FolderNode[] lines: { covered: number, total: number } functions: { covered: number, total: number } branches: { covered: number, total: number } } function sumBy(arr: T[], fn: (item: T) => number): number { return arr.reduce((acc, item) => acc + fn(item), 0) } function sortByFileName(a: CoverageFile, b: CoverageFile): number { const aName = a.path.split('/').pop() ?? a.path const bName = b.path.split('/').pop() ?? b.path return aName.localeCompare(bName) } function countFiles(folder: FolderNode): number { return folder.files.length + folder.folders.reduce((sum, child) => sum + countFiles(child), 0) } function buildFolderTree(files: CoverageFile[]): { folders: FolderNode[], rootFiles: CoverageFile[] } { interface BuildNode { name: string files: CoverageFile[] children: Map } const root: BuildNode = { name: '', files: [], children: new Map() } for (const file of files) { const parts = file.path.replace(/\\/g, '/').split('/') parts.pop() let current = root for (const part of parts) { if (!current.children.has(part)) current.children.set(part, { name: part, files: [], children: new Map() }) current = current.children.get(part)! } current.files.push(file) } function getAllFiles(node: BuildNode): CoverageFile[] { const result = [...node.files] for (const child of node.children.values()) result.push(...getAllFiles(child)) return result } function convert(node: BuildNode): FolderNode { const allFiles = getAllFiles(node) const folders = Array.from(node.children.values()) .map(convert) .sort((a, b) => a.name.localeCompare(b.name)) return { name: node.name, files: [...node.files].sort(sortByFileName), folders, lines: { covered: sumBy(allFiles, f => f.lines.covered), total: sumBy(allFiles, f => f.lines.total) }, functions: { covered: sumBy(allFiles, f => f.functions.covered), total: sumBy(allFiles, f => f.functions.total) }, branches: { covered: sumBy(allFiles, f => f.branches.covered), total: sumBy(allFiles, f => f.branches.total) }, } } const rootNode = convert(root) return { folders: rootNode.folders, rootFiles: rootNode.files } } function CoverageBar(props: { pct: number }) { return (
) } function CoverageCell(props: { covered: number, total: number, pct: number }) { const pct = () => normalizeMetricPct(props.covered, props.total, props.pct) const hasData = () => props.total > 0 return (
{formatCoveragePct(props.covered, props.total, props.pct)}
) } function SummaryMetricCard(props: { label: string icon: string covered: number total: number pct: number }) { const displayPct = () => normalizeMetricPct(props.covered, props.total, props.pct) const hasData = () => props.total > 0 return (

{props.label}

{props.covered} {' / '} {props.total}

{hasData() ? `${displayPct().toFixed(1)}%` : '—'}

) } const GRID_3 = 'grid-cols-[1fr_5.5rem_5.5rem_5.5rem]' const GRID_2 = 'grid-cols-[1fr_5.5rem_5.5rem]' function FolderTreeItem(props: { folder: FolderNode, depth: number, hasBranches: boolean }) { const [expanded, setExpanded] = createSignal(true) const fnPct = () => computePct(props.folder.functions.covered, props.folder.functions.total) const lnPct = () => computePct(props.folder.lines.covered, props.folder.lines.total) const brPct = () => computePct(props.folder.branches.covered, props.folder.branches.total) const totalFiles = () => countFiles(props.folder) const folderIconColor = () => props.folder.lines.total <= 0 ? 'text-gray-500' : getCoverageColorClass(lnPct()) return (
setExpanded(prev => !prev)} >
{props.folder.name} {totalFiles()}
{subfolder => ( )} {file => ( )}
) } function FileRow(props: { file: CoverageFile, depth: number, hasBranches: boolean }) { const fileName = () => props.file.path.split('/').pop() ?? props.file.path const linePct = () => normalizeMetricPct(props.file.lines.covered, props.file.lines.total, props.file.lines.pct) const fileIconColor = () => props.file.lines.total <= 0 ? 'text-gray-600' : getCoverageColorClass(linePct()) return (
{fileName()}
) } const Coverage: Component = (props) => { const totals = () => props.report?.totals const files = () => props.report?.files ?? [] const tree = createMemo(() => buildFolderTree(files())) const fileCount = createMemo(() => files().length) const hasBranchData = createMemo(() => { const summary = totals() return (summary?.branches.total ?? 0) > 0 || files().some(file => file.branches.total > 0) }) return (

No coverage data from Bun

Enable it in bunfig.toml with [test].coverage = true

)} >
Collecting coverage...
)} > No coverage report available yet. The report is expected at {' '} {props.dir} /lcov.info .
)} > {summary => ( <>
Files {fileCount()} {' '} file {fileCount() === 1 ? '' : 's'}
Funcs Lines Branch
{folder => ( )} {file => ( )}

No files with coverage data.

)}
) } export default Coverage