@arghajit/dummy 0.3.27 → 0.3.31

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.
@@ -362,19 +362,24 @@ class PlaywrightPulseReporter {
362
362
  attempts.sort((a, b) => a.retries - b.retries);
363
363
  const firstAttempt = attempts[0];
364
364
  const retryAttempts = attempts.slice(1);
365
- if (retryAttempts.length > 0) {
365
+ // Only populate retryHistory if there were actual failures that triggered retries
366
+ // If all attempts passed, we don't need to show retry history
367
+ const hasActualRetries = retryAttempts.length > 0 && retryAttempts.some(attempt => attempt.status === 'failed' || attempt.status === 'flaky' || firstAttempt.status === 'failed' || firstAttempt.status === 'flaky');
368
+ if (hasActualRetries) {
366
369
  firstAttempt.retryHistory = retryAttempts;
367
370
  // Calculate final status and outcome from the last attempt if retries exist
368
371
  const lastAttempt = attempts[attempts.length - 1];
369
372
  firstAttempt.final_status = lastAttempt.status;
370
373
  // If the last attempt was flaky, ensure outcome is set on the main result
371
- if (lastAttempt.outcome === 'flaky') {
374
+ if (lastAttempt.outcome === 'flaky' || lastAttempt.status === 'flaky') {
372
375
  firstAttempt.outcome = 'flaky';
376
+ firstAttempt.status = 'flaky';
373
377
  }
374
378
  }
375
379
  else {
376
- // If no retries, ensure final_status is undefined (as requested)
380
+ // If no actual retries (all attempts passed), ensure final_status and retryHistory are removed
377
381
  delete firstAttempt.final_status;
382
+ delete firstAttempt.retryHistory;
378
383
  }
379
384
  finalResults.push(firstAttempt);
380
385
  }
@@ -438,6 +443,7 @@ class PlaywrightPulseReporter {
438
443
  finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
439
444
  finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
440
445
  finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
446
+ finalRunData.flaky = finalResultsList.filter((r) => r.status === "flaky").length;
441
447
  finalRunData.totalTests = finalResultsList.length;
442
448
  const reviveDates = (key, value) => {
443
449
  const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
@@ -498,6 +504,7 @@ class PlaywrightPulseReporter {
498
504
  passed: finalResults.filter((r) => r.status === "passed").length,
499
505
  failed: finalResults.filter((r) => r.status === "failed").length,
500
506
  skipped: finalResults.filter((r) => r.status === "skipped").length,
507
+ flaky: finalResults.filter((r) => r.status === "flaky").length,
501
508
  duration,
502
509
  environment: environmentDetails,
503
510
  };
@@ -70,6 +70,7 @@ export interface TestRun {
70
70
  passed: number;
71
71
  failed: number;
72
72
  skipped: number;
73
+ flaky?: number;
73
74
  duration: number;
74
75
  environment?: EnvDetails | EnvDetails[];
75
76
  }
@@ -78,6 +79,7 @@ export interface TrendDataPoint {
78
79
  passed: number;
79
80
  failed: number;
80
81
  skipped: number;
82
+ flaky?: number;
81
83
  }
82
84
  export interface SummaryMetric {
83
85
  label: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arghajit/dummy",
3
3
  "author": "Arghajit Singha",
4
- "version": "0.3.27",
4
+ "version": "0.3.31",
5
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
6
  "homepage": "https://arghajit47.github.io/playwright-pulse/",
7
7
  "repository": {
@@ -160,6 +160,8 @@ function getStatusClass(status) {
160
160
  return "status-failed";
161
161
  case "skipped":
162
162
  return "status-skipped";
163
+ case "flaky":
164
+ return "status-flaky";
163
165
  default:
164
166
  return "status-unknown";
165
167
  }
@@ -172,6 +174,8 @@ function getStatusIcon(status) {
172
174
  return "❌";
173
175
  case "skipped":
174
176
  return "⏭️";
177
+ case "flaky":
178
+ return "⚠";
175
179
  default:
176
180
  return "❓";
177
181
  }
@@ -243,13 +247,16 @@ function generateMinifiedHTML(reportData) {
243
247
  )}; font-size: 0.8em; font-weight: 600; padding: 3px 8px; border-radius: 4px; color: #fff; margin-left: 10px; white-space: nowrap;">${severity}</span>`;
244
248
 
245
249
  // --- NEW: Retry Count Badge ---
246
- const retryCountBadge = (test.retryHistory && test.retryHistory.length > 0)
250
+ const unsuccessfulRetries = (test.retryHistory || []).filter(attempt =>
251
+ attempt.status === 'failed' || attempt.status === 'timedout' || attempt.status === 'flaky'
252
+ );
253
+ const retryCountBadge = (unsuccessfulRetries.length > 0)
247
254
  ? `<span style="background-color: #f59e0b; border: 1px solid #d97706; font-size: 0.8em; font-weight: 700; padding: 4px 10px; border-radius: 50px; color: #fff; margin-left: 10px; white-space: nowrap; display: inline-flex; align-items: center; gap: 4px;">
248
255
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle;">
249
256
  <path d="M1 4v6h6"/>
250
257
  <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
251
258
  </svg>
252
- Retry Count: ${test.retryHistory.length}
259
+ Retry Count: ${unsuccessfulRetries.length}
253
260
  </span>`
254
261
  : '';
255
262
 
@@ -310,6 +317,7 @@ function generateMinifiedHTML(reportData) {
310
317
  --light-gray-color: #ecf0f1; /* Light Grey */
311
318
  --medium-gray-color: #bdc3c7; /* Medium Grey */
312
319
  --dark-gray-color: #7f8c8d; /* Dark Grey */
320
+ --flaky-color: #00ccd3; /* Cyan/Teal for Flaky */
313
321
  --text-color: #34495e; /* Dark Grey/Blue for text */
314
322
  --background-color: #f8f9fa;
315
323
  --card-background-color: #ffffff;
@@ -400,6 +408,8 @@ function generateMinifiedHTML(reportData) {
400
408
  .stat-card.failed .value { color: var(--danger-color); }
401
409
  .stat-card.skipped { border-left-color: var(--warning-color); }
402
410
  .stat-card.skipped .value { color: var(--warning-color); }
411
+ .stat-card.flaky { border-left-color: var(--flaky-color); }
412
+ .stat-card.flaky .value { color: var(--flaky-color); }
403
413
 
404
414
  .section-title {
405
415
  font-size: 1.5em;
@@ -494,6 +504,7 @@ function generateMinifiedHTML(reportData) {
494
504
  .test-item.status-passed .test-status-label { background-color: var(--success-color); }
495
505
  .test-item.status-failed .test-status-label { background-color: var(--danger-color); }
496
506
  .test-item.status-skipped .test-status-label { background-color: var(--warning-color); }
507
+ .test-item.status-flaky .test-status-label { background-color: var(--flaky-color); }
497
508
  .test-item.status-unknown .test-status-label { background-color: var(--dark-gray-color); }
498
509
 
499
510
  .no-tests {
@@ -570,6 +581,10 @@ function generateMinifiedHTML(reportData) {
570
581
  <h3>Skipped</h3>
571
582
  <div class="value">${runSummary.skipped || 0}</div>
572
583
  </div>
584
+ <div class="stat-card flaky">
585
+ <h3>Flaky</h3>
586
+ <div class="value">${runSummary.flaky || 0}</div>
587
+ </div>
573
588
  </div>
574
589
  </section>
575
590
 
@@ -599,7 +614,7 @@ function generateMinifiedHTML(reportData) {
599
614
  <option value="passed">✅ Passed</option>
600
615
  <option value="failed">❌ Failed</option>
601
616
  <option value="skipped">⏭️ Skipped</option>
602
- <option value="unknown">❓ Unknown</option>
617
+ <option value="flaky">⚠ Flaky</option>
603
618
  </select>
604
619
  <select id="filter-min-browser">
605
620
  <option value="">All Browsers</option>
@@ -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: "#00ccd3",
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 = "#00ccd3";
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: "#00ccd3" },
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 = '#00ccd3';
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>
@@ -1594,6 +1612,7 @@ function getSuitesData(results) {
1594
1612
  browser: browser,
1595
1613
  passed: 0,
1596
1614
  failed: 0,
1615
+ flaky: 0,
1597
1616
  skipped: 0,
1598
1617
  count: 0,
1599
1618
  statusOverall: "passed",
@@ -1601,12 +1620,15 @@ function getSuitesData(results) {
1601
1620
  }
1602
1621
  const suite = suitesMap.get(key);
1603
1622
  suite.count++;
1604
- const currentStatus = String(test.status).toLowerCase();
1623
+ let currentStatus = String(test.status).toLowerCase();
1624
+ if (test.outcome === 'flaky') currentStatus = 'flaky';
1605
1625
  if (currentStatus && suite[currentStatus] !== undefined) {
1606
1626
  suite[currentStatus]++;
1607
1627
  }
1608
1628
  if (currentStatus === "failed") suite.statusOverall = "failed";
1609
- 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")
1610
1632
  suite.statusOverall = "skipped";
1611
1633
  });
1612
1634
  return Array.from(suitesMap.values());
@@ -1656,6 +1678,10 @@ function generateSuitesWidget(suitesData) {
1656
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>
1657
1679
  ${suite.failed}
1658
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>
1659
1685
  <span class="stat-pill skipped" title="Skipped">
1660
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>
1661
1687
  ${suite.skipped}
@@ -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: "#00ccd3" },
2094
2124
  { name: "Skipped", data: data.skipped, color: "var(--warning-color)" },
2095
2125
  ];
2096
2126
 
@@ -2204,21 +2234,27 @@ 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
2246
+ let retriedTestsCount = 0;
2219
2247
  const totalRetried = (results || []).reduce((acc, test) => {
2220
2248
  if (test.retryHistory && test.retryHistory.length > 0) {
2221
- return acc + test.retryHistory.length;
2249
+ // Filter out any "passed" or "skipped" entries in the history
2250
+ // We only count attempts that actually failed or timed out, triggering a retry.
2251
+ const unsuccessfulRetries = test.retryHistory.filter(attempt =>
2252
+ attempt.status === 'failed' || attempt.status === 'timedout' || attempt.status === 'flaky'
2253
+ );
2254
+ if (unsuccessfulRetries.length > 0) {
2255
+ retriedTestsCount++;
2256
+ }
2257
+ return acc + unsuccessfulRetries.length;
2222
2258
  }
2223
2259
  return acc;
2224
2260
  }, 0);
@@ -2227,19 +2263,30 @@ function generateHTML(reportData, trendData = null) {
2227
2263
  let calculatedPassed = 0;
2228
2264
  let calculatedFailed = 0;
2229
2265
  let calculatedSkipped = 0;
2266
+ let calculatedFlaky = 0;
2230
2267
  let calculatedTotal = 0;
2231
2268
 
2232
2269
  (results || []).forEach(test => {
2233
2270
  calculatedTotal++;
2234
- // If retries exist and final_status is present, use it. Otherwise use test.status.
2235
- const statusToUse = (test.retryHistory && test.retryHistory.length > 0 && test.final_status)
2236
- ? test.final_status
2237
- : test.status;
2271
+ // New Logic: If outcome is 'flaky', it overrides everything.
2272
+ let statusToUse = test.status;
2273
+ if (test.outcome === 'flaky') {
2274
+ statusToUse = 'flaky';
2275
+ } else if (test.status === 'flaky') {
2276
+ // Just in case outcome wasn't set but status was (unlikely with new reporter)
2277
+ statusToUse = 'flaky';
2278
+ } else if (test.retryHistory && test.retryHistory.length > 0 && test.final_status) {
2279
+ statusToUse = test.final_status;
2280
+ }
2281
+
2282
+ // Update test status in place for charts
2283
+ test.status = statusToUse;
2238
2284
 
2239
2285
  const s = String(statusToUse).toLowerCase();
2240
2286
  if (s === 'passed') calculatedPassed++;
2241
2287
  else if (s === 'skipped') calculatedSkipped++;
2242
- else calculatedFailed++; // Treat everything else as failed for simplicity, or add specific checks
2288
+ else if (s === 'flaky') calculatedFlaky++;
2289
+ else calculatedFailed++; // failed, timedout, interrupted
2243
2290
  });
2244
2291
 
2245
2292
  // Override runSummary counts with our calculated ones if results exist
@@ -2247,9 +2294,18 @@ function generateHTML(reportData, trendData = null) {
2247
2294
  runSummary.passed = calculatedPassed;
2248
2295
  runSummary.failed = calculatedFailed;
2249
2296
  runSummary.skipped = calculatedSkipped;
2297
+ runSummary.flaky = calculatedFlaky;
2250
2298
  runSummary.totalTests = calculatedTotal;
2251
2299
  }
2252
2300
 
2301
+ const totalTestsOr1 = runSummary.totalTests || 1;
2302
+ const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
2303
+ const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
2304
+ const skipPercentage = Math.round(
2305
+ ((runSummary.skipped || 0) / totalTestsOr1) * 100,
2306
+ );
2307
+ const flakyPercentage = Math.round(((runSummary.flaky || 0) / totalTestsOr1) * 100);
2308
+
2253
2309
 
2254
2310
  // Calculate browser distribution
2255
2311
  const browserStats = (results || []).reduce((acc, test) => {
@@ -2284,8 +2340,8 @@ function generateHTML(reportData, trendData = null) {
2284
2340
  const severityBadge = `<span class="severity-badge" data-severity="${severity.toLowerCase()}">${severity}</span>`;
2285
2341
 
2286
2342
  // --- Retry Count Badge (only show if retries occurred) ---
2287
- const retryCount = test.retryHistory ? test.retryHistory.length : 0;
2288
- const retryBadge = retryCount > 0 ? `<span class="retry-badge">Retry Count: ${retryCount}</span>` : '';
2343
+ const retryCount = (test.retryHistory && test.retryHistory.length > 0) ? test.retryHistory.length : 0;
2344
+ const retryBadge = (test.retryHistory && test.retryHistory.length > 0) ? `<span class="retry-badge">Retry Count: ${retryCount}</span>` : '';
2289
2345
  const generateStepsHTML = (steps, depth = 0) => {
2290
2346
  if (!steps || steps.length === 0)
2291
2347
  return "<div class='no-steps'>No steps recorded for this test.</div>";
@@ -2381,6 +2437,7 @@ function generateHTML(reportData, trendData = null) {
2381
2437
  if(s === 'passed') colorVar = 'var(--success-color)';
2382
2438
  else if(s === 'failed') colorVar = 'var(--danger-color)';
2383
2439
  else if(s === 'skipped') colorVar = 'var(--warning-color)';
2440
+ else if(s === 'flaky') colorVar = '#00ccd3';
2384
2441
 
2385
2442
  return `<span style="
2386
2443
  display: inline-block;
@@ -2654,7 +2711,7 @@ function generateHTML(reportData, trendData = null) {
2654
2711
  ? test.final_status
2655
2712
  : test.status;
2656
2713
 
2657
- const outcomeBadge = (test.outcome)
2714
+ const outcomeBadge = (test.outcome && test.outcome !== 'flaky')
2658
2715
  ? `<span class="outcome-badge ${test.outcome}">${test.outcome}</span>`
2659
2716
  : '';
2660
2717
 
@@ -2747,7 +2804,8 @@ function generateHTML(reportData, trendData = null) {
2747
2804
  --success-color: #10b981; --success-dark: #059669; --success-light: #34d399;
2748
2805
  --danger-color: #ef4444; --danger-dark: #dc2626; --danger-light: #f87171;
2749
2806
  --warning-color: #f59e0b; --warning-dark: #d97706; --warning-light: #fbbf24;
2750
- --info-color: #3b82f6;
2807
+ --info-color: #3b82f6;
2808
+ --flaky-color: #00ccd3;
2751
2809
  --neutral-50: #fafafa; --neutral-100: #f5f5f5; --neutral-200: #e5e5e5; --neutral-300: #d4d4d4;
2752
2810
  --neutral-400: #a3a3a3; --neutral-500: #737373; --neutral-600: #525252; --neutral-700: #404040;
2753
2811
  --neutral-800: #262626; --neutral-900: #171717;
@@ -2970,12 +3028,17 @@ function generateHTML(reportData, trendData = null) {
2970
3028
  }
2971
3029
  }
2972
3030
 
3031
+
3032
+ .stat-pill.flaky { color: #4b5563; }
3033
+
2973
3034
  .dashboard-grid {
2974
3035
  display: grid;
2975
3036
  grid-template-columns: repeat(4, 1fr);
2976
3037
  gap: 0;
2977
3038
  margin: 0 0 40px 0;
2978
3039
  }
3040
+ .stats-pill.failed { color: var(--danger-dark); }
3041
+ .stats-pill.flaky { color: #4b5563; }
2979
3042
  .browser-breakdown {
2980
3043
  display: flex;
2981
3044
  flex-direction: column;
@@ -3420,12 +3483,19 @@ function generateHTML(reportData, trendData = null) {
3420
3483
  box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2);
3421
3484
  }
3422
3485
  .summary-card.status-failed .value { color: #ef4444; }
3486
+ .summary-card.status-flaky::before { background: #00ccd3; }
3423
3487
  .summary-card.status-skipped { background: rgba(245, 158, 11, 0.02); }
3424
3488
  .summary-card.status-skipped:hover {
3425
3489
  background: rgba(245, 158, 11, 0.15);
3426
3490
  box-shadow: 0 4px 12px rgba(245, 158, 11, 0.2);
3427
3491
  }
3428
3492
  .summary-card.status-skipped .value { color: #f59e0b; }
3493
+ .summary-card.flaky-status { background: rgba(0, 204, 211, 0.05); }
3494
+ .summary-card.flaky-status:hover {
3495
+ background: rgba(0, 204, 211, 0.15);
3496
+ box-shadow: 0 4px 12px rgba(0, 204, 211, 0.2);
3497
+ }
3498
+ .summary-card.flaky-status .value { color: #00ccd3; }
3429
3499
  .summary-card:not([class*='status-']) .value { color: #0f172a; }
3430
3500
  .dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: start; }
3431
3501
  .dashboard-column {
@@ -3525,6 +3595,7 @@ function generateHTML(reportData, trendData = null) {
3525
3595
  }
3526
3596
  .suite-card.status-passed::before { background: var(--success-color); }
3527
3597
  .suite-card.status-failed::before { background: var(--danger-color); }
3598
+ .suite-card.status-flaky::before { background: #00ccd3; }
3528
3599
  .suite-card.status-skipped::before { background: var(--warning-color); }
3529
3600
 
3530
3601
  /* Outcome Badge */
@@ -3540,8 +3611,8 @@ function generateHTML(reportData, trendData = null) {
3540
3611
  letter-spacing: 0.5px;
3541
3612
  }
3542
3613
  .outcome-badge.flaky {
3543
- background-color: #eab308; /* Yellow-500 */
3544
- color: #000;
3614
+ background-color: #00ccd3;
3615
+ color: #fff;
3545
3616
  }
3546
3617
 
3547
3618
  .suite-card-header {
@@ -3570,8 +3641,9 @@ function generateHTML(reportData, trendData = null) {
3570
3641
  }
3571
3642
  .status-indicator-dot.status-passed { background-color: var(--success-color); box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.15); }
3572
3643
  .status-indicator-dot.status-failed { background-color: var(--danger-color); box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.15); }
3644
+ .status-indicator-dot.status-flaky { background-color: #00ccd3; box-shadow: 0 0 0 4px rgba(0, 204, 211, 0.15); }
3573
3645
  .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); }
3574
- .status-flaky { background-color: rgba(234, 179, 8, 0.1); color: var(--warning-dark); border: 1px solid rgba(234, 179, 8, 0.2); }
3646
+ .status-flaky { background-color: rgba(0, 204, 211, 0.1); color: #00ccd3; border: 1px solid #00ccd3; }
3575
3647
 
3576
3648
  .browser-tag {
3577
3649
  font-size: 0.8em;
@@ -3623,6 +3695,7 @@ function generateHTML(reportData, trendData = null) {
3623
3695
  .stat-pill svg { width: 14px; height: 14px; }
3624
3696
  .stat-pill.passed { color: var(--success-dark); }
3625
3697
  .stat-pill.failed { color: var(--danger-dark); }
3698
+ .stat-pill.flaky { color: #00ccd3; }
3626
3699
  .stat-pill.skipped { color: var(--warning-dark); }
3627
3700
  .filters {
3628
3701
  display: flex;
@@ -3787,7 +3860,7 @@ function generateHTML(reportData, trendData = null) {
3787
3860
  border-radius: 0;
3788
3861
  font-size: 0.7em;
3789
3862
  font-weight: 800;
3790
- color: white;
3863
+ color: black;
3791
3864
  text-transform: uppercase;
3792
3865
  min-width: 100px;
3793
3866
  text-align: center;
@@ -4496,14 +4569,16 @@ function generateHTML(reportData, trendData = null) {
4496
4569
  <div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
4497
4570
  runSummary.skipped || 0
4498
4571
  }</div><div class="trend-percentage">${skipPercentage}%</div></div>
4499
- <div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
4500
- <div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
4501
- runSummary.duration,
4502
- )}</div></div>
4503
- <div class="summary-card">
4504
- <h3>Total Retry Count</h3>
4505
- <div class="value">${totalRetried}</div>
4506
- </div>
4572
+ <div class="summary-card flaky-status"><h3>Flaky</h3><div class="value">${runSummary.flaky || 0}</div>
4573
+ <div class="trend-percentage">${flakyPercentage}%</div></div>
4574
+ <div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
4575
+ runSummary.duration,
4576
+ )}</div><div class="trend-percentage">Avg. Test Duration ${avgTestDuration}</div></div>
4577
+ <div class="summary-card">
4578
+ <h3>Total Retry Count</h3>
4579
+ <div class="value">${totalRetried}</div>
4580
+ <div class="trend-percentage">Test Retried ${retriedTestsCount}</div>
4581
+ </div>
4507
4582
  <div class="summary-card">
4508
4583
  <h3>🌐 Browser Distribution <span style="font-size: 0.7em; color: var(--text-color-secondary); font-weight: 400;">(${browserBreakdown.length} total)</span></h3>
4509
4584
  <div class="browser-breakdown" style="max-height: 200px; overflow-y: auto; padding-right: 4px;">
@@ -4533,6 +4608,7 @@ function generateHTML(reportData, trendData = null) {
4533
4608
  [
4534
4609
  { label: "Passed", value: runSummary.passed },
4535
4610
  { label: "Failed", value: runSummary.failed },
4611
+ { label: "Flaky", value: runSummary.flaky || 0 },
4536
4612
  { label: "Skipped", value: runSummary.skipped || 0 },
4537
4613
  ],
4538
4614
  400,
@@ -4550,7 +4626,7 @@ function generateHTML(reportData, trendData = null) {
4550
4626
  <div id="test-runs" class="tab-content">
4551
4627
  <div class="filters">
4552
4628
  <input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
4553
- <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>
4629
+ <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>
4554
4630
  <select id="filter-browser"><option value="">All Browsers</option>${Array.from(
4555
4631
  new Set(
4556
4632
  (results || []).map((test) => test.browser || "unknown"),
@@ -5308,6 +5384,7 @@ async function main() {
5308
5384
  passed: histRunReport.run.passed,
5309
5385
  failed: histRunReport.run.failed,
5310
5386
  skipped: histRunReport.run.skipped || 0,
5387
+ flaky: histRunReport.run.flaky || (histRunReport.results ? histRunReport.results.filter(r => r.status === 'flaky' || r.outcome === 'flaky').length : 0),
5311
5388
  });
5312
5389
 
5313
5390
  if (histRunReport.results && Array.isArray(histRunReport.results)) {