@arghajit/playwright-pulse-report 0.2.2 → 0.2.3
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 +4 -54
- package/dist/reporter/attachment-utils.js +41 -33
- package/dist/reporter/playwright-pulse-reporter.js +65 -127
- package/dist/types/index.d.ts +6 -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;
|
|
@@ -21,16 +21,17 @@ try {
|
|
|
21
21
|
gray: (text) => text,
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
|
|
24
25
|
// Default configuration
|
|
25
26
|
const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
26
27
|
const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
|
|
27
28
|
const DEFAULT_HTML_FILE = "playwright-pulse-static-report.html";
|
|
29
|
+
|
|
28
30
|
// Helper functions
|
|
29
31
|
export function ansiToHtml(text) {
|
|
30
32
|
if (!text) {
|
|
31
33
|
return "";
|
|
32
34
|
}
|
|
33
|
-
|
|
34
35
|
const codes = {
|
|
35
36
|
0: "color:inherit;font-weight:normal;font-style:normal;text-decoration:none;opacity:1;background-color:inherit;",
|
|
36
37
|
1: "font-weight:bold",
|
|
@@ -82,18 +83,14 @@ export function ansiToHtml(text) {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
};
|
|
85
|
-
|
|
86
86
|
const resetAndApplyNewCodes = (newCodesStr) => {
|
|
87
87
|
const newCodes = newCodesStr.split(";");
|
|
88
|
-
|
|
89
88
|
if (newCodes.includes("0")) {
|
|
90
89
|
currentStylesArray = [];
|
|
91
90
|
if (codes["0"]) currentStylesArray.push(codes["0"]);
|
|
92
91
|
}
|
|
93
|
-
|
|
94
92
|
for (const code of newCodes) {
|
|
95
93
|
if (code === "0") continue;
|
|
96
|
-
|
|
97
94
|
if (codes[code]) {
|
|
98
95
|
if (code === "39") {
|
|
99
96
|
currentStylesArray = currentStylesArray.filter(
|
|
@@ -123,12 +120,9 @@ export function ansiToHtml(text) {
|
|
|
123
120
|
}
|
|
124
121
|
applyStyles();
|
|
125
122
|
};
|
|
126
|
-
|
|
127
123
|
const segments = text.split(/(\x1b\[[0-9;]*m)/g);
|
|
128
|
-
|
|
129
124
|
for (const segment of segments) {
|
|
130
125
|
if (!segment) continue;
|
|
131
|
-
|
|
132
126
|
if (segment.startsWith("\x1b[") && segment.endsWith("m")) {
|
|
133
127
|
const command = segment.slice(2, -1);
|
|
134
128
|
resetAndApplyNewCodes(command);
|
|
@@ -142,28 +136,23 @@ export function ansiToHtml(text) {
|
|
|
142
136
|
html += escapedContent;
|
|
143
137
|
}
|
|
144
138
|
}
|
|
145
|
-
|
|
146
139
|
if (openSpan) {
|
|
147
140
|
html += "</span>";
|
|
148
141
|
}
|
|
149
|
-
|
|
150
142
|
return html;
|
|
151
143
|
}
|
|
144
|
+
|
|
152
145
|
function sanitizeHTML(str) {
|
|
153
146
|
if (str === null || str === undefined) return "";
|
|
154
|
-
return String(str).replace(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
"<": "<",
|
|
158
|
-
|
|
159
|
-
'"': '"',
|
|
160
|
-
"'": "'", // or '
|
|
161
|
-
};
|
|
162
|
-
return replacements[match] || match;
|
|
163
|
-
});
|
|
147
|
+
return String(str).replace(
|
|
148
|
+
/[&<>"']/g,
|
|
149
|
+
(match) =>
|
|
150
|
+
({ "&": "&", "<": "<", ">": ">", '"': '"', "'": "'" }[match] || match)
|
|
151
|
+
);
|
|
164
152
|
}
|
|
153
|
+
|
|
165
154
|
function capitalize(str) {
|
|
166
|
-
if (!str) return "";
|
|
155
|
+
if (!str) return "";
|
|
167
156
|
return str[0].toUpperCase() + str.slice(1).toLowerCase();
|
|
168
157
|
}
|
|
169
158
|
function formatPlaywrightError(error) {
|
|
@@ -1304,11 +1293,12 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1304
1293
|
runSummary.totalTests > 0
|
|
1305
1294
|
? formatDuration(runSummary.duration / runSummary.totalTests)
|
|
1306
1295
|
: "0.0s";
|
|
1296
|
+
|
|
1307
1297
|
function generateTestCasesHTML() {
|
|
1308
1298
|
if (!results || results.length === 0)
|
|
1309
1299
|
return '<div class="no-tests">No test results found in this run.</div>';
|
|
1310
1300
|
return results
|
|
1311
|
-
.map((test
|
|
1301
|
+
.map((test) => {
|
|
1312
1302
|
const browser = test.browser || "unknown";
|
|
1313
1303
|
const testFileParts = test.name.split(" > ");
|
|
1314
1304
|
const testTitle =
|
|
@@ -1324,319 +1314,242 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1324
1314
|
? `step-hook step-hook-${step.hookType}`
|
|
1325
1315
|
: "";
|
|
1326
1316
|
const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
|
|
1327
|
-
return
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
<span class="step-icon">${getStatusIcon(step.status)}</span>
|
|
1331
|
-
<span class="step-title">${sanitizeHTML(
|
|
1317
|
+
return `<div class="step-item" style="--depth: ${depth};"><div class="step-header ${stepClass}" role="button" aria-expanded="false"><span class="step-icon">${getStatusIcon(
|
|
1318
|
+
step.status
|
|
1319
|
+
)}</span><span class="step-title">${sanitizeHTML(
|
|
1332
1320
|
step.title
|
|
1333
|
-
)}${hookIndicator}</span
|
|
1334
|
-
<span class="step-duration">${formatDuration(
|
|
1321
|
+
)}${hookIndicator}</span><span class="step-duration">${formatDuration(
|
|
1335
1322
|
step.duration
|
|
1336
|
-
)}</span
|
|
1337
|
-
</div>
|
|
1338
|
-
<div class="step-details" style="display: none;">
|
|
1339
|
-
${
|
|
1323
|
+
)}</span></div><div class="step-details" style="display: none;">${
|
|
1340
1324
|
step.codeLocation
|
|
1341
1325
|
? `<div class="step-info"><strong>Location:</strong> ${sanitizeHTML(
|
|
1342
1326
|
step.codeLocation
|
|
1343
1327
|
)}</div>`
|
|
1344
1328
|
: ""
|
|
1345
|
-
}
|
|
1346
|
-
${
|
|
1329
|
+
}${
|
|
1347
1330
|
step.errorMessage
|
|
1348
|
-
? `<div class="step-error"
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
}
|
|
1356
|
-
<button
|
|
1357
|
-
class="copy-error-btn"
|
|
1358
|
-
onclick="copyErrorToClipboard(this)"
|
|
1359
|
-
style="
|
|
1360
|
-
margin-top: 8px;
|
|
1361
|
-
padding: 4px 8px;
|
|
1362
|
-
background: #f0f0f0;
|
|
1363
|
-
border: 2px solid #ccc;
|
|
1364
|
-
border-radius: 4px;
|
|
1365
|
-
cursor: pointer;
|
|
1366
|
-
font-size: 12px;
|
|
1367
|
-
border-color: #8B0000;
|
|
1368
|
-
color: #8B0000;
|
|
1369
|
-
"
|
|
1370
|
-
onmouseover="this.style.background='#e0e0e0'"
|
|
1371
|
-
onmouseout="this.style.background='#f0f0f0'"
|
|
1372
|
-
>
|
|
1373
|
-
Copy Error Prompt
|
|
1374
|
-
</button>
|
|
1375
|
-
</div>`
|
|
1331
|
+
? `<div class="step-error">${
|
|
1332
|
+
step.stackTrace
|
|
1333
|
+
? `<div class="stack-trace">${formatPlaywrightError(
|
|
1334
|
+
step.stackTrace
|
|
1335
|
+
)}</div>`
|
|
1336
|
+
: ""
|
|
1337
|
+
}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
|
|
1376
1338
|
: ""
|
|
1377
|
-
}
|
|
1378
|
-
${
|
|
1339
|
+
}${
|
|
1379
1340
|
hasNestedSteps
|
|
1380
1341
|
? `<div class="nested-steps">${generateStepsHTML(
|
|
1381
1342
|
step.steps,
|
|
1382
1343
|
depth + 1
|
|
1383
1344
|
)}</div>`
|
|
1384
1345
|
: ""
|
|
1385
|
-
}
|
|
1386
|
-
</div>
|
|
1387
|
-
</div>`;
|
|
1346
|
+
}</div></div>`;
|
|
1388
1347
|
})
|
|
1389
1348
|
.join("");
|
|
1390
1349
|
};
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
/[&<>"']/g,
|
|
1397
|
-
(match) =>
|
|
1398
|
-
({ "&": "&", "<": "<", ">": ">", '"': '"', "'": "'" }[match] ||
|
|
1399
|
-
match)
|
|
1400
|
-
);
|
|
1401
|
-
};
|
|
1402
|
-
return `
|
|
1403
|
-
<div class="test-case" data-status="${
|
|
1404
|
-
test.status
|
|
1405
|
-
}" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
|
|
1350
|
+
return `<div class="test-case" data-status="${
|
|
1351
|
+
test.status
|
|
1352
|
+
}" data-browser="${sanitizeHTML(browser)}" data-tags="${(
|
|
1353
|
+
test.tags || []
|
|
1354
|
+
)
|
|
1406
1355
|
.join(",")
|
|
1407
1356
|
.toLowerCase()}">
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1357
|
+
<div class="test-case-header" role="button" aria-expanded="false"><div class="test-case-summary"><span class="status-badge ${getStatusClass(
|
|
1358
|
+
test.status
|
|
1359
|
+
)}">${String(
|
|
1411
1360
|
test.status
|
|
1412
|
-
).toUpperCase()}</span
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
test.tracePath
|
|
1605
|
-
? `<div class="attachments-section"><h4>Trace Files</h4><div class="attachments-grid">${(() => {
|
|
1606
|
-
// Traces
|
|
1607
|
-
const traces = Array.isArray(test.tracePath)
|
|
1608
|
-
? test.tracePath
|
|
1609
|
-
: [test.tracePath];
|
|
1610
|
-
return traces
|
|
1611
|
-
.map((trace, index) => {
|
|
1612
|
-
const traceUrl =
|
|
1613
|
-
typeof trace === "object" ? trace.url || "" : trace;
|
|
1614
|
-
const traceName =
|
|
1615
|
-
typeof trace === "object"
|
|
1616
|
-
? trace.name || `Trace ${index + 1}`
|
|
1617
|
-
: `Trace ${index + 1}`;
|
|
1618
|
-
const traceFileName = String(traceUrl).split("/").pop();
|
|
1619
|
-
return `<div class="attachment-item"><div class="trace-preview"><span class="trace-icon">📄</span><span class="trace-name">${sanitizeHTML(
|
|
1620
|
-
traceName
|
|
1621
|
-
)}</span></div><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
|
|
1622
|
-
traceUrl
|
|
1623
|
-
)}" target="_blank" download="${sanitizeHTML(
|
|
1624
|
-
traceFileName
|
|
1625
|
-
)}" class="download-trace">Download</a></div></div></div>`;
|
|
1626
|
-
})
|
|
1627
|
-
.join("");
|
|
1628
|
-
})()}</div></div>`
|
|
1629
|
-
: ""
|
|
1630
|
-
}
|
|
1631
|
-
${
|
|
1632
|
-
test.codeSnippet
|
|
1633
|
-
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${sanitizeHTML(
|
|
1634
|
-
test.codeSnippet
|
|
1635
|
-
)}</code></pre></div>`
|
|
1636
|
-
: ""
|
|
1637
|
-
}
|
|
1638
|
-
</div>
|
|
1639
|
-
</div>`;
|
|
1361
|
+
).toUpperCase()}</span><span class="test-case-title" title="${sanitizeHTML(
|
|
1362
|
+
test.name
|
|
1363
|
+
)}">${sanitizeHTML(
|
|
1364
|
+
testTitle
|
|
1365
|
+
)}</span><span class="test-case-browser">(${sanitizeHTML(
|
|
1366
|
+
browser
|
|
1367
|
+
)})</span></div><div class="test-case-meta">${
|
|
1368
|
+
test.tags && test.tags.length > 0
|
|
1369
|
+
? test.tags
|
|
1370
|
+
.map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
|
|
1371
|
+
.join(" ")
|
|
1372
|
+
: ""
|
|
1373
|
+
}<span class="test-duration">${formatDuration(
|
|
1374
|
+
test.duration
|
|
1375
|
+
)}</span></div></div>
|
|
1376
|
+
<div class="test-case-content" style="display: none;">
|
|
1377
|
+
<p><strong>Full Path:</strong> ${sanitizeHTML(
|
|
1378
|
+
test.name
|
|
1379
|
+
)}</p>
|
|
1380
|
+
<p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
|
|
1381
|
+
test.workerId
|
|
1382
|
+
)} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
|
|
1383
|
+
test.totalWorkers
|
|
1384
|
+
)}]</p>
|
|
1385
|
+
${
|
|
1386
|
+
test.errorMessage
|
|
1387
|
+
? `<div class="test-error-summary">${formatPlaywrightError(
|
|
1388
|
+
test.errorMessage
|
|
1389
|
+
)}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
|
|
1390
|
+
: ""
|
|
1391
|
+
}
|
|
1392
|
+
<h4>Steps</h4><div class="steps-list">${generateStepsHTML(
|
|
1393
|
+
test.steps
|
|
1394
|
+
)}</div>
|
|
1395
|
+
${
|
|
1396
|
+
test.stdout && test.stdout.length > 0
|
|
1397
|
+
? `<div class="console-output-section"><h4>Console Output (stdout)</h4><pre class="console-log stdout-log">${formatPlaywrightError(
|
|
1398
|
+
test.stdout.join("\\n")
|
|
1399
|
+
)}</pre></div>`
|
|
1400
|
+
: ""
|
|
1401
|
+
}
|
|
1402
|
+
${
|
|
1403
|
+
test.stderr && test.stderr.length > 0
|
|
1404
|
+
? `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log">${test.stderr
|
|
1405
|
+
.map((line) => sanitizeHTML(line))
|
|
1406
|
+
.join("\\n")}</pre></div>`
|
|
1407
|
+
: ""
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
${(() => {
|
|
1411
|
+
if (
|
|
1412
|
+
!test.screenshots ||
|
|
1413
|
+
test.screenshots.length === 0
|
|
1414
|
+
)
|
|
1415
|
+
return "";
|
|
1416
|
+
return `<div class="attachments-section"><h4>Screenshots</h4><div class="attachments-grid">${test.screenshots
|
|
1417
|
+
.map((screenshotPath, index) => {
|
|
1418
|
+
try {
|
|
1419
|
+
const imagePath = path.resolve(
|
|
1420
|
+
DEFAULT_OUTPUT_DIR,
|
|
1421
|
+
screenshotPath
|
|
1422
|
+
);
|
|
1423
|
+
if (!fsExistsSync(imagePath))
|
|
1424
|
+
return `<div class="attachment-item error">Screenshot not found: ${sanitizeHTML(
|
|
1425
|
+
screenshotPath
|
|
1426
|
+
)}</div>`;
|
|
1427
|
+
const base64ImageData =
|
|
1428
|
+
readFileSync(imagePath).toString("base64");
|
|
1429
|
+
return `<div class="attachment-item"><img src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" data-src="data:image/png;base64,${base64ImageData}" alt="Screenshot ${
|
|
1430
|
+
index + 1
|
|
1431
|
+
}" class="lazy-load-image"><div class="attachment-info"><div class="trace-actions"><a href="data:image/png;base64,${base64ImageData}" target="_blank" download="screenshot-${index}.png">Download</a></div></div></div>`;
|
|
1432
|
+
} catch (e) {
|
|
1433
|
+
return `<div class="attachment-item error">Failed to load screenshot: ${sanitizeHTML(
|
|
1434
|
+
screenshotPath
|
|
1435
|
+
)}</div>`;
|
|
1436
|
+
}
|
|
1437
|
+
})
|
|
1438
|
+
.join("")}</div></div>`;
|
|
1439
|
+
})()}
|
|
1440
|
+
|
|
1441
|
+
${(() => {
|
|
1442
|
+
if (!test.videoPath || test.videoPath.length === 0)
|
|
1443
|
+
return "";
|
|
1444
|
+
return `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
|
|
1445
|
+
.map((videoPath, index) => {
|
|
1446
|
+
try {
|
|
1447
|
+
const videoFilePath = path.resolve(
|
|
1448
|
+
DEFAULT_OUTPUT_DIR,
|
|
1449
|
+
videoPath
|
|
1450
|
+
);
|
|
1451
|
+
if (!fsExistsSync(videoFilePath))
|
|
1452
|
+
return `<div class="attachment-item error">Video not found: ${sanitizeHTML(
|
|
1453
|
+
videoPath
|
|
1454
|
+
)}</div>`;
|
|
1455
|
+
const videoBase64 =
|
|
1456
|
+
readFileSync(videoFilePath).toString(
|
|
1457
|
+
"base64"
|
|
1458
|
+
);
|
|
1459
|
+
const fileExtension = path
|
|
1460
|
+
.extname(videoPath)
|
|
1461
|
+
.slice(1)
|
|
1462
|
+
.toLowerCase();
|
|
1463
|
+
const mimeType =
|
|
1464
|
+
{
|
|
1465
|
+
mp4: "video/mp4",
|
|
1466
|
+
webm: "video/webm",
|
|
1467
|
+
ogg: "video/ogg",
|
|
1468
|
+
mov: "video/quicktime",
|
|
1469
|
+
avi: "video/x-msvideo",
|
|
1470
|
+
}[fileExtension] || "video/mp4";
|
|
1471
|
+
const videoDataUri = `data:${mimeType};base64,${videoBase64}`;
|
|
1472
|
+
return `<div class="attachment-item video-item"><video controls preload="none" class="lazy-load-video"><source data-src="${videoDataUri}" type="${mimeType}"></video><div class="attachment-info"><div class="trace-actions"><a href="${videoDataUri}" target="_blank" download="video-${index}.${fileExtension}">Download</a></div></div></div>`;
|
|
1473
|
+
} catch (e) {
|
|
1474
|
+
return `<div class="attachment-item error">Failed to load video: ${sanitizeHTML(
|
|
1475
|
+
videoPath
|
|
1476
|
+
)}</div>`;
|
|
1477
|
+
}
|
|
1478
|
+
})
|
|
1479
|
+
.join("")}</div></div>`;
|
|
1480
|
+
})()}
|
|
1481
|
+
|
|
1482
|
+
${(() => {
|
|
1483
|
+
if (!test.tracePath) return "";
|
|
1484
|
+
try {
|
|
1485
|
+
const traceFilePath = path.resolve(
|
|
1486
|
+
DEFAULT_OUTPUT_DIR,
|
|
1487
|
+
test.tracePath
|
|
1488
|
+
);
|
|
1489
|
+
if (!fsExistsSync(traceFilePath))
|
|
1490
|
+
return `<div class="attachments-section"><h4>Trace File</h4><div class="attachment-item error">Trace file not found: ${sanitizeHTML(
|
|
1491
|
+
test.tracePath
|
|
1492
|
+
)}</div></div>`;
|
|
1493
|
+
const traceBase64 =
|
|
1494
|
+
readFileSync(traceFilePath).toString("base64");
|
|
1495
|
+
const traceDataUri = `data:application/zip;base64,${traceBase64}`;
|
|
1496
|
+
return `<div class="attachments-section"><h4>Trace File</h4><div class="attachments-grid"><div class="attachment-item generic-attachment"><div class="attachment-icon">📄</div><div class="attachment-caption"><span class="attachment-name">trace.zip</span></div><div class="attachment-info"><div class="trace-actions"><a href="#" data-href="${traceDataUri}" class="lazy-load-attachment" download="trace.zip">Download Trace</a></div></div></div></div></div>`;
|
|
1497
|
+
} catch (e) {
|
|
1498
|
+
return `<div class="attachments-section"><h4>Trace File</h4><div class="attachment-item error">Failed to load trace file.</div></div>`;
|
|
1499
|
+
}
|
|
1500
|
+
})()}
|
|
1501
|
+
|
|
1502
|
+
${(() => {
|
|
1503
|
+
if (
|
|
1504
|
+
!test.attachments ||
|
|
1505
|
+
test.attachments.length === 0
|
|
1506
|
+
)
|
|
1507
|
+
return "";
|
|
1508
|
+
return `<div class="attachments-section"><h4>Other Attachments</h4><div class="attachments-grid">${test.attachments
|
|
1509
|
+
.map((attachment) => {
|
|
1510
|
+
try {
|
|
1511
|
+
const attachmentPath = path.resolve(
|
|
1512
|
+
DEFAULT_OUTPUT_DIR,
|
|
1513
|
+
attachment.path
|
|
1514
|
+
);
|
|
1515
|
+
if (!fsExistsSync(attachmentPath))
|
|
1516
|
+
return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
|
|
1517
|
+
attachment.name
|
|
1518
|
+
)}</div>`;
|
|
1519
|
+
const attachmentBase64 =
|
|
1520
|
+
readFileSync(attachmentPath).toString(
|
|
1521
|
+
"base64"
|
|
1522
|
+
);
|
|
1523
|
+
const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
|
|
1524
|
+
return `<div class="attachment-item generic-attachment"><div class="attachment-icon">${getAttachmentIcon(
|
|
1525
|
+
attachment.contentType
|
|
1526
|
+
)}</div><div class="attachment-caption"><span class="attachment-name" title="${sanitizeHTML(
|
|
1527
|
+
attachment.name
|
|
1528
|
+
)}">${sanitizeHTML(
|
|
1529
|
+
attachment.name
|
|
1530
|
+
)}</span><span class="attachment-type">${sanitizeHTML(
|
|
1531
|
+
attachment.contentType
|
|
1532
|
+
)}</span></div><div class="attachment-info"><div class="trace-actions"><a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
|
|
1533
|
+
attachment.name
|
|
1534
|
+
)}">Download</a></div></div></div>`;
|
|
1535
|
+
} catch (e) {
|
|
1536
|
+
return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(
|
|
1537
|
+
attachment.name
|
|
1538
|
+
)}</div>`;
|
|
1539
|
+
}
|
|
1540
|
+
})
|
|
1541
|
+
.join("")}</div></div>`;
|
|
1542
|
+
})()}
|
|
1543
|
+
|
|
1544
|
+
${
|
|
1545
|
+
test.codeSnippet
|
|
1546
|
+
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${sanitizeHTML(
|
|
1547
|
+
test.codeSnippet
|
|
1548
|
+
)}</code></pre></div>`
|
|
1549
|
+
: ""
|
|
1550
|
+
}
|
|
1551
|
+
</div>
|
|
1552
|
+
</div>`;
|
|
1640
1553
|
})
|
|
1641
1554
|
.join("");
|
|
1642
1555
|
}
|
|
@@ -1668,7 +1581,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1668
1581
|
.highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
|
|
1669
1582
|
.highcharts-axis-title { fill: var(--text-color) !important; }
|
|
1670
1583
|
.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; }
|
|
1671
|
-
|
|
1672
1584
|
body { font-family: var(--font-family); margin: 0; background-color: var(--background-color); color: var(--text-color); line-height: 1.65; font-size: 16px; }
|
|
1673
1585
|
.container { padding: 30px; border-radius: var(--border-radius); box-shadow: var(--box-shadow); background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec); }
|
|
1674
1586
|
.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; }
|
|
@@ -1763,7 +1675,8 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1763
1675
|
.attachments-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 22px; }
|
|
1764
1676
|
.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; }
|
|
1765
1677
|
.attachment-item:hover { transform: translateY(-4px); box-shadow: var(--box-shadow); }
|
|
1766
|
-
.attachment-item img { width: 100%; height: 180px; object-fit: cover; display: block; border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease; }
|
|
1678
|
+
.attachment-item img, .attachment-item video { width: 100%; height: 180px; object-fit: cover; display: block; background-color: #eee; border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease; }
|
|
1679
|
+
.attachment-info { padding: 12px; margin-top: auto; background-color: #fafafa;}
|
|
1767
1680
|
.attachment-item a:hover img { opacity: 0.85; }
|
|
1768
1681
|
.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); }
|
|
1769
1682
|
.video-item a, .trace-item a { display: block; margin-bottom: 8px; color: var(--primary-color); text-decoration: none; font-weight: 500; }
|
|
@@ -1807,6 +1720,12 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1807
1720
|
@media (max-width: 992px) { .dashboard-bottom-row { grid-template-columns: 1fr; } .pie-chart-wrapper div[id^="pieChart-"] { max-width: 350px; margin: 0 auto; } .filters input { min-width: 180px; } .filters select { min-width: 150px; } }
|
|
1808
1721
|
@media (max-width: 768px) { body { font-size: 15px; } .container { margin: 10px; padding: 20px; } .header { flex-direction: column; align-items: flex-start; gap: 15px; } .header h1 { font-size: 1.6em; } .run-info { text-align: left; font-size:0.9em; } .tabs { margin-bottom: 25px;} .tab-button { padding: 12px 20px; font-size: 1.05em;} .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;} .summary-card .value {font-size: 2em;} .summary-card h3 {font-size: 0.95em;} .filters { flex-direction: column; padding: 18px; gap: 12px;} .filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;} .test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; } .test-case-summary {gap: 10px;} .test-case-title {font-size: 1.05em;} .test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;} .attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;} .test-history-grid {grid-template-columns: 1fr;} .pie-chart-wrapper {min-height: auto;} }
|
|
1809
1722
|
@media (max-width: 480px) { body {font-size: 14px;} .container {padding: 15px;} .header h1 {font-size: 1.4em;} #report-logo { height: 35px; width: 35px; } .tab-button {padding: 10px 15px; font-size: 1em;} .summary-card .value {font-size: 1.8em;} .attachments-grid {grid-template-columns: 1fr;} .step-item {padding-left: calc(var(--depth, 0) * 18px);} .test-case-content, .step-details {padding: 15px;} .trend-charts-row {gap: 20px;} .trend-chart {padding: 20px;} }
|
|
1723
|
+
.trace-actions a { text-decoration: none; color: var(--primary-color); font-weight: 500; font-size: 0.9em; }
|
|
1724
|
+
.generic-attachment { text-align: center; padding: 1rem; justify-content: center; align-items: center; }
|
|
1725
|
+
.attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
|
|
1726
|
+
.attachment-caption { display: flex; flex-direction: column; align-items: center; justify-content: center; flex-grow: 1; }
|
|
1727
|
+
.attachment-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
|
1728
|
+
.attachment-type { font-size: 0.8rem; color: var(--text-color-secondary); }
|
|
1810
1729
|
</style>
|
|
1811
1730
|
</head>
|
|
1812
1731
|
<body>
|
|
@@ -2035,84 +1954,115 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2035
1954
|
if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
|
|
2036
1955
|
if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
|
|
2037
1956
|
// --- Intersection Observer for Lazy Loading ---
|
|
2038
|
-
const lazyLoadElements = document.querySelectorAll('.lazy-load-chart, .lazy-load-iframe');
|
|
1957
|
+
const lazyLoadElements = document.querySelectorAll('.lazy-load-chart, .lazy-load-iframe, .lazy-load-image, .lazy-load-video, .lazy-load-attachment');
|
|
2039
1958
|
if ('IntersectionObserver' in window) {
|
|
2040
1959
|
let lazyObserver = new IntersectionObserver((entries, observer) => {
|
|
2041
1960
|
entries.forEach(entry => {
|
|
2042
1961
|
if (entry.isIntersecting) {
|
|
2043
1962
|
const element = entry.target;
|
|
2044
|
-
if (element.classList.contains('lazy-load-
|
|
1963
|
+
if (element.classList.contains('lazy-load-image')) {
|
|
1964
|
+
if (element.dataset.src) {
|
|
1965
|
+
element.src = element.dataset.src;
|
|
1966
|
+
element.removeAttribute('data-src');
|
|
1967
|
+
}
|
|
1968
|
+
} else if (element.classList.contains('lazy-load-video')) {
|
|
1969
|
+
const source = element.querySelector('source');
|
|
1970
|
+
if (source && source.dataset.src) {
|
|
1971
|
+
source.src = source.dataset.src;
|
|
1972
|
+
source.removeAttribute('data-src');
|
|
1973
|
+
element.load();
|
|
1974
|
+
}
|
|
1975
|
+
} else if (element.classList.contains('lazy-load-attachment')) {
|
|
1976
|
+
if (element.dataset.href) {
|
|
1977
|
+
element.href = element.dataset.href;
|
|
1978
|
+
element.removeAttribute('data-href');
|
|
1979
|
+
}
|
|
1980
|
+
} else if (element.classList.contains('lazy-load-iframe')) {
|
|
2045
1981
|
if (element.dataset.src) {
|
|
2046
1982
|
element.src = element.dataset.src;
|
|
2047
|
-
element.removeAttribute('data-src');
|
|
2048
|
-
console.log('Lazy loaded iframe:', element.title || 'Untitled Iframe');
|
|
1983
|
+
element.removeAttribute('data-src');
|
|
2049
1984
|
}
|
|
2050
1985
|
} else if (element.classList.contains('lazy-load-chart')) {
|
|
2051
1986
|
const renderFunctionName = element.dataset.renderFunctionName;
|
|
2052
1987
|
if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
|
|
2053
|
-
|
|
2054
|
-
console.log('Lazy loading chart with function:', renderFunctionName);
|
|
2055
|
-
window[renderFunctionName](); // Call the render function
|
|
2056
|
-
} catch (e) {
|
|
2057
|
-
console.error(\`Error lazy-loading chart \${element.id} using \${renderFunctionName}:\`, e);
|
|
2058
|
-
element.innerHTML = '<div class="no-data-chart">Error lazy-loading chart.</div>';
|
|
2059
|
-
}
|
|
2060
|
-
} else {
|
|
2061
|
-
console.warn(\`Render function \${renderFunctionName} not found or not a function for chart:\`, element.id);
|
|
1988
|
+
window[renderFunctionName]();
|
|
2062
1989
|
}
|
|
2063
1990
|
}
|
|
2064
|
-
observer.unobserve(element);
|
|
1991
|
+
observer.unobserve(element);
|
|
2065
1992
|
}
|
|
2066
1993
|
});
|
|
2067
|
-
}, {
|
|
2068
|
-
|
|
2069
|
-
});
|
|
2070
|
-
|
|
2071
|
-
lazyLoadElements.forEach(el => {
|
|
2072
|
-
lazyObserver.observe(el);
|
|
2073
|
-
});
|
|
1994
|
+
}, { rootMargin: "0px 0px 200px 0px" });
|
|
1995
|
+
lazyLoadElements.forEach(el => lazyObserver.observe(el));
|
|
2074
1996
|
} else { // Fallback for browsers without IntersectionObserver
|
|
2075
|
-
console.warn("IntersectionObserver not supported. Loading all items immediately.");
|
|
2076
1997
|
lazyLoadElements.forEach(element => {
|
|
2077
|
-
if (element.classList.contains('lazy-load-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
} else if (element.classList.contains('lazy-load-chart')) {
|
|
2083
|
-
const renderFunctionName = element.dataset.renderFunctionName;
|
|
2084
|
-
if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
|
|
2085
|
-
try {
|
|
2086
|
-
window[renderFunctionName]();
|
|
2087
|
-
} catch (e) {
|
|
2088
|
-
console.error(\`Error loading chart (fallback) \${element.id} using \${renderFunctionName}:\`, e);
|
|
2089
|
-
element.innerHTML = '<div class="no-data-chart">Error loading chart (fallback).</div>';
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
1998
|
+
if (element.classList.contains('lazy-load-image') && element.dataset.src) element.src = element.dataset.src;
|
|
1999
|
+
else if (element.classList.contains('lazy-load-video')) { const source = element.querySelector('source'); if (source && source.dataset.src) { source.src = source.dataset.src; element.load(); } }
|
|
2000
|
+
else if (element.classList.contains('lazy-load-attachment') && element.dataset.href) element.href = element.dataset.href;
|
|
2001
|
+
else if (element.classList.contains('lazy-load-iframe') && element.dataset.src) element.src = element.dataset.src;
|
|
2002
|
+
else if (element.classList.contains('lazy-load-chart')) { const renderFn = element.dataset.renderFunctionName; if(renderFn && window[renderFn]) window[renderFn](); }
|
|
2093
2003
|
});
|
|
2094
2004
|
}
|
|
2095
2005
|
}
|
|
2096
2006
|
document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
|
|
2097
2007
|
|
|
2098
2008
|
function copyErrorToClipboard(button) {
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2009
|
+
// 1. Find the main error container, which should always be present.
|
|
2010
|
+
const errorContainer = button.closest('.step-error');
|
|
2011
|
+
if (!errorContainer) {
|
|
2012
|
+
console.error("Could not find '.step-error' container. The report's HTML structure might have changed.");
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
let errorText;
|
|
2017
|
+
|
|
2018
|
+
// 2. First, try to find the preferred .stack-trace element (the "happy path").
|
|
2019
|
+
const stackTraceElement = errorContainer.querySelector('.stack-trace');
|
|
2020
|
+
|
|
2021
|
+
if (stackTraceElement) {
|
|
2022
|
+
// If it exists, use its text content. This handles standard assertion errors.
|
|
2023
|
+
errorText = stackTraceElement.textContent;
|
|
2024
|
+
} else {
|
|
2025
|
+
// 3. FALLBACK: If .stack-trace doesn't exist, this is likely an unstructured error.
|
|
2026
|
+
// We clone the container to avoid manipulating the live DOM or copying the button's own text.
|
|
2027
|
+
const clonedContainer = errorContainer.cloneNode(true);
|
|
2028
|
+
|
|
2029
|
+
// Remove the button from our clone before extracting the text.
|
|
2030
|
+
const buttonInClone = clonedContainer.querySelector('button');
|
|
2031
|
+
if (buttonInClone) {
|
|
2032
|
+
buttonInClone.remove();
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// Use the text content of the cleaned container as the fallback.
|
|
2036
|
+
errorText = clonedContainer.textContent;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// 4. Proceed with the clipboard logic, ensuring text is not null and is trimmed.
|
|
2040
|
+
if (!errorText) {
|
|
2041
|
+
console.error('Could not extract error text.');
|
|
2042
|
+
button.textContent = 'Nothing to copy';
|
|
2043
|
+
setTimeout(() => { button.textContent = 'Copy Error'; }, 2000);
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
const textarea = document.createElement('textarea');
|
|
2048
|
+
textarea.value = errorText.trim(); // Trim whitespace for a cleaner copy.
|
|
2049
|
+
textarea.style.position = 'fixed'; // Prevent screen scroll
|
|
2050
|
+
textarea.style.top = '-9999px';
|
|
2051
|
+
document.body.appendChild(textarea);
|
|
2052
|
+
textarea.select();
|
|
2053
|
+
|
|
2054
|
+
try {
|
|
2055
|
+
const successful = document.execCommand('copy');
|
|
2056
|
+
const originalText = button.textContent;
|
|
2057
|
+
button.textContent = successful ? 'Copied!' : 'Failed';
|
|
2058
|
+
setTimeout(() => {
|
|
2059
|
+
button.textContent = originalText;
|
|
2060
|
+
}, 2000);
|
|
2061
|
+
} catch (err) {
|
|
2062
|
+
console.error('Failed to copy: ', err);
|
|
2063
|
+
button.textContent = 'Failed';
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2116
2066
|
document.body.removeChild(textarea);
|
|
2117
2067
|
}
|
|
2118
2068
|
</script>
|