diff --git a/CHANGELOG.md b/CHANGELOG.md index 452732c..35587dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ### Unreleased +- **HTML report: tab and failure fixes** — the report always opens on the Dashboard (it no longer restores the last-open tab), failure details start collapsed, and the Failures tab no longer goes empty after you expand or collapse a class in Test Cases (both tabs shared one group id). + - **AI provider is configurable** — `ai.provider` selects the backend for failure analysis and the run summary: `anthropic` (default, unchanged), `gemini`, or `openai` (any OpenAI-compatible `/chat/completions` endpoint — OpenAI, Azure, Groq, local Ollama via `ai.baseUrl`). `ai.model` now defaults per provider. Existing configs keep working with no change. - **AI run summary (opt-in)** — `ai.runSummary: true` adds one short paragraph to the HTML dashboard summarising the run's failures: which same-cause groups account for most of them, which look like product bugs versus test or environment problems, and what to check first. One API call per run, independent of `ai.failureAnalysis`; only grouped error messages and test names are sent (see the data note in the HTML report docs). Absent when disabled, when nothing failed, or on any error; the suite result is never affected. The text is exported as `aiRunSummary` in the metrics JSON. diff --git a/docs-site/docs/changelog.md b/docs-site/docs/changelog.md index a28340d..d12d4fe 100644 --- a/docs-site/docs/changelog.md +++ b/docs-site/docs/changelog.md @@ -11,6 +11,8 @@ All notable changes to Selenium Boot are documented here. ## [Unreleased] +- **HTML report: tab and failure fixes** — the report always opens on the Dashboard (it no longer restores the last-open tab), failure details start collapsed, and the Failures tab no longer goes empty after you expand or collapse a class in Test Cases (both tabs shared one group id). + - **AI provider is configurable** — `ai.provider` selects the backend for failure analysis and the run summary: `anthropic` (default, unchanged), `gemini`, or `openai` (any OpenAI-compatible `/chat/completions` endpoint — OpenAI, Azure, Groq, local Ollama via `ai.baseUrl`). `ai.model` now defaults per provider. Existing configs keep working with no change. - **AI run summary (opt-in)** — `ai.runSummary: true` adds one short paragraph to the HTML dashboard summarising the run's failures: which same-cause groups account for most of them, which look like product bugs versus test or environment problems, and what to check first. One API call per run, independent of `ai.failureAnalysis`; only grouped error messages and test names are sent (see the data note in the HTML report docs). Absent when disabled, when nothing failed, or on any error; the suite result is never affected. The text is exported as `aiRunSummary` in the metrics JSON. diff --git a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java index c9aa361..7a6a012 100644 --- a/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java +++ b/src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java @@ -341,6 +341,15 @@ private static String buildRow(JsonNode test, int rowIndex, String groupId, bool } private static String buildRow(JsonNode test, int rowIndex, String groupId, boolean collapsed, boolean showBrowser) { + return buildRow(test, rowIndex, groupId, collapsed, collapsed, showBrowser); + } + + /** + * @param collapsed hide the test row itself (its class group is collapsed) + * @param detailHidden hide the expandable detail row under it + */ + private static String buildRow(JsonNode test, int rowIndex, String groupId, boolean collapsed, + boolean detailHidden, boolean showBrowser) { String status = test.has("status") ? test.get("status").asText() : "UNKNOWN"; String statusClass = "status-" + status.toLowerCase(); String rawTestId = test.has("testId") ? test.get("testId").asText() : ""; @@ -394,7 +403,7 @@ private static String buildRow(JsonNode test, int rowIndex, String groupId, bool ? "
Steps (" + test.get("steps").size() + ")
" + "
" + stepsHtml + "
" : ""; - String detailDisplay = collapsed ? " style=\"display:none\"" : ""; + String detailDisplay = detailHidden ? " style=\"display:none\"" : ""; detailRow = "" + "
" + failureDetail + perfHtml + sessionLink + traceLink + stepsSection + recordingHtml + aiHtml + errorHtml + traceHtml @@ -405,7 +414,7 @@ private static String buildRow(JsonNode test, int rowIndex, String groupId, bool String rowClass = "group-member" + (hasDetail ? " expandable" : ""); String clickAttr = hasDetail ? " onclick=\"toggleDetail(" + rowIndex + ")\"" : ""; String memberStyle = collapsed ? " style=\"display:none\"" : ""; - String iconOpen = (!collapsed && hasDetail) ? " open" : ""; + String iconOpen = (!detailHidden && hasDetail) ? " open" : ""; String browserCell = showBrowser ? "" + escapeHtml(browser) + "" : ""; @@ -588,7 +597,9 @@ private static String buildFailureRows(JsonNode tests, boolean showBrowser) { + " — " + total + " failure" + (total != 1 ? "s" : "") + "" + ""); for (JsonNode test : entry.getValue()) { - rows.append(buildRow(test, rowIndex++, entry.getKey(), false, showBrowser)); + // The "failures:" prefix keeps these rows out of the Test Cases tab's group toggle, + // which selects by data-group. Details start collapsed; the row stays visible. + rows.append(buildRow(test, rowIndex++, "failures:" + entry.getKey(), false, true, showBrowser)); } } return rows.toString(); diff --git a/src/main/resources/report-template.html b/src/main/resources/report-template.html index a0bf3fd..d3bb0ae 100644 --- a/src/main/resources/report-template.html +++ b/src/main/resources/report-template.html @@ -531,19 +531,7 @@

Selenium Boot Report

document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.getElementById('tab-' + tabId).classList.add('active'); if (el) el.classList.add('active'); - try { localStorage.setItem('sb-tab', tabId); } catch(e) {} } -// Restore last active tab on load -(function() { - try { - const last = localStorage.getItem('sb-tab'); - if (last && document.getElementById('tab-' + last)) { - const el = document.querySelector('.nav-item[onclick*="\'' + last + '\'"]'); - showTab(last, el); - } - } catch(e) {} -})(); - // ── Lightbox ── function openLightbox(src) { document.getElementById('lightbox-img').src = src; diff --git a/src/test/java/com/seleniumboot/unit/ReportTabBehaviourTest.java b/src/test/java/com/seleniumboot/unit/ReportTabBehaviourTest.java new file mode 100644 index 0000000..332077a --- /dev/null +++ b/src/test/java/com/seleniumboot/unit/ReportTabBehaviourTest.java @@ -0,0 +1,51 @@ +package com.seleniumboot.unit; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; + +public class ReportTabBehaviourTest { + + private static String failureRows() throws Exception { + JsonNode tests = new ObjectMapper().readTree(""" + [{"testClassName":"com.x.LoginTest","testName":"com.x.LoginTest.badLogin","status":"FAILED", + "errorMessage":"boom","stackTrace":"java.lang.AssertionError: boom\\n\\tat x"}] + """); + Method m = com.seleniumboot.reporting.HtmlReportGenerator.class + .getDeclaredMethod("buildFailureRows", JsonNode.class, boolean.class); + m.setAccessible(true); + return (String) m.invoke(null, tests, false); + } + + @Test + public void failureRowsDoNotShareGroupIdWithTestCasesTable() throws Exception { + // The Test Cases group toggle selects by data-group; sharing the id hid failure rows too. + String html = failureRows(); + Assert.assertFalse(html.contains("data-group=\"com.x.LoginTest\""), html); + Assert.assertTrue(html.contains("data-group=\"failures:com.x.LoginTest\""), html); + } + + @Test + public void failureDetailsStartCollapsedButRowsStayVisible() throws Exception { + String html = failureRows(); + int detail = html.indexOf("class=\"detail-row"); + Assert.assertTrue(detail > 0, html); + Assert.assertTrue(html.substring(detail, html.indexOf('>', detail)).contains("display:none"), html); + int row = html.indexOf("class=\"group-member expandable\""); + Assert.assertTrue(row > 0, html); + Assert.assertFalse(html.substring(row, html.indexOf('>', row)).contains("display:none"), html); + Assert.assertFalse(html.contains("expand-icon open"), html); + } + + @Test + public void reportAlwaysOpensOnDashboard() throws Exception { + try (var in = getClass().getResourceAsStream("/report-template.html")) { + String t = new String(in.readAllBytes(), StandardCharsets.UTF_8); + Assert.assertFalse(t.contains("sb-tab"), "report must not restore the last-open tab"); + } + } +}