From 613ed82eccfe420179bec2e5a41784f43556078b Mon Sep 17 00:00:00 2001
From: Pushpak Jaiswal <149317014+PUSHPAK-JAISWAL@users.noreply.github.com>
Date: Fri, 6 Nov 2026 15:19:17 +0530
Subject: [PATCH 1/5] feat(report): add sortable column headers and row
duration data attributes (#217)
---
.../com/seleniumboot/reporting/HtmlReportGenerator.java | 8 +++++---
src/main/resources/report/partials/tests.html | 8 ++++----
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java
index 7376cf6..888075f 100644
--- a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java
+++ b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java
@@ -365,8 +365,8 @@ private static String buildRow(JsonNode test, int rowIndex, String groupId, bool
int lastDot = rawTestId.lastIndexOf('.');
String methodName = escapeHtml(lastDot >= 0 ? rawTestId.substring(lastDot + 1) : rawTestId);
String description = test.has("description") ? escapeHtml(test.get("description").asText()) : "";
- long logicMs = test.has("testLogicMs") ? test.get("testLogicMs").asLong() : 0L;
- long totalMs = test.has("totalMs") ? test.get("totalMs").asLong() : 0L;
+ long logicMs = test.hasNonNull("testLogicMs") ? test.get("testLogicMs").asLong(0L) : 0L;
+ long totalMs = test.hasNonNull("totalMs") ? test.get("totalMs").asLong(0L) : 0L;
int retryCount = test.has("retryCount") ? test.get("retryCount").asInt() : 0;
String errorMsg = test.has("errorMessage") ? test.get("errorMessage").asText() : null;
String stackTrace = test.has("stackTrace") ? test.get("stackTrace").asText() : null;
@@ -465,7 +465,9 @@ private static String buildRow(JsonNode test, int rowIndex, String groupId, bool
+ " data-group=\"" + groupKey + "\""
+ " data-status=\"" + status + "\""
+ " data-tags=\"" + tagDataAttr + "\""
- + " data-test=\"" + methodName.toLowerCase() + "\">"
+ + " data-test=\"" + methodName.toLowerCase() + "\""
+ + " data-logic-ms=\"" + logicMs + "\""
+ + " data-total-ms=\"" + totalMs + "\">"
+ "
" + testCellContent + " | "
+ "" + description + " | "
+ browserCell
diff --git a/src/main/resources/report/partials/tests.html b/src/main/resources/report/partials/tests.html
index 6377659..c8880bd 100644
--- a/src/main/resources/report/partials/tests.html
+++ b/src/main/resources/report/partials/tests.html
@@ -24,11 +24,11 @@
- | Test |
+ Test |
Description |
- Status |
- Logic (ms) |
- Total (ms) |
+ Status |
+ Logic (ms) |
+ Total (ms) |
Screenshot |
Detail |
From 20d949a99952f37aa1895cb4b1dc683c1b9014d3 Mon Sep 17 00:00:00 2001
From: Pushpak Jaiswal <149317014+PUSHPAK-JAISWAL@users.noreply.github.com>
Date: Fri, 6 Nov 2026 15:21:58 +0530
Subject: [PATCH 2/5] feat(report): implement client-side table sorting with
filter cooperation (#217)
---
src/main/resources/report/report.css | 43 ++++++++
src/main/resources/report/report.js | 153 +++++++++++++++++++++++++++
2 files changed, 196 insertions(+)
diff --git a/src/main/resources/report/report.css b/src/main/resources/report/report.css
index 0c4122c..751a40f 100644
--- a/src/main/resources/report/report.css
+++ b/src/main/resources/report/report.css
@@ -253,6 +253,44 @@
padding: 11px 14px; font-size: 12px; font-weight: 500;
text-transform: uppercase; letter-spacing: .5px; text-align: left; white-space: nowrap;
}
+
+ /* ── Sortable table headers ── */
+ th.sortable {
+ cursor: pointer;
+ user-select: none;
+ -webkit-user-select: none;
+ transition: background .15s ease, color .15s ease;
+ }
+ th.sortable:hover {
+ background: var(--primary-dark);
+ }
+ th.sortable.active {
+ background: var(--primary-dark);
+ }
+ .sort-icon {
+ display: inline-block;
+ margin-left: 6px;
+ font-size: 11px;
+ line-height: 1;
+ vertical-align: middle;
+ opacity: 0.4;
+ transition: opacity .15s ease;
+ }
+ .sort-icon::after {
+ content: '\21C5';
+ }
+ th.sortable:hover .sort-icon {
+ opacity: 0.8;
+ }
+ th.sortable.active .sort-icon {
+ opacity: 1;
+ }
+ .sort-icon.asc::after {
+ content: '\25B2';
+ }
+ .sort-icon.desc::after {
+ content: '\25BC';
+ }
tbody td { padding: 10px 14px; font-size: 13px; border-bottom: 1px solid var(--border); color: var(--text); vertical-align: middle; }
tbody tr:nth-child(even):not(.detail-row) { background: var(--surface2); }
tbody tr.expandable:hover { background: var(--hover); cursor: pointer; }
@@ -489,6 +527,7 @@
.active-tag-pill,
.failures-shortcut,
.lightbox,
+ .sort-icon,
.footer {
display: none !important;
}
@@ -577,6 +616,10 @@
display: table-header-group !important;
}
+ th.sortable {
+ cursor: default !important;
+ }
+
tr {
break-inside: avoid;
page-break-inside: avoid;
diff --git a/src/main/resources/report/report.js b/src/main/resources/report/report.js
index d9334e1..23e8b64 100644
--- a/src/main/resources/report/report.js
+++ b/src/main/resources/report/report.js
@@ -250,6 +250,159 @@ function applySearch() {
});
}
+// ── Column sorting (Test Cases tab) ──
+let currentSortCol = null;
+let currentSortDir = 'asc';
+
+function getNumericVal(row, attr, fallbackIndex) {
+ const val = row.getAttribute(attr);
+ if (val !== null && val !== '') {
+ const n = parseFloat(val);
+ if (!isNaN(n)) return n;
+ }
+ const numericCells = row.querySelectorAll('td.numeric');
+ if (numericCells.length > fallbackIndex) {
+ const n = parseFloat(numericCells[fallbackIndex].textContent.trim());
+ if (!isNaN(n)) return n;
+ }
+ return 0;
+}
+
+function compareTestRows(pairA, pairB, colKey, dir) {
+ const rowA = pairA.testRow;
+ const rowB = pairB.testRow;
+ let cmp = 0;
+
+ if (colKey === 'test') {
+ const a = (rowA.getAttribute('data-test') || '').toLowerCase();
+ const b = (rowB.getAttribute('data-test') || '').toLowerCase();
+ cmp = a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
+ } else if (colKey === 'status') {
+ const a = (rowA.getAttribute('data-status') || '').toUpperCase();
+ const b = (rowB.getAttribute('data-status') || '').toUpperCase();
+ cmp = a.localeCompare(b);
+ if (cmp === 0) {
+ const testA = (rowA.getAttribute('data-test') || '').toLowerCase();
+ const testB = (rowB.getAttribute('data-test') || '').toLowerCase();
+ cmp = testA.localeCompare(testB, undefined, { numeric: true, sensitivity: 'base' });
+ }
+ } else if (colKey === 'logic') {
+ const a = getNumericVal(rowA, 'data-logic-ms', 0);
+ const b = getNumericVal(rowB, 'data-logic-ms', 0);
+ cmp = a - b;
+ if (cmp === 0) {
+ const testA = (rowA.getAttribute('data-test') || '').toLowerCase();
+ const testB = (rowB.getAttribute('data-test') || '').toLowerCase();
+ cmp = testA.localeCompare(testB, undefined, { numeric: true, sensitivity: 'base' });
+ }
+ } else if (colKey === 'total') {
+ const a = getNumericVal(rowA, 'data-total-ms', 1);
+ const b = getNumericVal(rowB, 'data-total-ms', 1);
+ cmp = a - b;
+ if (cmp === 0) {
+ const testA = (rowA.getAttribute('data-test') || '').toLowerCase();
+ const testB = (rowB.getAttribute('data-test') || '').toLowerCase();
+ cmp = testA.localeCompare(testB, undefined, { numeric: true, sensitivity: 'base' });
+ }
+ }
+
+ return dir === 'desc' ? -cmp : cmp;
+}
+
+function updateSortHeaders(activeColKey, dir) {
+ const headers = document.querySelectorAll('#testTable thead th.sortable');
+ headers.forEach(th => {
+ const key = th.getAttribute('data-sort-key');
+ const icon = th.querySelector('.sort-icon');
+ if (key === activeColKey) {
+ th.classList.add('active');
+ th.setAttribute('aria-sort', dir === 'asc' ? 'ascending' : 'descending');
+ th.title = 'Sorted ' + (dir === 'asc' ? 'ascending' : 'descending') + '. Click to reverse.';
+ if (icon) {
+ icon.classList.remove('asc', 'desc');
+ icon.classList.add(dir);
+ }
+ } else {
+ th.classList.remove('active');
+ th.setAttribute('aria-sort', 'none');
+ th.title = 'Click to sort';
+ if (icon) {
+ icon.classList.remove('asc', 'desc');
+ }
+ }
+ });
+}
+
+function sortTestTable(colKey) {
+ const table = document.getElementById('testTable');
+ if (!table) return;
+
+ // Toggle direction if already active column; otherwise default to ascending
+ if (currentSortCol === colKey) {
+ currentSortDir = currentSortDir === 'asc' ? 'desc' : 'asc';
+ } else {
+ currentSortCol = colKey;
+ currentSortDir = 'asc';
+ }
+
+ const groupHeaders = document.querySelectorAll('#testTable tbody tr.group-header');
+
+ if (groupHeaders.length > 0) {
+ // Group-Preserving sorting: sort test rows within each group
+ const allMemberRows = Array.from(document.querySelectorAll('#testTable tbody tr.group-member:not(.detail-row)'));
+
+ groupHeaders.forEach(hdr => {
+ const groupId = hdr.getAttribute('data-group');
+ const groupMembers = allMemberRows.filter(r => r.getAttribute('data-group') === groupId);
+
+ // Collect atomic test row + detail row pairs
+ const pairs = groupMembers.map(testRow => {
+ const next = testRow.nextElementSibling;
+ const detailRow = (next && next.classList.contains('detail-row')) ? next : null;
+ return { testRow, detailRow };
+ });
+
+ // Sort pairs within group
+ pairs.sort((a, b) => compareTestRows(a, b, colKey, currentSortDir));
+
+ // Re-insert pairs in sorted order immediately after group header
+ let anchor = hdr;
+ pairs.forEach(pair => {
+ anchor.insertAdjacentElement('afterend', pair.testRow);
+ anchor = pair.testRow;
+ if (pair.detailRow) {
+ anchor.insertAdjacentElement('afterend', pair.detailRow);
+ anchor = pair.detailRow;
+ }
+ });
+ });
+ } else {
+ // Fallback for ungrouped tables
+ const tbody = table.querySelector('tbody');
+ if (tbody) {
+ const testRows = Array.from(tbody.querySelectorAll('tr.group-member:not(.detail-row)'));
+ const pairs = testRows.map(testRow => {
+ const next = testRow.nextElementSibling;
+ const detailRow = (next && next.classList.contains('detail-row')) ? next : null;
+ return { testRow, detailRow };
+ });
+
+ pairs.sort((a, b) => compareTestRows(a, b, colKey, currentSortDir));
+
+ pairs.forEach(pair => {
+ tbody.appendChild(pair.testRow);
+ if (pair.detailRow) tbody.appendChild(pair.detailRow);
+ });
+ }
+ }
+
+ // Update sort indicators on column headers
+ updateSortHeaders(currentSortCol, currentSortDir);
+
+ // Refresh visibility respecting active status filters, search query, and tag filters
+ applySearch();
+}
+
// ── Copy failure details ──
function copyFailure(btn, event) {
if (event) event.stopPropagation();
From 0328f51bf677a0e0f8d43be689e43e34674cffab Mon Sep 17 00:00:00 2001
From: Pushpak Jaiswal <149317014+PUSHPAK-JAISWAL@users.noreply.github.com>
Date: Fri, 6 Nov 2026 15:24:48 +0530
Subject: [PATCH 3/5] test(report): verify sortable column headers, data
attributes, and sort script (#217)
---
.../reporting/HtmlReportGeneratorTest.java | 284 ++++++++++++++++++
1 file changed, 284 insertions(+)
diff --git a/src/test/java/com/seleniumboot/reporting/HtmlReportGeneratorTest.java b/src/test/java/com/seleniumboot/reporting/HtmlReportGeneratorTest.java
index 25e6606..cadda16 100644
--- a/src/test/java/com/seleniumboot/reporting/HtmlReportGeneratorTest.java
+++ b/src/test/java/com/seleniumboot/reporting/HtmlReportGeneratorTest.java
@@ -410,4 +410,288 @@ public void buildHtml_filterRestoration_preservesDashboardTabWithoutTabPersisten
assertFalse(restorationScript.contains("showTab('tests'"),
"Filter restoration must not call showTab('tests') on load");
}
+
+ // ── Sortable Columns in Test Cases Table (Issue #217) ──
+
+ @Test
+ public void buildHtml_sortableHeaders_haveSortableClassClickHandlersAndSortIcons() throws Exception {
+ String reportHtml = html(1, 0);
+
+ int tableStart = reportHtml.indexOf("");
+ int theadEnd = reportHtml.indexOf("", tableStart);
+ assertTrue(tableStart > 0 && theadEnd > tableStart, "testTable thead must be present in HTML");
+ String theadHtml = reportHtml.substring(tableStart, theadEnd);
+
+ // 1. Test column header
+ assertTrue(theadHtml.contains("sortTestTable('test')"),
+ "Test column header must contain sortTestTable('test') onclick handler");
+ assertTrue(theadHtml.contains("data-sort-key=\"test\""),
+ "Test column header must carry data-sort-key=\"test\"");
+
+ // 2. Status column header
+ assertTrue(theadHtml.contains("sortTestTable('status')"),
+ "Status column header must contain sortTestTable('status') onclick handler");
+ assertTrue(theadHtml.contains("data-sort-key=\"status\""),
+ "Status column header must carry data-sort-key=\"status\"");
+
+ // 3. Logic (ms) column header
+ assertTrue(theadHtml.contains("sortTestTable('logic')"),
+ "Logic ms column header must contain sortTestTable('logic') onclick handler");
+ assertTrue(theadHtml.contains("data-sort-key=\"logic\""),
+ "Logic ms column header must carry data-sort-key=\"logic\"");
+
+ // 4. Total (ms) column header
+ assertTrue(theadHtml.contains("sortTestTable('total')"),
+ "Total ms column header must contain sortTestTable('total') onclick handler");
+ assertTrue(theadHtml.contains("data-sort-key=\"total\""),
+ "Total ms column header must carry data-sort-key=\"total\"");
+
+ // 5. Check sortable class and sort-icon presence
+ int sortableClassCount = 0;
+ int idx = 0;
+ while ((idx = theadHtml.indexOf("class=\"sortable\"", idx)) >= 0) {
+ sortableClassCount++;
+ idx += "class=\"sortable\"".length();
+ }
+ assertEquals(sortableClassCount, 4, "There should be exactly 4 sortable column headers");
+
+ int sortIconCount = 0;
+ idx = 0;
+ while ((idx = theadHtml.indexOf("class=\"sort-icon\"", idx)) >= 0) {
+ sortIconCount++;
+ idx += "class=\"sort-icon\"".length();
+ }
+ assertEquals(sortIconCount, 4, "There should be exactly 4 sort-icon elements in the thead");
+ }
+
+ @Test
+ public void buildHtml_nonSortableHeaders_doNotCarrySortControls() throws Exception {
+ String reportHtml = html(1, 0);
+
+ int tableStart = reportHtml.indexOf("");
+ int theadEnd = reportHtml.indexOf("", tableStart);
+ assertTrue(tableStart > 0 && theadEnd > tableStart, "testTable thead must be present in HTML");
+ String theadHtml = reportHtml.substring(tableStart, theadEnd);
+
+ // Split into | chunks to inspect non-sortable columns individually
+ String[] ths = theadHtml.split(" | Description<") || th.contains(">Screenshot<") || th.contains(">Detail<")) {
+ assertFalse(th.contains("sortable"), "Non-sortable column must not have sortable class: " + th);
+ assertFalse(th.contains("sortTestTable"), "Non-sortable column must not have sort onclick: " + th);
+ assertFalse(th.contains("sort-icon"), "Non-sortable column must not have sort-icon span: " + th);
+ }
+ }
+ }
+
+ @Test
+ public void buildHtml_testRows_carryLogicAndTotalMsDataAttributes() throws Exception {
+ String json = """
+ {
+ "totalTests": 2,
+ "passedTests": 1,
+ "failedTests": 1,
+ "skippedTests": 0,
+ "passRate": 50.0,
+ "tests": [
+ {
+ "testId": "com.example.OrderTest.checkoutSuccess",
+ "testClassName": "OrderTest",
+ "status": "PASSED",
+ "testLogicMs": 1420,
+ "totalMs": 2850
+ },
+ {
+ "testId": "com.example.OrderTest.paymentFailure",
+ "testClassName": "OrderTest",
+ "status": "FAILED",
+ "testLogicMs": 0,
+ "totalMs": 500,
+ "errorMessage": "gateway timeout"
+ }
+ ]
+ }
+ """;
+ JsonNode root = new ObjectMapper().readTree(json);
+ String reportHtml = HtmlReportGenerator.buildHtml(root);
+
+ // Passed test row assertions
+ assertTrue(reportHtml.contains("data-test=\"checkoutsuccess\""), "methodName lowercased in data-test");
+ assertTrue(reportHtml.contains("data-status=\"PASSED\""), "status attribute present");
+ assertTrue(reportHtml.contains("data-logic-ms=\"1420\""), "data-logic-ms must match testLogicMs");
+ assertTrue(reportHtml.contains("data-total-ms=\"2850\""), "data-total-ms must match totalMs");
+
+ // Failed test row assertions
+ assertTrue(reportHtml.contains("data-test=\"paymentfailure\""), "methodName lowercased in data-test");
+ assertTrue(reportHtml.contains("data-status=\"FAILED\""), "status attribute present");
+ assertTrue(reportHtml.contains("data-logic-ms=\"0\""), "data-logic-ms handles 0 cleanly");
+ assertTrue(reportHtml.contains("data-total-ms=\"500\""), "data-total-ms must match totalMs");
+ }
+
+ @Test
+ public void buildHtml_sortingScript_includesSortingLogicAndFilterIntegration() 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);
+
+ // 1. Function and state tracking
+ assertTrue(js.contains("function sortTestTable(") || js.contains("sortTestTable = function("),
+ "Report script must define sortTestTable function");
+ assertTrue(js.contains("currentSortCol") && js.contains("currentSortDir"),
+ "Report script must track currentSortCol and currentSortDir");
+
+ // 2. Supported sort keys
+ assertTrue(js.contains("'test'") && js.contains("'status'") && js.contains("'logic'") && js.contains("'total'"),
+ "Sorting logic must support 'test', 'status', 'logic', and 'total' keys");
+
+ // 3. Numeric attributes and comparison
+ assertTrue(js.contains("data-logic-ms") && js.contains("data-total-ms"),
+ "Sorting logic must query data-logic-ms and data-total-ms attributes");
+
+ // 4. Sort indicator styling update
+ assertTrue(js.contains(".sort-icon") && js.contains("'asc'") && js.contains("'desc'"),
+ "Sorting logic must toggle asc and desc classes on .sort-icon elements");
+
+ // 5. Group preservation and detail row pairing
+ assertTrue(js.contains("detail-row"),
+ "Sorting logic must maintain pairing with contiguous detail-row elements");
+ assertTrue(js.contains("group-header") || js.contains("data-group"),
+ "Sorting logic must preserve class group headers");
+
+ // 6. Filter cooperation
+ assertTrue(js.contains("applySearch()"),
+ "sortTestTable must call applySearch() to refresh visibility under active filters");
+ }
+
+ @Test
+ public void buildHtml_matrixRun_preservesSortingDataAttributesAndBrowserColumn() throws Exception {
+ String json = """
+ {
+ "totalTests": 2,
+ "passedTests": 2,
+ "failedTests": 0,
+ "skippedTests": 0,
+ "passRate": 100.0,
+ "tests": [
+ {
+ "testId": "com.example.MatrixTest.testCrossBrowser",
+ "testClassName": "MatrixTest",
+ "browser": "chrome",
+ "status": "PASSED",
+ "testLogicMs": 850,
+ "totalMs": 1200
+ },
+ {
+ "testId": "com.example.MatrixTest.testCrossBrowser",
+ "testClassName": "MatrixTest",
+ "browser": "firefox",
+ "status": "PASSED",
+ "testLogicMs": 950,
+ "totalMs": 1350
+ }
+ ]
+ }
+ """;
+ JsonNode root = new ObjectMapper().readTree(json);
+ String reportHtml = HtmlReportGenerator.buildHtml(root);
+
+ // Verify matrix groups
+ assertTrue(reportHtml.contains("MatrixTest [Chrome]"), "Matrix run must include browser in group header");
+ assertTrue(reportHtml.contains("MatrixTest [Firefox]"), "Matrix run must include browser in group header");
+
+ // Verify browser cells
+ assertTrue(reportHtml.contains(" | Chrome | "));
+ assertTrue(reportHtml.contains("Firefox | "));
+
+ // Verify sorting data attributes are present on both matrix rows
+ assertTrue(reportHtml.contains("data-logic-ms=\"850\""));
+ assertTrue(reportHtml.contains("data-total-ms=\"1200\""));
+ assertTrue(reportHtml.contains("data-logic-ms=\"950\""));
+ assertTrue(reportHtml.contains("data-total-ms=\"1350\""));
+
+ // Verify table still contains sortable headers
+ assertTrue(reportHtml.contains("onclick=\"sortTestTable('test')\""));
+ assertTrue(reportHtml.contains("onclick=\"sortTestTable('logic')\""));
+ }
+
+ @Test
+ public void buildHtml_sortableHeaderCss_includesSortIconsAndActiveStates() throws Exception {
+ String reportHtml = html(1, 0);
+
+ int styleStart = reportHtml.indexOf("");
+ assertTrue(styleStart > 0 && styleEnd > styleStart, "Report must contain embedded style block");
+ String css = reportHtml.substring(styleStart, styleEnd);
+
+ assertTrue(css.contains(".sortable"), "CSS must contain .sortable selector");
+ assertTrue(css.contains(".sort-icon"), "CSS must contain .sort-icon selector");
+ assertTrue(css.contains(".sort-icon.asc") || css.contains(".asc"), "CSS must support asc sort icon state");
+ assertTrue(css.contains(".sort-icon.desc") || css.contains(".desc"), "CSS must support desc sort icon state");
+ }
+
+ @Test
+ public void generate_writesValidReportWithSortableColumnsToDisk() throws Exception {
+ java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("sb-e2e-report");
+ String originalReportsDir = System.getProperty("seleniumboot.reports.dir");
+ System.setProperty("seleniumboot.reports.dir", tempDir.toString());
+ try {
+ // Write a sample metrics JSON file
+ String sampleMetricsJson = """
+ {
+ "totalTests": 2,
+ "passedTests": 1,
+ "failedTests": 1,
+ "skippedTests": 0,
+ "passRate": 50.0,
+ "suiteDurationMs": 2500,
+ "tests": [
+ {
+ "testId": "com.example.SampleTest.testAlpha",
+ "testClassName": "SampleTest",
+ "status": "PASSED",
+ "testLogicMs": 500,
+ "totalMs": 800
+ },
+ {
+ "testId": "com.example.SampleTest.testBeta",
+ "testClassName": "SampleTest",
+ "status": "FAILED",
+ "testLogicMs": 1200,
+ "totalMs": 1700,
+ "errorMessage": "Assertion failed"
+ }
+ ]
+ }
+ """;
+ java.nio.file.Files.writeString(tempDir.resolve("selenium-boot-metrics.json"), sampleMetricsJson);
+
+ // Execute generate()
+ HtmlReportGenerator.generate();
+
+ // Verify report file was created
+ java.nio.file.Path reportPath = tempDir.resolve("selenium-boot-report.html");
+ assertTrue(java.nio.file.Files.exists(reportPath), "selenium-boot-report.html must exist on disk");
+ assertTrue(java.nio.file.Files.size(reportPath) > 0, "Report file must not be empty");
+
+ // Read generated HTML and verify sortable columns and data attributes
+ String generatedHtml = java.nio.file.Files.readString(reportPath);
+ assertTrue(generatedHtml.contains("class=\"sortable\""), "Generated report on disk must contain sortable headers");
+ assertTrue(generatedHtml.contains("sortTestTable('test')"), "Generated report on disk must contain test sort handler");
+ assertTrue(generatedHtml.contains("sortTestTable('logic')"), "Generated report on disk must contain logic sort handler");
+ assertTrue(generatedHtml.contains("data-logic-ms=\"500\""), "Generated report on disk must contain row data-logic-ms");
+ assertTrue(generatedHtml.contains("data-total-ms=\"800\""), "Generated report on disk must contain row data-total-ms");
+ assertTrue(generatedHtml.contains("data-logic-ms=\"1200\""), "Generated report on disk must contain row data-logic-ms");
+ assertTrue(generatedHtml.contains("data-total-ms=\"1700\""), "Generated report on disk must contain row data-total-ms");
+ assertTrue(generatedHtml.contains("function sortTestTable"), "Generated report on disk must contain sorting script");
+ } finally {
+ if (originalReportsDir != null) {
+ System.setProperty("seleniumboot.reports.dir", originalReportsDir);
+ } else {
+ System.clearProperty("seleniumboot.reports.dir");
+ }
+ }
+ }
}
From f4d294b2c0121b8d9bf286f5c10da36c5e4b7375 Mon Sep 17 00:00:00 2001
From: Pushpak Jaiswal <149317014+PUSHPAK-JAISWAL@users.noreply.github.com>
Date: Fri, 6 Nov 2026 15:27:56 +0530
Subject: [PATCH 4/5] docs(report): document sortable columns and
group-preserving behavior in changelog (#217)
---
CHANGELOG.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 89f22ff..c9a80aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
### v3.10.0 — 2026-11-04
+- **HTML report: sortable columns in Test Cases table** — clicking headers for Test, Status, Logic (ms), and Total (ms) sorts rows within each test class group ascending or descending with visual sort indicators, while preserving detail row attachments and cooperating seamlessly with active status, tag, and search filters. (#217)
+
+
- **HTML report: print stylesheet for "Save as PDF"** — printing the report now produces a clean document: UI chrome is hidden, all sections print instead of only the active tab, collapsed groups and failed-test details (error and full stack trace) are expanded, the light palette is used even in dark mode, and your on-screen filters are restored afterwards. (#221, #236)
- **HTML report: execution timeline** — the dashboard now shows a Gantt-style timeline of when each test started and ended, built from the new per-test `startedAtMs` export. (#201)
From f494a4df8d1139a848abcde85aaa17bdf78d7448 Mon Sep 17 00:00:00 2001
From: Mohammad Saddam Hossain
Date: Sat, 7 Nov 2026 10:57:15 +0530
Subject: [PATCH 5/5] docs(changelog): move sortable columns entry to
Unreleased (#217)
Co-Authored-By: Claude Sonnet 5.5
---
CHANGELOG.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c9a80aa..5b41eb2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,10 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
---
-### v3.10.0 — 2026-11-04
+### Unreleased
- **HTML report: sortable columns in Test Cases table** — clicking headers for Test, Status, Logic (ms), and Total (ms) sorts rows within each test class group ascending or descending with visual sort indicators, while preserving detail row attachments and cooperating seamlessly with active status, tag, and search filters. (#217)
+### v3.10.0 — 2026-11-04
- **HTML report: print stylesheet for "Save as PDF"** — printing the report now produces a clean document: UI chrome is hidden, all sections print instead of only the active tab, collapsed groups and failed-test details (error and full stack trace) are expanded, the light palette is used even in dark mode, and your on-screen filters are restored afterwards. (#221, #236)