Skip to content

Commit c4bb143

Browse files
fix: dashboard exclude patterns, tile scroll targets, sync embedded templates
- DataCollector now respects source.exclude_patterns from config (README.md, _index.md no longer appear in coverage) - KPI tile click scrolls to correct section and auto-expands details - Synced dashboard-site/ files to embedded templates in CLI package - Added 'Show uncovered only' filter button to all coverage sections
1 parent 27d3e0e commit c4bb143

File tree

4 files changed

+149
-10
lines changed

4 files changed

+149
-10
lines changed

dashboard-site/scripts/app.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -963,7 +963,7 @@ function renderKPICards(summary) {
963963
const colorClass = pct >= 80 ? 'green' : pct >= 50 ? 'amber' : 'red';
964964

965965
html += `
966-
<div class="cov-kpi-card ${colorClass}" onclick="document.getElementById('details-${s.key}-coverage')?.scrollIntoView({behavior:'smooth'})">
966+
<div class="cov-kpi-card ${colorClass}" onclick="scrollToCoverageSection('${s.target}')">
967967
<div class="cov-kpi-label">${s.label}</div>
968968
<div class="cov-kpi-value ${colorClass}">${pct.toFixed(1)}%</div>
969969
<div class="cov-kpi-bar"><div class="cov-kpi-bar-fill ${colorClass}" style="width:${Math.min(pct,100)}%"></div></div>
@@ -1462,6 +1462,23 @@ function filterCoverageDetails(sectionId, query) {
14621462
}
14631463
}
14641464

1465+
/**
1466+
* Scroll to a coverage section and auto-expand its details.
1467+
*/
1468+
function scrollToCoverageSection(sectionId) {
1469+
// The detail list has id="details-{sectionId}"
1470+
const detailList = document.getElementById('details-' + sectionId);
1471+
// The section wrapper is the parent .coverage-section
1472+
const section = detailList?.closest('.coverage-section');
1473+
if (section) {
1474+
// Auto-expand if collapsed
1475+
if (detailList.classList.contains('collapsed')) {
1476+
toggleCoverageDetails(sectionId);
1477+
}
1478+
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
1479+
}
1480+
}
1481+
14651482
/**
14661483
* Toggle showing only uncovered items in a coverage detail list.
14671484
*/

src/Spectra.CLI/Dashboard/DataCollector.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,11 +683,44 @@ private static CoverageData BuildCoverageData(IReadOnlyList<TestEntry> testEntri
683683
var docsDir = Path.Combine(_basePath, "docs");
684684
if (Directory.Exists(docsDir))
685685
{
686+
// Load exclude patterns from config
687+
var excludePatterns = new List<string> { "**/CHANGELOG.md" };
688+
var configPath2 = Path.Combine(_basePath, "spectra.config.json");
689+
if (File.Exists(configPath2))
690+
{
691+
try
692+
{
693+
var configJson = await File.ReadAllTextAsync(configPath2);
694+
var configDoc = JsonSerializer.Deserialize<JsonElement>(configJson, s_jsonOptions);
695+
if (configDoc.TryGetProperty("source", out var sourceEl) &&
696+
sourceEl.TryGetProperty("exclude_patterns", out var patternsEl) &&
697+
patternsEl.ValueKind == JsonValueKind.Array)
698+
{
699+
excludePatterns = patternsEl.EnumerateArray()
700+
.Select(e => e.GetString() ?? "")
701+
.Where(s => !string.IsNullOrEmpty(s))
702+
.ToList();
703+
}
704+
}
705+
catch { /* use defaults */ }
706+
}
707+
686708
var docFiles = Directory.GetFiles(docsDir, "*.md", SearchOption.AllDirectories);
687709
foreach (var docFile in docFiles)
688710
{
689711
var relativePath = SourceRefNormalizer.NormalizePath(
690712
Path.GetRelativePath(_basePath, docFile));
713+
var fileName = Path.GetFileName(docFile);
714+
715+
// Check exclude patterns
716+
var excluded = excludePatterns.Any(pattern =>
717+
{
718+
var p = pattern.Replace("**/", "");
719+
return fileName.Equals(p, StringComparison.OrdinalIgnoreCase) ||
720+
relativePath.EndsWith(p, StringComparison.OrdinalIgnoreCase);
721+
});
722+
if (excluded) continue;
723+
691724
var testIds = docToTests.TryGetValue(relativePath, out var ids)
692725
? ids.OrderBy(id => id, StringComparer.OrdinalIgnoreCase).ToList()
693726
: [];

src/Spectra.CLI/Dashboard/Templates/scripts/app.js

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -963,7 +963,7 @@ function renderKPICards(summary) {
963963
const colorClass = pct >= 80 ? 'green' : pct >= 50 ? 'amber' : 'red';
964964

965965
html += `
966-
<div class="cov-kpi-card ${colorClass}" onclick="document.getElementById('details-${s.key}-coverage')?.scrollIntoView({behavior:'smooth'})">
966+
<div class="cov-kpi-card ${colorClass}" onclick="scrollToCoverageSection('${s.target}')">
967967
<div class="cov-kpi-label">${s.label}</div>
968968
<div class="cov-kpi-value ${colorClass}">${pct.toFixed(1)}%</div>
969969
<div class="cov-kpi-bar"><div class="cov-kpi-bar-fill ${colorClass}" style="width:${Math.min(pct,100)}%"></div></div>
@@ -1356,13 +1356,17 @@ function renderCoverageSection(label, sectionData, unit, renderDetailsFn) {
13561356
// Expandable detail list
13571357
const detailCount = sectionData.details.length;
13581358
html += `<button class="coverage-toggle-btn" onclick="toggleCoverageDetails('${sectionId}')">Show details (${detailCount})</button>`;
1359-
// Add search filter for large lists
1359+
// Add search filter and uncovered toggle
1360+
const uncoveredCount = sectionData.details.filter(d => d.covered === false || (d.percentage !== undefined && d.percentage < 100)).length;
1361+
html += `<div class="coverage-detail-filter collapsed" id="filter-${sectionId}">`;
13601362
if (detailCount > 20) {
1361-
html += `<div class="coverage-detail-filter collapsed" id="filter-${sectionId}">
1362-
<input type="text" placeholder="Filter by ID or text..." oninput="filterCoverageDetails('${sectionId}', this.value)" />
1363-
<span class="filter-count" id="filter-count-${sectionId}">${detailCount} items</span>
1364-
</div>`;
1363+
html += `<input type="text" placeholder="Filter by ID or text..." oninput="filterCoverageDetails('${sectionId}', this.value)" />`;
1364+
}
1365+
if (uncoveredCount > 0 && uncoveredCount < detailCount) {
1366+
html += `<button class="uncovered-filter-btn" id="uncovered-btn-${sectionId}" onclick="toggleUncoveredFilter('${sectionId}')" title="Show only uncovered items">Show uncovered only (${uncoveredCount})</button>`;
13651367
}
1368+
html += `<span class="filter-count" id="filter-count-${sectionId}">${detailCount} items</span>
1369+
</div>`;
13661370
html += `<ul class="coverage-detail-list collapsed" id="details-${sectionId}">`;
13671371
html += renderDetailsFn(sectionData);
13681372
html += '</ul>';
@@ -1458,6 +1462,61 @@ function filterCoverageDetails(sectionId, query) {
14581462
}
14591463
}
14601464

1465+
/**
1466+
* Scroll to a coverage section and auto-expand its details.
1467+
*/
1468+
function scrollToCoverageSection(sectionId) {
1469+
// The detail list has id="details-{sectionId}"
1470+
const detailList = document.getElementById('details-' + sectionId);
1471+
// The section wrapper is the parent .coverage-section
1472+
const section = detailList?.closest('.coverage-section');
1473+
if (section) {
1474+
// Auto-expand if collapsed
1475+
if (detailList.classList.contains('collapsed')) {
1476+
toggleCoverageDetails(sectionId);
1477+
}
1478+
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
1479+
}
1480+
}
1481+
1482+
/**
1483+
* Toggle showing only uncovered items in a coverage detail list.
1484+
*/
1485+
function toggleUncoveredFilter(sectionId) {
1486+
const list = document.getElementById('details-' + sectionId);
1487+
const btn = document.getElementById('uncovered-btn-' + sectionId);
1488+
const countEl = document.getElementById('filter-count-' + sectionId);
1489+
if (!list || !btn) return;
1490+
1491+
const isActive = btn.classList.toggle('active');
1492+
1493+
let visible = 0;
1494+
for (const li of list.children) {
1495+
const isCovered = li.getAttribute('data-covered') === 'true';
1496+
if (isActive && isCovered) {
1497+
li.style.display = 'none';
1498+
} else if (li.style.display === 'none' && !isCovered) {
1499+
// Already hidden by text filter — leave it
1500+
} else if (!isActive) {
1501+
li.style.display = '';
1502+
visible++;
1503+
} else {
1504+
visible++;
1505+
}
1506+
}
1507+
1508+
// Recount visible items
1509+
visible = 0;
1510+
for (const li of list.children) {
1511+
if (li.style.display !== 'none') visible++;
1512+
}
1513+
1514+
if (countEl) {
1515+
countEl.textContent = isActive ? `${visible} uncovered` : `${list.children.length} items`;
1516+
}
1517+
btn.textContent = isActive ? 'Show all' : `Show uncovered only (${list.querySelectorAll('[data-covered="false"]').length})`;
1518+
}
1519+
14611520
/**
14621521
* Render documentation detail list items.
14631522
*/
@@ -1467,7 +1526,7 @@ function renderDocDetails(section) {
14671526
const icon = d.covered
14681527
? '<span class="detail-icon coverage-green">&#10003;</span>'
14691528
: '<span class="detail-icon coverage-red">&#10007;</span>';
1470-
html += `<li>
1529+
html += `<li data-covered="${d.covered ? 'true' : 'false'}">
14711530
<span class="detail-name" title="${escapeHtml(d.doc)}">${escapeHtml(d.doc)}</span>
14721531
<span class="detail-meta">${d.test_count} test${d.test_count !== 1 ? 's' : ''}</span>
14731532
${icon}
@@ -1490,7 +1549,7 @@ function renderCriteriaDetails(section) {
14901549
const displayName = titleText
14911550
? `<strong>${escapeHtml(d.id)}</strong> ${escapeHtml(titleText)}`
14921551
: `<strong>${escapeHtml(d.id)}</strong>`;
1493-
html += `<li title="${escapeHtml(titleText)}">
1552+
html += `<li data-covered="${d.covered ? 'true' : 'false'}" title="${escapeHtml(titleText)}">
14941553
<span class="detail-name">${displayName}</span>
14951554
<span class="detail-meta">${testList}</span>
14961555
${icon}
@@ -1506,7 +1565,8 @@ function renderAutoDetails(section) {
15061565
let html = '';
15071566
for (const d of section.details) {
15081567
const suiteColor = d.percentage >= 80 ? 'coverage-green' : d.percentage >= 50 ? 'coverage-yellow' : 'coverage-red';
1509-
html += `<li>
1568+
const fullyCovered = d.percentage >= 100;
1569+
html += `<li data-covered="${fullyCovered ? 'true' : 'false'}">
15101570
<span class="detail-name">${escapeHtml(d.suite)}</span>
15111571
<span class="detail-meta ${suiteColor}">${d.automated}/${d.total} (${d.percentage.toFixed(1)}%)</span>
15121572
</li>`;

src/Spectra.CLI/Dashboard/Templates/styles/main.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,35 @@ details.result-row[open] summary::before {
14081408
border-color: var(--text-muted);
14091409
}
14101410

1411+
.uncovered-filter-btn {
1412+
background: none;
1413+
border: 1px solid var(--border-color);
1414+
color: var(--text-muted);
1415+
padding: 0.3rem 0.75rem;
1416+
border-radius: var(--radius-sm);
1417+
cursor: pointer;
1418+
font-size: 0.8rem;
1419+
transition: all 0.2s ease;
1420+
white-space: nowrap;
1421+
}
1422+
1423+
.uncovered-filter-btn:hover {
1424+
background: var(--bg-color);
1425+
color: var(--text-color);
1426+
border-color: var(--text-muted);
1427+
}
1428+
1429+
.uncovered-filter-btn.active {
1430+
background: #dc3545;
1431+
color: #fff;
1432+
border-color: #dc3545;
1433+
}
1434+
1435+
.uncovered-filter-btn.active:hover {
1436+
background: #c82333;
1437+
border-color: #c82333;
1438+
}
1439+
14111440
/* Coverage Empty State */
14121441
.coverage-empty-state {
14131442
display: flex;

0 commit comments

Comments
 (0)