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
10 changes: 10 additions & 0 deletions docs-site/docs/reporting/html-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@ All screenshots are Base64-encoded and embedded inline. The report is a single f

No images folder, no asset references, no server needed.

### Share a single test

The full report can run to several megabytes. To share one test, expand it and click
**Download this test** in its detail panel. You get a standalone `<testId>.html` file with
that test's status, error, stack trace, steps and screenshots. It opens offline in any
browser, keeps the report's current light or dark theme, and is small enough to attach
to a Jira ticket, GitHub issue or Slack thread.

The file is built in your browser when you click, so it adds nothing to the report size.

---

## Step timeline
Expand Down
14 changes: 14 additions & 0 deletions src/main/resources/report/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,17 @@ function escapeHtmlForExport(str) {
.replace(/'/g, '&#39;');
}

// Tag pills filter the full report; the standalone export has nothing to filter,
// so neutralise their handlers instead of shipping calls to undefined functions.
// They still stop propagation, so clicking a pill doesn't collapse the detail row.
function stripReportOnlyHandlers(root) {
root.querySelectorAll('[onclick^="filterByTag"]').forEach(function(el) {
el.setAttribute('onclick', 'event.stopPropagation()');
el.removeAttribute('title');
el.style.cursor = 'default';
});
}

function buildStandaloneTestHtml(testId, testRow, panel, activeTheme) {
const styleTag = document.querySelector('style');
const css = styleTag ? styleTag.textContent : '';
Expand Down Expand Up @@ -484,6 +495,7 @@ function buildStandaloneTestHtml(testId, testRow, panel, activeTheme) {
if (icon) {
icon.classList.add('open');
}
stripReportOnlyHandlers(testRowClone);
testRowHtml = testRowClone.outerHTML;
}

Expand All @@ -498,10 +510,12 @@ function buildStandaloneTestHtml(testId, testRow, panel, activeTheme) {
if (panelClone) {
panelClone.querySelectorAll('.copy-failure-btn, .download-test-btn').forEach(function(b) { b.remove(); });
}
stripReportOnlyHandlers(detailRowClone);
detailRowHtml = detailRowClone.outerHTML;
} else if (panel) {
const panelClone = panel.cloneNode(true);
panelClone.querySelectorAll('.copy-failure-btn, .download-test-btn').forEach(function(b) { b.remove(); });
stripReportOnlyHandlers(panelClone);
detailRowHtml = '<tr class="detail-row" id="standaloneDetailRow"><td colspan="' + colspan + '">'
+ '<div class="detail-panel">' + panelClone.innerHTML + '</div>'
+ '</td></tr>';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,11 @@ public void downloadTestScript_containsStandaloneExportLogic() throws Exception
// Self-contained lightbox and offline script
assertTrue(reportHtml.contains("function openLightbox("), "offline script must define lightbox support");
assertTrue(reportHtml.contains("function toggleDark("), "offline script must define theme toggle");

// Tag pills call filterByTag, which the standalone file doesn't define
assertTrue(reportHtml.contains("function stripReportOnlyHandlers("), "export must neutralise report-only handlers");
assertTrue(reportHtml.contains("stripReportOnlyHandlers(testRowClone)"), "test row clone must be stripped");
assertTrue(reportHtml.contains("stripReportOnlyHandlers(detailRowClone)"), "detail row clone must be stripped");
}

@Test
Expand Down Expand Up @@ -808,87 +813,4 @@ public void downloadTest_standaloneExport_isFullySelfContainedAndHasNoExternalFe
assertTrue(js.contains("new Blob("), "Export must generate file in-memory using client-side Blob");
assertTrue(js.contains("buildStandaloneTestHtml"), "Export must include standalone HTML builder");
}

@Test
public void generate_writesSampleReportToTargetForManualVerification() throws Exception {
java.nio.file.Path targetDir = java.nio.file.Paths.get("target");
if (!java.nio.file.Files.exists(targetDir)) {
java.nio.file.Files.createDirectories(targetDir);
}
String pngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
java.nio.file.Path sampleScreenshot = targetDir.resolve("sample-fail.png");
java.nio.file.Files.write(sampleScreenshot, java.util.Base64.getDecoder().decode(pngBase64));

String json = String.format("""
{
"totalTests": 3,
"passedTests": 1,
"failedTests": 1,
"skippedTests": 1,
"passRate": 33.3,
"suiteDurationMs": 5200,
"tests": [
{
"testId": "com.example.cart.CheckoutTest.successfulCheckout",
"testClassName": "CheckoutTest",
"status": "PASSED",
"browser": "chrome",
"tags": ["smoke", "checkout"],
"testLogicMs": 1400,
"totalMs": 2100,
"description": "Verify checkout completes with valid credentials",
"steps": [
{"name": "Open login page", "status": "PASS", "offsetMs": 200},
{"name": "Submit credentials", "status": "PASS", "offsetMs": 800},
{"name": "Place order", "status": "PASS", "offsetMs": 1400}
]
},
{
"testId": "com.example.auth.LoginTest.invalidPasswordShowsError",
"testClassName": "LoginTest",
"status": "FAILED",
"browser": "firefox",
"tags": ["regression", "auth"],
"testLogicMs": 850,
"totalMs": 1600,
"description": "Verify alert banner is displayed when password is wrong",
"screenshotPath": "%s",
"errorMessage": "Expected error banner not visible after 5000ms",
"stackTrace": "org.openqa.selenium.TimeoutException: Expected condition failed: waiting for visibility of element located by By.id: alert-banner\\n\\tat com.example.auth.LoginTest.invalidPasswordShowsError(LoginTest.java:42)",
"failureCategory": "TIMEOUT",
"failureType": "PRODUCT_BUG",
"suggestedAction": "Check login service latency or increase banner wait timeout",
"failureDetail": {
"failingLocator": "By.id: alert-banner",
"applicationFrame": "LoginTest.java:42",
"lastSuccessfulStep": "Enter password",
"pageUrl": "https://example.com/login",
"pageTitle": "Login - Example App",
"consoleErrors": ["401 Unauthorized: Invalid credentials"]
},
"steps": [
{"name": "Navigate to /login", "status": "PASS", "offsetMs": 150},
{"name": "Enter wrong password", "status": "PASS", "offsetMs": 400},
{"name": "Wait for banner", "status": "FAIL", "offsetMs": 850, "screenshotBase64": "%s"}
]
},
{
"testId": "com.example.auth.LoginTest.ssoLoginBypassed",
"testClassName": "LoginTest",
"status": "SKIPPED",
"browser": "chrome",
"tags": ["sso"],
"testLogicMs": 0,
"totalMs": 10,
"errorMessage": "Skipped: SSO identity provider is temporarily disabled in test environment"
}
]
}
""", sampleScreenshot.toString().replace("\\", "\\\\"), pngBase64);

String reportHtml = HtmlReportGenerator.buildHtml(new ObjectMapper().readTree(json));
java.nio.file.Path sampleReportPath = targetDir.resolve("sample-verification-report.html");
java.nio.file.Files.writeString(sampleReportPath, reportHtml);
assertTrue(java.nio.file.Files.exists(sampleReportPath), "Sample verification report must be written to target");
}
}
Loading