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: "Copy failure details" button** — failed tests with an error now have a button in the expanded detail row (and on the Failures tab) that copies the test id, error message and stack trace to the clipboard, with brief "Copied" feedback. Falls back to `execCommand('copy')` when `navigator.clipboard` is unavailable (e.g. some `file://` setups). (#216)

- **Fix: `@PreCondition` session restore on a fresh browser** — the cached cookies and localStorage were silently dropped when the next test started on `about:blank`, so only the first test per thread was logged in. Restore now navigates to the origin the session was captured on first.

- **LambdaTest cloud provider** — `execution.mode: lambdatest` now creates W3C
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,10 @@ private static String buildRow(JsonNode test, int rowIndex, String groupId, bool
+ (test.has("failureCategory") ? buildFailureWhy(test) : "");
String errorHtml = whyHtml + (errorMsg != null ? "<div class=\"error-msg\">" + escapeHtml(errorMsg) + "</div>" : "");
String traceHtml = stackTrace != null ? "<pre class=\"stack-trace\">" + escapeHtml(stackTrace) + "</pre>" : "";
String copyBtn = "FAILED".equals(status) && (errorMsg != null || stackTrace != null)
? "<button type=\"button\" class=\"copy-failure-btn\" data-test-id=\"" + escapeHtml(rawTestId)
+ "\" onclick=\"copyFailure(this, event)\">Copy failure details</button>"
: "";
String recordingHtml = recordingPath != null ? buildRecordingCell(recordingPath) : "";
String traceLink = tracePath != null ? buildTraceLink(tracePath) : "";
String sessionLink = sessionUrl != null ? buildSessionLink(sessionUrl) : "";
Expand All @@ -415,7 +419,7 @@ private static String buildRow(JsonNode test, int rowIndex, String groupId, bool
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
+ copyBtn + failureDetail + perfHtml + sessionLink + traceLink + stepsSection + recordingHtml + aiHtml + errorHtml + traceHtml
+ "</div></td>"
+ "</tr>";
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/report/report.css
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@
/* ── Detail / error panel ── */
.detail-row td { padding: 0; }
.detail-panel { padding: 16px 24px; background: var(--surface2); border-top: 2px solid var(--bad); }
.copy-failure-btn { float: right; margin: 0 0 8px 12px; border: 1px solid var(--border); background: var(--surface); color: var(--text-secondary); border-radius: 12px; padding: 2px 12px; font-size: 12px; cursor: pointer; }
.copy-failure-btn:hover { background: var(--primary); color: #fff; border-color: var(--primary); }
.error-msg { font-size: 13px; font-weight: 600; color: var(--bad); margin-bottom: 10px; }
.failure-group { padding: 10px 0; border-bottom: 1px solid var(--border); }
.failure-group:last-child { border-bottom: 0; }
Expand Down
36 changes: 36 additions & 0 deletions src/main/resources/report/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,39 @@ function applySearch() {
hdr.style.display = visibleGroups.has(groupId) ? '' : 'none';
});
}

// ── Copy failure details ──
function copyFailure(btn, event) {
if (event) event.stopPropagation();
const panel = btn.closest('.detail-panel');
const err = panel.querySelector('.error-msg');
const trace = panel.querySelector('.stack-trace');
const text = [
'Test: ' + btn.getAttribute('data-test-id'),
err ? 'Error: ' + err.textContent : '',
trace ? '\n' + trace.textContent : ''
].filter(Boolean).join('\n');
const done = ok => {
const label = btn.getAttribute('data-label') || btn.textContent;
btn.setAttribute('data-label', label);
btn.textContent = ok ? 'Copied' : 'Copy failed';
setTimeout(() => { btn.textContent = label; }, 1500);
};
const fallback = () => {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
let ok = false;
try { ok = document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
done(ok);
};
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => done(true), fallback);
} else {
fallback();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,21 @@ public void buildHtml_tagWithQuote_renderedSafelyWithDataTag() throws Exception
assertFalse(reportHtml.contains("filterByTag('it's'"));
assertFalse(reportHtml.contains("filterByTag('x');alert(1);//'"));
}

@Test
public void copyFailureButton_onlyOnFailedTestsWithErrorDetail() throws Exception {
String json = """
{"totalTests":3,"passedTests":1,"failedTests":1,"skippedTests":1,"passRate":33.3,"tests":[
{"testId":"a.A.ok","status":"PASSED","testLogicMs":1,"totalMs":1,"stepsNote":"x","description":"d","performanceMetrics":{"pageLoadMs":1}},
{"testId":"a.A.bad<x>","status":"FAILED","testLogicMs":1,"totalMs":1,"errorMessage":"boom","stackTrace":"at a.b"},
{"testId":"a.A.skip","status":"SKIPPED","testLogicMs":1,"totalMs":1,"errorMessage":"skipped because"}
]}""";
String reportHtml = HtmlReportGenerator.buildHtml(new ObjectMapper().readTree(json));
String body = reportHtml.substring(reportHtml.indexOf("<tbody"));
assertFalse(body.contains("data-test-id=\"a.A.ok\""), "no button on passed tests");
assertFalse(body.contains("data-test-id=\"a.A.skip\""), "no button on skipped tests");
assertTrue(body.contains("data-test-id=\"a.A.bad&lt;x&gt;\""), "test id is escaped in the attribute");
assertTrue(reportHtml.contains("function copyFailure("));
assertTrue(reportHtml.contains("document.execCommand('copy')"), "file:// fallback present");
}
}
Loading