Skip to content

Commit d0a8693

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 d0a8693

File tree

8 files changed

+222
-121
lines changed

8 files changed

+222
-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: 130 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

@@ -266,6 +290,64 @@ function materializeSourceFile(sourceFileId: string): HTMLElement | null {
266290
return el;
267291
}
268292

293+
// --- Bar width equalization ------------------------------------
294+
295+
function setBarWidth(bars: Element[], headers: Element[], px: number): void {
296+
const w = px + 'px';
297+
headers.forEach(h => h.setAttribute('colspan', '4'));
298+
bars.forEach(b => {
299+
const s = (b as HTMLElement).style;
300+
s.display = '';
301+
s.width = w; s.minWidth = w; s.maxWidth = w;
302+
});
303+
}
304+
305+
function hideBars(bars: Element[], headers: Element[]): void {
306+
headers.forEach(h => h.setAttribute('colspan', '3'));
307+
bars.forEach(b => {
308+
const s = (b as HTMLElement).style;
309+
s.display = 'none'; s.width = ''; s.minWidth = ''; s.maxWidth = '';
310+
});
311+
}
312+
313+
function equalizeBarWidths(): void {
314+
$$('.file_list_container').forEach(container => {
315+
if ((container as HTMLElement).style.display === 'none') return;
316+
if ((container as HTMLElement).offsetWidth === 0) return;
317+
318+
const table = $('table.file_list', container) as HTMLTableElement | null;
319+
if (!table) return;
320+
const bars = $$('td.cell--bar', table);
321+
const headers = $$('th[colspan]', table);
322+
if (bars.length === 0) return;
323+
324+
const wrapper = table.closest('.file_list--responsive') as HTMLElement | null;
325+
if (!wrapper) return;
326+
327+
const firstDataRow = $('tbody tr', table) || $('thead tr.totals-row', table);
328+
const barsPerRow = firstDataRow ? $$('td.cell--bar', firstDataRow).length : 1;
329+
330+
// Step 1: Hide bars, measure content width with table at auto
331+
hideBars(bars, headers);
332+
table.style.width = 'auto';
333+
void table.offsetWidth;
334+
const contentWidth = table.scrollWidth;
335+
336+
// Step 2: Restore table width, measure available space
337+
table.style.width = '';
338+
void table.offsetWidth;
339+
const availableWidth = wrapper.clientWidth;
340+
341+
// Step 3: Calculate bar width
342+
const totalBarSpace = availableWidth - contentWidth;
343+
const perBar = Math.floor(totalBarSpace / barsPerRow);
344+
345+
if (perBar < 100) return; // keep bars hidden
346+
347+
setBarWidth(bars, headers, Math.min(perBar, 200));
348+
});
349+
}
350+
269351
// --- Source file dialog ----------------------------------------
270352

271353
let dialog: HTMLDialogElement;
@@ -314,6 +396,11 @@ function showFileList(tabId: string): void {
314396
if (target) target.style.display = '';
315397
}
316398
}
399+
// Only equalize bars if the wrapper is actually visible
400+
const wrapper = document.getElementById('wrapper');
401+
if (wrapper && !wrapper.classList.contains('hide')) {
402+
equalizeBarWidths();
403+
}
317404
}
318405

319406
function navigateToHash(): void {
@@ -385,12 +472,15 @@ document.addEventListener('DOMContentLoaded', function () {
385472
});
386473
})();
387474

388-
// Table sorting
475+
// Table sorting — compute td index from th colspan
389476
$$('table.file_list').forEach(table => {
390-
$$('thead tr:first-child th', table).forEach((th, colIndex) => {
477+
let tdIndex = 0;
478+
$$('thead tr:first-child th', table).forEach((th) => {
479+
const myTdIndex = tdIndex;
391480
th.classList.add('sorting');
392481
(th as HTMLElement).style.cursor = 'pointer';
393-
th.addEventListener('click', () => sortTable(table, colIndex));
482+
th.addEventListener('click', () => sortTable(table, myTdIndex));
483+
tdIndex += parseInt(th.getAttribute('colspan') || '1', 10);
394484
});
395485
});
396486

@@ -483,19 +573,27 @@ document.addEventListener('DOMContentLoaded', function () {
483573
window.location.hash = this.getAttribute('href')!.replace('#', '#_');
484574
});
485575

576+
// Equalize bar column widths within each table
577+
// Defer until after wrapper is visible
578+
window.addEventListener('resize', equalizeBarWidths);
579+
486580
// Initial state
487581
navigateToHash();
488582

489583
// Finalize loading
490584
clearInterval((window as any)._simplecovLoadingTimer);
585+
clearTimeout((window as any)._simplecovShowTimeout);
491586

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);
587+
const loadingEl = document.getElementById('loading');
588+
if (loadingEl) {
589+
loadingEl.style.transition = 'opacity 0.3s';
590+
loadingEl.style.opacity = '0';
591+
setTimeout(() => { loadingEl.style.display = 'none'; }, 300);
497592
}
498593

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

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)