@test-station/render-html 0.2.15 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +120 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@test-station/render-html",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/index.js CHANGED
@@ -630,6 +630,15 @@ export function renderHtmlReport(report, options = {}) {
630
630
  border-collapse: collapse;
631
631
  margin-top: 12px;
632
632
  font-size: 0.84rem;
633
+ table-layout: fixed;
634
+ }
635
+ .coverage-table col.coverage-table__fileCol { width: auto; }
636
+ .coverage-table col.coverage-table__metricCol { width: 170px; }
637
+ .coverage-table col.coverage-table__statementCol { width: 110px; }
638
+ .coverage-table col.coverage-table__attributionCol { width: 220px; }
639
+ .coverage-table td:first-child,
640
+ .coverage-table th:first-child {
641
+ width: auto;
633
642
  }
634
643
  .coverage-table th,
635
644
  .coverage-table td {
@@ -644,7 +653,64 @@ export function renderHtmlReport(report, options = {}) {
644
653
  text-transform: uppercase;
645
654
  letter-spacing: 0.08em;
646
655
  }
647
- .coverage-table code { font-size: 0.8rem; color: #d7e5ff; }
656
+ .coverage-table code {
657
+ display: block;
658
+ font-size: 0.8rem;
659
+ color: #d7e5ff;
660
+ white-space: pre-wrap;
661
+ word-break: break-word;
662
+ }
663
+ .coverage-table__metric {
664
+ display: grid;
665
+ gap: 8px;
666
+ min-width: 0;
667
+ }
668
+ .coverage-table__metricValue {
669
+ font-size: 0.92rem;
670
+ font-weight: 700;
671
+ letter-spacing: -0.02em;
672
+ font-variant-numeric: tabular-nums;
673
+ }
674
+ .coverage-table__metricBar {
675
+ height: 10px;
676
+ border-radius: 999px;
677
+ overflow: hidden;
678
+ background: rgba(255, 111, 143, 0.14);
679
+ border: 1px solid rgba(124, 160, 224, 0.12);
680
+ }
681
+ .coverage-table__metricFill {
682
+ height: 100%;
683
+ border-radius: inherit;
684
+ transition: width 180ms ease-out;
685
+ }
686
+ .coverage-table__statementCell {
687
+ text-align: center;
688
+ vertical-align: middle;
689
+ }
690
+ .coverage-table__statementIcon {
691
+ display: inline-flex;
692
+ align-items: center;
693
+ justify-content: center;
694
+ width: 32px;
695
+ height: 32px;
696
+ border-radius: 999px;
697
+ border: 1px solid rgba(124, 160, 224, 0.16);
698
+ background: rgba(11, 20, 36, 0.78);
699
+ color: var(--muted);
700
+ font-size: 0.78rem;
701
+ font-weight: 700;
702
+ letter-spacing: 0.04em;
703
+ cursor: help;
704
+ user-select: none;
705
+ }
706
+ .coverage-table__statementIcon--active {
707
+ color: var(--text);
708
+ border-color: color-mix(in srgb, var(--accent) 30%, transparent);
709
+ background: color-mix(in srgb, var(--accent) 12%, rgba(11, 20, 36, 0.82));
710
+ }
711
+ .coverage-table__statementIcon--disabled {
712
+ opacity: 0.45;
713
+ }
648
714
  .policy-block {
649
715
  margin: 0 0 16px;
650
716
  padding: 14px;
@@ -1402,10 +1468,10 @@ function renderCoverageBlock(coverage, rootDir) {
1402
1468
  return `
1403
1469
  <tr>
1404
1470
  <td><code>${escapeHtml(displayPath)}</code></td>
1405
- <td>${escapeHtml(file.lines ? `${file.lines.pct.toFixed(2)}%` : 'n/a')}</td>
1406
- <td>${escapeHtml(file.branches ? `${file.branches.pct.toFixed(2)}%` : 'n/a')}</td>
1407
- <td>${escapeHtml(file.functions ? `${file.functions.pct.toFixed(2)}%` : 'n/a')}</td>
1408
- <td>${escapeHtml(file.statements ? `${file.statements.pct.toFixed(2)}%` : 'n/a')}</td>
1471
+ <td>${renderCoverageTableMetric(file.lines, 'Lines')}</td>
1472
+ <td>${renderCoverageTableMetric(file.branches, 'Branches')}</td>
1473
+ <td>${renderCoverageTableMetric(file.functions, 'Functions')}</td>
1474
+ <td class="coverage-table__statementCell">${renderStatementsIcon(file.statements)}</td>
1409
1475
  ${showAttribution ? `<td>${escapeHtml(formatCoverageAttribution(file))}</td>` : ''}
1410
1476
  </tr>`;
1411
1477
  })
@@ -1421,6 +1487,14 @@ function renderCoverageBlock(coverage, rootDir) {
1421
1487
  <details>
1422
1488
  <summary>Coverage by file (${files.length} files, lowest line coverage first)</summary>
1423
1489
  <table class="coverage-table">
1490
+ <colgroup>
1491
+ <col class="coverage-table__fileCol" />
1492
+ <col class="coverage-table__metricCol" />
1493
+ <col class="coverage-table__metricCol" />
1494
+ <col class="coverage-table__metricCol" />
1495
+ <col class="coverage-table__statementCol" />
1496
+ ${showAttribution ? '<col class="coverage-table__attributionCol" />' : ''}
1497
+ </colgroup>
1424
1498
  <thead>
1425
1499
  <tr>
1426
1500
  <th>File</th>
@@ -1439,7 +1513,7 @@ function renderCoverageBlock(coverage, rootDir) {
1439
1513
  }
1440
1514
 
1441
1515
  function renderCoverageMetric(label, metric) {
1442
- const pct = metric ? metric.pct.toFixed(2) : '0.00';
1516
+ const pct = metric ? formatCoveragePercent(metric.pct) : '0.0';
1443
1517
  const counts = metric ? `${formatCoverageCount(metric.covered)}/${formatCoverageCount(metric.total)}` : 'n/a';
1444
1518
  const fillStyle = metric
1445
1519
  ? `width:${metric.pct.toFixed(2)}%;background:hsl(${coverageHue(metric.pct)} 68% 48%);`
@@ -1458,6 +1532,42 @@ function renderCoverageMetric(label, metric) {
1458
1532
  `;
1459
1533
  }
1460
1534
 
1535
+ function renderCoverageTableMetric(metric, label) {
1536
+ if (!hasCoverageMetric(metric)) {
1537
+ return `<div class="coverage-table__metric" title="${escapeHtml(`No ${label.toLowerCase()} coverage recorded`)}"><strong class="coverage-table__metricValue">n/a</strong><div class="coverage-table__metricBar" aria-hidden="true"><div class="coverage-table__metricFill" style="width:0%;background:hsl(0 68% 48%);"></div></div></div>`;
1538
+ }
1539
+
1540
+ const fillStyle = `width:${metric.pct.toFixed(2)}%;background:hsl(${coverageHue(metric.pct)} 68% 48%);`;
1541
+ return `
1542
+ <div class="coverage-table__metric" title="${escapeHtml(formatCoverageMetricTooltip(label, metric))}">
1543
+ <strong class="coverage-table__metricValue">${escapeHtml(formatCoveragePercent(metric.pct))}%</strong>
1544
+ <div class="coverage-table__metricBar" aria-hidden="true">
1545
+ <div class="coverage-table__metricFill" style="${fillStyle}"></div>
1546
+ </div>
1547
+ </div>
1548
+ `;
1549
+ }
1550
+
1551
+ function renderStatementsIcon(metric) {
1552
+ const active = hasCoverageMetric(metric);
1553
+ const title = active
1554
+ ? formatCoverageMetricTooltip('Statements', metric)
1555
+ : 'No statement coverage recorded';
1556
+ return `<span class="coverage-table__statementIcon coverage-table__statementIcon--${active ? 'active' : 'disabled'}" title="${escapeHtml(title)}" aria-label="${escapeHtml(title)}">ST</span>`;
1557
+ }
1558
+
1559
+ function hasCoverageMetric(metric) {
1560
+ return Boolean(metric) && (
1561
+ Number.isFinite(metric.pct)
1562
+ || Number.isFinite(metric.total) && metric.total > 0
1563
+ || Number.isFinite(metric.covered) && metric.covered > 0
1564
+ );
1565
+ }
1566
+
1567
+ function formatCoverageMetricTooltip(label, metric) {
1568
+ return `${label}: ${formatCoveragePercent(metric?.pct)}% (${formatCoverageCount(metric?.covered)}/${formatCoverageCount(metric?.total)})`;
1569
+ }
1570
+
1461
1571
  function formatCoverageAttribution(file) {
1462
1572
  const parts = [];
1463
1573
  if (file.module) {
@@ -1509,6 +1619,10 @@ function coverageHue(pct) {
1509
1619
  return `${Math.round((normalized / 100) * 120)}deg`;
1510
1620
  }
1511
1621
 
1622
+ function formatCoveragePercent(value) {
1623
+ return Number.isFinite(value) ? Number(value).toFixed(1) : '0.0';
1624
+ }
1625
+
1512
1626
  function formatCoverageCount(value) {
1513
1627
  if (!Number.isFinite(value)) {
1514
1628
  return '0';