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: 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.
Expand Down
2 changes: 2 additions & 0 deletions docs-site/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 14 additions & 3 deletions src/main/java/com/seleniumboot/reporting/HtmlReportGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() : "";
Expand Down Expand Up @@ -394,7 +403,7 @@ private static String buildRow(JsonNode test, int rowIndex, String groupId, bool
? "<div class=\"step-timeline-section\"><div class=\"step-timeline-header\">Steps (" + test.get("steps").size() + ")</div>"
+ "<div class=\"step-timeline\">" + stepsHtml + "</div></div>"
: "";
String detailDisplay = collapsed ? " style=\"display:none\"" : "";
String detailDisplay = detailHidden ? " style=\"display:none\"" : "";
detailRow = "<tr class=\"detail-row group-member\" data-group=\"" + groupKey + "\" id=\"detail-" + rowIndex + "\"" + detailDisplay + ">"
+ "<td colspan=\"" + colspan + "\"><div class=\"detail-panel\">"
+ failureDetail + perfHtml + sessionLink + traceLink + stepsSection + recordingHtml + aiHtml + errorHtml + traceHtml
Expand All @@ -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 ? "<td class=\"browser-cell\">" + escapeHtml(browser) + "</td>" : "";

Expand Down Expand Up @@ -588,7 +597,9 @@ private static String buildFailureRows(JsonNode tests, boolean showBrowser) {
+ "<span class=\"group-count\"> &mdash; " + total + " failure" + (total != 1 ? "s" : "") + "</span>"
+ "</td></tr>");
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();
Expand Down
12 changes: 0 additions & 12 deletions src/main/resources/report-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -531,19 +531,7 @@ <h1>Selenium Boot Report</h1>
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;
Expand Down
51 changes: 51 additions & 0 deletions src/test/java/com/seleniumboot/unit/ReportTabBehaviourTest.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
Loading