@reportforge/playwright-pdf 0.5.0 → 0.6.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 +24 -0
- package/README.md +35 -0
- package/dist/demo/cli.js +188 -54
- package/dist/index.d.mts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +189 -55
- package/dist/index.mjs +189 -55
- package/dist/templates/layouts/detailed.hbs +14 -10
- package/dist/templates/layouts/executive.hbs +16 -8
- package/dist/templates/layouts/minimal.hbs +19 -5
- package/dist/templates/styles/base.css +198 -1
- package/dist/templates/styles/detailed.css +3 -104
- package/dist/templates/styles/executive.css +4 -99
- package/dist/templates/styles/minimal.css +4 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
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.6.0] — 2026-06-22
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **Configurable report sections** — a new `sections` option adds or removes any section in any built-in template (`minimal` / `detailed` / `executive`) straight from `playwright.config`. Flat keys set a baseline for every chosen template; per-template keys override per template. Covers every block (cover page, charts, trend, requirements traceability, CI/environment, suite breakdown, failures, failure analysis, slow tests, defect log, release gate) plus display modifiers (pass-rate, full environment table, retries, failure and stack-trace depth). Unknown keys are rejected with a clear configuration error. Reports render identically to before when the option is omitted.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Shared section styles (cover page, charts, defect log, coverage bars, environment table, failure analysis, suite breakdown) moved to the base stylesheet, so a section toggled into a template that didn't ship it before now renders fully styled.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## [0.5.1] — 2026-06-19
|
|
19
|
+
|
|
20
|
+
### Security
|
|
21
|
+
|
|
22
|
+
- **Chart labels escaped for inline-script safety** — a suite / describe / file title containing `</script>` can no longer break out of the inline Chart.js data block in the rendered report (relevant when the reporter runs over untrusted test code, e.g. external-contributor CI).
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **Sharper failure-analysis classification** — high-precision deterministic rules now label the error shapes the offline model struggles with: timed-out web-first assertions (`toHaveClass`/`toHaveText`/… with the element resolved) as **assertions**, connection/DNS/SSL errors as **network**, aborted/redirected/interrupted navigations as **navigation**, and resolved-to-0 / strict-mode locators as **locator-not-found**. Rules match the error header only, so call-log context and asserted values no longer cause mislabels (a navigation-wait timeout stays a timeout, a refused `page.goto` is network).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
6
30
|
## [0.5.0] — 2026-06-19
|
|
7
31
|
|
|
8
32
|
### Added
|
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ Three templates, one reporter. Every PDF is fully offline — fonts, charts, and
|
|
|
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
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
|
+
- **Configurable sections** — add or remove any report section per template via `sections`.
|
|
61
62
|
- **Hybrid licensing** — one short key unlocks it; reports keep rendering offline via a cached JWT between refreshes
|
|
62
63
|
|
|
63
64
|
---
|
|
@@ -198,6 +199,7 @@ All options are the second element of the reporter tuple.
|
|
|
198
199
|
| `showTrend` | `boolean` | `true` | Show the pass-rate sparkline and delta badge in the `detailed` template. Set to `false` to disable history tracking entirely. |
|
|
199
200
|
| `flakinessTopN` | `number` | `5` | Maximum flaky tests to show in the flakiness table (`detailed` template). Set to `0` to disable the table entirely. |
|
|
200
201
|
| `templatePath` | `string \| string[]` | — | Path to a custom Handlebars (`.hbs`) template file. Takes precedence over `template`. Pass an array to generate one PDF per custom template. See the Custom Templates docs. |
|
|
202
|
+
| `sections` | `object` | per-template | Override which report sections appear, on top of the chosen template's defaults. Flat keys are the baseline for every chosen template; per-template keys (`minimal`\|`detailed`\|`executive`) override per template. Block toggles: `coverPage`, `analysisOneliner`, `releaseGate`, `summary`, `charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`, `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`. Display modifiers: `passRate`, `fullEnvironment`, `retries`, `fullFailures`, `stackTraces`. See the Report Sections docs. |
|
|
201
203
|
<!-- AUTOGEN:options-table END -->
|
|
202
204
|
|
|
203
205
|
Full reference: [reportforge.org/docs#configuration](https://reportforge.org/docs#configuration).
|
|
@@ -360,6 +362,38 @@ reporter: [
|
|
|
360
362
|
|
|
361
363
|
One license check, one data-collection pass — faster than multiple reporter instances. Duplicate entries are silently deduplicated.
|
|
362
364
|
|
|
365
|
+
### Report Sections
|
|
366
|
+
|
|
367
|
+
Each template ships a curated set of sections. Override them per run with the
|
|
368
|
+
`sections` option — add a section a template hides, or remove one it shows.
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
reporter: [['@reportforge/playwright-pdf', {
|
|
372
|
+
template: ['minimal', 'detailed'],
|
|
373
|
+
sections: {
|
|
374
|
+
defectLog: false, // baseline: drop the defect log everywhere
|
|
375
|
+
minimal: { charts: true }, // add charts to the minimal report
|
|
376
|
+
detailed: { ciEnvironment: false },
|
|
377
|
+
},
|
|
378
|
+
}]]
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Flat keys form the baseline for every chosen template; per-template keys
|
|
382
|
+
(`minimal` / `detailed` / `executive`) override the baseline for that template.
|
|
383
|
+
|
|
384
|
+
**Block toggles:** `coverPage`, `analysisOneliner`, `releaseGate`, `summary`,
|
|
385
|
+
`charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`,
|
|
386
|
+
`failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`.
|
|
387
|
+
|
|
388
|
+
**Display modifiers:** `passRate` (summary), `fullEnvironment` (full env table vs
|
|
389
|
+
compact), `retries` (suite retries column), `fullFailures` + `stackTraces`
|
|
390
|
+
(failure detail depth).
|
|
391
|
+
|
|
392
|
+
`trend` renders only when `charts` is also on **and** history data exists (`showTrend` enabled). Data-gated sections
|
|
393
|
+
(`failureAnalysis`, `requirementsMatrix`, `slowTests`, `defectLog`) appear only
|
|
394
|
+
when matching data exists — `sections` controls rendering, never data collection.
|
|
395
|
+
An unknown key throws a config error (typo-safe).
|
|
396
|
+
|
|
363
397
|
---
|
|
364
398
|
|
|
365
399
|
## CI/CD Integration
|
|
@@ -546,6 +580,7 @@ Full documentation at [reportforge.org/docs](https://reportforge.org/docs):
|
|
|
546
580
|
- [Configuration reference](https://reportforge.org/docs#configuration) — all options
|
|
547
581
|
- [Filename tokens](https://reportforge.org/docs#tokens) — `{date}`, `{branch}`, `{status}`
|
|
548
582
|
- [Templates](https://reportforge.org/docs#templates) — minimal, detailed, executive
|
|
583
|
+
- [Report Sections](https://reportforge.org/docs#report-sections) — add or remove sections per template
|
|
549
584
|
- [License & offline behaviour](https://reportforge.org/docs#license) — JWT cache, machine cap, cancellation
|
|
550
585
|
- [Troubleshooting](https://reportforge.org/docs#troubleshooting) — common issues and debug flags
|
|
551
586
|
- **Changelog** — [CHANGELOG.md](./CHANGELOG.md)
|
package/dist/demo/cli.js
CHANGED
|
@@ -6531,12 +6531,134 @@ function dominantCategory(clusters) {
|
|
|
6531
6531
|
return clusters[0]?.category ?? "unknown";
|
|
6532
6532
|
}
|
|
6533
6533
|
|
|
6534
|
+
// src/templates/sections.ts
|
|
6535
|
+
init_cjs_shims();
|
|
6536
|
+
var SECTION_KEYS = [
|
|
6537
|
+
// block toggles
|
|
6538
|
+
"coverPage",
|
|
6539
|
+
"analysisOneliner",
|
|
6540
|
+
"releaseGate",
|
|
6541
|
+
"summary",
|
|
6542
|
+
"charts",
|
|
6543
|
+
"trend",
|
|
6544
|
+
"requirementsMatrix",
|
|
6545
|
+
"ciEnvironment",
|
|
6546
|
+
"suiteBreakdown",
|
|
6547
|
+
"failureDeepDive",
|
|
6548
|
+
"failureAnalysis",
|
|
6549
|
+
"slowTests",
|
|
6550
|
+
"defectLog",
|
|
6551
|
+
// display modifiers
|
|
6552
|
+
"passRate",
|
|
6553
|
+
"fullEnvironment",
|
|
6554
|
+
"retries",
|
|
6555
|
+
"fullFailures",
|
|
6556
|
+
"stackTraces"
|
|
6557
|
+
];
|
|
6558
|
+
var SECTION_DEFAULTS = {
|
|
6559
|
+
minimal: {
|
|
6560
|
+
coverPage: false,
|
|
6561
|
+
analysisOneliner: false,
|
|
6562
|
+
releaseGate: true,
|
|
6563
|
+
summary: true,
|
|
6564
|
+
charts: false,
|
|
6565
|
+
trend: false,
|
|
6566
|
+
requirementsMatrix: false,
|
|
6567
|
+
ciEnvironment: true,
|
|
6568
|
+
suiteBreakdown: true,
|
|
6569
|
+
failureDeepDive: true,
|
|
6570
|
+
failureAnalysis: false,
|
|
6571
|
+
slowTests: false,
|
|
6572
|
+
defectLog: false,
|
|
6573
|
+
passRate: true,
|
|
6574
|
+
fullEnvironment: false,
|
|
6575
|
+
retries: true,
|
|
6576
|
+
fullFailures: true,
|
|
6577
|
+
stackTraces: true
|
|
6578
|
+
},
|
|
6579
|
+
detailed: {
|
|
6580
|
+
coverPage: false,
|
|
6581
|
+
analysisOneliner: false,
|
|
6582
|
+
releaseGate: true,
|
|
6583
|
+
summary: true,
|
|
6584
|
+
charts: true,
|
|
6585
|
+
trend: true,
|
|
6586
|
+
requirementsMatrix: true,
|
|
6587
|
+
ciEnvironment: true,
|
|
6588
|
+
suiteBreakdown: true,
|
|
6589
|
+
failureDeepDive: true,
|
|
6590
|
+
failureAnalysis: true,
|
|
6591
|
+
slowTests: true,
|
|
6592
|
+
defectLog: true,
|
|
6593
|
+
passRate: true,
|
|
6594
|
+
fullEnvironment: true,
|
|
6595
|
+
retries: true,
|
|
6596
|
+
fullFailures: true,
|
|
6597
|
+
stackTraces: true
|
|
6598
|
+
},
|
|
6599
|
+
executive: {
|
|
6600
|
+
coverPage: true,
|
|
6601
|
+
analysisOneliner: true,
|
|
6602
|
+
releaseGate: true,
|
|
6603
|
+
summary: true,
|
|
6604
|
+
charts: true,
|
|
6605
|
+
trend: true,
|
|
6606
|
+
requirementsMatrix: false,
|
|
6607
|
+
ciEnvironment: true,
|
|
6608
|
+
suiteBreakdown: false,
|
|
6609
|
+
failureDeepDive: true,
|
|
6610
|
+
failureAnalysis: false,
|
|
6611
|
+
slowTests: true,
|
|
6612
|
+
defectLog: false,
|
|
6613
|
+
passRate: true,
|
|
6614
|
+
fullEnvironment: false,
|
|
6615
|
+
retries: false,
|
|
6616
|
+
fullFailures: false,
|
|
6617
|
+
stackTraces: false
|
|
6618
|
+
}
|
|
6619
|
+
};
|
|
6620
|
+
var SECTION_KEY_SET = new Set(SECTION_KEYS);
|
|
6621
|
+
function resolveSections(template, userSections) {
|
|
6622
|
+
const base = SECTION_DEFAULTS[template];
|
|
6623
|
+
if (!userSections) return { ...base };
|
|
6624
|
+
const flat = {};
|
|
6625
|
+
for (const [k, v] of Object.entries(userSections)) {
|
|
6626
|
+
if (v !== void 0 && SECTION_KEY_SET.has(k)) flat[k] = v;
|
|
6627
|
+
}
|
|
6628
|
+
const perTemplateRaw = userSections[template] ?? {};
|
|
6629
|
+
const perTemplate = {};
|
|
6630
|
+
for (const [k, v] of Object.entries(perTemplateRaw)) {
|
|
6631
|
+
if (v !== void 0) perTemplate[k] = v;
|
|
6632
|
+
}
|
|
6633
|
+
const result = { ...base, ...flat, ...perTemplate };
|
|
6634
|
+
if (!result.charts) result.trend = false;
|
|
6635
|
+
return result;
|
|
6636
|
+
}
|
|
6637
|
+
function allSectionsOn() {
|
|
6638
|
+
return Object.fromEntries(SECTION_KEYS.map((k) => [k, true]));
|
|
6639
|
+
}
|
|
6640
|
+
|
|
6534
6641
|
// src/templates/engine.ts
|
|
6642
|
+
function jsonForInlineScript(value) {
|
|
6643
|
+
return JSON.stringify(value).replace(/<\//g, "<\\/");
|
|
6644
|
+
}
|
|
6645
|
+
function deriveOptions(s) {
|
|
6646
|
+
return {
|
|
6647
|
+
showCharts: s.charts,
|
|
6648
|
+
showTrend: s.trend,
|
|
6649
|
+
showStackTraces: s.stackTraces,
|
|
6650
|
+
showFullEnvironment: s.fullEnvironment,
|
|
6651
|
+
showPassRate: s.passRate,
|
|
6652
|
+
showRetries: s.retries,
|
|
6653
|
+
showFullFailures: s.fullFailures
|
|
6654
|
+
};
|
|
6655
|
+
}
|
|
6535
6656
|
var TemplateEngine = class {
|
|
6536
6657
|
constructor() {
|
|
6537
6658
|
this.chartjsInline = "";
|
|
6538
6659
|
this.fontsBundleCss = "";
|
|
6539
6660
|
this.customTemplateCache = /* @__PURE__ */ new Map();
|
|
6661
|
+
this.styleCache = /* @__PURE__ */ new Map();
|
|
6540
6662
|
this.handlebars = import_handlebars.default.create();
|
|
6541
6663
|
this.templatesDir = import_path.default.resolve(__dirname, "templates");
|
|
6542
6664
|
if (!import_fs.default.existsSync(this.templatesDir)) {
|
|
@@ -6556,14 +6678,14 @@ var TemplateEngine = class {
|
|
|
6556
6678
|
setChartjsBundle(inline) {
|
|
6557
6679
|
this.chartjsInline = inline;
|
|
6558
6680
|
}
|
|
6559
|
-
render(data, template) {
|
|
6681
|
+
render(data, template, userSections) {
|
|
6560
6682
|
const layoutPath = import_path.default.join(this.templatesDir, "layouts", `${template}.hbs`);
|
|
6561
6683
|
if (!import_fs.default.existsSync(layoutPath)) {
|
|
6562
6684
|
throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
|
|
6563
6685
|
}
|
|
6564
6686
|
const layoutSource = import_fs.default.readFileSync(layoutPath, "utf8");
|
|
6565
6687
|
const compiledLayout = this.handlebars.compile(layoutSource);
|
|
6566
|
-
const ctx = this.buildContext(data, template);
|
|
6688
|
+
const ctx = this.buildContext(data, template, userSections);
|
|
6567
6689
|
return compiledLayout(ctx);
|
|
6568
6690
|
}
|
|
6569
6691
|
renderFromPath(data, filePath) {
|
|
@@ -6578,17 +6700,11 @@ var TemplateEngine = class {
|
|
|
6578
6700
|
return compiledFn(this.buildCustomContext(data));
|
|
6579
6701
|
}
|
|
6580
6702
|
buildCustomContext(data) {
|
|
6703
|
+
const sections = allSectionsOn();
|
|
6581
6704
|
return {
|
|
6582
6705
|
...data,
|
|
6583
|
-
options:
|
|
6584
|
-
|
|
6585
|
-
showTrend: true,
|
|
6586
|
-
showStackTraces: true,
|
|
6587
|
-
showFullEnvironment: true,
|
|
6588
|
-
showPassRate: true,
|
|
6589
|
-
showRetries: true,
|
|
6590
|
-
showFullFailures: true
|
|
6591
|
-
},
|
|
6706
|
+
options: deriveOptions(sections),
|
|
6707
|
+
sections,
|
|
6592
6708
|
tagGroups: this.buildTagGroups(data),
|
|
6593
6709
|
slowTests: this.buildSlowTests(data),
|
|
6594
6710
|
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
@@ -6596,47 +6712,20 @@ var TemplateEngine = class {
|
|
|
6596
6712
|
chartjsScript: this.buildChartjsScript()
|
|
6597
6713
|
};
|
|
6598
6714
|
}
|
|
6599
|
-
buildContext(data, template) {
|
|
6715
|
+
buildContext(data, template, userSections) {
|
|
6600
6716
|
const baseCSS = this.fontsBundleCss + "\n" + this.readStyle("base.css");
|
|
6601
6717
|
const templateCSS = this.readStyle(`${template}.css`);
|
|
6602
|
-
const
|
|
6603
|
-
|
|
6604
|
-
showCharts: false,
|
|
6605
|
-
showTrend: false,
|
|
6606
|
-
showStackTraces: true,
|
|
6607
|
-
showFullEnvironment: false,
|
|
6608
|
-
showPassRate: true,
|
|
6609
|
-
showRetries: true,
|
|
6610
|
-
showFullFailures: true
|
|
6611
|
-
},
|
|
6612
|
-
detailed: {
|
|
6613
|
-
showCharts: true,
|
|
6614
|
-
showTrend: true,
|
|
6615
|
-
showStackTraces: true,
|
|
6616
|
-
showFullEnvironment: true,
|
|
6617
|
-
showPassRate: true,
|
|
6618
|
-
showRetries: true,
|
|
6619
|
-
showFullFailures: true
|
|
6620
|
-
},
|
|
6621
|
-
executive: {
|
|
6622
|
-
showCharts: true,
|
|
6623
|
-
showTrend: true,
|
|
6624
|
-
showStackTraces: false,
|
|
6625
|
-
showFullEnvironment: false,
|
|
6626
|
-
showPassRate: true,
|
|
6627
|
-
showRetries: false,
|
|
6628
|
-
showFullFailures: false
|
|
6629
|
-
}
|
|
6630
|
-
};
|
|
6631
|
-
const chartjsScript = this.buildChartjsScript();
|
|
6718
|
+
const sections = resolveSections(template, userSections);
|
|
6719
|
+
const options = deriveOptions(sections);
|
|
6632
6720
|
return {
|
|
6633
6721
|
...data,
|
|
6634
|
-
options
|
|
6722
|
+
options,
|
|
6723
|
+
sections,
|
|
6635
6724
|
tagGroups: this.buildTagGroups(data),
|
|
6636
6725
|
slowTests: this.buildSlowTests(data),
|
|
6637
6726
|
baseCSS,
|
|
6638
6727
|
templateCSS,
|
|
6639
|
-
chartjsScript
|
|
6728
|
+
chartjsScript: this.buildChartjsScript()
|
|
6640
6729
|
};
|
|
6641
6730
|
}
|
|
6642
6731
|
buildChartjsScript() {
|
|
@@ -6684,9 +6773,12 @@ var TemplateEngine = class {
|
|
|
6684
6773
|
return tests.sort((a, b) => b.durationMs - a.durationMs).slice(0, 10);
|
|
6685
6774
|
}
|
|
6686
6775
|
readStyle(filename) {
|
|
6776
|
+
const cached = this.styleCache.get(filename);
|
|
6777
|
+
if (cached !== void 0) return cached;
|
|
6687
6778
|
const stylePath = import_path.default.join(this.templatesDir, "styles", filename);
|
|
6688
|
-
|
|
6689
|
-
|
|
6779
|
+
const css = import_fs.default.existsSync(stylePath) ? import_fs.default.readFileSync(stylePath, "utf8") : "";
|
|
6780
|
+
this.styleCache.set(filename, css);
|
|
6781
|
+
return css;
|
|
6690
6782
|
}
|
|
6691
6783
|
registerPartials() {
|
|
6692
6784
|
const partialsDir = import_path.default.join(this.templatesDir, "partials");
|
|
@@ -6747,7 +6839,7 @@ var TemplateEngine = class {
|
|
|
6747
6839
|
if (!Array.isArray(arr)) return "";
|
|
6748
6840
|
const values = arr.map((item) => {
|
|
6749
6841
|
const v = item[key];
|
|
6750
|
-
return typeof v === "string" ?
|
|
6842
|
+
return typeof v === "string" ? jsonForInlineScript(v) : String(v ?? 0);
|
|
6751
6843
|
});
|
|
6752
6844
|
return new import_handlebars.default.SafeString(values.join(","));
|
|
6753
6845
|
}
|
|
@@ -6758,7 +6850,7 @@ var TemplateEngine = class {
|
|
|
6758
6850
|
this.handlebars.registerHelper("absFixed", (n) => Math.abs(n).toFixed(0));
|
|
6759
6851
|
this.handlebars.registerHelper(
|
|
6760
6852
|
"safeJsonData",
|
|
6761
|
-
(value) => new import_handlebars.default.SafeString(
|
|
6853
|
+
(value) => new import_handlebars.default.SafeString(jsonForInlineScript(value))
|
|
6762
6854
|
);
|
|
6763
6855
|
this.handlebars.registerHelper(
|
|
6764
6856
|
"reverse",
|
|
@@ -7271,6 +7363,12 @@ var PdfGenerator = class {
|
|
|
7271
7363
|
`Compression: level=${compression.effective} jpeg-q=${compression.quality} max-w=${compression.maxWidth}px inline-failures\u2264${compression.maxInlineFailures}`
|
|
7272
7364
|
);
|
|
7273
7365
|
const outputPaths = [];
|
|
7366
|
+
if (options.sections !== void 0) {
|
|
7367
|
+
const hasCustom = sources.some((s) => s.kind !== "builtin");
|
|
7368
|
+
if (hasCustom) {
|
|
7369
|
+
logger.warn('[reportforge] "sections" applies to built-in templates only; it is ignored for custom templatePath templates.');
|
|
7370
|
+
}
|
|
7371
|
+
}
|
|
7274
7372
|
const { page } = await this.browserManager.launch(options.puppeteerExecutablePath);
|
|
7275
7373
|
try {
|
|
7276
7374
|
for (const source of sources) {
|
|
@@ -7378,13 +7476,13 @@ var PdfGenerator = class {
|
|
|
7378
7476
|
}
|
|
7379
7477
|
const templateLabel = source.kind === "builtin" ? source.id : source.label;
|
|
7380
7478
|
logger.info(`Rendering template: ${templateLabel}`);
|
|
7381
|
-
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id) : this.templateEngine.renderFromPath(renderData, source.absolutePath);
|
|
7479
|
+
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, options.sections) : this.templateEngine.renderFromPath(renderData, source.absolutePath);
|
|
7382
7480
|
const tempHtmlPath = await this.htmlWriter.write(html);
|
|
7383
7481
|
const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
|
|
7384
7482
|
try {
|
|
7385
7483
|
logger.info(`Navigating to temp HTML (${Math.round(html.length / 1024)} KB)...`);
|
|
7386
7484
|
await page.goto(fileUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
7387
|
-
const showCharts = source.kind === "builtin" ? source.id
|
|
7485
|
+
const showCharts = source.kind === "builtin" ? resolveSections(source.id, options.sections).charts : true;
|
|
7388
7486
|
await this.chartRenderer.waitForCharts(page, showCharts);
|
|
7389
7487
|
logger.info(`Generating PDF \u2192 ${import_path6.default.relative(process.cwd(), outputPath)}`);
|
|
7390
7488
|
await page.pdf({
|
|
@@ -11513,6 +11611,15 @@ var failureAnalysisConfigSchema = external_exports.object({
|
|
|
11513
11611
|
collectUnclassified: external_exports.boolean().default(true),
|
|
11514
11612
|
autoUpdateModel: external_exports.boolean().default(true)
|
|
11515
11613
|
}).default({});
|
|
11614
|
+
var sectionFlagsShape = Object.fromEntries(
|
|
11615
|
+
SECTION_KEYS.map((k) => [k, external_exports.boolean().optional()])
|
|
11616
|
+
);
|
|
11617
|
+
var sectionFlagsSchema = external_exports.object(sectionFlagsShape).strict();
|
|
11618
|
+
var sectionsSchema = sectionFlagsSchema.extend({
|
|
11619
|
+
minimal: sectionFlagsSchema.optional(),
|
|
11620
|
+
detailed: sectionFlagsSchema.optional(),
|
|
11621
|
+
executive: sectionFlagsSchema.optional()
|
|
11622
|
+
}).strict();
|
|
11516
11623
|
var ReporterOptionsSchema = external_exports.object({
|
|
11517
11624
|
outputFile: external_exports.string().optional().default(DEFAULT_OPTIONS.outputFile),
|
|
11518
11625
|
template: external_exports.union([
|
|
@@ -11550,7 +11657,8 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
11550
11657
|
).min(1, "templatePath array must not be empty")
|
|
11551
11658
|
]).optional().transform(
|
|
11552
11659
|
(v) => v === void 0 ? void 0 : Array.isArray(v) ? v : [v]
|
|
11553
|
-
)
|
|
11660
|
+
),
|
|
11661
|
+
sections: sectionsSchema.optional()
|
|
11554
11662
|
});
|
|
11555
11663
|
function parseOptions(raw) {
|
|
11556
11664
|
const result = ReporterOptionsSchema.safeParse(raw ?? {});
|
|
@@ -11684,11 +11792,33 @@ var STRENGTH = {
|
|
|
11684
11792
|
strongMinMargin: 0.25
|
|
11685
11793
|
};
|
|
11686
11794
|
var STRENGTH_LEVELS = ["weak", "moderate", "strong"];
|
|
11687
|
-
var
|
|
11795
|
+
var OVERRIDE_MARGIN = 999;
|
|
11688
11796
|
|
|
11689
11797
|
// src/analysis/FailureClusterer.ts
|
|
11690
11798
|
var MAX_TESTS_PER_CLUSTER = 20;
|
|
11691
11799
|
var MAX_MESSAGE_CHARS = 200;
|
|
11800
|
+
function errorHeader(message) {
|
|
11801
|
+
const beforeCallLog = message.split(/\n\s*Call log:/i)[0];
|
|
11802
|
+
return beforeCallLog.split("\n").filter((line) => !/^\s*[-+]?\s*(?:Expected|Received)\b/i.test(line)).join("\n");
|
|
11803
|
+
}
|
|
11804
|
+
function isAssertionTimeout(message) {
|
|
11805
|
+
return /Expect "[^"]+" with timeout/.test(message) && /resolved to [1-9]\d* element/.test(message);
|
|
11806
|
+
}
|
|
11807
|
+
function isNetwork(message) {
|
|
11808
|
+
return /net::ERR_(CONNECTION|NAME_NOT_RESOLVED|INTERNET_DISCONNECTED|SSL|TIMED_OUT|ADDRESS|NETWORK_CHANGED|EMPTY_RESPONSE)|\bECONN(?:REFUSED|RESET)\b|\bENOTFOUND\b|\bEAI_AGAIN\b|getaddrinfo|socket hang up|fetch failed/.test(errorHeader(message));
|
|
11809
|
+
}
|
|
11810
|
+
function isNavigation(message) {
|
|
11811
|
+
return /net::ERR_(?:ABORTED|TOO_MANY_REDIRECTS)|navigation interrupted|interrupted by (?:a|another) navigation|because of a navigation|no history entry|page crashed|Execution context was destroyed|frame was detached/.test(errorHeader(message));
|
|
11812
|
+
}
|
|
11813
|
+
function isLocatorNotFound(message) {
|
|
11814
|
+
return /strict mode violation|resolved to 0 element|element not found|not attached to the DOM|No node found for selector|no element matches|Unable to find an element/i.test(errorHeader(message));
|
|
11815
|
+
}
|
|
11816
|
+
var MESSAGE_RULES = [
|
|
11817
|
+
[isAssertionTimeout, "assertion"],
|
|
11818
|
+
[isNavigation, "navigation"],
|
|
11819
|
+
[isNetwork, "network"],
|
|
11820
|
+
[isLocatorNotFound, "locator-not-found"]
|
|
11821
|
+
];
|
|
11692
11822
|
function strengthOf(margin) {
|
|
11693
11823
|
if (margin >= STRENGTH.strongMinMargin) return "strong";
|
|
11694
11824
|
if (margin >= STRENGTH.moderateMinMargin) return "moderate";
|
|
@@ -11711,11 +11841,15 @@ function representativeMessage(message) {
|
|
|
11711
11841
|
return firstLine.length > MAX_MESSAGE_CHARS ? `${firstLine.slice(0, MAX_MESSAGE_CHARS)}\u2026` : firstLine;
|
|
11712
11842
|
}
|
|
11713
11843
|
function classifyOne(f, model) {
|
|
11844
|
+
const frame = topFrame(f.error.stack);
|
|
11714
11845
|
if (f.retryHistory.some((r) => r.status === "passed")) {
|
|
11715
|
-
return { category: "flaky-by-retry", margin:
|
|
11846
|
+
return { category: "flaky-by-retry", margin: OVERRIDE_MARGIN, failure: f, frame };
|
|
11847
|
+
}
|
|
11848
|
+
for (const [match, category] of MESSAGE_RULES) {
|
|
11849
|
+
if (match(f.error.message)) return { category, margin: OVERRIDE_MARGIN, failure: f, frame };
|
|
11716
11850
|
}
|
|
11717
11851
|
const { label, margin } = classify(model, classifyInput(f));
|
|
11718
|
-
return { category: label, margin, failure: f, frame
|
|
11852
|
+
return { category: label, margin, failure: f, frame };
|
|
11719
11853
|
}
|
|
11720
11854
|
function classifyAll(failures, model) {
|
|
11721
11855
|
return failures.map((f) => classifyOne(f, model));
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Canonical section catalog. 13 block toggles (section present/absent) followed
|
|
5
|
+
* by 5 display modifiers (tweak a section that is on). Single source of truth —
|
|
6
|
+
* the Zod schema in src/config/schema.ts builds its flag shape from this array.
|
|
7
|
+
*/
|
|
8
|
+
declare const SECTION_KEYS: readonly ["coverPage", "analysisOneliner", "releaseGate", "summary", "charts", "trend", "requirementsMatrix", "ciEnvironment", "suiteBreakdown", "failureDeepDive", "failureAnalysis", "slowTests", "defectLog", "passRate", "fullEnvironment", "retries", "fullFailures", "stackTraces"];
|
|
9
|
+
type SectionKey = (typeof SECTION_KEYS)[number];
|
|
10
|
+
/** A partial map of section flags — used both as the flat baseline and per-template. */
|
|
11
|
+
type SectionFlags = Partial<Record<SectionKey, boolean>>;
|
|
12
|
+
/** The public `sections` option shape: flat flags + optional per-template overrides. */
|
|
13
|
+
type SectionsOption = SectionFlags & {
|
|
14
|
+
minimal?: SectionFlags;
|
|
15
|
+
detailed?: SectionFlags;
|
|
16
|
+
executive?: SectionFlags;
|
|
17
|
+
};
|
|
18
|
+
|
|
3
19
|
declare const TEMPLATE_IDS: readonly ["minimal", "detailed", "executive"];
|
|
4
20
|
type TemplateId = (typeof TEMPLATE_IDS)[number];
|
|
5
21
|
/**
|
|
@@ -230,6 +246,30 @@ interface ReporterOptions {
|
|
|
230
246
|
* templatePath: ['./templates/exec.hbs', './templates/eng.hbs']
|
|
231
247
|
*/
|
|
232
248
|
templatePath?: string | string[];
|
|
249
|
+
/**
|
|
250
|
+
* Per-section override of which report sections appear, on top of the chosen
|
|
251
|
+
* template's defaults. Both-layered: flat keys are the baseline applied to
|
|
252
|
+
* every chosen template; per-template keys (`minimal` / `detailed` /
|
|
253
|
+
* `executive`) override the baseline for that template only.
|
|
254
|
+
*
|
|
255
|
+
* Block toggles: `coverPage`, `analysisOneliner`, `releaseGate`, `summary`,
|
|
256
|
+
* `charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`,
|
|
257
|
+
* `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`.
|
|
258
|
+
* Display modifiers: `passRate`, `fullEnvironment`, `retries`, `fullFailures`,
|
|
259
|
+
* `stackTraces`.
|
|
260
|
+
*
|
|
261
|
+
* `trend` renders only when `charts` is also on and history data exists (`showTrend` enabled). Sections gated on data
|
|
262
|
+
* (`failureAnalysis`, `requirementsMatrix`, `slowTests`, `defectLog`) appear
|
|
263
|
+
* only when matching data exists — this option never changes data collection.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* sections: {
|
|
267
|
+
* defectLog: false, // baseline for all chosen templates
|
|
268
|
+
* minimal: { charts: true }, // add charts to the minimal report
|
|
269
|
+
* detailed: { ciEnvironment: false },
|
|
270
|
+
* }
|
|
271
|
+
*/
|
|
272
|
+
sections?: SectionsOption;
|
|
233
273
|
}
|
|
234
274
|
|
|
235
275
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Canonical section catalog. 13 block toggles (section present/absent) followed
|
|
5
|
+
* by 5 display modifiers (tweak a section that is on). Single source of truth —
|
|
6
|
+
* the Zod schema in src/config/schema.ts builds its flag shape from this array.
|
|
7
|
+
*/
|
|
8
|
+
declare const SECTION_KEYS: readonly ["coverPage", "analysisOneliner", "releaseGate", "summary", "charts", "trend", "requirementsMatrix", "ciEnvironment", "suiteBreakdown", "failureDeepDive", "failureAnalysis", "slowTests", "defectLog", "passRate", "fullEnvironment", "retries", "fullFailures", "stackTraces"];
|
|
9
|
+
type SectionKey = (typeof SECTION_KEYS)[number];
|
|
10
|
+
/** A partial map of section flags — used both as the flat baseline and per-template. */
|
|
11
|
+
type SectionFlags = Partial<Record<SectionKey, boolean>>;
|
|
12
|
+
/** The public `sections` option shape: flat flags + optional per-template overrides. */
|
|
13
|
+
type SectionsOption = SectionFlags & {
|
|
14
|
+
minimal?: SectionFlags;
|
|
15
|
+
detailed?: SectionFlags;
|
|
16
|
+
executive?: SectionFlags;
|
|
17
|
+
};
|
|
18
|
+
|
|
3
19
|
declare const TEMPLATE_IDS: readonly ["minimal", "detailed", "executive"];
|
|
4
20
|
type TemplateId = (typeof TEMPLATE_IDS)[number];
|
|
5
21
|
/**
|
|
@@ -230,6 +246,30 @@ interface ReporterOptions {
|
|
|
230
246
|
* templatePath: ['./templates/exec.hbs', './templates/eng.hbs']
|
|
231
247
|
*/
|
|
232
248
|
templatePath?: string | string[];
|
|
249
|
+
/**
|
|
250
|
+
* Per-section override of which report sections appear, on top of the chosen
|
|
251
|
+
* template's defaults. Both-layered: flat keys are the baseline applied to
|
|
252
|
+
* every chosen template; per-template keys (`minimal` / `detailed` /
|
|
253
|
+
* `executive`) override the baseline for that template only.
|
|
254
|
+
*
|
|
255
|
+
* Block toggles: `coverPage`, `analysisOneliner`, `releaseGate`, `summary`,
|
|
256
|
+
* `charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`,
|
|
257
|
+
* `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`.
|
|
258
|
+
* Display modifiers: `passRate`, `fullEnvironment`, `retries`, `fullFailures`,
|
|
259
|
+
* `stackTraces`.
|
|
260
|
+
*
|
|
261
|
+
* `trend` renders only when `charts` is also on and history data exists (`showTrend` enabled). Sections gated on data
|
|
262
|
+
* (`failureAnalysis`, `requirementsMatrix`, `slowTests`, `defectLog`) appear
|
|
263
|
+
* only when matching data exists — this option never changes data collection.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* sections: {
|
|
267
|
+
* defectLog: false, // baseline for all chosen templates
|
|
268
|
+
* minimal: { charts: true }, // add charts to the minimal report
|
|
269
|
+
* detailed: { ciEnvironment: false },
|
|
270
|
+
* }
|
|
271
|
+
*/
|
|
272
|
+
sections?: SectionsOption;
|
|
233
273
|
}
|
|
234
274
|
|
|
235
275
|
/**
|