@arghajit/playwright-pulse-report 0.2.10 → 0.3.0
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 +36 -0
- package/dist/reporter/playwright-pulse-reporter.js +5 -4
- package/dist/types/index.d.ts +9 -0
- package/package.json +2 -2
- package/scripts/config-reader.mjs +132 -0
- package/scripts/generate-email-report.mjs +21 -1
- package/scripts/generate-report.mjs +525 -274
- package/scripts/generate-static-report.mjs +227 -47
- package/scripts/generate-trend.mjs +11 -1
- package/scripts/merge-pulse-report.js +46 -14
- package/scripts/sendReport.mjs +36 -21
|
@@ -5,6 +5,7 @@ import { readFileSync, existsSync as fsExistsSync } from "fs";
|
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { fork } from "child_process";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
|
+
import { getOutputDir } from "./config-reader.mjs";
|
|
8
9
|
|
|
9
10
|
// Use dynamic import for chalk as it's ESM only
|
|
10
11
|
let chalk;
|
|
@@ -615,8 +616,9 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
615
616
|
chart: {
|
|
616
617
|
type: 'pie',
|
|
617
618
|
width: ${chartWidth},
|
|
618
|
-
height: ${
|
|
619
|
-
|
|
619
|
+
height: ${
|
|
620
|
+
chartHeight - 40
|
|
621
|
+
}, // Adjusted height to make space for legend if chartHeight is for the whole wrapper
|
|
620
622
|
backgroundColor: 'transparent',
|
|
621
623
|
plotShadow: false,
|
|
622
624
|
spacingBottom: 40 // Ensure space for legend
|
|
@@ -668,8 +670,9 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
668
670
|
return `
|
|
669
671
|
<div class="pie-chart-wrapper" style="align-items: center; max-height: 450px">
|
|
670
672
|
<div style="display: flex; align-items: start; width: 100%;"><h3>Test Distribution</h3></div>
|
|
671
|
-
<div id="${chartId}" style="width: ${chartWidth}px; height: ${
|
|
672
|
-
|
|
673
|
+
<div id="${chartId}" style="width: ${chartWidth}px; height: ${
|
|
674
|
+
chartHeight - 40
|
|
675
|
+
}px;"></div>
|
|
673
676
|
<script>
|
|
674
677
|
document.addEventListener('DOMContentLoaded', function() {
|
|
675
678
|
if (typeof Highcharts !== 'undefined') {
|
|
@@ -897,14 +900,15 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
897
900
|
<span class="env-detail-value">
|
|
898
901
|
<div class="env-cpu-cores">
|
|
899
902
|
${Array.from(
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
903
|
+
{ length: Math.max(0, environment.cpu.cores || 0) },
|
|
904
|
+
(_, i) =>
|
|
905
|
+
`<div class="env-core-indicator ${
|
|
906
|
+
i >=
|
|
907
|
+
(environment.cpu.cores >= 8 ? 8 : environment.cpu.cores)
|
|
908
|
+
? "inactive"
|
|
909
|
+
: ""
|
|
910
|
+
}" title="Core ${i + 1}"></div>`
|
|
911
|
+
).join("")}
|
|
908
912
|
<span>${environment.cpu.cores || "N/A"} cores</span>
|
|
909
913
|
</div>
|
|
910
914
|
</span>
|
|
@@ -924,20 +928,23 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
924
928
|
<div class="env-card-content">
|
|
925
929
|
<div class="env-detail-row">
|
|
926
930
|
<span class="env-detail-label">OS Type</span>
|
|
927
|
-
<span class="env-detail-value">${
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
+
<span class="env-detail-value">${
|
|
932
|
+
environment.os.split(" ")[0] === "darwin"
|
|
933
|
+
? "darwin (macOS)"
|
|
934
|
+
: environment.os.split(" ")[0] || "Unknown"
|
|
935
|
+
}</span>
|
|
931
936
|
</div>
|
|
932
937
|
<div class="env-detail-row">
|
|
933
938
|
<span class="env-detail-label">OS Version</span>
|
|
934
|
-
<span class="env-detail-value">${
|
|
935
|
-
|
|
939
|
+
<span class="env-detail-value">${
|
|
940
|
+
environment.os.split(" ")[1] || "N/A"
|
|
941
|
+
}</span>
|
|
936
942
|
</div>
|
|
937
943
|
<div class="env-detail-row">
|
|
938
944
|
<span class="env-detail-label">Hostname</span>
|
|
939
|
-
<span class="env-detail-value" title="${environment.host}">${
|
|
940
|
-
|
|
945
|
+
<span class="env-detail-value" title="${environment.host}">${
|
|
946
|
+
environment.host
|
|
947
|
+
}</span>
|
|
941
948
|
</div>
|
|
942
949
|
</div>
|
|
943
950
|
</div>
|
|
@@ -958,10 +965,11 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
958
965
|
</div>
|
|
959
966
|
<div class="env-detail-row">
|
|
960
967
|
<span class="env-detail-label">Working Dir</span>
|
|
961
|
-
<span class="env-detail-value" title="${environment.cwd}">${
|
|
968
|
+
<span class="env-detail-value" title="${environment.cwd}">${
|
|
969
|
+
environment.cwd.length > 25
|
|
962
970
|
? "..." + environment.cwd.slice(-22)
|
|
963
971
|
: environment.cwd
|
|
964
|
-
|
|
972
|
+
}</span>
|
|
965
973
|
</div>
|
|
966
974
|
</div>
|
|
967
975
|
</div>
|
|
@@ -975,30 +983,33 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
975
983
|
<div class="env-detail-row">
|
|
976
984
|
<span class="env-detail-label">Platform Arch</span>
|
|
977
985
|
<span class="env-detail-value">
|
|
978
|
-
<span class="env-chip ${
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
986
|
+
<span class="env-chip ${
|
|
987
|
+
environment.os.includes("darwin") &&
|
|
988
|
+
environment.cpu.model.toLowerCase().includes("apple")
|
|
989
|
+
? "env-chip-success"
|
|
990
|
+
: "env-chip-warning"
|
|
991
|
+
}">
|
|
992
|
+
${
|
|
993
|
+
environment.os.includes("darwin") &&
|
|
994
|
+
environment.cpu.model.toLowerCase().includes("apple")
|
|
995
|
+
? "Apple Silicon"
|
|
996
|
+
: environment.cpu.model.toLowerCase().includes("arm") ||
|
|
997
|
+
environment.cpu.model.toLowerCase().includes("aarch64")
|
|
998
|
+
? "ARM-based"
|
|
999
|
+
: "x86/Other"
|
|
1000
|
+
}
|
|
991
1001
|
</span>
|
|
992
1002
|
</span>
|
|
993
1003
|
</div>
|
|
994
1004
|
<div class="env-detail-row">
|
|
995
1005
|
<span class="env-detail-label">Memory per Core</span>
|
|
996
|
-
<span class="env-detail-value">${
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1006
|
+
<span class="env-detail-value">${
|
|
1007
|
+
environment.cpu.cores > 0
|
|
1008
|
+
? (
|
|
1009
|
+
parseFloat(environment.memory) / environment.cpu.cores
|
|
1010
|
+
).toFixed(2) + " GB"
|
|
1011
|
+
: "N/A"
|
|
1012
|
+
}</span>
|
|
1002
1013
|
</div>
|
|
1003
1014
|
<div class="env-detail-row">
|
|
1004
1015
|
<span class="env-detail-label">Run Context</span>
|
|
@@ -1330,19 +1341,19 @@ function generateTestHistoryContent(trendData) {
|
|
|
1330
1341
|
|
|
1331
1342
|
<div class="test-history-grid">
|
|
1332
1343
|
${testHistory
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1344
|
+
.map((test) => {
|
|
1345
|
+
const latestRun =
|
|
1346
|
+
test.history.length > 0
|
|
1347
|
+
? test.history[test.history.length - 1]
|
|
1348
|
+
: { status: "unknown" };
|
|
1349
|
+
return `
|
|
1339
1350
|
<div class="test-history-card" data-test-name="${sanitizeHTML(
|
|
1340
|
-
|
|
1341
|
-
|
|
1351
|
+
test.testTitle.toLowerCase()
|
|
1352
|
+
)}" data-latest-status="${latestRun.status}">
|
|
1342
1353
|
<div class="test-history-header">
|
|
1343
1354
|
<p title="${sanitizeHTML(test.testTitle)}">${capitalize(
|
|
1344
|
-
|
|
1345
|
-
|
|
1355
|
+
sanitizeHTML(test.testTitle)
|
|
1356
|
+
)}</p>
|
|
1346
1357
|
<span class="status-badge ${getStatusClass(latestRun.status)}">
|
|
1347
1358
|
${String(latestRun.status).toUpperCase()}
|
|
1348
1359
|
</span>
|
|
@@ -1357,27 +1368,27 @@ function generateTestHistoryContent(trendData) {
|
|
|
1357
1368
|
<thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Date</th></tr></thead>
|
|
1358
1369
|
<tbody>
|
|
1359
1370
|
${test.history
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1371
|
+
.slice()
|
|
1372
|
+
.reverse()
|
|
1373
|
+
.map(
|
|
1374
|
+
(run) => `
|
|
1364
1375
|
<tr>
|
|
1365
1376
|
<td>${run.runId}</td>
|
|
1366
1377
|
<td><span class="status-badge-small ${getStatusClass(
|
|
1367
|
-
|
|
1368
|
-
|
|
1378
|
+
run.status
|
|
1379
|
+
)}">${String(run.status).toUpperCase()}</span></td>
|
|
1369
1380
|
<td>${formatDuration(run.duration)}</td>
|
|
1370
1381
|
<td>${formatDate(run.timestamp)}</td>
|
|
1371
1382
|
</tr>`
|
|
1372
|
-
|
|
1373
|
-
|
|
1383
|
+
)
|
|
1384
|
+
.join("")}
|
|
1374
1385
|
</tbody>
|
|
1375
1386
|
</table>
|
|
1376
1387
|
</div>
|
|
1377
1388
|
</details>
|
|
1378
1389
|
</div>`;
|
|
1379
|
-
|
|
1380
|
-
|
|
1390
|
+
})
|
|
1391
|
+
.join("")}
|
|
1381
1392
|
</div>
|
|
1382
1393
|
</div>
|
|
1383
1394
|
`;
|
|
@@ -1462,11 +1473,12 @@ function generateSuitesWidget(suitesData) {
|
|
|
1462
1473
|
<div class="suites-widget">
|
|
1463
1474
|
<div class="suites-header">
|
|
1464
1475
|
<h2>Test Suites</h2>
|
|
1465
|
-
<span class="summary-badge">${
|
|
1476
|
+
<span class="summary-badge">${
|
|
1477
|
+
suitesData.length
|
|
1466
1478
|
} suites • ${suitesData.reduce(
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1479
|
+
(sum, suite) => sum + suite.count,
|
|
1480
|
+
0
|
|
1481
|
+
)} tests</span>
|
|
1470
1482
|
</div>
|
|
1471
1483
|
<div class="suites-grid">
|
|
1472
1484
|
${suitesData
|
|
@@ -1479,24 +1491,28 @@ function generateSuitesWidget(suitesData) {
|
|
|
1479
1491
|
)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
|
|
1480
1492
|
</div>
|
|
1481
1493
|
<div>🖥️ <span class="browser-tag">${sanitizeHTML(
|
|
1482
|
-
|
|
1483
|
-
|
|
1494
|
+
suite.browser
|
|
1495
|
+
)}</span></div>
|
|
1484
1496
|
<div class="suite-card-body">
|
|
1485
|
-
<span class="test-count">${suite.count} test${
|
|
1486
|
-
|
|
1497
|
+
<span class="test-count">${suite.count} test${
|
|
1498
|
+
suite.count !== 1 ? "s" : ""
|
|
1499
|
+
}</span>
|
|
1487
1500
|
<div class="suite-stats">
|
|
1488
|
-
${
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1501
|
+
${
|
|
1502
|
+
suite.passed > 0
|
|
1503
|
+
? `<span class="stat-passed" title="Passed"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/></svg> ${suite.passed}</span>`
|
|
1504
|
+
: ""
|
|
1505
|
+
}
|
|
1506
|
+
${
|
|
1507
|
+
suite.failed > 0
|
|
1508
|
+
? `<span class="stat-failed" title="Failed"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/></svg> ${suite.failed}</span>`
|
|
1509
|
+
: ""
|
|
1510
|
+
}
|
|
1511
|
+
${
|
|
1512
|
+
suite.skipped > 0
|
|
1513
|
+
? `<span class="stat-skipped" title="Skipped"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-exclamation-triangle-fill" viewBox="0 0 16 16"><path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/></svg> ${suite.skipped}</span>`
|
|
1514
|
+
: ""
|
|
1515
|
+
}
|
|
1500
1516
|
</div>
|
|
1501
1517
|
</div>
|
|
1502
1518
|
</div>`
|
|
@@ -1519,7 +1535,9 @@ function getAttachmentIcon(contentType) {
|
|
|
1519
1535
|
return "📎";
|
|
1520
1536
|
}
|
|
1521
1537
|
function generateAIFailureAnalyzerTab(results) {
|
|
1522
|
-
const failedTests = (results || []).filter(
|
|
1538
|
+
const failedTests = (results || []).filter(
|
|
1539
|
+
(test) => test.status === "failed"
|
|
1540
|
+
);
|
|
1523
1541
|
|
|
1524
1542
|
if (failedTests.length === 0) {
|
|
1525
1543
|
return `
|
|
@@ -1529,7 +1547,7 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1529
1547
|
}
|
|
1530
1548
|
|
|
1531
1549
|
// btoa is not available in Node.js environment, so we define a simple polyfill for it.
|
|
1532
|
-
const btoa = (str) => Buffer.from(str).toString(
|
|
1550
|
+
const btoa = (str) => Buffer.from(str).toString("base64");
|
|
1533
1551
|
|
|
1534
1552
|
return `
|
|
1535
1553
|
<h2 class="tab-main-title">AI Failure Analysis</h2>
|
|
@@ -1539,41 +1557,61 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1539
1557
|
<span class="stat-label">Failed Tests</span>
|
|
1540
1558
|
</div>
|
|
1541
1559
|
<div class="stat-item">
|
|
1542
|
-
<span class="stat-number">${
|
|
1560
|
+
<span class="stat-number">${
|
|
1561
|
+
new Set(failedTests.map((t) => t.browser)).size
|
|
1562
|
+
}</span>
|
|
1543
1563
|
<span class="stat-label">Browsers</span>
|
|
1544
1564
|
</div>
|
|
1545
1565
|
<div class="stat-item">
|
|
1546
|
-
<span class="stat-number">${
|
|
1566
|
+
<span class="stat-number">${Math.round(
|
|
1567
|
+
failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) /
|
|
1568
|
+
1000
|
|
1569
|
+
)}s</span>
|
|
1547
1570
|
<span class="stat-label">Total Duration</span>
|
|
1548
1571
|
</div>
|
|
1549
1572
|
</div>
|
|
1550
1573
|
<p class="ai-analyzer-description">
|
|
1551
|
-
Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for
|
|
1574
|
+
Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for instant analysis or use Copy AI Prompt to analyze with your preferred AI tool.
|
|
1552
1575
|
</p>
|
|
1553
1576
|
|
|
1554
1577
|
<div class="compact-failure-list">
|
|
1555
|
-
${failedTests
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1578
|
+
${failedTests
|
|
1579
|
+
.map((test) => {
|
|
1580
|
+
const testTitle = test.name.split(" > ").pop() || "Unnamed Test";
|
|
1581
|
+
const testJson = btoa(JSON.stringify(test)); // Base64 encode the test object
|
|
1582
|
+
const truncatedError =
|
|
1583
|
+
(test.errorMessage || "No error message").slice(0, 150) +
|
|
1584
|
+
(test.errorMessage && test.errorMessage.length > 150 ? "..." : "");
|
|
1585
|
+
|
|
1586
|
+
return `
|
|
1562
1587
|
<div class="compact-failure-item">
|
|
1563
1588
|
<div class="failure-header">
|
|
1564
1589
|
<div class="failure-main-info">
|
|
1565
|
-
<h3 class="failure-title" title="${sanitizeHTML(
|
|
1590
|
+
<h3 class="failure-title" title="${sanitizeHTML(
|
|
1591
|
+
test.name
|
|
1592
|
+
)}">${sanitizeHTML(testTitle)}</h3>
|
|
1566
1593
|
<div class="failure-meta">
|
|
1567
|
-
<span class="browser-indicator">${sanitizeHTML(
|
|
1568
|
-
|
|
1594
|
+
<span class="browser-indicator">${sanitizeHTML(
|
|
1595
|
+
test.browser || "unknown"
|
|
1596
|
+
)}</span>
|
|
1597
|
+
<span class="duration-indicator">${formatDuration(
|
|
1598
|
+
test.duration
|
|
1599
|
+
)}</span>
|
|
1569
1600
|
</div>
|
|
1570
1601
|
</div>
|
|
1571
|
-
<
|
|
1572
|
-
<
|
|
1573
|
-
|
|
1602
|
+
<div class="ai-buttons-group">
|
|
1603
|
+
<button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
|
|
1604
|
+
<span class="ai-text">AI Fix</span>
|
|
1605
|
+
</button>
|
|
1606
|
+
<button class="copy-prompt-btn" onclick="copyAIPrompt(this)" data-test-json="${testJson}" title="Copy AI Prompt">
|
|
1607
|
+
<span class="copy-prompt-text">Copy AI Prompt</span>
|
|
1608
|
+
</button>
|
|
1609
|
+
</div>
|
|
1574
1610
|
</div>
|
|
1575
1611
|
<div class="failure-error-preview">
|
|
1576
|
-
<div class="error-snippet">${formatPlaywrightError(
|
|
1612
|
+
<div class="error-snippet">${formatPlaywrightError(
|
|
1613
|
+
truncatedError
|
|
1614
|
+
)}</div>
|
|
1577
1615
|
<button class="expand-error-btn" onclick="toggleErrorDetails(this)">
|
|
1578
1616
|
<span class="expand-text">Show Full Error</span>
|
|
1579
1617
|
<span class="expand-icon">▼</span>
|
|
@@ -1581,12 +1619,15 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1581
1619
|
</div>
|
|
1582
1620
|
<div class="full-error-details" style="display: none;">
|
|
1583
1621
|
<div class="full-error-content">
|
|
1584
|
-
${formatPlaywrightError(
|
|
1622
|
+
${formatPlaywrightError(
|
|
1623
|
+
test.errorMessage || "No detailed error message available"
|
|
1624
|
+
)}
|
|
1585
1625
|
</div>
|
|
1586
1626
|
</div>
|
|
1587
1627
|
</div>
|
|
1588
|
-
|
|
1589
|
-
|
|
1628
|
+
`;
|
|
1629
|
+
})
|
|
1630
|
+
.join("")}
|
|
1590
1631
|
</div>
|
|
1591
1632
|
|
|
1592
1633
|
<!-- AI Fix Modal -->
|
|
@@ -1618,7 +1659,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1618
1659
|
const fixPath = (p) => {
|
|
1619
1660
|
if (!p) return "";
|
|
1620
1661
|
// This regex handles both forward slashes and backslashes
|
|
1621
|
-
return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`),
|
|
1662
|
+
return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`), "");
|
|
1622
1663
|
};
|
|
1623
1664
|
|
|
1624
1665
|
const totalTestsOr1 = runSummary.totalTests || 1;
|
|
@@ -1663,20 +1704,23 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1663
1704
|
)}</span>
|
|
1664
1705
|
</div>
|
|
1665
1706
|
<div class="step-details" style="display: none;">
|
|
1666
|
-
${
|
|
1707
|
+
${
|
|
1708
|
+
step.codeLocation
|
|
1667
1709
|
? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
|
|
1668
|
-
|
|
1669
|
-
|
|
1710
|
+
step.codeLocation
|
|
1711
|
+
)}</div>`
|
|
1670
1712
|
: ""
|
|
1671
|
-
|
|
1672
|
-
${
|
|
1713
|
+
}
|
|
1714
|
+
${
|
|
1715
|
+
step.errorMessage
|
|
1673
1716
|
? `<div class="test-error-summary">
|
|
1674
|
-
${
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1717
|
+
${
|
|
1718
|
+
step.stackTrace
|
|
1719
|
+
? `<div class="stack-trace">${formatPlaywrightError(
|
|
1720
|
+
step.stackTrace
|
|
1721
|
+
)}</div>`
|
|
1722
|
+
: ""
|
|
1723
|
+
}
|
|
1680
1724
|
<button
|
|
1681
1725
|
class="copy-error-btn"
|
|
1682
1726
|
onclick="copyErrorToClipboard(this)"
|
|
@@ -1698,14 +1742,15 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1698
1742
|
</button>
|
|
1699
1743
|
</div>`
|
|
1700
1744
|
: ""
|
|
1701
|
-
|
|
1702
|
-
${
|
|
1745
|
+
}
|
|
1746
|
+
${
|
|
1747
|
+
hasNestedSteps
|
|
1703
1748
|
? `<div class="nested-steps">${generateStepsHTML(
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1749
|
+
step.steps,
|
|
1750
|
+
depth + 1
|
|
1751
|
+
)}</div>`
|
|
1707
1752
|
: ""
|
|
1708
|
-
|
|
1753
|
+
}
|
|
1709
1754
|
</div>
|
|
1710
1755
|
</div>`;
|
|
1711
1756
|
})
|
|
@@ -1713,41 +1758,86 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1713
1758
|
};
|
|
1714
1759
|
|
|
1715
1760
|
return `
|
|
1716
|
-
<div class="test-case" data-status="${
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1761
|
+
<div class="test-case" data-status="${
|
|
1762
|
+
test.status
|
|
1763
|
+
}" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
|
|
1764
|
+
.join(",")
|
|
1765
|
+
.toLowerCase()}">
|
|
1720
1766
|
<div class="test-case-header" role="button" aria-expanded="false">
|
|
1721
1767
|
<div class="test-case-summary">
|
|
1722
1768
|
<span class="status-badge ${getStatusClass(test.status)}">${String(
|
|
1723
|
-
|
|
1724
|
-
|
|
1769
|
+
test.status
|
|
1770
|
+
).toUpperCase()}</span>
|
|
1725
1771
|
<span class="test-case-title" title="${sanitizeHTML(
|
|
1726
1772
|
test.name
|
|
1727
1773
|
)}">${sanitizeHTML(testTitle)}</span>
|
|
1728
1774
|
<span class="test-case-browser">(${sanitizeHTML(browser)})</span>
|
|
1729
1775
|
</div>
|
|
1730
1776
|
<div class="test-case-meta">
|
|
1731
|
-
${
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1777
|
+
${
|
|
1778
|
+
test.tags && test.tags.length > 0
|
|
1779
|
+
? test.tags
|
|
1780
|
+
.map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
|
|
1781
|
+
.join(" ")
|
|
1782
|
+
: ""
|
|
1783
|
+
}
|
|
1737
1784
|
<span class="test-duration">${formatDuration(test.duration)}</span>
|
|
1738
1785
|
</div>
|
|
1739
1786
|
</div>
|
|
1740
1787
|
<div class="test-case-content" style="display: none;">
|
|
1741
1788
|
<p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
|
|
1789
|
+
${
|
|
1790
|
+
test.annotations && test.annotations.length > 0
|
|
1791
|
+
? `<div class="annotations-section" style="margin: 12px 0; padding: 12px; background-color: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.3); border-left: 4px solid #8b5cf6; border-radius: 4px;">
|
|
1792
|
+
<h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
|
|
1793
|
+
${test.annotations
|
|
1794
|
+
.map((annotation, idx) => {
|
|
1795
|
+
const isIssueOrBug =
|
|
1796
|
+
annotation.type === "issue" ||
|
|
1797
|
+
annotation.type === "bug";
|
|
1798
|
+
const descriptionText = annotation.description || "";
|
|
1799
|
+
const typeLabel = sanitizeHTML(annotation.type);
|
|
1800
|
+
const descriptionHtml =
|
|
1801
|
+
isIssueOrBug && descriptionText.match(/^[A-Z]+-\d+$/)
|
|
1802
|
+
? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
|
|
1803
|
+
descriptionText
|
|
1804
|
+
)}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
|
|
1805
|
+
descriptionText
|
|
1806
|
+
)}</a>`
|
|
1807
|
+
: sanitizeHTML(descriptionText);
|
|
1808
|
+
const locationText = annotation.location
|
|
1809
|
+
? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
|
|
1810
|
+
annotation.location.file
|
|
1811
|
+
)}:${annotation.location.line}:${
|
|
1812
|
+
annotation.location.column
|
|
1813
|
+
}</div>`
|
|
1814
|
+
: "";
|
|
1815
|
+
return `<div style="margin-bottom: ${
|
|
1816
|
+
idx < test.annotations.length - 1 ? "10px" : "0"
|
|
1817
|
+
};">
|
|
1818
|
+
<strong style="color: #8b5cf6;">Type:</strong> <span style="background-color: rgba(139, 92, 246, 0.2); padding: 2px 8px; border-radius: 4px; font-size: 0.9em;">${typeLabel}</span>
|
|
1819
|
+
${
|
|
1820
|
+
descriptionText
|
|
1821
|
+
? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
|
|
1822
|
+
: ""
|
|
1823
|
+
}
|
|
1824
|
+
${locationText}
|
|
1825
|
+
</div>`;
|
|
1826
|
+
})
|
|
1827
|
+
.join("")}
|
|
1828
|
+
</div>`
|
|
1829
|
+
: ""
|
|
1830
|
+
}
|
|
1742
1831
|
<p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
|
|
1743
1832
|
test.workerId
|
|
1744
1833
|
)} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
${
|
|
1748
|
-
|
|
1749
|
-
test
|
|
1750
|
-
|
|
1834
|
+
test.totalWorkers
|
|
1835
|
+
)}]</p>
|
|
1836
|
+
${
|
|
1837
|
+
test.errorMessage
|
|
1838
|
+
? `<div class="test-error-summary">${formatPlaywrightError(
|
|
1839
|
+
test.errorMessage
|
|
1840
|
+
)}
|
|
1751
1841
|
<button
|
|
1752
1842
|
class="copy-error-btn"
|
|
1753
1843
|
onclick="copyErrorToClipboard(this)"
|
|
@@ -1768,13 +1858,14 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1768
1858
|
Copy Error Prompt
|
|
1769
1859
|
</button>
|
|
1770
1860
|
</div>`
|
|
1771
|
-
|
|
1861
|
+
: ""
|
|
1772
1862
|
}
|
|
1773
|
-
${
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1863
|
+
${
|
|
1864
|
+
test.snippet
|
|
1865
|
+
? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
|
|
1866
|
+
test.snippet
|
|
1867
|
+
)}</code></pre></div>`
|
|
1868
|
+
: ""
|
|
1778
1869
|
}
|
|
1779
1870
|
<h4>Steps</h4>
|
|
1780
1871
|
<div class="steps-list">${generateStepsHTML(test.steps)}</div>
|
|
@@ -1793,75 +1884,86 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1793
1884
|
</div>
|
|
1794
1885
|
</div>`;
|
|
1795
1886
|
})()}
|
|
1796
|
-
${
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1887
|
+
${
|
|
1888
|
+
test.stderr && test.stderr.length > 0
|
|
1889
|
+
? `<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(
|
|
1890
|
+
test.stderr.map((line) => sanitizeHTML(line)).join("\n")
|
|
1891
|
+
)}</pre></div>`
|
|
1892
|
+
: ""
|
|
1801
1893
|
}
|
|
1802
|
-
${
|
|
1803
|
-
|
|
1894
|
+
${
|
|
1895
|
+
test.screenshots && test.screenshots.length > 0
|
|
1896
|
+
? `
|
|
1804
1897
|
<div class="attachments-section">
|
|
1805
1898
|
<h4>Screenshots</h4>
|
|
1806
1899
|
<div class="attachments-grid">
|
|
1807
1900
|
${test.screenshots
|
|
1808
|
-
|
|
1809
|
-
|
|
1901
|
+
.map(
|
|
1902
|
+
(screenshot, index) => `
|
|
1810
1903
|
<div class="attachment-item">
|
|
1811
|
-
<img src="${fixPath(screenshot)}" alt="Screenshot ${
|
|
1904
|
+
<img src="${fixPath(screenshot)}" alt="Screenshot ${
|
|
1905
|
+
index + 1
|
|
1906
|
+
}">
|
|
1812
1907
|
<div class="attachment-info">
|
|
1813
1908
|
<div class="trace-actions">
|
|
1814
|
-
<a href="${fixPath(
|
|
1815
|
-
|
|
1909
|
+
<a href="${fixPath(
|
|
1910
|
+
screenshot
|
|
1911
|
+
)}" target="_blank" class="view-full">View Full Image</a>
|
|
1912
|
+
<a href="${fixPath(
|
|
1913
|
+
screenshot
|
|
1914
|
+
)}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
|
|
1816
1915
|
</div>
|
|
1817
1916
|
</div>
|
|
1818
1917
|
</div>
|
|
1819
1918
|
`
|
|
1820
|
-
|
|
1821
|
-
|
|
1919
|
+
)
|
|
1920
|
+
.join("")}
|
|
1822
1921
|
</div>
|
|
1823
1922
|
</div>
|
|
1824
1923
|
`
|
|
1825
|
-
|
|
1924
|
+
: ""
|
|
1826
1925
|
}
|
|
1827
|
-
${
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1926
|
+
${
|
|
1927
|
+
test.videoPath && test.videoPath.length > 0
|
|
1928
|
+
? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
|
|
1929
|
+
.map((videoUrl, index) => {
|
|
1930
|
+
const fixedVideoUrl = fixPath(videoUrl);
|
|
1931
|
+
const fileExtension = String(fixedVideoUrl)
|
|
1932
|
+
.split(".")
|
|
1933
|
+
.pop()
|
|
1934
|
+
.toLowerCase();
|
|
1935
|
+
const mimeType =
|
|
1936
|
+
{
|
|
1937
|
+
mp4: "video/mp4",
|
|
1938
|
+
webm: "video/webm",
|
|
1939
|
+
ogg: "video/ogg",
|
|
1940
|
+
mov: "video/quicktime",
|
|
1941
|
+
avi: "video/x-msvideo",
|
|
1942
|
+
}[fileExtension] || "video/mp4";
|
|
1943
|
+
return `<div class="attachment-item video-item">
|
|
1944
|
+
<video controls width="100%" height="auto" title="Video ${
|
|
1945
|
+
index + 1
|
|
1946
|
+
}">
|
|
1846
1947
|
<source src="${sanitizeHTML(
|
|
1847
|
-
|
|
1848
|
-
|
|
1948
|
+
fixedVideoUrl
|
|
1949
|
+
)}" type="${mimeType}">
|
|
1849
1950
|
Your browser does not support the video tag.
|
|
1850
1951
|
</video>
|
|
1851
1952
|
<div class="attachment-info">
|
|
1852
1953
|
<div class="trace-actions">
|
|
1853
1954
|
<a href="${sanitizeHTML(
|
|
1854
|
-
|
|
1855
|
-
|
|
1955
|
+
fixedVideoUrl
|
|
1956
|
+
)}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
|
|
1856
1957
|
</div>
|
|
1857
1958
|
</div>
|
|
1858
1959
|
</div>`;
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1960
|
+
})
|
|
1961
|
+
.join("")}</div></div>`
|
|
1962
|
+
: ""
|
|
1862
1963
|
}
|
|
1863
|
-
${
|
|
1864
|
-
|
|
1964
|
+
${
|
|
1965
|
+
test.tracePath
|
|
1966
|
+
? `
|
|
1865
1967
|
<div class="attachments-section">
|
|
1866
1968
|
<h4>Trace Files</h4>
|
|
1867
1969
|
<div class="attachments-grid">
|
|
@@ -1869,70 +1971,72 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1869
1971
|
<div class="trace-preview">
|
|
1870
1972
|
<span class="trace-icon">📄</span>
|
|
1871
1973
|
<span class="trace-name">${sanitizeHTML(
|
|
1872
|
-
|
|
1873
|
-
|
|
1974
|
+
path.basename(test.tracePath)
|
|
1975
|
+
)}</span>
|
|
1874
1976
|
</div>
|
|
1875
1977
|
<div class="attachment-info">
|
|
1876
1978
|
<div class="trace-actions">
|
|
1877
1979
|
<a href="${sanitizeHTML(
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1980
|
+
fixPath(test.tracePath)
|
|
1981
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
1982
|
+
path.basename(test.tracePath)
|
|
1983
|
+
)}" class="download-trace">Download Trace</a>
|
|
1882
1984
|
</div>
|
|
1883
1985
|
</div>
|
|
1884
1986
|
</div>
|
|
1885
1987
|
</div>
|
|
1886
1988
|
</div>
|
|
1887
1989
|
`
|
|
1888
|
-
|
|
1990
|
+
: ""
|
|
1889
1991
|
}
|
|
1890
|
-
${
|
|
1891
|
-
|
|
1992
|
+
${
|
|
1993
|
+
test.attachments && test.attachments.length > 0
|
|
1994
|
+
? `
|
|
1892
1995
|
<div class="attachments-section">
|
|
1893
1996
|
<h4>Other Attachments</h4>
|
|
1894
1997
|
<div class="attachments-grid">
|
|
1895
1998
|
${test.attachments
|
|
1896
|
-
|
|
1897
|
-
|
|
1999
|
+
.map(
|
|
2000
|
+
(attachment) => `
|
|
1898
2001
|
<div class="attachment-item generic-attachment">
|
|
1899
2002
|
<div class="attachment-icon">${getAttachmentIcon(
|
|
1900
|
-
|
|
1901
|
-
|
|
2003
|
+
attachment.contentType
|
|
2004
|
+
)}</div>
|
|
1902
2005
|
<div class="attachment-caption">
|
|
1903
2006
|
<span class="attachment-name" title="${sanitizeHTML(
|
|
1904
|
-
|
|
1905
|
-
|
|
2007
|
+
attachment.name
|
|
2008
|
+
)}">${sanitizeHTML(attachment.name)}</span>
|
|
1906
2009
|
<span class="attachment-type">${sanitizeHTML(
|
|
1907
|
-
|
|
1908
|
-
|
|
2010
|
+
attachment.contentType
|
|
2011
|
+
)}</span>
|
|
1909
2012
|
</div>
|
|
1910
2013
|
<div class="attachment-info">
|
|
1911
2014
|
<div class="trace-actions">
|
|
1912
2015
|
<a href="${sanitizeHTML(
|
|
1913
|
-
|
|
1914
|
-
|
|
2016
|
+
fixPath(attachment.path)
|
|
2017
|
+
)}" target="_blank" class="view-full">View</a>
|
|
1915
2018
|
<a href="${sanitizeHTML(
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2019
|
+
fixPath(attachment.path)
|
|
2020
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
2021
|
+
attachment.name
|
|
2022
|
+
)}" class="download-trace">Download</a>
|
|
1920
2023
|
</div>
|
|
1921
2024
|
</div>
|
|
1922
2025
|
</div>
|
|
1923
2026
|
`
|
|
1924
|
-
|
|
1925
|
-
|
|
2027
|
+
)
|
|
2028
|
+
.join("")}
|
|
1926
2029
|
</div>
|
|
1927
2030
|
</div>
|
|
1928
2031
|
`
|
|
1929
|
-
|
|
2032
|
+
: ""
|
|
1930
2033
|
}
|
|
1931
|
-
${
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2034
|
+
${
|
|
2035
|
+
test.codeSnippet
|
|
2036
|
+
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
|
|
2037
|
+
sanitizeHTML(test.codeSnippet)
|
|
2038
|
+
)}</code></pre></div>`
|
|
2039
|
+
: ""
|
|
1936
2040
|
}
|
|
1937
2041
|
</div>
|
|
1938
2042
|
</div>`;
|
|
@@ -2238,6 +2342,35 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2238
2342
|
.ai-text {
|
|
2239
2343
|
font-size: 0.95em;
|
|
2240
2344
|
}
|
|
2345
|
+
.ai-buttons-group {
|
|
2346
|
+
display: flex;
|
|
2347
|
+
gap: 10px;
|
|
2348
|
+
flex-wrap: wrap;
|
|
2349
|
+
}
|
|
2350
|
+
.copy-prompt-btn {
|
|
2351
|
+
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
|
2352
|
+
color: white;
|
|
2353
|
+
border: none;
|
|
2354
|
+
padding: 12px 18px;
|
|
2355
|
+
border-radius: 6px;
|
|
2356
|
+
cursor: pointer;
|
|
2357
|
+
font-weight: 600;
|
|
2358
|
+
display: flex;
|
|
2359
|
+
align-items: center;
|
|
2360
|
+
gap: 8px;
|
|
2361
|
+
transition: all 0.3s ease;
|
|
2362
|
+
white-space: nowrap;
|
|
2363
|
+
}
|
|
2364
|
+
.copy-prompt-btn:hover {
|
|
2365
|
+
transform: translateY(-2px);
|
|
2366
|
+
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.4);
|
|
2367
|
+
}
|
|
2368
|
+
.copy-prompt-btn.copied {
|
|
2369
|
+
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
2370
|
+
}
|
|
2371
|
+
.copy-prompt-text {
|
|
2372
|
+
font-size: 0.95em;
|
|
2373
|
+
}
|
|
2241
2374
|
.failure-error-preview {
|
|
2242
2375
|
padding: 0 20px 18px 20px;
|
|
2243
2376
|
border-top: 1px solid var(--light-gray-color);
|
|
@@ -2313,9 +2446,14 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2313
2446
|
.failure-meta {
|
|
2314
2447
|
justify-content: center;
|
|
2315
2448
|
}
|
|
2316
|
-
.
|
|
2449
|
+
.ai-buttons-group {
|
|
2450
|
+
flex-direction: column;
|
|
2451
|
+
width: 100%;
|
|
2452
|
+
}
|
|
2453
|
+
.compact-ai-btn, .copy-prompt-btn {
|
|
2317
2454
|
justify-content: center;
|
|
2318
2455
|
padding: 12px 20px;
|
|
2456
|
+
width: 100%;
|
|
2319
2457
|
}
|
|
2320
2458
|
}
|
|
2321
2459
|
@media (max-width: 480px) {
|
|
@@ -2345,8 +2483,8 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2345
2483
|
<h1>Playwright Pulse Report</h1>
|
|
2346
2484
|
</div>
|
|
2347
2485
|
<div class="run-info"><strong>Run Date:</strong> ${formatDate(
|
|
2348
|
-
|
|
2349
|
-
|
|
2486
|
+
runSummary.timestamp
|
|
2487
|
+
)}<br><strong>Total Duration:</strong> ${formatDuration(
|
|
2350
2488
|
runSummary.duration
|
|
2351
2489
|
)}</div>
|
|
2352
2490
|
</header>
|
|
@@ -2358,35 +2496,40 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2358
2496
|
</div>
|
|
2359
2497
|
<div id="dashboard" class="tab-content active">
|
|
2360
2498
|
<div class="dashboard-grid">
|
|
2361
|
-
<div class="summary-card"><h3>Total Tests</h3><div class="value">${
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
<div class="summary-card status-
|
|
2368
|
-
|
|
2499
|
+
<div class="summary-card"><h3>Total Tests</h3><div class="value">${
|
|
2500
|
+
runSummary.totalTests
|
|
2501
|
+
}</div></div>
|
|
2502
|
+
<div class="summary-card status-passed"><h3>Passed</h3><div class="value">${
|
|
2503
|
+
runSummary.passed
|
|
2504
|
+
}</div><div class="trend-percentage">${passPercentage}%</div></div>
|
|
2505
|
+
<div class="summary-card status-failed"><h3>Failed</h3><div class="value">${
|
|
2506
|
+
runSummary.failed
|
|
2507
|
+
}</div><div class="trend-percentage">${failPercentage}%</div></div>
|
|
2508
|
+
<div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
|
|
2509
|
+
runSummary.skipped || 0
|
|
2510
|
+
}</div><div class="trend-percentage">${skipPercentage}%</div></div>
|
|
2369
2511
|
<div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
|
|
2370
2512
|
<div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
|
|
2371
|
-
|
|
2372
|
-
|
|
2513
|
+
runSummary.duration
|
|
2514
|
+
)}</div></div>
|
|
2373
2515
|
</div>
|
|
2374
2516
|
<div class="dashboard-bottom-row">
|
|
2375
2517
|
<div style="display: grid; gap: 20px">
|
|
2376
2518
|
${generatePieChart(
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
${
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2519
|
+
[
|
|
2520
|
+
{ label: "Passed", value: runSummary.passed },
|
|
2521
|
+
{ label: "Failed", value: runSummary.failed },
|
|
2522
|
+
{ label: "Skipped", value: runSummary.skipped || 0 },
|
|
2523
|
+
],
|
|
2524
|
+
400,
|
|
2525
|
+
390
|
|
2526
|
+
)}
|
|
2527
|
+
${
|
|
2528
|
+
runSummary.environment &&
|
|
2529
|
+
Object.keys(runSummary.environment).length > 0
|
|
2530
|
+
? generateEnvironmentDashboard(runSummary.environment)
|
|
2531
|
+
: '<div class="no-data">Environment data not available.</div>'
|
|
2532
|
+
}
|
|
2390
2533
|
</div>
|
|
2391
2534
|
${generateSuitesWidget(suitesData)}
|
|
2392
2535
|
</div>
|
|
@@ -2396,17 +2539,17 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2396
2539
|
<input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
|
|
2397
2540
|
<select id="filter-status"><option value="">All Statuses</option><option value="passed">Passed</option><option value="failed">Failed</option><option value="skipped">Skipped</option></select>
|
|
2398
2541
|
<select id="filter-browser"><option value="">All Browsers</option>${Array.from(
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2542
|
+
new Set(
|
|
2543
|
+
(results || []).map((test) => test.browser || "unknown")
|
|
2544
|
+
)
|
|
2545
|
+
)
|
|
2546
|
+
.map(
|
|
2547
|
+
(browser) =>
|
|
2548
|
+
`<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
|
|
2549
|
+
browser
|
|
2550
|
+
)}</option>`
|
|
2551
|
+
)
|
|
2552
|
+
.join("")}</select>
|
|
2410
2553
|
<button id="expand-all-tests">Expand All</button> <button id="collapse-all-tests">Collapse All</button> <button id="clear-run-summary-filters" class="clear-filters-btn">Clear Filters</button>
|
|
2411
2554
|
</div>
|
|
2412
2555
|
<div class="test-cases-list">${generateTestCasesHTML()}</div>
|
|
@@ -2415,16 +2558,18 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2415
2558
|
<h2 class="tab-main-title">Execution Trends</h2>
|
|
2416
2559
|
<div class="trend-charts-row">
|
|
2417
2560
|
<div class="trend-chart"><h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
|
|
2418
|
-
${
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2561
|
+
${
|
|
2562
|
+
trendData && trendData.overall && trendData.overall.length > 0
|
|
2563
|
+
? generateTestTrendsChart(trendData)
|
|
2564
|
+
: '<div class="no-data">Overall trend data not available for test counts.</div>'
|
|
2565
|
+
}
|
|
2422
2566
|
</div>
|
|
2423
2567
|
<div class="trend-chart"><h3 class="chart-title-header">Execution Duration Trends</h3>
|
|
2424
|
-
${
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2568
|
+
${
|
|
2569
|
+
trendData && trendData.overall && trendData.overall.length > 0
|
|
2570
|
+
? generateDurationTrendChart(trendData)
|
|
2571
|
+
: '<div class="no-data">Overall trend data not available for durations.</div>'
|
|
2572
|
+
}
|
|
2428
2573
|
</div>
|
|
2429
2574
|
</div>
|
|
2430
2575
|
<h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
|
|
@@ -2434,12 +2579,13 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2434
2579
|
</div>
|
|
2435
2580
|
</div>
|
|
2436
2581
|
<h2 class="tab-main-title">Individual Test History</h2>
|
|
2437
|
-
${
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2582
|
+
${
|
|
2583
|
+
trendData &&
|
|
2584
|
+
trendData.testRuns &&
|
|
2585
|
+
Object.keys(trendData.testRuns).length > 0
|
|
2586
|
+
? generateTestHistoryContent(trendData)
|
|
2587
|
+
: '<div class="no-data">Individual test history data not available.</div>'
|
|
2588
|
+
}
|
|
2443
2589
|
</div>
|
|
2444
2590
|
<div id="ai-failure-analyzer" class="tab-content">
|
|
2445
2591
|
${generateAIFailureAnalyzerTab(results)}
|
|
@@ -2582,6 +2728,81 @@ function getAIFix(button) {
|
|
|
2582
2728
|
}
|
|
2583
2729
|
|
|
2584
2730
|
|
|
2731
|
+
function copyAIPrompt(button) {
|
|
2732
|
+
try {
|
|
2733
|
+
const testJson = button.dataset.testJson;
|
|
2734
|
+
const test = JSON.parse(atob(testJson));
|
|
2735
|
+
|
|
2736
|
+
const testName = test.name || 'Unknown Test';
|
|
2737
|
+
const failureLogsAndErrors = [
|
|
2738
|
+
'Error Message:',
|
|
2739
|
+
test.errorMessage || 'Not available.',
|
|
2740
|
+
'\\n\\n--- stdout ---',
|
|
2741
|
+
(test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
|
|
2742
|
+
'\\n\\n--- stderr ---',
|
|
2743
|
+
(test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
|
|
2744
|
+
].join('\\n');
|
|
2745
|
+
const codeSnippet = test.snippet || '';
|
|
2746
|
+
|
|
2747
|
+
const aiPrompt = \`You are an expert Playwright test automation engineer specializing in debugging test failures.
|
|
2748
|
+
|
|
2749
|
+
INSTRUCTIONS:
|
|
2750
|
+
1. Analyze the test failure carefully
|
|
2751
|
+
2. Provide a brief root cause analysis
|
|
2752
|
+
3. Provide EXACTLY 5 specific, actionable fixes
|
|
2753
|
+
4. Each fix MUST include a code snippet (codeSnippet field)
|
|
2754
|
+
5. Return ONLY valid JSON, no markdown or extra text
|
|
2755
|
+
|
|
2756
|
+
REQUIRED JSON FORMAT:
|
|
2757
|
+
{
|
|
2758
|
+
"rootCause": "Brief explanation of why the test failed",
|
|
2759
|
+
"suggestedFixes": [
|
|
2760
|
+
{
|
|
2761
|
+
"description": "Clear explanation of the fix",
|
|
2762
|
+
"codeSnippet": "await page.waitForSelector('.button', { timeout: 5000 });"
|
|
2763
|
+
}
|
|
2764
|
+
],
|
|
2765
|
+
"affectedTests": ["test1", "test2"]
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
IMPORTANT:
|
|
2769
|
+
- Always return valid JSON only
|
|
2770
|
+
- Always provide exactly 5 fixes in suggestedFixes array
|
|
2771
|
+
- Each fix must have both description and codeSnippet fields
|
|
2772
|
+
- Make code snippets practical and Playwright-specific
|
|
2773
|
+
|
|
2774
|
+
---
|
|
2775
|
+
|
|
2776
|
+
Test Name: \${testName}
|
|
2777
|
+
|
|
2778
|
+
Failure Logs and Errors:
|
|
2779
|
+
\${failureLogsAndErrors}
|
|
2780
|
+
|
|
2781
|
+
Code Snippet:
|
|
2782
|
+
\${codeSnippet}\`;
|
|
2783
|
+
|
|
2784
|
+
navigator.clipboard.writeText(aiPrompt).then(() => {
|
|
2785
|
+
const originalText = button.querySelector('.copy-prompt-text').textContent;
|
|
2786
|
+
button.querySelector('.copy-prompt-text').textContent = 'Copied!';
|
|
2787
|
+
button.classList.add('copied');
|
|
2788
|
+
|
|
2789
|
+
const shortTestName = testName.split(' > ').pop() || testName;
|
|
2790
|
+
alert(\`AI prompt to generate a suggested fix for "\${shortTestName}" has been copied to your clipboard.\`);
|
|
2791
|
+
|
|
2792
|
+
setTimeout(() => {
|
|
2793
|
+
button.querySelector('.copy-prompt-text').textContent = originalText;
|
|
2794
|
+
button.classList.remove('copied');
|
|
2795
|
+
}, 2000);
|
|
2796
|
+
}).catch(err => {
|
|
2797
|
+
console.error('Failed to copy AI prompt:', err);
|
|
2798
|
+
alert('Failed to copy AI prompt to clipboard. Please try again.');
|
|
2799
|
+
});
|
|
2800
|
+
} catch (e) {
|
|
2801
|
+
console.error('Error processing test data for AI Prompt copy:', e);
|
|
2802
|
+
alert('Could not process test data. Please try again.');
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2585
2806
|
function closeAiModal() {
|
|
2586
2807
|
const modal = document.getElementById('ai-fix-modal');
|
|
2587
2808
|
if(modal) modal.style.display = 'none';
|
|
@@ -2702,6 +2923,19 @@ function getAIFix(button) {
|
|
|
2702
2923
|
}
|
|
2703
2924
|
if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
|
|
2704
2925
|
if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
|
|
2926
|
+
// --- Annotation Link Handler ---
|
|
2927
|
+
document.querySelectorAll('a.annotation-link').forEach(link => {
|
|
2928
|
+
link.addEventListener('click', (e) => {
|
|
2929
|
+
e.preventDefault();
|
|
2930
|
+
const annotationId = link.dataset.annotation;
|
|
2931
|
+
if (annotationId) {
|
|
2932
|
+
const jiraUrl = prompt('Enter your JIRA/Ticket system base URL (e.g., https://your-company.atlassian.net/browse/):', 'https://your-company.atlassian.net/browse/');
|
|
2933
|
+
if (jiraUrl) {
|
|
2934
|
+
window.open(jiraUrl + annotationId, '_blank');
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
});
|
|
2938
|
+
});
|
|
2705
2939
|
// --- Intersection Observer for Lazy Loading ---
|
|
2706
2940
|
const lazyLoadElements = document.querySelectorAll('.lazy-load-chart');
|
|
2707
2941
|
if ('IntersectionObserver' in window) {
|
|
@@ -2817,10 +3051,10 @@ function copyErrorToClipboard(button) {
|
|
|
2817
3051
|
</html>
|
|
2818
3052
|
`;
|
|
2819
3053
|
}
|
|
2820
|
-
async function runScript(scriptPath) {
|
|
3054
|
+
async function runScript(scriptPath, args = []) {
|
|
2821
3055
|
return new Promise((resolve, reject) => {
|
|
2822
3056
|
console.log(chalk.blue(`Executing script: ${scriptPath}...`));
|
|
2823
|
-
const process = fork(scriptPath,
|
|
3057
|
+
const process = fork(scriptPath, args, {
|
|
2824
3058
|
stdio: "inherit",
|
|
2825
3059
|
});
|
|
2826
3060
|
|
|
@@ -2845,13 +3079,22 @@ async function main() {
|
|
|
2845
3079
|
const __filename = fileURLToPath(import.meta.url);
|
|
2846
3080
|
const __dirname = path.dirname(__filename);
|
|
2847
3081
|
|
|
3082
|
+
const args = process.argv.slice(2);
|
|
3083
|
+
let customOutputDir = null;
|
|
3084
|
+
for (let i = 0; i < args.length; i++) {
|
|
3085
|
+
if (args[i] === "--outputDir" || args[i] === "-o") {
|
|
3086
|
+
customOutputDir = args[i + 1];
|
|
3087
|
+
break;
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
|
|
2848
3091
|
// Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
|
|
2849
3092
|
const archiveRunScriptPath = path.resolve(
|
|
2850
3093
|
__dirname,
|
|
2851
3094
|
"generate-trend.mjs" // Keeping the filename as per your request
|
|
2852
3095
|
);
|
|
2853
3096
|
|
|
2854
|
-
const outputDir =
|
|
3097
|
+
const outputDir = await getOutputDir(customOutputDir);
|
|
2855
3098
|
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
|
|
2856
3099
|
const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
|
|
2857
3100
|
|
|
@@ -2861,10 +3104,18 @@ async function main() {
|
|
|
2861
3104
|
|
|
2862
3105
|
console.log(chalk.blue(`Starting static HTML report generation...`));
|
|
2863
3106
|
console.log(chalk.blue(`Output directory set to: ${outputDir}`));
|
|
3107
|
+
if (customOutputDir) {
|
|
3108
|
+
console.log(chalk.gray(` (from CLI argument)`));
|
|
3109
|
+
} else {
|
|
3110
|
+
console.log(
|
|
3111
|
+
chalk.gray(` (auto-detected from playwright.config or using default)`)
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
2864
3114
|
|
|
2865
3115
|
// Step 1: Ensure current run data is archived to the history folder
|
|
2866
3116
|
try {
|
|
2867
|
-
|
|
3117
|
+
const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
|
|
3118
|
+
await runScript(archiveRunScriptPath, archiveArgs);
|
|
2868
3119
|
console.log(
|
|
2869
3120
|
chalk.green("Current run data archiving to history completed.")
|
|
2870
3121
|
);
|