@reportforge/playwright-pdf 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -11216,16 +11216,31 @@ function runFailureAnalysis(reportData, options, cwd) {
11216
11216
  // src/collector/DataCollector.ts
11217
11217
  init_esm_shims();
11218
11218
 
11219
+ // src/utils/strip-ansi.ts
11220
+ init_esm_shims();
11221
+ var ANSI_PATTERN = new RegExp(
11222
+ [
11223
+ "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
11224
+ "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"
11225
+ ].join("|"),
11226
+ "g"
11227
+ );
11228
+ function stripAnsi(input) {
11229
+ return input.replace(ANSI_PATTERN, "");
11230
+ }
11231
+
11219
11232
  // src/collector/SuiteWalker.ts
11220
11233
  init_esm_shims();
11221
11234
 
11222
11235
  // src/collector/stats-utils.ts
11223
11236
  init_esm_shims();
11237
+ var TOP_SUITES_FOR_CHART = 10;
11224
11238
  function statsFromTests(tests) {
11225
11239
  return {
11226
11240
  total: tests.length,
11227
11241
  passed: tests.filter((t) => t.status === "passed").length,
11228
- failed: tests.filter((t) => t.status === "failed" || t.status === "timedOut").length,
11242
+ failed: tests.filter((t) => t.status === "failed").length,
11243
+ timedOut: tests.filter((t) => t.status === "timedOut").length,
11229
11244
  skipped: tests.filter((t) => t.status === "skipped").length,
11230
11245
  flaky: tests.filter((t) => t.status === "flaky").length
11231
11246
  };
@@ -11236,12 +11251,32 @@ function aggregateStats(parts) {
11236
11251
  total: acc.total + s.total,
11237
11252
  passed: acc.passed + s.passed,
11238
11253
  failed: acc.failed + s.failed,
11254
+ timedOut: acc.timedOut + s.timedOut,
11239
11255
  skipped: acc.skipped + s.skipped,
11240
11256
  flaky: acc.flaky + s.flaky
11241
11257
  }),
11242
- { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 }
11258
+ { total: 0, passed: 0, failed: 0, timedOut: 0, skipped: 0, flaky: 0 }
11243
11259
  );
11244
11260
  }
11261
+ function toRow(s) {
11262
+ return {
11263
+ label: s.title,
11264
+ passed: s.stats.passed,
11265
+ failed: s.stats.failed,
11266
+ timedOut: s.stats.timedOut,
11267
+ skipped: s.stats.skipped
11268
+ };
11269
+ }
11270
+ function buildSuiteResults(projects, limit) {
11271
+ const top = projects.flatMap((p) => p.suites);
11272
+ if (top.length <= 1 && top[0] && top[0].suites.length > 0) {
11273
+ const file = top[0];
11274
+ const fs14 = statsFromTests(file.tests);
11275
+ const rootRow = file.tests.length > 0 ? [{ label: file.title, passed: fs14.passed, failed: fs14.failed, timedOut: fs14.timedOut, skipped: fs14.skipped }] : [];
11276
+ return [...rootRow, ...file.suites.map(toRow)].slice(0, limit);
11277
+ }
11278
+ return top.map(toRow).slice(0, limit);
11279
+ }
11245
11280
 
11246
11281
  // src/collector/SuiteWalker.ts
11247
11282
  var SuiteWalker = class {
@@ -11301,6 +11336,18 @@ var SuiteWalker = class {
11301
11336
  }
11302
11337
  };
11303
11338
 
11339
+ // src/collector/severity.ts
11340
+ init_esm_shims();
11341
+ var SEVERITY_RANK = {
11342
+ critical: 0,
11343
+ high: 1,
11344
+ medium: 2,
11345
+ low: 3
11346
+ };
11347
+ function sortBySeverity(failures) {
11348
+ return [...failures].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
11349
+ }
11350
+
11304
11351
  // src/utils/env.ts
11305
11352
  init_esm_shims();
11306
11353
  import { execSync } from "child_process";
@@ -11390,7 +11437,6 @@ function gitCommit() {
11390
11437
  }
11391
11438
 
11392
11439
  // src/collector/DataCollector.ts
11393
- var TOP_SUITES_FOR_CHART = 10;
11394
11440
  var DataCollector = class {
11395
11441
  constructor() {
11396
11442
  this.resultMap = /* @__PURE__ */ new Map();
@@ -11427,13 +11473,13 @@ var DataCollector = class {
11427
11473
  finalize(fullResult) {
11428
11474
  const walker = new SuiteWalker();
11429
11475
  const projects = walker.build(this.rootSuite, this.resultMap);
11430
- const stats = this.computeStats(projects, fullResult);
11476
+ const stats = this.computeStats(fullResult);
11431
11477
  const environment = this.buildEnvironment();
11432
11478
  const charts = this.buildChartData(stats, projects);
11433
11479
  return {
11434
11480
  projects,
11435
11481
  stats,
11436
- failures: [...this.failureRecords],
11482
+ failures: sortBySeverity(this.failureRecords),
11437
11483
  environment,
11438
11484
  charts
11439
11485
  };
@@ -11450,8 +11496,8 @@ var DataCollector = class {
11450
11496
  testTitle: test.title,
11451
11497
  suitePath,
11452
11498
  error: {
11453
- message: error.message ?? String(error),
11454
- stack: error.stack
11499
+ message: stripAnsi(error.message ?? String(error)),
11500
+ stack: error.stack ? stripAnsi(error.stack) : void 0
11455
11501
  },
11456
11502
  screenshotPath: screenshotAttachment?.path,
11457
11503
  screenshotBuffer: screenshotAttachment?.body,
@@ -11477,13 +11523,16 @@ var DataCollector = class {
11477
11523
  if (allLabels.some((l) => l.includes("low") || l.includes("minor"))) return "low";
11478
11524
  return "medium";
11479
11525
  }
11480
- computeStats(projects, fullResult) {
11526
+ computeStats(fullResult) {
11481
11527
  let total = 0, passed = 0, failed = 0, skipped = 0, flaky = 0, timedOut = 0;
11482
11528
  for (const [, result] of this.resultMap) {
11483
11529
  total++;
11484
11530
  switch (result.status) {
11531
+ // A test that passes on a retry (retry > 0) is flaky, NOT a clean pass.
11532
+ // Counting it in `passed` too would double-count it against `flaky` and
11533
+ // push passRate over 100%.
11485
11534
  case "passed":
11486
- passed++;
11535
+ result.retry > 0 ? flaky++ : passed++;
11487
11536
  break;
11488
11537
  case "failed":
11489
11538
  failed++;
@@ -11500,13 +11549,6 @@ var DataCollector = class {
11500
11549
  break;
11501
11550
  }
11502
11551
  }
11503
- for (const project of projects) {
11504
- for (const suite of this.flattenSuites(project.suites)) {
11505
- for (const test of suite.tests) {
11506
- if (test.status === "flaky") flaky++;
11507
- }
11508
- }
11509
- }
11510
11552
  const duration = fullResult.duration ?? Date.now() - this.startTime;
11511
11553
  const passRate = total > 0 ? Math.round((passed + flaky) / total * 100) : 0;
11512
11554
  return {
@@ -11552,27 +11594,18 @@ var DataCollector = class {
11552
11594
  };
11553
11595
  }
11554
11596
  buildChartData(stats, projects) {
11555
- const suiteResults = projects.flatMap(
11556
- (p) => p.suites.map((s) => ({
11557
- label: s.title,
11558
- passed: s.stats.passed,
11559
- failed: s.stats.failed,
11560
- skipped: s.stats.skipped
11561
- }))
11562
- ).slice(0, TOP_SUITES_FOR_CHART);
11597
+ const suiteResults = buildSuiteResults(projects, TOP_SUITES_FOR_CHART);
11563
11598
  return {
11564
11599
  passRate: {
11565
11600
  passed: stats.passed,
11566
11601
  failed: stats.failed,
11602
+ timedOut: stats.timedOut,
11567
11603
  skipped: stats.skipped,
11568
11604
  flaky: stats.flaky
11569
11605
  },
11570
11606
  suiteResults
11571
11607
  };
11572
11608
  }
11573
- flattenSuites(suites) {
11574
- return suites.flatMap((s) => [s, ...this.flattenSuites(s.suites)]);
11575
- }
11576
11609
  };
11577
11610
 
11578
11611
  // src/collector/ShardMerger.ts
@@ -11580,15 +11613,14 @@ init_esm_shims();
11580
11613
  import { readFileSync } from "fs";
11581
11614
  import { globSync } from "glob";
11582
11615
  import { createRequire } from "module";
11583
- var TOP_SUITES_FOR_CHART2 = 10;
11584
11616
  var ShardMerger = class {
11585
11617
  merge(filePaths) {
11586
11618
  if (filePaths.length === 0) throw new Error("shardResults: no valid JSON shard files found");
11587
11619
  const resolved = this.resolveShardPaths(filePaths);
11588
11620
  const reports = this.loadReports(resolved);
11589
11621
  const projects = this.mergeProjects(reports);
11590
- const stats = this.mergeStats(reports);
11591
- const failures = this.extractFailures(reports);
11622
+ const stats = this.mergeStats(reports, projects);
11623
+ const failures = sortBySeverity(this.extractFailures(reports));
11592
11624
  const environment = this.buildEnvironment(reports[0]);
11593
11625
  const charts = this.buildCharts(stats, projects);
11594
11626
  return { projects, stats, failures, environment, charts };
@@ -11695,7 +11727,7 @@ var ShardMerger = class {
11695
11727
  id: spec.id,
11696
11728
  title: spec.title,
11697
11729
  fullTitle: [...suitePath, spec.title].join(" > "),
11698
- status: this.resolveStatus(test),
11730
+ status: this.resolveStatus(test, lastResult),
11699
11731
  duration: lastResult?.duration ?? 0,
11700
11732
  retryCount: test ? test.results.length - 1 : 0,
11701
11733
  tags: spec.tags ?? [],
@@ -11703,8 +11735,9 @@ var ShardMerger = class {
11703
11735
  location: `${spec.file}:${spec.line}`
11704
11736
  };
11705
11737
  }
11706
- resolveStatus(test) {
11738
+ resolveStatus(test, lastResult) {
11707
11739
  if (!test) return "skipped";
11740
+ if (lastResult?.status === "timedOut") return "timedOut";
11708
11741
  switch (test.status) {
11709
11742
  case "expected":
11710
11743
  return "passed";
@@ -11718,7 +11751,7 @@ var ShardMerger = class {
11718
11751
  return "failed";
11719
11752
  }
11720
11753
  }
11721
- mergeStats(reports) {
11754
+ mergeStats(reports, projects) {
11722
11755
  let expected = 0, unexpected = 0, flaky = 0, skipped = 0, maxDuration = 0;
11723
11756
  for (const r of reports) {
11724
11757
  expected += r.stats.expected;
@@ -11729,13 +11762,14 @@ var ShardMerger = class {
11729
11762
  }
11730
11763
  const total = expected + unexpected + flaky + skipped;
11731
11764
  const passRate = total > 0 ? Math.round((expected + flaky) / total * 100) : 0;
11765
+ const timedOut = projects.reduce((n, p) => n + p.stats.timedOut, 0);
11732
11766
  return {
11733
11767
  total,
11734
11768
  passed: expected,
11735
- failed: unexpected,
11769
+ failed: Math.max(0, unexpected - timedOut),
11736
11770
  skipped,
11737
11771
  flaky,
11738
- timedOut: 0,
11772
+ timedOut,
11739
11773
  duration: maxDuration,
11740
11774
  passRate,
11741
11775
  verdict: unexpected > 0 ? "failed" : "passed"
@@ -11773,8 +11807,8 @@ var ShardMerger = class {
11773
11807
  testTitle: spec.title,
11774
11808
  suitePath,
11775
11809
  error: {
11776
- message: error?.message ?? "Test failed",
11777
- stack: error?.stack
11810
+ message: stripAnsi(error?.message ?? "Test failed"),
11811
+ stack: error?.stack ? stripAnsi(error.stack) : void 0
11778
11812
  },
11779
11813
  screenshotPath: lastResult?.attachments.find(
11780
11814
  (a) => a.name.toLowerCase().includes("screenshot") && a.contentType.startsWith("image/")
@@ -11813,18 +11847,13 @@ var ShardMerger = class {
11813
11847
  };
11814
11848
  }
11815
11849
  buildCharts(stats, projects) {
11816
- const suiteResults = projects.flatMap(
11817
- (p) => p.suites.map((s) => ({
11818
- label: s.title,
11819
- passed: s.stats.passed,
11820
- failed: s.stats.failed,
11821
- skipped: s.stats.skipped
11822
- }))
11823
- ).slice(0, TOP_SUITES_FOR_CHART2);
11850
+ const suiteResults = buildSuiteResults(projects, TOP_SUITES_FOR_CHART);
11824
11851
  return {
11825
11852
  passRate: {
11826
11853
  passed: stats.passed,
11827
11854
  failed: stats.failed,
11855
+ timedOut: stats.timedOut,
11856
+ // always 0 in shard JSON (no separate timedOut count)
11828
11857
  skipped: stats.skipped,
11829
11858
  flaky: stats.flaky
11830
11859
  },
@@ -12039,6 +12068,19 @@ var TemplateEngine = class {
12039
12068
  "upperCase",
12040
12069
  (s) => typeof s === "string" ? s.toUpperCase() : s
12041
12070
  );
12071
+ this.handlebars.registerHelper("verdictLabel", (v) => {
12072
+ const map = {
12073
+ passed: "PASSED",
12074
+ failed: "FAILED",
12075
+ timedout: "TIMED OUT",
12076
+ interrupted: "INTERRUPTED"
12077
+ };
12078
+ return map[v] ?? (typeof v === "string" ? v.toUpperCase() : v);
12079
+ });
12080
+ this.handlebars.registerHelper("coverageBand", (pct) => {
12081
+ const n = Number(pct);
12082
+ return n >= 80 ? "high" : n >= 50 ? "mid" : "low";
12083
+ });
12042
12084
  this.handlebars.registerHelper("addOne", (n) => (n ?? 0) + 1);
12043
12085
  this.handlebars.registerHelper(
12044
12086
  "join",
@@ -13056,7 +13098,7 @@ var PdfReporter = class {
13056
13098
  this.dataCollector = new DataCollector();
13057
13099
  this.pdfGenerator = new PdfGenerator();
13058
13100
  this.options = parseOptions(rawOptions);
13059
- const version = true ? "0.4.0" : "0.x";
13101
+ const version = true ? "0.5.0" : "0.x";
13060
13102
  logger.info(`@reportforge/playwright-pdf v${version} initialised`);
13061
13103
  this.licenseClient = new LicenseClient({
13062
13104
  licenseKey: this.options.licenseKey,
@@ -12,14 +12,15 @@
12
12
  </div>
13
13
  </div>
14
14
 
15
+ {{> release-gate}}
16
+
15
17
  {{> executive-summary options=(obj showPassRate=true) }}
16
18
 
17
19
  {{> charts options=(obj showCharts=true showTrend=options.showTrend) }}
18
20
 
19
- <div class="page-group">
20
- {{> requirements-matrix }}
21
- {{> ci-environment options=(obj showFullEnvironment=true) }}
22
- </div>
21
+ {{> requirements-matrix }}
22
+
23
+ {{> ci-environment options=(obj showFullEnvironment=true) }}
23
24
 
24
25
  {{> suite-breakdown options=(obj showRetries=true) }}
25
26
 
@@ -12,6 +12,8 @@
12
12
  </div>
13
13
  </div>
14
14
 
15
+ {{> release-gate}}
16
+
15
17
  {{> executive-summary options=(obj showPassRate=true showCharts=false) }}
16
18
 
17
19
  {{> suite-breakdown options=(obj showRetries=true) }}
@@ -71,12 +71,14 @@
71
71
  var remaining = 0;
72
72
  function chartDone() { if (--remaining === 0) window.__chartsReady = true; }
73
73
 
74
- var passed = {{charts.passRate.passed}};
75
- var failed = {{charts.passRate.failed}};
76
- var skipped = {{charts.passRate.skipped}};
77
- var flaky = {{charts.passRate.flaky}};
78
- var total = passed + failed + skipped + flaky;
79
- var passRate = total > 0 ? Math.round((passed / total) * 100) : 0;
74
+ var passed = {{charts.passRate.passed}};
75
+ var failed = {{charts.passRate.failed}};
76
+ var timedOut = {{charts.passRate.timedOut}};
77
+ var skipped = {{charts.passRate.skipped}};
78
+ var flaky = {{charts.passRate.flaky}};
79
+ // Pass rate is the canonical server-side stat (flaky counts as pass, denominator
80
+ // is every test) so the doughnut centre can never disagree with the KPI strip.
81
+ var passRate = {{stats.passRate}};
80
82
 
81
83
  /* ── Centre-text plugin for the doughnut ─────────────────────────────── */
82
84
  var centerTextPlugin = {
@@ -107,11 +109,11 @@
107
109
  new Chart(document.getElementById('passRateChart'), {
108
110
  type: 'doughnut',
109
111
  data: {
110
- labels: ['Passed', 'Failed', 'Skipped', 'Flaky'],
112
+ labels: ['Passed', 'Failed', 'Timed Out', 'Skipped', 'Flaky'],
111
113
  datasets: [{
112
- data: [passed, failed, skipped, flaky],
113
- backgroundColor: ['#3F7D58', '#C3553C', '#7A786E', '#D49452'],
114
- borderColor: ['#FAF9F5', '#FAF9F5', '#FAF9F5', '#FAF9F5'],
114
+ data: [passed, failed, timedOut, skipped, flaky],
115
+ backgroundColor: ['#3F7D58', '#C3553C', '#B5803A', '#7A786E', '#D49452'],
116
+ borderColor: ['#FAF9F5', '#FAF9F5', '#FAF9F5', '#FAF9F5', '#FAF9F5'],
115
117
  borderWidth: 2,
116
118
  hoverOffset: 4
117
119
  }]
@@ -135,8 +137,9 @@
135
137
  /* ── Horizontal suite results bar ────────────────────────────────────── */
136
138
  var suiteLabels = [{{{jsonArray charts.suiteResults 'label'}}}];
137
139
  var suitePassed = [{{{jsonArray charts.suiteResults 'passed'}}}];
138
- var suiteFailed = [{{{jsonArray charts.suiteResults 'failed'}}}];
139
- var suiteSkipped = [{{{jsonArray charts.suiteResults 'skipped'}}}];
140
+ var suiteFailed = [{{{jsonArray charts.suiteResults 'failed'}}}];
141
+ var suiteTimedOut = [{{{jsonArray charts.suiteResults 'timedOut'}}}];
142
+ var suiteSkipped = [{{{jsonArray charts.suiteResults 'skipped'}}}];
140
143
 
141
144
  remaining += 1;
142
145
  new Chart(document.getElementById('suitesBarChart'), {
@@ -144,9 +147,10 @@
144
147
  data: {
145
148
  labels: suiteLabels,
146
149
  datasets: [
147
- { label: 'Passed', data: suitePassed, backgroundColor: '#3F7D58', borderRadius: 3 },
148
- { label: 'Failed', data: suiteFailed, backgroundColor: '#C3553C', borderRadius: 3 },
149
- { label: 'Skipped', data: suiteSkipped, backgroundColor: '#7A786E', borderRadius: 3 }
150
+ { label: 'Passed', data: suitePassed, backgroundColor: '#3F7D58', borderRadius: 3 },
151
+ { label: 'Failed', data: suiteFailed, backgroundColor: '#C3553C', borderRadius: 3 },
152
+ { label: 'Timed Out', data: suiteTimedOut, backgroundColor: '#B5803A', borderRadius: 3 },
153
+ { label: 'Skipped', data: suiteSkipped, backgroundColor: '#7A786E', borderRadius: 3 }
150
154
  ]
151
155
  },
152
156
  options: {
@@ -161,7 +165,17 @@
161
165
  },
162
166
  y: {
163
167
  stacked: true,
164
- ticks: { font: { size: 9, family: 'Inter, sans-serif' }, color: '#1F1F1C' },
168
+ // Widen the label gutter so the longest label isn't clipped at the canvas edge.
169
+ afterFit: function(scale) { if (!scale.$rfPadded) { scale.$rfPadded = true; scale.width += 8; } },
170
+ ticks: {
171
+ font: { size: 9, family: 'Inter, sans-serif' }, color: '#1F1F1C', autoSkip: false,
172
+ // Truncate long suite / describe labels so they fit the axis gutter
173
+ // instead of clipping at the canvas edge.
174
+ callback: function(value) {
175
+ var l = this.getLabelForValue(value);
176
+ return l.length > 24 ? l.slice(0, 23) + '…' : l;
177
+ }
178
+ },
165
179
  grid: { display: false },
166
180
  border: { color: '#D6D4C8' }
167
181
  }
@@ -9,7 +9,7 @@
9
9
  <div class="cover-page__divider"></div>
10
10
 
11
11
  <span class="cover-page__verdict cover-page__verdict--{{stats.verdict}}">
12
- {{upperCase stats.verdict}}
12
+ {{verdictLabel stats.verdict}}
13
13
  </span>
14
14
 
15
15
  <div class="cover-page__stats-strip">
@@ -1,6 +1,6 @@
1
1
  <div class="section">
2
2
  <span class="verdict verdict--{{stats.verdict}}">
3
- {{upperCase stats.verdict}}
3
+ {{verdictLabel stats.verdict}}
4
4
  </span>
5
5
 
6
6
  <div class="kpi-strip">
@@ -19,6 +19,10 @@
19
19
  <div class="kpi-card__value">{{stats.failed}}</div>
20
20
  <div class="kpi-card__label">Failed</div>
21
21
  </div>
22
+ <div class="kpi-card kpi-card--timedout">
23
+ <div class="kpi-card__value">{{stats.timedOut}}</div>
24
+ <div class="kpi-card__label">Timed Out</div>
25
+ </div>
22
26
  <div class="kpi-card kpi-card--skip">
23
27
  <div class="kpi-card__value">{{stats.skipped}}</div>
24
28
  <div class="kpi-card__label">Skipped</div>
@@ -1,5 +1,5 @@
1
1
  {{#if failures.length}}
2
- <div class="section page-break">
2
+ <div class="section">
3
3
  <h2>Failures ({{failures.length}}{{#if overflowFailureCount}} shown · {{overflowFailureCount}} more in sidecar{{/if}})</h2>
4
4
  {{#if overflowFailureCount}}
5
5
  <div class="overflow-banner">
@@ -9,7 +9,7 @@
9
9
  </div>
10
10
  {{/if}}
11
11
  {{#each failures}}
12
- <div class="failure-item no-break">
12
+ <div class="failure-item failure-item--{{severity}} no-break">
13
13
  {{#if ../options.showFullFailures}}
14
14
 
15
15
  <div class="failure-item__header">
@@ -23,7 +23,7 @@
23
23
  <td style="color:{{#if failed}}var(--fail){{else}}var(--pass){{/if}};font-weight:600;">{{failed}}</td>
24
24
  <td>
25
25
  <div class="coverage-bar">
26
- <div class="coverage-bar__fill" style="width:{{coveragePct}}%;"></div>
26
+ <div class="coverage-bar__fill coverage-bar__fill--{{coverageBand coveragePct}}" style="width:{{coveragePct}}%;"></div>
27
27
  </div>
28
28
  <span style="font-size:9.5px;color:var(--text-3);">{{coveragePct}}%</span>
29
29
  </td>
@@ -16,9 +16,9 @@
16
16
  <tr class="no-break">
17
17
  <td style="font-weight:500;">{{title}}</td>
18
18
  <td style="font-size:9px;color:var(--text-3);font-family:'JetBrains Mono',monospace;">{{suiteTitle}}</td>
19
- {{#if ../showProject}}<td style="font-size:9px;color:var(--text-3);">{{project}}</td>{{/if}}
19
+ {{#if ../showProject}}<td style="font-size:9px;color:var(--text-3);white-space:nowrap;">{{project}}</td>{{/if}}
20
20
  <td style="font-family:'JetBrains Mono',monospace;font-size:9.5px;color:var(--text-2);text-align:right;">{{durationFormatted}}</td>
21
- <td><span class="badge badge--{{status}}">{{status}}</span></td>
21
+ <td>{{statusBadge status}}</td>
22
22
  </tr>
23
23
  {{/each}}
24
24
  </tbody>
@@ -15,6 +15,7 @@
15
15
  <span class="suite-stats">
16
16
  <span class="badge badge--passed">{{stats.passed}}</span>
17
17
  {{#if stats.failed}}<span class="badge badge--failed">{{stats.failed}}</span>{{/if}}
18
+ {{#if stats.timedOut}}<span class="badge badge--timedout">{{stats.timedOut}}</span>{{/if}}
18
19
  {{#if stats.skipped}}<span class="badge badge--skipped">{{stats.skipped}}</span>{{/if}}
19
20
  {{#if stats.flaky}}<span class="badge badge--flaky">{{stats.flaky}}</span>{{/if}}
20
21
  </span>
@@ -41,7 +42,7 @@
41
42
  </div>
42
43
  {{/if}}
43
44
  </td>
44
- <td><span class="badge badge--{{status}}">{{status}}</span></td>
45
+ <td>{{statusBadge status}}</td>
45
46
  <td style="color:var(--text-2);font-family:'JetBrains Mono',monospace;font-size:9.5px;">{{formatDuration duration}}</td>
46
47
  {{#if ../../options.showRetries}}<td style="color:var(--text-3);text-align:center;">{{retryCount}}</td>{{/if}}
47
48
  </tr>