@arghajit/dummy 0.3.19 → 0.3.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.
@@ -339,6 +339,12 @@ function generateTestTrendsChart(trendData) {
339
339
  color: "var(--warning-color)",
340
340
  marker: { symbol: "circle" },
341
341
  },
342
+ {
343
+ name: "Flaky",
344
+ data: runs.map((r) => r.flaky || 0),
345
+ color: "var(--neutral-500)",
346
+ marker: { symbol: "circle" },
347
+ },
342
348
  ];
343
349
  const runsForTooltip = runs.map((r) => ({
344
350
  runId: r.runId,
@@ -542,6 +548,9 @@ function generateTestHistoryChart(history) {
542
548
  case "skipped":
543
549
  color = "var(--warning-color)";
544
550
  break;
551
+ case "flaky":
552
+ color = "var(--neutral-500)";
553
+ break;
545
554
  default:
546
555
  color = "var(--dark-gray-color)";
547
556
  }
@@ -658,6 +667,9 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
658
667
  case "Failed":
659
668
  color = "var(--danger-color)";
660
669
  break;
670
+ case "Flaky":
671
+ color = "var(--neutral-500)";
672
+ break;
661
673
  case "Skipped":
662
674
  color = "var(--warning-color)";
663
675
  break;
@@ -904,7 +916,10 @@ function generateEnvironmentSection(environmentData) {
904
916
  }
905
917
 
906
918
  function generateEnvironmentDashboard(environment, hideHeader = false) {
907
- const cpuInfo = `model: ${environment.cpu.model}, cores: ${environment.cpu.cores}`;
919
+ const cpuModel = environment.cpu && environment.cpu.model ? environment.cpu.model : "N/A";
920
+ const cpuCores = environment.cpu && environment.cpu.cores ? environment.cpu.cores : "N/A";
921
+ const cpuInfo = `model: ${cpuModel}, cores: ${cpuCores}`;
922
+ const cwdInfo = environment.cwd || "N/A";
908
923
 
909
924
  return `
910
925
  <div class="env-modern-card${hideHeader ? " no-header" : ""}">
@@ -1294,7 +1309,7 @@ function generateEnvironmentDashboard(environment, hideHeader = false) {
1294
1309
  </div>
1295
1310
  <div class="env-item-content">
1296
1311
  <p class="env-item-label">Working Dir</p>
1297
- <div class="env-item-value" title="${environment.cwd}">${environment.cwd.length > 30 ? "..." + environment.cwd.slice(-27) : environment.cwd}</div>
1312
+ <div class="env-item-value" title="${cwdInfo}">${cwdInfo.length > 30 ? "..." + cwdInfo.slice(-27) : cwdInfo}</div>
1298
1313
  </div>
1299
1314
  </div>
1300
1315
  </div>
@@ -1323,11 +1338,11 @@ function generateWorkerDistributionChart(results) {
1323
1338
  const workerId =
1324
1339
  typeof test.workerId !== "undefined" ? test.workerId : "N/A";
1325
1340
  if (!acc[workerId]) {
1326
- acc[workerId] = { passed: 0, failed: 0, skipped: 0, tests: [] };
1341
+ acc[workerId] = { passed: 0, failed: 0, skipped: 0, flaky: 0, tests: [] };
1327
1342
  }
1328
1343
 
1329
1344
  const status = String(test.status).toLowerCase();
1330
- if (status === "passed" || status === "failed" || status === "skipped") {
1345
+ if (status === "passed" || status === "failed" || status === "skipped" || status === "flaky") {
1331
1346
  acc[workerId][status]++;
1332
1347
  }
1333
1348
 
@@ -1372,6 +1387,7 @@ function generateWorkerDistributionChart(results) {
1372
1387
  const passedData = workerIds.map((id) => workerData[id].passed);
1373
1388
  const failedData = workerIds.map((id) => workerData[id].failed);
1374
1389
  const skippedData = workerIds.map((id) => workerData[id].skipped);
1390
+ const flakyData = workerIds.map((id) => workerData[id].flaky);
1375
1391
 
1376
1392
  const categoriesString = JSON.stringify(categories);
1377
1393
  const fullDataString = JSON.stringify(fullWorkerData);
@@ -1379,6 +1395,7 @@ function generateWorkerDistributionChart(results) {
1379
1395
  { name: "Passed", data: passedData, color: "var(--success-color)" },
1380
1396
  { name: "Failed", data: failedData, color: "var(--danger-color)" },
1381
1397
  { name: "Skipped", data: skippedData, color: "var(--warning-color)" },
1398
+ { name: "Flaky", data: flakyData, color: "var(--neutral-500)" },
1382
1399
  ]);
1383
1400
 
1384
1401
  // The HTML now includes the chart container, the modal, and styles for the modal
@@ -1645,6 +1662,7 @@ function generateTestHistoryContent(trendData) {
1645
1662
  <option value="">All Statuses</option>
1646
1663
  <option value="passed">Passed</option>
1647
1664
  <option value="failed">Failed</option>
1665
+ <option value="flaky">Flaky</option>
1648
1666
  <option value="skipped">Skipped</option>
1649
1667
  </select>
1650
1668
  <button id="clear-history-filters" class="clear-filters-btn">Clear Filters</button>
@@ -1717,6 +1735,8 @@ function getStatusClass(status) {
1717
1735
  return "status-failed";
1718
1736
  case "skipped":
1719
1737
  return "status-skipped";
1738
+ case "flaky":
1739
+ return "status-flaky";
1720
1740
  default:
1721
1741
  return "status-unknown";
1722
1742
  }
@@ -1734,6 +1754,8 @@ function getStatusIcon(status) {
1734
1754
  return "❌";
1735
1755
  case "skipped":
1736
1756
  return "⏭️";
1757
+ case "flaky":
1758
+ return "⚠️";
1737
1759
  default:
1738
1760
  return "❓";
1739
1761
  }
@@ -1774,6 +1796,7 @@ function getSuitesData(results) {
1774
1796
  browser: browser,
1775
1797
  passed: 0,
1776
1798
  failed: 0,
1799
+ flaky: 0,
1777
1800
  skipped: 0,
1778
1801
  count: 0,
1779
1802
  statusOverall: "passed",
@@ -1781,12 +1804,15 @@ function getSuitesData(results) {
1781
1804
  }
1782
1805
  const suite = suitesMap.get(key);
1783
1806
  suite.count++;
1784
- const currentStatus = String(test.status).toLowerCase();
1807
+ let currentStatus = String(test.status).toLowerCase();
1808
+ if (test.outcome === 'flaky') currentStatus = 'flaky';
1785
1809
  if (currentStatus && suite[currentStatus] !== undefined) {
1786
1810
  suite[currentStatus]++;
1787
1811
  }
1788
1812
  if (currentStatus === "failed") suite.statusOverall = "failed";
1789
- else if (currentStatus === "skipped" && suite.statusOverall !== "failed")
1813
+ else if (currentStatus === "flaky" && suite.statusOverall !== "failed")
1814
+ suite.statusOverall = "flaky";
1815
+ else if (currentStatus === "skipped" && suite.statusOverall !== "failed" && suite.statusOverall !== "flaky")
1790
1816
  suite.statusOverall = "skipped";
1791
1817
  });
1792
1818
  return Array.from(suitesMap.values());
@@ -1832,8 +1858,8 @@ function generateSuitesWidget(suitesData) {
1832
1858
 
1833
1859
  // Added inline styles for height consistency with Pie Chart (approx 450px) and scrolling
1834
1860
  return `
1835
- <div class="suites-widget" style="height: 450px; display: flex; flex-direction: column;">
1836
- <div class="suites-header" style="flex-shrink: 0;">
1861
+ <div class="suites-widget fixed-height-widget">
1862
+ <div class="suites-header">
1837
1863
  <h2>Test Suites</h2>
1838
1864
  <span class="summary-badge">${
1839
1865
  suitesData.length
@@ -1843,40 +1869,40 @@ function generateSuitesWidget(suitesData) {
1843
1869
  )} tests</span>
1844
1870
  </div>
1845
1871
 
1846
- <div class="suites-grid-container" style="flex-grow: 1; overflow-y: auto; padding-right: 5px;">
1872
+ <div class="suites-grid-container">
1847
1873
  <div class="suites-grid">
1848
1874
  ${suitesData
1849
1875
  .map(
1850
1876
  (suite) => `
1851
1877
  <div class="suite-card status-${suite.statusOverall}">
1852
1878
  <div class="suite-card-header">
1853
- <h3 class="suite-name" title="${sanitizeHTML(
1854
- suite.name,
1855
- )} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
1879
+ <h3 class="suite-name" title="${sanitizeHTML(suite.name)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
1880
+ <div class="status-indicator-dot status-${suite.statusOverall}" title="${suite.statusOverall.charAt(0).toUpperCase() + suite.statusOverall.slice(1)}"></div>
1856
1881
  </div>
1857
- <div style="margin-bottom: 12px;"><span class="browser-tag" title="🌐 ${sanitizeHTML(suite.browser)}">🌐 ${sanitizeHTML(
1858
- suite.browser,
1859
- )}</span></div>
1882
+
1883
+ <div class="browser-tag" title="🌐Browser: ${sanitizeHTML(suite.browser)}">
1884
+ <span style="font-size: 1.1em;">🌐</span> ${sanitizeHTML(suite.browser)}
1885
+ </div>
1886
+
1860
1887
  <div class="suite-card-body">
1861
- <span class="test-count">${suite.count} test${
1862
- suite.count !== 1 ? "s" : ""
1863
- }</span>
1888
+ <span class="test-count-label">${suite.count} Test${suite.count !== 1 ? "s" : ""}</span>
1864
1889
  <div class="suite-stats">
1865
- ${
1866
- suite.passed > 0
1867
- ? `<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>`
1868
- : ""
1869
- }
1870
- ${
1871
- suite.failed > 0
1872
- ? `<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>`
1873
- : ""
1874
- }
1875
- ${
1876
- suite.skipped > 0
1877
- ? `<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>`
1878
- : ""
1879
- }
1890
+ <span class="stat-pill passed" title="Passed">
1891
+ <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" 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>
1892
+ ${suite.passed}
1893
+ </span>
1894
+ <span class="stat-pill failed" title="Failed">
1895
+ <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" 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>
1896
+ ${suite.failed}
1897
+ </span>
1898
+ <span class="stat-pill flaky" title="Flaky">
1899
+ <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" 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>
1900
+ ${suite.flaky || 0}
1901
+ </span>
1902
+ <span class="stat-pill skipped" title="Skipped">
1903
+ <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" 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>
1904
+ ${suite.skipped}
1905
+ </span>
1880
1906
  </div>
1881
1907
  </div>
1882
1908
  </div>`,
@@ -2255,6 +2281,7 @@ function generateSeverityDistributionChart(results) {
2255
2281
  const data = {
2256
2282
  passed: [0, 0, 0, 0, 0],
2257
2283
  failed: [0, 0, 0, 0, 0],
2284
+ flaky: [0, 0, 0, 0, 0],
2258
2285
  skipped: [0, 0, 0, 0, 0],
2259
2286
  };
2260
2287
 
@@ -2273,6 +2300,8 @@ function generateSeverityDistributionChart(results) {
2273
2300
  status === "interrupted"
2274
2301
  ) {
2275
2302
  data.failed[index]++;
2303
+ } else if (status === "flaky") {
2304
+ data.flaky[index]++;
2276
2305
  } else {
2277
2306
  data.skipped[index]++;
2278
2307
  }
@@ -2286,6 +2315,7 @@ function generateSeverityDistributionChart(results) {
2286
2315
  const seriesData = [
2287
2316
  { name: "Passed", data: data.passed, color: "var(--success-color)" },
2288
2317
  { name: "Failed", data: data.failed, color: "var(--danger-color)" },
2318
+ { name: "Flaky", data: data.flaky, color: "var(--neutral-500)" },
2289
2319
  { name: "Skipped", data: data.skipped, color: "var(--warning-color)" },
2290
2320
  ];
2291
2321
 
@@ -2429,32 +2459,76 @@ function generateHTML(reportData, trendData = null) {
2429
2459
  duration: 0,
2430
2460
  timestamp: new Date().toISOString(),
2431
2461
  };
2432
- const totalTestsOr1 = runSummary.totalTests || 1;
2433
- const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
2434
- const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
2435
- const skipPercentage = Math.round(
2436
- ((runSummary.skipped || 0) / totalTestsOr1) * 100,
2437
- );
2462
+
2438
2463
  const avgTestDuration =
2439
2464
  runSummary.totalTests > 0
2440
2465
  ? formatDuration(runSummary.duration / runSummary.totalTests)
2441
2466
  : "0.0s";
2442
2467
 
2468
+ const flakyCount = (results || []).filter(r => r.outcome === 'flaky').length;
2469
+
2443
2470
  // Calculate retry statistics
2444
2471
  const totalRetried = (results || []).reduce((acc, test) => {
2445
- if (test.retries && test.retries > 0) {
2446
- return acc + 1;
2472
+ if (test.retryHistory && test.retryHistory.length > 0) {
2473
+ return acc + test.retryHistory.length;
2447
2474
  }
2448
2475
  return acc;
2449
2476
  }, 0);
2450
2477
 
2478
+ // --- RECALCULATE KPI METRICS BASED ON FINAL_STATUS ---
2479
+ let calculatedPassed = 0;
2480
+ let calculatedFailed = 0;
2481
+ let calculatedSkipped = 0;
2482
+ let calculatedFlaky = 0;
2483
+ let calculatedTotal = 0;
2484
+
2485
+ (results || []).forEach(test => {
2486
+ calculatedTotal++;
2487
+ // New Logic: If outcome is 'flaky', it overrides everything.
2488
+ let statusToUse = test.status;
2489
+ if (test.outcome === 'flaky') {
2490
+ statusToUse = 'flaky';
2491
+ } else if (test.status === 'flaky') {
2492
+ // Just in case outcome wasn't set but status was (unlikely with new reporter)
2493
+ statusToUse = 'flaky';
2494
+ } else if (test.retryHistory && test.retryHistory.length > 0 && test.final_status) {
2495
+ statusToUse = test.final_status;
2496
+ }
2497
+
2498
+ // Update test status in place for charts
2499
+ test.status = statusToUse;
2500
+
2501
+ const s = String(statusToUse).toLowerCase();
2502
+ if (s === 'passed') calculatedPassed++;
2503
+ else if (s === 'skipped') calculatedSkipped++;
2504
+ else if (s === 'flaky') calculatedFlaky++;
2505
+ else calculatedFailed++; // failed, timedout, interrupted
2506
+ });
2507
+
2508
+ // Override runSummary counts with our calculated ones if results exist
2509
+ if (results && results.length > 0) {
2510
+ runSummary.passed = calculatedPassed;
2511
+ runSummary.failed = calculatedFailed;
2512
+ runSummary.skipped = calculatedSkipped;
2513
+ runSummary.flaky = calculatedFlaky;
2514
+ runSummary.totalTests = calculatedTotal;
2515
+ }
2516
+
2517
+ const totalTestsOr1 = runSummary.totalTests || 1;
2518
+ const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
2519
+ const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
2520
+ const skipPercentage = Math.round(
2521
+ ((runSummary.skipped || 0) / totalTestsOr1) * 100,
2522
+ );
2523
+ const flakyPercentage = Math.round(((runSummary.flaky || 0) / totalTestsOr1) * 100);
2524
+
2525
+
2451
2526
  // Calculate browser distribution
2452
2527
  const browserStats = (results || []).reduce((acc, test) => {
2453
2528
  let browserName = "unknown";
2454
2529
  if (test.browser) {
2455
- // Extract browser name from strings like "Chrome v143 on Windows 10"
2456
- const match = test.browser.match(/^(\w+)/);
2457
- browserName = match ? match[1] : test.browser;
2530
+ // Use full browser name
2531
+ browserName = test.browser;
2458
2532
  }
2459
2533
  acc[browserName] = (acc[browserName] || 0) + 1;
2460
2534
  return acc;
@@ -2495,6 +2569,10 @@ function generateHTML(reportData, trendData = null) {
2495
2569
  const severity = test.severity || "Medium";
2496
2570
  const severityBadge = `<span class="severity-badge" data-severity="${severity.toLowerCase()}">${severity}</span>`;
2497
2571
 
2572
+ // --- Retry Count Badge (only show if retries occurred) ---
2573
+ const retryCount = test.retryHistory ? test.retryHistory.length : 0;
2574
+ const retryBadge = retryCount > 0 ? `<span class="retry-badge">Retry Count: ${retryCount}</span>` : '';
2575
+
2498
2576
  // --- Step Generation ---
2499
2577
  const generateStepsHTML = (steps, depth = 0) => {
2500
2578
  if (!steps || steps.length === 0)
@@ -2546,7 +2624,7 @@ function generateHTML(reportData, trendData = null) {
2546
2624
  )}</div>`
2547
2625
  : ""
2548
2626
  }
2549
- <button class="copy-error-btn" onclick="copyErrorToClipboard(this)" style="margin-top: 8px; padding: 6px 12px; background: rgba(248, 113, 113, 0.15); border: 2px solid var(--danger-color); border-radius: 6px; cursor: pointer; font-size: 12px; color: var(--danger-color); font-weight: 600; transition: all 0.2s;" onmouseover="this.style.background='rgba(248, 113, 113, 0.25)'" onmouseout="this.style.background='rgba(248, 113, 113, 0.15)'">Copy Error Prompt</button>
2627
+ <button class="copy-error-btn" onclick="copyErrorToClipboard(this)" style="margin-top: 8px; padding: 6px 12px; background: rgba(248, 113, 113, 0.15); border: 2px solid var(--danger-color); border-radius: 6px; cursor: pointer; font-size: 12px; color: var(--danger-color); font-weight: 600; transition: all 0.2s; align-self: flex-end; width: auto;" onmouseover="this.style.background='rgba(248, 113, 113, 0.25)'" onmouseout="this.style.background='rgba(248, 113, 113, 0.15)'">Copy Error Prompt</button>
2550
2628
  </div>`
2551
2629
  : ""
2552
2630
  }
@@ -2564,43 +2642,35 @@ function generateHTML(reportData, trendData = null) {
2564
2642
  .join("");
2565
2643
  };
2566
2644
 
2567
- return `
2568
- <div class="test-case" data-status="${
2569
- test.status
2570
- }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
2571
- .join(",")
2572
- .toLowerCase()}">
2573
- <div class="test-case-header" role="button" aria-expanded="false">
2574
- <div class="test-case-summary">
2575
- <span class="test-case-title" title="${sanitizeHTML(
2576
- test.name,
2577
- )}">${sanitizeHTML(testTitle)}</span>
2578
- <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
2579
- </div>
2580
- <div class="test-case-meta">
2581
- ${severityBadge}
2582
- ${
2583
- test.tags && test.tags.length > 0
2584
- ? test.tags
2585
- .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
2586
- .join(" ")
2587
- : ""
2588
- }
2589
- </div>
2590
- <div class="test-case-status-duration">
2591
- <span class="status-badge ${getStatusClass(test.status)}">${String(
2592
- test.status,
2593
- ).toUpperCase()}</span>
2594
- <span class="test-duration">${formatDuration(test.duration)}</span>
2595
- </div>
2596
- </div>
2597
- <div class="test-case-content" style="display: none;">
2598
- <p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
2645
+ // Helper for Tab Badges
2646
+ const getSmallStatusBadge = (status) => {
2647
+ const s = String(status).toLowerCase();
2648
+ let colorVar = 'var(--text-tertiary)';
2649
+ if(s === 'passed') colorVar = 'var(--success-color)';
2650
+ else if(s === 'failed') colorVar = 'var(--danger-color)';
2651
+ else if(s === 'skipped') colorVar = 'var(--warning-color)';
2652
+
2653
+ return `<span style="
2654
+ display: inline-block;
2655
+ width: 8px;
2656
+ height: 8px;
2657
+ border-radius: 50%;
2658
+ background-color: ${colorVar};
2659
+ margin-left: 6px;
2660
+ vertical-align: middle;
2661
+ " title="${s}"></span>`;
2662
+ };
2663
+
2664
+ // Function to generate test content HTML (used for base run and retry tabs)
2665
+ const getTestContentHTML = (testData, runSuffix) => {
2666
+ const logId = `stdout-log-${test.id || testIndex}-${runSuffix}`;
2667
+ return `
2668
+ <p><strong>Full Path:</strong> ${sanitizeHTML(testData.name)}</p>
2599
2669
  ${
2600
- test.annotations && test.annotations.length > 0
2601
- ? `<div class="annotations-section">
2602
- <h4 style="margin-top: 0; margin-bottom: 10px; font-size: 1.1em;">📌 Annotations</h4>
2603
- ${test.annotations
2670
+ testData.annotations && testData.annotations.length > 0
2671
+ ? `<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;">
2672
+ <h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
2673
+ ${testData.annotations
2604
2674
  .map((annotation, idx) => {
2605
2675
  const isIssueOrBug =
2606
2676
  annotation.type === "issue" ||
@@ -2608,176 +2678,312 @@ function generateHTML(reportData, trendData = null) {
2608
2678
  const descriptionText = annotation.description || "";
2609
2679
  const typeLabel = sanitizeHTML(annotation.type);
2610
2680
  const descriptionHtml =
2611
- isIssueOrBug && descriptionText.match(/^[A-Z]+-\d+$/)
2681
+ isIssueOrBug && descriptionText.match(/^[A-Z]+-\\d+$/)
2612
2682
  ? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
2613
2683
  descriptionText,
2614
- )}" style="color: var(--info-color); text-decoration: underline; cursor: pointer; font-weight: 600;">${sanitizeHTML(
2684
+ )}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
2615
2685
  descriptionText,
2616
2686
  )}</a>`
2617
2687
  : sanitizeHTML(descriptionText);
2618
2688
  const locationText = annotation.location
2619
- ? `<div style="font-size: 0.85em; color: var(--text-tertiary); margin-top: 4px;">Location: ${sanitizeHTML(
2689
+ ? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
2620
2690
  annotation.location.file,
2621
2691
  )}:${annotation.location.line}:${
2622
2692
  annotation.location.column
2623
2693
  }</div>`
2624
2694
  : "";
2625
2695
  return `<div style="margin-bottom: ${
2626
- idx < test.annotations.length - 1 ? "10px" : "0"
2627
- };"><strong>Type:</strong> <span style="background-color: rgba(139, 92, 246, 0.2); padding: 2px 8px; border-radius: 4px; font-size: 0.9em;">${typeLabel}</span>${
2696
+ idx < testData.annotations.length - 1 ? "10px" : "0"
2697
+ };">
2698
+ <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>
2699
+ ${
2628
2700
  descriptionText
2629
- ? `<br><strong>Description:</strong> ${descriptionHtml}`
2701
+ ? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
2630
2702
  : ""
2631
- }${locationText}</div>`;
2703
+ }
2704
+ ${locationText}
2705
+ </div>`;
2632
2706
  })
2633
2707
  .join("")}
2634
2708
  </div>`
2635
2709
  : ""
2636
2710
  }
2637
2711
  <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
2638
- test.workerId,
2712
+ testData.workerId,
2639
2713
  )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
2640
- test.totalWorkers,
2714
+ testData.totalWorkers,
2641
2715
  )}]</p>
2642
2716
  ${
2643
- test.errorMessage
2644
- ? `<div class="test-error-summary">${formatPlaywrightError(
2645
- test.errorMessage,
2646
- )}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)" style="margin-top: 8px; padding: 4px 8px; background: #f0f0f0; border: 2px solid #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; border-color: #8B0000; color: #8B0000;" onmouseover="this.style.background='#e0e0e0'" onmouseout="this.style.background='#f0f0f0'">Copy Error Prompt</button></div>`
2717
+ testData.errorMessage
2718
+ ? `<div class="test-error-summary"><div class="stack-trace">${formatPlaywrightError(
2719
+ testData.errorMessage,
2720
+ )}</div>
2721
+ <button
2722
+ class="copy-error-btn"
2723
+ onclick="copyErrorToClipboard(this)"
2724
+ style="
2725
+ margin-top: 8px;
2726
+ padding: 6px 12px;
2727
+ background: rgba(248, 113, 113, 0.15);
2728
+ border: 2px solid var(--danger-color);
2729
+ border-radius: 6px;
2730
+ cursor: pointer;
2731
+ font-size: 12px;
2732
+ color: var(--danger-color);
2733
+ font-weight: 600;
2734
+ transition: 0.2s;
2735
+ align-self: flex-end;
2736
+ width: auto;
2737
+ "
2738
+ onmouseover="this.style.background='#e0e0e0'"
2739
+ onmouseout="this.style.background='#f0f0f0'"
2740
+ >
2741
+ Copy Error Prompt
2742
+ </button>
2743
+ </div>`
2647
2744
  : ""
2648
2745
  }
2649
2746
  ${
2650
- test.snippet
2747
+ testData.snippet
2651
2748
  ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
2652
- test.snippet,
2749
+ testData.snippet,
2653
2750
  )}</code></pre></div>`
2654
2751
  : ""
2655
2752
  }
2656
2753
  <h4>Steps</h4>
2657
- <div class="steps-list">${generateStepsHTML(test.steps)}</div>
2658
-
2659
- ${(() => {
2660
- if (!test.stdout || test.stdout.length === 0) return "";
2661
- // FIXED: Now using 'testIndex' which is guaranteed to be defined
2662
- const logId = `stdout-log-${test.id || testIndex}`;
2663
- return `<div class="console-output-section"><h4>Console Output (stdout) <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy</button></h4><div class="log-wrapper"><pre id="${logId}" class="console-log stdout-log" style="background-color: var(--bg-tertiary); color: #f3e8c3; padding: 1.25em; border-radius: 0.85em; line-height: 1.2; border: 1px solid var(--border-light);">${formatPlaywrightError(
2664
- test.stdout.map((line) => sanitizeHTML(line)).join("\n"),
2665
- )}</pre></div></div>`;
2666
- })()}
2667
-
2668
- ${(() => {
2669
- if (!test.stderr || test.stderr.length === 0) return "";
2670
- // FIXED: Using 'testIndex'
2671
- const logId = `stderr-log-${test.id || testIndex}`;
2672
- return `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log" style="background-color: var(--bg-tertiary); color: #f87171; padding: 1.25em; border-radius: 0.85em; line-height: 1.2; border: 1px solid var(--border-light);">${formatPlaywrightError(
2673
- test.stderr.map((line) => sanitizeHTML(line)).join("\n"),
2674
- )}</pre></div>`;
2675
- })()}
2676
-
2754
+ <div class="steps-list">${generateStepsHTML(testData.steps)}</div>
2677
2755
  ${(() => {
2678
- if (!test.screenshots || test.screenshots.length === 0) return "";
2679
- const screenshotsHTML = test.screenshots
2680
- .map((screenshotPath, sIndex) => {
2681
- try {
2682
- const imagePath = path.resolve(
2683
- DEFAULT_OUTPUT_DIR,
2684
- screenshotPath,
2685
- );
2686
- if (!fsExistsSync(imagePath))
2687
- return `<div class="attachment-item error">Screenshot not found</div>`;
2688
- const base64ImageData =
2689
- readFileSync(imagePath).toString("base64");
2690
- // LAZY LOAD: Using helper with unique ID
2691
- return createLazyMedia(
2692
- base64ImageData,
2693
- "image/png",
2694
- "image",
2695
- sIndex + 1,
2696
- `screenshot-${testIndex}-${sIndex}.png`,
2697
- );
2698
- } catch (e) {
2699
- return `<div class="attachment-item error">Error loading screenshot</div>`;
2700
- }
2701
- })
2702
- .join("");
2703
- return `<div class="attachments-section"><h4>Screenshots</h4><div class="attachments-grid">${screenshotsHTML}</div></div>`;
2756
+ if (!testData.stdout || testData.stdout.length === 0) return "";
2757
+ return `<div class="console-output-section">
2758
+ <h4>Console Output (stdout)
2759
+ <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy</ button>
2760
+ </h4>
2761
+ <div class="log-wrapper">
2762
+ <pre id="${logId}" class="console-log stdout-log" style="background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
2763
+ testData.stdout
2764
+ .map((line) => sanitizeHTML(line))
2765
+ .join("\\n"),
2766
+ )}</pre>
2767
+ </div>
2768
+ </div>`;
2704
2769
  })()}
2705
-
2706
- ${(() => {
2707
- if (!test.videoPath || test.videoPath.length === 0) return "";
2708
- const videosHTML = test.videoPath
2709
- .map((videoPath, vIndex) => {
2710
- try {
2711
- const videoFilePath = path.resolve(
2712
- DEFAULT_OUTPUT_DIR,
2713
- videoPath,
2714
- );
2715
- if (!fsExistsSync(videoFilePath))
2716
- return `<div class="attachment-item error">Video not found</div>`;
2717
- const videoBase64 =
2718
- readFileSync(videoFilePath).toString("base64");
2719
- const ext = path.extname(videoPath).slice(1).toLowerCase();
2720
- const mime =
2721
- { mp4: "video/mp4", webm: "video/webm" }[ext] ||
2722
- "video/mp4";
2723
- // LAZY LOAD: Using helper with unique ID
2724
- return createLazyMedia(
2725
- videoBase64,
2726
- mime,
2727
- "video",
2728
- vIndex + 1,
2729
- `video-${testIndex}-${vIndex}.${ext}`,
2730
- );
2731
- } catch (e) {
2732
- return `<div class="attachment-item error">Error loading video</div>`;
2733
- }
2734
- })
2735
- .join("");
2736
- return `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${videosHTML}</div></div>`;
2737
- })()}
2738
-
2739
2770
  ${
2740
- test.tracePath
2741
- ? `<div class="attachments-section"><h4>Trace Files</h4><div class="attachments-grid"><div class="attachment-item trace-item"><div class="trace-preview"><span class="trace-icon">📄</span><span class="trace-name">${sanitizeHTML(
2742
- path.basename(test.tracePath),
2743
- )}</span></div><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
2744
- test.tracePath,
2745
- )}" target="_blank" download="${sanitizeHTML(
2746
- path.basename(test.tracePath),
2747
- )}" class="download-trace">Download Trace</a></div></div></div></div></div>`
2771
+ testData.stderr && testData.stderr.length > 0
2772
+ ? `<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(
2773
+ testData.stderr.map((line) => sanitizeHTML(line)).join("\\n"),
2774
+ )}</pre></div>`
2748
2775
  : ""
2749
2776
  }
2750
2777
  ${
2751
- test.attachments && test.attachments.length > 0
2752
- ? `<div class="attachments-section"><h4>Other Attachments</h4><div class="attachments-grid">${test.attachments
2778
+ testData.screenshots && testData.screenshots.length > 0
2779
+ ? `
2780
+ <div class="attachments-section">
2781
+ <h4>Screenshots</h4>
2782
+ <div class="attachments-grid">
2783
+ ${testData.screenshots
2753
2784
  .map(
2754
- (attachment) =>
2755
- `<div class="attachment-item generic-attachment"><div class="attachment-icon">${getAttachmentIcon(
2756
- attachment.contentType,
2757
- )}</div><div class="attachment-caption"><span class="attachment-name" title="${sanitizeHTML(
2758
- attachment.name,
2759
- )}">${sanitizeHTML(
2760
- attachment.name,
2761
- )}</span><span class="attachment-type">${sanitizeHTML(
2762
- attachment.contentType,
2763
- )}</span></div><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
2764
- attachment.path,
2765
- )}" target="_blank" class="view-full">View</a><a href="${sanitizeHTML(
2766
- attachment.path,
2767
- )}" target="_blank" download="${sanitizeHTML(
2768
- attachment.name,
2769
- )}" class="download-trace">Download</a></div></div></div>`,
2785
+ (screenshot, screenshotIndex) => `
2786
+ <div class="attachment-item">
2787
+ <img src="${sanitizeHTML(screenshot)}" alt="Screenshot ${
2788
+ screenshotIndex + 1
2789
+ }">
2790
+ <div class="attachment-info">
2791
+ <div class="trace-actions">
2792
+ <a href="${sanitizeHTML(
2793
+ screenshot,
2794
+ )}" target="_blank" class="view-full">View Full Image</a>
2795
+ <a href="${sanitizeHTML(
2796
+ screenshot,
2797
+ )}" target="_blank" download="screenshot-${Date.now()}-${screenshotIndex}.png">Download</a>
2798
+ </div>
2799
+ </div>
2800
+ </div>
2801
+ `,
2770
2802
  )
2803
+ .join("")}
2804
+ </div>
2805
+ </div>
2806
+ `
2807
+ : ""
2808
+ }
2809
+ ${
2810
+ testData.videoPath && testData.videoPath.length > 0
2811
+ ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${testData.videoPath
2812
+ .map((videoUrl, videoIndex) => {
2813
+ const fixedVideoUrl = sanitizeHTML(videoUrl);
2814
+ const fileExtension = String(fixedVideoUrl)
2815
+ .split(".")
2816
+ .pop()
2817
+ .toLowerCase();
2818
+ const mimeType =
2819
+ {
2820
+ mp4: "video/mp4",
2821
+ webm: "video/webm",
2822
+ ogg: "video/ogg",
2823
+ mov: "video/quicktime",
2824
+ avi: "video/x-msvideo",
2825
+ }[fileExtension] || "video/mp4";
2826
+ return `<div class="attachment-item video-item">
2827
+ <video controls width="100%" height="auto" title="Video ${
2828
+ videoIndex + 1
2829
+ }">
2830
+ <source src="${sanitizeHTML(
2831
+ fixedVideoUrl,
2832
+ )}" type="${mimeType}">
2833
+ Your browser does not support the video tag.
2834
+ </video>
2835
+ <div class="attachment-info">
2836
+ <div class="trace-actions">
2837
+ <a href="${sanitizeHTML(
2838
+ fixedVideoUrl,
2839
+ )}" target="_blank" download="video-${Date.now()}-${videoIndex}.${fileExtension}">Download</a>
2840
+ </div>
2841
+ </div>
2842
+ </div>`;
2843
+ })
2771
2844
  .join("")}</div></div>`
2772
2845
  : ""
2773
2846
  }
2774
2847
  ${
2775
- test.codeSnippet
2776
- ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
2777
- sanitizeHTML(test.codeSnippet),
2778
- )}</code></pre></div>`
2848
+ testData.tracePath
2849
+ ? `
2850
+ <div class="attachments-section">
2851
+ <h4>Trace Files</h4>
2852
+ <div class="attachments-grid">
2853
+ <div class="attachment-item trace-item">
2854
+ <div class="trace-preview">
2855
+ <span class="trace-icon">📄</span>
2856
+ <span class="trace-name">${sanitizeHTML(
2857
+ path.basename(testData.tracePath),
2858
+ )}</span>
2859
+ </div>
2860
+ <div class="attachment-info">
2861
+ <div class="trace-actions">
2862
+ <a href="${sanitizeHTML(
2863
+ sanitizeHTML(testData.tracePath),
2864
+ )}" target="_blank" download="${sanitizeHTML(
2865
+ path.basename(testData.tracePath),
2866
+ )}" class="download-trace">Download Trace</a>
2867
+ </div>
2868
+ </div>
2869
+ </div>
2870
+ </div>
2871
+ </div>
2872
+ `
2779
2873
  : ""
2780
2874
  }
2875
+ ${
2876
+ testData.attachments && testData.attachments.length > 0
2877
+ ? `
2878
+ <div class="attachments-section">
2879
+ <h4>Other Attachments</h4>
2880
+ <div class="attachments-grid">
2881
+ ${testData.attachments
2882
+ .map(
2883
+ (attachment) => `
2884
+ <div class="attachment-item generic-attachment">
2885
+ <div class="attachment-icon">${getAttachmentIcon(
2886
+ attachment.contentType,
2887
+ )}</div>
2888
+ <div class="attachment-caption">
2889
+ <span class="attachment-name" title="${sanitizeHTML(
2890
+ attachment.name,
2891
+ )}">${sanitizeHTML(attachment.name)}</span>
2892
+ <span class="attachment-type">${sanitizeHTML(
2893
+ attachment.contentType,
2894
+ )}</span>
2895
+ </div>
2896
+ <div class="attachment-info">
2897
+ <div class="trace-actions">
2898
+ <a href="${sanitizeHTML(
2899
+ sanitizeHTML(attachment.path),
2900
+ )}" target="_blank" class="view-full">View</a>
2901
+ <a href="${sanitizeHTML(
2902
+ sanitizeHTML(attachment.path),
2903
+ )}" target="_blank" download="${sanitizeHTML(
2904
+ attachment.name,
2905
+ )}" class="download-trace">Download</a>
2906
+ </div>
2907
+ </div>
2908
+ </div>
2909
+ `,
2910
+ )
2911
+ .join("")}
2912
+ </div>
2913
+ </div>
2914
+ `
2915
+ : ""
2916
+ }
2917
+ `;
2918
+ };
2919
+
2920
+
2921
+ // Determine header status: use final_status if retried, else normal status
2922
+ const headerStatus = (test.retryHistory && test.retryHistory.length > 0 && test.final_status)
2923
+ ? test.final_status
2924
+ : test.status;
2925
+
2926
+ const outcomeBadge = (test.outcome && test.outcome !== 'flaky')
2927
+ ? `<span class="outcome-badge ${test.outcome}">${test.outcome}</span>`
2928
+ : '';
2929
+
2930
+ return `
2931
+ <div class="test-case" data-status="${
2932
+ headerStatus
2933
+ }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
2934
+ .join(",")
2935
+ .toLowerCase()}">
2936
+ <div class="test-case-header" role="button" aria-expanded="false">
2937
+ <div class="test-case-summary">
2938
+ <span class="test-case-title" title="${sanitizeHTML(
2939
+ test.name,
2940
+ )}">${sanitizeHTML(testTitle)}</span>
2941
+ <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
2942
+ </div>
2943
+ <div class="test-case-meta">
2944
+ ${severityBadge}
2945
+ ${retryBadge}
2946
+ ${outcomeBadge}
2947
+ ${
2948
+ test.tags && test.tags.length > 0
2949
+ ? test.tags
2950
+ .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
2951
+ .join(" ")
2952
+ : ""
2953
+ }
2954
+ </div>
2955
+ <div class="test-case-status-duration">
2956
+ <span class="status-badge ${getStatusClass(headerStatus)}">${String(
2957
+ headerStatus,
2958
+ ).toUpperCase()}</span>
2959
+ <span class="test-duration">${formatDuration(test.duration)}</span>
2960
+ </div>
2961
+ </div>
2962
+ <div class="test-case-content" style="display: none;">
2963
+ ${test.retryHistory && test.retryHistory.length > 0 ? `
2964
+ <div class="retry-tabs-container">
2965
+ <div class="retry-tabs-header">
2966
+ <button class="retry-tab active" onclick="switchRetryTab(event, 'base-run-${test.id}')">
2967
+ Base Run ${getSmallStatusBadge(test.final_status || test.status)}
2968
+ </button>
2969
+ ${test.retryHistory.map((retry, idx) => `
2970
+ <button class="retry-tab" onclick="switchRetryTab(event, 'retry-${idx + 1}-${test.id}')">
2971
+ Retry ${idx + 1} ${getSmallStatusBadge(retry.final_status || retry.status)}
2972
+ </button>
2973
+ `).join('')}
2974
+ </div>
2975
+
2976
+ <div id="base-run-${test.id}" class="retry-tab-content active">
2977
+ ${getTestContentHTML(test, 'base')}
2978
+ </div>
2979
+
2980
+ ${test.retryHistory.map((retry, idx) => `
2981
+ <div id="retry-${idx + 1}-${test.id}" class="retry-tab-content" style="display: none;">
2982
+ ${getTestContentHTML(retry, `retry-${idx + 1}`)}
2983
+ </div>
2984
+ `).join('')}
2985
+ </div>
2986
+ ` : getTestContentHTML(test, 'single')}
2781
2987
  </div>
2782
2988
  </div>`;
2783
2989
  })
@@ -2819,6 +3025,8 @@ function generateHTML(reportData, trendData = null) {
2819
3025
  --light-gray-color: #262626; --medium-gray-color: #333333; --dark-gray-color: #a3a3a3;
2820
3026
  --text-color: #f9fafb; --text-color-secondary: #e5e7eb; --border-color: #262626;
2821
3027
  --card-background-color: #0d0d0d;
3028
+ --neutral-100: #171717; --neutral-200: #262626; --neutral-300: #404040;
3029
+ --bg-hover: #171717;
2822
3030
  --font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
2823
3031
  --radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-xl: 20px; --radius-2xl: 24px;
2824
3032
  --shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.8);
@@ -2910,7 +3118,6 @@ function generateHTML(reportData, trendData = null) {
2910
3118
  background: var(--gradient-card);
2911
3119
  border-radius: 14px;
2912
3120
  box-shadow: var(--shadow-md);
2913
- border: 1px solid var(--border-light);
2914
3121
  overflow: hidden;
2915
3122
  }
2916
3123
  .run-info-item {
@@ -3084,8 +3291,17 @@ function generateHTML(reportData, trendData = null) {
3084
3291
  color: #f9fafb;
3085
3292
  text-transform: capitalize;
3086
3293
  font-size: 1.05em;
3294
+ white-space: nowrap;
3295
+ overflow: hidden;
3296
+ text-overflow: ellipsis;
3297
+ flex: 1;
3298
+ min-width: 0;
3299
+ margin-right: 8px;
3087
3300
  }
3088
3301
  .browser-stats {
3302
+ color: #9ca3af;
3303
+ white-space: nowrap;
3304
+ flex-shrink: 0;
3089
3305
  color: #9ca3af;
3090
3306
  font-weight: 700;
3091
3307
  font-size: 0.95em;
@@ -3139,17 +3355,9 @@ function generateHTML(reportData, trendData = null) {
3139
3355
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
3140
3356
  gap: 20px;
3141
3357
  }
3358
+ /* Updated Suite Cards in Main Block */
3142
3359
  .suite-card {
3143
- background: var(--bg-secondary);
3144
- border: 1px solid var(--border-light);
3145
- border-radius: 8px;
3146
- padding: 20px;
3147
- transition: all 0.2s ease;
3148
- }
3149
- .suite-card:hover {
3150
- transform: translateY(-2px);
3151
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
3152
- border-color: var(--primary-color);
3360
+ /* See line ~3455 for main definition */
3153
3361
  }
3154
3362
  .suite-name {
3155
3363
  font-size: 1.1em;
@@ -3262,6 +3470,16 @@ function generateHTML(reportData, trendData = null) {
3262
3470
  .summary-card.status-skipped .value {
3263
3471
  color: #f59e0b;
3264
3472
  }
3473
+ .summary-card.flaky-status {
3474
+ background: rgba(156, 163, 175, 0.08);
3475
+ }
3476
+ .summary-card.flaky-status:hover {
3477
+ background: rgba(156, 163, 175, 0.15);
3478
+ box-shadow: 0 4px 12px rgba(156, 163, 175, 0.2);
3479
+ }
3480
+ .summary-card.flaky-status .value {
3481
+ color: #9ca3af;
3482
+ }
3265
3483
  .summary-card:not([class*='status-']) .value {
3266
3484
  color: #f9fafb;
3267
3485
  }
@@ -3324,70 +3542,171 @@ function generateHTML(reportData, trendData = null) {
3324
3542
  .status-badge-small-tooltip.status-unknown {
3325
3543
  background-color: #9ca3af;
3326
3544
  }
3327
- .suites-header {
3328
- display: flex;
3329
- justify-content: space-between;
3330
- align-items: center;
3331
- margin-bottom: 20px;
3332
- }
3333
- .summary-badge {
3334
- background-color: var(--border-light);
3335
- color: var(--text-secondary);
3336
- padding: 7px 14px;
3337
- border-radius: 16px;
3338
- font-size: 0.9em;
3545
+ .suites-header {
3546
+ flex-shrink: 0;
3547
+ display: flex;
3548
+ justify-content: space-between;
3549
+ align-items: center;
3550
+ margin-bottom: 20px;
3339
3551
  }
3340
- .suites-grid {
3341
- display: grid;
3342
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
3343
- gap: 20px;
3552
+ .summary-badge { background-color: var(--border-light); color: var(--text-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
3553
+ .suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
3554
+ .suites-widget {
3555
+ display: flex;
3556
+ flex-direction: column;
3344
3557
  }
3345
- .suite-card {
3346
- border: none;
3347
- border-left: 4px solid var(--border-light);
3348
- padding: 24px;
3349
- background-color: var(--bg-card);
3350
- transition: all 0.15s ease;
3558
+ .fixed-height-widget {
3559
+ height: 450px;
3351
3560
  }
3352
- .suite-card:hover {
3353
- background: rgba(255, 255, 255, 0.03);
3354
- border-left-color: var(--border-medium);
3561
+ .suites-grid-container {
3562
+ flex-grow: 1;
3563
+ overflow-y: auto;
3564
+ padding-right: 5px;
3355
3565
  }
3356
- .suite-card.status-passed {
3357
- border-left-color: #10b981;
3566
+
3567
+ @media (max-width: 768px) {
3568
+ .fixed-height-widget {
3569
+ height: auto;
3570
+ max-height: 600px;
3571
+ }
3358
3572
  }
3359
- .suite-card.status-passed:hover {
3360
- background: rgba(16, 185, 129, 0.05);
3573
+ .suite-card {
3574
+ background: var(--bg-card); /* Changed from #ffffff */
3575
+ border: 1px solid var(--border-medium); /* Changed from border-light for better contrast */
3576
+ border-radius: 16px;
3577
+ padding: 24px;
3578
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); /* Darker shadow */
3579
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3580
+ display: flex;
3581
+ flex-direction: column;
3582
+ height: 100%;
3583
+ position: relative;
3584
+ overflow: hidden;
3361
3585
  }
3362
- .suite-card.status-failed {
3363
- border-left-color: #ef4444;
3586
+ .suite-card:hover {
3587
+ transform: translateY(-4px);
3588
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4);
3589
+ background: var(--bg-card-hover);
3590
+ border-color: var(--primary-dark);
3364
3591
  }
3365
- .suite-card.status-failed:hover {
3366
- background: rgba(239, 68, 68, 0.05);
3592
+ .suite-card::before {
3593
+ content: '';
3594
+ position: absolute;
3595
+ top: 0;
3596
+ left: 0;
3597
+ width: 100%;
3598
+ height: 4px;
3599
+ background: var(--neutral-200);
3600
+ opacity: 0.8;
3601
+ transition: background 0.3s ease;
3602
+ }
3603
+ .suite-card.status-passed::before { background: var(--success-color); }
3604
+ .suite-card.status-failed::before { background: var(--danger-color); }
3605
+ .suite-card.status-flaky::before { background: var(--neutral-500); }
3606
+ .suite-card.status-skipped::before { background: var(--warning-color); }
3607
+
3608
+ .suite-card.status-skipped::before { background: var(--warning-color); }
3609
+
3610
+ /* Outcome Badge */
3611
+ .outcome-badge {
3612
+ background-color: var(--secondary-color);
3613
+ color: #000;
3614
+ padding: 2px 8px;
3615
+ border-radius: 4px;
3616
+ font-size: 0.75em;
3617
+ font-weight: 700;
3618
+ text-transform: uppercase;
3619
+ margin-right: 8px;
3620
+ letter-spacing: 0.5px;
3367
3621
  }
3368
- .suite-card.status-skipped {
3369
- border-left-color: #f59e0b;
3622
+ .outcome-badge.flaky {
3623
+ background-color: #eab308; /* Yellow-500 */
3624
+ color: #000;
3370
3625
  }
3371
- .suite-card.status-skipped:hover {
3372
- background: rgba(245, 158, 11, 0.05);
3626
+
3627
+ .suite-card-header {
3628
+ display: flex;
3629
+ justify-content: space-between;
3630
+ align-items: flex-start;
3631
+ margin-bottom: 16px;
3373
3632
  }
3374
- .suite-card-header {
3375
- display: flex;
3376
- justify-content: space-between;
3377
- align-items: flex-start;
3378
- margin-bottom: 12px;
3633
+ .suite-name {
3634
+ font-size: 1.15em;
3635
+ font-weight: 700;
3636
+ color: var(--text-primary);
3637
+ line-height: 1.4;
3638
+ display: -webkit-box;
3639
+ -webkit-line-clamp: 2;
3640
+ -webkit-box-orient: vertical;
3641
+ overflow: hidden;
3642
+ margin-right: 12px;
3379
3643
  }
3380
- .suite-name {
3381
- font-weight: 600;
3382
- font-size: 1.05em;
3383
- color: #f9fafb;
3384
- margin-right: 10px;
3385
- word-break: break-word;
3644
+ .status-indicator-dot {
3645
+ width: 10px;
3646
+ height: 10px;
3647
+ border-radius: 50%;
3648
+ flex-shrink: 0;
3649
+ margin-top: 6px;
3386
3650
  }
3651
+ .status-indicator-dot.status-passed { background-color: var(--success-color); box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.15); }
3652
+ .status-indicator-dot.status-failed { background-color: var(--danger-color); box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.15); }
3653
+ .status-indicator-dot.status-skipped { background-color: var(--warning-color); box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.15); }
3654
+
3387
3655
  .browser-tag {
3656
+ font-size: 0.8em;
3657
+ font-weight: 600;
3658
+ background: var(--bg-secondary);
3659
+ color: var(--text-secondary);
3660
+ padding: 4px 10px;
3661
+ border-radius: 20px;
3662
+ border: 1px solid var(--border-light);
3663
+ display: inline-flex;
3664
+ align-items: center;
3665
+ gap: 6px;
3666
+ margin-bottom: 20px;
3667
+ align-self: flex-start;
3668
+ box-shadow: none;
3669
+ text-shadow: none;
3670
+ }
3671
+ .browser-tag:hover {
3672
+ /* Remove hover effect from previous */
3673
+ }
3674
+
3675
+ .suite-card-body {
3676
+ margin-top: auto;
3677
+ }
3678
+
3679
+ .test-count-label {
3388
3680
  font-size: 0.85em;
3389
3681
  font-weight: 600;
3390
- background: linear-gradient(135deg, rgba(96, 165, 250, 0.2) 0%, rgba(59, 130, 246, 0.15) 100%);
3682
+ color: var(--text-tertiary);
3683
+ text-transform: uppercase;
3684
+ letter-spacing: 0.05em;
3685
+ margin-bottom: 8px;
3686
+ display: block;
3687
+ }
3688
+
3689
+ .suite-stats {
3690
+ display: flex;
3691
+ gap: 8px;
3692
+ background: var(--bg-secondary);
3693
+ padding: 10px 14px;
3694
+ border-radius: 10px;
3695
+ justify-content: space-between;
3696
+ }
3697
+
3698
+ .stat-pill {
3699
+ display: flex;
3700
+ align-items: center;
3701
+ gap: 6px;
3702
+ font-size: 0.9em;
3703
+ font-weight: 600;
3704
+ }
3705
+ .stat-pill svg { width: 14px; height: 14px; }
3706
+ .stat-pill.passed { color: var(--success-dark); }
3707
+ .stat-pill.failed { color: var(--danger-dark); }
3708
+ .stat-pill.flaky { color: #4b5563; }
3709
+ .stat-pill.skipped { color: var(--warning-dark); }
3391
3710
  color: #93c5fd;
3392
3711
  padding: 6px 12px;
3393
3712
  border-radius: var(--radius-sm);
@@ -3581,6 +3900,11 @@ function generateHTML(reportData, trendData = null) {
3581
3900
  .status-badge.status-skipped {
3582
3901
  background: var(--warning-color);
3583
3902
  }
3903
+ .status-badge.status-flaky {
3904
+ background-color: #C0C0C0;
3905
+ color: #000000;
3906
+ border: 1px solid #A0A0A0;
3907
+ }
3584
3908
  .status-badge.status-unknown {
3585
3909
  background: var(--dark-gray-color);
3586
3910
  }
@@ -3636,6 +3960,65 @@ function generateHTML(reportData, trendData = null) {
3636
3960
  border-color: rgba(148, 163, 184, 0.25);
3637
3961
  }
3638
3962
 
3963
+ /* --- RETRY COUNT BADGE --- */
3964
+ .retry-badge {
3965
+ display: inline-flex;
3966
+ align-items: center;
3967
+ padding: 5px 12px;
3968
+ border-radius: 12px;
3969
+ font-size: 0.75rem;
3970
+ font-weight: 600;
3971
+ background: rgba(147, 51, 234, 0.15);
3972
+ color: #a855f7;
3973
+ border: 1px solid rgba(147, 51, 234, 0.3);
3974
+ margin-left: 8px;
3975
+ }
3976
+
3977
+ /* --- RETRY TABS --- */
3978
+ .retry-tabs-container {
3979
+ margin-top: 16px;
3980
+ }
3981
+
3982
+ .retry-tabs-header {
3983
+ display: flex;
3984
+ gap: 8px;
3985
+ border-bottom: 2px solid var(--border-medium);
3986
+ margin-bottom: 20px;
3987
+ flex-wrap: wrap;
3988
+ }
3989
+
3990
+ .retry-tab {
3991
+ padding: 10px 20px;
3992
+ background: transparent;
3993
+ border: none;
3994
+ border-bottom: 3px solid transparent;
3995
+ cursor: pointer;
3996
+ font-size: 0.95rem;
3997
+ font-weight: 600;
3998
+ color: var(--text-color-secondary);
3999
+ transition: all 0.2s ease;
4000
+ }
4001
+
4002
+ .retry-tab:hover {
4003
+ color: var(--primary-color);
4004
+ background: rgba(147, 51, 234, 0.05);
4005
+ }
4006
+
4007
+ .retry-tab.active {
4008
+ color: #a855f7;
4009
+ border-bottom-color: #a855f7;
4010
+ background: rgba(147, 51, 234, 0.1);
4011
+ }
4012
+
4013
+ .retry-tab-content {
4014
+ animation: fadeIn 0.3s ease-in;
4015
+ }
4016
+
4017
+ @keyframes fadeIn {
4018
+ from { opacity: 0; }
4019
+ to { opacity: 1; }
4020
+ }
4021
+
3639
4022
  .tag {
3640
4023
  display: inline-flex;
3641
4024
  align-items: center;
@@ -3685,7 +4068,9 @@ function generateHTML(reportData, trendData = null) {
3685
4068
  background-color: rgba(248,113,113,0.08);
3686
4069
  border: 1px solid rgba(248,113,113,0.25);
3687
4070
  border-left: 4px solid var(--danger-color);
3688
- border-radius: 4px;
4071
+ border-radius: 4px;
4072
+ display: flex;
4073
+ flex-direction: column;
3689
4074
  }
3690
4075
  .test-error-summary h4 {
3691
4076
  color: #ef4444;
@@ -4153,6 +4538,10 @@ function generateHTML(reportData, trendData = null) {
4153
4538
  .status-badge-small.status-skipped {
4154
4539
  background-color: #f59e0b;
4155
4540
  }
4541
+ .status-badge-small.status-flaky {
4542
+ background-color: #C0C0C0;
4543
+ color: #000000;
4544
+ }
4156
4545
  .status-badge-small.status-unknown {
4157
4546
  background-color: var(--dark-gray-color);
4158
4547
  }
@@ -5378,8 +5767,17 @@ function generateHTML(reportData, trendData = null) {
5378
5767
  color: #f9fafb;
5379
5768
  text-transform: capitalize;
5380
5769
  font-size: 1.05em;
5770
+ white-space: nowrap;
5771
+ overflow: hidden;
5772
+ text-overflow: ellipsis;
5773
+ flex: 1;
5774
+ min-width: 0;
5775
+ margin-right: 8px;
5381
5776
  }
5382
5777
  .browser-stats {
5778
+ color: #9ca3af;
5779
+ white-space: nowrap;
5780
+ flex-shrink: 0;
5383
5781
  color: #9ca3af;
5384
5782
  font-weight: 700;
5385
5783
  font-size: 0.95em;
@@ -5844,31 +6242,32 @@ function generateHTML(reportData, trendData = null) {
5844
6242
  <div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
5845
6243
  runSummary.skipped || 0
5846
6244
  }</div><div class="trend-percentage">${skipPercentage}%</div></div>
5847
- <div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
6245
+ <div class="summary-card flaky-status"><h3>Flaky</h3><div class="value">${runSummary.flaky || 0}</div>
6246
+ <div class="trend-percentage">${flakyPercentage}%</div></div>
5848
6247
  <div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
5849
6248
  runSummary.duration,
5850
6249
  )}</div></div>
5851
6250
  <div class="summary-card">
5852
- <h3>🔄 Retry Count</h3>
6251
+ <h3>Total Retry Count</h3>
5853
6252
  <div class="value">${totalRetried}</div>
5854
6253
  </div>
5855
6254
  <div class="summary-card">
5856
6255
  <h3>🌐 Browser Distribution <span style="font-size: 0.7em; color: var(--text-color-secondary); font-weight: 400;">(${browserBreakdown.length} total)</span></h3>
5857
6256
  <div class="browser-breakdown" style="max-height: 200px; overflow-y: auto; padding-right: 4px;">
5858
6257
  ${browserBreakdown
5859
- .slice(0, 5)
6258
+ .slice(0, 3)
5860
6259
  .map(
5861
6260
  (b) =>
5862
6261
  `<div class="browser-item">
5863
- <span class="browser-name">${sanitizeHTML(b.browser)}</span>
6262
+ <span class="browser-name" title="${sanitizeHTML(b.browser)}">${sanitizeHTML(b.browser)}</span>
5864
6263
  <span class="browser-stats">${b.percentage}% (${b.count})</span>
5865
6264
  </div>`,
5866
6265
  )
5867
6266
  .join("")}
5868
6267
  ${
5869
- browserBreakdown.length > 5
6268
+ browserBreakdown.length > 3
5870
6269
  ? `<div class="browser-item" style="opacity: 0.6; font-style: italic; justify-content: center; border-top: 1px solid var(--border-light); margin-top: 8px; padding-top: 8px;">
5871
- <span>+${browserBreakdown.length - 5} more browsers</span>
6270
+ <span>+${browserBreakdown.length - 3} more browsers</span>
5872
6271
  </div>`
5873
6272
  : ""
5874
6273
  }
@@ -5881,6 +6280,7 @@ function generateHTML(reportData, trendData = null) {
5881
6280
  [
5882
6281
  { label: "Passed", value: runSummary.passed },
5883
6282
  { label: "Failed", value: runSummary.failed },
6283
+ { label: "Flaky", value: runSummary.flaky || 0 },
5884
6284
  { label: "Skipped", value: runSummary.skipped || 0 },
5885
6285
  ],
5886
6286
  400,
@@ -5897,7 +6297,7 @@ function generateHTML(reportData, trendData = null) {
5897
6297
  <div id="test-runs" class="tab-content">
5898
6298
  <div class="filters" style="border-color: black; border-style: groove;">
5899
6299
  <input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
5900
- <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>
6300
+ <select id="filter-status"><option value="">All Statuses</option><option value="passed">Passed</option><option value="failed">Failed</option><option value="flaky">Flaky</option><option value="skipped">Skipped</option></select>
5901
6301
  <select id="filter-browser"><option value="">All Browsers</option>${Array.from(
5902
6302
  new Set(
5903
6303
  (results || []).map((test) => test.browser || "unknown"),
@@ -5988,6 +6388,29 @@ function generateHTML(reportData, trendData = null) {
5988
6388
  return (ms / 1000).toFixed(1) + "s";
5989
6389
  }
5990
6390
  }
6391
+ function switchRetryTab(event, tabId) {
6392
+ // Find container
6393
+ const container = event.target.closest('.retry-tabs-container');
6394
+
6395
+ // Update tab buttons
6396
+ const buttons = container.querySelectorAll('.retry-tab');
6397
+ buttons.forEach(btn => btn.classList.remove('active'));
6398
+ event.target.classList.add('active');
6399
+
6400
+ // Update content
6401
+ const contents = container.querySelectorAll('.retry-tab-content');
6402
+ contents.forEach(content => {
6403
+ content.style.display = 'none';
6404
+ content.classList.remove('active');
6405
+ });
6406
+
6407
+ const activeContent = container.querySelector('#' + tabId);
6408
+ if (activeContent) {
6409
+ activeContent.style.display = 'block';
6410
+ activeContent.classList.add('active');
6411
+ }
6412
+ }
6413
+
5991
6414
  function copyLogContent(elementId, button) {
5992
6415
  const logElement = document.getElementById(elementId);
5993
6416
  if (!logElement) {
@@ -6814,6 +7237,7 @@ async function main() {
6814
7237
  passed: histRunReport.run.passed,
6815
7238
  failed: histRunReport.run.failed,
6816
7239
  skipped: histRunReport.run.skipped || 0,
7240
+ flaky: histRunReport.run.flaky || (histRunReport.results ? histRunReport.results.filter(r => r.status === 'flaky' || r.outcome === 'flaky').length : 0),
6817
7241
  });
6818
7242
 
6819
7243
  if (histRunReport.results && Array.isArray(histRunReport.results)) {
@@ -6822,7 +7246,7 @@ async function main() {
6822
7246
  (test) => ({
6823
7247
  testName: test.name,
6824
7248
  duration: test.duration,
6825
- status: test.status,
7249
+ status: test.final_status || test.status,
6826
7250
  timestamp: new Date(test.startTime),
6827
7251
  }),
6828
7252
  );