@reportforge/playwright-pdf 0.25.0 → 0.27.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Playwright PDF reporter: generate branded PDF reports from [Playwright Test](https://playwright.dev/) results. Three templates (minimal, detailed, executive) for developers, QA teams, and stakeholders. CI-ready, self-contained, with embedded screenshots and charts.
4
4
 
5
- Drop it into any Playwright project and get a PDF report on every CI run. No changes to your tests.
5
+ Drop it into any Playwright project and get a PDF report on every CI run. No changes to your tests. Testing with Python? The same reports ship as [`reportforge-playwright-pdf` on PyPI](https://pypi.org/project/reportforge-playwright-pdf/) for pytest-playwright, on the same license key (see [Python](#python)).
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/@reportforge/playwright-pdf.svg)](https://www.npmjs.com/package/@reportforge/playwright-pdf)
8
8
  [![Node](https://img.shields.io/node/v/@reportforge/playwright-pdf.svg)](https://nodejs.org/)
@@ -58,6 +58,7 @@ Three templates, one reporter. Every PDF is fully offline: fonts, charts, and sc
58
58
  - **Notifications**: post pass/fail summaries to Slack, Teams, Discord, or email after each run; configurable trigger (`always` / `failure` / `success`) per channel; email and Discord support optional PDF attachment via `attachPdf`
59
59
  - **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)
60
60
  - **Pass-rate trend**: cross-run trend chart from stored history; opt-in `remoteHistory` keeps the trend alive on ephemeral CI runners by storing aggregate numbers only (pass rate, counts, duration, verdict; no test titles or error text, and the branch name never travels in plaintext), with no extra network calls
61
+ - **Run-over-run diff**: every report shows what changed since the previous run on the same branch: new failures, fixed tests, and still-failing tests, and the same summary rides along in Slack, Teams, Discord, and email notifications
61
62
  - **Offline failure analysis**: sorts each failure into one of 7 root-cause buckets and renders a root-cause chip under each failed test in the `detailed` PDF (dominant-cause one-liner in `executive`). No network, no AI service: a small embedded classifier with automatic updates; pin it any time. Full details at [reportforge.org/docs/advanced/failure-analysis](https://reportforge.org/docs/advanced/failure-analysis).
62
63
  - **On-device training**: teach the classifier your team's failures without any data leaving your machine. Label locally-collected samples (`npx @reportforge/playwright-pdf label-feedback`), then `train-model` trains and cross-validates a personal layer on-device; it activates only when it measurably beats the base model on your own labeled set (`evaluate-model` shows the comparison any time). Commit the model file to your repo via `failureAnalysis.localModelPath` to share it with CI. Training data, the trained model, and evaluation are all local files; the analysis code cannot even name a network primitive (enforced by an automated source audit).
63
64
  - `npx @reportforge/playwright-pdf export-feedback`: merges your locally-collected feedback (tokenized + redacted) into one CSV for review or moving between machines. Local file only; there is no upload.
@@ -66,6 +67,7 @@ Three templates, one reporter. Every PDF is fully offline: fonts, charts, and sc
66
67
  - **Executive report brief band**: leads with a plain-language brief (trend + root causes) and one hero pass-rate number instead of a flat KPI grid
67
68
  - **Data protection**: AES-256 password-protected PDFs (no external tools) and opt-in redaction of credentials/PII in report content and live streams
68
69
  - **Hybrid licensing**: one short key unlocks it; reports keep rendering offline via a locally cached license between refreshes
70
+ - **Python support**: the same reports for pytest-playwright via the `reportforge-playwright-pdf` PyPI package; one subscription key covers both ecosystems (live runs and step trees are Node.js-only today)
69
71
  - **Update notice**: when a newer reporter version is published, each run ends with one info line naming it and the update command; no extra network calls
70
72
 
71
73
  ---
@@ -156,6 +158,31 @@ reporter: [['@reportforge/playwright-pdf', {
156
158
 
157
159
  ---
158
160
 
161
+ ## Python
162
+
163
+ The same reports for [pytest-playwright](https://playwright.dev/python/) runs, from the `reportforge-playwright-pdf` PyPI package. One subscription key covers Node.js and Python, and both count against the same machine allowance.
164
+
165
+ ```bash
166
+ pip install reportforge-playwright-pdf
167
+ python -m playwright install chromium
168
+
169
+ export RF_LICENSE_KEY=RFSU-XXXX-XXXX-XXXX-XXXX
170
+ pytest --pdf
171
+ ```
172
+
173
+ Project defaults go in `pyproject.toml` with the same camelCase option names as this reporter:
174
+
175
+ ```toml
176
+ [tool.reportforge]
177
+ enabled = true
178
+ template = "detailed"
179
+ outputFile = "reports/{date}-{branch}-report.pdf"
180
+ ```
181
+
182
+ The plugin reuses the Node runtime and Chromium that Playwright for Python already ships, so there is nothing else to install. pytest-xdist and pytest-rerunfailures are supported; failure screenshots from `--screenshot only-on-failure` are embedded. Live runs and step trees are Node.js-only today. Full guide: [reportforge.org/docs/python](https://reportforge.org/docs/python).
183
+
184
+ ---
185
+
159
186
  ## Pricing
160
187
 
161
188
  | Plan | INR | USD | Includes |
@@ -213,7 +240,7 @@ All options are the second element of the reporter tuple.
213
240
  | `slowTestThreshold` | `number` | `10` | Minimum timed-test count before `SLOW` badges appear in the suite breakdown: below this, every test is trivially the "slowest" so badges stay hidden. On larger runs only genuine outliers (at least 2× the median test duration) are badged, so the badge stays rare. Set to `0` to drop the run-size gate (outliers still required). |
214
241
  | `requirementTagPattern` | `string` | `'^@?[A-Z][A-Z0-9]*-\\d+$'` | Regex deciding which tags count as requirement IDs in the Requirements Traceability section (`detailed` template). Matching tags (ticket shapes like `@ODP-5328`, `REQ-001`) get the traceability matrix; everything else (`@regression`, `@sanity`) collapses into a compact tag summary instead of repeating identical rows. Set to `''` to disable the split and list every tag in the matrix. |
215
242
  | `templatePath` | `string \| string[]` | n/a | 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. |
216
- | `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`, `briefBand`. Display modifiers: `passRate`, `fullEnvironment`, `retries`, `fullFailures`, `stackTraces`. See the Report Sections docs. |
243
+ | `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. 20 keys total. Block toggles: `coverPage`, `analysisOneliner`, `releaseGate`, `summary`, `charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`, `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`, `briefBand`, `runDiff`. Display modifiers: `passRate`, `fullEnvironment`, `retries`, `fullFailures`, `stackTraces`. `runDiff` (block toggle, on by default in all three templates) shows new failures, fixed tests, and still-failing tests compared with the previous run on the same branch. See the Report Sections docs. |
217
244
  <!-- AUTOGEN:options-table END -->
218
245
 
219
246
  Full reference: [reportforge.org/docs/configuration](https://reportforge.org/docs/configuration).
@@ -269,7 +296,9 @@ reporter: [
269
296
 
270
297
  The merge step can run against a dummy test file with zero tests: `shardResults` takes precedence over live results.
271
298
 
272
- > **Note:** Timed-out tests are counted as failures in shard mode (Playwright JSON stats do not distinguish `timedOut` from `failed`).
299
+ Timed-out tests keep their own count in shard mode: the aggregate JSON stats fold them into failures, but the per-test result status carries `timedOut` and the report recovers it for the KPI cards and charts.
300
+
301
+ The merger also honors two optional fields in the JSON report's `config` block when the emitting run provides them: `workers` (shown as the worker count in the environment panel, defaults to 1) and `metadata.playwrightVersion` (shown as the Playwright version when the merge runs where `@playwright/test` is not installed).
273
302
 
274
303
  ### Notifications
275
304
 
@@ -316,7 +345,7 @@ The `on` trigger controls when the message fires:
316
345
 
317
346
  **PDF attachment** (`attachPdf: true`) is available on `email` and `discord`: Slack and Teams webhooks cannot carry file uploads. On Discord the PDF is uploaded with the message; if the upload fails or the PDF exceeds Discord's file-size cap, the summary still posts without the attachment. With multiple templates, the first PDF is attached.
318
347
 
319
- The summary includes pass rate, test counts, duration, and the report filename. Notifications require a valid license and fire after PDF generation (or after a PDF failure; you still get the ping).
348
+ The summary includes pass rate, test counts, duration, and the report filename. When a previous run on the same branch exists, the message also carries a run-over-run diff line: new failures, fixed tests, and tests still failing. Notifications require a valid license and fire after PDF generation (or after a PDF failure; you still get the ping).
320
349
 
321
350
  ### Data Protection
322
351
 
@@ -364,6 +393,8 @@ Full details at [reportforge.org/docs/advanced/security](https://reportforge.org
364
393
 
365
394
  After each run the reporter appends a summary entry to a local JSON file and renders a pass-rate sparkline + verdict row in the `detailed` template.
366
395
 
396
+ **Since Last Run**: every report also gets a section (`runDiff`, on by default in all three templates) comparing the current run against the previous run on the same branch: new failures, fixed tests, and tests still failing, with the same summary line carried into Slack, Teams, Discord, and email notifications. The first run after upgrading has no comparable prior run to diff against, so the section starts appearing from the second run onward. It reads only the local history file, so test names never leave your machine; see the ephemeral-CI note below for keeping that file around between runs. Like the flakiness table, it rides history tracking (`showTrend`, on by default), and the notification line follows history availability rather than the `runDiff` section toggle.
397
+
367
398
  **Local dev (zero config)**: history is written automatically to `~/.reportforge/{projectKey}/history.json`. The sparkline appears once two or more runs have been recorded.
368
399
 
369
400
  **CI with ephemeral runners (recommended)**: enable the server-side trend store; the local file is wiped with the workspace, leaving the chart stuck at one data point:
@@ -375,7 +406,7 @@ reporter: [['@reportforge/playwright-pdf', {
375
406
  }]]
376
407
  ```
377
408
 
378
- One small authenticated request per run, carrying aggregate numbers only (pass rate, counts, duration, verdict; no test titles, no error text, and the branch name never travels in plaintext). The server keeps roughly the last 100 runs per project + branch, expiring after 180 days, and returns them for the chart; any failure falls back to the local file. The flakiness table stays local-only by design (it needs test titles, which never leave your machine). Bitbucket Pipelines users especially: Bitbucket caches are immutable once written, so `remoteHistory` is the practical path there.
409
+ One small authenticated request per run, carrying aggregate numbers only (pass rate, counts, duration, verdict; no test titles, no error text, and the branch name never travels in plaintext). The server keeps roughly the last 100 runs per project + branch, expiring after 180 days, and returns them for the chart; any failure falls back to the local file. This feeds the trend chart only: the flakiness table and the Since Last Run diff both stay local-only by design (they need test titles, which never leave your machine); to keep either alive on ephemeral runners, use the CI caching alternative below to persist `historyFile` instead. Bitbucket Pipelines users especially: Bitbucket caches are immutable once written, so `remoteHistory` is the practical path for the trend chart there.
379
410
 
380
411
  **CI caching alternative** (keeps even aggregate numbers off the server): set `historyFile` to a project-relative path and cache it between runs:
381
412
 
@@ -673,6 +704,7 @@ ReportForge ships a **hybrid offline-first** model:
673
704
  3. Every subsequent run is **fully offline**: the cached license is validated on-device. No network call.
674
705
  4. The reporter refreshes the cache automatically in the background before it expires.
675
706
  5. Each subscription allows up to **25 active machines**. Machines that haven't run in 30 days free up automatically.
707
+ 6. The key is **language-neutral**: the same subscription activates the Node.js reporter and the Python plugin, and a machine running both uses one slot.
676
708
 
677
709
  Cancel the subscription and the reporter keeps working until the end of the current billing period, then stops generating PDFs until you restart. **A license issue never aborts your test run**: the reporter logs a warning and skips PDF generation; your Playwright tests still pass.
678
710
 
@@ -733,6 +765,7 @@ Full guide: [reportforge.org/docs/troubleshooting](https://reportforge.org/docs/
733
765
  Full documentation at [reportforge.org/docs](https://reportforge.org/docs):
734
766
 
735
767
  - [Quick start](https://reportforge.org/docs/quickstart): install, configure, activate
768
+ - [Python](https://reportforge.org/docs/python): the pytest-playwright plugin, same license key
736
769
  - [Chrome setup](https://reportforge.org/docs/running-tests#chrome-setup): Windows, Linux, macOS
737
770
  - [CI/CD integration](https://reportforge.org/docs/ci-cd): GitHub Actions, GitLab CI, Bitbucket Pipelines, Jenkins, Azure DevOps
738
771
  - [Configuration reference](https://reportforge.org/docs/configuration): all options
package/dist/cli/index.js CHANGED
@@ -311,6 +311,24 @@ function buildDemoCharts(passed, failed, skipped, flaky, total) {
311
311
  ]
312
312
  };
313
313
  }
314
+ function buildDemoRunDiff() {
315
+ return {
316
+ newFailures: [
317
+ { id: "auth-5", title: "should block brute-force attempts after 5 tries" },
318
+ { id: "cat-7", title: "should lazy-load images on scroll" }
319
+ ],
320
+ stillFailing: [
321
+ { id: "co-3", title: "should reject expired promo code" }
322
+ ],
323
+ fixed: [
324
+ { id: "co-5", title: "should complete Stripe checkout" }
325
+ ],
326
+ noLongerRun: [],
327
+ baselineRunId: "demo-1d",
328
+ baselineTimestamp: DEMO_BASE_TS - 864e5,
329
+ baselineTruncated: false
330
+ };
331
+ }
314
332
  function buildDemoReportData(template = "detailed") {
315
333
  const projects = [
316
334
  makeProject("chromium", [
@@ -362,7 +380,8 @@ function buildDemoReportData(template = "detailed") {
362
380
  projectCount: 2,
363
381
  workerCount: 4
364
382
  },
365
- charts
383
+ charts,
384
+ runDiff: buildDemoRunDiff()
366
385
  };
367
386
  }
368
387
  var DEMO_BASE_TS, DEMO_LICENSE_INFO, DEMO_AUTH_TESTS, DEMO_CHECKOUT_TESTS, DEMO_CATALOG_TESTS;
@@ -6498,6 +6517,7 @@ var init_sections = __esm({
6498
6517
  "slowTests",
6499
6518
  "defectLog",
6500
6519
  "briefBand",
6520
+ "runDiff",
6501
6521
  // display modifiers
6502
6522
  "passRate",
6503
6523
  "fullEnvironment",
@@ -6521,6 +6541,7 @@ var init_sections = __esm({
6521
6541
  slowTests: false,
6522
6542
  defectLog: false,
6523
6543
  briefBand: false,
6544
+ runDiff: true,
6524
6545
  passRate: true,
6525
6546
  fullEnvironment: false,
6526
6547
  retries: true,
@@ -6542,6 +6563,7 @@ var init_sections = __esm({
6542
6563
  slowTests: true,
6543
6564
  defectLog: true,
6544
6565
  briefBand: false,
6566
+ runDiff: true,
6545
6567
  passRate: true,
6546
6568
  fullEnvironment: true,
6547
6569
  retries: true,
@@ -6568,6 +6590,7 @@ var init_sections = __esm({
6568
6590
  slowTests: true,
6569
6591
  defectLog: false,
6570
6592
  briefBand: true,
6593
+ runDiff: true,
6571
6594
  passRate: true,
6572
6595
  fullEnvironment: false,
6573
6596
  retries: false,
@@ -6929,13 +6952,25 @@ var init_engine = __esm({
6929
6952
  });
6930
6953
 
6931
6954
  // src/screenshots/ScreenshotEmbedder.ts
6932
- var import_fs2, import_sharp, ScreenshotEmbedder;
6955
+ function loadSharp() {
6956
+ if (sharpModule === void 0) {
6957
+ try {
6958
+ sharpModule = require("sharp");
6959
+ } catch {
6960
+ sharpModule = null;
6961
+ logger.warn(
6962
+ "sharp is not available \u2014 screenshots are embedded at original size. Reports with many screenshots may exceed maxFileSizeMb more often."
6963
+ );
6964
+ }
6965
+ }
6966
+ return sharpModule;
6967
+ }
6968
+ var import_fs2, sharpModule, ScreenshotEmbedder;
6933
6969
  var init_ScreenshotEmbedder = __esm({
6934
6970
  "src/screenshots/ScreenshotEmbedder.ts"() {
6935
6971
  "use strict";
6936
6972
  init_cjs_shims();
6937
6973
  import_fs2 = __toESM(require("fs"));
6938
- import_sharp = __toESM(require("sharp"));
6939
6974
  init_logger();
6940
6975
  ScreenshotEmbedder = class {
6941
6976
  async embedAll(failures, opts) {
@@ -6972,7 +7007,11 @@ var init_ScreenshotEmbedder = __esm({
6972
7007
  if (this.looksLikeSvg(buf)) {
6973
7008
  return { buffer: buf, mime: "image/svg+xml" };
6974
7009
  }
6975
- const img = (0, import_sharp.default)(buf, { failOn: "none" });
7010
+ const sharp = loadSharp();
7011
+ if (!sharp) {
7012
+ return { buffer: buf, mime: this.detectMimeType(buf) };
7013
+ }
7014
+ const img = sharp(buf, { failOn: "none" });
6976
7015
  const meta = await img.metadata();
6977
7016
  const needsResize = (meta.width ?? 0) > opts.maxWidth;
6978
7017
  let pipeline = img.rotate();
package/dist/index.d.mts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { Reporter, FullConfig, Suite, TestCase, TestResult, TestStep, FullResult } from '@playwright/test/reporter';
2
2
 
3
3
  /**
4
- * Canonical section catalog. 14 block toggles (section present/absent) followed
4
+ * Canonical section catalog. 15 block toggles (section present/absent) followed
5
5
  * by 5 display modifiers (tweak a section that is on). Single source of truth —
6
6
  * the Zod schema in src/config/schema.ts builds its flag shape from this array.
7
7
  */
8
- declare const SECTION_KEYS: readonly ["coverPage", "analysisOneliner", "releaseGate", "summary", "charts", "trend", "requirementsMatrix", "ciEnvironment", "suiteBreakdown", "failureDeepDive", "failureAnalysis", "slowTests", "defectLog", "briefBand", "passRate", "fullEnvironment", "retries", "fullFailures", "stackTraces"];
8
+ declare const SECTION_KEYS: readonly ["coverPage", "analysisOneliner", "releaseGate", "summary", "charts", "trend", "requirementsMatrix", "ciEnvironment", "suiteBreakdown", "failureDeepDive", "failureAnalysis", "slowTests", "defectLog", "briefBand", "runDiff", "passRate", "fullEnvironment", "retries", "fullFailures", "stackTraces"];
9
9
  type SectionKey = (typeof SECTION_KEYS)[number];
10
10
  /** A partial map of section flags — used both as the flat baseline and per-template. */
11
11
  type SectionFlags = Partial<Record<SectionKey, boolean>>;
@@ -406,12 +406,18 @@ interface ReporterOptions {
406
406
  * every chosen template; per-template keys (`minimal` / `detailed` /
407
407
  * `executive`) override the baseline for that template only.
408
408
  *
409
+ * 20 keys total: 15 block toggles, 5 display modifiers.
410
+ *
409
411
  * Block toggles: `coverPage`, `analysisOneliner`, `releaseGate`, `summary`,
410
412
  * `charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`,
411
- * `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`, `briefBand`.
413
+ * `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`, `briefBand`,
414
+ * `runDiff`.
412
415
  * Display modifiers: `passRate`, `fullEnvironment`, `retries`, `fullFailures`,
413
416
  * `stackTraces`.
414
417
  *
418
+ * `runDiff` shows new failures, fixed tests, and still-failing tests against
419
+ * the previous run on the same branch; on by default in all three templates.
420
+ *
415
421
  * `trend` renders only when `charts` is also on and history data exists (`showTrend` enabled). Sections gated on data
416
422
  * (`failureAnalysis`, `requirementsMatrix`, `slowTests`, `defectLog`) appear
417
423
  * only when matching data exists — this option never changes data collection.
@@ -426,43 +432,6 @@ interface ReporterOptions {
426
432
  sections?: SectionsOption;
427
433
  }
428
434
 
429
- /**
430
- * The resolved license for the current reporter run.
431
- *
432
- * The mere existence of a LicenseInfo means the subscription is active —
433
- * "no license" is represented by `LicenseClient.resolve()` returning null,
434
- * not by a sentinel plan or `valid: false` flag.
435
- *
436
- * `source` surfaces how the license was obtained so the reporter can log
437
- * actionable diagnostics:
438
- * - 'jwt-online' → freshly activated/refreshed against the server
439
- * - 'jwt-cache' → served from ~/.reportforge/license.json (offline)
440
- */
441
- interface LicenseInfo {
442
- plan: LicensePlan;
443
- source: 'jwt-online' | 'jwt-cache' | 'demo';
444
- expiry: Date;
445
- key: string;
446
- /** Raw Ed25519-signed JWT from the server. Absent only in demo mode. */
447
- jwt?: string;
448
- machinesUsed?: number;
449
- machineLimit?: number;
450
- /**
451
- * Entitlement flags carried on the signed JWT (`features` claim). Surfaces
452
- * server-gated capabilities (e.g. `'live'`) to the reporter without a second
453
- * network trip. Undefined for caches written by reporter versions before this
454
- * field was persisted — treat absence as "no extra features".
455
- */
456
- features?: string[];
457
- /**
458
- * Latest published npm version of the reporter, as last reported by the
459
- * license server (activate/refresh piggyback). Absent when the server
460
- * could not reach the registry or the cache predates this field. Used
461
- * only for the non-blocking run-end update notice.
462
- */
463
- latestVersion?: string;
464
- }
465
-
466
435
  /**
467
436
  * PdfReporter — Playwright custom reporter that generates professional PDF reports.
468
437
  *
@@ -520,9 +489,6 @@ declare class PdfReporter implements Reporter {
520
489
  onExit(): Promise<void>;
521
490
  onEnd(result: FullResult): Promise<void>;
522
491
  private _collectData;
523
- private _appendTrend;
524
- private _populateHistoryCharts;
525
- private _buildReportData;
526
492
  printsToStdio(): boolean;
527
493
  /**
528
494
  * Memoized license resolution shared by the live stream (kicked early in
@@ -560,8 +526,43 @@ declare class PdfReporter implements Reporter {
560
526
  * posts one message, not N. The banner still prints from every shard's own log.
561
527
  */
562
528
  private announceLive;
563
- private resolveProjectName;
564
- private openPdf;
529
+ }
530
+
531
+ /**
532
+ * The resolved license for the current reporter run.
533
+ *
534
+ * The mere existence of a LicenseInfo means the subscription is active —
535
+ * "no license" is represented by `LicenseClient.resolve()` returning null,
536
+ * not by a sentinel plan or `valid: false` flag.
537
+ *
538
+ * `source` surfaces how the license was obtained so the reporter can log
539
+ * actionable diagnostics:
540
+ * - 'jwt-online' → freshly activated/refreshed against the server
541
+ * - 'jwt-cache' → served from ~/.reportforge/license.json (offline)
542
+ */
543
+ interface LicenseInfo {
544
+ plan: LicensePlan;
545
+ source: 'jwt-online' | 'jwt-cache' | 'demo';
546
+ expiry: Date;
547
+ key: string;
548
+ /** Raw Ed25519-signed JWT from the server. Absent only in demo mode. */
549
+ jwt?: string;
550
+ machinesUsed?: number;
551
+ machineLimit?: number;
552
+ /**
553
+ * Entitlement flags carried on the signed JWT (`features` claim). Surfaces
554
+ * server-gated capabilities (e.g. `'live'`) to the reporter without a second
555
+ * network trip. Undefined for caches written by reporter versions before this
556
+ * field was persisted — treat absence as "no extra features".
557
+ */
558
+ features?: string[];
559
+ /**
560
+ * Latest published npm version of the reporter, as last reported by the
561
+ * license server (activate/refresh piggyback). Absent when the server
562
+ * could not reach the registry or the cache predates this field. Used
563
+ * only for the non-blocking run-end update notice.
564
+ */
565
+ latestVersion?: string;
565
566
  }
566
567
 
567
568
  /**
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { Reporter, FullConfig, Suite, TestCase, TestResult, TestStep, FullResult } from '@playwright/test/reporter';
2
2
 
3
3
  /**
4
- * Canonical section catalog. 14 block toggles (section present/absent) followed
4
+ * Canonical section catalog. 15 block toggles (section present/absent) followed
5
5
  * by 5 display modifiers (tweak a section that is on). Single source of truth —
6
6
  * the Zod schema in src/config/schema.ts builds its flag shape from this array.
7
7
  */
8
- declare const SECTION_KEYS: readonly ["coverPage", "analysisOneliner", "releaseGate", "summary", "charts", "trend", "requirementsMatrix", "ciEnvironment", "suiteBreakdown", "failureDeepDive", "failureAnalysis", "slowTests", "defectLog", "briefBand", "passRate", "fullEnvironment", "retries", "fullFailures", "stackTraces"];
8
+ declare const SECTION_KEYS: readonly ["coverPage", "analysisOneliner", "releaseGate", "summary", "charts", "trend", "requirementsMatrix", "ciEnvironment", "suiteBreakdown", "failureDeepDive", "failureAnalysis", "slowTests", "defectLog", "briefBand", "runDiff", "passRate", "fullEnvironment", "retries", "fullFailures", "stackTraces"];
9
9
  type SectionKey = (typeof SECTION_KEYS)[number];
10
10
  /** A partial map of section flags — used both as the flat baseline and per-template. */
11
11
  type SectionFlags = Partial<Record<SectionKey, boolean>>;
@@ -406,12 +406,18 @@ interface ReporterOptions {
406
406
  * every chosen template; per-template keys (`minimal` / `detailed` /
407
407
  * `executive`) override the baseline for that template only.
408
408
  *
409
+ * 20 keys total: 15 block toggles, 5 display modifiers.
410
+ *
409
411
  * Block toggles: `coverPage`, `analysisOneliner`, `releaseGate`, `summary`,
410
412
  * `charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`,
411
- * `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`, `briefBand`.
413
+ * `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`, `briefBand`,
414
+ * `runDiff`.
412
415
  * Display modifiers: `passRate`, `fullEnvironment`, `retries`, `fullFailures`,
413
416
  * `stackTraces`.
414
417
  *
418
+ * `runDiff` shows new failures, fixed tests, and still-failing tests against
419
+ * the previous run on the same branch; on by default in all three templates.
420
+ *
415
421
  * `trend` renders only when `charts` is also on and history data exists (`showTrend` enabled). Sections gated on data
416
422
  * (`failureAnalysis`, `requirementsMatrix`, `slowTests`, `defectLog`) appear
417
423
  * only when matching data exists — this option never changes data collection.
@@ -426,43 +432,6 @@ interface ReporterOptions {
426
432
  sections?: SectionsOption;
427
433
  }
428
434
 
429
- /**
430
- * The resolved license for the current reporter run.
431
- *
432
- * The mere existence of a LicenseInfo means the subscription is active —
433
- * "no license" is represented by `LicenseClient.resolve()` returning null,
434
- * not by a sentinel plan or `valid: false` flag.
435
- *
436
- * `source` surfaces how the license was obtained so the reporter can log
437
- * actionable diagnostics:
438
- * - 'jwt-online' → freshly activated/refreshed against the server
439
- * - 'jwt-cache' → served from ~/.reportforge/license.json (offline)
440
- */
441
- interface LicenseInfo {
442
- plan: LicensePlan;
443
- source: 'jwt-online' | 'jwt-cache' | 'demo';
444
- expiry: Date;
445
- key: string;
446
- /** Raw Ed25519-signed JWT from the server. Absent only in demo mode. */
447
- jwt?: string;
448
- machinesUsed?: number;
449
- machineLimit?: number;
450
- /**
451
- * Entitlement flags carried on the signed JWT (`features` claim). Surfaces
452
- * server-gated capabilities (e.g. `'live'`) to the reporter without a second
453
- * network trip. Undefined for caches written by reporter versions before this
454
- * field was persisted — treat absence as "no extra features".
455
- */
456
- features?: string[];
457
- /**
458
- * Latest published npm version of the reporter, as last reported by the
459
- * license server (activate/refresh piggyback). Absent when the server
460
- * could not reach the registry or the cache predates this field. Used
461
- * only for the non-blocking run-end update notice.
462
- */
463
- latestVersion?: string;
464
- }
465
-
466
435
  /**
467
436
  * PdfReporter — Playwright custom reporter that generates professional PDF reports.
468
437
  *
@@ -520,9 +489,6 @@ declare class PdfReporter implements Reporter {
520
489
  onExit(): Promise<void>;
521
490
  onEnd(result: FullResult): Promise<void>;
522
491
  private _collectData;
523
- private _appendTrend;
524
- private _populateHistoryCharts;
525
- private _buildReportData;
526
492
  printsToStdio(): boolean;
527
493
  /**
528
494
  * Memoized license resolution shared by the live stream (kicked early in
@@ -560,8 +526,43 @@ declare class PdfReporter implements Reporter {
560
526
  * posts one message, not N. The banner still prints from every shard's own log.
561
527
  */
562
528
  private announceLive;
563
- private resolveProjectName;
564
- private openPdf;
529
+ }
530
+
531
+ /**
532
+ * The resolved license for the current reporter run.
533
+ *
534
+ * The mere existence of a LicenseInfo means the subscription is active —
535
+ * "no license" is represented by `LicenseClient.resolve()` returning null,
536
+ * not by a sentinel plan or `valid: false` flag.
537
+ *
538
+ * `source` surfaces how the license was obtained so the reporter can log
539
+ * actionable diagnostics:
540
+ * - 'jwt-online' → freshly activated/refreshed against the server
541
+ * - 'jwt-cache' → served from ~/.reportforge/license.json (offline)
542
+ */
543
+ interface LicenseInfo {
544
+ plan: LicensePlan;
545
+ source: 'jwt-online' | 'jwt-cache' | 'demo';
546
+ expiry: Date;
547
+ key: string;
548
+ /** Raw Ed25519-signed JWT from the server. Absent only in demo mode. */
549
+ jwt?: string;
550
+ machinesUsed?: number;
551
+ machineLimit?: number;
552
+ /**
553
+ * Entitlement flags carried on the signed JWT (`features` claim). Surfaces
554
+ * server-gated capabilities (e.g. `'live'`) to the reporter without a second
555
+ * network trip. Undefined for caches written by reporter versions before this
556
+ * field was persisted — treat absence as "no extra features".
557
+ */
558
+ features?: string[];
559
+ /**
560
+ * Latest published npm version of the reporter, as last reported by the
561
+ * license server (activate/refresh piggyback). Absent when the server
562
+ * could not reach the registry or the cache predates this field. Used
563
+ * only for the non-blocking run-end update notice.
564
+ */
565
+ latestVersion?: string;
565
566
  }
566
567
 
567
568
  /**