Skip to content

Commit e96b8bd

Browse files
committed
Align table columns, adaptive bar sizing, loading screen improvements
* Split coverage fractions into numerator/denominator columns for vertical alignment of the "/" separator (colspan 4 per coverage group) * Add fmt() helper for comma-separated numbers (e.g. 2,252) * Pluralize "file" vs "files" when filtering * Hide bars if < 100px available, cap at 200px max * All bars forced to identical width via min/max/width lock
1 parent d5ab84c commit e96b8bd

File tree

8 files changed

+221
-121
lines changed

8 files changed

+221
-121
lines changed

assets/stylesheets/screen.css

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -476,32 +476,59 @@ a.src_link {
476476
word-break: break-all;
477477
}
478478

479-
/* --- Coverage bar ------------------------------------------ */
479+
/* --- Coverage columns -------------------------------------- */
480480

481-
.coverage-cell {
482-
display: flex;
483-
align-items: center;
484-
gap: var(--sp-2);
481+
.cell--bar {
482+
padding-right: 0;
485483
}
486484

487-
.coverage-cell__pct {
488-
flex-shrink: 0;
485+
.cell--pct {
486+
text-align: right;
489487
font-variant-numeric: tabular-nums;
488+
white-space: nowrap;
489+
padding-left: var(--sp-2);
490+
padding-right: var(--sp-1);
491+
}
492+
493+
table.file_list td.cell--numerator {
490494
text-align: right;
495+
font-variant-numeric: tabular-nums;
496+
color: var(--text-secondary);
497+
font-size: 14px;
491498
white-space: nowrap;
499+
padding-left: var(--sp-1);
500+
padding-right: 0;
501+
}
502+
503+
table.file_list td.cell--denominator {
504+
text-align: left;
505+
font-variant-numeric: tabular-nums;
506+
color: var(--text-secondary);
507+
font-size: 14px;
508+
white-space: nowrap;
509+
padding-left: 0;
510+
padding-right: var(--sp-1);
511+
}
512+
513+
table.file_list .totals-row td.cell--numerator {
514+
color: var(--text);
515+
padding-right: 0;
516+
}
517+
518+
table.file_list .totals-row td.cell--denominator {
519+
color: var(--text);
520+
padding-left: 0;
492521
}
493522

494523
.coverage-cell__fraction {
495-
flex-shrink: 0;
496524
font-variant-numeric: tabular-nums;
497525
color: var(--text-secondary);
498526
font-size: 14px;
499527
white-space: nowrap;
500528
}
501529

502530
.coverage-bar {
503-
flex-shrink: 0;
504-
width: 80px;
531+
width: 100%;
505532
height: var(--bar-height);
506533
background: var(--bar-bg);
507534
border-radius: 6px;
@@ -521,9 +548,9 @@ a.src_link {
521548

522549
/* --- Color utility classes --------------------------------- */
523550

524-
.green { color: var(--green); }
525-
.red { color: var(--red); }
526-
.yellow { color: var(--yellow); }
551+
.green, table.file_list td.green { color: var(--green); }
552+
.red, table.file_list td.red { color: var(--red); }
553+
.yellow, table.file_list td.yellow { color: var(--yellow); }
527554
.missed-branch-text { color: var(--missed-branch-text); }
528555
.missed-method-text-color { color: var(--missed-method-text); }
529556

lib/simplecov-html/view_helpers.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ def to_id(value)
7979
value.sub(/\A[^a-zA-Z]+/, "").gsub(/[^a-zA-Z0-9\-_]/, "")
8080
end
8181

82+
def fmt(number)
83+
number.to_s.gsub(/(\d)(?=(\d{3})+(?!\d))/, '\\1,')
84+
end
85+
8286
def coverage_summary(stats, show_method_toggle: false)
8387
@_summary = {
8488
line: build_stats(stats.fetch(:covered_lines), stats.fetch(:total_lines)),

public/application.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/application.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app.ts

Lines changed: 129 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,33 @@ function pctClass(pct: number): string {
5757
return 'red';
5858
}
5959

60-
function buildCoverageCell(covered: number, total: number): string {
61-
if (total === 0) return '';
60+
function fmtNum(n: number): string {
61+
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
62+
}
63+
64+
function updateCoverageCells(
65+
container: Element,
66+
prefix: string,
67+
covered: number,
68+
total: number
69+
): void {
70+
const barEl = $(prefix + '-bar', container);
71+
const pctEl = $(prefix + '-pct', container);
72+
const numEl = $(prefix + '-num', container);
73+
const denEl = $(prefix + '-den', container);
74+
if (total === 0) {
75+
if (barEl) barEl.innerHTML = '';
76+
if (pctEl) { pctEl.textContent = ''; pctEl.className = pctEl.className.replace(/green|yellow|red/g, '').trim(); }
77+
if (numEl) numEl.textContent = '';
78+
if (denEl) denEl.textContent = '';
79+
return;
80+
}
6281
const p = (covered * 100.0) / total;
63-
return '<div class="coverage-cell">' +
64-
'<div class="coverage-bar"><div class="coverage-bar__fill coverage-bar__fill--' + pctClass(p) + '" style="width: ' + p.toFixed(1) + '%"></div></div>' +
65-
'<span class="coverage-cell__pct ' + pctClass(p) + '">' + p.toFixed(2) + '%</span>' +
66-
'<span class="coverage-cell__fraction">' + covered + '/' + total + '</span>' +
67-
'</div>';
82+
const cls = pctClass(p);
83+
if (barEl) barEl.innerHTML = '<div class="coverage-bar"><div class="coverage-bar__fill coverage-bar__fill--' + cls + '" style="width: ' + p.toFixed(1) + '%"></div></div>';
84+
if (pctEl) { pctEl.textContent = p.toFixed(2) + '%'; pctEl.className = pctEl.className.replace(/green|yellow|red/g, '').trim() + ' ' + cls; }
85+
if (numEl) numEl.textContent = fmtNum(covered) + '/';
86+
if (denEl) denEl.textContent = fmtNum(total);
6887
}
6988

7089
// --- Sort state -----------------------------------------------
@@ -110,9 +129,11 @@ function sortTable(table: Element, colIndex: number): void {
110129
rows.forEach(row => tbody.appendChild(row));
111130

112131
// Update sort indicators
113-
$$('th', table).forEach((th, i) => {
132+
let idx = 0;
133+
$$('thead tr:first-child th', table).forEach((th) => {
114134
th.classList.remove('sorting_asc', 'sorting_desc', 'sorting');
115-
th.classList.add(i === colIndex ? (dir === 'asc' ? 'sorting_asc' : 'sorting_desc') : 'sorting');
135+
th.classList.add(idx === colIndex ? (dir === 'asc' ? 'sorting_asc' : 'sorting_desc') : 'sorting');
136+
idx += parseInt(th.getAttribute('colspan') || '1', 10);
116137
});
117138
}
118139

@@ -219,31 +240,34 @@ function updateTotalsRow(container: Element): void {
219240
}
220241

221242
const fileCount = $('.t-file-count', container);
222-
if (fileCount) fileCount.textContent = rows.length + ' files';
243+
const totalFiles = parseInt(container.getAttribute('data-total-files') || '0', 10);
244+
if (fileCount) {
245+
const label = rows.length === 1 ? ' file' : ' files';
246+
fileCount.textContent = rows.length === totalFiles
247+
? fmtNum(totalFiles) + label
248+
: fmtNum(rows.length) + '/' + fmtNum(totalFiles) + label;
249+
}
223250

224251
const coveredLines = sumData('coveredLines');
225252
const relevantLines = sumData('relevantLines');
226-
const lineCov = $('.t-totals__line-coverage', container);
227-
if (lineCov) lineCov.innerHTML = buildCoverageCell(coveredLines, relevantLines);
253+
updateCoverageCells(container, '.t-totals__line', coveredLines, relevantLines);
228254

229255
const numberCells = $$('.totals-row .cell--number', container);
230-
if (numberCells[0]) numberCells[0].textContent = relevantLines ? String(relevantLines) : '';
256+
if (numberCells[0]) numberCells[0].textContent = relevantLines ? fmtNum(relevantLines) : '';
231257

232-
const branchCov = $('.t-totals__branch-coverage', container);
233-
if (branchCov) {
258+
if ($('.t-totals__branch-pct', container)) {
234259
const coveredBranches = sumData('coveredBranches');
235260
const totalBranches = sumData('totalBranches');
236-
branchCov.innerHTML = buildCoverageCell(coveredBranches, totalBranches);
237-
if (numberCells[1]) numberCells[1].textContent = totalBranches ? String(totalBranches) : '';
261+
updateCoverageCells(container, '.t-totals__branch', coveredBranches, totalBranches);
262+
if (numberCells[1]) numberCells[1].textContent = totalBranches ? fmtNum(totalBranches) : '';
238263
}
239264

240-
const methodCov = $('.t-totals__method-coverage', container);
241-
if (methodCov) {
265+
if ($('.t-totals__method-pct', container)) {
242266
const coveredMethods = sumData('coveredMethods');
243267
const totalMethods = sumData('totalMethods');
244-
methodCov.innerHTML = buildCoverageCell(coveredMethods, totalMethods);
245-
const idx = branchCov ? 2 : 1;
246-
if (numberCells[idx]) numberCells[idx].textContent = totalMethods ? String(totalMethods) : '';
268+
updateCoverageCells(container, '.t-totals__method', coveredMethods, totalMethods);
269+
const numIdx = $('.t-totals__branch-pct', container) ? 2 : 1;
270+
if (numberCells[numIdx]) numberCells[numIdx].textContent = totalMethods ? fmtNum(totalMethods) : '';
247271
}
248272
}
249273

@@ -314,6 +338,11 @@ function showFileList(tabId: string): void {
314338
if (target) target.style.display = '';
315339
}
316340
}
341+
// Only equalize bars if the wrapper is actually visible
342+
const wrapper = document.getElementById('wrapper');
343+
if (wrapper && !wrapper.classList.contains('hide')) {
344+
equalizeBarWidths();
345+
}
317346
}
318347

319348
function navigateToHash(): void {
@@ -385,12 +414,15 @@ document.addEventListener('DOMContentLoaded', function () {
385414
});
386415
})();
387416

388-
// Table sorting
417+
// Table sorting — compute td index from th colspan
389418
$$('table.file_list').forEach(table => {
390-
$$('thead tr:first-child th', table).forEach((th, colIndex) => {
419+
let tdIndex = 0;
420+
$$('thead tr:first-child th', table).forEach((th) => {
421+
const myTdIndex = tdIndex;
391422
th.classList.add('sorting');
392423
(th as HTMLElement).style.cursor = 'pointer';
393-
th.addEventListener('click', () => sortTable(table, colIndex));
424+
th.addEventListener('click', () => sortTable(table, myTdIndex));
425+
tdIndex += parseInt(th.getAttribute('colspan') || '1', 10);
394426
});
395427
});
396428

@@ -483,19 +515,84 @@ document.addEventListener('DOMContentLoaded', function () {
483515
window.location.hash = this.getAttribute('href')!.replace('#', '#_');
484516
});
485517

518+
// Equalize bar column widths within each table
519+
function setBarWidth(bars: Element[], headers: Element[], px: number): void {
520+
const w = px + 'px';
521+
headers.forEach(h => h.setAttribute('colspan', '4'));
522+
bars.forEach(b => {
523+
const s = (b as HTMLElement).style;
524+
s.display = '';
525+
s.width = w; s.minWidth = w; s.maxWidth = w;
526+
});
527+
}
528+
529+
function hideBars(bars: Element[], headers: Element[]): void {
530+
headers.forEach(h => h.setAttribute('colspan', '3'));
531+
bars.forEach(b => {
532+
const s = (b as HTMLElement).style;
533+
s.display = 'none'; s.width = ''; s.minWidth = ''; s.maxWidth = '';
534+
});
535+
}
536+
537+
function equalizeBarWidths(): void {
538+
// Only measure visible containers
539+
$$('.file_list_container').forEach(container => {
540+
if ((container as HTMLElement).style.display === 'none') return;
541+
if (container.offsetWidth === 0) return;
542+
543+
const table = $('table.file_list', container) as HTMLTableElement | null;
544+
if (!table) return;
545+
const bars = $$('td.cell--bar', table);
546+
const headers = $$('th[colspan]', table);
547+
if (bars.length === 0) return;
548+
549+
const wrapper = table.closest('.file_list--responsive') as HTMLElement | null;
550+
if (!wrapper) return;
551+
552+
const firstDataRow = $('tbody tr', table) || $('thead tr.totals-row', table);
553+
const barsPerRow = firstDataRow ? $$('td.cell--bar', firstDataRow).length : 1;
554+
555+
// Step 1: Hide bars, measure content width with table at auto
556+
hideBars(bars, headers);
557+
table.style.width = 'auto';
558+
void table.offsetWidth;
559+
const contentWidth = table.scrollWidth;
560+
561+
// Step 2: Restore table width, measure available space
562+
table.style.width = '';
563+
void table.offsetWidth;
564+
const availableWidth = wrapper.clientWidth;
565+
566+
// Step 3: Calculate bar width
567+
const totalBarSpace = availableWidth - contentWidth;
568+
const perBar = Math.floor(totalBarSpace / barsPerRow);
569+
570+
if (perBar < 100) return; // keep bars hidden
571+
572+
setBarWidth(bars, headers, Math.min(perBar, 200));
573+
});
574+
}
575+
576+
// Defer until after wrapper is visible
577+
window.addEventListener('resize', equalizeBarWidths);
578+
486579
// Initial state
487580
navigateToHash();
488581

489582
// Finalize loading
490583
clearInterval((window as any)._simplecovLoadingTimer);
584+
clearTimeout((window as any)._simplecovShowTimeout);
491585

492-
const loading = document.getElementById('loading');
493-
if (loading) {
494-
loading.style.transition = 'opacity 0.3s';
495-
loading.style.opacity = '0';
496-
setTimeout(() => { loading.style.display = 'none'; }, 300);
586+
const loadingEl = document.getElementById('loading');
587+
if (loadingEl) {
588+
loadingEl.style.transition = 'opacity 0.3s';
589+
loadingEl.style.opacity = '0';
590+
setTimeout(() => { loadingEl.style.display = 'none'; }, 300);
497591
}
498592

499-
const wrapper = document.getElementById('wrapper');
500-
if (wrapper) wrapper.classList.remove('hide');
593+
const wrapperEl = document.getElementById('wrapper');
594+
if (wrapperEl) wrapperEl.classList.remove('hide');
595+
596+
// Equalize bar widths now that wrapper is visible
597+
equalizeBarWidths();
501598
});

test/test_simple_cov-html.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def test_output_header_coverage
4343

4444
def test_output_line_coverages
4545
html_doc = format_results(CoverageFixtures::ALL_FIXTURES)
46-
pcts = html_doc.css("div#AllFiles table.file_list tr.t-file td.t-file__coverage .coverage-cell__pct")
46+
pcts = html_doc.css("div#AllFiles table.file_list tr.t-file td.cell--line-pct")
4747
table = pcts.map { |m| m.content.strip }
4848

4949
assert_equal EXPECTED_LINE_COVERAGES, table.sort_by(&:to_f)
@@ -54,11 +54,11 @@ def test_output_branch_coverages
5454

5555
html_doc = format_results(CoverageFixtures::ALL_FIXTURES)
5656

57-
branch_cell = html_doc.at_css("div#AllFiles td.t-totals__branch-coverage")
57+
branch_pct = html_doc.at_css("div#AllFiles td.t-totals__branch-pct")
5858

59-
assert branch_cell, "Expected branch coverage totals row"
59+
assert branch_pct, "Expected branch coverage totals row"
6060

61-
pcts = html_doc.css("div#AllFiles table.file_list tr.t-file td.t-file__branch-coverage .coverage-cell__pct")
61+
pcts = html_doc.css("div#AllFiles table.file_list tr.t-file td.cell--branch-pct")
6262
table = pcts.map { |m| m.content.strip }
6363

6464
assert_equal EXPECTED_BRANCH_COVERAGES, table.sort_by(&:to_f)
@@ -70,7 +70,7 @@ def test_output_with_method_coverage
7070
SimpleCov.enable_coverage(:method)
7171
html_doc = format_results("sample.rb" => CoverageFixtures::SAMPLE_RB)
7272

73-
assert html_doc.at_css("div#AllFiles td.t-totals__method-coverage"),
73+
assert html_doc.at_css("div#AllFiles td.t-totals__method-pct"),
7474
"Expected method coverage totals row"
7575
end
7676

0 commit comments

Comments
 (0)