From 955f13659447f536258b144f96f31f0277cf2a99 Mon Sep 17 00:00:00 2001 From: Pushpak Jaiswal <149317014+PUSHPAK-JAISWAL@users.noreply.github.com> Date: Sat, 31 Oct 2026 11:43:48 +0530 Subject: [PATCH 1/3] feat(report): persist status and tag filters across reloads (#215) --- src/main/resources/report/report.js | 75 +++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/main/resources/report/report.js b/src/main/resources/report/report.js index 81947ec..f6bcd85 100644 --- a/src/main/resources/report/report.js +++ b/src/main/resources/report/report.js @@ -118,12 +118,22 @@ function filterByTag(tagName, event) { pill.style.display = 'none'; } } + try { + if (activeTagFilter) { + localStorage.setItem('sb-tag-filter', tagName); + } else { + localStorage.removeItem('sb-tag-filter'); + } + } catch (e) {} applySearch(); } function clearTagFilter() { activeTagFilter = ''; const pill = document.getElementById('activeTagPill'); if (pill) pill.style.display = 'none'; + try { + localStorage.removeItem('sb-tag-filter'); + } catch (e) {} applySearch(); } @@ -133,6 +143,13 @@ function setFilter(status, btn) { activeFilter = status; document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); + try { + if (status === 'ALL') { + localStorage.removeItem('sb-status-filter'); + } else { + localStorage.setItem('sb-status-filter', status); + } + } catch (e) {} applySearch(); } function applySearch() { @@ -200,3 +217,61 @@ function copyFailure(btn, event) { fallback(); } } + +// ── Filter restoration on load ── +(function() { + try { + // Restore status filter + const savedStatus = localStorage.getItem('sb-status-filter'); + if (savedStatus && ['PASSED', 'FAILED', 'SKIPPED', 'FLAKY'].includes(savedStatus)) { + const btn = document.querySelector(".filter-btn[onclick*=\"'" + savedStatus + "'\"]"); + if (btn) { + activeFilter = savedStatus; + document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + } + } else if (savedStatus === 'ALL') { + const btn = document.querySelector(".filter-btn[onclick*=\"'ALL'\"]"); + if (btn) { + activeFilter = 'ALL'; + document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + } + } + + // Restore tag filter + const savedTag = localStorage.getItem('sb-tag-filter'); + if (savedTag) { + const tagToMatch = savedTag.toLowerCase().trim(); + if (tagToMatch) { + const rows = document.querySelectorAll('#testTable tbody tr.group-member'); + let tagFound = false; + for (let i = 0; i < rows.length; i++) { + const rawTags = rows[i].getAttribute('data-tags'); + if (rawTags) { + const tags = rawTags.split(',').map(t => t.trim().toLowerCase()); + if (tags.includes(tagToMatch)) { + tagFound = true; + break; + } + } + } + if (tagFound) { + activeTagFilter = tagToMatch; + const pill = document.getElementById('activeTagPill'); + const nameSpan = document.getElementById('activeTagName'); + if (pill && nameSpan) { + nameSpan.textContent = 'Tag: ' + savedTag; + pill.style.display = 'inline-flex'; + } + } + } + } + + // Apply filters to table if any restored + if (activeFilter !== 'ALL' || activeTagFilter) { + applySearch(); + } + } catch (e) {} +})(); + From 8e126dbebd0318ed0eec766ddf6320ca774dabe6 Mon Sep 17 00:00:00 2001 From: Pushpak Jaiswal <149317014+PUSHPAK-JAISWAL@users.noreply.github.com> Date: Sat, 31 Oct 2026 11:44:01 +0530 Subject: [PATCH 2/3] test(report): verify status and tag filter persistence across reloads (#215) --- .../reporting/HtmlReportGeneratorTest.java | 110 ++++++++++++++++++ .../unit/ReportTabBehaviourTest.java | 8 ++ 2 files changed, 118 insertions(+) diff --git a/src/test/java/com/seleniumboot/reporting/HtmlReportGeneratorTest.java b/src/test/java/com/seleniumboot/reporting/HtmlReportGeneratorTest.java index 16a3d5d..25e6606 100644 --- a/src/test/java/com/seleniumboot/reporting/HtmlReportGeneratorTest.java +++ b/src/test/java/com/seleniumboot/reporting/HtmlReportGeneratorTest.java @@ -300,4 +300,114 @@ public void copyFailureButton_onlyOnFailedTestsWithErrorDetail() throws Exceptio assertTrue(reportHtml.contains("function copyFailure(")); assertTrue(reportHtml.contains("document.execCommand('copy')"), "file:// fallback present"); } + + @Test + public void buildHtml_statusFilterPersistence_persistsAndRestoresFilter() throws Exception { + String reportHtml = html(1, 0); + + // 1. Canonical storage key + assertTrue(reportHtml.contains("sb-status-filter"), + "Report must use the canonical 'sb-status-filter' storage key"); + + // 2. setFilter persistence and removal on 'ALL' + assertTrue(reportHtml.contains("localStorage.setItem('sb-status-filter'"), + "setFilter must persist non-ALL status filter to localStorage"); + assertTrue(reportHtml.contains("localStorage.removeItem('sb-status-filter'"), + "setFilter must remove stored status filter when 'ALL' is selected"); + + // 3. Status filter restoration on page load + assertTrue(reportHtml.contains("localStorage.getItem('sb-status-filter'"), + "Report initialization must retrieve persisted status from localStorage"); + assertTrue(reportHtml.contains("['PASSED', 'FAILED', 'SKIPPED', 'FLAKY']"), + "Status restoration must validate against allowed status values"); + assertTrue(reportHtml.contains(".filter-btn"), + "Status restoration must update the active class on filter buttons"); + assertTrue(reportHtml.contains("activeFilter"), + "Status restoration must set activeFilter variable"); + } + + @Test + public void buildHtml_tagFilterPersistence_persistsAndClearsFilter() throws Exception { + String reportHtml = html(1, 0); + + // 1. Canonical storage key + assertTrue(reportHtml.contains("sb-tag-filter"), + "Report must use the canonical 'sb-tag-filter' storage key"); + + // 2. filterByTag persistence & clearTagFilter removal + assertTrue(reportHtml.contains("localStorage.setItem('sb-tag-filter'"), + "filterByTag must persist selected tag to localStorage"); + assertTrue(reportHtml.contains("localStorage.removeItem('sb-tag-filter'"), + "clearTagFilter must remove stored tag filter from localStorage"); + + // 3. Tag filter restoration on page load + assertTrue(reportHtml.contains("localStorage.getItem('sb-tag-filter'"), + "Report initialization must retrieve persisted tag from localStorage"); + assertTrue(reportHtml.contains("activeTagPill"), + "Tag restoration must manage activeTagPill visibility"); + assertTrue(reportHtml.contains("activeTagName"), + "Tag restoration must update activeTagName display"); + } + + @Test + public void buildHtml_tagFilterRestoration_guardsAgainstMissingTags() throws Exception { + String reportHtml = html(1, 0); + + // Verify missing tag validation inspects table rows before applying filter + assertTrue(reportHtml.contains("#testTable tbody tr.group-member"), + "Tag restoration must query test table member rows to locate matching tags"); + assertTrue(reportHtml.contains("data-tags"), + "Tag restoration must inspect data-tags on table rows before activating filter"); + assertTrue(reportHtml.contains("tagFound"), + "Tag restoration must track whether the stored tag actually exists in the report"); + } + + @Test + public void buildHtml_filterStorageAccess_isGuardedByTryCatch() throws Exception { + String reportHtml = html(1, 0); + + int scriptStart = reportHtml.lastIndexOf(""); + assertTrue(scriptStart > 0 && scriptEnd > scriptStart, "Generated HTML must contain embedded script block"); + String js = reportHtml.substring(scriptStart, scriptEnd); + + // General error guarding + assertTrue(js.contains("try {") || js.contains("try{"), "Storage access must use try blocks"); + assertTrue(js.contains("catch (e)") || js.contains("catch(e)") || js.contains("catch (err)"), + "Storage access must catch storage exceptions"); + + // Specific function error handling checks + assertTrue(js.matches("(?s).*function setFilter[^{]*\\{.*?try\\s*\\{.*?localStorage\\.(setItem|removeItem)\\('sb-status-filter'.*?\\}\\s*catch.*"), + "setFilter localStorage writes must be enclosed in try/catch"); + assertTrue(js.matches("(?s).*function filterByTag[^{]*\\{.*?try\\s*\\{.*?localStorage\\.(setItem|removeItem)\\('sb-tag-filter'.*?\\}\\s*catch.*"), + "filterByTag localStorage writes must be enclosed in try/catch"); + assertTrue(js.matches("(?s).*function clearTagFilter[^{]*\\{.*?try\\s*\\{.*?localStorage\\.removeItem\\('sb-tag-filter'.*?\\}\\s*catch.*"), + "clearTagFilter localStorage removal must be enclosed in try/catch"); + assertTrue(js.matches("(?s).*\\(function\\(\\)\\s*\\{.*?try\\s*\\{.*?sb-status-filter.*?sb-tag-filter.*?\\}\\s*catch.*"), + "Filter restoration routine on load must be enclosed in try/catch"); + } + + @Test + public void buildHtml_filterRestoration_preservesDashboardTabWithoutTabPersistence() throws Exception { + String reportHtml = html(1, 0); + + // Default markup states + assertTrue(reportHtml.contains("id=\"tab-dashboard\" class=\"tab-pane active\""), + "Dashboard tab pane must be active by default"); + assertTrue(reportHtml.contains("nav-item active\" onclick=\"showTab('dashboard', this)\""), + "Dashboard navigation item must be active by default"); + + // Must not introduce any tab persistence key + assertFalse(reportHtml.contains("sb-tab"), + "Generated report must not introduce sb-tab persistence keys"); + + // Filter restoration block must not switch tabs + int scriptStart = reportHtml.lastIndexOf("