@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.
@@ -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: ${chartHeight - 40
619
- }, // Adjusted height to make space for legend if chartHeight is for the whole wrapper
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: ${chartHeight - 40
672
- }px;"></div>
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
- { length: Math.max(0, environment.cpu.cores || 0) },
901
- (_, i) =>
902
- `<div class="env-core-indicator ${i >=
903
- (environment.cpu.cores >= 8 ? 8 : environment.cpu.cores)
904
- ? "inactive"
905
- : ""
906
- }" title="Core ${i + 1}"></div>`
907
- ).join("")}
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">${environment.os.split(" ")[0] === "darwin"
928
- ? "darwin (macOS)"
929
- : environment.os.split(" ")[0] || "Unknown"
930
- }</span>
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">${environment.os.split(" ")[1] || "N/A"
935
- }</span>
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}">${environment.host
940
- }</span>
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}">${environment.cwd.length > 25
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
- }</span>
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 ${environment.os.includes("darwin") &&
979
- environment.cpu.model.toLowerCase().includes("apple")
980
- ? "env-chip-success"
981
- : "env-chip-warning"
982
- }">
983
- ${environment.os.includes("darwin") &&
984
- environment.cpu.model.toLowerCase().includes("apple")
985
- ? "Apple Silicon"
986
- : environment.cpu.model.toLowerCase().includes("arm") ||
987
- environment.cpu.model.toLowerCase().includes("aarch64")
988
- ? "ARM-based"
989
- : "x86/Other"
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">${environment.cpu.cores > 0
997
- ? (
998
- parseFloat(environment.memory) / environment.cpu.cores
999
- ).toFixed(2) + " GB"
1000
- : "N/A"
1001
- }</span>
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">CI/Local Test</span>
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
- .map((test) => {
1334
- const latestRun =
1335
- test.history.length > 0
1336
- ? test.history[test.history.length - 1]
1337
- : { status: "unknown" };
1338
- return `
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
- test.testTitle.toLowerCase()
1341
- )}" data-latest-status="${latestRun.status}">
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
- sanitizeHTML(test.testTitle)
1345
- )}</p>
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
- .slice()
1361
- .reverse()
1362
- .map(
1363
- (run) => `
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
- run.status
1368
- )}">${String(run.status).toUpperCase()}</span></td>
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
- .join("")}
1404
+ )
1405
+ .join("")}
1374
1406
  </tbody>
1375
1407
  </table>
1376
1408
  </div>
1377
1409
  </details>
1378
1410
  </div>`;
1379
- })
1380
- .join("")}
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
- return `<div class="suites-widget"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
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">${suitesData.length
1500
+ <span class="summary-badge">${
1501
+ suitesData.length
1466
1502
  } suites • ${suitesData.reduce(
1467
- (sum, suite) => sum + suite.count,
1468
- 0
1469
- )} tests</span>
1503
+ (sum, suite) => sum + suite.count,
1504
+ 0
1505
+ )} tests</span>
1470
1506
  </div>
1471
- <div class="suites-grid">
1472
- ${suitesData
1473
- .map(
1474
- (suite) => `
1475
- <div class="suite-card status-${suite.statusOverall}">
1476
- <div class="suite-card-header">
1477
- <h3 class="suite-name" title="${sanitizeHTML(
1478
- suite.name
1479
- )} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
1480
- </div>
1481
- <div>🖥️ <span class="browser-tag">${sanitizeHTML(
1482
- suite.browser
1483
- )}</span></div>
1484
- <div class="suite-card-body">
1485
- <span class="test-count">${suite.count} test${suite.count !== 1 ? "s" : ""
1486
- }</span>
1487
- <div class="suite-stats">
1488
- ${suite.passed > 0
1489
- ? `<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>`
1490
- : ""
1491
- }
1492
- ${suite.failed > 0
1493
- ? `<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>`
1494
- : ""
1495
- }
1496
- ${suite.skipped > 0
1497
- ? `<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>`
1498
- : ""
1499
- }
1500
- </div>
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(test => test.status === 'failed');
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('base64');
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">${new Set(failedTests.map(t => t.browser)).size}</span>
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">${(Math.round(failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) / 1000))}s</span>
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 specific failed test.
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.map(test => {
1556
- const testTitle = test.name.split(" > ").pop() || "Unnamed Test";
1557
- const testJson = btoa(JSON.stringify(test)); // Base64 encode the test object
1558
- const truncatedError = (test.errorMessage || "No error message").slice(0, 150) +
1559
- (test.errorMessage && test.errorMessage.length > 150 ? "..." : "");
1560
-
1561
- return `
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(test.name)}">${sanitizeHTML(testTitle)}</h3>
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(test.browser || 'unknown')}</span>
1568
- <span class="duration-indicator">${formatDuration(test.duration)}</span>
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
- <button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
1572
- <span class="ai-text">AI Fix</span>
1573
- </button>
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(truncatedError)}</div>
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(test.errorMessage || "No detailed error message available")}
1651
+ ${formatPlaywrightError(
1652
+ test.errorMessage || "No detailed error message available"
1653
+ )}
1585
1654
  </div>
1586
1655
  </div>
1587
1656
  </div>
1588
- `
1589
- }).join('')}
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
- ${step.codeLocation
2130
+ ${
2131
+ step.codeLocation
1667
2132
  ? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
1668
- step.codeLocation
1669
- )}</div>`
2133
+ step.codeLocation
2134
+ )}</div>`
1670
2135
  : ""
1671
- }
1672
- ${step.errorMessage
2136
+ }
2137
+ ${
2138
+ step.errorMessage
1673
2139
  ? `<div class="test-error-summary">
1674
- ${step.stackTrace
1675
- ? `<div class="stack-trace">${formatPlaywrightError(
1676
- step.stackTrace
1677
- )}</div>`
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
- ${hasNestedSteps
2168
+ }
2169
+ ${
2170
+ hasNestedSteps
1703
2171
  ? `<div class="nested-steps">${generateStepsHTML(
1704
- step.steps,
1705
- depth + 1
1706
- )}</div>`
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="${test.status
1717
- }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
1718
- .join(",")
1719
- .toLowerCase()}">
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
- test.status
1724
- ).toUpperCase()}</span>
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
- ${test.tags && test.tags.length > 0
1732
- ? test.tags
1733
- .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
1734
- .join(" ")
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
- test.totalWorkers
1746
- )}]</p>
1747
- ${test.errorMessage
1748
- ? `<div class="test-error-summary">${formatPlaywrightError(
1749
- test.errorMessage
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
- ${test.snippet
1774
- ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
1775
- test.snippet
1776
- )}</code></pre></div>`
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
- ${test.stderr && test.stderr.length > 0
1797
- ? `<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(
1798
- test.stderr.map((line) => sanitizeHTML(line)).join("\n")
1799
- )}</pre></div>`
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
- ${test.screenshots && test.screenshots.length > 0
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
- .map(
1809
- (screenshot, index) => `
2325
+ .map(
2326
+ (screenshot, index) => `
1810
2327
  <div class="attachment-item">
1811
- <img src="${fixPath(screenshot)}" alt="Screenshot ${index + 1}">
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(screenshot)}" target="_blank" class="view-full">View Full Image</a>
1815
- <a href="${fixPath(screenshot)}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
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
- .join("")}
2343
+ )
2344
+ .join("")}
1822
2345
  </div>
1823
2346
  </div>
1824
2347
  `
1825
- : ""
2348
+ : ""
1826
2349
  }
1827
- ${test.videoPath && test.videoPath.length > 0
1828
- ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
1829
- .map((videoUrl, index) => {
1830
- const fixedVideoUrl = fixPath(videoUrl);
1831
- const fileExtension = String(fixedVideoUrl)
1832
- .split(".")
1833
- .pop()
1834
- .toLowerCase();
1835
- const mimeType =
1836
- {
1837
- mp4: "video/mp4",
1838
- webm: "video/webm",
1839
- ogg: "video/ogg",
1840
- mov: "video/quicktime",
1841
- avi: "video/x-msvideo",
1842
- }[fileExtension] || "video/mp4";
1843
- return `<div class="attachment-item video-item">
1844
- <video controls width="100%" height="auto" title="Video ${index + 1
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
- fixedVideoUrl
1848
- )}" type="${mimeType}">
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
- fixedVideoUrl
1855
- )}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
2379
+ fixedVideoUrl
2380
+ )}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
1856
2381
  </div>
1857
2382
  </div>
1858
2383
  </div>`;
1859
- })
1860
- .join("")}</div></div>`
1861
- : ""
2384
+ })
2385
+ .join("")}</div></div>`
2386
+ : ""
1862
2387
  }
1863
- ${test.tracePath
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
- path.basename(test.tracePath)
1873
- )}</span>
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
- fixPath(test.tracePath)
1879
- )}" target="_blank" download="${sanitizeHTML(
1880
- path.basename(test.tracePath)
1881
- )}" class="download-trace">Download Trace</a>
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
- ${test.attachments && test.attachments.length > 0
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
- .map(
1897
- (attachment) => `
2423
+ .map(
2424
+ (attachment) => `
1898
2425
  <div class="attachment-item generic-attachment">
1899
2426
  <div class="attachment-icon">${getAttachmentIcon(
1900
- attachment.contentType
1901
- )}</div>
2427
+ attachment.contentType
2428
+ )}</div>
1902
2429
  <div class="attachment-caption">
1903
2430
  <span class="attachment-name" title="${sanitizeHTML(
1904
- attachment.name
1905
- )}">${sanitizeHTML(attachment.name)}</span>
2431
+ attachment.name
2432
+ )}">${sanitizeHTML(attachment.name)}</span>
1906
2433
  <span class="attachment-type">${sanitizeHTML(
1907
- attachment.contentType
1908
- )}</span>
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
- fixPath(attachment.path)
1914
- )}" target="_blank" class="view-full">View</a>
2440
+ fixPath(attachment.path)
2441
+ )}" target="_blank" class="view-full">View</a>
1915
2442
  <a href="${sanitizeHTML(
1916
- fixPath(attachment.path)
1917
- )}" target="_blank" download="${sanitizeHTML(
1918
- attachment.name
1919
- )}" class="download-trace">Download</a>
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
- .join("")}
2451
+ )
2452
+ .join("")}
1926
2453
  </div>
1927
2454
  </div>
1928
2455
  `
1929
- : ""
2456
+ : ""
1930
2457
  }
1931
- ${test.codeSnippet
1932
- ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
1933
- sanitizeHTML(test.codeSnippet)
1934
- )}</code></pre></div>`
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://i.postimg.cc/v817w4sg/logo.png">
1949
- <link rel="apple-touch-icon" href="https://i.postimg.cc/v817w4sg/logo.png">
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>Playwright Pulse Report</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: stretch; }
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
- .compact-ai-btn {
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://i.postimg.cc/v817w4sg/logo.png" alt="Report Logo">
2345
- <h1>Playwright Pulse Report</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
- runSummary.timestamp
2349
- )}<br><strong>Total Duration:</strong> ${formatDuration(
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">${runSummary.totalTests
2362
- }</div></div>
2363
- <div class="summary-card status-passed"><h3>Passed</h3><div class="value">${runSummary.passed
2364
- }</div><div class="trend-percentage">${passPercentage}%</div></div>
2365
- <div class="summary-card status-failed"><h3>Failed</h3><div class="value">${runSummary.failed
2366
- }</div><div class="trend-percentage">${failPercentage}%</div></div>
2367
- <div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${runSummary.skipped || 0
2368
- }</div><div class="trend-percentage">${skipPercentage}%</div></div>
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
- runSummary.duration
2372
- )}</div></div>
2938
+ runSummary.duration
2939
+ )}</div></div>
2373
2940
  </div>
2374
2941
  <div class="dashboard-bottom-row">
2375
- <div style="display: grid; gap: 20px">
2942
+ <div style="display: flex; flex-direction: column; gap: 28px;">
2376
2943
  ${generatePieChart(
2377
- [
2378
- { label: "Passed", value: runSummary.passed },
2379
- { label: "Failed", value: runSummary.failed },
2380
- { label: "Skipped", value: runSummary.skipped || 0 },
2381
- ],
2382
- 400,
2383
- 390
2384
- )}
2385
- ${runSummary.environment &&
2386
- Object.keys(runSummary.environment).length > 0
2387
- ? generateEnvironmentDashboard(runSummary.environment)
2388
- : '<div class="no-data">Environment data not available.</div>'
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
- </div>
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
- new Set(
2400
- (results || []).map((test) => test.browser || "unknown")
2401
- )
2402
- )
2403
- .map(
2404
- (browser) =>
2405
- `<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
2406
- browser
2407
- )}</option>`
2408
- )
2409
- .join("")}</select>
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
- ${trendData && trendData.overall && trendData.overall.length > 0
2419
- ? generateTestTrendsChart(trendData)
2420
- : '<div class="no-data">Overall trend data not available for test counts.</div>'
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
- ${trendData && trendData.overall && trendData.overall.length > 0
2425
- ? generateDurationTrendChart(trendData)
2426
- : '<div class="no-data">Overall trend data not available for durations.</div>'
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
- ${trendData &&
2438
- trendData.testRuns &&
2439
- Object.keys(trendData.testRuns).length > 0
2440
- ? generateTestHistoryContent(trendData)
2441
- : '<div class="no-data">Individual test history data not available.</div>'
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 = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
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
- await runScript(archiveRunScriptPath); // This script now handles JSON history
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
+ });