@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 +23 -0
- package/README.md +4 -0
- package/dist/demo/cli.js +16 -2
- package/dist/index.js +71 -42
- package/dist/index.mjs +71 -42
- package/dist/templates/layouts/detailed.hbs +5 -4
- package/dist/templates/layouts/minimal.hbs +2 -0
- package/dist/templates/partials/charts.hbs +30 -16
- package/dist/templates/partials/cover-page.hbs +1 -1
- package/dist/templates/partials/executive-summary.hbs +5 -1
- package/dist/templates/partials/failure-deep-dive.hbs +2 -2
- package/dist/templates/partials/requirements-matrix.hbs +1 -1
- package/dist/templates/partials/slow-tests.hbs +2 -2
- package/dist/templates/partials/suite-breakdown.hbs +2 -1
- package/dist/templates/styles/base.css +244 -6
- package/dist/templates/styles/detailed.css +6 -159
- package/dist/templates/styles/executive.css +3 -76
- package/dist/templates/styles/minimal.css +3 -3
- package/package.json +1 -1
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 <50%, amber <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"
|
|
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"
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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"
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|