/** Coverage tool window: one tab per recent suite, a sortable tree of directories and files. */ import { useMemo, useState } from "react"; import clsx from "clsx"; import { executeAction } from "@/platform/actions"; import { useProject } from "@/platform/project"; import { EmptyState, Icon, IconButton, Tabs, Tree, fileIconName, T } from "@/ui"; import { editorService } from "@/workbench/contributions"; import { coverageTree, formatPercent, percent, sortTree, type CoverageNode, type SortKey } from "./model"; import { activeSuite, closeSuite, selectSuite, setCoverageVisible, useCoverage } from "./store"; import styles from "./Coverage.module.css"; export function CoverageTabs() { const suites = useCoverage((s) => s.suites); const activeId = useCoverage((s) => s.activeId); if (!suites.length) return null; return ( ({ id: s.id, label: s.name, closable: true, tooltip: `${s.data.runner}, ${new Date(s.createdAt).toLocaleTimeString()}`, }))} /> ); } export function CoverageHeaderActions() { const visible = useCoverage((s) => s.visible); const hasSuite = useCoverage((s) => !!s.activeId); return ( <> setCoverageVisible(!visible)} /> ); } function Bar({ covered, total }: { covered: number; total: number }) { const p = percent(covered, total); return ( {p !== null && ( ); } const COLUMNS: Array<{ key: SortKey; label: string }> = [ { key: "name", label: "Element" }, { key: "lines", label: "Line, %" }, { key: "branches", label: "Branch, %" }, ]; export function CoverageView() { const suite = useCoverage((s) => activeSuite(s)); const visible = useCoverage((s) => s.visible); const root = useProject((s) => s.project?.root ?? ""); const [sort, setSort] = useState<{ key: SortKey; ascending: boolean }>({ key: "name", ascending: true }); const [expanded, setExpanded] = useState | null>(null); const roots = useMemo( () => (suite ? sortTree(coverageTree(suite.data, root), sort.key, sort.ascending) : []), [suite, root, sort], ); const effectiveExpanded = useMemo( () => expanded ?? new Set(roots.filter((n) => n.dir).map((n) => n.path)), [expanded, roots], ); if (!suite) { return ( Run a configuration with coverage to see which lines its tests execute. ); } const branches = suite.data.hasBranches; const columns = branches ? COLUMNS : COLUMNS.slice(0, 2); const toggleSort = (key: SortKey) => setSort((s) => (s.key === key ? { key, ascending: !s.ascending } : { key, ascending: key === "name" })); return (
{!visible && (
Coverage is hidden in editors and the project view.
)}
{columns.map((c) => ( ))}
aria-label="Coverage" className={styles.tree} roots={roots} getKey={(n) => n.path} getLabel={(n) => n.name} getChildren={(n) => (n.dir ? n.children : undefined)} isExpandable={(n) => n.dir} expanded={effectiveExpanded} onExpandedChange={(next) => setExpanded(next)} onActivate={(n) => { if (!n.dir) void editorService().openFile(n.path); }} renderNode={(n) => ( {n.name} {branches && } )} empty={} />
); }