@arghajit/playwright-pulse-report 0.2.10 → 0.3.1
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 +97 -0
- package/dist/pulse.d.ts +12 -0
- package/dist/pulse.js +24 -0
- package/dist/reporter/index.d.ts +2 -0
- package/dist/reporter/index.js +5 -1
- package/dist/reporter/playwright-pulse-reporter.d.ts +1 -0
- package/dist/reporter/playwright-pulse-reporter.js +27 -9
- package/dist/types/index.d.ts +12 -0
- package/package.json +8 -8
- package/scripts/config-reader.mjs +180 -0
- package/scripts/generate-email-report.mjs +81 -11
- package/scripts/generate-report.mjs +996 -306
- package/scripts/generate-static-report.mjs +895 -318
- package/scripts/generate-trend.mjs +11 -1
- package/scripts/merge-pulse-report.js +46 -14
- package/scripts/sendReport.mjs +109 -34
|
@@ -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;
|
|
@@ -349,6 +350,7 @@ function generateTestTrendsChart(trendData) {
|
|
|
349
350
|
</script>
|
|
350
351
|
`;
|
|
351
352
|
}
|
|
353
|
+
const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
|
|
352
354
|
function generateDurationTrendChart(trendData) {
|
|
353
355
|
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
354
356
|
return '<div class="no-data">No overall trend data available for durations.</div>';
|
|
@@ -362,8 +364,6 @@ function generateDurationTrendChart(trendData) {
|
|
|
362
364
|
)}`;
|
|
363
365
|
const runs = trendData.overall;
|
|
364
366
|
|
|
365
|
-
const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
|
|
366
|
-
|
|
367
367
|
const chartDataString = JSON.stringify(runs.map((run) => run.duration));
|
|
368
368
|
const categoriesString = JSON.stringify(runs.map((run, i) => `Run ${i + 1}`));
|
|
369
369
|
const runsForTooltip = runs.map((r) => ({
|
|
@@ -615,8 +615,9 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
615
615
|
chart: {
|
|
616
616
|
type: 'pie',
|
|
617
617
|
width: ${chartWidth},
|
|
618
|
-
height: ${
|
|
619
|
-
|
|
618
|
+
height: ${
|
|
619
|
+
chartHeight - 40
|
|
620
|
+
}, // Adjusted height to make space for legend if chartHeight is for the whole wrapper
|
|
620
621
|
backgroundColor: 'transparent',
|
|
621
622
|
plotShadow: false,
|
|
622
623
|
spacingBottom: 40 // Ensure space for legend
|
|
@@ -668,8 +669,9 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
668
669
|
return `
|
|
669
670
|
<div class="pie-chart-wrapper" style="align-items: center; max-height: 450px">
|
|
670
671
|
<div style="display: flex; align-items: start; width: 100%;"><h3>Test Distribution</h3></div>
|
|
671
|
-
<div id="${chartId}" style="width: ${chartWidth}px; height: ${
|
|
672
|
-
|
|
672
|
+
<div id="${chartId}" style="width: ${chartWidth}px; height: ${
|
|
673
|
+
chartHeight - 40
|
|
674
|
+
}px;"></div>
|
|
673
675
|
<script>
|
|
674
676
|
document.addEventListener('DOMContentLoaded', function() {
|
|
675
677
|
if (typeof Highcharts !== 'undefined') {
|
|
@@ -700,6 +702,9 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
700
702
|
const cardHeight = Math.floor(dashboardHeight * 0.44);
|
|
701
703
|
const cardContentPadding = 16; // px
|
|
702
704
|
|
|
705
|
+
// Logic for Run Context
|
|
706
|
+
const runContext = process.env.CI ? "CI" : "Local Test";
|
|
707
|
+
|
|
703
708
|
return `
|
|
704
709
|
<div class="environment-dashboard-wrapper" id="${dashboardId}">
|
|
705
710
|
<style>
|
|
@@ -742,6 +747,20 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
742
747
|
gap: 20px;
|
|
743
748
|
font-size: 14px;
|
|
744
749
|
}
|
|
750
|
+
|
|
751
|
+
/* Mobile Responsiveness */
|
|
752
|
+
@media (max-width: 768px) {
|
|
753
|
+
.environment-dashboard-wrapper {
|
|
754
|
+
grid-template-columns: 1fr; /* Stack columns on mobile */
|
|
755
|
+
grid-template-rows: auto;
|
|
756
|
+
padding: 16px;
|
|
757
|
+
height: auto !important; /* Allow height to grow */
|
|
758
|
+
}
|
|
759
|
+
.env-card {
|
|
760
|
+
height: auto !important; /* Allow cards to grow based on content */
|
|
761
|
+
min-height: 200px;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
745
764
|
|
|
746
765
|
.env-dashboard-header {
|
|
747
766
|
grid-column: 1 / -1;
|
|
@@ -751,6 +770,8 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
751
770
|
border-bottom: 1px solid var(--border-color);
|
|
752
771
|
padding-bottom: 16px;
|
|
753
772
|
margin-bottom: 8px;
|
|
773
|
+
flex-wrap: wrap; /* Allow wrapping header items */
|
|
774
|
+
gap: 10px;
|
|
754
775
|
}
|
|
755
776
|
|
|
756
777
|
.env-dashboard-title {
|
|
@@ -808,6 +829,8 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
808
829
|
padding: 10px 0;
|
|
809
830
|
border-bottom: 1px solid var(--border-light-color);
|
|
810
831
|
font-size: 0.875rem;
|
|
832
|
+
flex-wrap: wrap; /* Allow details to wrap on very small screens */
|
|
833
|
+
gap: 8px;
|
|
811
834
|
}
|
|
812
835
|
|
|
813
836
|
.env-detail-row:last-child {
|
|
@@ -825,6 +848,7 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
825
848
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
826
849
|
text-align: right;
|
|
827
850
|
word-break: break-all;
|
|
851
|
+
margin-left: auto; /* Push to right */
|
|
828
852
|
}
|
|
829
853
|
|
|
830
854
|
.env-chip {
|
|
@@ -897,14 +921,15 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
897
921
|
<span class="env-detail-value">
|
|
898
922
|
<div class="env-cpu-cores">
|
|
899
923
|
${Array.from(
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
924
|
+
{ length: Math.max(0, environment.cpu.cores || 0) },
|
|
925
|
+
(_, i) =>
|
|
926
|
+
`<div class="env-core-indicator ${
|
|
927
|
+
i >=
|
|
928
|
+
(environment.cpu.cores >= 8 ? 8 : environment.cpu.cores)
|
|
929
|
+
? "inactive"
|
|
930
|
+
: ""
|
|
931
|
+
}" title="Core ${i + 1}"></div>`
|
|
932
|
+
).join("")}
|
|
908
933
|
<span>${environment.cpu.cores || "N/A"} cores</span>
|
|
909
934
|
</div>
|
|
910
935
|
</span>
|
|
@@ -924,20 +949,23 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
924
949
|
<div class="env-card-content">
|
|
925
950
|
<div class="env-detail-row">
|
|
926
951
|
<span class="env-detail-label">OS Type</span>
|
|
927
|
-
<span class="env-detail-value">${
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
952
|
+
<span class="env-detail-value">${
|
|
953
|
+
environment.os.split(" ")[0] === "darwin"
|
|
954
|
+
? "darwin (macOS)"
|
|
955
|
+
: environment.os.split(" ")[0] || "Unknown"
|
|
956
|
+
}</span>
|
|
931
957
|
</div>
|
|
932
958
|
<div class="env-detail-row">
|
|
933
959
|
<span class="env-detail-label">OS Version</span>
|
|
934
|
-
<span class="env-detail-value">${
|
|
935
|
-
|
|
960
|
+
<span class="env-detail-value">${
|
|
961
|
+
environment.os.split(" ")[1] || "N/A"
|
|
962
|
+
}</span>
|
|
936
963
|
</div>
|
|
937
964
|
<div class="env-detail-row">
|
|
938
965
|
<span class="env-detail-label">Hostname</span>
|
|
939
|
-
<span class="env-detail-value" title="${environment.host}">${
|
|
940
|
-
|
|
966
|
+
<span class="env-detail-value" title="${environment.host}">${
|
|
967
|
+
environment.host
|
|
968
|
+
}</span>
|
|
941
969
|
</div>
|
|
942
970
|
</div>
|
|
943
971
|
</div>
|
|
@@ -958,10 +986,11 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
958
986
|
</div>
|
|
959
987
|
<div class="env-detail-row">
|
|
960
988
|
<span class="env-detail-label">Working Dir</span>
|
|
961
|
-
<span class="env-detail-value" title="${environment.cwd}">${
|
|
989
|
+
<span class="env-detail-value" title="${environment.cwd}">${
|
|
990
|
+
environment.cwd.length > 25
|
|
962
991
|
? "..." + environment.cwd.slice(-22)
|
|
963
992
|
: environment.cwd
|
|
964
|
-
|
|
993
|
+
}</span>
|
|
965
994
|
</div>
|
|
966
995
|
</div>
|
|
967
996
|
</div>
|
|
@@ -975,34 +1004,37 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
975
1004
|
<div class="env-detail-row">
|
|
976
1005
|
<span class="env-detail-label">Platform Arch</span>
|
|
977
1006
|
<span class="env-detail-value">
|
|
978
|
-
<span class="env-chip ${
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1007
|
+
<span class="env-chip ${
|
|
1008
|
+
environment.os.includes("darwin") &&
|
|
1009
|
+
environment.cpu.model.toLowerCase().includes("apple")
|
|
1010
|
+
? "env-chip-success"
|
|
1011
|
+
: "env-chip-warning"
|
|
1012
|
+
}">
|
|
1013
|
+
${
|
|
1014
|
+
environment.os.includes("darwin") &&
|
|
1015
|
+
environment.cpu.model.toLowerCase().includes("apple")
|
|
1016
|
+
? "Apple Silicon"
|
|
1017
|
+
: environment.cpu.model.toLowerCase().includes("arm") ||
|
|
1018
|
+
environment.cpu.model.toLowerCase().includes("aarch64")
|
|
1019
|
+
? "ARM-based"
|
|
1020
|
+
: "x86/Other"
|
|
1021
|
+
}
|
|
991
1022
|
</span>
|
|
992
1023
|
</span>
|
|
993
1024
|
</div>
|
|
994
1025
|
<div class="env-detail-row">
|
|
995
1026
|
<span class="env-detail-label">Memory per Core</span>
|
|
996
|
-
<span class="env-detail-value">${
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1027
|
+
<span class="env-detail-value">${
|
|
1028
|
+
environment.cpu.cores > 0
|
|
1029
|
+
? (
|
|
1030
|
+
parseFloat(environment.memory) / environment.cpu.cores
|
|
1031
|
+
).toFixed(2) + " GB"
|
|
1032
|
+
: "N/A"
|
|
1033
|
+
}</span>
|
|
1002
1034
|
</div>
|
|
1003
1035
|
<div class="env-detail-row">
|
|
1004
1036
|
<span class="env-detail-label">Run Context</span>
|
|
1005
|
-
<span class="env-detail-value"
|
|
1037
|
+
<span class="env-detail-value">${runContext}</span>
|
|
1006
1038
|
</div>
|
|
1007
1039
|
</div>
|
|
1008
1040
|
</div>
|
|
@@ -1330,19 +1362,19 @@ function generateTestHistoryContent(trendData) {
|
|
|
1330
1362
|
|
|
1331
1363
|
<div class="test-history-grid">
|
|
1332
1364
|
${testHistory
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1365
|
+
.map((test) => {
|
|
1366
|
+
const latestRun =
|
|
1367
|
+
test.history.length > 0
|
|
1368
|
+
? test.history[test.history.length - 1]
|
|
1369
|
+
: { status: "unknown" };
|
|
1370
|
+
return `
|
|
1339
1371
|
<div class="test-history-card" data-test-name="${sanitizeHTML(
|
|
1340
|
-
|
|
1341
|
-
|
|
1372
|
+
test.testTitle.toLowerCase()
|
|
1373
|
+
)}" data-latest-status="${latestRun.status}">
|
|
1342
1374
|
<div class="test-history-header">
|
|
1343
1375
|
<p title="${sanitizeHTML(test.testTitle)}">${capitalize(
|
|
1344
|
-
|
|
1345
|
-
|
|
1376
|
+
sanitizeHTML(test.testTitle)
|
|
1377
|
+
)}</p>
|
|
1346
1378
|
<span class="status-badge ${getStatusClass(latestRun.status)}">
|
|
1347
1379
|
${String(latestRun.status).toUpperCase()}
|
|
1348
1380
|
</span>
|
|
@@ -1357,27 +1389,27 @@ function generateTestHistoryContent(trendData) {
|
|
|
1357
1389
|
<thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Date</th></tr></thead>
|
|
1358
1390
|
<tbody>
|
|
1359
1391
|
${test.history
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1392
|
+
.slice()
|
|
1393
|
+
.reverse()
|
|
1394
|
+
.map(
|
|
1395
|
+
(run) => `
|
|
1364
1396
|
<tr>
|
|
1365
1397
|
<td>${run.runId}</td>
|
|
1366
1398
|
<td><span class="status-badge-small ${getStatusClass(
|
|
1367
|
-
|
|
1368
|
-
|
|
1399
|
+
run.status
|
|
1400
|
+
)}">${String(run.status).toUpperCase()}</span></td>
|
|
1369
1401
|
<td>${formatDuration(run.duration)}</td>
|
|
1370
1402
|
<td>${formatDate(run.timestamp)}</td>
|
|
1371
1403
|
</tr>`
|
|
1372
|
-
|
|
1373
|
-
|
|
1404
|
+
)
|
|
1405
|
+
.join("")}
|
|
1374
1406
|
</tbody>
|
|
1375
1407
|
</table>
|
|
1376
1408
|
</div>
|
|
1377
1409
|
</details>
|
|
1378
1410
|
</div>`;
|
|
1379
|
-
|
|
1380
|
-
|
|
1411
|
+
})
|
|
1412
|
+
.join("")}
|
|
1381
1413
|
</div>
|
|
1382
1414
|
</div>
|
|
1383
1415
|
`;
|
|
@@ -1456,52 +1488,65 @@ function getSuitesData(results) {
|
|
|
1456
1488
|
}
|
|
1457
1489
|
function generateSuitesWidget(suitesData) {
|
|
1458
1490
|
if (!suitesData || suitesData.length === 0) {
|
|
1459
|
-
|
|
1491
|
+
// Maintain height consistency even if empty
|
|
1492
|
+
return `<div class="suites-widget" style="height: 450px;"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
|
|
1460
1493
|
}
|
|
1494
|
+
|
|
1495
|
+
// Added inline styles for height consistency with Pie Chart (approx 450px) and scrolling
|
|
1461
1496
|
return `
|
|
1462
|
-
<div class="suites-widget">
|
|
1463
|
-
<div class="suites-header">
|
|
1497
|
+
<div class="suites-widget" style="height: 450px; display: flex; flex-direction: column;">
|
|
1498
|
+
<div class="suites-header" style="flex-shrink: 0;">
|
|
1464
1499
|
<h2>Test Suites</h2>
|
|
1465
|
-
<span class="summary-badge">${
|
|
1500
|
+
<span class="summary-badge">${
|
|
1501
|
+
suitesData.length
|
|
1466
1502
|
} suites • ${suitesData.reduce(
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1503
|
+
(sum, suite) => sum + suite.count,
|
|
1504
|
+
0
|
|
1505
|
+
)} tests</span>
|
|
1470
1506
|
</div>
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
<
|
|
1478
|
-
suite
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1507
|
+
|
|
1508
|
+
<div class="suites-grid-container" style="flex-grow: 1; overflow-y: auto; padding-right: 5px;">
|
|
1509
|
+
<div class="suites-grid">
|
|
1510
|
+
${suitesData
|
|
1511
|
+
.map(
|
|
1512
|
+
(suite) => `
|
|
1513
|
+
<div class="suite-card status-${suite.statusOverall}">
|
|
1514
|
+
<div class="suite-card-header">
|
|
1515
|
+
<h3 class="suite-name" title="${sanitizeHTML(
|
|
1516
|
+
suite.name
|
|
1517
|
+
)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(
|
|
1518
|
+
suite.name
|
|
1519
|
+
)}</h3>
|
|
1520
|
+
</div>
|
|
1521
|
+
<div>🖥️ <span class="browser-tag">${sanitizeHTML(
|
|
1522
|
+
suite.browser
|
|
1523
|
+
)}</span></div>
|
|
1524
|
+
<div class="suite-card-body">
|
|
1525
|
+
<span class="test-count">${suite.count} test${
|
|
1526
|
+
suite.count !== 1 ? "s" : ""
|
|
1527
|
+
}</span>
|
|
1528
|
+
<div class="suite-stats">
|
|
1529
|
+
${
|
|
1530
|
+
suite.passed > 0
|
|
1531
|
+
? `<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>`
|
|
1532
|
+
: ""
|
|
1533
|
+
}
|
|
1534
|
+
${
|
|
1535
|
+
suite.failed > 0
|
|
1536
|
+
? `<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>`
|
|
1537
|
+
: ""
|
|
1538
|
+
}
|
|
1539
|
+
${
|
|
1540
|
+
suite.skipped > 0
|
|
1541
|
+
? `<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>`
|
|
1542
|
+
: ""
|
|
1543
|
+
}
|
|
1544
|
+
</div>
|
|
1545
|
+
</div>
|
|
1546
|
+
</div>`
|
|
1547
|
+
)
|
|
1548
|
+
.join("")}
|
|
1501
1549
|
</div>
|
|
1502
|
-
</div>`
|
|
1503
|
-
)
|
|
1504
|
-
.join("")}
|
|
1505
1550
|
</div>
|
|
1506
1551
|
</div>`;
|
|
1507
1552
|
}
|
|
@@ -1519,7 +1564,9 @@ function getAttachmentIcon(contentType) {
|
|
|
1519
1564
|
return "📎";
|
|
1520
1565
|
}
|
|
1521
1566
|
function generateAIFailureAnalyzerTab(results) {
|
|
1522
|
-
const failedTests = (results || []).filter(
|
|
1567
|
+
const failedTests = (results || []).filter(
|
|
1568
|
+
(test) => test.status === "failed"
|
|
1569
|
+
);
|
|
1523
1570
|
|
|
1524
1571
|
if (failedTests.length === 0) {
|
|
1525
1572
|
return `
|
|
@@ -1529,7 +1576,7 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1529
1576
|
}
|
|
1530
1577
|
|
|
1531
1578
|
// btoa is not available in Node.js environment, so we define a simple polyfill for it.
|
|
1532
|
-
const btoa = (str) => Buffer.from(str).toString(
|
|
1579
|
+
const btoa = (str) => Buffer.from(str).toString("base64");
|
|
1533
1580
|
|
|
1534
1581
|
return `
|
|
1535
1582
|
<h2 class="tab-main-title">AI Failure Analysis</h2>
|
|
@@ -1539,41 +1586,61 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1539
1586
|
<span class="stat-label">Failed Tests</span>
|
|
1540
1587
|
</div>
|
|
1541
1588
|
<div class="stat-item">
|
|
1542
|
-
<span class="stat-number">${
|
|
1589
|
+
<span class="stat-number">${
|
|
1590
|
+
new Set(failedTests.map((t) => t.browser)).size
|
|
1591
|
+
}</span>
|
|
1543
1592
|
<span class="stat-label">Browsers</span>
|
|
1544
1593
|
</div>
|
|
1545
1594
|
<div class="stat-item">
|
|
1546
|
-
<span class="stat-number">${
|
|
1595
|
+
<span class="stat-number">${Math.round(
|
|
1596
|
+
failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) /
|
|
1597
|
+
1000
|
|
1598
|
+
)}s</span>
|
|
1547
1599
|
<span class="stat-label">Total Duration</span>
|
|
1548
1600
|
</div>
|
|
1549
1601
|
</div>
|
|
1550
1602
|
<p class="ai-analyzer-description">
|
|
1551
|
-
Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for
|
|
1603
|
+
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
1604
|
</p>
|
|
1553
1605
|
|
|
1554
1606
|
<div class="compact-failure-list">
|
|
1555
|
-
${failedTests
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1607
|
+
${failedTests
|
|
1608
|
+
.map((test) => {
|
|
1609
|
+
const testTitle = test.name.split(" > ").pop() || "Unnamed Test";
|
|
1610
|
+
const testJson = btoa(JSON.stringify(test)); // Base64 encode the test object
|
|
1611
|
+
const truncatedError =
|
|
1612
|
+
(test.errorMessage || "No error message").slice(0, 150) +
|
|
1613
|
+
(test.errorMessage && test.errorMessage.length > 150 ? "..." : "");
|
|
1614
|
+
|
|
1615
|
+
return `
|
|
1562
1616
|
<div class="compact-failure-item">
|
|
1563
1617
|
<div class="failure-header">
|
|
1564
1618
|
<div class="failure-main-info">
|
|
1565
|
-
<h3 class="failure-title" title="${sanitizeHTML(
|
|
1619
|
+
<h3 class="failure-title" title="${sanitizeHTML(
|
|
1620
|
+
test.name
|
|
1621
|
+
)}">${sanitizeHTML(testTitle)}</h3>
|
|
1566
1622
|
<div class="failure-meta">
|
|
1567
|
-
<span class="browser-indicator">${sanitizeHTML(
|
|
1568
|
-
|
|
1623
|
+
<span class="browser-indicator">${sanitizeHTML(
|
|
1624
|
+
test.browser || "unknown"
|
|
1625
|
+
)}</span>
|
|
1626
|
+
<span class="duration-indicator">${formatDuration(
|
|
1627
|
+
test.duration
|
|
1628
|
+
)}</span>
|
|
1569
1629
|
</div>
|
|
1570
1630
|
</div>
|
|
1571
|
-
<
|
|
1572
|
-
<
|
|
1573
|
-
|
|
1631
|
+
<div class="ai-buttons-group">
|
|
1632
|
+
<button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
|
|
1633
|
+
<span class="ai-text">AI Fix</span>
|
|
1634
|
+
</button>
|
|
1635
|
+
<button class="copy-prompt-btn" onclick="copyAIPrompt(this)" data-test-json="${testJson}" title="Copy AI Prompt">
|
|
1636
|
+
<span class="copy-prompt-text">Copy AI Prompt</span>
|
|
1637
|
+
</button>
|
|
1638
|
+
</div>
|
|
1574
1639
|
</div>
|
|
1575
1640
|
<div class="failure-error-preview">
|
|
1576
|
-
<div class="error-snippet">${formatPlaywrightError(
|
|
1641
|
+
<div class="error-snippet">${formatPlaywrightError(
|
|
1642
|
+
truncatedError
|
|
1643
|
+
)}</div>
|
|
1577
1644
|
<button class="expand-error-btn" onclick="toggleErrorDetails(this)">
|
|
1578
1645
|
<span class="expand-text">Show Full Error</span>
|
|
1579
1646
|
<span class="expand-icon">▼</span>
|
|
@@ -1581,12 +1648,15 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1581
1648
|
</div>
|
|
1582
1649
|
<div class="full-error-details" style="display: none;">
|
|
1583
1650
|
<div class="full-error-content">
|
|
1584
|
-
${formatPlaywrightError(
|
|
1651
|
+
${formatPlaywrightError(
|
|
1652
|
+
test.errorMessage || "No detailed error message available"
|
|
1653
|
+
)}
|
|
1585
1654
|
</div>
|
|
1586
1655
|
</div>
|
|
1587
1656
|
</div>
|
|
1588
|
-
|
|
1589
|
-
|
|
1657
|
+
`;
|
|
1658
|
+
})
|
|
1659
|
+
.join("")}
|
|
1590
1660
|
</div>
|
|
1591
1661
|
|
|
1592
1662
|
<!-- AI Fix Modal -->
|
|
@@ -1603,6 +1673,378 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1603
1673
|
</div>
|
|
1604
1674
|
`;
|
|
1605
1675
|
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Generates a area chart showing the total duration per spec file.
|
|
1678
|
+
* The chart is lazy-loaded and rendered with Highcharts when scrolled into view.
|
|
1679
|
+
*
|
|
1680
|
+
* @param {Array<object>} results - Array of test result objects.
|
|
1681
|
+
* @returns {string} HTML string containing the chart container and lazy-loading script.
|
|
1682
|
+
*/
|
|
1683
|
+
function generateSpecDurationChart(results) {
|
|
1684
|
+
if (!results || results.length === 0)
|
|
1685
|
+
return '<div class="no-data">No results available.</div>';
|
|
1686
|
+
|
|
1687
|
+
const specDurations = {};
|
|
1688
|
+
results.forEach((test) => {
|
|
1689
|
+
// Use the dedicated 'spec_file' key
|
|
1690
|
+
const fileName = test.spec_file || "Unknown File";
|
|
1691
|
+
|
|
1692
|
+
if (!specDurations[fileName]) specDurations[fileName] = 0;
|
|
1693
|
+
specDurations[fileName] += test.duration;
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
const categories = Object.keys(specDurations);
|
|
1697
|
+
// We map 'name' here, which we will use in the tooltip later
|
|
1698
|
+
const data = categories.map((cat) => ({
|
|
1699
|
+
y: specDurations[cat],
|
|
1700
|
+
name: cat,
|
|
1701
|
+
}));
|
|
1702
|
+
|
|
1703
|
+
if (categories.length === 0)
|
|
1704
|
+
return '<div class="no-data">No spec data found.</div>';
|
|
1705
|
+
|
|
1706
|
+
const chartId = `specDurChart-${Date.now()}-${Math.random()
|
|
1707
|
+
.toString(36)
|
|
1708
|
+
.substring(2, 7)}`;
|
|
1709
|
+
const renderFunctionName = `renderSpecDurChart_${chartId.replace(/-/g, "_")}`;
|
|
1710
|
+
|
|
1711
|
+
const categoriesStr = JSON.stringify(categories);
|
|
1712
|
+
const dataStr = JSON.stringify(data);
|
|
1713
|
+
|
|
1714
|
+
return `
|
|
1715
|
+
<div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
|
|
1716
|
+
<div class="no-data">Loading Spec Duration Chart...</div>
|
|
1717
|
+
</div>
|
|
1718
|
+
<script>
|
|
1719
|
+
window.${renderFunctionName} = function() {
|
|
1720
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
1721
|
+
if (!chartContainer) return;
|
|
1722
|
+
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
1723
|
+
try {
|
|
1724
|
+
chartContainer.innerHTML = '';
|
|
1725
|
+
Highcharts.chart('${chartId}', {
|
|
1726
|
+
chart: { type: 'area', height: 350, backgroundColor: 'transparent' },
|
|
1727
|
+
title: { text: null },
|
|
1728
|
+
xAxis: {
|
|
1729
|
+
categories: ${categoriesStr},
|
|
1730
|
+
visible: false, // 1. HIDE THE X-AXIS
|
|
1731
|
+
title: { text: null },
|
|
1732
|
+
crosshair: true
|
|
1733
|
+
},
|
|
1734
|
+
yAxis: {
|
|
1735
|
+
min: 0,
|
|
1736
|
+
title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
|
|
1737
|
+
labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
|
|
1738
|
+
},
|
|
1739
|
+
legend: { layout: 'horizontal', align: 'center', verticalAlign: 'bottom', itemStyle: { fontSize: '12px', color: 'var(--text-color)' }},
|
|
1740
|
+
plotOptions: { area: { lineWidth: 2.5, states: { hover: { lineWidthPlus: 0 } }, threshold: null }},
|
|
1741
|
+
tooltip: {
|
|
1742
|
+
shared: true,
|
|
1743
|
+
useHTML: true,
|
|
1744
|
+
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
1745
|
+
borderColor: 'rgba(10,10,10,0.92)',
|
|
1746
|
+
style: { color: '#f5f5f5' },
|
|
1747
|
+
formatter: function() {
|
|
1748
|
+
const point = this.points ? this.points[0].point : this.point;
|
|
1749
|
+
const color = point.color || point.series.color;
|
|
1750
|
+
|
|
1751
|
+
// 2. FIX: Use 'point.name' instead of 'this.x' to get the actual filename
|
|
1752
|
+
return '<span style="color:' + color + '">●</span> <b>File: ' + point.name + '</b><br/>' +
|
|
1753
|
+
'Duration: <b>' + formatDuration(this.y) + '</b>';
|
|
1754
|
+
}
|
|
1755
|
+
},
|
|
1756
|
+
series: [{
|
|
1757
|
+
name: 'Duration',
|
|
1758
|
+
data: ${dataStr},
|
|
1759
|
+
color: 'var(--accent-color-alt)',
|
|
1760
|
+
type: 'area',
|
|
1761
|
+
marker: { symbol: 'circle', enabled: true, radius: 4, states: { hover: { radius: 6, lineWidthPlus: 0 } } },
|
|
1762
|
+
fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(${accentColorAltRGB}, 0.4)'], [1, 'rgba(${accentColorAltRGB}, 0.05)']] },
|
|
1763
|
+
lineWidth: 2.5
|
|
1764
|
+
}],
|
|
1765
|
+
credits: { enabled: false }
|
|
1766
|
+
});
|
|
1767
|
+
} catch (e) { console.error("Error rendering spec chart:", e); }
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
</script>
|
|
1771
|
+
`;
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Generates a vertical bar chart showing the total duration of each test describe block.
|
|
1775
|
+
* Tests without a describe block or with "n/a" / empty describe names are ignored.
|
|
1776
|
+
* @param {Array<object>} results - Array of test result objects.
|
|
1777
|
+
* @returns {string} HTML string containing the chart container and lazy-loading script.
|
|
1778
|
+
*/
|
|
1779
|
+
function generateDescribeDurationChart(results) {
|
|
1780
|
+
if (!results || results.length === 0)
|
|
1781
|
+
return '<div class="no-data">Seems like there is test describe block available in the executed test suite.</div>';
|
|
1782
|
+
|
|
1783
|
+
const describeMap = new Map();
|
|
1784
|
+
let foundAnyDescribe = false;
|
|
1785
|
+
|
|
1786
|
+
results.forEach((test) => {
|
|
1787
|
+
if (test.describe) {
|
|
1788
|
+
const describeName = test.describe;
|
|
1789
|
+
// Filter out invalid describe blocks
|
|
1790
|
+
if (
|
|
1791
|
+
!describeName ||
|
|
1792
|
+
describeName.trim().toLowerCase() === "n/a" ||
|
|
1793
|
+
describeName.trim() === ""
|
|
1794
|
+
) {
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
foundAnyDescribe = true;
|
|
1799
|
+
const fileName = test.spec_file || "Unknown File";
|
|
1800
|
+
const key = fileName + "::" + describeName;
|
|
1801
|
+
|
|
1802
|
+
if (!describeMap.has(key)) {
|
|
1803
|
+
describeMap.set(key, {
|
|
1804
|
+
duration: 0,
|
|
1805
|
+
file: fileName,
|
|
1806
|
+
describe: describeName,
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
describeMap.get(key).duration += test.duration;
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
if (!foundAnyDescribe) {
|
|
1814
|
+
return '<div class="no-data">No valid test describe blocks found.</div>';
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
const categories = [];
|
|
1818
|
+
const data = [];
|
|
1819
|
+
|
|
1820
|
+
for (const [key, val] of describeMap.entries()) {
|
|
1821
|
+
categories.push(val.describe);
|
|
1822
|
+
data.push({
|
|
1823
|
+
y: val.duration,
|
|
1824
|
+
name: val.describe,
|
|
1825
|
+
custom: {
|
|
1826
|
+
fileName: val.file,
|
|
1827
|
+
describeName: val.describe,
|
|
1828
|
+
},
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
const chartId = `descDurChart-${Date.now()}-${Math.random()
|
|
1833
|
+
.toString(36)
|
|
1834
|
+
.substring(2, 7)}`;
|
|
1835
|
+
const renderFunctionName = `renderDescDurChart_${chartId.replace(/-/g, "_")}`;
|
|
1836
|
+
|
|
1837
|
+
const categoriesStr = JSON.stringify(categories);
|
|
1838
|
+
const dataStr = JSON.stringify(data);
|
|
1839
|
+
|
|
1840
|
+
return `
|
|
1841
|
+
<div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
|
|
1842
|
+
<div class="no-data">Loading Describe Duration Chart...</div>
|
|
1843
|
+
</div>
|
|
1844
|
+
<script>
|
|
1845
|
+
window.${renderFunctionName} = function() {
|
|
1846
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
1847
|
+
if (!chartContainer) return;
|
|
1848
|
+
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
1849
|
+
try {
|
|
1850
|
+
chartContainer.innerHTML = '';
|
|
1851
|
+
Highcharts.chart('${chartId}', {
|
|
1852
|
+
chart: {
|
|
1853
|
+
type: 'column', // 1. CHANGED: 'bar' -> 'column' for vertical bars
|
|
1854
|
+
height: 400, // 2. CHANGED: Fixed height works better for vertical charts
|
|
1855
|
+
backgroundColor: 'transparent'
|
|
1856
|
+
},
|
|
1857
|
+
title: { text: null },
|
|
1858
|
+
xAxis: {
|
|
1859
|
+
categories: ${categoriesStr},
|
|
1860
|
+
visible: false, // Hidden as requested
|
|
1861
|
+
title: { text: null },
|
|
1862
|
+
crosshair: true
|
|
1863
|
+
},
|
|
1864
|
+
yAxis: {
|
|
1865
|
+
min: 0,
|
|
1866
|
+
title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
|
|
1867
|
+
labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
|
|
1868
|
+
},
|
|
1869
|
+
legend: { enabled: false },
|
|
1870
|
+
plotOptions: {
|
|
1871
|
+
series: {
|
|
1872
|
+
borderRadius: 4,
|
|
1873
|
+
borderWidth: 0,
|
|
1874
|
+
states: { hover: { brightness: 0.1 }}
|
|
1875
|
+
},
|
|
1876
|
+
column: { pointPadding: 0.2, groupPadding: 0.1 } // Adjust spacing for columns
|
|
1877
|
+
},
|
|
1878
|
+
tooltip: {
|
|
1879
|
+
shared: true,
|
|
1880
|
+
useHTML: true,
|
|
1881
|
+
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
1882
|
+
borderColor: 'rgba(10,10,10,0.92)',
|
|
1883
|
+
style: { color: '#f5f5f5' },
|
|
1884
|
+
formatter: function() {
|
|
1885
|
+
const point = this.points ? this.points[0].point : this.point;
|
|
1886
|
+
const file = (point.custom && point.custom.fileName) ? point.custom.fileName : 'Unknown';
|
|
1887
|
+
const desc = point.name || 'Unknown';
|
|
1888
|
+
const color = point.color || point.series.color;
|
|
1889
|
+
|
|
1890
|
+
return '<span style="color:' + color + '">●</span> <b>Describe: ' + desc + '</b><br/>' +
|
|
1891
|
+
'<span style="opacity: 0.8; font-size: 0.9em; color: #ddd;">File: ' + file + '</span><br/>' +
|
|
1892
|
+
'Duration: <b>' + formatDuration(point.y) + '</b>';
|
|
1893
|
+
}
|
|
1894
|
+
},
|
|
1895
|
+
series: [{
|
|
1896
|
+
name: 'Duration',
|
|
1897
|
+
data: ${dataStr},
|
|
1898
|
+
color: 'var(--accent-color-alt)',
|
|
1899
|
+
}],
|
|
1900
|
+
credits: { enabled: false }
|
|
1901
|
+
});
|
|
1902
|
+
} catch (e) { console.error("Error rendering describe chart:", e); }
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
</script>
|
|
1906
|
+
`;
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Generates a stacked column chart showing test results distributed by severity.
|
|
1910
|
+
* Matches dimensions of the System Environment section (~600px).
|
|
1911
|
+
* Lazy-loaded for performance.
|
|
1912
|
+
*/
|
|
1913
|
+
function generateSeverityDistributionChart(results) {
|
|
1914
|
+
if (!results || results.length === 0) {
|
|
1915
|
+
return '<div class="trend-chart" style="height: 600px;"><div class="no-data">No results available for severity distribution.</div></div>';
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const severityLevels = ["Critical", "High", "Medium", "Low", "Minor"];
|
|
1919
|
+
const data = {
|
|
1920
|
+
passed: [0, 0, 0, 0, 0],
|
|
1921
|
+
failed: [0, 0, 0, 0, 0],
|
|
1922
|
+
skipped: [0, 0, 0, 0, 0],
|
|
1923
|
+
};
|
|
1924
|
+
|
|
1925
|
+
results.forEach((test) => {
|
|
1926
|
+
const sev = test.severity || "Medium";
|
|
1927
|
+
const status = String(test.status).toLowerCase();
|
|
1928
|
+
|
|
1929
|
+
let index = severityLevels.indexOf(sev);
|
|
1930
|
+
if (index === -1) index = 2; // Default to Medium
|
|
1931
|
+
|
|
1932
|
+
if (status === "passed") {
|
|
1933
|
+
data.passed[index]++;
|
|
1934
|
+
} else if (
|
|
1935
|
+
status === "failed" ||
|
|
1936
|
+
status === "timedout" ||
|
|
1937
|
+
status === "interrupted"
|
|
1938
|
+
) {
|
|
1939
|
+
data.failed[index]++;
|
|
1940
|
+
} else {
|
|
1941
|
+
data.skipped[index]++;
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
const chartId = `sevDistChart-${Date.now()}-${Math.random()
|
|
1946
|
+
.toString(36)
|
|
1947
|
+
.substring(2, 7)}`;
|
|
1948
|
+
const renderFunctionName = `renderSevDistChart_${chartId.replace(/-/g, "_")}`;
|
|
1949
|
+
|
|
1950
|
+
const seriesData = [
|
|
1951
|
+
{ name: "Passed", data: data.passed, color: "var(--success-color)" },
|
|
1952
|
+
{ name: "Failed", data: data.failed, color: "var(--danger-color)" },
|
|
1953
|
+
{ name: "Skipped", data: data.skipped, color: "var(--warning-color)" },
|
|
1954
|
+
];
|
|
1955
|
+
|
|
1956
|
+
const seriesDataStr = JSON.stringify(seriesData);
|
|
1957
|
+
const categoriesStr = JSON.stringify(severityLevels);
|
|
1958
|
+
|
|
1959
|
+
return `
|
|
1960
|
+
<div class="trend-chart" style="height: 600px; padding: 28px; box-sizing: border-box;">
|
|
1961
|
+
<h3 class="chart-title-header">Severity Distribution</h3>
|
|
1962
|
+
<div id="${chartId}" class="lazy-load-chart" data-render-function-name="${renderFunctionName}" style="width: 100%; height: 100%;">
|
|
1963
|
+
<div class="no-data">Loading Severity Chart...</div>
|
|
1964
|
+
</div>
|
|
1965
|
+
<script>
|
|
1966
|
+
window.${renderFunctionName} = function() {
|
|
1967
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
1968
|
+
if (!chartContainer) return;
|
|
1969
|
+
|
|
1970
|
+
if (typeof Highcharts !== 'undefined') {
|
|
1971
|
+
try {
|
|
1972
|
+
chartContainer.innerHTML = '';
|
|
1973
|
+
Highcharts.chart('${chartId}', {
|
|
1974
|
+
chart: { type: 'column', backgroundColor: 'transparent' },
|
|
1975
|
+
title: { text: null },
|
|
1976
|
+
xAxis: {
|
|
1977
|
+
categories: ${categoriesStr},
|
|
1978
|
+
crosshair: true,
|
|
1979
|
+
labels: { style: { color: 'var(--text-color-secondary)' } }
|
|
1980
|
+
},
|
|
1981
|
+
yAxis: {
|
|
1982
|
+
min: 0,
|
|
1983
|
+
title: { text: 'Test Count', style: { color: 'var(--text-color)' } },
|
|
1984
|
+
stackLabels: { enabled: true, style: { fontWeight: 'bold', color: 'var(--text-color)' } },
|
|
1985
|
+
labels: { style: { color: 'var(--text-color-secondary)' } }
|
|
1986
|
+
},
|
|
1987
|
+
legend: {
|
|
1988
|
+
itemStyle: { color: 'var(--text-color)' }
|
|
1989
|
+
},
|
|
1990
|
+
tooltip: {
|
|
1991
|
+
shared: true,
|
|
1992
|
+
useHTML: true,
|
|
1993
|
+
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
1994
|
+
style: { color: '#f5f5f5' },
|
|
1995
|
+
formatter: function() {
|
|
1996
|
+
// Custom formatter to HIDE 0 values
|
|
1997
|
+
let tooltip = '';
|
|
1998
|
+
let hasItems = false;
|
|
1999
|
+
|
|
2000
|
+
this.points.forEach(point => {
|
|
2001
|
+
if (point.y > 0) { // ONLY show if count > 0
|
|
2002
|
+
tooltip += '<span style="color:' + point.series.color + '">●</span> ' +
|
|
2003
|
+
point.series.name + ': <b>' + point.y + '</b><br/>';
|
|
2004
|
+
hasItems = true;
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
|
|
2008
|
+
if (!hasItems) return false; // Hide tooltip entirely if no data
|
|
2009
|
+
|
|
2010
|
+
// Calculate total from visible points to ensure accuracy or use stackTotal
|
|
2011
|
+
tooltip += 'Total: ' + this.points[0].total;
|
|
2012
|
+
return tooltip;
|
|
2013
|
+
}
|
|
2014
|
+
},
|
|
2015
|
+
plotOptions: {
|
|
2016
|
+
column: {
|
|
2017
|
+
stacking: 'normal',
|
|
2018
|
+
dataLabels: {
|
|
2019
|
+
enabled: true,
|
|
2020
|
+
color: '#fff',
|
|
2021
|
+
style: { textOutline: 'none' },
|
|
2022
|
+
formatter: function() {
|
|
2023
|
+
return (this.y > 0) ? this.y : null; // Hide 0 labels on chart bars
|
|
2024
|
+
}
|
|
2025
|
+
},
|
|
2026
|
+
borderRadius: 3
|
|
2027
|
+
}
|
|
2028
|
+
},
|
|
2029
|
+
series: ${seriesDataStr},
|
|
2030
|
+
credits: { enabled: false }
|
|
2031
|
+
});
|
|
2032
|
+
} catch(e) {
|
|
2033
|
+
console.error("Error rendering severity chart:", e);
|
|
2034
|
+
chartContainer.innerHTML = '<div class="no-data">Error rendering chart.</div>';
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
</script>
|
|
2039
|
+
</div>
|
|
2040
|
+
`;
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Generates the HTML content for the report.
|
|
2044
|
+
* @param {object} reportData - The report data object containing run and results.
|
|
2045
|
+
* @param {object} trendData - Optional trend data object for additional trends.
|
|
2046
|
+
* @returns {string} HTML string for the report.
|
|
2047
|
+
*/
|
|
1606
2048
|
function generateHTML(reportData, trendData = null) {
|
|
1607
2049
|
const { run, results } = reportData;
|
|
1608
2050
|
const suitesData = getSuitesData(reportData.results || []);
|
|
@@ -1618,7 +2060,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1618
2060
|
const fixPath = (p) => {
|
|
1619
2061
|
if (!p) return "";
|
|
1620
2062
|
// This regex handles both forward slashes and backslashes
|
|
1621
|
-
return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`),
|
|
2063
|
+
return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`), "");
|
|
1622
2064
|
};
|
|
1623
2065
|
|
|
1624
2066
|
const totalTestsOr1 = runSummary.totalTests || 1;
|
|
@@ -1640,6 +2082,28 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1640
2082
|
const testFileParts = test.name.split(" > ");
|
|
1641
2083
|
const testTitle =
|
|
1642
2084
|
testFileParts[testFileParts.length - 1] || "Unnamed Test";
|
|
2085
|
+
// --- NEW: Severity Logic ---
|
|
2086
|
+
const severity = test.severity || "Medium";
|
|
2087
|
+
const getSeverityColor = (level) => {
|
|
2088
|
+
switch (level) {
|
|
2089
|
+
case "Minor":
|
|
2090
|
+
return "#006064";
|
|
2091
|
+
case "Low":
|
|
2092
|
+
return "#FFA07A";
|
|
2093
|
+
case "Medium":
|
|
2094
|
+
return "#577A11";
|
|
2095
|
+
case "High":
|
|
2096
|
+
return "#B71C1C";
|
|
2097
|
+
case "Critical":
|
|
2098
|
+
return "#64158A";
|
|
2099
|
+
default:
|
|
2100
|
+
return "#577A11";
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
const severityColor = getSeverityColor(severity);
|
|
2104
|
+
// We reuse 'status-badge' class for size/font consistency, but override background color
|
|
2105
|
+
const severityBadge = `<span class="status-badge" style="background-color: ${severityColor}; margin-right: 8px;">${severity}</span>`;
|
|
2106
|
+
// ---------------------------
|
|
1643
2107
|
const generateStepsHTML = (steps, depth = 0) => {
|
|
1644
2108
|
if (!steps || steps.length === 0)
|
|
1645
2109
|
return "<div class='no-steps'>No steps recorded for this test.</div>";
|
|
@@ -1663,20 +2127,23 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1663
2127
|
)}</span>
|
|
1664
2128
|
</div>
|
|
1665
2129
|
<div class="step-details" style="display: none;">
|
|
1666
|
-
${
|
|
2130
|
+
${
|
|
2131
|
+
step.codeLocation
|
|
1667
2132
|
? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
|
|
1668
|
-
|
|
1669
|
-
|
|
2133
|
+
step.codeLocation
|
|
2134
|
+
)}</div>`
|
|
1670
2135
|
: ""
|
|
1671
|
-
|
|
1672
|
-
${
|
|
2136
|
+
}
|
|
2137
|
+
${
|
|
2138
|
+
step.errorMessage
|
|
1673
2139
|
? `<div class="test-error-summary">
|
|
1674
|
-
${
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
2140
|
+
${
|
|
2141
|
+
step.stackTrace
|
|
2142
|
+
? `<div class="stack-trace">${formatPlaywrightError(
|
|
2143
|
+
step.stackTrace
|
|
2144
|
+
)}</div>`
|
|
2145
|
+
: ""
|
|
2146
|
+
}
|
|
1680
2147
|
<button
|
|
1681
2148
|
class="copy-error-btn"
|
|
1682
2149
|
onclick="copyErrorToClipboard(this)"
|
|
@@ -1698,14 +2165,15 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1698
2165
|
</button>
|
|
1699
2166
|
</div>`
|
|
1700
2167
|
: ""
|
|
1701
|
-
|
|
1702
|
-
${
|
|
2168
|
+
}
|
|
2169
|
+
${
|
|
2170
|
+
hasNestedSteps
|
|
1703
2171
|
? `<div class="nested-steps">${generateStepsHTML(
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
2172
|
+
step.steps,
|
|
2173
|
+
depth + 1
|
|
2174
|
+
)}</div>`
|
|
1707
2175
|
: ""
|
|
1708
|
-
|
|
2176
|
+
}
|
|
1709
2177
|
</div>
|
|
1710
2178
|
</div>`;
|
|
1711
2179
|
})
|
|
@@ -1713,41 +2181,87 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1713
2181
|
};
|
|
1714
2182
|
|
|
1715
2183
|
return `
|
|
1716
|
-
<div class="test-case" data-status="${
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
2184
|
+
<div class="test-case" data-status="${
|
|
2185
|
+
test.status
|
|
2186
|
+
}" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
|
|
2187
|
+
.join(",")
|
|
2188
|
+
.toLowerCase()}">
|
|
1720
2189
|
<div class="test-case-header" role="button" aria-expanded="false">
|
|
1721
2190
|
<div class="test-case-summary">
|
|
1722
2191
|
<span class="status-badge ${getStatusClass(test.status)}">${String(
|
|
1723
|
-
|
|
1724
|
-
|
|
2192
|
+
test.status
|
|
2193
|
+
).toUpperCase()}</span>
|
|
1725
2194
|
<span class="test-case-title" title="${sanitizeHTML(
|
|
1726
2195
|
test.name
|
|
1727
2196
|
)}">${sanitizeHTML(testTitle)}</span>
|
|
1728
2197
|
<span class="test-case-browser">(${sanitizeHTML(browser)})</span>
|
|
1729
2198
|
</div>
|
|
1730
2199
|
<div class="test-case-meta">
|
|
1731
|
-
${
|
|
1732
|
-
|
|
1733
|
-
.
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
2200
|
+
${severityBadge}
|
|
2201
|
+
${
|
|
2202
|
+
test.tags && test.tags.length > 0
|
|
2203
|
+
? test.tags
|
|
2204
|
+
.map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
|
|
2205
|
+
.join(" ")
|
|
2206
|
+
: ""
|
|
2207
|
+
}
|
|
1737
2208
|
<span class="test-duration">${formatDuration(test.duration)}</span>
|
|
1738
2209
|
</div>
|
|
1739
2210
|
</div>
|
|
1740
2211
|
<div class="test-case-content" style="display: none;">
|
|
1741
2212
|
<p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
|
|
2213
|
+
${
|
|
2214
|
+
test.annotations && test.annotations.length > 0
|
|
2215
|
+
? `<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;">
|
|
2216
|
+
<h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
|
|
2217
|
+
${test.annotations
|
|
2218
|
+
.map((annotation, idx) => {
|
|
2219
|
+
const isIssueOrBug =
|
|
2220
|
+
annotation.type === "issue" ||
|
|
2221
|
+
annotation.type === "bug";
|
|
2222
|
+
const descriptionText = annotation.description || "";
|
|
2223
|
+
const typeLabel = sanitizeHTML(annotation.type);
|
|
2224
|
+
const descriptionHtml =
|
|
2225
|
+
isIssueOrBug && descriptionText.match(/^[A-Z]+-\d+$/)
|
|
2226
|
+
? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
|
|
2227
|
+
descriptionText
|
|
2228
|
+
)}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
|
|
2229
|
+
descriptionText
|
|
2230
|
+
)}</a>`
|
|
2231
|
+
: sanitizeHTML(descriptionText);
|
|
2232
|
+
const locationText = annotation.location
|
|
2233
|
+
? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
|
|
2234
|
+
annotation.location.file
|
|
2235
|
+
)}:${annotation.location.line}:${
|
|
2236
|
+
annotation.location.column
|
|
2237
|
+
}</div>`
|
|
2238
|
+
: "";
|
|
2239
|
+
return `<div style="margin-bottom: ${
|
|
2240
|
+
idx < test.annotations.length - 1 ? "10px" : "0"
|
|
2241
|
+
};">
|
|
2242
|
+
<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>
|
|
2243
|
+
${
|
|
2244
|
+
descriptionText
|
|
2245
|
+
? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
|
|
2246
|
+
: ""
|
|
2247
|
+
}
|
|
2248
|
+
${locationText}
|
|
2249
|
+
</div>`;
|
|
2250
|
+
})
|
|
2251
|
+
.join("")}
|
|
2252
|
+
</div>`
|
|
2253
|
+
: ""
|
|
2254
|
+
}
|
|
1742
2255
|
<p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
|
|
1743
2256
|
test.workerId
|
|
1744
2257
|
)} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
${
|
|
1748
|
-
|
|
1749
|
-
test
|
|
1750
|
-
|
|
2258
|
+
test.totalWorkers
|
|
2259
|
+
)}]</p>
|
|
2260
|
+
${
|
|
2261
|
+
test.errorMessage
|
|
2262
|
+
? `<div class="test-error-summary">${formatPlaywrightError(
|
|
2263
|
+
test.errorMessage
|
|
2264
|
+
)}
|
|
1751
2265
|
<button
|
|
1752
2266
|
class="copy-error-btn"
|
|
1753
2267
|
onclick="copyErrorToClipboard(this)"
|
|
@@ -1768,13 +2282,14 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1768
2282
|
Copy Error Prompt
|
|
1769
2283
|
</button>
|
|
1770
2284
|
</div>`
|
|
1771
|
-
|
|
2285
|
+
: ""
|
|
1772
2286
|
}
|
|
1773
|
-
${
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
2287
|
+
${
|
|
2288
|
+
test.snippet
|
|
2289
|
+
? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
|
|
2290
|
+
test.snippet
|
|
2291
|
+
)}</code></pre></div>`
|
|
2292
|
+
: ""
|
|
1778
2293
|
}
|
|
1779
2294
|
<h4>Steps</h4>
|
|
1780
2295
|
<div class="steps-list">${generateStepsHTML(test.steps)}</div>
|
|
@@ -1793,75 +2308,86 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1793
2308
|
</div>
|
|
1794
2309
|
</div>`;
|
|
1795
2310
|
})()}
|
|
1796
|
-
${
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
2311
|
+
${
|
|
2312
|
+
test.stderr && test.stderr.length > 0
|
|
2313
|
+
? `<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(
|
|
2314
|
+
test.stderr.map((line) => sanitizeHTML(line)).join("\n")
|
|
2315
|
+
)}</pre></div>`
|
|
2316
|
+
: ""
|
|
1801
2317
|
}
|
|
1802
|
-
${
|
|
1803
|
-
|
|
2318
|
+
${
|
|
2319
|
+
test.screenshots && test.screenshots.length > 0
|
|
2320
|
+
? `
|
|
1804
2321
|
<div class="attachments-section">
|
|
1805
2322
|
<h4>Screenshots</h4>
|
|
1806
2323
|
<div class="attachments-grid">
|
|
1807
2324
|
${test.screenshots
|
|
1808
|
-
|
|
1809
|
-
|
|
2325
|
+
.map(
|
|
2326
|
+
(screenshot, index) => `
|
|
1810
2327
|
<div class="attachment-item">
|
|
1811
|
-
<img src="${fixPath(screenshot)}" alt="Screenshot ${
|
|
2328
|
+
<img src="${fixPath(screenshot)}" alt="Screenshot ${
|
|
2329
|
+
index + 1
|
|
2330
|
+
}">
|
|
1812
2331
|
<div class="attachment-info">
|
|
1813
2332
|
<div class="trace-actions">
|
|
1814
|
-
<a href="${fixPath(
|
|
1815
|
-
|
|
2333
|
+
<a href="${fixPath(
|
|
2334
|
+
screenshot
|
|
2335
|
+
)}" target="_blank" class="view-full">View Full Image</a>
|
|
2336
|
+
<a href="${fixPath(
|
|
2337
|
+
screenshot
|
|
2338
|
+
)}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
|
|
1816
2339
|
</div>
|
|
1817
2340
|
</div>
|
|
1818
2341
|
</div>
|
|
1819
2342
|
`
|
|
1820
|
-
|
|
1821
|
-
|
|
2343
|
+
)
|
|
2344
|
+
.join("")}
|
|
1822
2345
|
</div>
|
|
1823
2346
|
</div>
|
|
1824
2347
|
`
|
|
1825
|
-
|
|
2348
|
+
: ""
|
|
1826
2349
|
}
|
|
1827
|
-
${
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2350
|
+
${
|
|
2351
|
+
test.videoPath && test.videoPath.length > 0
|
|
2352
|
+
? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
|
|
2353
|
+
.map((videoUrl, index) => {
|
|
2354
|
+
const fixedVideoUrl = fixPath(videoUrl);
|
|
2355
|
+
const fileExtension = String(fixedVideoUrl)
|
|
2356
|
+
.split(".")
|
|
2357
|
+
.pop()
|
|
2358
|
+
.toLowerCase();
|
|
2359
|
+
const mimeType =
|
|
2360
|
+
{
|
|
2361
|
+
mp4: "video/mp4",
|
|
2362
|
+
webm: "video/webm",
|
|
2363
|
+
ogg: "video/ogg",
|
|
2364
|
+
mov: "video/quicktime",
|
|
2365
|
+
avi: "video/x-msvideo",
|
|
2366
|
+
}[fileExtension] || "video/mp4";
|
|
2367
|
+
return `<div class="attachment-item video-item">
|
|
2368
|
+
<video controls width="100%" height="auto" title="Video ${
|
|
2369
|
+
index + 1
|
|
2370
|
+
}">
|
|
1846
2371
|
<source src="${sanitizeHTML(
|
|
1847
|
-
|
|
1848
|
-
|
|
2372
|
+
fixedVideoUrl
|
|
2373
|
+
)}" type="${mimeType}">
|
|
1849
2374
|
Your browser does not support the video tag.
|
|
1850
2375
|
</video>
|
|
1851
2376
|
<div class="attachment-info">
|
|
1852
2377
|
<div class="trace-actions">
|
|
1853
2378
|
<a href="${sanitizeHTML(
|
|
1854
|
-
|
|
1855
|
-
|
|
2379
|
+
fixedVideoUrl
|
|
2380
|
+
)}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
|
|
1856
2381
|
</div>
|
|
1857
2382
|
</div>
|
|
1858
2383
|
</div>`;
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
2384
|
+
})
|
|
2385
|
+
.join("")}</div></div>`
|
|
2386
|
+
: ""
|
|
1862
2387
|
}
|
|
1863
|
-
${
|
|
1864
|
-
|
|
2388
|
+
${
|
|
2389
|
+
test.tracePath
|
|
2390
|
+
? `
|
|
1865
2391
|
<div class="attachments-section">
|
|
1866
2392
|
<h4>Trace Files</h4>
|
|
1867
2393
|
<div class="attachments-grid">
|
|
@@ -1869,70 +2395,72 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1869
2395
|
<div class="trace-preview">
|
|
1870
2396
|
<span class="trace-icon">📄</span>
|
|
1871
2397
|
<span class="trace-name">${sanitizeHTML(
|
|
1872
|
-
|
|
1873
|
-
|
|
2398
|
+
path.basename(test.tracePath)
|
|
2399
|
+
)}</span>
|
|
1874
2400
|
</div>
|
|
1875
2401
|
<div class="attachment-info">
|
|
1876
2402
|
<div class="trace-actions">
|
|
1877
2403
|
<a href="${sanitizeHTML(
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
2404
|
+
fixPath(test.tracePath)
|
|
2405
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
2406
|
+
path.basename(test.tracePath)
|
|
2407
|
+
)}" class="download-trace">Download Trace</a>
|
|
1882
2408
|
</div>
|
|
1883
2409
|
</div>
|
|
1884
2410
|
</div>
|
|
1885
2411
|
</div>
|
|
1886
2412
|
</div>
|
|
1887
2413
|
`
|
|
1888
|
-
|
|
2414
|
+
: ""
|
|
1889
2415
|
}
|
|
1890
|
-
${
|
|
1891
|
-
|
|
2416
|
+
${
|
|
2417
|
+
test.attachments && test.attachments.length > 0
|
|
2418
|
+
? `
|
|
1892
2419
|
<div class="attachments-section">
|
|
1893
2420
|
<h4>Other Attachments</h4>
|
|
1894
2421
|
<div class="attachments-grid">
|
|
1895
2422
|
${test.attachments
|
|
1896
|
-
|
|
1897
|
-
|
|
2423
|
+
.map(
|
|
2424
|
+
(attachment) => `
|
|
1898
2425
|
<div class="attachment-item generic-attachment">
|
|
1899
2426
|
<div class="attachment-icon">${getAttachmentIcon(
|
|
1900
|
-
|
|
1901
|
-
|
|
2427
|
+
attachment.contentType
|
|
2428
|
+
)}</div>
|
|
1902
2429
|
<div class="attachment-caption">
|
|
1903
2430
|
<span class="attachment-name" title="${sanitizeHTML(
|
|
1904
|
-
|
|
1905
|
-
|
|
2431
|
+
attachment.name
|
|
2432
|
+
)}">${sanitizeHTML(attachment.name)}</span>
|
|
1906
2433
|
<span class="attachment-type">${sanitizeHTML(
|
|
1907
|
-
|
|
1908
|
-
|
|
2434
|
+
attachment.contentType
|
|
2435
|
+
)}</span>
|
|
1909
2436
|
</div>
|
|
1910
2437
|
<div class="attachment-info">
|
|
1911
2438
|
<div class="trace-actions">
|
|
1912
2439
|
<a href="${sanitizeHTML(
|
|
1913
|
-
|
|
1914
|
-
|
|
2440
|
+
fixPath(attachment.path)
|
|
2441
|
+
)}" target="_blank" class="view-full">View</a>
|
|
1915
2442
|
<a href="${sanitizeHTML(
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2443
|
+
fixPath(attachment.path)
|
|
2444
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
2445
|
+
attachment.name
|
|
2446
|
+
)}" class="download-trace">Download</a>
|
|
1920
2447
|
</div>
|
|
1921
2448
|
</div>
|
|
1922
2449
|
</div>
|
|
1923
2450
|
`
|
|
1924
|
-
|
|
1925
|
-
|
|
2451
|
+
)
|
|
2452
|
+
.join("")}
|
|
1926
2453
|
</div>
|
|
1927
2454
|
</div>
|
|
1928
2455
|
`
|
|
1929
|
-
|
|
2456
|
+
: ""
|
|
1930
2457
|
}
|
|
1931
|
-
${
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2458
|
+
${
|
|
2459
|
+
test.codeSnippet
|
|
2460
|
+
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
|
|
2461
|
+
sanitizeHTML(test.codeSnippet)
|
|
2462
|
+
)}</code></pre></div>`
|
|
2463
|
+
: ""
|
|
1936
2464
|
}
|
|
1937
2465
|
</div>
|
|
1938
2466
|
</div>`;
|
|
@@ -1945,10 +2473,10 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1945
2473
|
<head>
|
|
1946
2474
|
<meta charset="UTF-8">
|
|
1947
2475
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1948
|
-
<link rel="icon" type="image/png" href="https://
|
|
1949
|
-
<link rel="apple-touch-icon" href="https://
|
|
2476
|
+
<link rel="icon" type="image/png" href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png">
|
|
2477
|
+
<link rel="apple-touch-icon" href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png">
|
|
1950
2478
|
<script src="https://code.highcharts.com/highcharts.js" defer></script>
|
|
1951
|
-
<title>
|
|
2479
|
+
<title>Pulse Report</title>
|
|
1952
2480
|
<style>
|
|
1953
2481
|
:root {
|
|
1954
2482
|
--primary-color: #3f51b5; --secondary-color: #ff4081; --accent-color: #673ab7; --accent-color-alt: #FF9800;
|
|
@@ -1989,7 +2517,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1989
2517
|
.status-passed .value, .stat-passed svg { color: var(--success-color); }
|
|
1990
2518
|
.status-failed .value, .stat-failed svg { color: var(--danger-color); }
|
|
1991
2519
|
.status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
|
|
1992
|
-
.dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items:
|
|
2520
|
+
.dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: start; }
|
|
1993
2521
|
.pie-chart-wrapper, .suites-widget, .trend-chart { background-color: var(--card-background-color); padding: 28px; border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); display: flex; flex-direction: column; }
|
|
1994
2522
|
.pie-chart-wrapper h3, .suites-header h2, .trend-chart h3 { text-align: center; margin-top: 0; margin-bottom: 25px; font-size: 1.25em; font-weight: 600; color: var(--text-color); }
|
|
1995
2523
|
.trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { flex-grow: 1; min-height: 250px; }
|
|
@@ -2092,6 +2620,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2092
2620
|
.status-badge-small.status-failed { background-color: var(--danger-color); }
|
|
2093
2621
|
.status-badge-small.status-skipped { background-color: var(--warning-color); }
|
|
2094
2622
|
.status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
|
|
2623
|
+
.badge-severity { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; color: white; text-transform: uppercase; margin-right: 8px; vertical-align: middle; }
|
|
2095
2624
|
.no-data, .no-tests, .no-steps, .no-data-chart { padding: 28px; text-align: center; color: var(--dark-gray-color); font-style: italic; font-size:1.1em; background-color: var(--light-gray-color); border-radius: var(--border-radius); margin: 18px 0; border: 1px dashed var(--medium-gray-color); }
|
|
2096
2625
|
.no-data-chart {font-size: 0.95em; padding: 18px;}
|
|
2097
2626
|
.ai-failure-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 22px; }
|
|
@@ -2238,6 +2767,35 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2238
2767
|
.ai-text {
|
|
2239
2768
|
font-size: 0.95em;
|
|
2240
2769
|
}
|
|
2770
|
+
.ai-buttons-group {
|
|
2771
|
+
display: flex;
|
|
2772
|
+
gap: 10px;
|
|
2773
|
+
flex-wrap: wrap;
|
|
2774
|
+
}
|
|
2775
|
+
.copy-prompt-btn {
|
|
2776
|
+
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
|
2777
|
+
color: white;
|
|
2778
|
+
border: none;
|
|
2779
|
+
padding: 12px 18px;
|
|
2780
|
+
border-radius: 6px;
|
|
2781
|
+
cursor: pointer;
|
|
2782
|
+
font-weight: 600;
|
|
2783
|
+
display: flex;
|
|
2784
|
+
align-items: center;
|
|
2785
|
+
gap: 8px;
|
|
2786
|
+
transition: all 0.3s ease;
|
|
2787
|
+
white-space: nowrap;
|
|
2788
|
+
}
|
|
2789
|
+
.copy-prompt-btn:hover {
|
|
2790
|
+
transform: translateY(-2px);
|
|
2791
|
+
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.4);
|
|
2792
|
+
}
|
|
2793
|
+
.copy-prompt-btn.copied {
|
|
2794
|
+
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
2795
|
+
}
|
|
2796
|
+
.copy-prompt-text {
|
|
2797
|
+
font-size: 0.95em;
|
|
2798
|
+
}
|
|
2241
2799
|
.failure-error-preview {
|
|
2242
2800
|
padding: 0 20px 18px 20px;
|
|
2243
2801
|
border-top: 1px solid var(--light-gray-color);
|
|
@@ -2313,9 +2871,14 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2313
2871
|
.failure-meta {
|
|
2314
2872
|
justify-content: center;
|
|
2315
2873
|
}
|
|
2316
|
-
.
|
|
2874
|
+
.ai-buttons-group {
|
|
2875
|
+
flex-direction: column;
|
|
2876
|
+
width: 100%;
|
|
2877
|
+
}
|
|
2878
|
+
.compact-ai-btn, .copy-prompt-btn {
|
|
2317
2879
|
justify-content: center;
|
|
2318
2880
|
padding: 12px 20px;
|
|
2881
|
+
width: 100%;
|
|
2319
2882
|
}
|
|
2320
2883
|
}
|
|
2321
2884
|
@media (max-width: 480px) {
|
|
@@ -2341,12 +2904,12 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2341
2904
|
<div class="container">
|
|
2342
2905
|
<header class="header">
|
|
2343
2906
|
<div class="header-title">
|
|
2344
|
-
<img id="report-logo" src="https://
|
|
2345
|
-
<h1>
|
|
2907
|
+
<img id="report-logo" src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png" alt="Report Logo">
|
|
2908
|
+
<h1>Pulse Report</h1>
|
|
2346
2909
|
</div>
|
|
2347
2910
|
<div class="run-info"><strong>Run Date:</strong> ${formatDate(
|
|
2348
|
-
|
|
2349
|
-
|
|
2911
|
+
runSummary.timestamp
|
|
2912
|
+
)}<br><strong>Total Duration:</strong> ${formatDuration(
|
|
2350
2913
|
runSummary.duration
|
|
2351
2914
|
)}</div>
|
|
2352
2915
|
</header>
|
|
@@ -2358,55 +2921,64 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2358
2921
|
</div>
|
|
2359
2922
|
<div id="dashboard" class="tab-content active">
|
|
2360
2923
|
<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
|
-
|
|
2924
|
+
<div class="summary-card"><h3>Total Tests</h3><div class="value">${
|
|
2925
|
+
runSummary.totalTests
|
|
2926
|
+
}</div></div>
|
|
2927
|
+
<div class="summary-card status-passed"><h3>Passed</h3><div class="value">${
|
|
2928
|
+
runSummary.passed
|
|
2929
|
+
}</div><div class="trend-percentage">${passPercentage}%</div></div>
|
|
2930
|
+
<div class="summary-card status-failed"><h3>Failed</h3><div class="value">${
|
|
2931
|
+
runSummary.failed
|
|
2932
|
+
}</div><div class="trend-percentage">${failPercentage}%</div></div>
|
|
2933
|
+
<div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
|
|
2934
|
+
runSummary.skipped || 0
|
|
2935
|
+
}</div><div class="trend-percentage">${skipPercentage}%</div></div>
|
|
2369
2936
|
<div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
|
|
2370
2937
|
<div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
|
|
2371
|
-
|
|
2372
|
-
|
|
2938
|
+
runSummary.duration
|
|
2939
|
+
)}</div></div>
|
|
2373
2940
|
</div>
|
|
2374
2941
|
<div class="dashboard-bottom-row">
|
|
2375
|
-
<div style="display:
|
|
2942
|
+
<div style="display: flex; flex-direction: column; gap: 28px;">
|
|
2376
2943
|
${generatePieChart(
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
${
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2944
|
+
[
|
|
2945
|
+
{ label: "Passed", value: runSummary.passed },
|
|
2946
|
+
{ label: "Failed", value: runSummary.failed },
|
|
2947
|
+
{ label: "Skipped", value: runSummary.skipped || 0 },
|
|
2948
|
+
],
|
|
2949
|
+
400,
|
|
2950
|
+
390
|
|
2951
|
+
)}
|
|
2952
|
+
${
|
|
2953
|
+
runSummary.environment &&
|
|
2954
|
+
Object.keys(runSummary.environment).length > 0
|
|
2955
|
+
? generateEnvironmentDashboard(runSummary.environment)
|
|
2956
|
+
: '<div class="no-data">Environment data not available.</div>'
|
|
2957
|
+
}
|
|
2390
2958
|
</div>
|
|
2959
|
+
|
|
2960
|
+
<div style="display: flex; flex-direction: column; gap: 28px;">
|
|
2391
2961
|
${generateSuitesWidget(suitesData)}
|
|
2962
|
+
${generateSeverityDistributionChart(results)}
|
|
2963
|
+
</div>
|
|
2392
2964
|
</div>
|
|
2393
|
-
|
|
2965
|
+
</div>
|
|
2394
2966
|
<div id="test-runs" class="tab-content">
|
|
2395
2967
|
<div class="filters">
|
|
2396
2968
|
<input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
|
|
2397
2969
|
<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
2970
|
<select id="filter-browser"><option value="">All Browsers</option>${Array.from(
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2971
|
+
new Set(
|
|
2972
|
+
(results || []).map((test) => test.browser || "unknown")
|
|
2973
|
+
)
|
|
2974
|
+
)
|
|
2975
|
+
.map(
|
|
2976
|
+
(browser) =>
|
|
2977
|
+
`<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
|
|
2978
|
+
browser
|
|
2979
|
+
)}</option>`
|
|
2980
|
+
)
|
|
2981
|
+
.join("")}</select>
|
|
2410
2982
|
<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
2983
|
</div>
|
|
2412
2984
|
<div class="test-cases-list">${generateTestCasesHTML()}</div>
|
|
@@ -2415,16 +2987,28 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2415
2987
|
<h2 class="tab-main-title">Execution Trends</h2>
|
|
2416
2988
|
<div class="trend-charts-row">
|
|
2417
2989
|
<div class="trend-chart"><h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
|
|
2418
|
-
${
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2990
|
+
${
|
|
2991
|
+
trendData && trendData.overall && trendData.overall.length > 0
|
|
2992
|
+
? generateTestTrendsChart(trendData)
|
|
2993
|
+
: '<div class="no-data">Overall trend data not available for test counts.</div>'
|
|
2994
|
+
}
|
|
2422
2995
|
</div>
|
|
2423
2996
|
<div class="trend-chart"><h3 class="chart-title-header">Execution Duration Trends</h3>
|
|
2424
|
-
${
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2997
|
+
${
|
|
2998
|
+
trendData && trendData.overall && trendData.overall.length > 0
|
|
2999
|
+
? generateDurationTrendChart(trendData)
|
|
3000
|
+
: '<div class="no-data">Overall trend data not available for durations.</div>'
|
|
3001
|
+
}
|
|
3002
|
+
</div>
|
|
3003
|
+
</div>
|
|
3004
|
+
<div class="trend-charts-row">
|
|
3005
|
+
<div class="trend-chart">
|
|
3006
|
+
<h3 class="chart-title-header">Duration by Spec files</h3>
|
|
3007
|
+
${generateSpecDurationChart(results)}
|
|
3008
|
+
</div>
|
|
3009
|
+
<div class="trend-chart">
|
|
3010
|
+
<h3 class="chart-title-header">Duration by Test Describe</h3>
|
|
3011
|
+
${generateDescribeDurationChart(results)}
|
|
2428
3012
|
</div>
|
|
2429
3013
|
</div>
|
|
2430
3014
|
<h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
|
|
@@ -2434,12 +3018,13 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2434
3018
|
</div>
|
|
2435
3019
|
</div>
|
|
2436
3020
|
<h2 class="tab-main-title">Individual Test History</h2>
|
|
2437
|
-
${
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
3021
|
+
${
|
|
3022
|
+
trendData &&
|
|
3023
|
+
trendData.testRuns &&
|
|
3024
|
+
Object.keys(trendData.testRuns).length > 0
|
|
3025
|
+
? generateTestHistoryContent(trendData)
|
|
3026
|
+
: '<div class="no-data">Individual test history data not available.</div>'
|
|
3027
|
+
}
|
|
2443
3028
|
</div>
|
|
2444
3029
|
<div id="ai-failure-analyzer" class="tab-content">
|
|
2445
3030
|
${generateAIFailureAnalyzerTab(results)}
|
|
@@ -2582,6 +3167,81 @@ function getAIFix(button) {
|
|
|
2582
3167
|
}
|
|
2583
3168
|
|
|
2584
3169
|
|
|
3170
|
+
function copyAIPrompt(button) {
|
|
3171
|
+
try {
|
|
3172
|
+
const testJson = button.dataset.testJson;
|
|
3173
|
+
const test = JSON.parse(atob(testJson));
|
|
3174
|
+
|
|
3175
|
+
const testName = test.name || 'Unknown Test';
|
|
3176
|
+
const failureLogsAndErrors = [
|
|
3177
|
+
'Error Message:',
|
|
3178
|
+
test.errorMessage || 'Not available.',
|
|
3179
|
+
'\\n\\n--- stdout ---',
|
|
3180
|
+
(test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
|
|
3181
|
+
'\\n\\n--- stderr ---',
|
|
3182
|
+
(test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
|
|
3183
|
+
].join('\\n');
|
|
3184
|
+
const codeSnippet = test.snippet || '';
|
|
3185
|
+
|
|
3186
|
+
const aiPrompt = \`You are an expert Playwright test automation engineer specializing in debugging test failures.
|
|
3187
|
+
|
|
3188
|
+
INSTRUCTIONS:
|
|
3189
|
+
1. Analyze the test failure carefully
|
|
3190
|
+
2. Provide a brief root cause analysis
|
|
3191
|
+
3. Provide EXACTLY 5 specific, actionable fixes
|
|
3192
|
+
4. Each fix MUST include a code snippet (codeSnippet field)
|
|
3193
|
+
5. Return ONLY valid JSON, no markdown or extra text
|
|
3194
|
+
|
|
3195
|
+
REQUIRED JSON FORMAT:
|
|
3196
|
+
{
|
|
3197
|
+
"rootCause": "Brief explanation of why the test failed",
|
|
3198
|
+
"suggestedFixes": [
|
|
3199
|
+
{
|
|
3200
|
+
"description": "Clear explanation of the fix",
|
|
3201
|
+
"codeSnippet": "await page.waitForSelector('.button', { timeout: 5000 });"
|
|
3202
|
+
}
|
|
3203
|
+
],
|
|
3204
|
+
"affectedTests": ["test1", "test2"]
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
IMPORTANT:
|
|
3208
|
+
- Always return valid JSON only
|
|
3209
|
+
- Always provide exactly 5 fixes in suggestedFixes array
|
|
3210
|
+
- Each fix must have both description and codeSnippet fields
|
|
3211
|
+
- Make code snippets practical and Playwright-specific
|
|
3212
|
+
|
|
3213
|
+
---
|
|
3214
|
+
|
|
3215
|
+
Test Name: \${testName}
|
|
3216
|
+
|
|
3217
|
+
Failure Logs and Errors:
|
|
3218
|
+
\${failureLogsAndErrors}
|
|
3219
|
+
|
|
3220
|
+
Code Snippet:
|
|
3221
|
+
\${codeSnippet}\`;
|
|
3222
|
+
|
|
3223
|
+
navigator.clipboard.writeText(aiPrompt).then(() => {
|
|
3224
|
+
const originalText = button.querySelector('.copy-prompt-text').textContent;
|
|
3225
|
+
button.querySelector('.copy-prompt-text').textContent = 'Copied!';
|
|
3226
|
+
button.classList.add('copied');
|
|
3227
|
+
|
|
3228
|
+
const shortTestName = testName.split(' > ').pop() || testName;
|
|
3229
|
+
alert(\`AI prompt to generate a suggested fix for "\${shortTestName}" has been copied to your clipboard.\`);
|
|
3230
|
+
|
|
3231
|
+
setTimeout(() => {
|
|
3232
|
+
button.querySelector('.copy-prompt-text').textContent = originalText;
|
|
3233
|
+
button.classList.remove('copied');
|
|
3234
|
+
}, 2000);
|
|
3235
|
+
}).catch(err => {
|
|
3236
|
+
console.error('Failed to copy AI prompt:', err);
|
|
3237
|
+
alert('Failed to copy AI prompt to clipboard. Please try again.');
|
|
3238
|
+
});
|
|
3239
|
+
} catch (e) {
|
|
3240
|
+
console.error('Error processing test data for AI Prompt copy:', e);
|
|
3241
|
+
alert('Could not process test data. Please try again.');
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
|
|
2585
3245
|
function closeAiModal() {
|
|
2586
3246
|
const modal = document.getElementById('ai-fix-modal');
|
|
2587
3247
|
if(modal) modal.style.display = 'none';
|
|
@@ -2702,6 +3362,19 @@ function getAIFix(button) {
|
|
|
2702
3362
|
}
|
|
2703
3363
|
if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
|
|
2704
3364
|
if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
|
|
3365
|
+
// --- Annotation Link Handler ---
|
|
3366
|
+
document.querySelectorAll('a.annotation-link').forEach(link => {
|
|
3367
|
+
link.addEventListener('click', (e) => {
|
|
3368
|
+
e.preventDefault();
|
|
3369
|
+
const annotationId = link.dataset.annotation;
|
|
3370
|
+
if (annotationId) {
|
|
3371
|
+
const jiraUrl = prompt('Enter your JIRA/Ticket system base URL (e.g., https://your-company.atlassian.net/browse/):', 'https://your-company.atlassian.net/browse/');
|
|
3372
|
+
if (jiraUrl) {
|
|
3373
|
+
window.open(jiraUrl + annotationId, '_blank');
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
});
|
|
3377
|
+
});
|
|
2705
3378
|
// --- Intersection Observer for Lazy Loading ---
|
|
2706
3379
|
const lazyLoadElements = document.querySelectorAll('.lazy-load-chart');
|
|
2707
3380
|
if ('IntersectionObserver' in window) {
|
|
@@ -2817,10 +3490,10 @@ function copyErrorToClipboard(button) {
|
|
|
2817
3490
|
</html>
|
|
2818
3491
|
`;
|
|
2819
3492
|
}
|
|
2820
|
-
async function runScript(scriptPath) {
|
|
3493
|
+
async function runScript(scriptPath, args = []) {
|
|
2821
3494
|
return new Promise((resolve, reject) => {
|
|
2822
3495
|
console.log(chalk.blue(`Executing script: ${scriptPath}...`));
|
|
2823
|
-
const process = fork(scriptPath,
|
|
3496
|
+
const process = fork(scriptPath, args, {
|
|
2824
3497
|
stdio: "inherit",
|
|
2825
3498
|
});
|
|
2826
3499
|
|
|
@@ -2845,13 +3518,22 @@ async function main() {
|
|
|
2845
3518
|
const __filename = fileURLToPath(import.meta.url);
|
|
2846
3519
|
const __dirname = path.dirname(__filename);
|
|
2847
3520
|
|
|
3521
|
+
const args = process.argv.slice(2);
|
|
3522
|
+
let customOutputDir = null;
|
|
3523
|
+
for (let i = 0; i < args.length; i++) {
|
|
3524
|
+
if (args[i] === "--outputDir" || args[i] === "-o") {
|
|
3525
|
+
customOutputDir = args[i + 1];
|
|
3526
|
+
break;
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
|
|
2848
3530
|
// Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
|
|
2849
3531
|
const archiveRunScriptPath = path.resolve(
|
|
2850
3532
|
__dirname,
|
|
2851
3533
|
"generate-trend.mjs" // Keeping the filename as per your request
|
|
2852
3534
|
);
|
|
2853
3535
|
|
|
2854
|
-
const outputDir =
|
|
3536
|
+
const outputDir = await getOutputDir(customOutputDir);
|
|
2855
3537
|
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
|
|
2856
3538
|
const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
|
|
2857
3539
|
|
|
@@ -2861,10 +3543,18 @@ async function main() {
|
|
|
2861
3543
|
|
|
2862
3544
|
console.log(chalk.blue(`Starting static HTML report generation...`));
|
|
2863
3545
|
console.log(chalk.blue(`Output directory set to: ${outputDir}`));
|
|
3546
|
+
if (customOutputDir) {
|
|
3547
|
+
console.log(chalk.gray(` (from CLI argument)`));
|
|
3548
|
+
} else {
|
|
3549
|
+
console.log(
|
|
3550
|
+
chalk.gray(` (auto-detected from playwright.config or using default)`)
|
|
3551
|
+
);
|
|
3552
|
+
}
|
|
2864
3553
|
|
|
2865
3554
|
// Step 1: Ensure current run data is archived to the history folder
|
|
2866
3555
|
try {
|
|
2867
|
-
|
|
3556
|
+
const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
|
|
3557
|
+
await runScript(archiveRunScriptPath, archiveArgs);
|
|
2868
3558
|
console.log(
|
|
2869
3559
|
chalk.green("Current run data archiving to history completed.")
|
|
2870
3560
|
);
|
|
@@ -3031,4 +3721,4 @@ main().catch((err) => {
|
|
|
3031
3721
|
);
|
|
3032
3722
|
console.error(err.stack);
|
|
3033
3723
|
process.exit(1);
|
|
3034
|
-
});
|
|
3724
|
+
});
|