Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

### Unreleased

- **HTML report: status and tag filter persistence** — the active status filter (All, Passed, Failed, Skipped, Flaky) and tag filter in the Test Cases tab are now persisted across page reloads and reopens using `localStorage` (`sb-status-filter` and `sb-tag-filter`). Storage reads and writes are safely guarded against restricted environments (private browsing, disabled storage, quota limits); if a previously stored tag is not present in the current report, it is gracefully ignored without restricting rows, and restoring filters preserves the default Dashboard tab view. (#215)

- **HTML report: "Copy failure details" button** — failed tests with an error now have a button in the expanded detail row (and on the Failures tab) that copies the test id, error message and stack trace to the clipboard, with brief "Copied" feedback. Falls back to `execCommand('copy')` when `navigator.clipboard` is unavailable (e.g. some `file://` setups). (#216)

- **Fix: `@PreCondition` session restore on a fresh browser** — the cached cookies and localStorage were silently dropped when the next test started on `about:blank`, so only the first test per thread was logged in. Restore now navigates to the origin the session was captured on first.
Expand Down
75 changes: 75 additions & 0 deletions src/main/resources/report/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -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() {
Expand Down Expand Up @@ -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) {}
})();

110 changes: 110 additions & 0 deletions src/test/java/com/seleniumboot/reporting/HtmlReportGeneratorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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("<script>");
int scriptEnd = reportHtml.lastIndexOf("</script>");
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("<script>");
String js = reportHtml.substring(scriptStart);
int restoreBlock = js.indexOf("sb-status-filter");
assertTrue(restoreBlock > 0, "Filter restoration script must exist");
String restorationScript = js.substring(restoreBlock);
assertFalse(restorationScript.contains("showTab('tests'"),
"Filter restoration must not call showTab('tests') on load");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,12 @@ public void reportAlwaysOpensOnDashboard() throws Exception {
Assert.assertFalse(t.contains("sb-tab"), "report must not restore the last-open tab");
}
}

@Test
public void reportJsDoesNotContainSbTabKey() throws Exception {
try (var in = getClass().getResourceAsStream("/report/report.js")) {
String js = new String(in.readAllBytes(), StandardCharsets.UTF_8);
Assert.assertFalse(js.contains("sb-tab"), "report.js must not restore the last-open tab");
}
}
}
Loading