@reportforge/playwright-pdf 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,32 @@
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.2] — 2026-06-23
7
+
8
+ ### Fixed
9
+
10
+ - **Watermark no longer prints over content** — the optional watermark sits behind charts, screenshots, and tables instead of across them (z-index + a body stacking context).
11
+ - **Absent git metadata is hidden** — local runs with no branch/commit no longer render "unknown · unknown" or a stray separator; the environment table shows "n/a".
12
+ - **Failure errors render once** — the message and stack are no longer printed twice; the formatted stack is shown when available (it already begins with the message).
13
+ - **Executive analysis one-liner ordering** — the one-line summary sits below the header instead of orphaning at the top of page 2.
14
+ - **Retry attempts are 1-based** — the retry history reads `#1`, `#2`… instead of a zero-based `#0`.
15
+ - **Zero-count suite pill hidden** — the "passed" pill no longer renders when its count is 0.
16
+ - **Suite-bar labels stop clipping** — long suite / describe labels are truncated and the axis gutter widened.
17
+ - **Consistent footer copy** across all three templates.
18
+
19
+ ---
20
+
21
+ ## [0.6.1] — 2026-06-22
22
+
23
+ ### Documentation
24
+
25
+ - **Expanded the Report Sections reference** — per-section descriptions for all 13 block toggles, the 5 display modifiers, a per-template defaults matrix, and the dependency rules (`trend` needs `charts`, data-gated sections, render-layer only) in both the README and the docs site.
26
+ - Fixed the README documentation links to point at the real `/docs/<page>` routes instead of non-existent `/docs#<anchor>` anchors.
27
+
28
+ _No runtime changes — `@reportforge/playwright-pdf@0.6.1` is identical in behaviour to `0.6.0`._
29
+
30
+ ---
31
+
6
32
  ## [0.6.0] — 2026-06-22
7
33
 
8
34
  ### Added
package/README.md CHANGED
@@ -109,7 +109,7 @@ npx playwright test
109
109
 
110
110
  ### Chrome setup
111
111
 
112
- The reporter auto-detects Chrome via `PUPPETEER_EXECUTABLE_PATH`, then system Chrome, then `chrome-finder`. Full per-OS instructions: [reportforge.org/docs#chrome-setup](https://reportforge.org/docs#chrome-setup).
112
+ The reporter auto-detects Chrome via `PUPPETEER_EXECUTABLE_PATH`, then system Chrome, then `chrome-finder`. Full per-OS instructions: [reportforge.org/docs/running-tests#chrome-setup](https://reportforge.org/docs/running-tests#chrome-setup).
113
113
 
114
114
  **Windows**
115
115
 
@@ -202,7 +202,7 @@ All options are the second element of the reporter tuple.
202
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. |
203
203
  <!-- AUTOGEN:options-table END -->
204
204
 
205
- Full reference: [reportforge.org/docs#configuration](https://reportforge.org/docs#configuration).
205
+ Full reference: [reportforge.org/docs/configuration](https://reportforge.org/docs/configuration).
206
206
 
207
207
  ### Shard Merging
208
208
 
@@ -364,41 +364,86 @@ One license check, one data-collection pass — faster than multiple reporter in
364
364
 
365
365
  ### Report Sections
366
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.
367
+ Each built-in template ships a curated set of sections. The `sections` option lets you **add a section a template hides or remove one it shows — per template** without writing a custom template.
369
368
 
370
369
  ```ts
371
370
  reporter: [['@reportforge/playwright-pdf', {
372
371
  template: ['minimal', 'detailed'],
373
372
  sections: {
374
- defectLog: false, // baseline: drop the defect log everywhere
375
- minimal: { charts: true }, // add charts to the minimal report
373
+ defectLog: false, // flat key: baseline for EVERY chosen template
374
+ minimal: { charts: true }, // per-template key: overrides the baseline
376
375
  detailed: { ciEnvironment: false },
377
376
  },
378
377
  }]]
379
378
  ```
380
379
 
381
- Flat keys form the baseline for every chosen template; per-template keys
382
- (`minimal` / `detailed` / `executive`) override the baseline for that template.
380
+ **Resolution order** (lowest → highest): the template's defaults → flat keys (baseline for all chosen templates) per-template keys (`minimal` / `detailed` / `executive`). An unknown key throws a configuration error (typo-safe).
383
381
 
384
- **Block toggles:** `coverPage`, `analysisOneliner`, `releaseGate`, `summary`,
385
- `charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`,
386
- `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`.
382
+ #### Block toggles which sections appear
387
383
 
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).
384
+ | Key | Renders |
385
+ |---|---|
386
+ | `coverPage` | Full-page cover: title, verdict badge, KPI stats strip, branch + commit |
387
+ | `analysisOneliner` | One-line failure-analysis summary banner |
388
+ | `releaseGate` | Ship/hold recommendation banner (APPROVED TO SHIP / HOLD) |
389
+ | `summary` | KPI strip (total · passed · failed · timed-out · skipped · flaky), verdict, duration, pass rate, flaky callout |
390
+ | `charts` | Pass-rate doughnut + stacked suite-results bar |
391
+ | `trend` | Pass-rate trend line, run-history table, and flakiness table (rendered inside `charts`) |
392
+ | `requirementsMatrix` | Tests grouped by `@tag('REQ-…')` with coverage bars (requirements traceability) |
393
+ | `ciEnvironment` | Branch, commit, browsers, CI provider, OS, Node, Playwright, projects, workers |
394
+ | `suiteBreakdown` | Per-suite → per-test table (status, duration, tags, retries) |
395
+ | `failureDeepDive` | Failure cards: error message, stack trace, screenshot, retry history |
396
+ | `failureAnalysis` | Clustered root-cause buckets from the offline classifier |
397
+ | `slowTests` | Slowest tests, duration-ranked |
398
+ | `defectLog` | `DEF-####` numbered failure table (severity, retries) |
399
+
400
+ #### Display modifiers — tune a section that's on
401
+
402
+ | Key | Effect |
403
+ |---|---|
404
+ | `passRate` | Show the pass-rate % on the Passed KPI card (`summary`) |
405
+ | `fullEnvironment` | Full environment table instead of the compact grid (`ciEnvironment`) |
406
+ | `retries` | Retries column in the suite breakdown (`suiteBreakdown`) |
407
+ | `fullFailures` | Full failure cards instead of one-line summaries (`failureDeepDive`) |
408
+ | `stackTraces` | Include `<pre>` stack traces in the failure cards (`failureDeepDive`) |
409
+
410
+ #### Per-template defaults
411
+
412
+ Omit `sections` entirely and each template renders exactly this (✓ = on):
413
+
414
+ | Section | minimal | detailed | executive |
415
+ |---|:-:|:-:|:-:|
416
+ | `coverPage` | – | – | ✓ |
417
+ | `analysisOneliner` | – | – | ✓ |
418
+ | `releaseGate` | ✓ | ✓ | ✓ |
419
+ | `summary` | ✓ | ✓ | ✓ |
420
+ | `charts` | – | ✓ | ✓ |
421
+ | `trend` | – | ✓ | ✓ |
422
+ | `requirementsMatrix` | – | ✓ | – |
423
+ | `ciEnvironment` | ✓ | ✓ | ✓ |
424
+ | `suiteBreakdown` | ✓ | ✓ | – |
425
+ | `failureDeepDive` | ✓ | ✓ | ✓ |
426
+ | `failureAnalysis` | – | ✓ | – |
427
+ | `slowTests` | – | ✓ | ✓ |
428
+ | `defectLog` | – | ✓ | – |
429
+ | `passRate` | ✓ | ✓ | ✓ |
430
+ | `fullEnvironment` | – | ✓ | – |
431
+ | `retries` | ✓ | ✓ | – |
432
+ | `fullFailures` | ✓ | ✓ | – |
433
+ | `stackTraces` | ✓ | ✓ | – |
434
+
435
+ #### Dependencies & gotchas
436
+
437
+ - **`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
+ - **Data-gated sections** — `failureAnalysis`, `requirementsMatrix`, `slowTests`, `defectLog` render only when there's matching data (clusters, tagged tests, timed tests, failures). Toggling them on with no data shows nothing.
439
+ - **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
+ - **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.
396
441
 
397
442
  ---
398
443
 
399
444
  ## CI/CD Integration
400
445
 
401
- Full workflows for all four CI providers: [reportforge.org/docs#ci-cd](https://reportforge.org/docs#ci-cd).
446
+ Full workflows for all four CI providers: [reportforge.org/docs/ci-cd](https://reportforge.org/docs/ci-cd).
402
447
 
403
448
  ### GitHub Actions
404
449
 
@@ -534,7 +579,7 @@ export RF_LICENSE_KEY=RFSU-XXXX-XXXX-XXXX-XXXX
534
579
  npx playwright test
535
580
  ```
536
581
 
537
- Full lifecycle + offline failure modes: [reportforge.org/docs#license](https://reportforge.org/docs#license).
582
+ Full lifecycle + offline failure modes: [reportforge.org/docs/licensing](https://reportforge.org/docs/licensing).
538
583
 
539
584
  ---
540
585
 
@@ -566,7 +611,7 @@ Common issues:
566
611
  - **`pdfPassword` ignored** → requires `qpdf` on `PATH` (`apt install qpdf` / `brew install qpdf` / `choco install qpdf`).
567
612
  - **No PDF generated** → verify `RF_LICENSE_KEY` is set and your subscription is active. `RF_DEBUG=1` will show the activation result; missing key, expired subscription, network failure, or no JWT cache all skip PDF generation.
568
613
 
569
- Full guide: [reportforge.org/docs#troubleshooting](https://reportforge.org/docs#troubleshooting).
614
+ Full guide: [reportforge.org/docs/troubleshooting](https://reportforge.org/docs/troubleshooting).
570
615
 
571
616
  ---
572
617
 
@@ -574,15 +619,15 @@ Full guide: [reportforge.org/docs#troubleshooting](https://reportforge.org/docs#
574
619
 
575
620
  Full documentation at [reportforge.org/docs](https://reportforge.org/docs):
576
621
 
577
- - [Quick start](https://reportforge.org/docs#install) — install, configure, activate
578
- - [Chrome setup](https://reportforge.org/docs#chrome-setup) — Windows, Linux, macOS
579
- - [CI/CD integration](https://reportforge.org/docs#ci-cd) — GitHub Actions, GitLab CI, Bitbucket Pipelines, Jenkins, Azure DevOps
580
- - [Configuration reference](https://reportforge.org/docs#configuration) — all options
581
- - [Filename tokens](https://reportforge.org/docs#tokens) — `{date}`, `{branch}`, `{status}`
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
584
- - [License & offline behaviour](https://reportforge.org/docs#license) — JWT cache, machine cap, cancellation
585
- - [Troubleshooting](https://reportforge.org/docs#troubleshooting) — common issues and debug flags
622
+ - [Quick start](https://reportforge.org/docs/quickstart) — install, configure, activate
623
+ - [Chrome setup](https://reportforge.org/docs/running-tests#chrome-setup) — Windows, Linux, macOS
624
+ - [CI/CD integration](https://reportforge.org/docs/ci-cd) — GitHub Actions, GitLab CI, Bitbucket Pipelines, Jenkins, Azure DevOps
625
+ - [Configuration reference](https://reportforge.org/docs/configuration) — all options
626
+ - [Filename tokens](https://reportforge.org/docs/configuration#tokens) — `{date}`, `{branch}`, `{status}`
627
+ - [Templates](https://reportforge.org/docs/templates) — minimal, detailed, executive
628
+ - [Report Sections](https://reportforge.org/docs/configuration#report-sections) — add or remove sections per template
629
+ - [License & offline behaviour](https://reportforge.org/docs/licensing) — JWT cache, machine cap, cancellation
630
+ - [Troubleshooting](https://reportforge.org/docs/troubleshooting) — common issues and debug flags
586
631
  - **Changelog** — [CHANGELOG.md](./CHANGELOG.md)
587
632
 
588
633
  ---
package/dist/demo/cli.js CHANGED
@@ -6342,9 +6342,9 @@ function buildDemoFailures() {
6342
6342
  stack: "Error: expect(received).toBe(expected)\n at Object.<anonymous> (tests/auth.spec.ts:79:28)\n at processTicksAndRejections (node:internal/process/task_queues:95:5)"
6343
6343
  },
6344
6344
  retryHistory: [
6345
- { attempt: 1, status: "failed", duration: 5200 },
6346
- { attempt: 2, status: "failed", duration: 4900 },
6347
- { attempt: 3, status: "failed", duration: 4900 }
6345
+ { attempt: 0, status: "failed", duration: 5200 },
6346
+ { attempt: 1, status: "failed", duration: 4900 },
6347
+ { attempt: 2, status: "failed", duration: 4900 }
6348
6348
  ],
6349
6349
  severity: "critical"
6350
6350
  },
@@ -6357,9 +6357,9 @@ function buildDemoFailures() {
6357
6357
  stack: "TimeoutError: Locator.waitFor: Timeout 5000ms exceeded.\n at tests/checkout.spec.ts:58:12"
6358
6358
  },
6359
6359
  retryHistory: [
6360
- { attempt: 1, status: "failed", duration: 5100 },
6361
- { attempt: 2, status: "failed", duration: 5e3 },
6362
- { attempt: 3, status: "failed", duration: 4900 }
6360
+ { attempt: 0, status: "failed", duration: 5100 },
6361
+ { attempt: 1, status: "failed", duration: 5e3 },
6362
+ { attempt: 2, status: "failed", duration: 4900 }
6363
6363
  ],
6364
6364
  severity: "high"
6365
6365
  },
@@ -6372,9 +6372,9 @@ function buildDemoFailures() {
6372
6372
  stack: "Error: expect(locator).toBeVisible()\n at tests/catalog.spec.ts:112:9"
6373
6373
  },
6374
6374
  retryHistory: [
6375
- { attempt: 1, status: "failed", duration: 4100 },
6376
- { attempt: 2, status: "failed", duration: 4e3 },
6377
- { attempt: 3, status: "failed", duration: 3900 }
6375
+ { attempt: 0, status: "failed", duration: 4100 },
6376
+ { attempt: 1, status: "failed", duration: 4e3 },
6377
+ { attempt: 2, status: "failed", duration: 3900 }
6378
6378
  ],
6379
6379
  severity: "medium"
6380
6380
  }
@@ -6642,6 +6642,9 @@ function allSectionsOn() {
6642
6642
  function jsonForInlineScript(value) {
6643
6643
  return JSON.stringify(value).replace(/<\//g, "<\\/");
6644
6644
  }
6645
+ function isUnknownValue(v) {
6646
+ return !v || v === "unknown";
6647
+ }
6645
6648
  function deriveOptions(s) {
6646
6649
  return {
6647
6650
  showCharts: s.charts,
@@ -6862,6 +6865,9 @@ var TemplateEngine = class {
6862
6865
  );
6863
6866
  this.handlebars.registerHelper("lt", (a, b) => a < b);
6864
6867
  this.handlebars.registerHelper("or", (a, b) => Boolean(a) || Boolean(b));
6868
+ this.handlebars.registerHelper("and", (a, b) => Boolean(a) && Boolean(b));
6869
+ this.handlebars.registerHelper("isUnknown", (v) => isUnknownValue(v));
6870
+ this.handlebars.registerHelper("gitVal", (v) => isUnknownValue(v) ? "n/a" : v);
6865
6871
  this.handlebars.registerHelper("categoryLabel", (cat) => CATEGORY_LABEL[cat] ?? cat);
6866
6872
  this.handlebars.registerHelper("strengthLabel", (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : "");
6867
6873
  this.handlebars.registerHelper("pluralise", (n, one, many) => n === 1 ? one : many);
package/dist/index.js CHANGED
@@ -12030,6 +12030,9 @@ function formatDuration(ms) {
12030
12030
  function jsonForInlineScript(value) {
12031
12031
  return JSON.stringify(value).replace(/<\//g, "<\\/");
12032
12032
  }
12033
+ function isUnknownValue(v) {
12034
+ return !v || v === "unknown";
12035
+ }
12033
12036
  function deriveOptions(s) {
12034
12037
  return {
12035
12038
  showCharts: s.charts,
@@ -12250,6 +12253,9 @@ var TemplateEngine = class {
12250
12253
  );
12251
12254
  this.handlebars.registerHelper("lt", (a, b) => a < b);
12252
12255
  this.handlebars.registerHelper("or", (a, b) => Boolean(a) || Boolean(b));
12256
+ this.handlebars.registerHelper("and", (a, b) => Boolean(a) && Boolean(b));
12257
+ this.handlebars.registerHelper("isUnknown", (v) => isUnknownValue(v));
12258
+ this.handlebars.registerHelper("gitVal", (v) => isUnknownValue(v) ? "n/a" : v);
12253
12259
  this.handlebars.registerHelper("categoryLabel", (cat) => CATEGORY_LABEL[cat] ?? cat);
12254
12260
  this.handlebars.registerHelper("strengthLabel", (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : "");
12255
12261
  this.handlebars.registerHelper("pluralise", (n, one, many) => n === 1 ? one : many);
@@ -13231,7 +13237,7 @@ var PdfReporter = class {
13231
13237
  this.dataCollector = new DataCollector();
13232
13238
  this.pdfGenerator = new PdfGenerator();
13233
13239
  this.options = parseOptions(rawOptions);
13234
- const version = true ? "0.6.0" : "0.x";
13240
+ const version = true ? "0.6.2" : "0.x";
13235
13241
  logger.info(`@reportforge/playwright-pdf v${version} initialised`);
13236
13242
  this.licenseClient = new LicenseClient({
13237
13243
  licenseKey: this.options.licenseKey,
package/dist/index.mjs CHANGED
@@ -12031,6 +12031,9 @@ function formatDuration(ms) {
12031
12031
  function jsonForInlineScript(value) {
12032
12032
  return JSON.stringify(value).replace(/<\//g, "<\\/");
12033
12033
  }
12034
+ function isUnknownValue(v) {
12035
+ return !v || v === "unknown";
12036
+ }
12034
12037
  function deriveOptions(s) {
12035
12038
  return {
12036
12039
  showCharts: s.charts,
@@ -12251,6 +12254,9 @@ var TemplateEngine = class {
12251
12254
  );
12252
12255
  this.handlebars.registerHelper("lt", (a, b) => a < b);
12253
12256
  this.handlebars.registerHelper("or", (a, b) => Boolean(a) || Boolean(b));
12257
+ this.handlebars.registerHelper("and", (a, b) => Boolean(a) && Boolean(b));
12258
+ this.handlebars.registerHelper("isUnknown", (v) => isUnknownValue(v));
12259
+ this.handlebars.registerHelper("gitVal", (v) => isUnknownValue(v) ? "n/a" : v);
12254
12260
  this.handlebars.registerHelper("categoryLabel", (cat) => CATEGORY_LABEL[cat] ?? cat);
12255
12261
  this.handlebars.registerHelper("strengthLabel", (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : "");
12256
12262
  this.handlebars.registerHelper("pluralise", (n, one, many) => n === 1 ? one : many);
@@ -13232,7 +13238,7 @@ var PdfReporter = class {
13232
13238
  this.dataCollector = new DataCollector();
13233
13239
  this.pdfGenerator = new PdfGenerator();
13234
13240
  this.options = parseOptions(rawOptions);
13235
- const version = true ? "0.6.0" : "0.x";
13241
+ const version = true ? "0.6.2" : "0.x";
13236
13242
  logger.info(`@reportforge/playwright-pdf v${version} initialised`);
13237
13243
  this.licenseClient = new LicenseClient({
13238
13244
  licenseKey: this.options.licenseKey,
@@ -37,8 +37,8 @@
37
37
  {{#if sections.defectLog}}{{> defect-log }}{{/if}}
38
38
 
39
39
  <div class="report-footer">
40
- <span>Generated by ReportForge</span>
41
- <span>{{formatDate meta.generatedAt}}</span>
40
+ <span>{{meta.projectName}} &nbsp;·&nbsp; Playwright Test Report</span>
41
+ <span>Generated by ReportForge &nbsp;·&nbsp; {{formatDate meta.generatedAt}}</span>
42
42
  </div>
43
43
 
44
44
  </body>
@@ -4,8 +4,6 @@
4
4
 
5
5
  {{#if sections.coverPage}}{{> cover-page}}{{/if}}
6
6
 
7
- {{#if sections.analysisOneliner}}{{> analysis-oneliner}}{{/if}}
8
-
9
7
  <div class="report-header">
10
8
  <div class="report-header__left">
11
9
  {{#if meta.branding.logoBase64}}
@@ -16,6 +14,8 @@
16
14
  <div class="report-header__date">{{formatDate meta.generatedAt}}</div>
17
15
  </div>
18
16
 
17
+ {{#if sections.analysisOneliner}}{{> analysis-oneliner}}{{/if}}
18
+
19
19
  {{#if sections.releaseGate}}{{> release-gate}}{{/if}}
20
20
 
21
21
  {{#if sections.summary}}{{> executive-summary options=(obj showPassRate=sections.passRate) }}{{/if}}
@@ -37,8 +37,8 @@
37
37
  {{#if sections.ciEnvironment}}{{> ci-environment options=(obj showFullEnvironment=sections.fullEnvironment) }}{{/if}}
38
38
 
39
39
  <div class="report-footer">
40
- <span>Generated by ReportForge</span>
41
- <span>{{formatDate meta.generatedAt}}</span>
40
+ <span>{{meta.projectName}} &nbsp;·&nbsp; Playwright Test Report</span>
41
+ <span>Generated by ReportForge &nbsp;·&nbsp; {{formatDate meta.generatedAt}}</span>
42
42
  </div>
43
43
 
44
44
  </body>
@@ -166,14 +166,14 @@
166
166
  y: {
167
167
  stacked: true,
168
168
  // Widen the label gutter so the longest label isn't clipped at the canvas edge.
169
- afterFit: function(scale) { if (!scale.$rfPadded) { scale.$rfPadded = true; scale.width += 8; } },
169
+ afterFit: function(scale) { if (!scale.$rfPadded) { scale.$rfPadded = true; scale.width += 12; } },
170
170
  ticks: {
171
171
  font: { size: 9, family: 'Inter, sans-serif' }, color: '#1F1F1C', autoSkip: false,
172
172
  // Truncate long suite / describe labels so they fit the axis gutter
173
173
  // instead of clipping at the canvas edge.
174
174
  callback: function(value) {
175
175
  var l = this.getLabelForValue(value);
176
- return l.length > 24 ? l.slice(0, 23) + '…' : l;
176
+ return l.length > 18 ? l.slice(0, 17) + '…' : l;
177
177
  }
178
178
  },
179
179
  grid: { display: false },
@@ -4,8 +4,8 @@
4
4
  <table class="env-table">
5
5
  <tbody>
6
6
  <tr><td>CI Provider</td><td class="env-value">{{environment.ciProvider}}</td></tr>
7
- <tr><td>Branch</td> <td class="env-value">{{environment.branch}}</td></tr>
8
- <tr><td>Commit</td> <td class="env-value">{{environment.commit}}</td></tr>
7
+ <tr><td>Branch</td> <td class="env-value">{{gitVal environment.branch}}</td></tr>
8
+ <tr><td>Commit</td> <td class="env-value">{{gitVal environment.commit}}</td></tr>
9
9
  {{#if environment.buildUrl}}
10
10
  <tr><td>Build URL</td> <td class="env-value"><a href="{{environment.buildUrl}}" style="color:var(--primary,#CC785C);">{{environment.buildUrl}}</a></td></tr>
11
11
  {{/if}}
@@ -21,11 +21,11 @@
21
21
  <div class="env-grid">
22
22
  <div class="env-item">
23
23
  <span class="env-item__key">Branch</span>
24
- <span class="env-item__val">{{environment.branch}}</span>
24
+ <span class="env-item__val">{{gitVal environment.branch}}</span>
25
25
  </div>
26
26
  <div class="env-item">
27
27
  <span class="env-item__key">Commit</span>
28
- <span class="env-item__val">{{environment.commit}}</span>
28
+ <span class="env-item__val">{{gitVal environment.commit}}</span>
29
29
  </div>
30
30
  <div class="env-item">
31
31
  <span class="env-item__key">Browsers</span>
@@ -38,9 +38,11 @@
38
38
  <div class="cover-page__meta">
39
39
  Generated {{formatDate meta.generatedAt}}
40
40
  </div>
41
+ {{#unless (and (isUnknown environment.branch) (isUnknown environment.commit))}}
41
42
  <div class="cover-page__meta">
42
- Branch: <strong style="color:var(--text-1);">{{environment.branch}}</strong>
43
- &nbsp;·&nbsp;
44
- Commit: <strong style="color:var(--text-1);">{{environment.commit}}</strong>
43
+ {{#unless (isUnknown environment.branch)}}Branch: <strong style="color:var(--text-1);">{{environment.branch}}</strong>{{/unless}}
44
+ {{#unless (or (isUnknown environment.branch) (isUnknown environment.commit))}}&nbsp;·&nbsp;{{/unless}}
45
+ {{#unless (isUnknown environment.commit)}}Commit: <strong style="color:var(--text-1);">{{environment.commit}}</strong>{{/unless}}
45
46
  </div>
47
+ {{/unless}}
46
48
  </div>
@@ -22,12 +22,13 @@
22
22
  </span>
23
23
  </div>
24
24
 
25
- <div class="failure-item__message">{{error.message}}</div>
26
-
27
- {{#if error.stack}}
28
- {{#if ../options.showStackTraces}}
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
29
  <pre>{{error.stack}}</pre>
30
- {{/if}}
30
+ {{else}}
31
+ <div class="failure-item__message">{{error.message}}</div>
31
32
  {{/if}}
32
33
 
33
34
  {{#if screenshotBase64}}
@@ -39,7 +40,7 @@
39
40
  Retry history:&nbsp;
40
41
  {{#each retryHistory}}
41
42
  <span class="retry-attempt retry-attempt--{{status}}">
42
- #{{attempt}} {{status}} · {{formatDuration duration}}
43
+ #{{addOne attempt}} {{status}} · {{formatDuration duration}}
43
44
  </span>
44
45
  {{/each}}
45
46
  </div>
@@ -12,9 +12,8 @@
12
12
  </div>
13
13
  </div>
14
14
  <div class="release-gate__meta">
15
- <span>{{environment.branch}}</span>
16
- <span class="release-gate__sep">&middot;</span>
17
- <span>{{environment.commit}}</span>
15
+ {{#unless (isUnknown environment.branch)}}<span>{{environment.branch}}</span>{{/unless}}
16
+ {{#unless (isUnknown environment.commit)}}<span>{{environment.commit}}</span>{{/unless}}
18
17
  </div>
19
18
  </div>
20
19
  {{else}}
@@ -31,9 +30,8 @@
31
30
  </div>
32
31
  </div>
33
32
  <div class="release-gate__meta">
34
- <span>{{environment.branch}}</span>
35
- <span class="release-gate__sep">&middot;</span>
36
- <span>{{environment.commit}}</span>
33
+ {{#unless (isUnknown environment.branch)}}<span>{{environment.branch}}</span>{{/unless}}
34
+ {{#unless (isUnknown environment.commit)}}<span>{{environment.commit}}</span>{{/unless}}
37
35
  </div>
38
36
  </div>
39
37
  {{else}}
@@ -50,11 +48,9 @@
50
48
  </div>
51
49
  </div>
52
50
  <div class="release-gate__meta">
53
- <span>{{environment.branch}}</span>
54
- <span class="release-gate__sep">&middot;</span>
55
- <span>{{environment.commit}}</span>
51
+ {{#unless (isUnknown environment.branch)}}<span>{{environment.branch}}</span>{{/unless}}
52
+ {{#unless (isUnknown environment.commit)}}<span>{{environment.commit}}</span>{{/unless}}
56
53
  {{#if stats.flaky}}
57
- <span class="release-gate__sep">&middot;</span>
58
54
  <span class="release-gate__flaky-warn">&#9888; {{stats.flaky}} flaky</span>
59
55
  {{/if}}
60
56
  </div>
@@ -13,7 +13,7 @@
13
13
  <div class="suite-group__header">
14
14
  <span class="suite-title">{{title}}</span>
15
15
  <span class="suite-stats">
16
- <span class="badge badge--passed">{{stats.passed}}</span>
16
+ {{#if stats.passed}}<span class="badge badge--passed">{{stats.passed}}</span>{{/if}}
17
17
  {{#if stats.failed}}<span class="badge badge--failed">{{stats.failed}}</span>{{/if}}
18
18
  {{#if stats.timedOut}}<span class="badge badge--timedout">{{stats.timedOut}}</span>{{/if}}
19
19
  {{#if stats.skipped}}<span class="badge badge--skipped">{{stats.skipped}}</span>{{/if}}
@@ -43,6 +43,9 @@ html, body {
43
43
  color: var(--text-1);
44
44
  background: var(--bg);
45
45
  }
46
+ /* Establish a stacking context on the body so the fixed watermark (z-index:-1)
47
+ paints behind all content instead of on top of cards, charts and screenshots. */
48
+ body { position: relative; z-index: 0; }
46
49
 
47
50
  /* ── Typography ─────────────────────────────────────────────────────────── */
48
51
  h1 {
@@ -143,7 +146,7 @@ tr:last-child td { border-bottom: none; }
143
146
  position: fixed; top: 50%; left: 50%;
144
147
  transform: translate(-50%, -50%) rotate(-30deg);
145
148
  font-family: 'Source Serif Pro', Georgia, serif; font-size: 60px; font-weight: 700;
146
- color: rgba(31,31,28,0.04); pointer-events: none; z-index: 0;
149
+ color: rgba(31,31,28,0.04); pointer-events: none; z-index: -1;
147
150
  white-space: nowrap; letter-spacing: 8px; text-transform: uppercase;
148
151
  user-select: none;
149
152
  }
@@ -239,7 +242,10 @@ code {
239
242
  }
240
243
 
241
244
  /* ── Failure-analysis one-liner — shared by detailed + executive templates ── */
242
- .analysis-oneliner { font-size: 0.9em; opacity: 0.9; margin-top: 4px; }
245
+ .analysis-oneliner {
246
+ font-size: 0.9em; opacity: 0.9; margin-top: 4px;
247
+ page-break-after: avoid; break-after: avoid;
248
+ }
243
249
 
244
250
  /* ── Release gate — ship/hold banner, shared by all templates ────────────── */
245
251
  .release-gate {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reportforge/playwright-pdf",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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",