@arghajit/dummy 0.1.0-beta-26 → 0.1.0-beta-28

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.
@@ -615,9 +615,8 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
615
615
  chart: {
616
616
  type: 'pie',
617
617
  width: ${chartWidth},
618
- height: ${
619
- chartHeight - 40
620
- }, // Adjusted height to make space for legend if chartHeight is for the whole wrapper
618
+ height: ${chartHeight - 40
619
+ }, // Adjusted height to make space for legend if chartHeight is for the whole wrapper
621
620
  backgroundColor: 'transparent',
622
621
  plotShadow: false,
623
622
  spacingBottom: 40 // Ensure space for legend
@@ -669,9 +668,8 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
669
668
  return `
670
669
  <div class="pie-chart-wrapper" style="align-items: center; max-height: 450px">
671
670
  <div style="display: flex; align-items: start; width: 100%;"><h3>Test Distribution</h3></div>
672
- <div id="${chartId}" style="width: ${chartWidth}px; height: ${
673
- chartHeight - 40
674
- }px;"></div>
671
+ <div id="${chartId}" style="width: ${chartWidth}px; height: ${chartHeight - 40
672
+ }px;"></div>
675
673
  <script>
676
674
  document.addEventListener('DOMContentLoaded', function() {
677
675
  if (typeof Highcharts !== 'undefined') {
@@ -899,15 +897,14 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
899
897
  <span class="env-detail-value">
900
898
  <div class="env-cpu-cores">
901
899
  ${Array.from(
902
- { length: Math.max(0, environment.cpu.cores || 0) },
903
- (_, i) =>
904
- `<div class="env-core-indicator ${
905
- i >=
906
- (environment.cpu.cores >= 8 ? 8 : environment.cpu.cores)
907
- ? "inactive"
908
- : ""
909
- }" title="Core ${i + 1}"></div>`
910
- ).join("")}
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("")}
911
908
  <span>${environment.cpu.cores || "N/A"} cores</span>
912
909
  </div>
913
910
  </span>
@@ -927,23 +924,20 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
927
924
  <div class="env-card-content">
928
925
  <div class="env-detail-row">
929
926
  <span class="env-detail-label">OS Type</span>
930
- <span class="env-detail-value">${
931
- environment.os.split(" ")[0] === "darwin"
932
- ? "darwin (macOS)"
933
- : environment.os.split(" ")[0] || "Unknown"
934
- }</span>
927
+ <span class="env-detail-value">${environment.os.split(" ")[0] === "darwin"
928
+ ? "darwin (macOS)"
929
+ : environment.os.split(" ")[0] || "Unknown"
930
+ }</span>
935
931
  </div>
936
932
  <div class="env-detail-row">
937
933
  <span class="env-detail-label">OS Version</span>
938
- <span class="env-detail-value">${
939
- environment.os.split(" ")[1] || "N/A"
940
- }</span>
934
+ <span class="env-detail-value">${environment.os.split(" ")[1] || "N/A"
935
+ }</span>
941
936
  </div>
942
937
  <div class="env-detail-row">
943
938
  <span class="env-detail-label">Hostname</span>
944
- <span class="env-detail-value" title="${environment.host}">${
945
- environment.host
946
- }</span>
939
+ <span class="env-detail-value" title="${environment.host}">${environment.host
940
+ }</span>
947
941
  </div>
948
942
  </div>
949
943
  </div>
@@ -964,11 +958,10 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
964
958
  </div>
965
959
  <div class="env-detail-row">
966
960
  <span class="env-detail-label">Working Dir</span>
967
- <span class="env-detail-value" title="${environment.cwd}">${
968
- environment.cwd.length > 25
961
+ <span class="env-detail-value" title="${environment.cwd}">${environment.cwd.length > 25
969
962
  ? "..." + environment.cwd.slice(-22)
970
963
  : environment.cwd
971
- }</span>
964
+ }</span>
972
965
  </div>
973
966
  </div>
974
967
  </div>
@@ -982,33 +975,30 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
982
975
  <div class="env-detail-row">
983
976
  <span class="env-detail-label">Platform Arch</span>
984
977
  <span class="env-detail-value">
985
- <span class="env-chip ${
986
- environment.os.includes("darwin") &&
987
- environment.cpu.model.toLowerCase().includes("apple")
988
- ? "env-chip-success"
989
- : "env-chip-warning"
990
- }">
991
- ${
992
- environment.os.includes("darwin") &&
993
- environment.cpu.model.toLowerCase().includes("apple")
994
- ? "Apple Silicon"
995
- : environment.cpu.model.toLowerCase().includes("arm") ||
996
- environment.cpu.model.toLowerCase().includes("aarch64")
997
- ? "ARM-based"
998
- : "x86/Other"
999
- }
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
+ }
1000
991
  </span>
1001
992
  </span>
1002
993
  </div>
1003
994
  <div class="env-detail-row">
1004
995
  <span class="env-detail-label">Memory per Core</span>
1005
- <span class="env-detail-value">${
1006
- environment.cpu.cores > 0
1007
- ? (
1008
- parseFloat(environment.memory) / environment.cpu.cores
1009
- ).toFixed(2) + " GB"
1010
- : "N/A"
1011
- }</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>
1012
1002
  </div>
1013
1003
  <div class="env-detail-row">
1014
1004
  <span class="env-detail-label">Run Context</span>
@@ -1340,19 +1330,19 @@ function generateTestHistoryContent(trendData) {
1340
1330
 
1341
1331
  <div class="test-history-grid">
1342
1332
  ${testHistory
1343
- .map((test) => {
1344
- const latestRun =
1345
- test.history.length > 0
1346
- ? test.history[test.history.length - 1]
1347
- : { status: "unknown" };
1348
- return `
1333
+ .map((test) => {
1334
+ const latestRun =
1335
+ test.history.length > 0
1336
+ ? test.history[test.history.length - 1]
1337
+ : { status: "unknown" };
1338
+ return `
1349
1339
  <div class="test-history-card" data-test-name="${sanitizeHTML(
1350
- test.testTitle.toLowerCase()
1351
- )}" data-latest-status="${latestRun.status}">
1340
+ test.testTitle.toLowerCase()
1341
+ )}" data-latest-status="${latestRun.status}">
1352
1342
  <div class="test-history-header">
1353
1343
  <p title="${sanitizeHTML(test.testTitle)}">${capitalize(
1354
- sanitizeHTML(test.testTitle)
1355
- )}</p>
1344
+ sanitizeHTML(test.testTitle)
1345
+ )}</p>
1356
1346
  <span class="status-badge ${getStatusClass(latestRun.status)}">
1357
1347
  ${String(latestRun.status).toUpperCase()}
1358
1348
  </span>
@@ -1367,27 +1357,27 @@ function generateTestHistoryContent(trendData) {
1367
1357
  <thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Date</th></tr></thead>
1368
1358
  <tbody>
1369
1359
  ${test.history
1370
- .slice()
1371
- .reverse()
1372
- .map(
1373
- (run) => `
1360
+ .slice()
1361
+ .reverse()
1362
+ .map(
1363
+ (run) => `
1374
1364
  <tr>
1375
1365
  <td>${run.runId}</td>
1376
1366
  <td><span class="status-badge-small ${getStatusClass(
1377
- run.status
1378
- )}">${String(run.status).toUpperCase()}</span></td>
1367
+ run.status
1368
+ )}">${String(run.status).toUpperCase()}</span></td>
1379
1369
  <td>${formatDuration(run.duration)}</td>
1380
1370
  <td>${formatDate(run.timestamp)}</td>
1381
1371
  </tr>`
1382
- )
1383
- .join("")}
1372
+ )
1373
+ .join("")}
1384
1374
  </tbody>
1385
1375
  </table>
1386
1376
  </div>
1387
1377
  </details>
1388
1378
  </div>`;
1389
- })
1390
- .join("")}
1379
+ })
1380
+ .join("")}
1391
1381
  </div>
1392
1382
  </div>
1393
1383
  `;
@@ -1472,12 +1462,11 @@ function generateSuitesWidget(suitesData) {
1472
1462
  <div class="suites-widget">
1473
1463
  <div class="suites-header">
1474
1464
  <h2>Test Suites</h2>
1475
- <span class="summary-badge">${
1476
- suitesData.length
1465
+ <span class="summary-badge">${suitesData.length
1477
1466
  } suites • ${suitesData.reduce(
1478
- (sum, suite) => sum + suite.count,
1479
- 0
1480
- )} tests</span>
1467
+ (sum, suite) => sum + suite.count,
1468
+ 0
1469
+ )} tests</span>
1481
1470
  </div>
1482
1471
  <div class="suites-grid">
1483
1472
  ${suitesData
@@ -1490,28 +1479,24 @@ function generateSuitesWidget(suitesData) {
1490
1479
  )} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
1491
1480
  </div>
1492
1481
  <div>🖥️ <span class="browser-tag">${sanitizeHTML(
1493
- suite.browser
1494
- )}</span></div>
1482
+ suite.browser
1483
+ )}</span></div>
1495
1484
  <div class="suite-card-body">
1496
- <span class="test-count">${suite.count} test${
1497
- suite.count !== 1 ? "s" : ""
1498
- }</span>
1485
+ <span class="test-count">${suite.count} test${suite.count !== 1 ? "s" : ""
1486
+ }</span>
1499
1487
  <div class="suite-stats">
1500
- ${
1501
- suite.passed > 0
1502
- ? `<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>`
1503
- : ""
1504
- }
1505
- ${
1506
- suite.failed > 0
1507
- ? `<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>`
1508
- : ""
1509
- }
1510
- ${
1511
- suite.skipped > 0
1512
- ? `<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>`
1513
- : ""
1514
- }
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
+ }
1515
1500
  </div>
1516
1501
  </div>
1517
1502
  </div>`
@@ -1533,6 +1518,91 @@ function getAttachmentIcon(contentType) {
1533
1518
  if (normalizedType.startsWith("text/")) return "📝";
1534
1519
  return "📎";
1535
1520
  }
1521
+ function generateAIFailureAnalyzerTab(results) {
1522
+ const failedTests = (results || []).filter(test => test.status === 'failed');
1523
+
1524
+ if (failedTests.length === 0) {
1525
+ return `
1526
+ <h2 class="tab-main-title">AI Failure Analysis</h2>
1527
+ <div class="no-data">Congratulations! No failed tests in this run.</div>
1528
+ `;
1529
+ }
1530
+
1531
+ // 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');
1533
+
1534
+ return `
1535
+ <h2 class="tab-main-title">AI Failure Analysis</h2>
1536
+ <div class="ai-analyzer-stats">
1537
+ <div class="stat-item">
1538
+ <span class="stat-number">${failedTests.length}</span>
1539
+ <span class="stat-label">Failed Tests</span>
1540
+ </div>
1541
+ <div class="stat-item">
1542
+ <span class="stat-number">${new Set(failedTests.map(t => t.browser)).size}</span>
1543
+ <span class="stat-label">Browsers</span>
1544
+ </div>
1545
+ <div class="stat-item">
1546
+ <span class="stat-number">${(Math.round(failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) / 1000))}s</span>
1547
+ <span class="stat-label">Total Duration</span>
1548
+ </div>
1549
+ </div>
1550
+ <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.
1552
+ </p>
1553
+
1554
+ <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 `
1562
+ <div class="compact-failure-item">
1563
+ <div class="failure-header">
1564
+ <div class="failure-main-info">
1565
+ <h3 class="failure-title" title="${sanitizeHTML(test.name)}">${sanitizeHTML(testTitle)}</h3>
1566
+ <div class="failure-meta">
1567
+ <span class="browser-indicator">${sanitizeHTML(test.browser || 'unknown')}</span>
1568
+ <span class="duration-indicator">${formatDuration(test.duration)}</span>
1569
+ </div>
1570
+ </div>
1571
+ <button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
1572
+ <span class="ai-text">AI Fix</span>
1573
+ </button>
1574
+ </div>
1575
+ <div class="failure-error-preview">
1576
+ <div class="error-snippet">${formatPlaywrightError(truncatedError)}</div>
1577
+ <button class="expand-error-btn" onclick="toggleErrorDetails(this)">
1578
+ <span class="expand-text">Show Full Error</span>
1579
+ <span class="expand-icon">▼</span>
1580
+ </button>
1581
+ </div>
1582
+ <div class="full-error-details" style="display: none;">
1583
+ <div class="full-error-content">
1584
+ ${formatPlaywrightError(test.errorMessage || "No detailed error message available")}
1585
+ </div>
1586
+ </div>
1587
+ </div>
1588
+ `
1589
+ }).join('')}
1590
+ </div>
1591
+
1592
+ <!-- AI Fix Modal -->
1593
+ <div id="ai-fix-modal" class="ai-modal-overlay" onclick="closeAiModal()">
1594
+ <div class="ai-modal-content" onclick="event.stopPropagation()">
1595
+ <div class="ai-modal-header">
1596
+ <h3 id="ai-fix-modal-title">AI Analysis</h3>
1597
+ <span class="ai-modal-close" onclick="closeAiModal()">×</span>
1598
+ </div>
1599
+ <div class="ai-modal-body" id="ai-fix-modal-content">
1600
+ <!-- Content will be injected by JavaScript -->
1601
+ </div>
1602
+ </div>
1603
+ </div>
1604
+ `;
1605
+ }
1536
1606
  function generateHTML(reportData, trendData = null) {
1537
1607
  const { run, results } = reportData;
1538
1608
  const suitesData = getSuitesData(reportData.results || []);
@@ -1544,6 +1614,13 @@ function generateHTML(reportData, trendData = null) {
1544
1614
  duration: 0,
1545
1615
  timestamp: new Date().toISOString(),
1546
1616
  };
1617
+
1618
+ const fixPath = (p) => {
1619
+ if (!p) return "";
1620
+ // This regex handles both forward slashes and backslashes
1621
+ return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`), '');
1622
+ };
1623
+
1547
1624
  const totalTestsOr1 = runSummary.totalTests || 1;
1548
1625
  const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
1549
1626
  const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
@@ -1586,23 +1663,20 @@ function generateHTML(reportData, trendData = null) {
1586
1663
  )}</span>
1587
1664
  </div>
1588
1665
  <div class="step-details" style="display: none;">
1589
- ${
1590
- step.codeLocation
1666
+ ${step.codeLocation
1591
1667
  ? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
1592
- step.codeLocation
1593
- )}</div>`
1668
+ step.codeLocation
1669
+ )}</div>`
1594
1670
  : ""
1595
- }
1596
- ${
1597
- step.errorMessage
1671
+ }
1672
+ ${step.errorMessage
1598
1673
  ? `<div class="test-error-summary">
1599
- ${
1600
- step.stackTrace
1601
- ? `<div class="stack-trace">${formatPlaywrightError(
1602
- step.stackTrace
1603
- )}</div>`
1604
- : ""
1605
- }
1674
+ ${step.stackTrace
1675
+ ? `<div class="stack-trace">${formatPlaywrightError(
1676
+ step.stackTrace
1677
+ )}</div>`
1678
+ : ""
1679
+ }
1606
1680
  <button
1607
1681
  class="copy-error-btn"
1608
1682
  onclick="copyErrorToClipboard(this)"
@@ -1624,15 +1698,14 @@ function generateHTML(reportData, trendData = null) {
1624
1698
  </button>
1625
1699
  </div>`
1626
1700
  : ""
1627
- }
1628
- ${
1629
- hasNestedSteps
1701
+ }
1702
+ ${hasNestedSteps
1630
1703
  ? `<div class="nested-steps">${generateStepsHTML(
1631
- step.steps,
1632
- depth + 1
1633
- )}</div>`
1704
+ step.steps,
1705
+ depth + 1
1706
+ )}</div>`
1634
1707
  : ""
1635
- }
1708
+ }
1636
1709
  </div>
1637
1710
  </div>`;
1638
1711
  })
@@ -1640,29 +1713,27 @@ function generateHTML(reportData, trendData = null) {
1640
1713
  };
1641
1714
 
1642
1715
  return `
1643
- <div class="test-case" data-status="${
1644
- test.status
1645
- }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
1646
- .join(",")
1647
- .toLowerCase()}">
1716
+ <div class="test-case" data-status="${test.status
1717
+ }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
1718
+ .join(",")
1719
+ .toLowerCase()}">
1648
1720
  <div class="test-case-header" role="button" aria-expanded="false">
1649
1721
  <div class="test-case-summary">
1650
1722
  <span class="status-badge ${getStatusClass(test.status)}">${String(
1651
- test.status
1652
- ).toUpperCase()}</span>
1723
+ test.status
1724
+ ).toUpperCase()}</span>
1653
1725
  <span class="test-case-title" title="${sanitizeHTML(
1654
1726
  test.name
1655
1727
  )}">${sanitizeHTML(testTitle)}</span>
1656
1728
  <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
1657
1729
  </div>
1658
1730
  <div class="test-case-meta">
1659
- ${
1660
- test.tags && test.tags.length > 0
1661
- ? test.tags
1662
- .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
1663
- .join(" ")
1664
- : ""
1665
- }
1731
+ ${test.tags && test.tags.length > 0
1732
+ ? test.tags
1733
+ .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
1734
+ .join(" ")
1735
+ : ""
1736
+ }
1666
1737
  <span class="test-duration">${formatDuration(test.duration)}</span>
1667
1738
  </div>
1668
1739
  </div>
@@ -1671,13 +1742,12 @@ function generateHTML(reportData, trendData = null) {
1671
1742
  <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
1672
1743
  test.workerId
1673
1744
  )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
1674
- test.totalWorkers
1675
- )}]</p>
1676
- ${
1677
- test.errorMessage
1678
- ? `<div class="test-error-summary">${formatPlaywrightError(
1679
- test.errorMessage
1680
- )}
1745
+ test.totalWorkers
1746
+ )}]</p>
1747
+ ${test.errorMessage
1748
+ ? `<div class="test-error-summary">${formatPlaywrightError(
1749
+ test.errorMessage
1750
+ )}
1681
1751
  <button
1682
1752
  class="copy-error-btn"
1683
1753
  onclick="copyErrorToClipboard(this)"
@@ -1698,14 +1768,20 @@ function generateHTML(reportData, trendData = null) {
1698
1768
  Copy Error Prompt
1699
1769
  </button>
1700
1770
  </div>`
1701
- : ""
1771
+ : ""
1772
+ }
1773
+ ${test.snippet
1774
+ ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
1775
+ test.snippet
1776
+ )}</code></pre></div>`
1777
+ : ""
1702
1778
  }
1703
1779
  <h4>Steps</h4>
1704
1780
  <div class="steps-list">${generateStepsHTML(test.steps)}</div>
1705
1781
  ${(() => {
1706
1782
  if (!test.stdout || test.stdout.length === 0) return "";
1707
1783
  // Create a unique ID for the <pre> element to target it for copying
1708
- const logId = `stdout-log-${test.id || testIndex}`;
1784
+ const logId = `stdout-log-${test.id || index}`;
1709
1785
  return `<div class="console-output-section">
1710
1786
  <h4>Console Output (stdout)
1711
1787
  <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy Console</button>
@@ -1717,79 +1793,75 @@ function generateHTML(reportData, trendData = null) {
1717
1793
  </div>
1718
1794
  </div>`;
1719
1795
  })()}
1720
- ${
1721
- test.stderr && test.stderr.length > 0
1722
- ? `<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(
1723
- test.stderr.map((line) => sanitizeHTML(line)).join("\n")
1724
- )}</pre></div>`
1725
- : ""
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
+ : ""
1726
1801
  }
1727
- ${
1728
- test.screenshots && test.screenshots.length > 0
1729
- ? `
1802
+ ${test.screenshots && test.screenshots.length > 0
1803
+ ? `
1730
1804
  <div class="attachments-section">
1731
1805
  <h4>Screenshots</h4>
1732
1806
  <div class="attachments-grid">
1733
1807
  ${test.screenshots
1734
- .map(
1735
- (screenshot, index) => `
1808
+ .map(
1809
+ (screenshot, index) => `
1736
1810
  <div class="attachment-item">
1737
- <img src="${screenshot}" alt="Screenshot ${index + 1}">
1811
+ <img src="${fixPath(screenshot)}" alt="Screenshot ${index + 1}">
1738
1812
  <div class="attachment-info">
1739
1813
  <div class="trace-actions">
1740
- <a href="${screenshot}" target="_blank" class="view-full">View Full Image</a>
1741
- <a href="${screenshot}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
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>
1742
1816
  </div>
1743
1817
  </div>
1744
1818
  </div>
1745
1819
  `
1746
- )
1747
- .join("")}
1820
+ )
1821
+ .join("")}
1748
1822
  </div>
1749
1823
  </div>
1750
1824
  `
1751
- : ""
1825
+ : ""
1752
1826
  }
1753
- ${
1754
- test.videoPath && test.videoPath.length > 0
1755
- ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
1756
- .map((videoUrl, index) => {
1757
- const fileExtension = String(videoUrl)
1758
- .split(".")
1759
- .pop()
1760
- .toLowerCase();
1761
- const mimeType =
1762
- {
1763
- mp4: "video/mp4",
1764
- webm: "video/webm",
1765
- ogg: "video/ogg",
1766
- mov: "video/quicktime",
1767
- avi: "video/x-msvideo",
1768
- }[fileExtension] || "video/mp4";
1769
- return `<div class="attachment-item video-item">
1770
- <video controls width="100%" height="auto" title="Video ${
1771
- index + 1
1772
- }">
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
+ }">
1773
1846
  <source src="${sanitizeHTML(
1774
- videoUrl
1775
- )}" type="${mimeType}">
1847
+ fixedVideoUrl
1848
+ )}" type="${mimeType}">
1776
1849
  Your browser does not support the video tag.
1777
1850
  </video>
1778
1851
  <div class="attachment-info">
1779
1852
  <div class="trace-actions">
1780
1853
  <a href="${sanitizeHTML(
1781
- videoUrl
1782
- )}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
1854
+ fixedVideoUrl
1855
+ )}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
1783
1856
  </div>
1784
1857
  </div>
1785
1858
  </div>`;
1786
- })
1787
- .join("")}</div></div>`
1788
- : ""
1859
+ })
1860
+ .join("")}</div></div>`
1861
+ : ""
1789
1862
  }
1790
- ${
1791
- test.tracePath
1792
- ? `
1863
+ ${test.tracePath
1864
+ ? `
1793
1865
  <div class="attachments-section">
1794
1866
  <h4>Trace Files</h4>
1795
1867
  <div class="attachments-grid">
@@ -1797,72 +1869,70 @@ function generateHTML(reportData, trendData = null) {
1797
1869
  <div class="trace-preview">
1798
1870
  <span class="trace-icon">📄</span>
1799
1871
  <span class="trace-name">${sanitizeHTML(
1800
- path.basename(test.tracePath)
1801
- )}</span>
1872
+ path.basename(test.tracePath)
1873
+ )}</span>
1802
1874
  </div>
1803
1875
  <div class="attachment-info">
1804
1876
  <div class="trace-actions">
1805
1877
  <a href="${sanitizeHTML(
1806
- test.tracePath
1807
- )}" target="_blank" download="${sanitizeHTML(
1808
- path.basename(test.tracePath)
1809
- )}" class="download-trace">Download Trace</a>
1878
+ fixPath(test.tracePath)
1879
+ )}" target="_blank" download="${sanitizeHTML(
1880
+ path.basename(test.tracePath)
1881
+ )}" class="download-trace">Download Trace</a>
1810
1882
  </div>
1811
1883
  </div>
1812
1884
  </div>
1813
1885
  </div>
1814
1886
  </div>
1815
1887
  `
1816
- : ""
1888
+ : ""
1817
1889
  }
1818
- ${
1819
- test.attachments && test.attachments.length > 0
1820
- ? `
1890
+ ${test.attachments && test.attachments.length > 0
1891
+ ? `
1821
1892
  <div class="attachments-section">
1822
1893
  <h4>Other Attachments</h4>
1823
1894
  <div class="attachments-grid">
1824
1895
  ${test.attachments
1825
- .map(
1826
- (attachment) => `
1896
+ .map(
1897
+ (attachment) => `
1827
1898
  <div class="attachment-item generic-attachment">
1828
1899
  <div class="attachment-icon">${getAttachmentIcon(
1829
- attachment.contentType
1830
- )}</div>
1900
+ attachment.contentType
1901
+ )}</div>
1831
1902
  <div class="attachment-caption">
1832
1903
  <span class="attachment-name" title="${sanitizeHTML(
1833
- attachment.name
1834
- )}">${sanitizeHTML(attachment.name)}</span>
1904
+ attachment.name
1905
+ )}">${sanitizeHTML(attachment.name)}</span>
1835
1906
  <span class="attachment-type">${sanitizeHTML(
1836
- attachment.contentType
1837
- )}</span>
1907
+ attachment.contentType
1908
+ )}</span>
1838
1909
  </div>
1839
1910
  <div class="attachment-info">
1840
1911
  <div class="trace-actions">
1841
1912
  <a href="${sanitizeHTML(
1842
- attachment.path
1843
- )}" target="_blank" class="view-full">View</a>
1913
+ fixPath(attachment.path)
1914
+ )}" target="_blank" class="view-full">View</a>
1844
1915
  <a href="${sanitizeHTML(
1845
- attachment.path
1846
- )}" target="_blank" download="${sanitizeHTML(
1847
- attachment.name
1848
- )}" class="download-trace">Download</a>
1916
+ fixPath(attachment.path)
1917
+ )}" target="_blank" download="${sanitizeHTML(
1918
+ attachment.name
1919
+ )}" class="download-trace">Download</a>
1849
1920
  </div>
1850
1921
  </div>
1851
1922
  </div>
1852
1923
  `
1853
- )
1854
- .join("")}
1924
+ )
1925
+ .join("")}
1855
1926
  </div>
1856
1927
  </div>
1857
1928
  `
1858
- : ""
1929
+ : ""
1859
1930
  }
1860
- ${
1861
- test.codeSnippet
1862
- ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
1863
- sanitizeHTML(test.codeSnippet)
1864
- )}</code></pre></div>`
1865
- : ""
1931
+ ${test.codeSnippet
1932
+ ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
1933
+ sanitizeHTML(test.codeSnippet)
1934
+ )}</code></pre></div>`
1935
+ : ""
1866
1936
  }
1867
1937
  </div>
1868
1938
  </div>`;
@@ -2004,7 +2074,7 @@ function generateHTML(reportData, trendData = null) {
2004
2074
  .attachment-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2005
2075
  .attachment-type { font-size: 0.8rem; color: var(--text-color-secondary); }
2006
2076
  .trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
2007
- .test-history-container h2.tab-main-title { font-size: 1.6em; margin-bottom: 18px; color: var(--primary-color); border-bottom: 1px solid var(--border-color); padding-bottom: 12px;}
2077
+ .test-history-container h2.tab-main-title, .ai-analyzer-container h2.tab-main-title { font-size: 1.6em; margin-bottom: 18px; color: var(--primary-color); border-bottom: 1px solid var(--border-color); padding-bottom: 12px;}
2008
2078
  .test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
2009
2079
  .test-history-card { background: var(--card-background-color); border: 1px solid var(--border-color); border-radius: var(--border-radius); padding: 22px; box-shadow: var(--box-shadow-light); display: flex; flex-direction: column; }
2010
2080
  .test-history-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 14px; border-bottom: 1px solid var(--light-gray-color); }
@@ -2024,8 +2094,24 @@ function generateHTML(reportData, trendData = null) {
2024
2094
  .status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
2025
2095
  .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); }
2026
2096
  .no-data-chart {font-size: 0.95em; padding: 18px;}
2027
- #test-ai iframe { border: 1px solid var(--border-color); width: 100%; height: 85vh; border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); }
2028
- #test-ai p {margin-bottom: 18px; font-size: 1em; color: var(--text-color-secondary);}
2097
+ .ai-failure-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 22px; }
2098
+ .ai-failure-card { background: var(--card-background-color); border: 1px solid var(--border-color); border-left: 5px solid var(--danger-color); border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); display: flex; flex-direction: column; }
2099
+ .ai-failure-card-header { padding: 15px 20px; border-bottom: 1px solid var(--light-gray-color); display: flex; align-items: center; justify-content: space-between; gap: 15px; }
2100
+ .ai-failure-card-header h3 { margin: 0; font-size: 1.1em; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2101
+ .ai-failure-card-body { padding: 20px; }
2102
+ .ai-fix-btn { background-color: var(--primary-color); color: white; border: none; padding: 10px 18px; font-size: 1em; font-weight: 600; border-radius: 6px; cursor: pointer; transition: background-color 0.2s ease, transform 0.2s ease; display: inline-flex; align-items: center; gap: 8px; }
2103
+ .ai-fix-btn:hover { background-color: var(--accent-color); transform: translateY(-2px); }
2104
+ .ai-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.65); display: none; align-items: center; justify-content: center; z-index: 1050; animation: fadeIn 0.3s; }
2105
+ .ai-modal-content { background-color: var(--card-background-color); color: var(--text-color); border-radius: var(--border-radius); width: 90%; max-width: 800px; max-height: 90vh; box-shadow: 0 10px 30px rgba(0,0,0,0.2); display: flex; flex-direction: column; overflow: hidden; }
2106
+ .ai-modal-header { padding: 18px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
2107
+ .ai-modal-header h3 { margin: 0; font-size: 1.25em; }
2108
+ .ai-modal-close { font-size: 2rem; font-weight: 300; cursor: pointer; color: var(--dark-gray-color); line-height: 1; transition: color 0.2s; }
2109
+ .ai-modal-close:hover { color: var(--danger-color); }
2110
+ .ai-modal-body { padding: 25px; overflow-y: auto; }
2111
+ .ai-modal-body h4 { margin-top: 18px; margin-bottom: 10px; font-size: 1.1em; color: var(--primary-color); }
2112
+ .ai-modal-body p { margin-bottom: 15px; }
2113
+ .ai-loader { margin: 40px auto; border: 5px solid #f3f3f3; border-top: 5px solid var(--primary-color); border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite; }
2114
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
2029
2115
  .trace-preview { padding: 1rem; text-align: center; background: #f5f5f5; border-bottom: 1px solid #e1e1e1; }
2030
2116
  .trace-icon { font-size: 2rem; display: block; margin-bottom: 0.5rem; }
2031
2117
  .trace-name { word-break: break-word; font-size: 0.9rem; }
@@ -2038,9 +2124,216 @@ function generateHTML(reportData, trendData = null) {
2038
2124
  .filters button.clear-filters-btn { background-color: var(--medium-gray-color); color: var(--text-color); }
2039
2125
  .filters button.clear-filters-btn:hover { background-color: var(--dark-gray-color); color: #fff; }
2040
2126
  .copy-btn {color: var(--primary-color); background: #fefefe; border-radius: 8px; cursor: pointer; border-color: var(--primary-color); font-size: 1em; margin-left: 93%; font-weight: 600;}
2127
+ /* Compact AI Failure Analyzer Styles */
2128
+ .ai-analyzer-stats {
2129
+ display: flex;
2130
+ gap: 20px;
2131
+ margin-bottom: 25px;
2132
+ padding: 20px;
2133
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
2134
+ border-radius: var(--border-radius);
2135
+ justify-content: center;
2136
+ }
2137
+ .stat-item {
2138
+ text-align: center;
2139
+ color: white;
2140
+ }
2141
+ .stat-number {
2142
+ display: block;
2143
+ font-size: 2em;
2144
+ font-weight: 700;
2145
+ line-height: 1;
2146
+ }
2147
+ .stat-label {
2148
+ font-size: 0.9em;
2149
+ opacity: 0.9;
2150
+ font-weight: 500;
2151
+ }
2152
+ .ai-analyzer-description {
2153
+ margin-bottom: 25px;
2154
+ font-size: 1em;
2155
+ color: var(--text-color-secondary);
2156
+ text-align: center;
2157
+ max-width: 600px;
2158
+ margin-left: auto;
2159
+ margin-right: auto;
2160
+ }
2161
+ .compact-failure-list {
2162
+ display: flex;
2163
+ flex-direction: column;
2164
+ gap: 15px;
2165
+ }
2166
+ .compact-failure-item {
2167
+ background: var(--card-background-color);
2168
+ border: 1px solid var(--border-color);
2169
+ border-left: 4px solid var(--danger-color);
2170
+ border-radius: var(--border-radius);
2171
+ box-shadow: var(--box-shadow-light);
2172
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
2173
+ }
2174
+ .compact-failure-item:hover {
2175
+ transform: translateY(-2px);
2176
+ box-shadow: var(--box-shadow);
2177
+ }
2178
+ .failure-header {
2179
+ display: flex;
2180
+ justify-content: space-between;
2181
+ align-items: center;
2182
+ padding: 18px 20px;
2183
+ gap: 15px;
2184
+ }
2185
+ .failure-main-info {
2186
+ flex: 1;
2187
+ min-width: 0;
2188
+ }
2189
+ .failure-title {
2190
+ margin: 0 0 8px 0;
2191
+ font-size: 1.1em;
2192
+ font-weight: 600;
2193
+ color: var(--text-color);
2194
+ white-space: nowrap;
2195
+ overflow: hidden;
2196
+ text-overflow: ellipsis;
2197
+ }
2198
+ .failure-meta {
2199
+ display: flex;
2200
+ gap: 12px;
2201
+ align-items: center;
2202
+ }
2203
+ .browser-indicator, .duration-indicator {
2204
+ font-size: 0.85em;
2205
+ padding: 3px 8px;
2206
+ border-radius: 12px;
2207
+ font-weight: 500;
2208
+ }
2209
+ .browser-indicator {
2210
+ background: var(--info-color);
2211
+ color: white;
2212
+ }
2213
+ .duration-indicator {
2214
+ background: var(--medium-gray-color);
2215
+ color: var(--text-color);
2216
+ }
2217
+ .compact-ai-btn {
2218
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
2219
+ color: white;
2220
+ border: none;
2221
+ padding: 12px 18px;
2222
+ border-radius: 6px;
2223
+ cursor: pointer;
2224
+ font-weight: 600;
2225
+ display: flex;
2226
+ align-items: center;
2227
+ gap: 8px;
2228
+ transition: all 0.3s ease;
2229
+ white-space: nowrap;
2230
+ }
2231
+ .compact-ai-btn:hover {
2232
+ transform: translateY(-2px);
2233
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
2234
+ }
2235
+ .ai-icon {
2236
+ font-size: 1.2em;
2237
+ }
2238
+ .ai-text {
2239
+ font-size: 0.95em;
2240
+ }
2241
+ .failure-error-preview {
2242
+ padding: 0 20px 18px 20px;
2243
+ border-top: 1px solid var(--light-gray-color);
2244
+ }
2245
+ .error-snippet {
2246
+ background: rgba(244, 67, 54, 0.05);
2247
+ border: 1px solid rgba(244, 67, 54, 0.2);
2248
+ border-radius: 6px;
2249
+ padding: 12px;
2250
+ margin-bottom: 12px;
2251
+ font-family: monospace;
2252
+ font-size: 0.9em;
2253
+ color: var(--danger-color);
2254
+ line-height: 1.4;
2255
+ }
2256
+ .expand-error-btn {
2257
+ background: none;
2258
+ border: 1px solid var(--border-color);
2259
+ color: var(--text-color-secondary);
2260
+ padding: 6px 12px;
2261
+ border-radius: 4px;
2262
+ cursor: pointer;
2263
+ font-size: 0.85em;
2264
+ display: flex;
2265
+ align-items: center;
2266
+ gap: 6px;
2267
+ transition: all 0.2s ease;
2268
+ }
2269
+ .expand-error-btn:hover {
2270
+ background: var(--light-gray-color);
2271
+ border-color: var(--medium-gray-color);
2272
+ }
2273
+ .expand-icon {
2274
+ transition: transform 0.2s ease;
2275
+ font-size: 0.8em;
2276
+ }
2277
+ .expand-error-btn.expanded .expand-icon {
2278
+ transform: rotate(180deg);
2279
+ }
2280
+ .full-error-details {
2281
+ padding: 0 20px 20px 20px;
2282
+ border-top: 1px solid var(--light-gray-color);
2283
+ margin-top: 0;
2284
+ }
2285
+ .full-error-content {
2286
+ background: rgba(244, 67, 54, 0.05);
2287
+ border: 1px solid rgba(244, 67, 54, 0.2);
2288
+ border-radius: 6px;
2289
+ padding: 15px;
2290
+ font-family: monospace;
2291
+ font-size: 0.9em;
2292
+ color: var(--danger-color);
2293
+ line-height: 1.4;
2294
+ max-height: 300px;
2295
+ overflow-y: auto;
2296
+ }
2297
+
2298
+ /* Responsive adjustments for compact design */
2299
+ @media (max-width: 768px) {
2300
+ .ai-analyzer-stats {
2301
+ flex-direction: column;
2302
+ gap: 15px;
2303
+ text-align: center;
2304
+ }
2305
+ .failure-header {
2306
+ flex-direction: column;
2307
+ align-items: stretch;
2308
+ gap: 15px;
2309
+ }
2310
+ .failure-main-info {
2311
+ text-align: center;
2312
+ }
2313
+ .failure-meta {
2314
+ justify-content: center;
2315
+ }
2316
+ .compact-ai-btn {
2317
+ justify-content: center;
2318
+ padding: 12px 20px;
2319
+ }
2320
+ }
2321
+ @media (max-width: 480px) {
2322
+ .stat-item .stat-number {
2323
+ font-size: 1.5em;
2324
+ }
2325
+ .failure-header {
2326
+ padding: 15px;
2327
+ }
2328
+ .failure-error-preview, .full-error-details {
2329
+ padding-left: 15px;
2330
+ padding-right: 15px;
2331
+ }
2332
+ }
2333
+
2041
2334
  @media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
2042
2335
  @media (max-width: 992px) { .dashboard-bottom-row { grid-template-columns: 1fr; } .pie-chart-wrapper div[id^="pieChart-"] { max-width: 350px; margin: 0 auto; } .filters input { min-width: 180px; } .filters select { min-width: 150px; } }
2043
- @media (max-width: 768px) { body { font-size: 15px; } .container { margin: 10px; padding: 20px; } .header { flex-direction: column; align-items: flex-start; gap: 15px; } .header h1 { font-size: 1.6em; } .run-info { text-align: left; font-size:0.9em; } .tabs { margin-bottom: 25px;} .tab-button { padding: 12px 20px; font-size: 1.05em;} .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;} .summary-card .value {font-size: 2em;} .summary-card h3 {font-size: 0.95em;} .filters { flex-direction: column; padding: 18px; gap: 12px;} .filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;} .test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; } .test-case-summary {gap: 10px;} .test-case-title {font-size: 1.05em;} .test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;} .attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;} .test-history-grid {grid-template-columns: 1fr;} .pie-chart-wrapper {min-height: auto;} }
2336
+ @media (max-width: 768px) { body { font-size: 15px; } .container { margin: 10px; padding: 20px; } .header { flex-direction: column; align-items: flex-start; gap: 15px; } .header h1 { font-size: 1.6em; } .run-info { text-align: left; font-size:0.9em; } .tabs { margin-bottom: 25px;} .tab-button { padding: 12px 20px; font-size: 1.05em;} .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;} .summary-card .value {font-size: 2em;} .summary-card h3 {font-size: 0.95em;} .filters { flex-direction: column; padding: 18px; gap: 12px;} .filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;} .test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; } .test-case-summary {gap: 10px;} .test-case-title {font-size: 1.05em;} .test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;} .attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;} .test-history-grid {grid-template-columns: 1fr;} .pie-chart-wrapper {min-height: auto;} .ai-failure-cards-grid { grid-template-columns: 1fr; } }
2044
2337
  @media (max-width: 480px) { body {font-size: 14px;} .container {padding: 15px;} .header h1 {font-size: 1.4em;} #report-logo { height: 35px; width: 50px; } .tab-button {padding: 10px 15px; font-size: 1em;} .summary-card .value {font-size: 1.8em;} .attachments-grid {grid-template-columns: 1fr;} .step-item {padding-left: calc(var(--depth, 0) * 18px);} .test-case-content, .step-details {padding: 15px;} .trend-charts-row {gap: 20px;} .trend-chart {padding: 20px;} }
2045
2338
  </style>
2046
2339
  </head>
@@ -2052,8 +2345,8 @@ function generateHTML(reportData, trendData = null) {
2052
2345
  <h1>Playwright Pulse Report</h1>
2053
2346
  </div>
2054
2347
  <div class="run-info"><strong>Run Date:</strong> ${formatDate(
2055
- runSummary.timestamp
2056
- )}<br><strong>Total Duration:</strong> ${formatDuration(
2348
+ runSummary.timestamp
2349
+ )}<br><strong>Total Duration:</strong> ${formatDuration(
2057
2350
  runSummary.duration
2058
2351
  )}</div>
2059
2352
  </header>
@@ -2061,44 +2354,39 @@ function generateHTML(reportData, trendData = null) {
2061
2354
  <button class="tab-button active" data-tab="dashboard">Dashboard</button>
2062
2355
  <button class="tab-button" data-tab="test-runs">Test Run Summary</button>
2063
2356
  <button class="tab-button" data-tab="test-history">Test History</button>
2064
- <button class="tab-button" data-tab="test-ai">AI Analysis</button>
2357
+ <button class="tab-button" data-tab="ai-failure-analyzer">AI Failure Analyzer</button>
2065
2358
  </div>
2066
2359
  <div id="dashboard" class="tab-content active">
2067
2360
  <div class="dashboard-grid">
2068
- <div class="summary-card"><h3>Total Tests</h3><div class="value">${
2069
- runSummary.totalTests
2070
- }</div></div>
2071
- <div class="summary-card status-passed"><h3>Passed</h3><div class="value">${
2072
- runSummary.passed
2073
- }</div><div class="trend-percentage">${passPercentage}%</div></div>
2074
- <div class="summary-card status-failed"><h3>Failed</h3><div class="value">${
2075
- runSummary.failed
2076
- }</div><div class="trend-percentage">${failPercentage}%</div></div>
2077
- <div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
2078
- runSummary.skipped || 0
2079
- }</div><div class="trend-percentage">${skipPercentage}%</div></div>
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>
2080
2369
  <div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
2081
2370
  <div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
2082
- runSummary.duration
2083
- )}</div></div>
2371
+ runSummary.duration
2372
+ )}</div></div>
2084
2373
  </div>
2085
2374
  <div class="dashboard-bottom-row">
2086
2375
  <div style="display: grid; gap: 20px">
2087
2376
  ${generatePieChart(
2088
- [
2089
- { label: "Passed", value: runSummary.passed },
2090
- { label: "Failed", value: runSummary.failed },
2091
- { label: "Skipped", value: runSummary.skipped || 0 },
2092
- ],
2093
- 400,
2094
- 390
2095
- )}
2096
- ${
2097
- runSummary.environment &&
2098
- Object.keys(runSummary.environment).length > 0
2099
- ? generateEnvironmentDashboard(runSummary.environment)
2100
- : '<div class="no-data">Environment data not available.</div>'
2101
- }
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
+ }
2102
2390
  </div>
2103
2391
  ${generateSuitesWidget(suitesData)}
2104
2392
  </div>
@@ -2108,17 +2396,17 @@ function generateHTML(reportData, trendData = null) {
2108
2396
  <input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
2109
2397
  <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>
2110
2398
  <select id="filter-browser"><option value="">All Browsers</option>${Array.from(
2111
- new Set(
2112
- (results || []).map((test) => test.browser || "unknown")
2113
- )
2114
- )
2115
- .map(
2116
- (browser) =>
2117
- `<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
2118
- browser
2119
- )}</option>`
2120
- )
2121
- .join("")}</select>
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>
2122
2410
  <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>
2123
2411
  </div>
2124
2412
  <div class="test-cases-list">${generateTestCasesHTML()}</div>
@@ -2127,18 +2415,16 @@ function generateHTML(reportData, trendData = null) {
2127
2415
  <h2 class="tab-main-title">Execution Trends</h2>
2128
2416
  <div class="trend-charts-row">
2129
2417
  <div class="trend-chart"><h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
2130
- ${
2131
- trendData && trendData.overall && trendData.overall.length > 0
2132
- ? generateTestTrendsChart(trendData)
2133
- : '<div class="no-data">Overall trend data not available for test counts.</div>'
2134
- }
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
+ }
2135
2422
  </div>
2136
2423
  <div class="trend-chart"><h3 class="chart-title-header">Execution Duration Trends</h3>
2137
- ${
2138
- trendData && trendData.overall && trendData.overall.length > 0
2139
- ? generateDurationTrendChart(trendData)
2140
- : '<div class="no-data">Overall trend data not available for durations.</div>'
2141
- }
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
+ }
2142
2428
  </div>
2143
2429
  </div>
2144
2430
  <h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
@@ -2148,16 +2434,15 @@ function generateHTML(reportData, trendData = null) {
2148
2434
  </div>
2149
2435
  </div>
2150
2436
  <h2 class="tab-main-title">Individual Test History</h2>
2151
- ${
2152
- trendData &&
2153
- trendData.testRuns &&
2154
- Object.keys(trendData.testRuns).length > 0
2155
- ? generateTestHistoryContent(trendData)
2156
- : '<div class="no-data">Individual test history data not available.</div>'
2157
- }
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
+ }
2158
2443
  </div>
2159
- <div id="test-ai" class="tab-content">
2160
- <iframe data-src="https://ai-test-analyser.netlify.app/" width="100%" height="100%" frameborder="0" allowfullscreen class="lazy-load-iframe" title="AI Test Analyser" style="border: none; height: 100vh;"></iframe>
2444
+ <div id="ai-failure-analyzer" class="tab-content">
2445
+ ${generateAIFailureAnalyzerTab(results)}
2161
2446
  </div>
2162
2447
  <footer style="padding: 0.5rem; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); text-align: center; font-family: 'Segoe UI', system-ui, sans-serif;">
2163
2448
  <div style="display: inline-flex; align-items: center; gap: 0.5rem; color: #333; font-size: 0.9rem; font-weight: 600; letter-spacing: 0.5px;">
@@ -2189,7 +2474,135 @@ function generateHTML(reportData, trendData = null) {
2189
2474
  button.textContent = 'Failed';
2190
2475
  setTimeout(() => { button.textContent = 'Copy'; }, 2000);
2191
2476
  });
2192
- }
2477
+ }
2478
+
2479
+ // --- AI Failure Analyzer Functions ---
2480
+ function getAIFix(button) {
2481
+ const modal = document.getElementById('ai-fix-modal');
2482
+ const modalContent = document.getElementById('ai-fix-modal-content');
2483
+ const modalTitle = document.getElementById('ai-fix-modal-title');
2484
+
2485
+ modal.style.display = 'flex';
2486
+ modalTitle.textContent = 'Analyzing...';
2487
+ modalContent.innerHTML = '<div class="ai-loader"></div>';
2488
+
2489
+ try {
2490
+ const testJson = button.dataset.testJson;
2491
+ const test = JSON.parse(atob(testJson));
2492
+
2493
+ const testName = test.name || 'Unknown Test';
2494
+ const failureLogsAndErrors = [
2495
+ 'Error Message:',
2496
+ test.errorMessage || 'Not available.',
2497
+ '\\n\\n--- stdout ---',
2498
+ (test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
2499
+ '\\n\\n--- stderr ---',
2500
+ (test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
2501
+ ].join('\\n');
2502
+ const codeSnippet = test.snippet || '';
2503
+
2504
+ const shortTestName = testName.split(' > ').pop();
2505
+ modalTitle.textContent = \`Analysis for: \${shortTestName}\`;
2506
+
2507
+ const apiUrl = 'https://ai-test-analyser.netlify.app/api/analyze';
2508
+ fetch(apiUrl, {
2509
+ method: 'POST',
2510
+ headers: { 'Content-Type': 'application/json' },
2511
+ body: JSON.stringify({
2512
+ testName: testName,
2513
+ failureLogsAndErrors: failureLogsAndErrors,
2514
+ codeSnippet: codeSnippet,
2515
+ }),
2516
+ })
2517
+ .then(response => {
2518
+ if (!response.ok) {
2519
+ return response.text().then(text => {
2520
+ throw new Error(\`API request failed with status \${response.status}: \${text || response.statusText}\`);
2521
+ });
2522
+ }
2523
+ return response.text();
2524
+ })
2525
+ .then(text => {
2526
+ if (!text) {
2527
+ throw new Error("The AI analyzer returned an empty response. This might happen during high load or if the request was blocked. Please try again in a moment.");
2528
+ }
2529
+ try {
2530
+ return JSON.parse(text);
2531
+ } catch (e) {
2532
+ console.error("Failed to parse JSON:", text);
2533
+ throw new Error(\`The AI analyzer returned an invalid response. \${e.message}\`);
2534
+ }
2535
+ })
2536
+ .then(data => {
2537
+ // Helper function to prevent XSS by escaping HTML characters
2538
+ const escapeHtml = (unsafe) => {
2539
+ if (typeof unsafe !== 'string') return '';
2540
+ return unsafe
2541
+ .replace(/&/g, "&amp;")
2542
+ .replace(/</g, "&lt;")
2543
+ .replace(/>/g, "&gt;")
2544
+ .replace(/"/g, "&quot;")
2545
+ .replace(/'/g, "&#039;");
2546
+ };
2547
+
2548
+ // Build the "Analysis" part from the 'rootCause' field
2549
+ const analysisHtml = \`<h4>Analysis</h4><p>\${escapeHtml(data.rootCause) || 'No analysis provided.'}</p>\`;
2550
+
2551
+ // Build the "Suggestions" part by iterating through the 'suggestedFixes' array
2552
+ let suggestionsHtml = '<h4>Suggestions</h4>';
2553
+ if (data.suggestedFixes && data.suggestedFixes.length > 0) {
2554
+ suggestionsHtml += '<div class="suggestions-list" style="margin-top: 15px;">';
2555
+ data.suggestedFixes.forEach(fix => {
2556
+ suggestionsHtml += \`
2557
+ <div class="suggestion-item" style="margin-bottom: 22px; border-left: 3px solid var(--accent-color-alt); padding-left: 15px;">
2558
+ <p style="margin: 0 0 8px 0; font-weight: 500;">\${escapeHtml(fix.description)}</p>
2559
+ \${fix.codeSnippet ? \`<div class="code-section"><pre><code>\${escapeHtml(fix.codeSnippet)}</code></pre></div>\` : ''}
2560
+ </div>
2561
+ \`;
2562
+ });
2563
+ suggestionsHtml += '</div>';
2564
+ } else {
2565
+ // Fallback if there are no suggestions
2566
+ suggestionsHtml += \`<div class="code-section"><pre><code>No suggestion provided.</code></pre></div>\`;
2567
+ }
2568
+
2569
+ // Combine both parts and set the modal content
2570
+ modalContent.innerHTML = analysisHtml + suggestionsHtml;
2571
+ })
2572
+ .catch(err => {
2573
+ console.error('AI Fix Error:', err);
2574
+ modalContent.innerHTML = \`<div class="test-error-summary"><strong>Error:</strong> Failed to get AI analysis. Please check the console for details. <br><br> \${err.message}</div>\`;
2575
+ });
2576
+
2577
+ } catch (e) {
2578
+ console.error('Error processing test data for AI Fix:', e);
2579
+ modalTitle.textContent = 'Error';
2580
+ modalContent.innerHTML = \`<div class="test-error-summary">Could not process test data. Is it formatted correctly?</div>\`;
2581
+ }
2582
+ }
2583
+
2584
+
2585
+ function closeAiModal() {
2586
+ const modal = document.getElementById('ai-fix-modal');
2587
+ if(modal) modal.style.display = 'none';
2588
+ }
2589
+
2590
+ function toggleErrorDetails(button) {
2591
+ const errorDetails = button.closest('.compact-failure-item').querySelector('.full-error-details');
2592
+ const expandText = button.querySelector('.expand-text');
2593
+ const expandIcon = button.querySelector('.expand-icon');
2594
+
2595
+ if (errorDetails.style.display === 'none' || !errorDetails.style.display) {
2596
+ errorDetails.style.display = 'block';
2597
+ expandText.textContent = 'Hide Full Error';
2598
+ button.classList.add('expanded');
2599
+ } else {
2600
+ errorDetails.style.display = 'none';
2601
+ expandText.textContent = 'Show Full Error';
2602
+ button.classList.remove('expanded');
2603
+ }
2604
+ }
2605
+
2193
2606
  function initializeReportInteractivity() {
2194
2607
  const tabButtons = document.querySelectorAll('.tab-button');
2195
2608
  const tabContents = document.querySelectorAll('.tab-content');
@@ -2202,9 +2615,9 @@ function generateHTML(reportData, trendData = null) {
2202
2615
  const activeContent = document.getElementById(tabId);
2203
2616
  if (activeContent) {
2204
2617
  activeContent.classList.add('active');
2205
- // Check if IntersectionObserver is already handling elements in this tab
2206
- // For simplicity, we assume if an element is observed, it will be handled when it becomes visible.
2207
- // If IntersectionObserver is not supported, already-visible elements would have been loaded by fallback.
2618
+ if ('IntersectionObserver' in window) {
2619
+ // Handled by observer
2620
+ }
2208
2621
  }
2209
2622
  });
2210
2623
  });
@@ -2290,19 +2703,13 @@ function generateHTML(reportData, trendData = null) {
2290
2703
  if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
2291
2704
  if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
2292
2705
  // --- Intersection Observer for Lazy Loading ---
2293
- const lazyLoadElements = document.querySelectorAll('.lazy-load-chart, .lazy-load-iframe');
2706
+ const lazyLoadElements = document.querySelectorAll('.lazy-load-chart');
2294
2707
  if ('IntersectionObserver' in window) {
2295
2708
  let lazyObserver = new IntersectionObserver((entries, observer) => {
2296
2709
  entries.forEach(entry => {
2297
2710
  if (entry.isIntersecting) {
2298
2711
  const element = entry.target;
2299
- if (element.classList.contains('lazy-load-iframe')) {
2300
- if (element.dataset.src) {
2301
- element.src = element.dataset.src;
2302
- element.removeAttribute('data-src'); // Optional: remove data-src after loading
2303
- console.log('Lazy loaded iframe:', element.title || 'Untitled Iframe');
2304
- }
2305
- } else if (element.classList.contains('lazy-load-chart')) {
2712
+ if (element.classList.contains('lazy-load-chart')) {
2306
2713
  const renderFunctionName = element.dataset.renderFunctionName;
2307
2714
  if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
2308
2715
  try {
@@ -2329,12 +2736,7 @@ function generateHTML(reportData, trendData = null) {
2329
2736
  } else { // Fallback for browsers without IntersectionObserver
2330
2737
  console.warn("IntersectionObserver not supported. Loading all items immediately.");
2331
2738
  lazyLoadElements.forEach(element => {
2332
- if (element.classList.contains('lazy-load-iframe')) {
2333
- if (element.dataset.src) {
2334
- element.src = element.dataset.src;
2335
- element.removeAttribute('data-src');
2336
- }
2337
- } else if (element.classList.contains('lazy-load-chart')) {
2739
+ if (element.classList.contains('lazy-load-chart')) {
2338
2740
  const renderFunctionName = element.dataset.renderFunctionName;
2339
2741
  if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
2340
2742
  try {