@arghajit/playwright-pulse-report 0.2.10 → 0.3.0

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