@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.
@@ -295,6 +295,12 @@ function generateTestTrendsChart(trendData) {
295
295
  color: "var(--warning-color)",
296
296
  marker: { symbol: "circle" },
297
297
  },
298
+ {
299
+ name: "Flaky",
300
+ data: runs.map((r) => r.flaky || 0),
301
+ color: "var(--neutral-500)",
302
+ marker: { symbol: "circle" },
303
+ },
298
304
  ];
299
305
  const runsForTooltip = runs.map((r) => ({
300
306
  runId: r.runId,
@@ -481,6 +487,9 @@ function generateTestHistoryChart(history) {
481
487
  case "skipped":
482
488
  color = "var(--warning-color)";
483
489
  break;
490
+ case "flaky":
491
+ color = "var(--neutral-500)";
492
+ break;
484
493
  default:
485
494
  color = "var(--dark-gray-color)";
486
495
  }
@@ -590,6 +599,9 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
590
599
  case "Failed":
591
600
  color = "var(--danger-color)";
592
601
  break;
602
+ case "Flaky":
603
+ color = "var(--neutral-500)";
604
+ break;
593
605
  case "Skipped":
594
606
  color = "var(--warning-color)";
595
607
  break;
@@ -828,7 +840,9 @@ function generateEnvironmentSection(environmentData) {
828
840
  }
829
841
 
830
842
  function generateEnvironmentDashboard(environment, hideHeader = false) {
831
- const cpuInfo = `model: ${environment.cpu.model}, cores: ${environment.cpu.cores}`;
843
+ const cpuModel = environment.cpu && environment.cpu.model ? environment.cpu.model : "N/A";
844
+ const cpuCores = environment.cpu && environment.cpu.cores ? environment.cpu.cores : "N/A";
845
+ const cpuInfo = `model: ${cpuModel}, cores: ${cpuCores}`;
832
846
  const osInfo = environment.os || "N/A";
833
847
  const nodeInfo = environment.node || "N/A";
834
848
  const v8Info = environment.v8 || "N/A";
@@ -1140,7 +1154,7 @@ function generateEnvironmentDashboard(environment, hideHeader = false) {
1140
1154
  </div>
1141
1155
  <div class="env-item-content">
1142
1156
  <p class="env-item-label">Working Dir</p>
1143
- <div class="env-item-value" title="${environment.cwd}">${environment.cwd.length > 30 ? "..." + environment.cwd.slice(-27) : environment.cwd}</div>
1157
+ <div class="env-item-value" title="${cwdInfo}">${cwdInfo.length > 30 ? "..." + cwdInfo.slice(-27) : cwdInfo}</div>
1144
1158
  </div>
1145
1159
  </div>
1146
1160
  </div>
@@ -1164,11 +1178,11 @@ function generateWorkerDistributionChart(results) {
1164
1178
  const workerId =
1165
1179
  typeof test.workerId !== "undefined" ? test.workerId : "N/A";
1166
1180
  if (!acc[workerId]) {
1167
- acc[workerId] = { passed: 0, failed: 0, skipped: 0, tests: [] };
1181
+ acc[workerId] = { passed: 0, failed: 0, skipped: 0, flaky: 0, tests: [] };
1168
1182
  }
1169
1183
 
1170
1184
  const status = String(test.status).toLowerCase();
1171
- if (status === "passed" || status === "failed" || status === "skipped") {
1185
+ if (status === "passed" || status === "failed" || status === "skipped" || status === "flaky") {
1172
1186
  acc[workerId][status]++;
1173
1187
  }
1174
1188
 
@@ -1213,12 +1227,14 @@ function generateWorkerDistributionChart(results) {
1213
1227
  const passedData = workerIds.map((id) => workerData[id].passed);
1214
1228
  const failedData = workerIds.map((id) => workerData[id].failed);
1215
1229
  const skippedData = workerIds.map((id) => workerData[id].skipped);
1230
+ const flakyData = workerIds.map((id) => workerData[id].flaky);
1216
1231
 
1217
1232
  const categoriesString = JSON.stringify(categories);
1218
1233
  const fullDataString = JSON.stringify(fullWorkerData);
1219
1234
  const seriesString = JSON.stringify([
1220
1235
  { name: "Passed", data: passedData, color: "var(--success-color)" },
1221
1236
  { name: "Failed", data: failedData, color: "var(--danger-color)" },
1237
+ { name: "Flaky", data: flakyData, color: "var(--neutral-500)" },
1222
1238
  { name: "Skipped", data: skippedData, color: "var(--warning-color)" },
1223
1239
  ]);
1224
1240
 
@@ -1311,6 +1327,7 @@ function generateWorkerDistributionChart(results) {
1311
1327
  if (test.status === 'passed') color = 'var(--success-color)';
1312
1328
  else if (test.status === 'failed') color = 'var(--danger-color)';
1313
1329
  else if (test.status === 'skipped') color = 'var(--warning-color)';
1330
+ else if (test.status === 'flaky') color = 'var(--neutral-500)';
1314
1331
 
1315
1332
  const escapedName = test.name.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
1316
1333
  testListHtml += \`<li style="color: \${color};"><span style="color: \${color}">[\${test.status.toUpperCase()}]</span> \${escapedName}</li>\`;
@@ -1476,6 +1493,7 @@ function generateTestHistoryContent(trendData) {
1476
1493
  <option value="">All Statuses</option>
1477
1494
  <option value="passed">Passed</option>
1478
1495
  <option value="failed">Failed</option>
1496
+ <option value="flaky">Flaky</option>
1479
1497
  <option value="skipped">Skipped</option>
1480
1498
  </select>
1481
1499
  <button id="clear-history-filters" class="clear-filters-btn">Clear Filters</button>
@@ -1543,6 +1561,8 @@ function getStatusClass(status) {
1543
1561
  return "status-failed";
1544
1562
  case "skipped":
1545
1563
  return "status-skipped";
1564
+ case "flaky":
1565
+ return "status-flaky";
1546
1566
  default:
1547
1567
  return "status-unknown";
1548
1568
  }
@@ -1555,6 +1575,8 @@ function getStatusIcon(status) {
1555
1575
  return "❌";
1556
1576
  case "skipped":
1557
1577
  return "⏭️";
1578
+ case "flaky":
1579
+ return "⚠️";
1558
1580
  default:
1559
1581
  return "❓";
1560
1582
  }
@@ -1590,6 +1612,7 @@ function getSuitesData(results) {
1590
1612
  browser: browser,
1591
1613
  passed: 0,
1592
1614
  failed: 0,
1615
+ flaky: 0,
1593
1616
  skipped: 0,
1594
1617
  count: 0,
1595
1618
  statusOverall: "passed",
@@ -1597,12 +1620,15 @@ function getSuitesData(results) {
1597
1620
  }
1598
1621
  const suite = suitesMap.get(key);
1599
1622
  suite.count++;
1600
- const currentStatus = String(test.status).toLowerCase();
1623
+ let currentStatus = String(test.status).toLowerCase();
1624
+ if (test.outcome === 'flaky') currentStatus = 'flaky';
1601
1625
  if (currentStatus && suite[currentStatus] !== undefined) {
1602
1626
  suite[currentStatus]++;
1603
1627
  }
1604
1628
  if (currentStatus === "failed") suite.statusOverall = "failed";
1605
- else if (currentStatus === "skipped" && suite.statusOverall !== "failed")
1629
+ else if (currentStatus === "flaky" && suite.statusOverall !== "failed")
1630
+ suite.statusOverall = "flaky";
1631
+ else if (currentStatus === "skipped" && suite.statusOverall !== "failed" && suite.statusOverall !== "flaky")
1606
1632
  suite.statusOverall = "skipped";
1607
1633
  });
1608
1634
  return Array.from(suitesMap.values());
@@ -1613,10 +1639,10 @@ function generateSuitesWidget(suitesData) {
1613
1639
  return `<div class="suites-widget" style="height: 450px;"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
1614
1640
  }
1615
1641
 
1616
- // Added inline styles for height consistency with Pie Chart (approx 450px) and scrolling
1642
+ // Uses CSS classes for responsiveness instead of inline styles
1617
1643
  return `
1618
- <div class="suites-widget" style="height: 450px; display: flex; flex-direction: column;">
1619
- <div class="suites-header" style="flex-shrink: 0;">
1644
+ <div class="suites-widget fixed-height-widget">
1645
+ <div class="suites-header">
1620
1646
  <h2>Test Suites</h2>
1621
1647
  <span class="summary-badge">${
1622
1648
  suitesData.length
@@ -1626,40 +1652,40 @@ function generateSuitesWidget(suitesData) {
1626
1652
  )} tests</span>
1627
1653
  </div>
1628
1654
 
1629
- <div class="suites-grid-container" style="flex-grow: 1; overflow-y: auto; padding-right: 5px;">
1655
+ <div class="suites-grid-container">
1630
1656
  <div class="suites-grid">
1631
1657
  ${suitesData
1632
1658
  .map(
1633
1659
  (suite) => `
1634
1660
  <div class="suite-card status-${suite.statusOverall}">
1635
1661
  <div class="suite-card-header">
1636
- <h3 class="suite-name" title="${sanitizeHTML(
1637
- suite.name,
1638
- )} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
1639
- </div>
1640
- <div style="margin-bottom: 12px;"><span class="browser-tag" title="🌐 ${sanitizeHTML(suite.browser)}">🌐 ${sanitizeHTML(
1641
- suite.browser,
1642
- )}</span></div>
1643
- <div class="suite-card-body">
1644
- <span class="test-count">${suite.count} test${
1645
- suite.count !== 1 ? "s" : ""
1646
- }</span>
1662
+ <h3 class="suite-name" title="${sanitizeHTML(suite.name)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
1663
+ <div class="status-indicator-dot status-${suite.statusOverall}" title="${suite.statusOverall.charAt(0).toUpperCase() + suite.statusOverall.slice(1)}"></div>
1664
+ </div>
1665
+
1666
+ <div class="browser-tag" title="🌐Browser: ${sanitizeHTML(suite.browser)}">
1667
+ <span style="font-size: 1.1em;">🌐</span> ${sanitizeHTML(suite.browser)}
1668
+ </div>
1669
+
1670
+ <div class="suite-card-body">
1671
+ <span class="test-count-label">${suite.count} Test${suite.count !== 1 ? "s" : ""}</span>
1647
1672
  <div class="suite-stats">
1648
- ${
1649
- suite.passed > 0
1650
- ? `<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>`
1651
- : ""
1652
- }
1653
- ${
1654
- suite.failed > 0
1655
- ? `<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>`
1656
- : ""
1657
- }
1658
- ${
1659
- suite.skipped > 0
1660
- ? `<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>`
1661
- : ""
1662
- }
1673
+ <span class="stat-pill passed" title="Passed">
1674
+ <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>
1675
+ ${suite.passed}
1676
+ </span>
1677
+ <span class="stat-pill failed" title="Failed">
1678
+ <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>
1679
+ ${suite.failed}
1680
+ </span>
1681
+ <span class="stat-pill flaky" title="Flaky">
1682
+ <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>
1683
+ ${suite.flaky || 0}
1684
+ </span>
1685
+ <span class="stat-pill skipped" title="Skipped">
1686
+ <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>
1687
+ ${suite.skipped}
1688
+ </span>
1663
1689
  </div>
1664
1690
  </div>
1665
1691
  </div>`,
@@ -2060,6 +2086,7 @@ function generateSeverityDistributionChart(results) {
2060
2086
  const data = {
2061
2087
  passed: [0, 0, 0, 0, 0],
2062
2088
  failed: [0, 0, 0, 0, 0],
2089
+ flaky: [0, 0, 0, 0, 0],
2063
2090
  skipped: [0, 0, 0, 0, 0],
2064
2091
  };
2065
2092
 
@@ -2078,6 +2105,8 @@ function generateSeverityDistributionChart(results) {
2078
2105
  status === "interrupted"
2079
2106
  ) {
2080
2107
  data.failed[index]++;
2108
+ } else if (status === "flaky") {
2109
+ data.flaky[index]++;
2081
2110
  } else {
2082
2111
  data.skipped[index]++;
2083
2112
  }
@@ -2091,6 +2120,7 @@ function generateSeverityDistributionChart(results) {
2091
2120
  const seriesData = [
2092
2121
  { name: "Passed", data: data.passed, color: "var(--success-color)" },
2093
2122
  { name: "Failed", data: data.failed, color: "var(--danger-color)" },
2123
+ { name: "Flaky", data: data.flaky, color: "var(--neutral-500)" },
2094
2124
  { name: "Skipped", data: data.skipped, color: "var(--warning-color)" },
2095
2125
  ];
2096
2126
 
@@ -2204,32 +2234,76 @@ function generateHTML(reportData, trendData = null) {
2204
2234
  return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`), "");
2205
2235
  };
2206
2236
 
2207
- const totalTestsOr1 = runSummary.totalTests || 1;
2208
- const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
2209
- const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
2210
- const skipPercentage = Math.round(
2211
- ((runSummary.skipped || 0) / totalTestsOr1) * 100,
2212
- );
2237
+
2213
2238
  const avgTestDuration =
2214
2239
  runSummary.totalTests > 0
2215
2240
  ? formatDuration(runSummary.duration / runSummary.totalTests)
2216
2241
  : "0.0s";
2217
2242
 
2243
+ const flakyCount = (results || []).filter(r => r.outcome === 'flaky').length;
2244
+
2218
2245
  // Calculate retry statistics
2219
2246
  const totalRetried = (results || []).reduce((acc, test) => {
2220
- if (test.retries && test.retries > 0) {
2221
- return acc + 1;
2247
+ if (test.retryHistory && test.retryHistory.length > 0) {
2248
+ return acc + test.retryHistory.length;
2222
2249
  }
2223
2250
  return acc;
2224
2251
  }, 0);
2225
2252
 
2253
+ // --- RECALCULATE KPI METRICS BASED ON FINAL_STATUS ---
2254
+ let calculatedPassed = 0;
2255
+ let calculatedFailed = 0;
2256
+ let calculatedSkipped = 0;
2257
+ let calculatedFlaky = 0;
2258
+ let calculatedTotal = 0;
2259
+
2260
+ (results || []).forEach(test => {
2261
+ calculatedTotal++;
2262
+ // New Logic: If outcome is 'flaky', it overrides everything.
2263
+ let statusToUse = test.status;
2264
+ if (test.outcome === 'flaky') {
2265
+ statusToUse = 'flaky';
2266
+ } else if (test.status === 'flaky') {
2267
+ // Just in case outcome wasn't set but status was (unlikely with new reporter)
2268
+ statusToUse = 'flaky';
2269
+ } else if (test.retryHistory && test.retryHistory.length > 0 && test.final_status) {
2270
+ statusToUse = test.final_status;
2271
+ }
2272
+
2273
+ // Update test status in place for charts
2274
+ test.status = statusToUse;
2275
+
2276
+ const s = String(statusToUse).toLowerCase();
2277
+ if (s === 'passed') calculatedPassed++;
2278
+ else if (s === 'skipped') calculatedSkipped++;
2279
+ else if (s === 'flaky') calculatedFlaky++;
2280
+ else calculatedFailed++; // failed, timedout, interrupted
2281
+ });
2282
+
2283
+ // Override runSummary counts with our calculated ones if results exist
2284
+ if (results && results.length > 0) {
2285
+ runSummary.passed = calculatedPassed;
2286
+ runSummary.failed = calculatedFailed;
2287
+ runSummary.skipped = calculatedSkipped;
2288
+ runSummary.flaky = calculatedFlaky;
2289
+ runSummary.totalTests = calculatedTotal;
2290
+ }
2291
+
2292
+ const totalTestsOr1 = runSummary.totalTests || 1;
2293
+ const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
2294
+ const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
2295
+ const skipPercentage = Math.round(
2296
+ ((runSummary.skipped || 0) / totalTestsOr1) * 100,
2297
+ );
2298
+ const flakyPercentage = Math.round(((runSummary.flaky || 0) / totalTestsOr1) * 100);
2299
+
2300
+
2226
2301
  // Calculate browser distribution
2227
2302
  const browserStats = (results || []).reduce((acc, test) => {
2228
2303
  let browserName = "unknown";
2229
2304
  if (test.browser) {
2230
- // Extract browser name from strings like "Chrome v143 on Windows 10"
2231
- const match = test.browser.match(/^(\w+)/);
2232
- browserName = match ? match[1] : test.browser;
2305
+ // Use full browser name
2306
+ browserName = test.browser;
2233
2307
  }
2234
2308
  acc[browserName] = (acc[browserName] || 0) + 1;
2235
2309
  return acc;
@@ -2255,6 +2329,10 @@ function generateHTML(reportData, trendData = null) {
2255
2329
  // --- Simplified Severity Badge ---
2256
2330
  const severity = test.severity || "Medium";
2257
2331
  const severityBadge = `<span class="severity-badge" data-severity="${severity.toLowerCase()}">${severity}</span>`;
2332
+
2333
+ // --- Retry Count Badge (only show if retries occurred) ---
2334
+ const retryCount = test.retryHistory ? test.retryHistory.length : 0;
2335
+ const retryBadge = retryCount > 0 ? `<span class="retry-badge">Retry Count: ${retryCount}</span>` : '';
2258
2336
  const generateStepsHTML = (steps, depth = 0) => {
2259
2337
  if (!steps || steps.length === 0)
2260
2338
  return "<div class='no-steps'>No steps recorded for this test.</div>";
@@ -2310,7 +2388,7 @@ function generateHTML(reportData, trendData = null) {
2310
2388
  onclick="copyErrorToClipboard(this)"
2311
2389
  style="
2312
2390
  margin-top: 8px;
2313
- padding: 4px 8px;
2391
+ padding: 6px 12px;
2314
2392
  background: #f0f0f0;
2315
2393
  border: 2px solid #ccc;
2316
2394
  border-radius: 4px;
@@ -2318,6 +2396,8 @@ function generateHTML(reportData, trendData = null) {
2318
2396
  font-size: 12px;
2319
2397
  border-color: #8B0000;
2320
2398
  color: #8B0000;
2399
+ align-self: flex-end;
2400
+ width: auto;
2321
2401
  "
2322
2402
  onmouseover="this.style.background='#e0e0e0'"
2323
2403
  onmouseout="this.style.background='#f0f0f0'"
@@ -2341,43 +2421,35 @@ function generateHTML(reportData, trendData = null) {
2341
2421
  .join("");
2342
2422
  };
2343
2423
 
2344
- return `
2345
- <div class="test-case" data-status="${
2346
- test.status
2347
- }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
2348
- .join(",")
2349
- .toLowerCase()}">
2350
- <div class="test-case-header" role="button" aria-expanded="false">
2351
- <div class="test-case-summary">
2352
- <span class="test-case-title" title="${sanitizeHTML(
2353
- test.name,
2354
- )}">${sanitizeHTML(testTitle)}</span>
2355
- <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
2356
- </div>
2357
- <div class="test-case-meta">
2358
- ${severityBadge}
2359
- ${
2360
- test.tags && test.tags.length > 0
2361
- ? test.tags
2362
- .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
2363
- .join(" ")
2364
- : ""
2365
- }
2366
- </div>
2367
- <div class="test-case-status-duration">
2368
- <span class="status-badge ${getStatusClass(test.status)}">${String(
2369
- test.status,
2370
- ).toUpperCase()}</span>
2371
- <span class="test-duration">${formatDuration(test.duration)}</span>
2372
- </div>
2373
- </div>
2374
- <div class="test-case-content" style="display: none;">
2375
- <p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
2424
+ // Helper for Tab Badges
2425
+ const getSmallStatusBadge = (status) => {
2426
+ const s = String(status).toLowerCase();
2427
+ let colorVar = 'var(--text-tertiary)';
2428
+ if(s === 'passed') colorVar = 'var(--success-color)';
2429
+ else if(s === 'failed') colorVar = 'var(--danger-color)';
2430
+ else if(s === 'skipped') colorVar = 'var(--warning-color)';
2431
+
2432
+ return `<span style="
2433
+ display: inline-block;
2434
+ width: 8px;
2435
+ height: 8px;
2436
+ border-radius: 50%;
2437
+ background-color: ${colorVar};
2438
+ margin-left: 6px;
2439
+ vertical-align: middle;
2440
+ " title="${s}"></span>`;
2441
+ };
2442
+
2443
+ // Function to generate test content HTML (used for base run and retry tabs)
2444
+ const getTestContentHTML = (testData, runSuffix) => {
2445
+ const logId = `stdout-log-${test.id || index}-${runSuffix}`;
2446
+ return `
2447
+ <p><strong>Full Path:</strong> ${sanitizeHTML(testData.name)}</p>
2376
2448
  ${
2377
- test.annotations && test.annotations.length > 0
2449
+ testData.annotations && testData.annotations.length > 0
2378
2450
  ? `<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;">
2379
2451
  <h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
2380
- ${test.annotations
2452
+ ${testData.annotations
2381
2453
  .map((annotation, idx) => {
2382
2454
  const isIssueOrBug =
2383
2455
  annotation.type === "issue" ||
@@ -2385,7 +2457,7 @@ function generateHTML(reportData, trendData = null) {
2385
2457
  const descriptionText = annotation.description || "";
2386
2458
  const typeLabel = sanitizeHTML(annotation.type);
2387
2459
  const descriptionHtml =
2388
- isIssueOrBug && descriptionText.match(/^[A-Z]+-\d+$/)
2460
+ isIssueOrBug && descriptionText.match(/^[A-Z]+-\\d+$/)
2389
2461
  ? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
2390
2462
  descriptionText,
2391
2463
  )}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
@@ -2400,7 +2472,7 @@ function generateHTML(reportData, trendData = null) {
2400
2472
  }</div>`
2401
2473
  : "";
2402
2474
  return `<div style="margin-bottom: ${
2403
- idx < test.annotations.length - 1 ? "10px" : "0"
2475
+ idx < testData.annotations.length - 1 ? "10px" : "0"
2404
2476
  };">
2405
2477
  <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>
2406
2478
  ${
@@ -2416,21 +2488,21 @@ function generateHTML(reportData, trendData = null) {
2416
2488
  : ""
2417
2489
  }
2418
2490
  <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
2419
- test.workerId,
2491
+ testData.workerId,
2420
2492
  )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
2421
- test.totalWorkers,
2493
+ testData.totalWorkers,
2422
2494
  )}]</p>
2423
2495
  ${
2424
- test.errorMessage
2425
- ? `<div class="test-error-summary">${formatPlaywrightError(
2426
- test.errorMessage,
2427
- )}
2496
+ testData.errorMessage
2497
+ ? `<div class="test-error-summary"><div class="stack-trace">${formatPlaywrightError(
2498
+ testData.errorMessage,
2499
+ )}</div>
2428
2500
  <button
2429
2501
  class="copy-error-btn"
2430
2502
  onclick="copyErrorToClipboard(this)"
2431
2503
  style="
2432
2504
  margin-top: 8px;
2433
- padding: 4px 8px;
2505
+ padding: 6px 12px;
2434
2506
  background: #f0f0f0;
2435
2507
  border: 2px solid #ccc;
2436
2508
  border-radius: 4px;
@@ -2438,6 +2510,8 @@ function generateHTML(reportData, trendData = null) {
2438
2510
  font-size: 12px;
2439
2511
  border-color: #8B0000;
2440
2512
  color: #8B0000;
2513
+ align-self: flex-end;
2514
+ width: auto;
2441
2515
  "
2442
2516
  onmouseover="this.style.background='#e0e0e0'"
2443
2517
  onmouseout="this.style.background='#f0f0f0'"
@@ -2448,50 +2522,48 @@ function generateHTML(reportData, trendData = null) {
2448
2522
  : ""
2449
2523
  }
2450
2524
  ${
2451
- test.snippet
2525
+ testData.snippet
2452
2526
  ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
2453
- test.snippet,
2527
+ testData.snippet,
2454
2528
  )}</code></pre></div>`
2455
2529
  : ""
2456
2530
  }
2457
2531
  <h4>Steps</h4>
2458
- <div class="steps-list">${generateStepsHTML(test.steps)}</div>
2532
+ <div class="steps-list">${generateStepsHTML(testData.steps)}</div>
2459
2533
  ${(() => {
2460
- if (!test.stdout || test.stdout.length === 0) return "";
2461
- // Create a unique ID for the <pre> element to target it for copying
2462
- const logId = `stdout-log-${test.id || index}`;
2534
+ if (!testData.stdout || testData.stdout.length === 0) return "";
2463
2535
  return `<div class="console-output-section">
2464
2536
  <h4>Console Output (stdout)
2465
- <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy</button>
2537
+ <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy</ button>
2466
2538
  </h4>
2467
2539
  <div class="log-wrapper">
2468
2540
  <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(
2469
- test.stdout
2541
+ testData.stdout
2470
2542
  .map((line) => sanitizeHTML(line))
2471
- .join("\n"),
2543
+ .join("\\n"),
2472
2544
  )}</pre>
2473
2545
  </div>
2474
2546
  </div>`;
2475
2547
  })()}
2476
2548
  ${
2477
- test.stderr && test.stderr.length > 0
2549
+ testData.stderr && testData.stderr.length > 0
2478
2550
  ? `<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(
2479
- test.stderr.map((line) => sanitizeHTML(line)).join("\n"),
2551
+ testData.stderr.map((line) => sanitizeHTML(line)).join("\\n"),
2480
2552
  )}</pre></div>`
2481
2553
  : ""
2482
2554
  }
2483
2555
  ${
2484
- test.screenshots && test.screenshots.length > 0
2556
+ testData.screenshots && testData.screenshots.length > 0
2485
2557
  ? `
2486
2558
  <div class="attachments-section">
2487
2559
  <h4>Screenshots</h4>
2488
2560
  <div class="attachments-grid">
2489
- ${test.screenshots
2561
+ ${testData.screenshots
2490
2562
  .map(
2491
- (screenshot, index) => `
2563
+ (screenshot, screenshotIndex) => `
2492
2564
  <div class="attachment-item">
2493
2565
  <img src="${fixPath(screenshot)}" alt="Screenshot ${
2494
- index + 1
2566
+ screenshotIndex + 1
2495
2567
  }">
2496
2568
  <div class="attachment-info">
2497
2569
  <div class="trace-actions">
@@ -2500,7 +2572,7 @@ function generateHTML(reportData, trendData = null) {
2500
2572
  )}" target="_blank" class="view-full">View Full Image</a>
2501
2573
  <a href="${fixPath(
2502
2574
  screenshot,
2503
- )}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
2575
+ )}" target="_blank" download="screenshot-${Date.now()}-${screenshotIndex}.png">Download</a>
2504
2576
  </div>
2505
2577
  </div>
2506
2578
  </div>
@@ -2513,9 +2585,9 @@ function generateHTML(reportData, trendData = null) {
2513
2585
  : ""
2514
2586
  }
2515
2587
  ${
2516
- test.videoPath && test.videoPath.length > 0
2517
- ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
2518
- .map((videoUrl, index) => {
2588
+ testData.videoPath && testData.videoPath.length > 0
2589
+ ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${testData.videoPath
2590
+ .map((videoUrl, videoIndex) => {
2519
2591
  const fixedVideoUrl = fixPath(videoUrl);
2520
2592
  const fileExtension = String(fixedVideoUrl)
2521
2593
  .split(".")
@@ -2531,7 +2603,7 @@ function generateHTML(reportData, trendData = null) {
2531
2603
  }[fileExtension] || "video/mp4";
2532
2604
  return `<div class="attachment-item video-item">
2533
2605
  <video controls width="100%" height="auto" title="Video ${
2534
- index + 1
2606
+ videoIndex + 1
2535
2607
  }">
2536
2608
  <source src="${sanitizeHTML(
2537
2609
  fixedVideoUrl,
@@ -2542,7 +2614,7 @@ function generateHTML(reportData, trendData = null) {
2542
2614
  <div class="trace-actions">
2543
2615
  <a href="${sanitizeHTML(
2544
2616
  fixedVideoUrl,
2545
- )}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
2617
+ )}" target="_blank" download="video-${Date.now()}-${videoIndex}.${fileExtension}">Download</a>
2546
2618
  </div>
2547
2619
  </div>
2548
2620
  </div>`;
@@ -2551,7 +2623,7 @@ function generateHTML(reportData, trendData = null) {
2551
2623
  : ""
2552
2624
  }
2553
2625
  ${
2554
- test.tracePath
2626
+ testData.tracePath
2555
2627
  ? `
2556
2628
  <div class="attachments-section">
2557
2629
  <h4>Trace Files</h4>
@@ -2560,15 +2632,15 @@ function generateHTML(reportData, trendData = null) {
2560
2632
  <div class="trace-preview">
2561
2633
  <span class="trace-icon">📄</span>
2562
2634
  <span class="trace-name">${sanitizeHTML(
2563
- path.basename(test.tracePath),
2635
+ path.basename(testData.tracePath),
2564
2636
  )}</span>
2565
2637
  </div>
2566
2638
  <div class="attachment-info">
2567
2639
  <div class="trace-actions">
2568
2640
  <a href="${sanitizeHTML(
2569
- fixPath(test.tracePath),
2641
+ fixPath(testData.tracePath),
2570
2642
  )}" target="_blank" download="${sanitizeHTML(
2571
- path.basename(test.tracePath),
2643
+ path.basename(testData.tracePath),
2572
2644
  )}" class="download-trace">Download Trace</a>
2573
2645
  </div>
2574
2646
  </div>
@@ -2579,12 +2651,12 @@ function generateHTML(reportData, trendData = null) {
2579
2651
  : ""
2580
2652
  }
2581
2653
  ${
2582
- test.attachments && test.attachments.length > 0
2654
+ testData.attachments && testData.attachments.length > 0
2583
2655
  ? `
2584
2656
  <div class="attachments-section">
2585
2657
  <h4>Other Attachments</h4>
2586
2658
  <div class="attachments-grid">
2587
- ${test.attachments
2659
+ ${testData.attachments
2588
2660
  .map(
2589
2661
  (attachment) => `
2590
2662
  <div class="attachment-item generic-attachment">
@@ -2620,13 +2692,76 @@ function generateHTML(reportData, trendData = null) {
2620
2692
  `
2621
2693
  : ""
2622
2694
  }
2623
- ${
2624
- test.codeSnippet
2625
- ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
2626
- sanitizeHTML(test.codeSnippet),
2627
- )}</code></pre></div>`
2628
- : ""
2629
- }
2695
+
2696
+ `;
2697
+ };
2698
+
2699
+ // Determine header status: use final_status if retried, else normal status
2700
+ const headerStatus = (test.retryHistory && test.retryHistory.length > 0 && test.final_status)
2701
+ ? test.final_status
2702
+ : test.status;
2703
+
2704
+ const outcomeBadge = (test.outcome && test.outcome !== 'flaky')
2705
+ ? `<span class="outcome-badge ${test.outcome}">${test.outcome}</span>`
2706
+ : '';
2707
+
2708
+ return `
2709
+ <div class="test-case" data-status="${
2710
+ headerStatus
2711
+ }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
2712
+ .join(",")
2713
+ .toLowerCase()}">
2714
+ <div class="test-case-header" role="button" aria-expanded="false">
2715
+ <div class="test-case-summary">
2716
+ <span class="test-case-title" title="${sanitizeHTML(
2717
+ test.name,
2718
+ )}">${sanitizeHTML(testTitle)}</span>
2719
+ <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
2720
+ </div>
2721
+ <div class="test-case-meta">
2722
+ ${severityBadge}
2723
+ ${retryBadge}
2724
+ ${outcomeBadge}
2725
+ ${
2726
+ test.tags && test.tags.length > 0
2727
+ ? test.tags
2728
+ .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
2729
+ .join(" ")
2730
+ : ""
2731
+ }
2732
+ </div>
2733
+ <div class="test-case-status-duration">
2734
+ <span class="status-badge ${getStatusClass(headerStatus)}">${String(
2735
+ headerStatus,
2736
+ ).toUpperCase()}</span>
2737
+ <span class="test-duration">${formatDuration(test.duration)}</span>
2738
+ </div>
2739
+ </div>
2740
+ <div class="test-case-content" style="display: none;">
2741
+ ${test.retryHistory && test.retryHistory.length > 0 ? `
2742
+ <div class="retry-tabs-container">
2743
+ <div class="retry-tabs-header">
2744
+ <button class="retry-tab active" onclick="switchRetryTab(event, 'base-run-${test.id}')">
2745
+ Base Run ${getSmallStatusBadge(test.final_status || test.status)}
2746
+ </button>
2747
+ ${test.retryHistory.map((retry, idx) => `
2748
+ <button class="retry-tab" onclick="switchRetryTab(event, 'retry-${idx + 1}-${test.id}')">
2749
+ Retry ${idx + 1} ${getSmallStatusBadge(retry.final_status || retry.status)}
2750
+ </button>
2751
+ `).join('')}
2752
+ </div>
2753
+
2754
+ <div id="base-run-${test.id}" class="retry-tab-content active">
2755
+ ${getTestContentHTML(test, 'base')}
2756
+ </div>
2757
+
2758
+ ${test.retryHistory.map((retry, idx) => `
2759
+ <div id="retry-${idx + 1}-${test.id}" class="retry-tab-content" style="display: none;">
2760
+ ${getTestContentHTML(retry, `retry-${idx + 1}`)}
2761
+ </div>
2762
+ `).join('')}
2763
+ </div>
2764
+ ` : getTestContentHTML(test, 'single')}
2630
2765
  </div>
2631
2766
  </div>`;
2632
2767
  })
@@ -2677,7 +2812,9 @@ function generateHTML(reportData, trendData = null) {
2677
2812
  --glow-primary: 0 0 20px rgba(99, 102, 241, 0.4), 0 0 40px rgba(99, 102, 241, 0.2);
2678
2813
  --glow-success: 0 0 20px rgba(16, 185, 129, 0.4), 0 0 40px rgba(16, 185, 129, 0.2);
2679
2814
  --glow-danger: 0 0 20px rgba(239, 68, 68, 0.4), 0 0 40px rgba(239, 68, 68, 0.2);
2680
- }
2815
+ --bg-card: #ffffff; --bg-card-hover: #f8fafc;
2816
+ --gradient-card: linear-gradient(145deg, #ffffff 0%, #f9fafb 100%);
2817
+ --border-medium: #cbd5e1;
2681
2818
  * { margin: 0; padding: 0; box-sizing: border-box; }
2682
2819
  ::selection { background: var(--primary-color); color: white; }
2683
2820
  ::-webkit-scrollbar { width: 0; height: 0; display: none; }
@@ -2749,11 +2886,11 @@ function generateHTML(reportData, trendData = null) {
2749
2886
  display: flex;
2750
2887
  gap: 16px;
2751
2888
  align-items: stretch;
2752
- background: #ffffff;
2889
+ background: transparent;
2753
2890
  border-radius: 12px;
2754
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
2755
- border: 1px solid #e2e8f0;
2756
- overflow: hidden;
2891
+ padding: 0;
2892
+ box-shadow: var(--shadow-md); /* Inherited from base static style */
2893
+ overflow: hidden; /* Inherited */
2757
2894
  }
2758
2895
  .run-info-item {
2759
2896
  display: flex;
@@ -2762,54 +2899,61 @@ function generateHTML(reportData, trendData = null) {
2762
2899
  padding: 16px 28px;
2763
2900
  position: relative;
2764
2901
  flex: 1;
2765
- min-width: 0;
2766
- max-width: 100%;
2767
- overflow-wrap: break-word;
2768
- word-break: break-word;
2769
- }
2770
- .run-info-item:not(:last-child)::after {
2771
- content: '';
2772
- position: absolute;
2773
- right: 0;
2774
- top: 20%;
2775
- bottom: 20%;
2776
- width: 1px;
2777
- background: linear-gradient(to bottom, transparent, #e2e8f0 20%, #e2e8f0 80%, transparent);
2902
+ min-width: fit-content;
2778
2903
  }
2904
+
2779
2905
  .run-info-item:first-child {
2780
- background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
2906
+ background: linear-gradient(135deg, rgba(251, 191, 36, 0.2) 0%, rgba(245, 158, 11, 0.15) 50%, rgba(217, 119, 6, 0.1) 100%);
2907
+ border: 1px solid rgba(251, 191, 36, 0.3);
2908
+ border-radius: var(--radius-md);
2909
+ box-shadow: 0 4px 16px rgba(251, 191, 36, 0.2), inset 0 1px 0 rgba(251, 191, 36, 0.25), 0 0 40px rgba(251, 191, 36, 0.08);
2910
+ }
2911
+ .run-info-item:first-child:hover {
2912
+ background: linear-gradient(135deg, rgba(251, 191, 36, 0.28) 0%, rgba(245, 158, 11, 0.22) 50%, rgba(217, 119, 6, 0.15) 100%);
2913
+ border-color: rgba(251, 191, 36, 0.4);
2914
+ box-shadow: 0 8px 24px rgba(251, 191, 36, 0.3), inset 0 1px 0 rgba(251, 191, 36, 0.35), 0 0 50px rgba(251, 191, 36, 0.15);
2781
2915
  }
2782
2916
  .run-info-item:last-child {
2783
- background: linear-gradient(135deg, #ddd6fe 0%, #c4b5fd 100%);
2917
+ background: linear-gradient(135deg, rgba(139, 92, 246, 0.18) 0%, rgba(124, 58, 237, 0.12) 50%, rgba(109, 40, 217, 0.08) 100%);
2918
+ border: 1px solid rgba(139, 92, 246, 0.3);
2919
+ border-radius: var(--radius-md);
2920
+ box-shadow: 0 4px 16px rgba(139, 92, 246, 0.2), inset 0 1px 0 rgba(139, 92, 246, 0.25), 0 0 40px rgba(139, 92, 246, 0.08);
2921
+ }
2922
+ .run-info-item:last-child:hover {
2923
+ background: linear-gradient(135deg, rgba(139, 92, 246, 0.25) 0%, rgba(124, 58, 237, 0.18) 50%, rgba(109, 40, 217, 0.12) 100%);
2924
+ border-color: rgba(139, 92, 246, 0.4);
2925
+ box-shadow: 0 8px 24px rgba(139, 92, 246, 0.3), inset 0 1px 0 rgba(139, 92, 246, 0.35), 0 0 50px rgba(139, 92, 246, 0.15);
2784
2926
  }
2785
2927
  .run-info strong {
2786
2928
  display: flex;
2787
2929
  align-items: center;
2788
- gap: 6px;
2930
+ gap: 8px;
2789
2931
  font-size: 0.7em;
2790
2932
  text-transform: uppercase;
2791
- letter-spacing: 1px;
2792
- color: #64748b;
2933
+ letter-spacing: 1.2px;
2934
+ color: #9ca3af;
2793
2935
  margin: 0;
2794
2936
  font-weight: 700;
2795
2937
  }
2796
2938
  .run-info strong::before {
2797
2939
  content: '';
2798
- width: 8px;
2799
- height: 8px;
2940
+ width: 10px;
2941
+ height: 10px;
2800
2942
  border-radius: 50%;
2801
2943
  background: currentColor;
2802
- opacity: 0.6;
2944
+ opacity: 0.7;
2945
+ box-shadow: 0 0 8px currentColor;
2803
2946
  }
2804
2947
  .run-info-item:first-child strong {
2805
- color: #92400e;
2948
+ color: var(--warning-light);
2806
2949
  }
2807
2950
  .run-info-item:last-child strong {
2808
- color: #5b21b6;
2951
+ color: var(--secondary-light);
2809
2952
  }
2810
2953
  .run-info span {
2954
+ font-size: 1.5em;
2811
2955
  font-weight: 800;
2812
- color: #0f172a;
2956
+ color: #0f172a; /* Adjusted for light theme consistency, static uses #f9fafb */
2813
2957
  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
2814
2958
  letter-spacing: -0.02em;
2815
2959
  line-height: 1.2;
@@ -2873,12 +3017,17 @@ function generateHTML(reportData, trendData = null) {
2873
3017
  }
2874
3018
  }
2875
3019
 
3020
+
3021
+ .stat-pill.flaky { color: #4b5563; }
3022
+
2876
3023
  .dashboard-grid {
2877
3024
  display: grid;
2878
3025
  grid-template-columns: repeat(4, 1fr);
2879
3026
  gap: 0;
2880
3027
  margin: 0 0 40px 0;
2881
3028
  }
3029
+ .stats-pill.failed { color: var(--danger-dark); }
3030
+ .stats-pill.flaky { color: #4b5563; }
2882
3031
  .browser-breakdown {
2883
3032
  display: flex;
2884
3033
  flex-direction: column;
@@ -2919,9 +3068,17 @@ function generateHTML(reportData, trendData = null) {
2919
3068
  color: #0f172a;
2920
3069
  text-transform: capitalize;
2921
3070
  font-size: 1.05em;
3071
+ white-space: nowrap;
3072
+ overflow: hidden;
3073
+ text-overflow: ellipsis;
3074
+ flex: 1;
3075
+ min-width: 0;
3076
+ margin-right: 8px;
2922
3077
  }
2923
3078
  .browser-stats {
2924
3079
  color: #64748b;
3080
+ white-space: nowrap;
3081
+ flex-shrink: 0;
2925
3082
  font-weight: 700;
2926
3083
  font-size: 0.95em;
2927
3084
  }
@@ -2965,9 +3122,11 @@ function generateHTML(reportData, trendData = null) {
2965
3122
  align-items: flex-start;
2966
3123
  }
2967
3124
  .run-info {
3125
+ flex-direction: column;
3126
+ gap: 0;
2968
3127
  width: 100%;
2969
- justify-content: flex-start;
2970
- gap: 24px;
3128
+ border-radius: 14px;
3129
+ overflow: hidden;
2971
3130
  }
2972
3131
  .dashboard-grid {
2973
3132
  grid-template-columns: repeat(2, 1fr);
@@ -2976,11 +3135,23 @@ function generateHTML(reportData, trendData = null) {
2976
3135
  .summary-card:nth-child(n+7) { border-bottom: none; }
2977
3136
  .filters {
2978
3137
  padding: 24px;
2979
- flex-direction: column;
3138
+ flex-wrap: wrap;
3139
+ gap: 12px;
3140
+ }
3141
+ .filters input {
3142
+ flex: 1 1 auto;
3143
+ min-width: 0;
3144
+ width: auto;
3145
+ }
3146
+ .filters select {
3147
+ flex: 0 0 auto;
3148
+ min-width: 0;
3149
+ width: auto;
3150
+ }
3151
+ .filters button {
3152
+ width: auto;
3153
+ flex: 0 0 auto;
2980
3154
  }
2981
- .filters input { min-width: 100%; }
2982
- .filters select { min-width: 100%; }
2983
- .filters button { width: 100%; }
2984
3155
  .copy-btn {
2985
3156
  font-size: 0.75em;
2986
3157
  padding: 8px 16px;
@@ -3179,16 +3350,13 @@ function generateHTML(reportData, trendData = null) {
3179
3350
  display: none;
3180
3351
  }
3181
3352
  .run-info-item:not(:last-child) {
3182
- border-bottom: 1px solid var(--light-gray-color);
3353
+ border-bottom: 1px solid var(--border-medium);
3183
3354
  }
3184
- .run-info strong {
3185
- font-size: 0.65em;
3355
+ .run-info strong {
3356
+ font-size: 0.65em;
3186
3357
  }
3187
- .run-info span {
3188
- font-size: 1.1em;
3189
- white-space: normal;
3190
- word-break: break-word;
3191
- overflow-wrap: break-word;
3358
+ .run-info span {
3359
+ font-size: 1.1em;
3192
3360
  }
3193
3361
  .tabs {
3194
3362
  flex-wrap: wrap;
@@ -3304,12 +3472,19 @@ function generateHTML(reportData, trendData = null) {
3304
3472
  box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2);
3305
3473
  }
3306
3474
  .summary-card.status-failed .value { color: #ef4444; }
3475
+ .summary-card.status-flaky::before { background: var(--neutral-500); }
3307
3476
  .summary-card.status-skipped { background: rgba(245, 158, 11, 0.02); }
3308
3477
  .summary-card.status-skipped:hover {
3309
3478
  background: rgba(245, 158, 11, 0.15);
3310
3479
  box-shadow: 0 4px 12px rgba(245, 158, 11, 0.2);
3311
3480
  }
3312
3481
  .summary-card.status-skipped .value { color: #f59e0b; }
3482
+ .summary-card.flaky-status { background: rgba(156, 163, 175, 0.05); }
3483
+ .summary-card.flaky-status:hover {
3484
+ background: rgba(156, 163, 175, 0.15);
3485
+ box-shadow: 0 4px 12px rgba(156, 163, 175, 0.2);
3486
+ }
3487
+ .summary-card.flaky-status .value { color: #64748b; }
3313
3488
  .summary-card:not([class*='status-']) .value { color: #0f172a; }
3314
3489
  .dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: start; }
3315
3490
  .dashboard-column {
@@ -3350,59 +3525,167 @@ function generateHTML(reportData, trendData = null) {
3350
3525
  .status-badge-small-tooltip.status-failed { background-color: var(--danger-color); }
3351
3526
  .status-badge-small-tooltip.status-skipped { background-color: var(--warning-color); }
3352
3527
  .status-badge-small-tooltip.status-unknown { background-color: var(--dark-gray-color); }
3353
- .suites-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
3528
+ .suites-header {
3529
+ flex-shrink: 0;
3530
+ display: flex;
3531
+ justify-content: space-between;
3532
+ align-items: center;
3533
+ margin-bottom: 20px;
3534
+ }
3354
3535
  .summary-badge { background-color: var(--light-gray-color); color: var(--text-color-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
3355
3536
  .suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
3356
- .suite-card {
3357
- border: none;
3358
- border-left: 4px solid #e2e8f0;
3359
- padding: 24px;
3360
- background: cornsilk;
3361
- transition: all 0.15s ease;
3362
- border-radius: 10px;
3537
+ .suites-widget {
3538
+ display: flex;
3539
+ flex-direction: column;
3363
3540
  }
3364
- .suite-card:hover {
3365
- background: #fafbfc;
3366
- border-left-color: #6366f1;
3367
- }
3368
- .suite-card.status-passed { border-left-color: #10b981; }
3369
- .suite-card.status-passed:hover { background: rgba(16, 185, 129, 0.02); }
3370
- .suite-card.status-failed { border-left-color: #ef4444; }
3371
- .suite-card.status-failed:hover { background: rgba(239, 68, 68, 0.02); }
3372
- .suite-card.status-skipped { border-left-color: #f59e0b; }
3373
- .suite-card.status-skipped:hover { background: rgba(245, 158, 11, 0.02); }
3374
- .suite-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
3375
- .suite-name { font-weight: 600; font-size: 1.05em; color: var(--text-color); margin-right: 10px; word-break: break-word;}
3541
+ .fixed-height-widget {
3542
+ height: 450px;
3543
+ }
3544
+ .suites-grid-container {
3545
+ flex-grow: 1;
3546
+ overflow-y: auto;
3547
+ padding-right: 5px;
3548
+ }
3549
+
3550
+ @media (max-width: 768px) {
3551
+ .fixed-height-widget {
3552
+ height: auto;
3553
+ max-height: 600px;
3554
+ }
3555
+ }
3556
+ .suite-card {
3557
+ background: #ffffff;
3558
+ border: 1px solid var(--border-light);
3559
+ border-radius: 16px;
3560
+ padding: 24px;
3561
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
3562
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3563
+ display: flex;
3564
+ flex-direction: column;
3565
+ height: 100%;
3566
+ position: relative;
3567
+ overflow: hidden;
3568
+ }
3569
+ .suite-card:hover {
3570
+ transform: translateY(-4px);
3571
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
3572
+ border-color: var(--primary-light);
3573
+ }
3574
+ .suite-card::before {
3575
+ content: '';
3576
+ position: absolute;
3577
+ top: 0;
3578
+ left: 0;
3579
+ width: 100%;
3580
+ height: 4px;
3581
+ background: var(--neutral-200);
3582
+ opacity: 0.8;
3583
+ transition: background 0.3s ease;
3584
+ }
3585
+ .suite-card.status-passed::before { background: var(--success-color); }
3586
+ .suite-card.status-failed::before { background: var(--danger-color); }
3587
+ .suite-card.status-flaky::before { background: var(--neutral-500); }
3588
+ .suite-card.status-skipped::before { background: var(--warning-color); }
3589
+
3590
+ /* Outcome Badge */
3591
+ .outcome-badge {
3592
+ background-color: var(--secondary-color);
3593
+ color: #fff;
3594
+ padding: 2px 8px;
3595
+ border-radius: 4px;
3596
+ font-size: 0.75em;
3597
+ font-weight: 700;
3598
+ text-transform: uppercase;
3599
+ margin-right: 8px;
3600
+ letter-spacing: 0.5px;
3601
+ }
3602
+ .outcome-badge.flaky {
3603
+ background-color: #eab308; /* Yellow-500 */
3604
+ color: #000;
3605
+ }
3606
+
3607
+ .suite-card-header {
3608
+ display: flex;
3609
+ justify-content: space-between;
3610
+ align-items: flex-start;
3611
+ margin-bottom: 16px;
3612
+ }
3613
+ .suite-name {
3614
+ font-size: 1.15em;
3615
+ font-weight: 700;
3616
+ color: var(--text-primary);
3617
+ line-height: 1.4;
3618
+ display: -webkit-box;
3619
+ -webkit-line-clamp: 2;
3620
+ -webkit-box-orient: vertical;
3621
+ overflow: hidden;
3622
+ margin-right: 12px;
3623
+ }
3624
+ .status-indicator-dot {
3625
+ width: 10px;
3626
+ height: 10px;
3627
+ border-radius: 50%;
3628
+ flex-shrink: 0;
3629
+ margin-top: 6px;
3630
+ }
3631
+ .status-indicator-dot.status-passed { background-color: var(--success-color); box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.15); }
3632
+ .status-indicator-dot.status-failed { background-color: var(--danger-color); box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.15); }
3633
+ .status-indicator-dot.status-flaky { background-color: var(--neutral-500); box-shadow: 0 0 0 4px rgba(107, 114, 128, 0.15); }
3634
+ .status-indicator-dot.status-skipped { background-color: rgba(245, 158, 11, 0.1); color: var(--warning-dark); border: 1px solid rgba(245, 158, 11, 0.2); }
3635
+ .status-flaky { background-color: #C0C0C0; color: #000000; border: 1px solid #A0A0A0; }
3636
+
3376
3637
  .browser-tag {
3638
+ font-size: 0.8em;
3639
+ font-weight: 600;
3640
+ background: var(--bg-secondary);
3641
+ color: var(--text-secondary);
3642
+ padding: 4px 10px;
3643
+ border-radius: 20px;
3644
+ border: 1px solid var(--border-light);
3645
+ display: inline-flex;
3646
+ align-items: center;
3647
+ gap: 6px;
3648
+ margin-bottom: 20px;
3649
+ align-self: flex-start;
3650
+ box-shadow: none;
3651
+ text-shadow: none;
3652
+ }
3653
+
3654
+ .suite-card-body {
3655
+ margin-top: auto;
3656
+ }
3657
+
3658
+ .test-count-label {
3377
3659
  font-size: 0.85em;
3378
3660
  font-weight: 600;
3379
- background: linear-gradient(135deg, rgba(96, 165, 250, 0.2) 0%, rgba(59, 130, 246, 0.15) 100%);
3380
- padding: 6px 12px;
3381
- border-radius: var(--radius-sm);
3382
- border: 1px solid rgba(96, 165, 250, 0.3);
3383
- display: inline-block;
3384
- box-shadow: 0 2px 8px rgba(96, 165, 250, 0.15), inset 0 1px 0 rgba(96, 165, 250, 0.2);
3385
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
3386
- letter-spacing: 0.3px;
3387
- max-width: 200px;
3388
- overflow: hidden;
3389
- text-overflow: ellipsis;
3390
- white-space: nowrap;
3391
- vertical-align: middle;
3392
- cursor: help;
3393
- transition: all 0.2s ease;
3661
+ color: var(--text-tertiary);
3662
+ text-transform: uppercase;
3663
+ letter-spacing: 0.05em;
3664
+ margin-bottom: 8px;
3665
+ display: block;
3394
3666
  }
3395
- .browser-tag:hover {
3396
- background: linear-gradient(135deg, rgba(96, 165, 250, 0.3) 0%, rgba(59, 130, 246, 0.25) 100%);
3397
- border-color: rgba(96, 165, 250, 0.5);
3398
- }
3399
- .suite-card-body .test-count { font-size: 0.95em; color: var(--text-color-secondary); display: block; margin-bottom: 10px; }
3400
- .suite-stats { display: flex; gap: 14px; font-size: 0.95em; align-items: center; }
3401
- .suite-stats span { display: flex; align-items: center; gap: 6px; }
3402
- .suite-stats svg { vertical-align: middle; font-size: 1.15em; }
3403
- .suite-stats .stat-passed { color: #10b981; }
3404
- .suite-stats .stat-failed { color: #ef4444; }
3405
- .suite-stats .stat-skipped { color: #f59e0b; }
3667
+
3668
+ .suite-stats {
3669
+ display: flex;
3670
+ gap: 8px;
3671
+ background: var(--bg-secondary);
3672
+ padding: 10px 14px;
3673
+ border-radius: 10px;
3674
+ justify-content: space-between;
3675
+ }
3676
+
3677
+ .stat-pill {
3678
+ display: flex;
3679
+ align-items: center;
3680
+ gap: 6px;
3681
+ font-size: 0.9em;
3682
+ font-weight: 600;
3683
+ }
3684
+ .stat-pill svg { width: 14px; height: 14px; }
3685
+ .stat-pill.passed { color: var(--success-dark); }
3686
+ .stat-pill.failed { color: var(--danger-dark); }
3687
+ .stat-pill.flaky { color: #4b5563; }
3688
+ .stat-pill.skipped { color: var(--warning-dark); }
3406
3689
  .filters {
3407
3690
  display: flex;
3408
3691
  flex-wrap: wrap;
@@ -3434,6 +3717,7 @@ function generateHTML(reportData, trendData = null) {
3434
3717
  min-width: 180px;
3435
3718
  background: white;
3436
3719
  cursor: pointer;
3720
+ width: 100%;
3437
3721
  }
3438
3722
  .filters select:focus {
3439
3723
  outline: none;
@@ -3627,6 +3911,65 @@ function generateHTML(reportData, trendData = null) {
3627
3911
  border-color: rgba(148, 163, 184, 0.25);
3628
3912
  }
3629
3913
 
3914
+ /* --- RETRY COUNT BADGE --- */
3915
+ .retry-badge {
3916
+ display: inline-flex;
3917
+ align-items: center;
3918
+ padding: 5px 12px;
3919
+ border-radius: 12px;
3920
+ font-size: 0.75rem;
3921
+ font-weight: 600;
3922
+ background: rgba(147, 51, 234, 0.15);
3923
+ color: #a855f7;
3924
+ border: 1px solid rgba(147, 51, 234, 0.3);
3925
+ margin-left: 8px;
3926
+ }
3927
+
3928
+ /* --- RETRY TABS --- */
3929
+ .retry-tabs-container {
3930
+ margin-top: 16px;
3931
+ }
3932
+
3933
+ .retry-tabs-header {
3934
+ display: flex;
3935
+ gap: 8px;
3936
+ border-bottom: 2px solid var(--border-medium);
3937
+ margin-bottom: 20px;
3938
+ flex-wrap: wrap;
3939
+ }
3940
+
3941
+ .retry-tab {
3942
+ padding: 10px 20px;
3943
+ background: transparent;
3944
+ border: none;
3945
+ border-bottom: 3px solid transparent;
3946
+ cursor: pointer;
3947
+ font-size: 0.95rem;
3948
+ font-weight: 600;
3949
+ color: var(--text-color-secondary);
3950
+ transition: all 0.2s ease;
3951
+ }
3952
+
3953
+ .retry-tab:hover {
3954
+ color: var(--primary-color);
3955
+ background: rgba(147, 51, 234, 0.05);
3956
+ }
3957
+
3958
+ .retry-tab.active {
3959
+ color: #a855f7;
3960
+ border-bottom-color: #a855f7;
3961
+ background: rgba(147, 51, 234, 0.1);
3962
+ }
3963
+
3964
+ .retry-tab-content {
3965
+ animation: fadeIn 0.3s ease-in;
3966
+ }
3967
+
3968
+ @keyframes fadeIn {
3969
+ from { opacity: 0; }
3970
+ to { opacity: 1; }
3971
+ }
3972
+
3630
3973
  .tag {
3631
3974
  display: inline-flex;
3632
3975
  align-items: center;
@@ -3657,7 +4000,16 @@ function generateHTML(reportData, trendData = null) {
3657
4000
  }
3658
4001
  .test-case-content h4 { margin-top: 22px; margin-bottom: 14px; font-size: 1.15em; color: var(--primary-color); }
3659
4002
  .test-case-content p { margin-bottom: 10px; font-size: 1em; }
3660
- .test-error-summary { margin-bottom: 20px; padding: 14px; background-color: rgba(244,67,54,0.05); border: 1px solid rgba(244,67,54,0.2); border-left: 4px solid var(--danger-color); border-radius: 4px; }
4003
+ .test-error-summary {
4004
+ margin-bottom: 20px;
4005
+ padding: 14px;
4006
+ background-color: rgba(244,67,54,0.05);
4007
+ border: 1px solid rgba(244,67,54,0.2);
4008
+ border-left: 4px solid var(--danger-color);
4009
+ border-radius: 4px;
4010
+ display: flex;
4011
+ flex-direction: column;
4012
+ }
3661
4013
  .test-error-summary h4 { color: var(--danger-color); margin-top:0;}
3662
4014
  .test-error-summary pre { white-space: pre-wrap; word-break: break-all; color: var(--danger-color); font-size: 0.95em;}
3663
4015
  .steps-list { margin: 18px 0; }
@@ -3810,6 +4162,7 @@ function generateHTML(reportData, trendData = null) {
3810
4162
  color: var(--text-color);
3811
4163
  pointer-events: auto;
3812
4164
  cursor: pointer;
4165
+ width: 100%;
3813
4166
  }
3814
4167
  .filters button.clear-filters-btn:active,
3815
4168
  .filters button.clear-filters-btn:focus {
@@ -4205,31 +4558,32 @@ function generateHTML(reportData, trendData = null) {
4205
4558
  <div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
4206
4559
  runSummary.skipped || 0
4207
4560
  }</div><div class="trend-percentage">${skipPercentage}%</div></div>
4208
- <div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
4561
+ <div class="summary-card flaky-status"><h3>Flaky</h3><div class="value">${runSummary.flaky || 0}</div>
4562
+ <div class="trend-percentage">${flakyPercentage}%</div></div>
4209
4563
  <div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
4210
4564
  runSummary.duration,
4211
4565
  )}</div></div>
4212
4566
  <div class="summary-card">
4213
- <h3>🔄 Retry Count</h3>
4567
+ <h3>Total Retry Count</h3>
4214
4568
  <div class="value">${totalRetried}</div>
4215
4569
  </div>
4216
4570
  <div class="summary-card">
4217
4571
  <h3>🌐 Browser Distribution <span style="font-size: 0.7em; color: var(--text-color-secondary); font-weight: 400;">(${browserBreakdown.length} total)</span></h3>
4218
4572
  <div class="browser-breakdown" style="max-height: 200px; overflow-y: auto; padding-right: 4px;">
4219
4573
  ${browserBreakdown
4220
- .slice(0, 5)
4574
+ .slice(0, 3)
4221
4575
  .map(
4222
4576
  (b) =>
4223
4577
  `<div class="browser-item">
4224
- <span class="browser-name">${sanitizeHTML(b.browser)}</span>
4578
+ <span class="browser-name" title="${sanitizeHTML(b.browser)}">${sanitizeHTML(b.browser)}</span>
4225
4579
  <span class="browser-stats">${b.percentage}% (${b.count})</span>
4226
4580
  </div>`,
4227
4581
  )
4228
4582
  .join("")}
4229
4583
  ${
4230
- browserBreakdown.length > 5
4584
+ browserBreakdown.length > 3
4231
4585
  ? `<div class="browser-item" style="opacity: 0.6; font-style: italic; justify-content: center; border-top: 1px solid #e2e8f0; margin-top: 8px; padding-top: 8px;">
4232
- <span>+${browserBreakdown.length - 5} more browsers</span>
4586
+ <span>+${browserBreakdown.length - 3} more browsers</span>
4233
4587
  </div>`
4234
4588
  : ""
4235
4589
  }
@@ -4242,6 +4596,7 @@ function generateHTML(reportData, trendData = null) {
4242
4596
  [
4243
4597
  { label: "Passed", value: runSummary.passed },
4244
4598
  { label: "Failed", value: runSummary.failed },
4599
+ { label: "Flaky", value: runSummary.flaky || 0 },
4245
4600
  { label: "Skipped", value: runSummary.skipped || 0 },
4246
4601
  ],
4247
4602
  400,
@@ -4259,7 +4614,7 @@ function generateHTML(reportData, trendData = null) {
4259
4614
  <div id="test-runs" class="tab-content">
4260
4615
  <div class="filters">
4261
4616
  <input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
4262
- <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>
4617
+ <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>
4263
4618
  <select id="filter-browser"><option value="">All Browsers</option>${Array.from(
4264
4619
  new Set(
4265
4620
  (results || []).map((test) => test.browser || "unknown"),
@@ -4356,6 +4711,33 @@ function generateHTML(reportData, trendData = null) {
4356
4711
  });
4357
4712
  }
4358
4713
 
4714
+ // --- Retry Tab Switching Function ---
4715
+ function switchRetryTab(event, tabId) {
4716
+ const tabButton = event.currentTarget;
4717
+ const tabsContainer = tabButton.closest('.retry-tabs-container');
4718
+
4719
+ // Hide all tab contents in this container
4720
+ const allTabContents = tabsContainer.querySelectorAll('.retry-tab-content');
4721
+ allTabContents.forEach(content => {
4722
+ content.style.display = 'none';
4723
+ content.classList.remove('active');
4724
+ });
4725
+
4726
+ // Remove active class from all tabs
4727
+ const allTabs = tabsContainer.querySelectorAll('.retry-tab');
4728
+ allTabs.forEach(tab => tab.classList.remove('active'));
4729
+
4730
+ // Show selected tab content
4731
+ const selectedContent = document.getElementById(tabId);
4732
+ if (selectedContent) {
4733
+ selectedContent.style.display = 'block';
4734
+ selectedContent.classList.add('active');
4735
+ }
4736
+
4737
+ // Add active class to clicked tab
4738
+ tabButton.classList.add('active');
4739
+ }
4740
+
4359
4741
  // --- AI Failure Analyzer Functions ---
4360
4742
  function getAIFix(button) {
4361
4743
  const failureItem = button.closest('.compact-failure-item');
@@ -4990,6 +5372,7 @@ async function main() {
4990
5372
  passed: histRunReport.run.passed,
4991
5373
  failed: histRunReport.run.failed,
4992
5374
  skipped: histRunReport.run.skipped || 0,
5375
+ flaky: histRunReport.run.flaky || (histRunReport.results ? histRunReport.results.filter(r => r.status === 'flaky' || r.outcome === 'flaky').length : 0),
4993
5376
  });
4994
5377
 
4995
5378
  if (histRunReport.results && Array.isArray(histRunReport.results)) {
@@ -4998,7 +5381,7 @@ async function main() {
4998
5381
  (test) => ({
4999
5382
  testName: test.name,
5000
5383
  duration: test.duration,
5001
- status: test.status,
5384
+ status: test.final_status || test.status,
5002
5385
  timestamp: new Date(test.startTime),
5003
5386
  }),
5004
5387
  );