@arghajit/playwright-pulse-report 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -54
- package/dist/reporter/attachment-utils.js +41 -33
- package/dist/reporter/playwright-pulse-reporter.d.ts +3 -0
- package/dist/reporter/playwright-pulse-reporter.js +190 -172
- package/dist/types/index.d.ts +7 -1
- package/package.json +8 -3
- package/scripts/generate-report.mjs +222 -158
- package/scripts/generate-static-report.mjs +324 -374
- package/scripts/generate-trend.mjs +1 -1
- package/scripts/sendReport.mjs +5 -5
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
|
-
import { readFileSync, existsSync as fsExistsSync } from "fs";
|
|
4
|
+
import { readFileSync, existsSync as fsExistsSync } from "fs";
|
|
5
5
|
import path from "path";
|
|
6
|
-
import { fork } from "child_process";
|
|
7
|
-
import { fileURLToPath } from "url";
|
|
6
|
+
import { fork } from "child_process";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
8
|
|
|
9
9
|
// Use dynamic import for chalk as it's ESM only
|
|
10
10
|
let chalk;
|
|
@@ -157,13 +157,13 @@ function sanitizeHTML(str) {
|
|
|
157
157
|
"<": "<",
|
|
158
158
|
">": ">",
|
|
159
159
|
'"': '"',
|
|
160
|
-
"'": "'",
|
|
160
|
+
"'": "'",
|
|
161
161
|
};
|
|
162
162
|
return replacements[match] || match;
|
|
163
163
|
});
|
|
164
164
|
}
|
|
165
165
|
function capitalize(str) {
|
|
166
|
-
if (!str) return "";
|
|
166
|
+
if (!str) return "";
|
|
167
167
|
return str[0].toUpperCase() + str.slice(1).toLowerCase();
|
|
168
168
|
}
|
|
169
169
|
function formatPlaywrightError(error) {
|
|
@@ -171,22 +171,18 @@ function formatPlaywrightError(error) {
|
|
|
171
171
|
return convertPlaywrightErrorToHTML(commandOutput);
|
|
172
172
|
}
|
|
173
173
|
function convertPlaywrightErrorToHTML(str) {
|
|
174
|
-
return
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
.replace(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
.replace(/<\/intensity>/g, "</span>")
|
|
187
|
-
// Convert newlines to <br> after processing other replacements
|
|
188
|
-
.replace(/\n/g, "<br>")
|
|
189
|
-
);
|
|
174
|
+
if (!str) return "";
|
|
175
|
+
return str
|
|
176
|
+
.replace(/^(\s+)/gm, (match) =>
|
|
177
|
+
match.replace(/ /g, " ").replace(/\t/g, " ")
|
|
178
|
+
)
|
|
179
|
+
.replace(/<red>/g, '<span style="color: red;">')
|
|
180
|
+
.replace(/<green>/g, '<span style="color: green;">')
|
|
181
|
+
.replace(/<dim>/g, '<span style="opacity: 0.6;">')
|
|
182
|
+
.replace(/<intensity>/g, '<span style="font-weight: bold;">')
|
|
183
|
+
.replace(/<\/color>/g, "</span>")
|
|
184
|
+
.replace(/<\/intensity>/g, "</span>")
|
|
185
|
+
.replace(/\n/g, "<br>");
|
|
190
186
|
}
|
|
191
187
|
function formatDuration(ms, options = {}) {
|
|
192
188
|
const {
|
|
@@ -227,19 +223,12 @@ function formatDuration(ms, options = {}) {
|
|
|
227
223
|
|
|
228
224
|
const totalRawSeconds = numMs / MS_PER_SECOND;
|
|
229
225
|
|
|
230
|
-
// Decision: Are we going to display hours or minutes?
|
|
231
|
-
// This happens if the duration is inherently >= 1 minute OR
|
|
232
|
-
// if it's < 1 minute but ceiling the seconds makes it >= 1 minute.
|
|
233
226
|
if (
|
|
234
227
|
totalRawSeconds < SECONDS_PER_MINUTE &&
|
|
235
228
|
Math.ceil(totalRawSeconds) < SECONDS_PER_MINUTE
|
|
236
229
|
) {
|
|
237
|
-
// Strictly seconds-only display, use precision.
|
|
238
230
|
return `${totalRawSeconds.toFixed(validPrecision)}s`;
|
|
239
231
|
} else {
|
|
240
|
-
// Display will include minutes and/or hours, or seconds round up to a minute.
|
|
241
|
-
// Seconds part should be an integer (ceiling).
|
|
242
|
-
// Round the total milliseconds UP to the nearest full second.
|
|
243
232
|
const totalMsRoundedUpToSecond =
|
|
244
233
|
Math.ceil(numMs / MS_PER_SECOND) * MS_PER_SECOND;
|
|
245
234
|
|
|
@@ -251,21 +240,15 @@ function formatDuration(ms, options = {}) {
|
|
|
251
240
|
const m = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_MINUTE));
|
|
252
241
|
remainingMs %= MS_PER_SECOND * SECONDS_PER_MINUTE;
|
|
253
242
|
|
|
254
|
-
const s = Math.floor(remainingMs / MS_PER_SECOND);
|
|
243
|
+
const s = Math.floor(remainingMs / MS_PER_SECOND);
|
|
255
244
|
|
|
256
245
|
const parts = [];
|
|
257
246
|
if (h > 0) {
|
|
258
247
|
parts.push(`${h}h`);
|
|
259
248
|
}
|
|
260
|
-
|
|
261
|
-
// Show minutes if:
|
|
262
|
-
// - hours are present (e.g., "1h 0m 5s")
|
|
263
|
-
// - OR minutes themselves are > 0 (e.g., "5m 10s")
|
|
264
|
-
// - OR the original duration was >= 1 minute (ensures "1m 0s" for 60000ms)
|
|
265
249
|
if (h > 0 || m > 0 || numMs >= MS_PER_SECOND * SECONDS_PER_MINUTE) {
|
|
266
250
|
parts.push(`${m}m`);
|
|
267
251
|
}
|
|
268
|
-
|
|
269
252
|
parts.push(`${s}s`);
|
|
270
253
|
|
|
271
254
|
return parts.join(" ");
|
|
@@ -1283,6 +1266,14 @@ function generateSuitesWidget(suitesData) {
|
|
|
1283
1266
|
</div>
|
|
1284
1267
|
</div>`;
|
|
1285
1268
|
}
|
|
1269
|
+
function getAttachmentIcon(contentType) {
|
|
1270
|
+
if (contentType.includes("pdf")) return "📄";
|
|
1271
|
+
if (contentType.includes("json")) return "{ }";
|
|
1272
|
+
if (contentType.includes("html") || contentType.includes("xml")) return "</>";
|
|
1273
|
+
if (contentType.includes("csv")) return "📊";
|
|
1274
|
+
if (contentType.startsWith("text/")) return "📝";
|
|
1275
|
+
return "📎";
|
|
1276
|
+
}
|
|
1286
1277
|
function generateHTML(reportData, trendData = null) {
|
|
1287
1278
|
const { run, results } = reportData;
|
|
1288
1279
|
const suitesData = getSuitesData(reportData.results || []);
|
|
@@ -1389,16 +1380,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1389
1380
|
.join("");
|
|
1390
1381
|
};
|
|
1391
1382
|
|
|
1392
|
-
// Local escapeHTML for screenshot rendering part, ensuring it uses proper entities
|
|
1393
|
-
const escapeHTMLForScreenshots = (str) => {
|
|
1394
|
-
if (str === null || str === undefined) return "";
|
|
1395
|
-
return String(str).replace(
|
|
1396
|
-
/[&<>"']/g,
|
|
1397
|
-
(match) =>
|
|
1398
|
-
({ "&": "&", "<": "<", ">": ">", '"': '"', "'": "'" }[match] ||
|
|
1399
|
-
match)
|
|
1400
|
-
);
|
|
1401
|
-
};
|
|
1402
1383
|
return `
|
|
1403
1384
|
<div class="test-case" data-status="${
|
|
1404
1385
|
test.status
|
|
@@ -1428,6 +1409,11 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1428
1409
|
</div>
|
|
1429
1410
|
<div class="test-case-content" style="display: none;">
|
|
1430
1411
|
<p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
|
|
1412
|
+
<p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
|
|
1413
|
+
test.workerId
|
|
1414
|
+
)} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
|
|
1415
|
+
test.totalWorkers
|
|
1416
|
+
)}]</p>
|
|
1431
1417
|
${
|
|
1432
1418
|
test.errorMessage
|
|
1433
1419
|
? `<div class="test-error-summary">${formatPlaywrightError(
|
|
@@ -1459,119 +1445,152 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1459
1445
|
<div class="steps-list">${generateStepsHTML(test.steps)}</div>
|
|
1460
1446
|
${
|
|
1461
1447
|
test.stdout && test.stdout.length > 0
|
|
1462
|
-
? `<div class="console-output-section"><h4>Console Output (stdout)</h4><pre class="console-log stdout-log" style="background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${
|
|
1463
|
-
.map((line) => sanitizeHTML(line))
|
|
1464
|
-
|
|
1448
|
+
? `<div class="console-output-section"><h4>Console Output (stdout)</h4><pre class="console-log stdout-log" style="background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
|
|
1449
|
+
test.stdout.map((line) => sanitizeHTML(line)).join("\n")
|
|
1450
|
+
)}</pre></div>`
|
|
1465
1451
|
: ""
|
|
1466
1452
|
}
|
|
1467
1453
|
${
|
|
1468
1454
|
test.stderr && test.stderr.length > 0
|
|
1469
|
-
? `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log" style="background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${
|
|
1470
|
-
.map((line) => sanitizeHTML(line))
|
|
1471
|
-
|
|
1455
|
+
? `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log" style="background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
|
|
1456
|
+
test.stderr.map((line) => sanitizeHTML(line)).join("\n")
|
|
1457
|
+
)}</pre></div>`
|
|
1472
1458
|
: ""
|
|
1473
1459
|
}
|
|
1474
1460
|
${
|
|
1475
1461
|
test.screenshots && test.screenshots.length > 0
|
|
1476
1462
|
? `
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1463
|
+
<div class="attachments-section">
|
|
1464
|
+
<h4>Screenshots</h4>
|
|
1465
|
+
<div class="attachments-grid">
|
|
1466
|
+
${test.screenshots
|
|
1467
|
+
.map(
|
|
1468
|
+
(screenshot, index) => `
|
|
1469
|
+
<div class="attachment-item">
|
|
1470
|
+
<img src="${screenshot}" alt="Screenshot ${index + 1}">
|
|
1471
|
+
<div class="attachment-info">
|
|
1472
|
+
<div class="trace-actions">
|
|
1473
|
+
<a href="${screenshot}" target="_blank" class="view-full">View Full Image</a>
|
|
1474
|
+
<a href="${screenshot}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
|
|
1475
|
+
</div>
|
|
1476
|
+
</div>
|
|
1477
|
+
</div>
|
|
1478
|
+
`
|
|
1479
|
+
)
|
|
1480
|
+
.join("")}
|
|
1481
|
+
</div>
|
|
1491
1482
|
</div>
|
|
1492
|
-
|
|
1493
|
-
</div>
|
|
1494
|
-
`
|
|
1495
|
-
)
|
|
1496
|
-
.join("")}
|
|
1497
|
-
</div>
|
|
1498
|
-
</div>
|
|
1499
|
-
`
|
|
1483
|
+
`
|
|
1500
1484
|
: ""
|
|
1501
1485
|
}
|
|
1502
1486
|
${
|
|
1503
|
-
test.videoPath
|
|
1504
|
-
? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
)}.${fileExtension}">Download</a></div></div></div>`;
|
|
1538
|
-
})
|
|
1539
|
-
.join("");
|
|
1540
|
-
})()}</div></div>`
|
|
1487
|
+
test.videoPath && test.videoPath.length > 0
|
|
1488
|
+
? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
|
|
1489
|
+
.map((videoUrl, index) => {
|
|
1490
|
+
const fileExtension = String(videoUrl)
|
|
1491
|
+
.split(".")
|
|
1492
|
+
.pop()
|
|
1493
|
+
.toLowerCase();
|
|
1494
|
+
const mimeType =
|
|
1495
|
+
{
|
|
1496
|
+
mp4: "video/mp4",
|
|
1497
|
+
webm: "video/webm",
|
|
1498
|
+
ogg: "video/ogg",
|
|
1499
|
+
mov: "video/quicktime",
|
|
1500
|
+
avi: "video/x-msvideo",
|
|
1501
|
+
}[fileExtension] || "video/mp4";
|
|
1502
|
+
return `<div class="attachment-item video-item">
|
|
1503
|
+
<video controls width="100%" height="auto" title="Video ${
|
|
1504
|
+
index + 1
|
|
1505
|
+
}">
|
|
1506
|
+
<source src="${sanitizeHTML(
|
|
1507
|
+
videoUrl
|
|
1508
|
+
)}" type="${mimeType}">
|
|
1509
|
+
Your browser does not support the video tag.
|
|
1510
|
+
</video>
|
|
1511
|
+
<div class="attachment-info">
|
|
1512
|
+
<div class="trace-actions">
|
|
1513
|
+
<a href="${sanitizeHTML(
|
|
1514
|
+
videoUrl
|
|
1515
|
+
)}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
|
|
1516
|
+
</div>
|
|
1517
|
+
</div>
|
|
1518
|
+
</div>`;
|
|
1519
|
+
})
|
|
1520
|
+
.join("")}</div></div>`
|
|
1541
1521
|
: ""
|
|
1542
1522
|
}
|
|
1543
1523
|
${
|
|
1544
1524
|
test.tracePath
|
|
1545
|
-
?
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1525
|
+
? `
|
|
1526
|
+
<div class="attachments-section">
|
|
1527
|
+
<h4>Trace Files</h4>
|
|
1528
|
+
<div class="attachments-grid">
|
|
1529
|
+
<div class="attachment-item trace-item">
|
|
1530
|
+
<div class="trace-preview">
|
|
1531
|
+
<span class="trace-icon">📄</span>
|
|
1532
|
+
<span class="trace-name">${sanitizeHTML(
|
|
1533
|
+
path.basename(test.tracePath)
|
|
1534
|
+
)}</span>
|
|
1535
|
+
</div>
|
|
1536
|
+
<div class="attachment-info">
|
|
1537
|
+
<div class="trace-actions">
|
|
1538
|
+
<a href="${sanitizeHTML(
|
|
1539
|
+
test.tracePath
|
|
1540
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
1541
|
+
path.basename(test.tracePath)
|
|
1542
|
+
)}" class="download-trace">Download Trace</a>
|
|
1543
|
+
</div>
|
|
1544
|
+
</div>
|
|
1545
|
+
</div>
|
|
1546
|
+
</div>
|
|
1547
|
+
</div>
|
|
1548
|
+
`
|
|
1549
|
+
: ""
|
|
1550
|
+
}
|
|
1551
|
+
${
|
|
1552
|
+
test.attachments && test.attachments.length > 0
|
|
1553
|
+
? `
|
|
1554
|
+
<div class="attachments-section">
|
|
1555
|
+
<h4>Other Attachments</h4>
|
|
1556
|
+
<div class="attachments-grid">
|
|
1557
|
+
${test.attachments
|
|
1558
|
+
.map(
|
|
1559
|
+
(attachment) => `
|
|
1560
|
+
<div class="attachment-item generic-attachment">
|
|
1561
|
+
<div class="attachment-icon">${getAttachmentIcon(
|
|
1562
|
+
attachment.contentType
|
|
1563
|
+
)}</div>
|
|
1564
|
+
<div class="attachment-caption">
|
|
1565
|
+
<span class="attachment-name" title="${sanitizeHTML(
|
|
1566
|
+
attachment.name
|
|
1567
|
+
)}">${sanitizeHTML(attachment.name)}</span>
|
|
1568
|
+
<span class="attachment-type">${sanitizeHTML(
|
|
1569
|
+
attachment.contentType
|
|
1570
|
+
)}</span>
|
|
1571
|
+
</div>
|
|
1572
|
+
<div class="attachment-info">
|
|
1573
|
+
<div class="trace-actions">
|
|
1574
|
+
<a href="${sanitizeHTML(
|
|
1575
|
+
attachment.path
|
|
1576
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
1577
|
+
attachment.name
|
|
1578
|
+
)}" class="download-trace">Download</a>
|
|
1579
|
+
</div>
|
|
1580
|
+
</div>
|
|
1581
|
+
</div>
|
|
1582
|
+
`
|
|
1583
|
+
)
|
|
1584
|
+
.join("")}
|
|
1585
|
+
</div>
|
|
1586
|
+
</div>
|
|
1587
|
+
`
|
|
1569
1588
|
: ""
|
|
1570
1589
|
}
|
|
1571
1590
|
${
|
|
1572
1591
|
test.codeSnippet
|
|
1573
|
-
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${
|
|
1574
|
-
test.codeSnippet
|
|
1592
|
+
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
|
|
1593
|
+
sanitizeHTML(test.codeSnippet)
|
|
1575
1594
|
)}</code></pre></div>`
|
|
1576
1595
|
: ""
|
|
1577
1596
|
}
|
|
@@ -1601,14 +1620,11 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1601
1620
|
}
|
|
1602
1621
|
.trend-chart-container, .test-history-trend div[id^="testHistoryChart-"] { min-height: 100px; }
|
|
1603
1622
|
.lazy-load-chart .no-data, .lazy-load-chart .no-data-chart { display: flex; align-items: center; justify-content: center; height: 100%; font-style: italic; color: var(--dark-gray-color); }
|
|
1604
|
-
|
|
1605
|
-
/* General Highcharts styling */
|
|
1606
1623
|
.highcharts-background { fill: transparent; }
|
|
1607
1624
|
.highcharts-title, .highcharts-subtitle { font-family: var(--font-family); }
|
|
1608
1625
|
.highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
|
|
1609
1626
|
.highcharts-axis-title { fill: var(--text-color) !important; }
|
|
1610
1627
|
.highcharts-tooltip > span { background-color: rgba(10,10,10,0.92) !important; border-color: rgba(10,10,10,0.92) !important; color: #f5f5f5 !important; padding: 10px !important; border-radius: 6px !important; }
|
|
1611
|
-
|
|
1612
1628
|
body { font-family: var(--font-family); margin: 0; background-color: var(--background-color); color: var(--text-color); line-height: 1.65; font-size: 16px; }
|
|
1613
1629
|
.container { padding: 30px; border-radius: var(--border-radius); box-shadow: var(--box-shadow); background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec); }
|
|
1614
1630
|
.header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; padding-bottom: 25px; border-bottom: 1px solid var(--border-color); margin-bottom: 25px; }
|
|
@@ -1704,11 +1720,19 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1704
1720
|
.attachment-item { border: 1px solid var(--border-color); border-radius: var(--border-radius); background-color: #fff; box-shadow: var(--box-shadow-light); overflow: hidden; display: flex; flex-direction: column; transition: transform 0.2s ease-out, box-shadow 0.2s ease-out; }
|
|
1705
1721
|
.attachment-item:hover { transform: translateY(-4px); box-shadow: var(--box-shadow); }
|
|
1706
1722
|
.attachment-item img { width: 100%; height: 180px; object-fit: cover; display: block; border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease; }
|
|
1723
|
+
.attachment-info { padding: 12px; margin-top: auto; background-color: #fafafa;}
|
|
1707
1724
|
.attachment-item a:hover img { opacity: 0.85; }
|
|
1708
1725
|
.attachment-caption { padding: 12px 15px; font-size: 0.9em; text-align: center; color: var(--text-color-secondary); word-break: break-word; background-color: var(--light-gray-color); }
|
|
1709
1726
|
.video-item a, .trace-item a { display: block; margin-bottom: 8px; color: var(--primary-color); text-decoration: none; font-weight: 500; }
|
|
1710
1727
|
.video-item a:hover, .trace-item a:hover { text-decoration: underline; }
|
|
1711
1728
|
.code-section pre { background-color: #2d2d2d; color: #f0f0f0; padding: 20px; border-radius: 6px; overflow-x: auto; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 0.95em; line-height:1.6;}
|
|
1729
|
+
.trace-actions { display: flex; justify-content: center; }
|
|
1730
|
+
.trace-actions a { text-decoration: none; color: var(--primary-color); font-weight: 500; font-size: 0.9em; }
|
|
1731
|
+
.generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
|
|
1732
|
+
.attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
|
|
1733
|
+
.attachment-caption { display: flex; flex-direction: column; }
|
|
1734
|
+
.attachment-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1735
|
+
.attachment-type { font-size: 0.8rem; color: var(--text-color-secondary); }
|
|
1712
1736
|
.trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
|
|
1713
1737
|
.test-history-container h2.tab-main-title { font-size: 1.6em; margin-bottom: 18px; color: var(--primary-color); border-bottom: 1px solid var(--border-color); padding-bottom: 12px;}
|
|
1714
1738
|
.test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
|
|
@@ -2035,24 +2059,64 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2035
2059
|
}
|
|
2036
2060
|
document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
|
|
2037
2061
|
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2062
|
+
function copyErrorToClipboard(button) {
|
|
2063
|
+
// 1. Find the main error container, which should always be present.
|
|
2064
|
+
const errorContainer = button.closest('.step-error');
|
|
2065
|
+
if (!errorContainer) {
|
|
2066
|
+
console.error("Could not find '.step-error' container. The report's HTML structure might have changed.");
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
let errorText;
|
|
2071
|
+
|
|
2072
|
+
// 2. First, try to find the preferred .stack-trace element (the "happy path").
|
|
2073
|
+
const stackTraceElement = errorContainer.querySelector('.stack-trace');
|
|
2074
|
+
|
|
2075
|
+
if (stackTraceElement) {
|
|
2076
|
+
// If it exists, use its text content. This handles standard assertion errors.
|
|
2077
|
+
errorText = stackTraceElement.textContent;
|
|
2078
|
+
} else {
|
|
2079
|
+
// 3. FALLBACK: If .stack-trace doesn't exist, this is likely an unstructured error.
|
|
2080
|
+
// We clone the container to avoid manipulating the live DOM or copying the button's own text.
|
|
2081
|
+
const clonedContainer = errorContainer.cloneNode(true);
|
|
2082
|
+
|
|
2083
|
+
// Remove the button from our clone before extracting the text.
|
|
2084
|
+
const buttonInClone = clonedContainer.querySelector('button');
|
|
2085
|
+
if (buttonInClone) {
|
|
2086
|
+
buttonInClone.remove();
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// Use the text content of the cleaned container as the fallback.
|
|
2090
|
+
errorText = clonedContainer.textContent;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// 4. Proceed with the clipboard logic, ensuring text is not null and is trimmed.
|
|
2094
|
+
if (!errorText) {
|
|
2095
|
+
console.error('Could not extract error text.');
|
|
2096
|
+
button.textContent = 'Nothing to copy';
|
|
2097
|
+
setTimeout(() => { button.textContent = 'Copy Error'; }, 2000);
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
const textarea = document.createElement('textarea');
|
|
2102
|
+
textarea.value = errorText.trim(); // Trim whitespace for a cleaner copy.
|
|
2103
|
+
textarea.style.position = 'fixed'; // Prevent screen scroll
|
|
2104
|
+
textarea.style.top = '-9999px';
|
|
2105
|
+
document.body.appendChild(textarea);
|
|
2106
|
+
textarea.select();
|
|
2107
|
+
|
|
2108
|
+
try {
|
|
2109
|
+
const successful = document.execCommand('copy');
|
|
2110
|
+
const originalText = button.textContent;
|
|
2111
|
+
button.textContent = successful ? 'Copied!' : 'Failed';
|
|
2112
|
+
setTimeout(() => {
|
|
2113
|
+
button.textContent = originalText;
|
|
2114
|
+
}, 2000);
|
|
2115
|
+
} catch (err) {
|
|
2116
|
+
console.error('Failed to copy: ', err);
|
|
2117
|
+
button.textContent = 'Failed';
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2056
2120
|
document.body.removeChild(textarea);
|
|
2057
2121
|
}
|
|
2058
2122
|
</script>
|