diff --git a/CHANGELOG.md b/CHANGELOG.md
index 89f22ff..5b41eb2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
---
+### 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)
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 |
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();
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");
+ }
+ }
+ }
}