@reportforge/playwright-pdf 0.4.1 → 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/CHANGELOG.md CHANGED
@@ -3,6 +3,29 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.5.0] — 2026-06-19
7
+
8
+ ### Added
9
+
10
+ - **Timed-out tests are first-class** — surfaced as their own KPI card, pass-rate doughnut slice, and Suite Results bar segment instead of being folded into "Failed".
11
+ - **Release gate on every template** — the ship/hold verdict banner now appears in `minimal` and `detailed`, not just `executive`.
12
+ - **Severity-ranked failures** — failure cards are colour-coded and ordered critical-first (also drives defect-log numbering and sidecar-overflow priority).
13
+ - **Describe-block Suite Results** — a single-file run breaks down by describe block instead of rendering one bar.
14
+ - **Threshold-coloured requirements coverage bars** — red `<50%`, amber `<80%`, green `≥80%`.
15
+
16
+ ### Changed
17
+
18
+ - **Compact page flow** — large sections flow and break between rows instead of each starting on a fresh page; a typical `detailed` run drops ~2 pages.
19
+ - The pass-rate verdict renders "TIMED OUT" instead of "TIMEDOUT".
20
+
21
+ ### Fixed
22
+
23
+ - **Pass rate could exceed 100%** on runs with flaky tests — a passed-on-retry test was double-counted in both `passed` and `flaky`. It is now counted as flaky only, so the rate is correct and the KPI cards reconcile to the total.
24
+ - The pass-rate doughnut centre now matches the KPI strip (single source of truth) rather than computing a second, divergent figure.
25
+ - Timed-out status badges render with the correct styling, and timed-out tests are recovered in sharded (`shardResults`) runs instead of being reported as failures.
26
+
27
+ ---
28
+
6
29
  ## [0.4.0] — 2026-06-12
7
30
 
8
31
  ### Added
package/README.md CHANGED
@@ -334,6 +334,10 @@ Everything in `minimal`, **plus** Chart.js pass-rate + suite-results charts, a n
334
334
 
335
335
  Full-page cover with verdict + KPI strip, followed by a compact dashboard with charts and a failures summary (titles only — **no raw stack traces**). Best for sprint reports and management dashboards.
336
336
 
337
+ ### Shared across all three
338
+
339
+ Every report leads with a **release-gate** ship/hold banner derived from the run verdict, surfaces **timed-out** tests as their own KPI card and pass-rate chart slice, and lists failures **most-severe first** (with severity-coloured card borders in `minimal` and `detailed`). Pages **pack compactly** — large sections flow and break between rows instead of each starting on a fresh page. The Suite Results chart breaks a single-file run down by **describe block** so it never collapses to one bar. Requirements coverage bars are coloured by threshold (red &lt;50%, amber &lt;80%, green ≥80%).
340
+
337
341
  ### Generating multiple templates in one run
338
342
 
339
343
  Pass an array to `template` to generate one PDF per template from a single test run. The template name is appended to the output filename automatically:
package/dist/demo/cli.js CHANGED
@@ -6251,7 +6251,8 @@ function statsFromTests(tests) {
6251
6251
  return {
6252
6252
  total: tests.length,
6253
6253
  passed: tests.filter((t) => t.status === "passed").length,
6254
- failed: tests.filter((t) => t.status === "failed" || t.status === "timedOut").length,
6254
+ failed: tests.filter((t) => t.status === "failed").length,
6255
+ timedOut: tests.filter((t) => t.status === "timedOut").length,
6255
6256
  skipped: tests.filter((t) => t.status === "skipped").length,
6256
6257
  flaky: tests.filter((t) => t.status === "flaky").length
6257
6258
  };
@@ -6381,7 +6382,7 @@ function buildDemoFailures() {
6381
6382
  }
6382
6383
  function buildDemoCharts(passed, failed, skipped, flaky, total) {
6383
6384
  return {
6384
- passRate: { passed, failed, skipped, flaky },
6385
+ passRate: { passed, failed, timedOut: 0, skipped, flaky },
6385
6386
  suiteResults: [
6386
6387
  suiteEntry("auth", DEMO_AUTH_TESTS),
6387
6388
  suiteEntry("checkout", DEMO_CHECKOUT_TESTS),
@@ -6714,6 +6715,19 @@ var TemplateEngine = class {
6714
6715
  "upperCase",
6715
6716
  (s) => typeof s === "string" ? s.toUpperCase() : s
6716
6717
  );
6718
+ this.handlebars.registerHelper("verdictLabel", (v) => {
6719
+ const map = {
6720
+ passed: "PASSED",
6721
+ failed: "FAILED",
6722
+ timedout: "TIMED OUT",
6723
+ interrupted: "INTERRUPTED"
6724
+ };
6725
+ return map[v] ?? (typeof v === "string" ? v.toUpperCase() : v);
6726
+ });
6727
+ this.handlebars.registerHelper("coverageBand", (pct) => {
6728
+ const n = Number(pct);
6729
+ return n >= 80 ? "high" : n >= 50 ? "mid" : "low";
6730
+ });
6717
6731
  this.handlebars.registerHelper("addOne", (n) => (n ?? 0) + 1);
6718
6732
  this.handlebars.registerHelper(
6719
6733
  "join",
package/dist/index.js CHANGED
@@ -11233,11 +11233,13 @@ init_cjs_shims();
11233
11233
 
11234
11234
  // src/collector/stats-utils.ts
11235
11235
  init_cjs_shims();
11236
+ var TOP_SUITES_FOR_CHART = 10;
11236
11237
  function statsFromTests(tests) {
11237
11238
  return {
11238
11239
  total: tests.length,
11239
11240
  passed: tests.filter((t) => t.status === "passed").length,
11240
- failed: tests.filter((t) => t.status === "failed" || t.status === "timedOut").length,
11241
+ failed: tests.filter((t) => t.status === "failed").length,
11242
+ timedOut: tests.filter((t) => t.status === "timedOut").length,
11241
11243
  skipped: tests.filter((t) => t.status === "skipped").length,
11242
11244
  flaky: tests.filter((t) => t.status === "flaky").length
11243
11245
  };
@@ -11248,12 +11250,32 @@ function aggregateStats(parts) {
11248
11250
  total: acc.total + s.total,
11249
11251
  passed: acc.passed + s.passed,
11250
11252
  failed: acc.failed + s.failed,
11253
+ timedOut: acc.timedOut + s.timedOut,
11251
11254
  skipped: acc.skipped + s.skipped,
11252
11255
  flaky: acc.flaky + s.flaky
11253
11256
  }),
11254
- { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 }
11257
+ { total: 0, passed: 0, failed: 0, timedOut: 0, skipped: 0, flaky: 0 }
11255
11258
  );
11256
11259
  }
11260
+ function toRow(s) {
11261
+ return {
11262
+ label: s.title,
11263
+ passed: s.stats.passed,
11264
+ failed: s.stats.failed,
11265
+ timedOut: s.stats.timedOut,
11266
+ skipped: s.stats.skipped
11267
+ };
11268
+ }
11269
+ function buildSuiteResults(projects, limit) {
11270
+ const top = projects.flatMap((p) => p.suites);
11271
+ if (top.length <= 1 && top[0] && top[0].suites.length > 0) {
11272
+ const file = top[0];
11273
+ const fs14 = statsFromTests(file.tests);
11274
+ const rootRow = file.tests.length > 0 ? [{ label: file.title, passed: fs14.passed, failed: fs14.failed, timedOut: fs14.timedOut, skipped: fs14.skipped }] : [];
11275
+ return [...rootRow, ...file.suites.map(toRow)].slice(0, limit);
11276
+ }
11277
+ return top.map(toRow).slice(0, limit);
11278
+ }
11257
11279
 
11258
11280
  // src/collector/SuiteWalker.ts
11259
11281
  var SuiteWalker = class {
@@ -11313,6 +11335,18 @@ var SuiteWalker = class {
11313
11335
  }
11314
11336
  };
11315
11337
 
11338
+ // src/collector/severity.ts
11339
+ init_cjs_shims();
11340
+ var SEVERITY_RANK = {
11341
+ critical: 0,
11342
+ high: 1,
11343
+ medium: 2,
11344
+ low: 3
11345
+ };
11346
+ function sortBySeverity(failures) {
11347
+ return [...failures].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
11348
+ }
11349
+
11316
11350
  // src/utils/env.ts
11317
11351
  init_cjs_shims();
11318
11352
  var import_child_process = require("child_process");
@@ -11402,7 +11436,6 @@ function gitCommit() {
11402
11436
  }
11403
11437
 
11404
11438
  // src/collector/DataCollector.ts
11405
- var TOP_SUITES_FOR_CHART = 10;
11406
11439
  var DataCollector = class {
11407
11440
  constructor() {
11408
11441
  this.resultMap = /* @__PURE__ */ new Map();
@@ -11439,13 +11472,13 @@ var DataCollector = class {
11439
11472
  finalize(fullResult) {
11440
11473
  const walker = new SuiteWalker();
11441
11474
  const projects = walker.build(this.rootSuite, this.resultMap);
11442
- const stats = this.computeStats(projects, fullResult);
11475
+ const stats = this.computeStats(fullResult);
11443
11476
  const environment = this.buildEnvironment();
11444
11477
  const charts = this.buildChartData(stats, projects);
11445
11478
  return {
11446
11479
  projects,
11447
11480
  stats,
11448
- failures: [...this.failureRecords],
11481
+ failures: sortBySeverity(this.failureRecords),
11449
11482
  environment,
11450
11483
  charts
11451
11484
  };
@@ -11489,13 +11522,16 @@ var DataCollector = class {
11489
11522
  if (allLabels.some((l) => l.includes("low") || l.includes("minor"))) return "low";
11490
11523
  return "medium";
11491
11524
  }
11492
- computeStats(projects, fullResult) {
11525
+ computeStats(fullResult) {
11493
11526
  let total = 0, passed = 0, failed = 0, skipped = 0, flaky = 0, timedOut = 0;
11494
11527
  for (const [, result] of this.resultMap) {
11495
11528
  total++;
11496
11529
  switch (result.status) {
11530
+ // A test that passes on a retry (retry > 0) is flaky, NOT a clean pass.
11531
+ // Counting it in `passed` too would double-count it against `flaky` and
11532
+ // push passRate over 100%.
11497
11533
  case "passed":
11498
- passed++;
11534
+ result.retry > 0 ? flaky++ : passed++;
11499
11535
  break;
11500
11536
  case "failed":
11501
11537
  failed++;
@@ -11512,13 +11548,6 @@ var DataCollector = class {
11512
11548
  break;
11513
11549
  }
11514
11550
  }
11515
- for (const project of projects) {
11516
- for (const suite of this.flattenSuites(project.suites)) {
11517
- for (const test of suite.tests) {
11518
- if (test.status === "flaky") flaky++;
11519
- }
11520
- }
11521
- }
11522
11551
  const duration = fullResult.duration ?? Date.now() - this.startTime;
11523
11552
  const passRate = total > 0 ? Math.round((passed + flaky) / total * 100) : 0;
11524
11553
  return {
@@ -11564,27 +11593,18 @@ var DataCollector = class {
11564
11593
  };
11565
11594
  }
11566
11595
  buildChartData(stats, projects) {
11567
- const suiteResults = projects.flatMap(
11568
- (p) => p.suites.map((s) => ({
11569
- label: s.title,
11570
- passed: s.stats.passed,
11571
- failed: s.stats.failed,
11572
- skipped: s.stats.skipped
11573
- }))
11574
- ).slice(0, TOP_SUITES_FOR_CHART);
11596
+ const suiteResults = buildSuiteResults(projects, TOP_SUITES_FOR_CHART);
11575
11597
  return {
11576
11598
  passRate: {
11577
11599
  passed: stats.passed,
11578
11600
  failed: stats.failed,
11601
+ timedOut: stats.timedOut,
11579
11602
  skipped: stats.skipped,
11580
11603
  flaky: stats.flaky
11581
11604
  },
11582
11605
  suiteResults
11583
11606
  };
11584
11607
  }
11585
- flattenSuites(suites) {
11586
- return suites.flatMap((s) => [s, ...this.flattenSuites(s.suites)]);
11587
- }
11588
11608
  };
11589
11609
 
11590
11610
  // src/collector/ShardMerger.ts
@@ -11592,15 +11612,14 @@ init_cjs_shims();
11592
11612
  var import_fs3 = require("fs");
11593
11613
  var import_glob = require("glob");
11594
11614
  var import_node_module = require("module");
11595
- var TOP_SUITES_FOR_CHART2 = 10;
11596
11615
  var ShardMerger = class {
11597
11616
  merge(filePaths) {
11598
11617
  if (filePaths.length === 0) throw new Error("shardResults: no valid JSON shard files found");
11599
11618
  const resolved = this.resolveShardPaths(filePaths);
11600
11619
  const reports = this.loadReports(resolved);
11601
11620
  const projects = this.mergeProjects(reports);
11602
- const stats = this.mergeStats(reports);
11603
- const failures = this.extractFailures(reports);
11621
+ const stats = this.mergeStats(reports, projects);
11622
+ const failures = sortBySeverity(this.extractFailures(reports));
11604
11623
  const environment = this.buildEnvironment(reports[0]);
11605
11624
  const charts = this.buildCharts(stats, projects);
11606
11625
  return { projects, stats, failures, environment, charts };
@@ -11707,7 +11726,7 @@ var ShardMerger = class {
11707
11726
  id: spec.id,
11708
11727
  title: spec.title,
11709
11728
  fullTitle: [...suitePath, spec.title].join(" > "),
11710
- status: this.resolveStatus(test),
11729
+ status: this.resolveStatus(test, lastResult),
11711
11730
  duration: lastResult?.duration ?? 0,
11712
11731
  retryCount: test ? test.results.length - 1 : 0,
11713
11732
  tags: spec.tags ?? [],
@@ -11715,8 +11734,9 @@ var ShardMerger = class {
11715
11734
  location: `${spec.file}:${spec.line}`
11716
11735
  };
11717
11736
  }
11718
- resolveStatus(test) {
11737
+ resolveStatus(test, lastResult) {
11719
11738
  if (!test) return "skipped";
11739
+ if (lastResult?.status === "timedOut") return "timedOut";
11720
11740
  switch (test.status) {
11721
11741
  case "expected":
11722
11742
  return "passed";
@@ -11730,7 +11750,7 @@ var ShardMerger = class {
11730
11750
  return "failed";
11731
11751
  }
11732
11752
  }
11733
- mergeStats(reports) {
11753
+ mergeStats(reports, projects) {
11734
11754
  let expected = 0, unexpected = 0, flaky = 0, skipped = 0, maxDuration = 0;
11735
11755
  for (const r of reports) {
11736
11756
  expected += r.stats.expected;
@@ -11741,13 +11761,14 @@ var ShardMerger = class {
11741
11761
  }
11742
11762
  const total = expected + unexpected + flaky + skipped;
11743
11763
  const passRate = total > 0 ? Math.round((expected + flaky) / total * 100) : 0;
11764
+ const timedOut = projects.reduce((n, p) => n + p.stats.timedOut, 0);
11744
11765
  return {
11745
11766
  total,
11746
11767
  passed: expected,
11747
- failed: unexpected,
11768
+ failed: Math.max(0, unexpected - timedOut),
11748
11769
  skipped,
11749
11770
  flaky,
11750
- timedOut: 0,
11771
+ timedOut,
11751
11772
  duration: maxDuration,
11752
11773
  passRate,
11753
11774
  verdict: unexpected > 0 ? "failed" : "passed"
@@ -11825,18 +11846,13 @@ var ShardMerger = class {
11825
11846
  };
11826
11847
  }
11827
11848
  buildCharts(stats, projects) {
11828
- const suiteResults = projects.flatMap(
11829
- (p) => p.suites.map((s) => ({
11830
- label: s.title,
11831
- passed: s.stats.passed,
11832
- failed: s.stats.failed,
11833
- skipped: s.stats.skipped
11834
- }))
11835
- ).slice(0, TOP_SUITES_FOR_CHART2);
11849
+ const suiteResults = buildSuiteResults(projects, TOP_SUITES_FOR_CHART);
11836
11850
  return {
11837
11851
  passRate: {
11838
11852
  passed: stats.passed,
11839
11853
  failed: stats.failed,
11854
+ timedOut: stats.timedOut,
11855
+ // always 0 in shard JSON (no separate timedOut count)
11840
11856
  skipped: stats.skipped,
11841
11857
  flaky: stats.flaky
11842
11858
  },
@@ -12051,6 +12067,19 @@ var TemplateEngine = class {
12051
12067
  "upperCase",
12052
12068
  (s) => typeof s === "string" ? s.toUpperCase() : s
12053
12069
  );
12070
+ this.handlebars.registerHelper("verdictLabel", (v) => {
12071
+ const map = {
12072
+ passed: "PASSED",
12073
+ failed: "FAILED",
12074
+ timedout: "TIMED OUT",
12075
+ interrupted: "INTERRUPTED"
12076
+ };
12077
+ return map[v] ?? (typeof v === "string" ? v.toUpperCase() : v);
12078
+ });
12079
+ this.handlebars.registerHelper("coverageBand", (pct) => {
12080
+ const n = Number(pct);
12081
+ return n >= 80 ? "high" : n >= 50 ? "mid" : "low";
12082
+ });
12054
12083
  this.handlebars.registerHelper("addOne", (n) => (n ?? 0) + 1);
12055
12084
  this.handlebars.registerHelper(
12056
12085
  "join",
@@ -13068,7 +13097,7 @@ var PdfReporter = class {
13068
13097
  this.dataCollector = new DataCollector();
13069
13098
  this.pdfGenerator = new PdfGenerator();
13070
13099
  this.options = parseOptions(rawOptions);
13071
- const version = true ? "0.4.1" : "0.x";
13100
+ const version = true ? "0.5.0" : "0.x";
13072
13101
  logger.info(`@reportforge/playwright-pdf v${version} initialised`);
13073
13102
  this.licenseClient = new LicenseClient({
13074
13103
  licenseKey: this.options.licenseKey,
package/dist/index.mjs CHANGED
@@ -11234,11 +11234,13 @@ init_esm_shims();
11234
11234
 
11235
11235
  // src/collector/stats-utils.ts
11236
11236
  init_esm_shims();
11237
+ var TOP_SUITES_FOR_CHART = 10;
11237
11238
  function statsFromTests(tests) {
11238
11239
  return {
11239
11240
  total: tests.length,
11240
11241
  passed: tests.filter((t) => t.status === "passed").length,
11241
- 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,
11242
11244
  skipped: tests.filter((t) => t.status === "skipped").length,
11243
11245
  flaky: tests.filter((t) => t.status === "flaky").length
11244
11246
  };
@@ -11249,12 +11251,32 @@ function aggregateStats(parts) {
11249
11251
  total: acc.total + s.total,
11250
11252
  passed: acc.passed + s.passed,
11251
11253
  failed: acc.failed + s.failed,
11254
+ timedOut: acc.timedOut + s.timedOut,
11252
11255
  skipped: acc.skipped + s.skipped,
11253
11256
  flaky: acc.flaky + s.flaky
11254
11257
  }),
11255
- { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 }
11258
+ { total: 0, passed: 0, failed: 0, timedOut: 0, skipped: 0, flaky: 0 }
11256
11259
  );
11257
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
+ }
11258
11280
 
11259
11281
  // src/collector/SuiteWalker.ts
11260
11282
  var SuiteWalker = class {
@@ -11314,6 +11336,18 @@ var SuiteWalker = class {
11314
11336
  }
11315
11337
  };
11316
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
+
11317
11351
  // src/utils/env.ts
11318
11352
  init_esm_shims();
11319
11353
  import { execSync } from "child_process";
@@ -11403,7 +11437,6 @@ function gitCommit() {
11403
11437
  }
11404
11438
 
11405
11439
  // src/collector/DataCollector.ts
11406
- var TOP_SUITES_FOR_CHART = 10;
11407
11440
  var DataCollector = class {
11408
11441
  constructor() {
11409
11442
  this.resultMap = /* @__PURE__ */ new Map();
@@ -11440,13 +11473,13 @@ var DataCollector = class {
11440
11473
  finalize(fullResult) {
11441
11474
  const walker = new SuiteWalker();
11442
11475
  const projects = walker.build(this.rootSuite, this.resultMap);
11443
- const stats = this.computeStats(projects, fullResult);
11476
+ const stats = this.computeStats(fullResult);
11444
11477
  const environment = this.buildEnvironment();
11445
11478
  const charts = this.buildChartData(stats, projects);
11446
11479
  return {
11447
11480
  projects,
11448
11481
  stats,
11449
- failures: [...this.failureRecords],
11482
+ failures: sortBySeverity(this.failureRecords),
11450
11483
  environment,
11451
11484
  charts
11452
11485
  };
@@ -11490,13 +11523,16 @@ var DataCollector = class {
11490
11523
  if (allLabels.some((l) => l.includes("low") || l.includes("minor"))) return "low";
11491
11524
  return "medium";
11492
11525
  }
11493
- computeStats(projects, fullResult) {
11526
+ computeStats(fullResult) {
11494
11527
  let total = 0, passed = 0, failed = 0, skipped = 0, flaky = 0, timedOut = 0;
11495
11528
  for (const [, result] of this.resultMap) {
11496
11529
  total++;
11497
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%.
11498
11534
  case "passed":
11499
- passed++;
11535
+ result.retry > 0 ? flaky++ : passed++;
11500
11536
  break;
11501
11537
  case "failed":
11502
11538
  failed++;
@@ -11513,13 +11549,6 @@ var DataCollector = class {
11513
11549
  break;
11514
11550
  }
11515
11551
  }
11516
- for (const project of projects) {
11517
- for (const suite of this.flattenSuites(project.suites)) {
11518
- for (const test of suite.tests) {
11519
- if (test.status === "flaky") flaky++;
11520
- }
11521
- }
11522
- }
11523
11552
  const duration = fullResult.duration ?? Date.now() - this.startTime;
11524
11553
  const passRate = total > 0 ? Math.round((passed + flaky) / total * 100) : 0;
11525
11554
  return {
@@ -11565,27 +11594,18 @@ var DataCollector = class {
11565
11594
  };
11566
11595
  }
11567
11596
  buildChartData(stats, projects) {
11568
- const suiteResults = projects.flatMap(
11569
- (p) => p.suites.map((s) => ({
11570
- label: s.title,
11571
- passed: s.stats.passed,
11572
- failed: s.stats.failed,
11573
- skipped: s.stats.skipped
11574
- }))
11575
- ).slice(0, TOP_SUITES_FOR_CHART);
11597
+ const suiteResults = buildSuiteResults(projects, TOP_SUITES_FOR_CHART);
11576
11598
  return {
11577
11599
  passRate: {
11578
11600
  passed: stats.passed,
11579
11601
  failed: stats.failed,
11602
+ timedOut: stats.timedOut,
11580
11603
  skipped: stats.skipped,
11581
11604
  flaky: stats.flaky
11582
11605
  },
11583
11606
  suiteResults
11584
11607
  };
11585
11608
  }
11586
- flattenSuites(suites) {
11587
- return suites.flatMap((s) => [s, ...this.flattenSuites(s.suites)]);
11588
- }
11589
11609
  };
11590
11610
 
11591
11611
  // src/collector/ShardMerger.ts
@@ -11593,15 +11613,14 @@ init_esm_shims();
11593
11613
  import { readFileSync } from "fs";
11594
11614
  import { globSync } from "glob";
11595
11615
  import { createRequire } from "module";
11596
- var TOP_SUITES_FOR_CHART2 = 10;
11597
11616
  var ShardMerger = class {
11598
11617
  merge(filePaths) {
11599
11618
  if (filePaths.length === 0) throw new Error("shardResults: no valid JSON shard files found");
11600
11619
  const resolved = this.resolveShardPaths(filePaths);
11601
11620
  const reports = this.loadReports(resolved);
11602
11621
  const projects = this.mergeProjects(reports);
11603
- const stats = this.mergeStats(reports);
11604
- const failures = this.extractFailures(reports);
11622
+ const stats = this.mergeStats(reports, projects);
11623
+ const failures = sortBySeverity(this.extractFailures(reports));
11605
11624
  const environment = this.buildEnvironment(reports[0]);
11606
11625
  const charts = this.buildCharts(stats, projects);
11607
11626
  return { projects, stats, failures, environment, charts };
@@ -11708,7 +11727,7 @@ var ShardMerger = class {
11708
11727
  id: spec.id,
11709
11728
  title: spec.title,
11710
11729
  fullTitle: [...suitePath, spec.title].join(" > "),
11711
- status: this.resolveStatus(test),
11730
+ status: this.resolveStatus(test, lastResult),
11712
11731
  duration: lastResult?.duration ?? 0,
11713
11732
  retryCount: test ? test.results.length - 1 : 0,
11714
11733
  tags: spec.tags ?? [],
@@ -11716,8 +11735,9 @@ var ShardMerger = class {
11716
11735
  location: `${spec.file}:${spec.line}`
11717
11736
  };
11718
11737
  }
11719
- resolveStatus(test) {
11738
+ resolveStatus(test, lastResult) {
11720
11739
  if (!test) return "skipped";
11740
+ if (lastResult?.status === "timedOut") return "timedOut";
11721
11741
  switch (test.status) {
11722
11742
  case "expected":
11723
11743
  return "passed";
@@ -11731,7 +11751,7 @@ var ShardMerger = class {
11731
11751
  return "failed";
11732
11752
  }
11733
11753
  }
11734
- mergeStats(reports) {
11754
+ mergeStats(reports, projects) {
11735
11755
  let expected = 0, unexpected = 0, flaky = 0, skipped = 0, maxDuration = 0;
11736
11756
  for (const r of reports) {
11737
11757
  expected += r.stats.expected;
@@ -11742,13 +11762,14 @@ var ShardMerger = class {
11742
11762
  }
11743
11763
  const total = expected + unexpected + flaky + skipped;
11744
11764
  const passRate = total > 0 ? Math.round((expected + flaky) / total * 100) : 0;
11765
+ const timedOut = projects.reduce((n, p) => n + p.stats.timedOut, 0);
11745
11766
  return {
11746
11767
  total,
11747
11768
  passed: expected,
11748
- failed: unexpected,
11769
+ failed: Math.max(0, unexpected - timedOut),
11749
11770
  skipped,
11750
11771
  flaky,
11751
- timedOut: 0,
11772
+ timedOut,
11752
11773
  duration: maxDuration,
11753
11774
  passRate,
11754
11775
  verdict: unexpected > 0 ? "failed" : "passed"
@@ -11826,18 +11847,13 @@ var ShardMerger = class {
11826
11847
  };
11827
11848
  }
11828
11849
  buildCharts(stats, projects) {
11829
- const suiteResults = projects.flatMap(
11830
- (p) => p.suites.map((s) => ({
11831
- label: s.title,
11832
- passed: s.stats.passed,
11833
- failed: s.stats.failed,
11834
- skipped: s.stats.skipped
11835
- }))
11836
- ).slice(0, TOP_SUITES_FOR_CHART2);
11850
+ const suiteResults = buildSuiteResults(projects, TOP_SUITES_FOR_CHART);
11837
11851
  return {
11838
11852
  passRate: {
11839
11853
  passed: stats.passed,
11840
11854
  failed: stats.failed,
11855
+ timedOut: stats.timedOut,
11856
+ // always 0 in shard JSON (no separate timedOut count)
11841
11857
  skipped: stats.skipped,
11842
11858
  flaky: stats.flaky
11843
11859
  },
@@ -12052,6 +12068,19 @@ var TemplateEngine = class {
12052
12068
  "upperCase",
12053
12069
  (s) => typeof s === "string" ? s.toUpperCase() : s
12054
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
+ });
12055
12084
  this.handlebars.registerHelper("addOne", (n) => (n ?? 0) + 1);
12056
12085
  this.handlebars.registerHelper(
12057
12086
  "join",
@@ -13069,7 +13098,7 @@ var PdfReporter = class {
13069
13098
  this.dataCollector = new DataCollector();
13070
13099
  this.pdfGenerator = new PdfGenerator();
13071
13100
  this.options = parseOptions(rawOptions);
13072
- const version = true ? "0.4.1" : "0.x";
13101
+ const version = true ? "0.5.0" : "0.x";
13073
13102
  logger.info(`@reportforge/playwright-pdf v${version} initialised`);
13074
13103
  this.licenseClient = new LicenseClient({
13075
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) }}