@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/CHANGELOG.md +31 -0
- package/README.md +5 -1
- package/dist/demo/cli.js +16 -2
- package/dist/index.js +88 -46
- package/dist/index.mjs +88 -46
- 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,37 @@
|
|
|
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
|
+
|
|
29
|
+
## [0.4.0] — 2026-06-12
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **Failure analysis** — every failed test is classified into one of seven root-cause categories (assertion, timeout, selector/locator, network, and more) and failures that share a cause are grouped into clusters, rendered as a ranked breakdown in the `detailed` template. Classification runs fully offline — no network call, no AI service. The classifier model refreshes from the licensing server in the background (offline-tolerant; the bundled model is always the floor), with optional privacy-preserving local feedback collection. See `docs/advanced/failure-analysis`.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
6
37
|
## [0.3.1] — 2026-06-09
|
|
7
38
|
|
|
8
39
|
### Security
|
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Three templates, one reporter. Every PDF is fully offline — fonts, charts, and
|
|
|
56
56
|
- **Shard merging** — combine N Playwright JSON shard reports into one PDF via `shardResults`; accepts glob patterns or explicit paths
|
|
57
57
|
- **Notifications** — post pass/fail summaries to Slack, Teams, Discord, or email after each run; configurable trigger (`always` / `failure` / `success`) per channel; email supports optional PDF attachment via `attachPdf`
|
|
58
58
|
- **Flakiness trend** — top-N flakiest tests table with per-test dot sparkline across stored history runs in the `detailed` template; `flakinessTopN` option (default 5, `0` disables)
|
|
59
|
-
- **Offline failure analysis** —
|
|
59
|
+
- **Offline failure analysis** — sorts failures into 7 root-cause buckets and clusters them, right inside the `detailed` PDF (one-liner in `executive`). No network, no AI service — a small embedded classifier that updates itself safely over your existing license refresh; pin it any time. Full details at [reportforge.org/docs/advanced/failure-analysis](https://reportforge.org/docs/advanced/failure-analysis).
|
|
60
60
|
- `reportforge-export-feedback` — exports locally-collected unrecognized failures (tokenized, local-only) for optional manual sharing.
|
|
61
61
|
- **Hybrid licensing** — one short key unlocks it; reports keep rendering offline via a cached JWT between refreshes
|
|
62
62
|
|
|
@@ -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
|
@@ -11215,16 +11215,31 @@ function runFailureAnalysis(reportData, options, cwd) {
|
|
|
11215
11215
|
// src/collector/DataCollector.ts
|
|
11216
11216
|
init_cjs_shims();
|
|
11217
11217
|
|
|
11218
|
+
// src/utils/strip-ansi.ts
|
|
11219
|
+
init_cjs_shims();
|
|
11220
|
+
var ANSI_PATTERN = new RegExp(
|
|
11221
|
+
[
|
|
11222
|
+
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
|
|
11223
|
+
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"
|
|
11224
|
+
].join("|"),
|
|
11225
|
+
"g"
|
|
11226
|
+
);
|
|
11227
|
+
function stripAnsi(input) {
|
|
11228
|
+
return input.replace(ANSI_PATTERN, "");
|
|
11229
|
+
}
|
|
11230
|
+
|
|
11218
11231
|
// src/collector/SuiteWalker.ts
|
|
11219
11232
|
init_cjs_shims();
|
|
11220
11233
|
|
|
11221
11234
|
// src/collector/stats-utils.ts
|
|
11222
11235
|
init_cjs_shims();
|
|
11236
|
+
var TOP_SUITES_FOR_CHART = 10;
|
|
11223
11237
|
function statsFromTests(tests) {
|
|
11224
11238
|
return {
|
|
11225
11239
|
total: tests.length,
|
|
11226
11240
|
passed: tests.filter((t) => t.status === "passed").length,
|
|
11227
|
-
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,
|
|
11228
11243
|
skipped: tests.filter((t) => t.status === "skipped").length,
|
|
11229
11244
|
flaky: tests.filter((t) => t.status === "flaky").length
|
|
11230
11245
|
};
|
|
@@ -11235,12 +11250,32 @@ function aggregateStats(parts) {
|
|
|
11235
11250
|
total: acc.total + s.total,
|
|
11236
11251
|
passed: acc.passed + s.passed,
|
|
11237
11252
|
failed: acc.failed + s.failed,
|
|
11253
|
+
timedOut: acc.timedOut + s.timedOut,
|
|
11238
11254
|
skipped: acc.skipped + s.skipped,
|
|
11239
11255
|
flaky: acc.flaky + s.flaky
|
|
11240
11256
|
}),
|
|
11241
|
-
{ total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 }
|
|
11257
|
+
{ total: 0, passed: 0, failed: 0, timedOut: 0, skipped: 0, flaky: 0 }
|
|
11242
11258
|
);
|
|
11243
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
|
+
}
|
|
11244
11279
|
|
|
11245
11280
|
// src/collector/SuiteWalker.ts
|
|
11246
11281
|
var SuiteWalker = class {
|
|
@@ -11300,6 +11335,18 @@ var SuiteWalker = class {
|
|
|
11300
11335
|
}
|
|
11301
11336
|
};
|
|
11302
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
|
+
|
|
11303
11350
|
// src/utils/env.ts
|
|
11304
11351
|
init_cjs_shims();
|
|
11305
11352
|
var import_child_process = require("child_process");
|
|
@@ -11389,7 +11436,6 @@ function gitCommit() {
|
|
|
11389
11436
|
}
|
|
11390
11437
|
|
|
11391
11438
|
// src/collector/DataCollector.ts
|
|
11392
|
-
var TOP_SUITES_FOR_CHART = 10;
|
|
11393
11439
|
var DataCollector = class {
|
|
11394
11440
|
constructor() {
|
|
11395
11441
|
this.resultMap = /* @__PURE__ */ new Map();
|
|
@@ -11426,13 +11472,13 @@ var DataCollector = class {
|
|
|
11426
11472
|
finalize(fullResult) {
|
|
11427
11473
|
const walker = new SuiteWalker();
|
|
11428
11474
|
const projects = walker.build(this.rootSuite, this.resultMap);
|
|
11429
|
-
const stats = this.computeStats(
|
|
11475
|
+
const stats = this.computeStats(fullResult);
|
|
11430
11476
|
const environment = this.buildEnvironment();
|
|
11431
11477
|
const charts = this.buildChartData(stats, projects);
|
|
11432
11478
|
return {
|
|
11433
11479
|
projects,
|
|
11434
11480
|
stats,
|
|
11435
|
-
failures:
|
|
11481
|
+
failures: sortBySeverity(this.failureRecords),
|
|
11436
11482
|
environment,
|
|
11437
11483
|
charts
|
|
11438
11484
|
};
|
|
@@ -11449,8 +11495,8 @@ var DataCollector = class {
|
|
|
11449
11495
|
testTitle: test.title,
|
|
11450
11496
|
suitePath,
|
|
11451
11497
|
error: {
|
|
11452
|
-
message: error.message ?? String(error),
|
|
11453
|
-
stack: error.stack
|
|
11498
|
+
message: stripAnsi(error.message ?? String(error)),
|
|
11499
|
+
stack: error.stack ? stripAnsi(error.stack) : void 0
|
|
11454
11500
|
},
|
|
11455
11501
|
screenshotPath: screenshotAttachment?.path,
|
|
11456
11502
|
screenshotBuffer: screenshotAttachment?.body,
|
|
@@ -11476,13 +11522,16 @@ var DataCollector = class {
|
|
|
11476
11522
|
if (allLabels.some((l) => l.includes("low") || l.includes("minor"))) return "low";
|
|
11477
11523
|
return "medium";
|
|
11478
11524
|
}
|
|
11479
|
-
computeStats(
|
|
11525
|
+
computeStats(fullResult) {
|
|
11480
11526
|
let total = 0, passed = 0, failed = 0, skipped = 0, flaky = 0, timedOut = 0;
|
|
11481
11527
|
for (const [, result] of this.resultMap) {
|
|
11482
11528
|
total++;
|
|
11483
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%.
|
|
11484
11533
|
case "passed":
|
|
11485
|
-
passed++;
|
|
11534
|
+
result.retry > 0 ? flaky++ : passed++;
|
|
11486
11535
|
break;
|
|
11487
11536
|
case "failed":
|
|
11488
11537
|
failed++;
|
|
@@ -11499,13 +11548,6 @@ var DataCollector = class {
|
|
|
11499
11548
|
break;
|
|
11500
11549
|
}
|
|
11501
11550
|
}
|
|
11502
|
-
for (const project of projects) {
|
|
11503
|
-
for (const suite of this.flattenSuites(project.suites)) {
|
|
11504
|
-
for (const test of suite.tests) {
|
|
11505
|
-
if (test.status === "flaky") flaky++;
|
|
11506
|
-
}
|
|
11507
|
-
}
|
|
11508
|
-
}
|
|
11509
11551
|
const duration = fullResult.duration ?? Date.now() - this.startTime;
|
|
11510
11552
|
const passRate = total > 0 ? Math.round((passed + flaky) / total * 100) : 0;
|
|
11511
11553
|
return {
|
|
@@ -11551,27 +11593,18 @@ var DataCollector = class {
|
|
|
11551
11593
|
};
|
|
11552
11594
|
}
|
|
11553
11595
|
buildChartData(stats, projects) {
|
|
11554
|
-
const suiteResults = projects
|
|
11555
|
-
(p) => p.suites.map((s) => ({
|
|
11556
|
-
label: s.title,
|
|
11557
|
-
passed: s.stats.passed,
|
|
11558
|
-
failed: s.stats.failed,
|
|
11559
|
-
skipped: s.stats.skipped
|
|
11560
|
-
}))
|
|
11561
|
-
).slice(0, TOP_SUITES_FOR_CHART);
|
|
11596
|
+
const suiteResults = buildSuiteResults(projects, TOP_SUITES_FOR_CHART);
|
|
11562
11597
|
return {
|
|
11563
11598
|
passRate: {
|
|
11564
11599
|
passed: stats.passed,
|
|
11565
11600
|
failed: stats.failed,
|
|
11601
|
+
timedOut: stats.timedOut,
|
|
11566
11602
|
skipped: stats.skipped,
|
|
11567
11603
|
flaky: stats.flaky
|
|
11568
11604
|
},
|
|
11569
11605
|
suiteResults
|
|
11570
11606
|
};
|
|
11571
11607
|
}
|
|
11572
|
-
flattenSuites(suites) {
|
|
11573
|
-
return suites.flatMap((s) => [s, ...this.flattenSuites(s.suites)]);
|
|
11574
|
-
}
|
|
11575
11608
|
};
|
|
11576
11609
|
|
|
11577
11610
|
// src/collector/ShardMerger.ts
|
|
@@ -11579,15 +11612,14 @@ init_cjs_shims();
|
|
|
11579
11612
|
var import_fs3 = require("fs");
|
|
11580
11613
|
var import_glob = require("glob");
|
|
11581
11614
|
var import_node_module = require("module");
|
|
11582
|
-
var TOP_SUITES_FOR_CHART2 = 10;
|
|
11583
11615
|
var ShardMerger = class {
|
|
11584
11616
|
merge(filePaths) {
|
|
11585
11617
|
if (filePaths.length === 0) throw new Error("shardResults: no valid JSON shard files found");
|
|
11586
11618
|
const resolved = this.resolveShardPaths(filePaths);
|
|
11587
11619
|
const reports = this.loadReports(resolved);
|
|
11588
11620
|
const projects = this.mergeProjects(reports);
|
|
11589
|
-
const stats = this.mergeStats(reports);
|
|
11590
|
-
const failures = this.extractFailures(reports);
|
|
11621
|
+
const stats = this.mergeStats(reports, projects);
|
|
11622
|
+
const failures = sortBySeverity(this.extractFailures(reports));
|
|
11591
11623
|
const environment = this.buildEnvironment(reports[0]);
|
|
11592
11624
|
const charts = this.buildCharts(stats, projects);
|
|
11593
11625
|
return { projects, stats, failures, environment, charts };
|
|
@@ -11694,7 +11726,7 @@ var ShardMerger = class {
|
|
|
11694
11726
|
id: spec.id,
|
|
11695
11727
|
title: spec.title,
|
|
11696
11728
|
fullTitle: [...suitePath, spec.title].join(" > "),
|
|
11697
|
-
status: this.resolveStatus(test),
|
|
11729
|
+
status: this.resolveStatus(test, lastResult),
|
|
11698
11730
|
duration: lastResult?.duration ?? 0,
|
|
11699
11731
|
retryCount: test ? test.results.length - 1 : 0,
|
|
11700
11732
|
tags: spec.tags ?? [],
|
|
@@ -11702,8 +11734,9 @@ var ShardMerger = class {
|
|
|
11702
11734
|
location: `${spec.file}:${spec.line}`
|
|
11703
11735
|
};
|
|
11704
11736
|
}
|
|
11705
|
-
resolveStatus(test) {
|
|
11737
|
+
resolveStatus(test, lastResult) {
|
|
11706
11738
|
if (!test) return "skipped";
|
|
11739
|
+
if (lastResult?.status === "timedOut") return "timedOut";
|
|
11707
11740
|
switch (test.status) {
|
|
11708
11741
|
case "expected":
|
|
11709
11742
|
return "passed";
|
|
@@ -11717,7 +11750,7 @@ var ShardMerger = class {
|
|
|
11717
11750
|
return "failed";
|
|
11718
11751
|
}
|
|
11719
11752
|
}
|
|
11720
|
-
mergeStats(reports) {
|
|
11753
|
+
mergeStats(reports, projects) {
|
|
11721
11754
|
let expected = 0, unexpected = 0, flaky = 0, skipped = 0, maxDuration = 0;
|
|
11722
11755
|
for (const r of reports) {
|
|
11723
11756
|
expected += r.stats.expected;
|
|
@@ -11728,13 +11761,14 @@ var ShardMerger = class {
|
|
|
11728
11761
|
}
|
|
11729
11762
|
const total = expected + unexpected + flaky + skipped;
|
|
11730
11763
|
const passRate = total > 0 ? Math.round((expected + flaky) / total * 100) : 0;
|
|
11764
|
+
const timedOut = projects.reduce((n, p) => n + p.stats.timedOut, 0);
|
|
11731
11765
|
return {
|
|
11732
11766
|
total,
|
|
11733
11767
|
passed: expected,
|
|
11734
|
-
failed: unexpected,
|
|
11768
|
+
failed: Math.max(0, unexpected - timedOut),
|
|
11735
11769
|
skipped,
|
|
11736
11770
|
flaky,
|
|
11737
|
-
timedOut
|
|
11771
|
+
timedOut,
|
|
11738
11772
|
duration: maxDuration,
|
|
11739
11773
|
passRate,
|
|
11740
11774
|
verdict: unexpected > 0 ? "failed" : "passed"
|
|
@@ -11772,8 +11806,8 @@ var ShardMerger = class {
|
|
|
11772
11806
|
testTitle: spec.title,
|
|
11773
11807
|
suitePath,
|
|
11774
11808
|
error: {
|
|
11775
|
-
message: error?.message ?? "Test failed",
|
|
11776
|
-
stack: error?.stack
|
|
11809
|
+
message: stripAnsi(error?.message ?? "Test failed"),
|
|
11810
|
+
stack: error?.stack ? stripAnsi(error.stack) : void 0
|
|
11777
11811
|
},
|
|
11778
11812
|
screenshotPath: lastResult?.attachments.find(
|
|
11779
11813
|
(a) => a.name.toLowerCase().includes("screenshot") && a.contentType.startsWith("image/")
|
|
@@ -11812,18 +11846,13 @@ var ShardMerger = class {
|
|
|
11812
11846
|
};
|
|
11813
11847
|
}
|
|
11814
11848
|
buildCharts(stats, projects) {
|
|
11815
|
-
const suiteResults = projects
|
|
11816
|
-
(p) => p.suites.map((s) => ({
|
|
11817
|
-
label: s.title,
|
|
11818
|
-
passed: s.stats.passed,
|
|
11819
|
-
failed: s.stats.failed,
|
|
11820
|
-
skipped: s.stats.skipped
|
|
11821
|
-
}))
|
|
11822
|
-
).slice(0, TOP_SUITES_FOR_CHART2);
|
|
11849
|
+
const suiteResults = buildSuiteResults(projects, TOP_SUITES_FOR_CHART);
|
|
11823
11850
|
return {
|
|
11824
11851
|
passRate: {
|
|
11825
11852
|
passed: stats.passed,
|
|
11826
11853
|
failed: stats.failed,
|
|
11854
|
+
timedOut: stats.timedOut,
|
|
11855
|
+
// always 0 in shard JSON (no separate timedOut count)
|
|
11827
11856
|
skipped: stats.skipped,
|
|
11828
11857
|
flaky: stats.flaky
|
|
11829
11858
|
},
|
|
@@ -12038,6 +12067,19 @@ var TemplateEngine = class {
|
|
|
12038
12067
|
"upperCase",
|
|
12039
12068
|
(s) => typeof s === "string" ? s.toUpperCase() : s
|
|
12040
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
|
+
});
|
|
12041
12083
|
this.handlebars.registerHelper("addOne", (n) => (n ?? 0) + 1);
|
|
12042
12084
|
this.handlebars.registerHelper(
|
|
12043
12085
|
"join",
|
|
@@ -13055,7 +13097,7 @@ var PdfReporter = class {
|
|
|
13055
13097
|
this.dataCollector = new DataCollector();
|
|
13056
13098
|
this.pdfGenerator = new PdfGenerator();
|
|
13057
13099
|
this.options = parseOptions(rawOptions);
|
|
13058
|
-
const version = true ? "0.
|
|
13100
|
+
const version = true ? "0.5.0" : "0.x";
|
|
13059
13101
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
13060
13102
|
this.licenseClient = new LicenseClient({
|
|
13061
13103
|
licenseKey: this.options.licenseKey,
|