@reportforge/playwright-pdf 0.6.3 → 0.7.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/README.md +12 -10
- package/dist/cli/index.js +88 -33
- package/dist/index.d.mts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +67 -13
- package/dist/index.mjs +67 -13
- package/dist/templates/layouts/detailed.hbs +1 -7
- package/dist/templates/layouts/executive.hbs +1 -7
- package/dist/templates/layouts/minimal.hbs +1 -7
- package/dist/templates/partials/failure-card.hbs +27 -0
- package/dist/templates/partials/suite-breakdown.hbs +30 -4
- package/dist/templates/styles/base.css +87 -26
- package/dist/templates/styles/detailed.css +1 -44
- package/dist/templates/styles/executive.css +2 -32
- package/dist/templates/styles/minimal.css +1 -46
- package/package.json +1 -1
- package/dist/templates/partials/failure-analysis.hbs +0 -42
- package/dist/templates/partials/failure-deep-dive.hbs +0 -62
- package/dist/templates/partials/slow-tests.hbs +0 -27
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ Three templates, one reporter. Every PDF is fully offline — fonts, charts, and
|
|
|
46
46
|
## Features
|
|
47
47
|
|
|
48
48
|
- **3 templates** — `minimal` (developer), `detailed` (QA team), `executive` (stakeholders)
|
|
49
|
-
- **Detailed reports** — KPI dashboard, suite breakdown
|
|
49
|
+
- **Detailed reports** — KPI dashboard, suite breakdown with inline failure detail (error, screenshot, root-cause chip) under each failed test, CI/CD environment
|
|
50
50
|
- **Embedded Chart.js** — pass-rate doughnut + per-suite bar chart, bundled inline (no CDN)
|
|
51
51
|
- **Self-contained PDFs** — screenshots, fonts, logos all embedded; share or archive with confidence
|
|
52
52
|
- **Custom branding** — logo, primary/accent colours, watermark, PDF password encryption
|
|
@@ -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** — sorts
|
|
59
|
+
- **Offline failure analysis** — sorts each failure into one of 7 root-cause buckets and renders a per-test root-cause chip inline under the failed test in the `detailed` PDF (dominant-cause 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
|
- `npx @reportforge/playwright-pdf export-feedback` — exports locally-collected unrecognized failures (tokenized, local-only) for optional manual sharing.
|
|
61
61
|
- **Configurable sections** — add or remove any report section per template via `sections`.
|
|
62
62
|
- **Hybrid licensing** — one short key unlocks it; reports keep rendering offline via a cached JWT between refreshes
|
|
@@ -198,6 +198,7 @@ All options are the second element of the reporter tuple.
|
|
|
198
198
|
| `historySize` | `number` | `10` | Maximum number of test runs to keep in the history file (integer ≥ 2). Older runs are pruned on append. |
|
|
199
199
|
| `showTrend` | `boolean` | `true` | Show the pass-rate sparkline and delta badge in the `detailed` template. Set to `false` to disable history tracking entirely. |
|
|
200
200
|
| `flakinessTopN` | `number` | `5` | Maximum flaky tests to show in the flakiness table (`detailed` template). Set to `0` to disable the table entirely. |
|
|
201
|
+
| `slowTestThreshold` | `number` | `10` | Minimum timed-test count before `SLOW` badges appear in the suite breakdown. The badge marks the 10 slowest tests; below this many it stays hidden (every test would be trivially "slowest"). Set to `0` to always badge the slowest. |
|
|
201
202
|
| `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
203
|
| `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. |
|
|
203
204
|
<!-- AUTOGEN:options-table END -->
|
|
@@ -391,10 +392,10 @@ reporter: [['@reportforge/playwright-pdf', {
|
|
|
391
392
|
| `trend` | Pass-rate trend line, run-history table, and flakiness table (rendered inside `charts`) |
|
|
392
393
|
| `requirementsMatrix` | Tests grouped by `@tag('REQ-…')` with coverage bars (requirements traceability) |
|
|
393
394
|
| `ciEnvironment` | Branch, commit, browsers, CI provider, OS, Node, Playwright, projects, workers |
|
|
394
|
-
| `suiteBreakdown` | Per-suite → per-test table (status, duration, tags, retries) |
|
|
395
|
-
| `failureDeepDive` |
|
|
396
|
-
| `failureAnalysis` |
|
|
397
|
-
| `slowTests` |
|
|
395
|
+
| `suiteBreakdown` | Per-suite → per-test table (status, duration, tags, retries). Failed tests carry their failure detail, root-cause chip, and a `SLOW` badge inline (the three keys below) |
|
|
396
|
+
| `failureDeepDive` | Inline failure detail under each failed test in the breakdown: error message, stack trace, screenshot, retry history |
|
|
397
|
+
| `failureAnalysis` | Inline per-test root-cause chip under each failed test (category + representative error + match strength, from the offline classifier) |
|
|
398
|
+
| `slowTests` | `SLOW` badge on the duration-ranked slowest tests, shown inline in the breakdown |
|
|
398
399
|
| `defectLog` | `DEF-####` numbered failure table (severity, retries) |
|
|
399
400
|
|
|
400
401
|
#### Display modifiers — tune a section that's on
|
|
@@ -404,8 +405,8 @@ reporter: [['@reportforge/playwright-pdf', {
|
|
|
404
405
|
| `passRate` | Show the pass-rate % on the Passed KPI card (`summary`) |
|
|
405
406
|
| `fullEnvironment` | Full environment table instead of the compact grid (`ciEnvironment`) |
|
|
406
407
|
| `retries` | Retries column in the suite breakdown (`suiteBreakdown`) |
|
|
407
|
-
| `fullFailures` |
|
|
408
|
-
| `stackTraces` | Include `<pre>` stack traces in the failure
|
|
408
|
+
| `fullFailures` | Include the screenshot in the inline failure detail (compact templates omit it) (`failureDeepDive`) |
|
|
409
|
+
| `stackTraces` | Include `<pre>` stack traces in the inline failure detail (`failureDeepDive`) |
|
|
409
410
|
|
|
410
411
|
#### Per-template defaults
|
|
411
412
|
|
|
@@ -421,7 +422,7 @@ Omit `sections` entirely and each template renders exactly this (✓ = on):
|
|
|
421
422
|
| `trend` | – | ✓ | ✓ |
|
|
422
423
|
| `requirementsMatrix` | – | ✓ | – |
|
|
423
424
|
| `ciEnvironment` | ✓ | ✓ | ✓ |
|
|
424
|
-
| `suiteBreakdown` | ✓ | ✓ |
|
|
425
|
+
| `suiteBreakdown` | ✓ | ✓ | ✓ |
|
|
425
426
|
| `failureDeepDive` | ✓ | ✓ | ✓ |
|
|
426
427
|
| `failureAnalysis` | – | ✓ | – |
|
|
427
428
|
| `slowTests` | – | ✓ | ✓ |
|
|
@@ -435,7 +436,8 @@ Omit `sections` entirely and each template renders exactly this (✓ = on):
|
|
|
435
436
|
#### Dependencies & gotchas
|
|
436
437
|
|
|
437
438
|
- **`trend` needs `charts`.** The trend line, run-history, and flakiness table live inside the charts block, so `trend: true` does nothing with `charts: false` (it's coerced off). It also needs history data — keep the top-level `showTrend` option on.
|
|
438
|
-
- **
|
|
439
|
+
- **Inline failure detail needs `suiteBreakdown`.** `failureDeepDive` (failure detail), `failureAnalysis` (root-cause chip), and `slowTests` (`SLOW` badge) now render *inside* the suite breakdown, so they show nothing when `suiteBreakdown` is off. The chip (`failureAnalysis`) also needs `failureDeepDive` — it lives inside the failure detail, so it's coerced off without it.
|
|
440
|
+
- **Data-gated sections** — `failureAnalysis`, `requirementsMatrix`, `slowTests`, `defectLog` render only when there's matching data (classified failures, tagged tests, a meaningful slow set, failures). Toggling them on with no data shows nothing.
|
|
439
441
|
- **Render-layer only.** `sections` controls what's drawn, never what's collected — the data switches stay separate: `showTrend` (history), `flakinessTopN` (flaky-table size), `failureAnalysis.enabled` (classifier), `includeScreenshots` (images).
|
|
440
442
|
- **Custom templates.** `sections` applies to the built-in templates; a custom `templatePath` controls its own layout (every section available) and ignores `sections` — you'll get a warning if both are set.
|
|
441
443
|
|
package/dist/cli/index.js
CHANGED
|
@@ -6243,6 +6243,29 @@ var require_lib = __commonJS({
|
|
|
6243
6243
|
}
|
|
6244
6244
|
});
|
|
6245
6245
|
|
|
6246
|
+
// src/config/defaults.ts
|
|
6247
|
+
var DEFAULT_OPTIONS;
|
|
6248
|
+
var init_defaults = __esm({
|
|
6249
|
+
"src/config/defaults.ts"() {
|
|
6250
|
+
"use strict";
|
|
6251
|
+
init_cjs_shims();
|
|
6252
|
+
DEFAULT_OPTIONS = {
|
|
6253
|
+
outputFile: "playwright-report/{date}-report.pdf",
|
|
6254
|
+
template: "minimal",
|
|
6255
|
+
primaryColor: "#CC785C",
|
|
6256
|
+
accentColor: "#3F7D58",
|
|
6257
|
+
reportTitle: "Playwright Test Report",
|
|
6258
|
+
open: false,
|
|
6259
|
+
compressionLevel: "auto",
|
|
6260
|
+
maxFileSizeMb: 8,
|
|
6261
|
+
historySize: 10,
|
|
6262
|
+
showTrend: true,
|
|
6263
|
+
flakinessTopN: 5,
|
|
6264
|
+
slowTestThreshold: 10
|
|
6265
|
+
};
|
|
6266
|
+
}
|
|
6267
|
+
});
|
|
6268
|
+
|
|
6246
6269
|
// src/utils/duration.ts
|
|
6247
6270
|
function formatDuration(ms) {
|
|
6248
6271
|
if (ms < 1e3) return `${ms}ms`;
|
|
@@ -6316,6 +6339,7 @@ function resolveSections(template, userSections) {
|
|
|
6316
6339
|
}
|
|
6317
6340
|
const result = { ...base, ...flat, ...perTemplate };
|
|
6318
6341
|
if (!result.charts) result.trend = false;
|
|
6342
|
+
if (!result.failureDeepDive) result.failureAnalysis = false;
|
|
6319
6343
|
return result;
|
|
6320
6344
|
}
|
|
6321
6345
|
function allSectionsOn() {
|
|
@@ -6398,7 +6422,10 @@ var init_sections = __esm({
|
|
|
6398
6422
|
trend: true,
|
|
6399
6423
|
requirementsMatrix: false,
|
|
6400
6424
|
ciEnvironment: true,
|
|
6401
|
-
|
|
6425
|
+
// Suite breakdown now hosts the inline failure detail + slow badge, so the
|
|
6426
|
+
// executive layout renders it too (compact: fullFailures stays false → no
|
|
6427
|
+
// screenshots; failureAnalysis stays off → no per-test chip).
|
|
6428
|
+
suiteBreakdown: true,
|
|
6402
6429
|
failureDeepDive: true,
|
|
6403
6430
|
failureAnalysis: false,
|
|
6404
6431
|
slowTests: true,
|
|
@@ -6454,6 +6481,7 @@ var init_engine = __esm({
|
|
|
6454
6481
|
import_handlebars = __toESM(require_lib());
|
|
6455
6482
|
import_fs = __toESM(require("fs"));
|
|
6456
6483
|
import_path = __toESM(require("path"));
|
|
6484
|
+
init_defaults();
|
|
6457
6485
|
init_duration();
|
|
6458
6486
|
init_SummaryWriter();
|
|
6459
6487
|
init_sections();
|
|
@@ -6482,17 +6510,17 @@ var init_engine = __esm({
|
|
|
6482
6510
|
setChartjsBundle(inline) {
|
|
6483
6511
|
this.chartjsInline = inline;
|
|
6484
6512
|
}
|
|
6485
|
-
render(data, template, userSections) {
|
|
6513
|
+
render(data, template, userSections, slowTestThreshold) {
|
|
6486
6514
|
const layoutPath = import_path.default.join(this.templatesDir, "layouts", `${template}.hbs`);
|
|
6487
6515
|
if (!import_fs.default.existsSync(layoutPath)) {
|
|
6488
6516
|
throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
|
|
6489
6517
|
}
|
|
6490
6518
|
const layoutSource = import_fs.default.readFileSync(layoutPath, "utf8");
|
|
6491
6519
|
const compiledLayout = this.handlebars.compile(layoutSource);
|
|
6492
|
-
const ctx = this.buildContext(data, template, userSections);
|
|
6520
|
+
const ctx = this.buildContext(data, template, userSections, slowTestThreshold);
|
|
6493
6521
|
return compiledLayout(ctx);
|
|
6494
6522
|
}
|
|
6495
|
-
renderFromPath(data, filePath) {
|
|
6523
|
+
renderFromPath(data, filePath, slowTestThreshold) {
|
|
6496
6524
|
if (!this.customTemplateCache.has(filePath)) {
|
|
6497
6525
|
if (!import_fs.default.existsSync(filePath)) {
|
|
6498
6526
|
throw new Error(`[reportforge] Custom template not found: ${filePath}`);
|
|
@@ -6501,32 +6529,41 @@ var init_engine = __esm({
|
|
|
6501
6529
|
this.customTemplateCache.set(filePath, this.handlebars.compile(source));
|
|
6502
6530
|
}
|
|
6503
6531
|
const compiledFn = this.customTemplateCache.get(filePath);
|
|
6504
|
-
return compiledFn(this.buildCustomContext(data));
|
|
6532
|
+
return compiledFn(this.buildCustomContext(data, slowTestThreshold));
|
|
6505
6533
|
}
|
|
6506
|
-
buildCustomContext(data) {
|
|
6534
|
+
buildCustomContext(data, slowTestThreshold) {
|
|
6507
6535
|
const sections = allSectionsOn();
|
|
6536
|
+
const slow = this.buildSlowTests(data);
|
|
6508
6537
|
return {
|
|
6509
6538
|
...data,
|
|
6539
|
+
projects: this.enrichProjects(data, slow, sections, slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold),
|
|
6510
6540
|
options: deriveOptions(sections),
|
|
6511
6541
|
sections,
|
|
6512
6542
|
tagGroups: this.buildTagGroups(data),
|
|
6513
|
-
slowTests:
|
|
6543
|
+
slowTests: slow.ranked,
|
|
6514
6544
|
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
6515
6545
|
templateCSS: "",
|
|
6516
6546
|
chartjsScript: this.buildChartjsScript()
|
|
6517
6547
|
};
|
|
6518
6548
|
}
|
|
6519
|
-
buildContext(data, template, userSections) {
|
|
6549
|
+
buildContext(data, template, userSections, slowTestThreshold) {
|
|
6520
6550
|
const baseCSS = this.fontsBundleCss + "\n" + this.readStyle("base.css");
|
|
6521
6551
|
const templateCSS = this.readStyle(`${template}.css`);
|
|
6522
6552
|
const sections = resolveSections(template, userSections);
|
|
6523
6553
|
const options = deriveOptions(sections);
|
|
6554
|
+
const slow = this.buildSlowTests(data);
|
|
6524
6555
|
return {
|
|
6525
6556
|
...data,
|
|
6557
|
+
projects: this.enrichProjects(
|
|
6558
|
+
data,
|
|
6559
|
+
slow,
|
|
6560
|
+
sections,
|
|
6561
|
+
slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold
|
|
6562
|
+
),
|
|
6526
6563
|
options,
|
|
6527
6564
|
sections,
|
|
6528
6565
|
tagGroups: this.buildTagGroups(data),
|
|
6529
|
-
slowTests:
|
|
6566
|
+
slowTests: slow.ranked,
|
|
6530
6567
|
baseCSS,
|
|
6531
6568
|
templateCSS,
|
|
6532
6569
|
chartjsScript: this.buildChartjsScript()
|
|
@@ -6563,6 +6600,7 @@ var init_engine = __esm({
|
|
|
6563
6600
|
for (const test of suite.tests) {
|
|
6564
6601
|
if (test.duration > 0) {
|
|
6565
6602
|
tests.push({
|
|
6603
|
+
id: test.id,
|
|
6566
6604
|
title: test.title,
|
|
6567
6605
|
suiteTitle: suite.title,
|
|
6568
6606
|
durationMs: test.duration,
|
|
@@ -6574,7 +6612,36 @@ var init_engine = __esm({
|
|
|
6574
6612
|
}
|
|
6575
6613
|
}
|
|
6576
6614
|
}
|
|
6577
|
-
|
|
6615
|
+
const ranked = tests.sort((a, b) => b.durationMs - a.durationMs).slice(0, 10);
|
|
6616
|
+
return { ranked, timedTotal: tests.length };
|
|
6617
|
+
}
|
|
6618
|
+
/**
|
|
6619
|
+
* Project the suite tree into its render form: attach each test's failure detail
|
|
6620
|
+
* (by testId), root-cause chip, and slow rank. A pure projection — the source
|
|
6621
|
+
* ReportData is never mutated. Gating lives here (not the template): a field is
|
|
6622
|
+
* attached only when its section is on, so the template just checks presence.
|
|
6623
|
+
* `failuresById` is built from the (possibly overflow-trimmed) `failures` array,
|
|
6624
|
+
* so a trimmed failure yields no inline card; the overflow note points to the sidecar.
|
|
6625
|
+
*/
|
|
6626
|
+
enrichProjects(data, slow, sections, slowTestThreshold) {
|
|
6627
|
+
const failuresById = sections.failureDeepDive ? new Map(data.failures.map((f) => [f.testId, f])) : /* @__PURE__ */ new Map();
|
|
6628
|
+
const chipById = sections.failureAnalysis ? new Map((data.classifications ?? []).map((c) => [c.testId, c])) : /* @__PURE__ */ new Map();
|
|
6629
|
+
const slowRankById = /* @__PURE__ */ new Map();
|
|
6630
|
+
if (sections.slowTests && slow.timedTotal > slowTestThreshold) {
|
|
6631
|
+
slow.ranked.forEach((t, i) => slowRankById.set(t.id, i + 1));
|
|
6632
|
+
}
|
|
6633
|
+
const enrichTest = (t) => ({
|
|
6634
|
+
...t,
|
|
6635
|
+
failure: failuresById.get(t.id),
|
|
6636
|
+
chip: chipById.get(t.id),
|
|
6637
|
+
slowRank: slowRankById.get(t.id)
|
|
6638
|
+
});
|
|
6639
|
+
const enrichSuite = (s) => ({
|
|
6640
|
+
...s,
|
|
6641
|
+
suites: s.suites.map(enrichSuite),
|
|
6642
|
+
tests: s.tests.map(enrichTest)
|
|
6643
|
+
});
|
|
6644
|
+
return data.projects.map((p) => ({ ...p, suites: p.suites.map(enrichSuite) }));
|
|
6578
6645
|
}
|
|
6579
6646
|
readStyle(filename) {
|
|
6580
6647
|
const cached = this.styleCache.get(filename);
|
|
@@ -7598,7 +7665,7 @@ var init_PdfGenerator = __esm({
|
|
|
7598
7665
|
}
|
|
7599
7666
|
const templateLabel = source.kind === "builtin" ? source.id : source.label;
|
|
7600
7667
|
logger.info(`Rendering template: ${templateLabel}`);
|
|
7601
|
-
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, options.sections) : this.templateEngine.renderFromPath(renderData, source.absolutePath);
|
|
7668
|
+
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, options.sections, options.slowTestThreshold) : this.templateEngine.renderFromPath(renderData, source.absolutePath, options.slowTestThreshold);
|
|
7602
7669
|
const tempHtmlPath = await this.htmlWriter.write(html);
|
|
7603
7670
|
const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
|
|
7604
7671
|
try {
|
|
@@ -11747,28 +11814,6 @@ var init_zod = __esm({
|
|
|
11747
11814
|
}
|
|
11748
11815
|
});
|
|
11749
11816
|
|
|
11750
|
-
// src/config/defaults.ts
|
|
11751
|
-
var DEFAULT_OPTIONS;
|
|
11752
|
-
var init_defaults = __esm({
|
|
11753
|
-
"src/config/defaults.ts"() {
|
|
11754
|
-
"use strict";
|
|
11755
|
-
init_cjs_shims();
|
|
11756
|
-
DEFAULT_OPTIONS = {
|
|
11757
|
-
outputFile: "playwright-report/{date}-report.pdf",
|
|
11758
|
-
template: "minimal",
|
|
11759
|
-
primaryColor: "#CC785C",
|
|
11760
|
-
accentColor: "#3F7D58",
|
|
11761
|
-
reportTitle: "Playwright Test Report",
|
|
11762
|
-
open: false,
|
|
11763
|
-
compressionLevel: "auto",
|
|
11764
|
-
maxFileSizeMb: 8,
|
|
11765
|
-
historySize: 10,
|
|
11766
|
-
showTrend: true,
|
|
11767
|
-
flakinessTopN: 5
|
|
11768
|
-
};
|
|
11769
|
-
}
|
|
11770
|
-
});
|
|
11771
|
-
|
|
11772
11817
|
// src/config/schema.ts
|
|
11773
11818
|
function parseOptions(raw) {
|
|
11774
11819
|
const result = ReporterOptionsSchema.safeParse(raw ?? {});
|
|
@@ -11856,6 +11901,7 @@ var init_schema = __esm({
|
|
|
11856
11901
|
historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
|
|
11857
11902
|
showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
|
|
11858
11903
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
11904
|
+
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
11859
11905
|
failureAnalysis: failureAnalysisConfigSchema,
|
|
11860
11906
|
templatePath: external_exports.union([
|
|
11861
11907
|
external_exports.string().regex(/\.hbs$/, "templatePath must end with .hbs").refine((v) => v.slice(0, -4).trim().length > 0, "templatePath basename must not be empty"),
|
|
@@ -12074,6 +12120,14 @@ function classifyOne(f, model) {
|
|
|
12074
12120
|
function classifyAll(failures, model) {
|
|
12075
12121
|
return failures.map((f) => classifyOne(f, model));
|
|
12076
12122
|
}
|
|
12123
|
+
function perTestClassifications(classified) {
|
|
12124
|
+
return classified.map((c) => ({
|
|
12125
|
+
testId: c.failure.testId,
|
|
12126
|
+
category: c.category,
|
|
12127
|
+
message: representativeMessage(c.failure.error.message),
|
|
12128
|
+
strength: strengthOf(c.margin)
|
|
12129
|
+
}));
|
|
12130
|
+
}
|
|
12077
12131
|
function clusterClassified(classified) {
|
|
12078
12132
|
const groups = /* @__PURE__ */ new Map();
|
|
12079
12133
|
for (const c of classified) {
|
|
@@ -12246,6 +12300,7 @@ function runFailureAnalysis(reportData, options, cwd) {
|
|
|
12246
12300
|
const analysed = reportData.failures.slice(0, cap);
|
|
12247
12301
|
const model = loadModel(fa.autoUpdateModel);
|
|
12248
12302
|
const classified = classifyAll(analysed, model);
|
|
12303
|
+
reportData.classifications = perTestClassifications(classified);
|
|
12249
12304
|
if (templatesConsumeAnalysis(options)) {
|
|
12250
12305
|
reportData.analysis = analyseClassified(classified, reportData.failures.length, fa);
|
|
12251
12306
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -234,6 +234,14 @@ interface ReporterOptions {
|
|
|
234
234
|
* @default 5
|
|
235
235
|
*/
|
|
236
236
|
flakinessTopN?: number;
|
|
237
|
+
/**
|
|
238
|
+
* Minimum number of timed tests before `SLOW` badges appear in the suite
|
|
239
|
+
* breakdown. The badge marks the 10 slowest tests; below this many timed
|
|
240
|
+
* tests every test would trivially be the "slowest", so badges stay hidden.
|
|
241
|
+
* Set to 0 to always badge the slowest. Must be a non-negative integer.
|
|
242
|
+
* @default 10
|
|
243
|
+
*/
|
|
244
|
+
slowTestThreshold?: number;
|
|
237
245
|
/**
|
|
238
246
|
* Path to a custom Handlebars (.hbs) template file. When set, takes
|
|
239
247
|
* precedence over `template`. Relative paths resolve from `process.cwd()`.
|
package/dist/index.d.ts
CHANGED
|
@@ -234,6 +234,14 @@ interface ReporterOptions {
|
|
|
234
234
|
* @default 5
|
|
235
235
|
*/
|
|
236
236
|
flakinessTopN?: number;
|
|
237
|
+
/**
|
|
238
|
+
* Minimum number of timed tests before `SLOW` badges appear in the suite
|
|
239
|
+
* breakdown. The badge marks the 10 slowest tests; below this many timed
|
|
240
|
+
* tests every test would trivially be the "slowest", so badges stay hidden.
|
|
241
|
+
* Set to 0 to always badge the slowest. Must be a non-negative integer.
|
|
242
|
+
* @default 10
|
|
243
|
+
*/
|
|
244
|
+
slowTestThreshold?: number;
|
|
237
245
|
/**
|
|
238
246
|
* Path to a custom Handlebars (.hbs) template file. When set, takes
|
|
239
247
|
* precedence over `template`. Relative paths resolve from `process.cwd()`.
|
package/dist/index.js
CHANGED
|
@@ -10329,7 +10329,8 @@ var DEFAULT_OPTIONS = {
|
|
|
10329
10329
|
maxFileSizeMb: 8,
|
|
10330
10330
|
historySize: 10,
|
|
10331
10331
|
showTrend: true,
|
|
10332
|
-
flakinessTopN: 5
|
|
10332
|
+
flakinessTopN: 5,
|
|
10333
|
+
slowTestThreshold: 10
|
|
10333
10334
|
};
|
|
10334
10335
|
|
|
10335
10336
|
// src/templates/sections.ts
|
|
@@ -10406,7 +10407,10 @@ var SECTION_DEFAULTS = {
|
|
|
10406
10407
|
trend: true,
|
|
10407
10408
|
requirementsMatrix: false,
|
|
10408
10409
|
ciEnvironment: true,
|
|
10409
|
-
|
|
10410
|
+
// Suite breakdown now hosts the inline failure detail + slow badge, so the
|
|
10411
|
+
// executive layout renders it too (compact: fullFailures stays false → no
|
|
10412
|
+
// screenshots; failureAnalysis stays off → no per-test chip).
|
|
10413
|
+
suiteBreakdown: true,
|
|
10410
10414
|
failureDeepDive: true,
|
|
10411
10415
|
failureAnalysis: false,
|
|
10412
10416
|
slowTests: true,
|
|
@@ -10433,6 +10437,7 @@ function resolveSections(template, userSections) {
|
|
|
10433
10437
|
}
|
|
10434
10438
|
const result = { ...base, ...flat, ...perTemplate };
|
|
10435
10439
|
if (!result.charts) result.trend = false;
|
|
10440
|
+
if (!result.failureDeepDive) result.failureAnalysis = false;
|
|
10436
10441
|
return result;
|
|
10437
10442
|
}
|
|
10438
10443
|
function allSectionsOn() {
|
|
@@ -10509,6 +10514,7 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10509
10514
|
historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
|
|
10510
10515
|
showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
|
|
10511
10516
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
10517
|
+
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10512
10518
|
failureAnalysis: failureAnalysisConfigSchema,
|
|
10513
10519
|
templatePath: external_exports.union([
|
|
10514
10520
|
external_exports.string().regex(/\.hbs$/, "templatePath must end with .hbs").refine((v) => v.slice(0, -4).trim().length > 0, "templatePath basename must not be empty"),
|
|
@@ -11172,6 +11178,14 @@ function classifyOne(f, model) {
|
|
|
11172
11178
|
function classifyAll(failures, model) {
|
|
11173
11179
|
return failures.map((f) => classifyOne(f, model));
|
|
11174
11180
|
}
|
|
11181
|
+
function perTestClassifications(classified) {
|
|
11182
|
+
return classified.map((c) => ({
|
|
11183
|
+
testId: c.failure.testId,
|
|
11184
|
+
category: c.category,
|
|
11185
|
+
message: representativeMessage(c.failure.error.message),
|
|
11186
|
+
strength: strengthOf(c.margin)
|
|
11187
|
+
}));
|
|
11188
|
+
}
|
|
11175
11189
|
function clusterClassified(classified) {
|
|
11176
11190
|
const groups = /* @__PURE__ */ new Map();
|
|
11177
11191
|
for (const c of classified) {
|
|
@@ -11347,6 +11361,7 @@ function runFailureAnalysis(reportData, options, cwd) {
|
|
|
11347
11361
|
const analysed = reportData.failures.slice(0, cap);
|
|
11348
11362
|
const model = loadModel(fa.autoUpdateModel);
|
|
11349
11363
|
const classified = classifyAll(analysed, model);
|
|
11364
|
+
reportData.classifications = perTestClassifications(classified);
|
|
11350
11365
|
if (templatesConsumeAnalysis(options)) {
|
|
11351
11366
|
reportData.analysis = analyseClassified(classified, reportData.failures.length, fa);
|
|
11352
11367
|
}
|
|
@@ -12069,17 +12084,17 @@ var TemplateEngine = class {
|
|
|
12069
12084
|
setChartjsBundle(inline) {
|
|
12070
12085
|
this.chartjsInline = inline;
|
|
12071
12086
|
}
|
|
12072
|
-
render(data, template, userSections) {
|
|
12087
|
+
render(data, template, userSections, slowTestThreshold) {
|
|
12073
12088
|
const layoutPath = import_path3.default.join(this.templatesDir, "layouts", `${template}.hbs`);
|
|
12074
12089
|
if (!import_fs4.default.existsSync(layoutPath)) {
|
|
12075
12090
|
throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
|
|
12076
12091
|
}
|
|
12077
12092
|
const layoutSource = import_fs4.default.readFileSync(layoutPath, "utf8");
|
|
12078
12093
|
const compiledLayout = this.handlebars.compile(layoutSource);
|
|
12079
|
-
const ctx = this.buildContext(data, template, userSections);
|
|
12094
|
+
const ctx = this.buildContext(data, template, userSections, slowTestThreshold);
|
|
12080
12095
|
return compiledLayout(ctx);
|
|
12081
12096
|
}
|
|
12082
|
-
renderFromPath(data, filePath) {
|
|
12097
|
+
renderFromPath(data, filePath, slowTestThreshold) {
|
|
12083
12098
|
if (!this.customTemplateCache.has(filePath)) {
|
|
12084
12099
|
if (!import_fs4.default.existsSync(filePath)) {
|
|
12085
12100
|
throw new Error(`[reportforge] Custom template not found: ${filePath}`);
|
|
@@ -12088,32 +12103,41 @@ var TemplateEngine = class {
|
|
|
12088
12103
|
this.customTemplateCache.set(filePath, this.handlebars.compile(source));
|
|
12089
12104
|
}
|
|
12090
12105
|
const compiledFn = this.customTemplateCache.get(filePath);
|
|
12091
|
-
return compiledFn(this.buildCustomContext(data));
|
|
12106
|
+
return compiledFn(this.buildCustomContext(data, slowTestThreshold));
|
|
12092
12107
|
}
|
|
12093
|
-
buildCustomContext(data) {
|
|
12108
|
+
buildCustomContext(data, slowTestThreshold) {
|
|
12094
12109
|
const sections = allSectionsOn();
|
|
12110
|
+
const slow = this.buildSlowTests(data);
|
|
12095
12111
|
return {
|
|
12096
12112
|
...data,
|
|
12113
|
+
projects: this.enrichProjects(data, slow, sections, slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold),
|
|
12097
12114
|
options: deriveOptions(sections),
|
|
12098
12115
|
sections,
|
|
12099
12116
|
tagGroups: this.buildTagGroups(data),
|
|
12100
|
-
slowTests:
|
|
12117
|
+
slowTests: slow.ranked,
|
|
12101
12118
|
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
12102
12119
|
templateCSS: "",
|
|
12103
12120
|
chartjsScript: this.buildChartjsScript()
|
|
12104
12121
|
};
|
|
12105
12122
|
}
|
|
12106
|
-
buildContext(data, template, userSections) {
|
|
12123
|
+
buildContext(data, template, userSections, slowTestThreshold) {
|
|
12107
12124
|
const baseCSS = this.fontsBundleCss + "\n" + this.readStyle("base.css");
|
|
12108
12125
|
const templateCSS = this.readStyle(`${template}.css`);
|
|
12109
12126
|
const sections = resolveSections(template, userSections);
|
|
12110
12127
|
const options = deriveOptions(sections);
|
|
12128
|
+
const slow = this.buildSlowTests(data);
|
|
12111
12129
|
return {
|
|
12112
12130
|
...data,
|
|
12131
|
+
projects: this.enrichProjects(
|
|
12132
|
+
data,
|
|
12133
|
+
slow,
|
|
12134
|
+
sections,
|
|
12135
|
+
slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold
|
|
12136
|
+
),
|
|
12113
12137
|
options,
|
|
12114
12138
|
sections,
|
|
12115
12139
|
tagGroups: this.buildTagGroups(data),
|
|
12116
|
-
slowTests:
|
|
12140
|
+
slowTests: slow.ranked,
|
|
12117
12141
|
baseCSS,
|
|
12118
12142
|
templateCSS,
|
|
12119
12143
|
chartjsScript: this.buildChartjsScript()
|
|
@@ -12150,6 +12174,7 @@ var TemplateEngine = class {
|
|
|
12150
12174
|
for (const test of suite.tests) {
|
|
12151
12175
|
if (test.duration > 0) {
|
|
12152
12176
|
tests.push({
|
|
12177
|
+
id: test.id,
|
|
12153
12178
|
title: test.title,
|
|
12154
12179
|
suiteTitle: suite.title,
|
|
12155
12180
|
durationMs: test.duration,
|
|
@@ -12161,7 +12186,36 @@ var TemplateEngine = class {
|
|
|
12161
12186
|
}
|
|
12162
12187
|
}
|
|
12163
12188
|
}
|
|
12164
|
-
|
|
12189
|
+
const ranked = tests.sort((a, b) => b.durationMs - a.durationMs).slice(0, 10);
|
|
12190
|
+
return { ranked, timedTotal: tests.length };
|
|
12191
|
+
}
|
|
12192
|
+
/**
|
|
12193
|
+
* Project the suite tree into its render form: attach each test's failure detail
|
|
12194
|
+
* (by testId), root-cause chip, and slow rank. A pure projection — the source
|
|
12195
|
+
* ReportData is never mutated. Gating lives here (not the template): a field is
|
|
12196
|
+
* attached only when its section is on, so the template just checks presence.
|
|
12197
|
+
* `failuresById` is built from the (possibly overflow-trimmed) `failures` array,
|
|
12198
|
+
* so a trimmed failure yields no inline card; the overflow note points to the sidecar.
|
|
12199
|
+
*/
|
|
12200
|
+
enrichProjects(data, slow, sections, slowTestThreshold) {
|
|
12201
|
+
const failuresById = sections.failureDeepDive ? new Map(data.failures.map((f) => [f.testId, f])) : /* @__PURE__ */ new Map();
|
|
12202
|
+
const chipById = sections.failureAnalysis ? new Map((data.classifications ?? []).map((c) => [c.testId, c])) : /* @__PURE__ */ new Map();
|
|
12203
|
+
const slowRankById = /* @__PURE__ */ new Map();
|
|
12204
|
+
if (sections.slowTests && slow.timedTotal > slowTestThreshold) {
|
|
12205
|
+
slow.ranked.forEach((t, i) => slowRankById.set(t.id, i + 1));
|
|
12206
|
+
}
|
|
12207
|
+
const enrichTest = (t) => ({
|
|
12208
|
+
...t,
|
|
12209
|
+
failure: failuresById.get(t.id),
|
|
12210
|
+
chip: chipById.get(t.id),
|
|
12211
|
+
slowRank: slowRankById.get(t.id)
|
|
12212
|
+
});
|
|
12213
|
+
const enrichSuite = (s) => ({
|
|
12214
|
+
...s,
|
|
12215
|
+
suites: s.suites.map(enrichSuite),
|
|
12216
|
+
tests: s.tests.map(enrichTest)
|
|
12217
|
+
});
|
|
12218
|
+
return data.projects.map((p) => ({ ...p, suites: p.suites.map(enrichSuite) }));
|
|
12165
12219
|
}
|
|
12166
12220
|
readStyle(filename) {
|
|
12167
12221
|
const cached = this.styleCache.get(filename);
|
|
@@ -12840,7 +12894,7 @@ var PdfGenerator = class {
|
|
|
12840
12894
|
}
|
|
12841
12895
|
const templateLabel = source.kind === "builtin" ? source.id : source.label;
|
|
12842
12896
|
logger.info(`Rendering template: ${templateLabel}`);
|
|
12843
|
-
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, options.sections) : this.templateEngine.renderFromPath(renderData, source.absolutePath);
|
|
12897
|
+
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, options.sections, options.slowTestThreshold) : this.templateEngine.renderFromPath(renderData, source.absolutePath, options.slowTestThreshold);
|
|
12844
12898
|
const tempHtmlPath = await this.htmlWriter.write(html);
|
|
12845
12899
|
const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
|
|
12846
12900
|
try {
|
|
@@ -13237,7 +13291,7 @@ var PdfReporter = class {
|
|
|
13237
13291
|
this.dataCollector = new DataCollector();
|
|
13238
13292
|
this.pdfGenerator = new PdfGenerator();
|
|
13239
13293
|
this.options = parseOptions(rawOptions);
|
|
13240
|
-
const version = true ? "0.
|
|
13294
|
+
const version = true ? "0.7.0" : "0.x";
|
|
13241
13295
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
13242
13296
|
this.licenseClient = new LicenseClient({
|
|
13243
13297
|
licenseKey: this.options.licenseKey,
|
package/dist/index.mjs
CHANGED
|
@@ -10330,7 +10330,8 @@ var DEFAULT_OPTIONS = {
|
|
|
10330
10330
|
maxFileSizeMb: 8,
|
|
10331
10331
|
historySize: 10,
|
|
10332
10332
|
showTrend: true,
|
|
10333
|
-
flakinessTopN: 5
|
|
10333
|
+
flakinessTopN: 5,
|
|
10334
|
+
slowTestThreshold: 10
|
|
10334
10335
|
};
|
|
10335
10336
|
|
|
10336
10337
|
// src/templates/sections.ts
|
|
@@ -10407,7 +10408,10 @@ var SECTION_DEFAULTS = {
|
|
|
10407
10408
|
trend: true,
|
|
10408
10409
|
requirementsMatrix: false,
|
|
10409
10410
|
ciEnvironment: true,
|
|
10410
|
-
|
|
10411
|
+
// Suite breakdown now hosts the inline failure detail + slow badge, so the
|
|
10412
|
+
// executive layout renders it too (compact: fullFailures stays false → no
|
|
10413
|
+
// screenshots; failureAnalysis stays off → no per-test chip).
|
|
10414
|
+
suiteBreakdown: true,
|
|
10411
10415
|
failureDeepDive: true,
|
|
10412
10416
|
failureAnalysis: false,
|
|
10413
10417
|
slowTests: true,
|
|
@@ -10434,6 +10438,7 @@ function resolveSections(template, userSections) {
|
|
|
10434
10438
|
}
|
|
10435
10439
|
const result = { ...base, ...flat, ...perTemplate };
|
|
10436
10440
|
if (!result.charts) result.trend = false;
|
|
10441
|
+
if (!result.failureDeepDive) result.failureAnalysis = false;
|
|
10437
10442
|
return result;
|
|
10438
10443
|
}
|
|
10439
10444
|
function allSectionsOn() {
|
|
@@ -10510,6 +10515,7 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10510
10515
|
historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
|
|
10511
10516
|
showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
|
|
10512
10517
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
10518
|
+
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10513
10519
|
failureAnalysis: failureAnalysisConfigSchema,
|
|
10514
10520
|
templatePath: external_exports.union([
|
|
10515
10521
|
external_exports.string().regex(/\.hbs$/, "templatePath must end with .hbs").refine((v) => v.slice(0, -4).trim().length > 0, "templatePath basename must not be empty"),
|
|
@@ -11173,6 +11179,14 @@ function classifyOne(f, model) {
|
|
|
11173
11179
|
function classifyAll(failures, model) {
|
|
11174
11180
|
return failures.map((f) => classifyOne(f, model));
|
|
11175
11181
|
}
|
|
11182
|
+
function perTestClassifications(classified) {
|
|
11183
|
+
return classified.map((c) => ({
|
|
11184
|
+
testId: c.failure.testId,
|
|
11185
|
+
category: c.category,
|
|
11186
|
+
message: representativeMessage(c.failure.error.message),
|
|
11187
|
+
strength: strengthOf(c.margin)
|
|
11188
|
+
}));
|
|
11189
|
+
}
|
|
11176
11190
|
function clusterClassified(classified) {
|
|
11177
11191
|
const groups = /* @__PURE__ */ new Map();
|
|
11178
11192
|
for (const c of classified) {
|
|
@@ -11348,6 +11362,7 @@ function runFailureAnalysis(reportData, options, cwd) {
|
|
|
11348
11362
|
const analysed = reportData.failures.slice(0, cap);
|
|
11349
11363
|
const model = loadModel(fa.autoUpdateModel);
|
|
11350
11364
|
const classified = classifyAll(analysed, model);
|
|
11365
|
+
reportData.classifications = perTestClassifications(classified);
|
|
11351
11366
|
if (templatesConsumeAnalysis(options)) {
|
|
11352
11367
|
reportData.analysis = analyseClassified(classified, reportData.failures.length, fa);
|
|
11353
11368
|
}
|
|
@@ -12070,17 +12085,17 @@ var TemplateEngine = class {
|
|
|
12070
12085
|
setChartjsBundle(inline) {
|
|
12071
12086
|
this.chartjsInline = inline;
|
|
12072
12087
|
}
|
|
12073
|
-
render(data, template, userSections) {
|
|
12088
|
+
render(data, template, userSections, slowTestThreshold) {
|
|
12074
12089
|
const layoutPath = path7.join(this.templatesDir, "layouts", `${template}.hbs`);
|
|
12075
12090
|
if (!fs5.existsSync(layoutPath)) {
|
|
12076
12091
|
throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
|
|
12077
12092
|
}
|
|
12078
12093
|
const layoutSource = fs5.readFileSync(layoutPath, "utf8");
|
|
12079
12094
|
const compiledLayout = this.handlebars.compile(layoutSource);
|
|
12080
|
-
const ctx = this.buildContext(data, template, userSections);
|
|
12095
|
+
const ctx = this.buildContext(data, template, userSections, slowTestThreshold);
|
|
12081
12096
|
return compiledLayout(ctx);
|
|
12082
12097
|
}
|
|
12083
|
-
renderFromPath(data, filePath) {
|
|
12098
|
+
renderFromPath(data, filePath, slowTestThreshold) {
|
|
12084
12099
|
if (!this.customTemplateCache.has(filePath)) {
|
|
12085
12100
|
if (!fs5.existsSync(filePath)) {
|
|
12086
12101
|
throw new Error(`[reportforge] Custom template not found: ${filePath}`);
|
|
@@ -12089,32 +12104,41 @@ var TemplateEngine = class {
|
|
|
12089
12104
|
this.customTemplateCache.set(filePath, this.handlebars.compile(source));
|
|
12090
12105
|
}
|
|
12091
12106
|
const compiledFn = this.customTemplateCache.get(filePath);
|
|
12092
|
-
return compiledFn(this.buildCustomContext(data));
|
|
12107
|
+
return compiledFn(this.buildCustomContext(data, slowTestThreshold));
|
|
12093
12108
|
}
|
|
12094
|
-
buildCustomContext(data) {
|
|
12109
|
+
buildCustomContext(data, slowTestThreshold) {
|
|
12095
12110
|
const sections = allSectionsOn();
|
|
12111
|
+
const slow = this.buildSlowTests(data);
|
|
12096
12112
|
return {
|
|
12097
12113
|
...data,
|
|
12114
|
+
projects: this.enrichProjects(data, slow, sections, slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold),
|
|
12098
12115
|
options: deriveOptions(sections),
|
|
12099
12116
|
sections,
|
|
12100
12117
|
tagGroups: this.buildTagGroups(data),
|
|
12101
|
-
slowTests:
|
|
12118
|
+
slowTests: slow.ranked,
|
|
12102
12119
|
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
12103
12120
|
templateCSS: "",
|
|
12104
12121
|
chartjsScript: this.buildChartjsScript()
|
|
12105
12122
|
};
|
|
12106
12123
|
}
|
|
12107
|
-
buildContext(data, template, userSections) {
|
|
12124
|
+
buildContext(data, template, userSections, slowTestThreshold) {
|
|
12108
12125
|
const baseCSS = this.fontsBundleCss + "\n" + this.readStyle("base.css");
|
|
12109
12126
|
const templateCSS = this.readStyle(`${template}.css`);
|
|
12110
12127
|
const sections = resolveSections(template, userSections);
|
|
12111
12128
|
const options = deriveOptions(sections);
|
|
12129
|
+
const slow = this.buildSlowTests(data);
|
|
12112
12130
|
return {
|
|
12113
12131
|
...data,
|
|
12132
|
+
projects: this.enrichProjects(
|
|
12133
|
+
data,
|
|
12134
|
+
slow,
|
|
12135
|
+
sections,
|
|
12136
|
+
slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold
|
|
12137
|
+
),
|
|
12114
12138
|
options,
|
|
12115
12139
|
sections,
|
|
12116
12140
|
tagGroups: this.buildTagGroups(data),
|
|
12117
|
-
slowTests:
|
|
12141
|
+
slowTests: slow.ranked,
|
|
12118
12142
|
baseCSS,
|
|
12119
12143
|
templateCSS,
|
|
12120
12144
|
chartjsScript: this.buildChartjsScript()
|
|
@@ -12151,6 +12175,7 @@ var TemplateEngine = class {
|
|
|
12151
12175
|
for (const test of suite.tests) {
|
|
12152
12176
|
if (test.duration > 0) {
|
|
12153
12177
|
tests.push({
|
|
12178
|
+
id: test.id,
|
|
12154
12179
|
title: test.title,
|
|
12155
12180
|
suiteTitle: suite.title,
|
|
12156
12181
|
durationMs: test.duration,
|
|
@@ -12162,7 +12187,36 @@ var TemplateEngine = class {
|
|
|
12162
12187
|
}
|
|
12163
12188
|
}
|
|
12164
12189
|
}
|
|
12165
|
-
|
|
12190
|
+
const ranked = tests.sort((a, b) => b.durationMs - a.durationMs).slice(0, 10);
|
|
12191
|
+
return { ranked, timedTotal: tests.length };
|
|
12192
|
+
}
|
|
12193
|
+
/**
|
|
12194
|
+
* Project the suite tree into its render form: attach each test's failure detail
|
|
12195
|
+
* (by testId), root-cause chip, and slow rank. A pure projection — the source
|
|
12196
|
+
* ReportData is never mutated. Gating lives here (not the template): a field is
|
|
12197
|
+
* attached only when its section is on, so the template just checks presence.
|
|
12198
|
+
* `failuresById` is built from the (possibly overflow-trimmed) `failures` array,
|
|
12199
|
+
* so a trimmed failure yields no inline card; the overflow note points to the sidecar.
|
|
12200
|
+
*/
|
|
12201
|
+
enrichProjects(data, slow, sections, slowTestThreshold) {
|
|
12202
|
+
const failuresById = sections.failureDeepDive ? new Map(data.failures.map((f) => [f.testId, f])) : /* @__PURE__ */ new Map();
|
|
12203
|
+
const chipById = sections.failureAnalysis ? new Map((data.classifications ?? []).map((c) => [c.testId, c])) : /* @__PURE__ */ new Map();
|
|
12204
|
+
const slowRankById = /* @__PURE__ */ new Map();
|
|
12205
|
+
if (sections.slowTests && slow.timedTotal > slowTestThreshold) {
|
|
12206
|
+
slow.ranked.forEach((t, i) => slowRankById.set(t.id, i + 1));
|
|
12207
|
+
}
|
|
12208
|
+
const enrichTest = (t) => ({
|
|
12209
|
+
...t,
|
|
12210
|
+
failure: failuresById.get(t.id),
|
|
12211
|
+
chip: chipById.get(t.id),
|
|
12212
|
+
slowRank: slowRankById.get(t.id)
|
|
12213
|
+
});
|
|
12214
|
+
const enrichSuite = (s) => ({
|
|
12215
|
+
...s,
|
|
12216
|
+
suites: s.suites.map(enrichSuite),
|
|
12217
|
+
tests: s.tests.map(enrichTest)
|
|
12218
|
+
});
|
|
12219
|
+
return data.projects.map((p) => ({ ...p, suites: p.suites.map(enrichSuite) }));
|
|
12166
12220
|
}
|
|
12167
12221
|
readStyle(filename) {
|
|
12168
12222
|
const cached = this.styleCache.get(filename);
|
|
@@ -12841,7 +12895,7 @@ var PdfGenerator = class {
|
|
|
12841
12895
|
}
|
|
12842
12896
|
const templateLabel = source.kind === "builtin" ? source.id : source.label;
|
|
12843
12897
|
logger.info(`Rendering template: ${templateLabel}`);
|
|
12844
|
-
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, options.sections) : this.templateEngine.renderFromPath(renderData, source.absolutePath);
|
|
12898
|
+
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, options.sections, options.slowTestThreshold) : this.templateEngine.renderFromPath(renderData, source.absolutePath, options.slowTestThreshold);
|
|
12845
12899
|
const tempHtmlPath = await this.htmlWriter.write(html);
|
|
12846
12900
|
const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
|
|
12847
12901
|
try {
|
|
@@ -13238,7 +13292,7 @@ var PdfReporter = class {
|
|
|
13238
13292
|
this.dataCollector = new DataCollector();
|
|
13239
13293
|
this.pdfGenerator = new PdfGenerator();
|
|
13240
13294
|
this.options = parseOptions(rawOptions);
|
|
13241
|
-
const version = true ? "0.
|
|
13295
|
+
const version = true ? "0.7.0" : "0.x";
|
|
13242
13296
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
13243
13297
|
this.licenseClient = new LicenseClient({
|
|
13244
13298
|
licenseKey: this.options.licenseKey,
|
|
@@ -26,13 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
{{#if sections.ciEnvironment}}{{> ci-environment options=(obj showFullEnvironment=sections.fullEnvironment) }}{{/if}}
|
|
28
28
|
|
|
29
|
-
{{#if sections.suiteBreakdown}}{{> suite-breakdown options=(obj showRetries=sections.retries) }}{{/if}}
|
|
30
|
-
|
|
31
|
-
{{#if sections.failureDeepDive}}{{> failure-deep-dive options=(obj showFullFailures=sections.fullFailures showStackTraces=sections.stackTraces) }}{{/if}}
|
|
32
|
-
|
|
33
|
-
{{#if sections.failureAnalysis}}{{> failure-analysis}}{{/if}}
|
|
34
|
-
|
|
35
|
-
{{#if sections.slowTests}}{{> slow-tests limit=10 showProject=true}}{{/if}}
|
|
29
|
+
{{#if sections.suiteBreakdown}}{{> suite-breakdown options=(obj showRetries=sections.retries showFullFailures=sections.fullFailures showStackTraces=sections.stackTraces) }}{{/if}}
|
|
36
30
|
|
|
37
31
|
{{#if sections.defectLog}}{{> defect-log }}{{/if}}
|
|
38
32
|
|
|
@@ -24,17 +24,11 @@
|
|
|
24
24
|
|
|
25
25
|
{{#if sections.requirementsMatrix}}{{> requirements-matrix }}{{/if}}
|
|
26
26
|
|
|
27
|
-
{{#if sections.slowTests}}{{> slow-tests limit=5 showProject=false}}{{/if}}
|
|
28
|
-
|
|
29
|
-
{{#if sections.failureDeepDive}}{{> failure-deep-dive options=(obj showFullFailures=sections.fullFailures showStackTraces=sections.stackTraces) }}{{/if}}
|
|
30
|
-
|
|
31
|
-
{{#if sections.failureAnalysis}}{{> failure-analysis}}{{/if}}
|
|
32
|
-
|
|
33
27
|
{{#if sections.defectLog}}{{> defect-log }}{{/if}}
|
|
34
28
|
|
|
35
29
|
{{#if sections.ciEnvironment}}{{> ci-environment options=(obj showFullEnvironment=sections.fullEnvironment) }}{{/if}}
|
|
36
30
|
|
|
37
|
-
{{#if sections.suiteBreakdown}}{{> suite-breakdown options=(obj showRetries=sections.retries) }}{{/if}}
|
|
31
|
+
{{#if sections.suiteBreakdown}}{{> suite-breakdown options=(obj showRetries=sections.retries showFullFailures=sections.fullFailures showStackTraces=sections.stackTraces) }}{{/if}}
|
|
38
32
|
|
|
39
33
|
<div class="report-footer">
|
|
40
34
|
<span>{{meta.projectName}} · Playwright Test Report</span>
|
|
@@ -24,13 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
{{#if sections.requirementsMatrix}}{{> requirements-matrix }}{{/if}}
|
|
26
26
|
|
|
27
|
-
{{#if sections.suiteBreakdown}}{{> suite-breakdown options=(obj showRetries=sections.retries) }}{{/if}}
|
|
28
|
-
|
|
29
|
-
{{#if sections.failureDeepDive}}{{> failure-deep-dive options=(obj showFullFailures=sections.fullFailures showStackTraces=sections.stackTraces) }}{{/if}}
|
|
30
|
-
|
|
31
|
-
{{#if sections.failureAnalysis}}{{> failure-analysis}}{{/if}}
|
|
32
|
-
|
|
33
|
-
{{#if sections.slowTests}}{{> slow-tests limit=10 showProject=true}}{{/if}}
|
|
27
|
+
{{#if sections.suiteBreakdown}}{{> suite-breakdown options=(obj showRetries=sections.retries showFullFailures=sections.fullFailures showStackTraces=sections.stackTraces) }}{{/if}}
|
|
34
28
|
|
|
35
29
|
{{#if sections.defectLog}}{{> defect-log }}{{/if}}
|
|
36
30
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{{!-- Inline failure detail, rendered inside the single .failure-detail block in
|
|
2
|
+
the suite breakdown (which provides the rail, indent, and page-break guard).
|
|
3
|
+
The test row already shows the title/location, so this is error + screenshot
|
|
4
|
+
+ retry history only. Caller passes the context:
|
|
5
|
+
failure — the FailureRecord for this test
|
|
6
|
+
showFull — include the screenshot (compact templates pass false)
|
|
7
|
+
showStack — render the stack trace instead of the bare message --}}
|
|
8
|
+
{{#if (and failure.error.stack showStack)}}
|
|
9
|
+
<pre>{{failure.error.stack}}</pre>
|
|
10
|
+
{{else}}
|
|
11
|
+
<div class="failure-item__message">{{failure.error.message}}</div>
|
|
12
|
+
{{/if}}
|
|
13
|
+
|
|
14
|
+
{{#if (and showFull failure.screenshotBase64)}}
|
|
15
|
+
<img class="screenshot" src="{{failure.screenshotBase64}}" alt="Screenshot at failure">
|
|
16
|
+
{{/if}}
|
|
17
|
+
|
|
18
|
+
{{#if failure.retryHistory.length}}
|
|
19
|
+
<div class="retry-history">
|
|
20
|
+
Retry history:
|
|
21
|
+
{{#each failure.retryHistory}}
|
|
22
|
+
<span class="retry-attempt retry-attempt--{{status}}">
|
|
23
|
+
#{{addOne attempt}} {{status}} · {{formatDuration duration}}
|
|
24
|
+
</span>
|
|
25
|
+
{{/each}}
|
|
26
|
+
</div>
|
|
27
|
+
{{/if}}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<div class="section">
|
|
2
2
|
<h2>Suite Breakdown</h2>
|
|
3
|
+
{{#if (and overflowFailureCount sections.failureDeepDive)}}
|
|
4
|
+
<div class="overflow-banner">
|
|
5
|
+
Inline failure detail covers the first {{failures.length}} failures.
|
|
6
|
+
{{overflowFailureCount}} more {{#if (gt overflowFailureCount 1)}}are{{else}}is{{/if}}
|
|
7
|
+
available in <code>{{overflowSidecarName}}</code> alongside this report.
|
|
8
|
+
</div>
|
|
9
|
+
{{/if}}
|
|
3
10
|
{{#each projects}}
|
|
4
11
|
<h3 style="color:var(--primary,#CC785C);margin-bottom:8px;">{{name}}</h3>
|
|
5
12
|
{{#each suites}}
|
|
@@ -25,9 +32,9 @@
|
|
|
25
32
|
<thead>
|
|
26
33
|
<tr>
|
|
27
34
|
<th>Test</th>
|
|
28
|
-
<th style="width:
|
|
35
|
+
<th style="width:112px;">Status</th>
|
|
29
36
|
<th style="width:72px;">Duration</th>
|
|
30
|
-
{{#if
|
|
37
|
+
{{#if @root.options.showRetries}}<th style="width:52px;">Retries</th>{{/if}}
|
|
31
38
|
</tr>
|
|
32
39
|
</thead>
|
|
33
40
|
<tbody>
|
|
@@ -42,10 +49,29 @@
|
|
|
42
49
|
</div>
|
|
43
50
|
{{/if}}
|
|
44
51
|
</td>
|
|
45
|
-
<td
|
|
52
|
+
<td style="white-space:nowrap;">
|
|
53
|
+
{{statusBadge status}}{{#if slowRank}}<span class="badge badge--slow" title="Among the slowest tests">SLOW</span>{{/if}}
|
|
54
|
+
</td>
|
|
46
55
|
<td style="color:var(--text-2);font-family:'JetBrains Mono',monospace;font-size:9.5px;">{{formatDuration duration}}</td>
|
|
47
|
-
{{#if
|
|
56
|
+
{{#if @root.options.showRetries}}<td style="color:var(--text-3);text-align:center;">{{retryCount}}</td>{{/if}}
|
|
57
|
+
</tr>
|
|
58
|
+
{{#if failure}}
|
|
59
|
+
<tr class="failure-row no-break">
|
|
60
|
+
<td colspan="{{#if @root.options.showRetries}}4{{else}}3{{/if}}">
|
|
61
|
+
<div class="failure-detail failure-detail--{{failure.severity}}">
|
|
62
|
+
{{> failure-card failure=failure showFull=@root.options.showFullFailures showStack=@root.options.showStackTraces}}
|
|
63
|
+
{{#if chip}}
|
|
64
|
+
<div class="root-cause-chip root-cause-chip--{{chip.category}}">
|
|
65
|
+
<span class="root-cause-chip__tag">Root cause</span>
|
|
66
|
+
<span class="root-cause-chip__label">{{categoryLabel chip.category}}</span>
|
|
67
|
+
<span class="root-cause-chip__msg">{{chip.message}}</span>
|
|
68
|
+
<span class="root-cause-chip__strength">{{strengthLabel chip.strength}} match</span>
|
|
69
|
+
</div>
|
|
70
|
+
{{/if}}
|
|
71
|
+
</div>
|
|
72
|
+
</td>
|
|
48
73
|
</tr>
|
|
74
|
+
{{/if}}
|
|
49
75
|
{{/each}}
|
|
50
76
|
</tbody>
|
|
51
77
|
</table>
|
|
@@ -292,12 +292,90 @@ code {
|
|
|
292
292
|
.release-gate__sep { color: var(--border-3); }
|
|
293
293
|
.release-gate__flaky-warn { color: var(--flaky); font-weight: 600; }
|
|
294
294
|
|
|
295
|
-
/* ──
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
.
|
|
299
|
-
.failure-
|
|
300
|
-
.failure-
|
|
295
|
+
/* ── Inline failure detail — ONE block hanging off the failed test row ────────
|
|
296
|
+
A single severity-coloured left rail + hairline dividers tie the error,
|
|
297
|
+
retry history, and root cause together as the failed test's detail (not
|
|
298
|
+
three separate boxes). Lives here so every template's breakdown can use it. */
|
|
299
|
+
.failure-row > td { padding: 0; border: none; background: transparent; }
|
|
300
|
+
.failure-detail {
|
|
301
|
+
border-left: 3px solid var(--fail);
|
|
302
|
+
margin: 2px 10px 10px 12px; /* slight indent under the test row + gap before next row */
|
|
303
|
+
padding: 11px 0 11px 16px;
|
|
304
|
+
page-break-inside: avoid;
|
|
305
|
+
}
|
|
306
|
+
.failure-detail--critical { border-left-color: var(--fail); }
|
|
307
|
+
.failure-detail--high { border-left-color: #A35E1E; }
|
|
308
|
+
.failure-detail--medium { border-left-color: var(--timeout); }
|
|
309
|
+
.failure-detail--low { border-left-color: var(--text-3); }
|
|
310
|
+
|
|
311
|
+
/* error — flat on the rail, no inner box (override the base pre panel) */
|
|
312
|
+
.failure-detail pre {
|
|
313
|
+
margin: 0; padding: 0;
|
|
314
|
+
background: transparent; border: none; border-radius: 0; max-height: none;
|
|
315
|
+
}
|
|
316
|
+
.failure-detail .failure-item__message {
|
|
317
|
+
background: transparent; border: none; border-radius: 0; padding: 0; margin: 0;
|
|
318
|
+
font-family: 'JetBrains Mono', monospace;
|
|
319
|
+
font-size: 9.5px; color: #8A2E1E; word-break: break-word; line-height: 1.6;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* retry history — divider above, same block */
|
|
323
|
+
.retry-history {
|
|
324
|
+
font-size: 9px; color: var(--text-3); margin-top: 11px;
|
|
325
|
+
padding-top: 10px; border-top: 1px solid var(--border);
|
|
326
|
+
font-family: 'Inter', system-ui, sans-serif; font-weight: 600;
|
|
327
|
+
letter-spacing: 0.5px; text-transform: uppercase;
|
|
328
|
+
}
|
|
329
|
+
.retry-attempt {
|
|
330
|
+
display: inline-block; padding: 2px 7px;
|
|
331
|
+
border-radius: 3px; margin-right: 4px;
|
|
332
|
+
font-size: 8.5px; font-family: 'Inter', system-ui, sans-serif; font-weight: 600;
|
|
333
|
+
-webkit-print-color-adjust: exact;
|
|
334
|
+
print-color-adjust: exact;
|
|
335
|
+
}
|
|
336
|
+
.retry-attempt--failed { background: var(--fail-dim); color: var(--fail); border: 1px solid #E5C9BD; }
|
|
337
|
+
.retry-attempt--passed { background: var(--pass-dim); color: var(--pass); border: 1px solid #C5DBC9; }
|
|
338
|
+
|
|
339
|
+
/* Slow-test badge — a compact tag inline beside the status badge (min-width:0
|
|
340
|
+
overrides the .badge 50px floor so it doesn't wrap onto its own line). */
|
|
341
|
+
.badge--slow {
|
|
342
|
+
background: var(--timeout-dim); color: var(--timeout);
|
|
343
|
+
border: 1px solid #E5D2A8;
|
|
344
|
+
min-width: 0; margin-left: 5px;
|
|
345
|
+
padding: 1px 5px; font-size: 7.5px; letter-spacing: 0.4px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* Root cause — footer of the same failure block: a divider above, no box and no
|
|
349
|
+
second rail (the block already carries one). */
|
|
350
|
+
.root-cause-chip {
|
|
351
|
+
display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
|
|
352
|
+
margin-top: 11px; padding-top: 10px;
|
|
353
|
+
border-top: 1px solid var(--border);
|
|
354
|
+
font-family: 'Inter', system-ui, sans-serif; font-size: 9.5px;
|
|
355
|
+
}
|
|
356
|
+
.root-cause-chip__tag {
|
|
357
|
+
font-size: 8px; font-weight: 700; letter-spacing: 0.8px; text-transform: uppercase;
|
|
358
|
+
color: var(--text-3);
|
|
359
|
+
}
|
|
360
|
+
.root-cause-chip__label { font-weight: 700; color: var(--text-1); }
|
|
361
|
+
.root-cause-chip__msg {
|
|
362
|
+
font-family: 'JetBrains Mono', monospace; color: var(--text-2);
|
|
363
|
+
word-break: break-word;
|
|
364
|
+
}
|
|
365
|
+
.root-cause-chip__strength {
|
|
366
|
+
margin-left: auto; font-size: 8.5px; font-weight: 600;
|
|
367
|
+
letter-spacing: 0.5px; text-transform: uppercase; color: var(--text-3);
|
|
368
|
+
}
|
|
369
|
+
/* Subtle category cue — tint only the category name (no box, no second rail), so
|
|
370
|
+
the chip stays "quiet" but failures are still scannable by root-cause type. */
|
|
371
|
+
.root-cause-chip--assertion .root-cause-chip__label,
|
|
372
|
+
.root-cause-chip--locator-not-found .root-cause-chip__label { color: oklch(52% 0.19 25); }
|
|
373
|
+
.root-cause-chip--timeout .root-cause-chip__label { color: oklch(50% 0.13 65); }
|
|
374
|
+
.root-cause-chip--network .root-cause-chip__label,
|
|
375
|
+
.root-cause-chip--navigation .root-cause-chip__label { color: oklch(50% 0.15 260); }
|
|
376
|
+
.root-cause-chip--flaky-by-retry .root-cause-chip__label { color: oklch(50% 0.15 290); }
|
|
377
|
+
.root-cause-chip--env-config .root-cause-chip__label { color: oklch(48% 0.14 145); }
|
|
378
|
+
.root-cause-chip--unknown .root-cause-chip__label { color: var(--text-2); }
|
|
301
379
|
|
|
302
380
|
/* ── History trend card — shared by detailed + executive (charts.hbs) ─────── */
|
|
303
381
|
.trend-card {
|
|
@@ -656,23 +734,6 @@ code {
|
|
|
656
734
|
font-size: 9.5px; color: var(--text-1);
|
|
657
735
|
}
|
|
658
736
|
|
|
659
|
-
/*
|
|
660
|
-
|
|
661
|
-
.
|
|
662
|
-
.failure-analysis .oneliner { font-weight: 600; }
|
|
663
|
-
.analysis-truncated { font-size: 0.85em; opacity: 0.8; }
|
|
664
|
-
|
|
665
|
-
.cluster { border-left: 3px solid; padding-left: 12px; margin-bottom: 16px; }
|
|
666
|
-
.cluster-header { display: flex; gap: 12px; align-items: baseline; }
|
|
667
|
-
.cluster-strength--strong { color: oklch(55% 0.15 145); }
|
|
668
|
-
.cluster-strength--moderate { color: oklch(60% 0.18 65); }
|
|
669
|
-
.cluster-strength--weak { color: oklch(55% 0.20 25); }
|
|
670
|
-
.cluster--timeout { border-color: oklch(60% 0.18 65); }
|
|
671
|
-
.cluster--assertion { border-color: oklch(55% 0.20 25); }
|
|
672
|
-
.cluster--locator-not-found { border-color: oklch(55% 0.20 25); }
|
|
673
|
-
.cluster--network { border-color: oklch(55% 0.15 260); }
|
|
674
|
-
.cluster--navigation { border-color: oklch(55% 0.15 260); }
|
|
675
|
-
.cluster--flaky-by-retry { border-color: oklch(55% 0.15 290); }
|
|
676
|
-
.cluster--env-config { border-color: oklch(55% 0.15 145); }
|
|
677
|
-
.cluster--unknown { border-color: oklch(60% 0.00 0); }
|
|
678
|
-
.confidence-warning { font-size: 0.8em; opacity: 0.75; }
|
|
737
|
+
/* The aggregate failure-analysis cluster section was retired — root causes now
|
|
738
|
+
render as inline per-test chips in the suite breakdown (.root-cause-chip above).
|
|
739
|
+
The .analysis-oneliner one-liner lives further up and is still used by executive. */
|
|
@@ -130,50 +130,7 @@
|
|
|
130
130
|
|
|
131
131
|
/* ── Suite breakdown · styles live in base.css (shared across templates) ──── */
|
|
132
132
|
|
|
133
|
-
/* ── Failure
|
|
134
|
-
.failure-item {
|
|
135
|
-
background: var(--surface);
|
|
136
|
-
border: 1px solid var(--border-2);
|
|
137
|
-
border-left: 3px solid var(--fail);
|
|
138
|
-
border-radius: 6px; padding: 16px 18px;
|
|
139
|
-
margin-bottom: 12px; page-break-inside: avoid;
|
|
140
|
-
}
|
|
141
|
-
.failure-item__header {
|
|
142
|
-
display: flex; justify-content: space-between;
|
|
143
|
-
align-items: flex-start; margin-bottom: 10px;
|
|
144
|
-
}
|
|
145
|
-
.failure-item__title {
|
|
146
|
-
font-family: 'Inter', system-ui, sans-serif;
|
|
147
|
-
font-weight: 600; font-size: 12px; color: var(--text-1);
|
|
148
|
-
}
|
|
149
|
-
.failure-item__path {
|
|
150
|
-
font-family: 'JetBrains Mono', monospace;
|
|
151
|
-
font-size: 8.5px; color: var(--text-3); margin-bottom: 10px;
|
|
152
|
-
}
|
|
153
|
-
.failure-item__message {
|
|
154
|
-
background: var(--fail-dim);
|
|
155
|
-
border: 1px solid #E5C9BD;
|
|
156
|
-
border-left: 2px solid var(--fail);
|
|
157
|
-
border-radius: 4px; padding: 10px 14px; margin-bottom: 12px;
|
|
158
|
-
font-family: 'JetBrains Mono', monospace;
|
|
159
|
-
font-size: 9.5px; color: #8A2E1E;
|
|
160
|
-
word-break: break-word; line-height: 1.6;
|
|
161
|
-
}
|
|
162
|
-
.retry-history {
|
|
163
|
-
font-size: 9px; color: var(--text-3); margin-top: 10px;
|
|
164
|
-
padding-top: 10px; border-top: 1px solid var(--border);
|
|
165
|
-
font-family: 'Inter', system-ui, sans-serif; font-weight: 600;
|
|
166
|
-
letter-spacing: 0.5px; text-transform: uppercase;
|
|
167
|
-
}
|
|
168
|
-
.retry-attempt {
|
|
169
|
-
display: inline-block; padding: 2px 7px;
|
|
170
|
-
border-radius: 3px; margin-right: 4px;
|
|
171
|
-
font-size: 8.5px; font-family: 'Inter', system-ui, sans-serif; font-weight: 600;
|
|
172
|
-
-webkit-print-color-adjust: exact;
|
|
173
|
-
print-color-adjust: exact;
|
|
174
|
-
}
|
|
175
|
-
.retry-attempt--failed { background: var(--fail-dim); color: var(--fail); border: 1px solid #E5C9BD; }
|
|
176
|
-
.retry-attempt--passed { background: var(--pass-dim); color: var(--pass); border: 1px solid #C5DBC9; }
|
|
133
|
+
/* ── Failure card · styles live in base.css (rendered inside the breakdown) ── */
|
|
177
134
|
|
|
178
135
|
/* ── Defect log, coverage bar, environment table, failure analysis ───────── */
|
|
179
136
|
/* All moved to base.css so executive/minimal can toggle these sections on too. */
|
|
@@ -132,35 +132,5 @@
|
|
|
132
132
|
letter-spacing: 1.3px;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/*
|
|
136
|
-
.
|
|
137
|
-
display: flex; align-items: center; justify-content: space-between;
|
|
138
|
-
padding: 12px 16px;
|
|
139
|
-
border-bottom: 1px solid var(--border);
|
|
140
|
-
background: var(--surface);
|
|
141
|
-
border-left: 3px solid var(--fail);
|
|
142
|
-
}
|
|
143
|
-
.failure-summary-item:first-child {
|
|
144
|
-
border-radius: 6px 6px 0 0;
|
|
145
|
-
border-top: 1px solid var(--border-2);
|
|
146
|
-
border-right: 1px solid var(--border-2);
|
|
147
|
-
}
|
|
148
|
-
.failure-summary-item:last-child {
|
|
149
|
-
border-radius: 0 0 6px 6px;
|
|
150
|
-
border-bottom: 1px solid var(--border-2);
|
|
151
|
-
border-right: 1px solid var(--border-2);
|
|
152
|
-
}
|
|
153
|
-
.failure-summary-item:only-child {
|
|
154
|
-
border-radius: 6px;
|
|
155
|
-
border: 1px solid var(--border-2);
|
|
156
|
-
border-left: 3px solid var(--fail);
|
|
157
|
-
}
|
|
158
|
-
.failure-summary-item__title {
|
|
159
|
-
font-size: 11px; font-weight: 600; color: var(--text-1);
|
|
160
|
-
padding-left: 6px;
|
|
161
|
-
}
|
|
162
|
-
.failure-summary-item__suite {
|
|
163
|
-
font-size: 9px; color: var(--text-3); margin-top: 3px;
|
|
164
|
-
font-family: 'JetBrains Mono', monospace;
|
|
165
|
-
padding-left: 6px;
|
|
166
|
-
}
|
|
135
|
+
/* Executive failures now render inline in the suite breakdown (compact, no
|
|
136
|
+
screenshots) — card styles live in base.css. */
|
|
@@ -143,49 +143,4 @@
|
|
|
143
143
|
}
|
|
144
144
|
.suite-stats { gap: 6px; }
|
|
145
145
|
|
|
146
|
-
/* ── Failure
|
|
147
|
-
.failure-item {
|
|
148
|
-
background: var(--surface);
|
|
149
|
-
border: 1px solid var(--border-2);
|
|
150
|
-
border-left: 3px solid var(--fail);
|
|
151
|
-
border-radius: 6px;
|
|
152
|
-
padding: 14px 16px; margin-bottom: 10px;
|
|
153
|
-
page-break-inside: avoid;
|
|
154
|
-
}
|
|
155
|
-
.failure-item__header {
|
|
156
|
-
display: flex; justify-content: space-between;
|
|
157
|
-
align-items: flex-start; margin-bottom: 8px;
|
|
158
|
-
}
|
|
159
|
-
.failure-item__title {
|
|
160
|
-
font-family: 'Inter', system-ui, sans-serif;
|
|
161
|
-
font-weight: 600; font-size: 12px;
|
|
162
|
-
color: var(--text-1); margin-bottom: 4px;
|
|
163
|
-
}
|
|
164
|
-
.failure-item__path {
|
|
165
|
-
font-family: 'JetBrains Mono', monospace;
|
|
166
|
-
font-size: 8.5px; color: var(--text-3); margin-bottom: 10px;
|
|
167
|
-
}
|
|
168
|
-
.failure-item__message {
|
|
169
|
-
background: var(--fail-dim);
|
|
170
|
-
border: 1px solid #E5C9BD;
|
|
171
|
-
border-left: 2px solid var(--fail);
|
|
172
|
-
border-radius: 4px;
|
|
173
|
-
padding: 9px 12px; margin-bottom: 10px;
|
|
174
|
-
font-family: 'JetBrains Mono', monospace;
|
|
175
|
-
font-size: 9.5px; color: #8A2E1E;
|
|
176
|
-
word-break: break-word; line-height: 1.6;
|
|
177
|
-
}
|
|
178
|
-
.retry-history {
|
|
179
|
-
font-size: 9px; color: var(--text-3); margin-top: 10px;
|
|
180
|
-
padding-top: 8px; border-top: 1px solid var(--border);
|
|
181
|
-
}
|
|
182
|
-
.retry-attempt {
|
|
183
|
-
display: inline-block; padding: 2px 7px;
|
|
184
|
-
border-radius: 3px; margin-right: 4px; font-size: 8.5px;
|
|
185
|
-
font-family: 'Inter', system-ui, sans-serif; font-weight: 600;
|
|
186
|
-
letter-spacing: 0.4px; text-transform: uppercase;
|
|
187
|
-
-webkit-print-color-adjust: exact;
|
|
188
|
-
print-color-adjust: exact;
|
|
189
|
-
}
|
|
190
|
-
.retry-attempt--failed { background: var(--fail-dim); color: var(--fail); border: 1px solid #E5C9BD; }
|
|
191
|
-
.retry-attempt--passed { background: var(--pass-dim); color: var(--pass); border: 1px solid #C5DBC9; }
|
|
146
|
+
/* ── Failure card · styles live in base.css (rendered inside the breakdown) ── */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reportforge/playwright-pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Enterprise-ready PDF reports for Playwright Test — minimal, detailed, and executive templates with CI/CD integrations",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ReportForge",
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
{{#if analysis.clusters.length}}
|
|
2
|
-
<section class="failure-analysis">
|
|
3
|
-
<h2>Failure Analysis</h2>
|
|
4
|
-
<p class="oneliner">{{analysis.oneLiner}}</p>
|
|
5
|
-
|
|
6
|
-
{{#if analysis.truncated}}
|
|
7
|
-
<p class="analysis-truncated">
|
|
8
|
-
Showing analysis of the first {{analysis.analysedCount}} of {{analysis.totalFailures}} failures.
|
|
9
|
-
Raise <code>failureAnalysis.maxFailuresToAnalyse</code> for full coverage.
|
|
10
|
-
</p>
|
|
11
|
-
{{/if}}
|
|
12
|
-
|
|
13
|
-
{{#each analysis.clusters}}
|
|
14
|
-
<div class="cluster cluster--{{category}}">
|
|
15
|
-
<div class="cluster-header">
|
|
16
|
-
<span class="cluster-category">{{categoryLabel category}}</span>
|
|
17
|
-
<span class="cluster-count">{{count}} {{pluralise count "test" "tests"}}</span>
|
|
18
|
-
<span class="cluster-strength cluster-strength--{{strength}}">{{strengthLabel strength}} match</span>
|
|
19
|
-
</div>
|
|
20
|
-
|
|
21
|
-
{{#if stackFrame}}
|
|
22
|
-
<div class="cluster-frame"><code>{{stackFrame}}</code></div>
|
|
23
|
-
{{/if}}
|
|
24
|
-
|
|
25
|
-
<div class="cluster-message">{{representativeMessage}}</div>
|
|
26
|
-
|
|
27
|
-
<ul class="cluster-tests">
|
|
28
|
-
{{#each (take tests 5)}}
|
|
29
|
-
<li>{{this}}</li>
|
|
30
|
-
{{/each}}
|
|
31
|
-
{{#if (gt testsTotal 5)}}
|
|
32
|
-
<li class="more">+{{subtract testsTotal 5}} more</li>
|
|
33
|
-
{{/if}}
|
|
34
|
-
</ul>
|
|
35
|
-
|
|
36
|
-
{{#if (eq strength "weak")}}
|
|
37
|
-
<p class="confidence-warning">* Weak match — manual review recommended</p>
|
|
38
|
-
{{/if}}
|
|
39
|
-
</div>
|
|
40
|
-
{{/each}}
|
|
41
|
-
</section>
|
|
42
|
-
{{/if}}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
{{#if failures.length}}
|
|
2
|
-
<div class="section">
|
|
3
|
-
<h2>Failures ({{failures.length}}{{#if overflowFailureCount}} shown · {{overflowFailureCount}} more in sidecar{{/if}})</h2>
|
|
4
|
-
{{#if overflowFailureCount}}
|
|
5
|
-
<div class="overflow-banner">
|
|
6
|
-
Showing the first {{failures.length}} failures. {{overflowFailureCount}} additional
|
|
7
|
-
failure{{#if (gt overflowFailureCount 1)}}s are{{else}} is{{/if}} available in
|
|
8
|
-
<code>{{overflowSidecarName}}</code> alongside this report.
|
|
9
|
-
</div>
|
|
10
|
-
{{/if}}
|
|
11
|
-
{{#each failures}}
|
|
12
|
-
<div class="failure-item failure-item--{{severity}} no-break">
|
|
13
|
-
{{#if ../options.showFullFailures}}
|
|
14
|
-
|
|
15
|
-
<div class="failure-item__header">
|
|
16
|
-
<div>
|
|
17
|
-
<div class="failure-item__title">{{testTitle}}</div>
|
|
18
|
-
<div class="failure-item__path">{{join suitePath " › "}}</div>
|
|
19
|
-
</div>
|
|
20
|
-
<span class="badge badge--failed severity--{{severity}}" style="white-space:nowrap;">
|
|
21
|
-
{{upperCase severity}}
|
|
22
|
-
</span>
|
|
23
|
-
</div>
|
|
24
|
-
|
|
25
|
-
{{!-- Playwright's error.stack already begins with the message, so rendering
|
|
26
|
-
both duplicates it. Show the formatted stack when available; otherwise
|
|
27
|
-
fall back to the bare message. --}}
|
|
28
|
-
{{#if (and error.stack ../options.showStackTraces)}}
|
|
29
|
-
<pre>{{error.stack}}</pre>
|
|
30
|
-
{{else}}
|
|
31
|
-
<div class="failure-item__message">{{error.message}}</div>
|
|
32
|
-
{{/if}}
|
|
33
|
-
|
|
34
|
-
{{#if screenshotBase64}}
|
|
35
|
-
<img class="screenshot" src="{{screenshotBase64}}" alt="Screenshot at failure">
|
|
36
|
-
{{/if}}
|
|
37
|
-
|
|
38
|
-
{{#if retryHistory.length}}
|
|
39
|
-
<div class="retry-history">
|
|
40
|
-
Retry history:
|
|
41
|
-
{{#each retryHistory}}
|
|
42
|
-
<span class="retry-attempt retry-attempt--{{status}}">
|
|
43
|
-
#{{addOne attempt}} {{status}} · {{formatDuration duration}}
|
|
44
|
-
</span>
|
|
45
|
-
{{/each}}
|
|
46
|
-
</div>
|
|
47
|
-
{{/if}}
|
|
48
|
-
|
|
49
|
-
{{else}}
|
|
50
|
-
{{!-- Executive: title + suite only --}}
|
|
51
|
-
<div class="failure-summary-item">
|
|
52
|
-
<div>
|
|
53
|
-
<div class="failure-summary-item__title">{{testTitle}}</div>
|
|
54
|
-
<div class="failure-summary-item__suite">{{join suitePath " › "}}</div>
|
|
55
|
-
</div>
|
|
56
|
-
<span class="badge badge--failed">FAILED</span>
|
|
57
|
-
</div>
|
|
58
|
-
{{/if}}
|
|
59
|
-
</div>
|
|
60
|
-
{{/each}}
|
|
61
|
-
</div>
|
|
62
|
-
{{/if}}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{{#if slowTests.length}}
|
|
2
|
-
<div class="section">
|
|
3
|
-
<h2>Slowest Tests</h2>
|
|
4
|
-
<table>
|
|
5
|
-
<thead>
|
|
6
|
-
<tr>
|
|
7
|
-
<th>Test</th>
|
|
8
|
-
<th style="width:130px;">Suite</th>
|
|
9
|
-
{{#if showProject}}<th style="width:80px;">Project</th>{{/if}}
|
|
10
|
-
<th style="width:76px;text-align:right;">Duration</th>
|
|
11
|
-
<th style="width:68px;">Status</th>
|
|
12
|
-
</tr>
|
|
13
|
-
</thead>
|
|
14
|
-
<tbody>
|
|
15
|
-
{{#each (take slowTests limit)}}
|
|
16
|
-
<tr class="no-break">
|
|
17
|
-
<td style="font-weight:500;">{{title}}</td>
|
|
18
|
-
<td style="font-size:9px;color:var(--text-3);font-family:'JetBrains Mono',monospace;">{{suiteTitle}}</td>
|
|
19
|
-
{{#if ../showProject}}<td style="font-size:9px;color:var(--text-3);white-space:nowrap;">{{project}}</td>{{/if}}
|
|
20
|
-
<td style="font-family:'JetBrains Mono',monospace;font-size:9.5px;color:var(--text-2);text-align:right;">{{durationFormatted}}</td>
|
|
21
|
-
<td>{{statusBadge status}}</td>
|
|
22
|
-
</tr>
|
|
23
|
-
{{/each}}
|
|
24
|
-
</tbody>
|
|
25
|
-
</table>
|
|
26
|
-
</div>
|
|
27
|
-
{{/if}}
|