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
? "
"
: "";
- 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");
+ }
+ }
+}