diff --git a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java index 6326172..a8ad7b1 100644 --- a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java +++ b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java @@ -285,6 +285,7 @@ static String buildHtml(JsonNode root) { return template .replace("{{REPORT_CSS}}", loadResource("report/report.css")) + .replace("{{REPORT_JS}}", loadResource("report/report.js")) .replace("{{RUN_SUMMARY}}", metadata[0]) .replace("{{METADATA}}", metadata[1]) .replace("{{PASSED}}", String.valueOf(passedTests)) diff --git a/src/main/resources/report-template.html b/src/main/resources/report-template.html index 81b4ebe..913fa61 100644 --- a/src/main/resources/report-template.html +++ b/src/main/resources/report-template.html @@ -214,167 +214,7 @@

Selenium Boot Report

diff --git a/src/main/resources/report/report.js b/src/main/resources/report/report.js new file mode 100644 index 0000000..ff89c88 --- /dev/null +++ b/src/main/resources/report/report.js @@ -0,0 +1,161 @@ +// ── Tab navigation ── +function showTab(tabId, el) { + document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); + document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); + document.getElementById('tab-' + tabId).classList.add('active'); + if (el) el.classList.add('active'); +} +// ── Lightbox ── +function openLightbox(src) { + document.getElementById('lightbox-img').src = src; + document.getElementById('lightbox').classList.add('open'); + document.body.style.overflow = 'hidden'; +} +function closeLightbox() { + document.getElementById('lightbox').classList.remove('open'); + document.getElementById('lightbox-img').src = ''; + document.body.style.overflow = ''; +} +document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') closeLightbox(); +}); + +// ── Dark mode ── +(function() { + try { if (localStorage.getItem('sb-theme') === 'dark') document.documentElement.setAttribute('data-theme', 'dark'); } catch(e) {} +})(); +function toggleDark() { + const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; + document.documentElement.setAttribute('data-theme', isDark ? 'light' : 'dark'); + try { localStorage.setItem('sb-theme', isDark ? 'light' : 'dark'); } catch(e) {} +} + +// ── Charts ── +const executionData = JSON.parse('{{EXECUTION_PERCENTILES}}'); +const donutData = {{DONUT_DATA}}; +function safeVal(obj, key) { return obj && obj[key] ? obj[key] : 0; } +if (window.Chart) { + new Chart(document.getElementById('donutChart'), { + type: 'doughnut', + data: { + labels: ['Passed', 'Failed', 'Skipped'], + datasets: [{ data: [donutData.passed, donutData.failed, donutData.skipped], + backgroundColor: ['#43a047','#e53935','#fb8c00'], borderWidth: 2, borderColor: '#fff' }] + }, + options: { responsive: true, maintainAspectRatio: true, cutout: '65%', + plugins: { legend: { position: 'bottom', labels: { font: { family: 'Roboto', size: 12 } } } } } + }); + new Chart(document.getElementById('executionChart'), { + type: 'bar', + data: { + labels: ['P50', 'P90', 'P95', 'P99'], + datasets: [{ label: 'Execution Time (ms)', + data: [safeVal(executionData,'p50'), safeVal(executionData,'p90'), + safeVal(executionData,'p95'), safeVal(executionData,'p99')], + backgroundColor: 'rgba(48,63,159,.25)', borderColor: '#303f9f', borderWidth: 1, borderRadius: 4 }] + }, + options: { responsive: true, maintainAspectRatio: false, + plugins: { legend: { labels: { font: { family: 'Roboto', size: 13 } } } }, + scales: { x: { grid: { display: false } }, y: { beginAtZero: true } } } + }); +} + +// ── Trend over runs ── +const trendData = {{TREND_DATA}}; +if (window.Chart && trendData.length >= 2) { + document.getElementById('trendRow').style.display = ''; + const labels = trendData.map(r => r.label); + const line = (el, label, data, color, max) => new Chart(document.getElementById(el), { + type: 'line', + data: { labels, datasets: [{ label, data, borderColor: color, backgroundColor: color + '33', + fill: true, tension: .3, pointRadius: 3 }] }, + options: { responsive: true, maintainAspectRatio: false, + plugins: { legend: { display: false } }, + scales: { x: { grid: { display: false } }, y: { beginAtZero: true, max } } } + }); + line('trendRateChart', 'Pass rate (%)', trendData.map(r => r.passRate), '#43a047', 100); + line('trendTimeChart', 'Duration (s)', trendData.map(r => r.durationSec), '#303f9f'); + line('trendFlakyChart', 'Flaky tests', trendData.map(r => r.flakyTests), '#f57c00'); +} + +// ── Collapse / expand class groups ── +function toggleGroup(groupId) { + const members = document.querySelectorAll(`.group-member[data-group="${groupId}"]`); + const icon = document.getElementById('gicon-' + groupId); + const isOpen = icon && !icon.classList.contains('closed'); + members.forEach(r => { r.style.display = isOpen ? 'none' : ''; }); + if (icon) icon.classList.toggle('closed', isOpen); +} + +// ── Expand / collapse detail rows ── +function toggleDetail(idx) { + const row = document.getElementById('detail-' + idx); + const icon = document.getElementById('icon-' + idx); + if (!row) return; + const isOpen = row.style.display !== 'none'; + row.style.display = isOpen ? 'none' : ''; + if (icon) icon.classList.toggle('open', !isOpen); +} + +// ── Tag filtering ── +let activeTagFilter = ''; +function filterByTag(tagName, event) { + if (event) event.stopPropagation(); + activeTagFilter = (tagName || '').toLowerCase().trim(); + showTab('tests', document.querySelector('.nav-item[onclick*=\'tests\']')); + const pill = document.getElementById('activeTagPill'); + const nameSpan = document.getElementById('activeTagName'); + if (pill && nameSpan) { + if (activeTagFilter) { + nameSpan.textContent = 'Tag: ' + tagName; + pill.style.display = 'inline-flex'; + } else { + pill.style.display = 'none'; + } + } + applySearch(); +} +function clearTagFilter() { + activeTagFilter = ''; + const pill = document.getElementById('activeTagPill'); + if (pill) pill.style.display = 'none'; + applySearch(); +} + +// ── Filter + search (Test Cases tab) ── +let activeFilter = 'ALL'; +function setFilter(status, btn) { + activeFilter = status; + document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + applySearch(); +} +function applySearch() { + const q = document.getElementById('searchInput').value.toLowerCase(); + const members = document.querySelectorAll('#testTable tbody tr.group-member:not(.detail-row)'); + const visibleGroups = new Set(); + members.forEach(row => { + const rowStatus = row.getAttribute('data-status') || ''; + const rowTest = row.getAttribute('data-test') || ''; + const groupId = row.getAttribute('data-group') || ''; + const rowTags = (row.getAttribute('data-tags') || '').split(',').map(t => t.trim().toLowerCase()).filter(Boolean); + const hasRetry = row.querySelector('.retry-badge') !== null; + const matchFilter = activeFilter === 'ALL' + || activeFilter === rowStatus + || (activeFilter === 'FLAKY' && hasRetry); + const matchTag = !activeTagFilter || rowTags.includes(activeTagFilter); + const matchSearch = !q + || rowTest.includes(q) + || groupId.toLowerCase().includes(q) + || rowTags.some(t => t.includes(q)); + const show = matchFilter && matchTag && matchSearch; + row.style.display = show ? '' : 'none'; + if (show) visibleGroups.add(groupId); + const next = row.nextElementSibling; + if (next && next.classList.contains('detail-row') && !show) next.style.display = 'none'; + }); + document.querySelectorAll('#testTable tbody tr.group-header').forEach(hdr => { + const groupId = hdr.getAttribute('data-group') || ''; + hdr.style.display = visibleGroups.has(groupId) ? '' : 'none'; + }); +}