# @reportforge/playwright-pdf

> 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.

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)).

[![npm](https://img.shields.io/npm/v/@reportforge/playwright-pdf.svg)](https://www.npmjs.com/package/@reportforge/playwright-pdf)
[![Node](https://img.shields.io/node/v/@reportforge/playwright-pdf.svg)](https://nodejs.org/)
[![License](https://img.shields.io/badge/license-Elastic--2.0-blue.svg)](./LICENSE)

<p align="center">
  <a href="https://reportforge.org/pricing">
    <img src="https://img.shields.io/badge/Start%20Free%20Trial-reportforge.org%2Fpricing-CC785C?style=for-the-badge" alt="Start free trial at reportforge.org/pricing" />
  </a><br>
  <sub>7-day trial · ₹999/mo or $12/mo after · 25 machines · all 3 templates · cancel anytime</sub>
</p>

**[Docs](https://reportforge.org/docs)** · **[Pricing](https://reportforge.org/pricing)** · **[Support](mailto:support@reportforge.org)**

---

## Report Gallery

Three templates, one reporter. Every PDF is fully offline: fonts, charts, and screenshots are embedded.

<table>
  <tr>
    <th align="center"><code>minimal</code><br/><sub>Developer / PR checks</sub></th>
    <th align="center"><code>detailed</code><br/><sub>QA team</sub></th>
    <th align="center"><code>executive</code><br/><sub>Stakeholders</sub></th>
  </tr>
  <tr>
    <td><a href="https://reportforge.org/template-minimal.png"><img src="https://reportforge.org/template-minimal.png" alt="minimal template preview" width="300"/></a></td>
    <td><a href="https://reportforge.org/template-detailed.png"><img src="https://reportforge.org/template-detailed.png" alt="detailed template preview" width="300"/></a></td>
    <td><a href="https://reportforge.org/template-executive.png"><img src="https://reportforge.org/template-executive.png" alt="executive template preview" width="300"/></a></td>
  </tr>
  <tr>
    <td align="center"><sub>KPI dashboard · suite breakdown · failure stack traces · embedded screenshots</sub></td>
    <td align="center"><sub>Everything in minimal <strong>+ charts · requirements matrix · defect log · full environment</strong></sub></td>
    <td align="center"><sub>Cover page · plain-language brief · hero pass-rate stat · trend chart · failure summary <em>(no stack traces)</em></sub></td>
  </tr>
</table>

---

## Features

- **3 templates**: `minimal` (developer), `detailed` (QA team), `executive` (stakeholders)
- **Detailed reports**: KPI dashboard, suite breakdown with inline failure detail (error, screenshot, root-cause chip) under each failed test, CI/CD environment
- **Embedded Chart.js**: pass-rate doughnut + per-suite bar chart, bundled inline (no CDN); every suite gets its own bar, and large runs switch to full-width suite charts that continue across pages, so per-suite coverage and failures stay visible run-wide
- **Self-contained PDFs**: screenshots, fonts, logos all embedded; share or archive with confidence
- **Custom branding**: logo, primary/accent colours, watermark, PDF password encryption
- **Dynamic filenames**: `{date}`, `{branch}`, `{status}` tokens in the output path
- **Instant demo report**: `npx @reportforge/playwright-pdf demo` generates a realistic sample PDF before subscribing, no license key required
- **CI/CD snippets**: GitHub Actions, GitLab CI, Bitbucket Pipelines, Jenkins, Azure DevOps
- **Live runs**: opt-in `live` streams per-test progress to an unguessable watch link printed at run start; all shards of one CI run converge on a single live dashboard while tests execute. The PDF is still produced at the end, unchanged
- **Shard merging**: combine N Playwright JSON shard reports into one PDF via `shardResults`; accepts glob patterns or explicit paths
- **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`
- **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)
- **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
- **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
- **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).
- **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).
- `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.
- **Rich execution capture**: opt-in "Steps to Reproduce" outline (a nested, copy-pasteable Markdown list of your `test.step`s and assertions; add `apiSteps` for the full click/fill/check trail), Node console tail, and trace/video links under each failed test in the defect log. Read straight from the Playwright step tree: no fixtures, no test-code changes. Enable via `capture` (`steps` / `apiSteps` / `console` / `evidence`); off by default.
- **Configurable sections**: add or remove any report section per template via `sections`.
- **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
- **Data protection**: AES-256 password-protected PDFs (no external tools) and opt-in redaction of credentials/PII in report content and live streams
- **Hybrid licensing**: one short key unlocks it; reports keep rendering offline via a locally cached license between refreshes
- **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)
- **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

---

## Quick Start

```bash
npm install --save-dev @reportforge/playwright-pdf puppeteer-core
```

```ts
// playwright.config.ts
import { defineConfig } from '@playwright/test';
import { defineReporterConfig } from '@reportforge/playwright-pdf';

export default defineConfig({
  reporter: [
    ['@reportforge/playwright-pdf', defineReporterConfig({
      template: 'detailed',
      outputFile: 'reports/{date}-{branch}-{status}.pdf',
    })],
  ],
  use: { screenshot: 'only-on-failure' },
});
```

`defineReporterConfig` is an optional typed identity helper: it gives you IntelliSense and type-checking on the options object without a separate `ReporterOptions` import. You can also pass a plain object literal if you prefer.

```bash
npx playwright test
# → reports/2026-05-01-main-failed.pdf
```

> **See it first.** Run `npx @reportforge/playwright-pdf demo` to generate a realistic sample PDF in under 60 seconds, no license key required. Supports `--template=minimal|detailed|executive` and `--output=<path>`.

> **License required.** Start a **[7-day free trial at reportforge.org/pricing](https://reportforge.org/pricing)**: card required, no charge until day 8. Paste the resulting `RFSU-…` key into `RF_LICENSE_KEY`. Without a valid license the reporter logs a warning and skips PDF generation; your Playwright tests still run normally. When a newer reporter version exists, runs end with a one-line update notice.

---

## Installation

### Requirements

- Node.js ≥ 20
- `@playwright/test` ≥ 1.40 (peer dependency)
- `puppeteer-core` ≥ 21 < 26 (peer dependency)
- Chrome or Chromium installed on the machine

### Chrome setup

The reporter locates Chrome via the `puppeteerExecutablePath` option first, then the `PUPPETEER_EXECUTABLE_PATH` environment variable, then `chrome-finder` (system Chrome), then puppeteer-core's bundled Chromium if present. Full per-OS instructions: [reportforge.org/docs/running-tests#chrome-setup](https://reportforge.org/docs/running-tests#chrome-setup).

**Windows**

```powershell
winget install Google.Chrome
$env:PUPPETEER_EXECUTABLE_PATH = "C:\Program Files\Google\Chrome\Application\chrome.exe"
```

**Linux (Debian / Ubuntu)**

```bash
wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | \
  sudo gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] \
  http://dl.google.com/linux/chrome/deb/ stable main" | \
  sudo tee /etc/apt/sources.list.d/google-chrome.list
sudo apt-get update -qq && sudo apt-get install -y google-chrome-stable
export PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable
```

> **Note:** Do not use the `chromium-browser` apt package: it installs as a snap on Ubuntu and Puppeteer cannot drive it.

**macOS**

```bash
brew install --cask google-chrome
export PUPPETEER_EXECUTABLE_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
```

Or set the path in `playwright.config.ts`:

```ts
reporter: [['@reportforge/playwright-pdf', {
  puppeteerExecutablePath: '/usr/bin/google-chrome-stable',
}]]
```

---

## Python

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.

```bash
pip install reportforge-playwright-pdf
python -m playwright install chromium

export RF_LICENSE_KEY=RFSU-XXXX-XXXX-XXXX-XXXX
pytest --pdf
```

Project defaults go in `pyproject.toml` with the same camelCase option names as this reporter:

```toml
[tool.reportforge]
enabled = true
template = "detailed"
outputFile = "reports/{date}-{branch}-report.pdf"
```

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).

---

## Pricing

| Plan | INR | USD | Includes |
|---|---|---|---|
| **Monthly** | **₹999 / month** | **$12 / month** | All three templates · custom branding · PDF encryption · up to 25 active machines · priority email support |
| **Yearly** | **₹8,499 / year** | **$99 / year** | Everything in Monthly · ~29-31% saving vs monthly |

Currency is auto-detected by region; toggle on the pricing page.

<p align="center">
  <a href="https://reportforge.org/pricing">
    <img src="https://img.shields.io/badge/Start%20Free%20Trial-reportforge.org%2Fpricing-CC785C?style=for-the-badge" alt="Start free trial" />
  </a><br>
  <sub>7-day trial · no charge until day 8 · cancel anytime from your dashboard</sub>
</p>

---

## Configuration

All options are the second element of the reporter tuple.

<!-- AUTOGEN:options-table START: edit scripts/docs/options-meta.ts, then run npm run docs:gen -->
| Option | Type | Default | Description |
|---|---|---|---|
| `outputFile` | `string` | `'playwright-report/{date}-report.pdf'` | Output path. Supports tokens: `{date}`, `{datetime}`, `{branch}`, `{status}`, `{total}`, `{passed}`, `{failed}`, `{project}`. Directory is created if absent. |
| `template` | `'minimal' \| 'detailed' \| 'executive'` or array | `'minimal'` | Template(s) to generate. Pass an array to produce one PDF per template in one run: the template name is appended to each output filename automatically (e.g. `report-detailed.pdf`). |
| `licenseKey` | `string` | env `RF_LICENSE_KEY` | Subscription key (format `RFSU-…`). Falls back to the `RF_LICENSE_KEY` environment variable. PDF generation is skipped when absent or invalid. |
| `logo` | `string` | n/a | Path to a logo image (PNG, JPG, or SVG) to embed in the report header. Supports absolute and relative paths. |
| `primaryColor` | `string` | `'#CC785C'` | Primary brand colour (3-, 6-, or 8-digit hex). Used for headers and accent bars. |
| `accentColor` | `string` | `'#3F7D58'` | Accent brand colour (hex). Used for highlights and badges. |
| `watermark` | `string` | n/a | Text to render as a diagonal watermark overlay from the Suite Breakdown page onward (summary/chart pages stay clean), e.g. `'CONFIDENTIAL'` or `'DRAFT'`. |
| `pdfPassword` | `string` | n/a | Password-protect the generated PDF with AES-256, applied in-process with no external tools required. Also encrypts the failure-overflow sidecar (`*-failures.json.enc`; decrypt with `npx @reportforge/playwright-pdf decrypt-failures`). Recommended: `process.env.RF_PDF_PASSWORD`. |
| `reportTitle` | `string` | `'Playwright Test Report'` | Custom title for the report cover page and running header. |
| `projectName` | `string` | from `package.json` | Project or application name. Inferred from the `package.json` `name` field if absent. |
| `open` | `boolean` | `false` | Open the generated PDF automatically after generation. For local use only; ignored in CI. |
| `logLevel` | `'silent' \| 'error' \| 'warn' \| 'info' \| 'debug'` | `'info'` | Console verbosity for `[reportforge]` log lines. `'silent'` suppresses everything including errors; `'debug'` adds diagnostics. The `RF_DEBUG=1` env var always forces debug, overriding this option. |
| `puppeteerExecutablePath` | `string` | auto-detect | Full path to a Chrome or Chromium binary. Falls back to `PUPPETEER_EXECUTABLE_PATH` env, then system Chrome discovery. |
| `serverUrl` | `string` | `'https://reportforge.org'` | Base URL for the ReportForge licensing server. Override only for self-hosting or local development. |
| `compressionLevel` | `'auto' \| 'none' \| 'balanced' \| 'max'` | `'auto'` | Screenshot JPEG quality preset. `'auto'` picks based on failure volume; `'none'` keeps original PNGs; `'balanced'` uses JPEG q85; `'max'` uses JPEG q70. |
| `includeScreenshots` | `boolean` | `true` | Embed Playwright screenshots in the PDF. Set to `false` to omit images; useful for exec-audience reports or reducing file size. |
| `maxInlineFailures` | `number` | derived from `compressionLevel` | Cap on failure entries rendered inline in the PDF. Overflow is written to a sibling `{basename}-failures.json` sidecar file. |
| `maxFileSizeMb` | `number` | `8` | Soft cap on the final PDF size in MB. If exceeded, the report is re-rendered once with the `'max'` compression preset. |
| `shardResults` | `string \| string[]` | n/a | Glob or path array of Playwright JSON shard report files to merge into one PDF. See the Shard Merging docs. |
| `notify` | `object` | n/a | Notification channels: `slack`, `teams` (each `{ url, enabled, on }`), `discord` (`{ url, enabled, on, attachPdf }`), `email` (`{ to, enabled, on, attachPdf }`). See the Notifications docs. |
| `failureAnalysis` | `object` | `{ enabled: true }` | Offline failure root-cause analysis (embedded classifier; no runtime network) with on-device personalization: label collected failures, then the `train-model` CLI trains + evaluates a local layer entirely on your machine; it activates only when it beats the base model, and nothing leaves the machine. Fields: `enabled` (default `true`), `maxClusters` (default `10`), `minStrength` (`weak`\|`moderate`\|`strong`, default `weak`), `maxFailuresToAnalyse` (default `500`), `collectUnclassified` (default `true`), `collectScope` (`blind-spots`\|`all`, default `blind-spots`), `autoUpdateModel` (default `true`), `localModel` (use the gate-passed on-device model, default `true`), `localModelPath` (share a team-trained model via a repo-committed file). See the Failure Analysis docs. |
| `capture` | `object` | `{}` (off) | Opt-in rich execution capture for the defect log (reporter-side; no fixtures, no test-code changes). Fields: `steps` (a copy-pasteable "Steps to Reproduce" outline built from the Playwright step tree; each row is a Markdown list line indented under its parent `test.step`, default `false`), `apiSteps` (include every top-level `pw:api` action (click/fill/check/goto) in the outline for a full action trail; without it the outline shows only `test.step` intent + `expect` assertions, default `false`), `console` (Node stdout/stderr tail, default `false`), `evidence` (trace/video file links, default `false`), `maxSteps` (default `50`), `maxConsoleLines` (default `50`). Renders in the Defect Log section of the `detailed` template. |
| `redact` | `object` | `{ enabled: false }` | Opt-in masking of likely credentials/PII (passwords, tokens, API keys, Bearer/JWT values, emails, high-entropy strings) in report text and the live stream. `builtins` (default `true`) toggles the built-in patterns; `patterns` adds custom regexes (fully masked); `mask` sets the replacement text (default `'[REDACTED]'`). Screenshots cannot be redacted; use `includeScreenshots: false` for strict environments. |
| `live` | `object` | `{}` (off) | Opt-in live test-execution streaming. When `enabled`, the reporter prints an unguessable watch link to the CI logs at run start and streams per-test progress to the hosted dashboard while tests run; all shards of one CI run converge on a single live view. Fields: `enabled` (default `false`), `runId` (override the auto-derived run id), `serverUrl` (override the streaming server), `steps` (`none`\|`failed`\|`intent`\|`all`, default `failed`; `intent` shows only your `test.step` names plus any failing step), `console` (stream stdout/stderr tails, default `false`), `flushMs` (batch debounce 500-10000, default `2000`). The PDF at the end of the run is unaffected. Requires an active subscription with the `live` entitlement. See the Live Runs docs. |
| `historyFile` | `string` | `~/.reportforge/{key}/history.json` | Path to the history JSON file. Relative paths resolve from `cwd`. Enables pass-rate trending charts in the `detailed` template. |
| `historySize` | `number` | `10` | Maximum number of test runs to keep in the history file (integer ≥ 2). Older runs are pruned on append. |
| `showTrend` | `boolean` | `true` | Show the pass-rate sparkline and delta badge in the `detailed` template. Set to `false` to disable history tracking entirely. |
| `remoteHistory` | `boolean` | `false` | Opt-in server-side trend store so the pass-rate trend survives ephemeral CI runners (the local history file is wiped with the workspace, leaving the chart stuck at one data point). One small authenticated request per run carrying aggregate numbers only — no test titles, no error text, and the branch name never travels in plaintext. Falls back to the local history file on any failure. See the History docs. |
| `flakinessTopN` | `number` | `5` | Maximum flaky tests to show in the flakiness table (`detailed` template). Set to `0` to disable the table entirely. |
| `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). |
| `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. |
| `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. |
| `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. |
<!-- AUTOGEN:options-table END -->

Full reference: [reportforge.org/docs/configuration](https://reportforge.org/docs/configuration).

### Live Runs

Watch a run as it happens. With `live` enabled, the reporter prints an unguessable watch link (boxed under a `Live Tracker` heading) to your CI logs at run start and streams per-test progress to a hosted dashboard while tests execute. All shards of one CI run converge on a single live view. Tests are grouped by spec file and describe block into collapsible suites, failures first. While a test runs you see its steps settle live under the row; once it finishes, the row expands into the full step tree: your `test.step` names with the Playwright calls and assertions nested beneath them, and on failure the complete error message, code frame, and stack attached to the exact step that failed (Playwright's hooks, fixtures, and teardown are filtered out unless the failure happened inside one). The PDF is still generated at the end, unchanged.

> The watch link is entitlement-gated. If you enable `live` but see no `Live Tracker` line, the reporter logs why (missing `RF_LICENSE_KEY`, or a cached token without the `live` entitlement; delete `~/.reportforge/license.json` to force re-activation).

```typescript
// playwright.config.ts
reporter: [
  ['@reportforge/playwright-pdf', {
    outputFile: 'reports/{date}-report.pdf',
    live: {
      enabled: true,
      steps: 'failed',   // 'none' | 'failed' | 'intent' | 'all'
      // console: false,  // stream stdout/stderr tails (off by default; see note)
      // flushMs: 2000,   // batch debounce (500-10000)
    },
  }],
],
```

`steps: 'intent'` shows only your `test.step` names (plus any failing step): the cleanest, most readable trail. `'all'` adds every action and assertion beneath its parent step; `'failed'` (default) keeps intent + assertions + failures. The mode shapes only the live trail while a test is still running; the full step tree a finished test expands into is always sent.

Sharded runs need no extra config: every shard of one CI run converges on the same live view automatically. Set `RF_LIVE_RUN_ID` to override when your CI is not auto-detected.

Live streaming is best-effort and entitlement-gated: if the server is unreachable the run is unaffected, and an active subscription with the `live` feature is required. If you've configured Slack/Teams/Discord notifications, the watch link is also posted to those channels when the run starts.

> **Security:** the watch link exposes test titles, statuses, step titles, and failure details (error messages, code frames, stacks) to anyone holding it — no login needed, which is the point: share it with whoever should watch. Your `redact` rules apply to everything on the link, error text included. The token is scoped to that one run, and rotating your license key never breaks an already-shared link. Leaving `console: false` is recommended: enabling it streams your tests' stdout/stderr to anyone holding the link. Full details at [reportforge.org/docs/advanced/live](https://reportforge.org/docs/advanced/live).

### Shard Merging

Combine parallel Playwright shards into one PDF without re-running any tests.

**Workflow:**

1. Run each shard with `--reporter=json` to produce a JSON output file.
2. In a separate step, point `shardResults` at those files; it accepts a glob or an explicit array.

```typescript
// playwright.config.ts (merge step)
reporter: [
  ['@reportforge/playwright-pdf', {
    shardResults: 'results/shard-*.json',   // glob
    // or: shardResults: ['results/shard-1.json', 'results/shard-2.json'],
    outputFile: 'reports/{date}-report.pdf',
  }],
],
```

The merge step can run against a dummy test file with zero tests: `shardResults` takes precedence over live results.

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.

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).

### Notifications

Post a summary to Slack, Teams, Discord, or email after each run.

```typescript
notify: {
  slack: {
    url: process.env.SLACK_WEBHOOK_URL,
    enabled: true,
    on: 'failure',               // 'always' | 'failure' | 'success'  (default: 'always')
  },
  teams: {
    url: process.env.TEAMS_WEBHOOK_URL,
    enabled: true,
    on: 'always',
  },
  discord: {
    url: process.env.DISCORD_WEBHOOK_URL,
    enabled: true,
    on: 'failure',
    attachPdf: true,             // upload the PDF with the message (default: false)
  },
  email: {
    to: ['team@company.com', 'qa@company.com'],
    enabled: true,
    on: 'failure',
    attachPdf: false,            // set true to attach the generated PDF
  },
},
```

Each channel is independent. `enabled: false` (the default) lets you store a URL without activating it; useful for staging configs.

The `on` trigger controls when the message fires:

| Value | Sends when |
|---|---|
| `'always'` | Every run |
| `'failure'` | `stats.failed > 0` or run timed out / interrupted |
| `'success'` | All tests passed |

**Email** requires `RESEND_API_KEY` in the environment (get one at [resend.com](https://resend.com)). Sender defaults to `noreply@reportforge.org`; override with `RESEND_FROM`. Store webhook URLs and API keys as CI secrets; never commit them.

**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.

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).

### Data Protection

Two independent, opt-in controls: encrypt the PDF itself, and mask likely secrets before they ever reach report content.

```typescript
// playwright.config.ts
reporter: [
  ['@reportforge/playwright-pdf', {
    pdfPassword: process.env.RF_PDF_PASSWORD,
    redact: {
      enabled: true,
      patterns: ['MYCO_[A-Z0-9]+'],   // custom regexes, fully masked
    },
  }],
],
```

**`pdfPassword`** encrypts the PDF with AES-256, applied in-process with no external tools required. When a failure-overflow sidecar is written (`maxInlineFailures` exceeded), it's encrypted too, as `{basename}-failures.json.enc`:

```bash
npx @reportforge/playwright-pdf decrypt-failures reports/2026-04-28-report-failures.json.enc --password=$RF_PDF_PASSWORD
# or set RF_PDF_PASSWORD and drop --password
```

**`redact`** masks likely credentials and PII in error messages, stack traces, step titles, captured console output, test/suite titles, annotations, failure-analysis text, chart labels, and in the live watch stream. It runs after failure analysis, so classifier accuracy is unaffected. Built-in patterns (`builtins: true`, the default) catch:

| Catches | Kept as-is | Example |
|---|---|---|
| `key=value` / `"key": "value"` credentials (`password`, `secret`, `token`, `apiKey`, `auth`, `credential`, `privateKey`, `accessKey`, `clientSecret`, …) | the key name | `password: "hunter2"` → `password: "[REDACTED]"` |
| `Authorization: Bearer …` / `Basic …` | the scheme word | `Bearer eyJhbGc...` → `Bearer [REDACTED]` |
| JWTs | n/a | `eyJhbGciOi...` |
| Provider key prefixes | n/a | AWS `AKIA…`, GitHub `ghp_`/`gho_`/`github_pat_`, Slack `xox*`, OpenAI-style `sk-`, Razorpay `rzp_live_`/`rzp_test_`, Google `AIza…` |
| URLs with embedded credentials | the username | `https://alice:hunter2@host` → `https://alice:[REDACTED]@host` |
| Emails | first character + domain | `jsmith@example.com` → `j***@example.com` |
| High-entropy strings (20+ mixed letters/digits) | n/a | tokens and keys with no recognizable prefix |

Add your own with `patterns` (strings compiled as case-sensitive regexes with the `g` flag); custom matches are always fully masked. `mask` overrides the default replacement text (`'[REDACTED]'`).

> **Screenshots cannot be redacted:** pixel content is opaque to text-based masking. Set `includeScreenshots: false` in strict environments.

Full details at [reportforge.org/docs/advanced/security](https://reportforge.org/docs/advanced/security).

### Test History Trending

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.

**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.

**Local dev (zero config)**: history is written automatically to `~/.reportforge/{projectKey}/history.json`. The sparkline appears once two or more runs have been recorded.

**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:

```typescript
reporter: [['@reportforge/playwright-pdf', {
  template: 'detailed',
  remoteHistory: true,
}]]
```

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.

**CI caching alternative** (keeps even aggregate numbers off the server): set `historyFile` to a project-relative path and cache it between runs:

```typescript
// playwright.config.ts
reporter: [['@reportforge/playwright-pdf', {
  template: 'detailed',
  historyFile: '.reportforge/history.json',
}]]
```

```yaml
# .github/workflows/test.yml
- uses: actions/cache@v4
  with:
    path: .reportforge/history.json
    key: reportforge-history-${{ github.ref }}
    # Omit restore-keys; cross-branch fallback causes misleading sparklines on PRs
```

**Monorepo**: each package needs a distinct `historyFile` (e.g. `.reportforge/api-history.json`, `.reportforge/web-history.json`) so runs do not overwrite each other.

### Flakiness Trend Table

The `detailed` template includes a **Top flaky tests** table showing which tests flake most often across your stored history. Each row shows the test name, flake rate (%), run count, and a dot sparkline (amber dot = flaky in that run).

```typescript
reporter: [['@reportforge/playwright-pdf', {
  template: 'detailed',
  flakinessTopN: 5,    // default: show top 5; set 0 to disable
}]]
```

The table is gated behind `showTrend: true` (the default) and appears automatically once history entries are present. Runs written before the flakiness feature was added are excluded from the denominator; the table fills in correctly as newer runs accumulate. Set `flakinessTopN: 0` to hide the table entirely.

### Filename tokens

| Token | Replaced with | Example |
|---|---|---|
| `{date}` | `YYYY-MM-DD` (UTC) | `2026-04-21` |
| `{branch}` | Git branch, sanitised | `feature-auth` |
| `{status}` | Overall verdict | `passed` / `failed` / `timedout` / `interrupted` |

---

## Templates

### `minimal`: Developer-focused

Clean layout, KPI numbers, hierarchical test table, failure details with stack traces and embedded screenshots. Fast to generate. Good for local development and PR checks.

### `detailed`: QA Team

Everything in `minimal`, **plus** Chart.js pass-rate + suite-results charts, a numbered defect log, a requirements traceability matrix derived from `@TAG` annotations, and the full CI/CD environment table.

### `executive`: Stakeholder presentations

Full-page cover with verdict + KPI strip, followed by a compact dashboard with charts and a failures summary (titles only, **no raw stack traces**). Best for sprint reports and management dashboards.

### Shared across all three

Every report leads with a **release-gate** ship/hold banner derived from the run verdict, surfaces **timed-out** tests as their own KPI card and pass-rate chart slice, and lists failures **most-severe first** (with severity-coloured card borders in `minimal` and `detailed`). Pages **pack compactly**: large sections flow and break between rows instead of each starting on a fresh page. The Suite Results chart breaks a single-file run down by **describe block** so it never collapses to one bar. Requirements coverage bars are coloured by threshold (red &lt;50%, amber &lt;80%, green ≥80%).

### Generating multiple templates in one run

Pass an array to `template` to generate one PDF per template from a single test run. The template name is appended to the output filename automatically:

```ts
// playwright.config.ts
reporter: [
  ['@reportforge/playwright-pdf', {
    template: ['detailed', 'executive'],
    outputFile: 'reports/{date}-report.pdf',
    licenseKey: process.env.RF_LICENSE_KEY,
  }],
]
```

```
# → reports/2026-04-28-report-detailed.pdf
# → reports/2026-04-28-report-executive.pdf
```

One license check, one data-collection pass: faster than multiple reporter instances. Duplicate entries are silently deduplicated.

### Report Sections

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.

```ts
reporter: [['@reportforge/playwright-pdf', {
  template: ['minimal', 'detailed'],
  sections: {
    defectLog: false,                 // flat key: baseline for EVERY chosen template
    minimal:  { charts: true },       // per-template key: overrides the baseline
    detailed: { ciEnvironment: false },
  },
}]]
```

**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).

#### Block toggles: which sections appear

| Key | Renders |
|---|---|
| `coverPage` | Full-page cover: title, verdict badge, KPI stats strip, branch + commit |
| `analysisOneliner` | One-line failure-analysis summary banner |
| `releaseGate` | Ship/hold recommendation banner (APPROVED TO SHIP / HOLD, or a neutral NO TESTS RAN when the run executed zero tests) |
| `summary` | KPI strip (total · passed · failed · timed-out · skipped · flaky), verdict, duration, pass rate, flaky callout |
| `charts` | Pass-rate doughnut + stacked suite-results bar |
| `trend` | Pass-rate trend line, run-history table, and flakiness table (rendered inside `charts`) |
| `requirementsMatrix` | Requirement-ID tags as a traceability matrix with pass-rate bars, plus a category tag summary |
| `ciEnvironment` | Branch, commit, browsers, CI provider, OS, Node, Playwright, projects, workers |
| `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) |
| `failureDeepDive` | Inline failure detail under each failed test in the breakdown: error message, stack trace, screenshot, retry history |
| `failureAnalysis` | Inline per-test root-cause chip under each failed test (category + representative error + match strength, from the offline classifier) |
| `slowTests` | `SLOW` badge on the duration-ranked slowest tests, shown inline in the breakdown |
| `defectLog` | `DEF-####` numbered failure table (severity, duration) + opt-in repro detail (`capture`) |
| `briefBand` | Executive brief band: one plain-language sentence (trend delta + root causes) and a hero pass-rate stat |

#### Display modifiers: tune a section that's on

| Key | Effect |
|---|---|
| `passRate` | Show the pass-rate % on the Passed KPI card (`summary`) |
| `fullEnvironment` | Full environment table instead of the compact grid (`ciEnvironment`) |
| `retries` | Retries column in the suite breakdown (`suiteBreakdown`) |
| `fullFailures` | Include the screenshot in the inline failure detail (compact templates omit it) (`failureDeepDive`) |
| `stackTraces` | Include `<pre>` stack traces in the inline failure detail (`failureDeepDive`) |

#### Per-template defaults

Omit `sections` entirely and each template renders exactly this (✓ = on):

| Section | minimal | detailed | executive |
|---|:-:|:-:|:-:|
| `coverPage` | – | – | ✓ |
| `analysisOneliner` | – | – | – |
| `releaseGate` | ✓ | ✓ | ✓ |
| `summary` | ✓ | ✓ | ✓ |
| `charts` | – | ✓ | ✓ |
| `trend` | – | ✓ | ✓ |
| `requirementsMatrix` | – | ✓ | – |
| `ciEnvironment` | ✓ | ✓ | ✓ |
| `suiteBreakdown` | ✓ | ✓ | ✓ |
| `failureDeepDive` | ✓ | ✓ | ✓ |
| `failureAnalysis` | – | ✓ | – |
| `slowTests` | – | ✓ | ✓ |
| `defectLog` | – | ✓ | – |
| `briefBand` | – | – | ✓ |
| `passRate` | ✓ | ✓ | ✓ |
| `fullEnvironment` | – | ✓ | – |
| `retries` | ✓ | ✓ | – |
| `fullFailures` | ✓ | ✓ | – |
| `stackTraces` | ✓ | ✓ | – |

#### Dependencies & gotchas

- **`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.
- **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.
- **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.
- **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).
- **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.

---

## CI/CD Integration

Full workflows for all four CI providers: [reportforge.org/docs/ci-cd](https://reportforge.org/docs/ci-cd).

### GitHub Actions

```yaml
- name: Run Playwright tests
  run: npx playwright test
  env:
    RF_LICENSE_KEY: ${{ secrets.RF_LICENSE_KEY }}
    PUPPETEER_EXECUTABLE_PATH: /usr/bin/google-chrome-stable

- name: Upload PDF report
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: playwright-pdf-report
    path: reports/*.pdf
```

### GitHub Actions: Sharded runs

Run shards in parallel with `--reporter=json`, then merge all JSON files into one PDF:

```yaml
jobs:
  test:
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npx playwright test --shard=${{ matrix.shard }}/4 --reporter=json
        env:
          PLAYWRIGHT_JSON_OUTPUT_NAME: results/shard-${{ matrix.shard }}.json
      - uses: actions/upload-artifact@v4
        with:
          name: shard-json-${{ matrix.shard }}
          path: results/shard-${{ matrix.shard }}.json

  merge-pdf:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - uses: actions/download-artifact@v4
        with: { pattern: shard-json-*, merge-multiple: true, path: results }
      - run: npx playwright test --reporter=@reportforge/playwright-pdf tests/dummy.spec.ts
        env:
          RF_LICENSE_KEY: ${{ secrets.RF_LICENSE_KEY }}
          PUPPETEER_EXECUTABLE_PATH: /usr/bin/google-chrome-stable
          SHARD_RESULTS_GLOB: 'results/shard-*.json'
      - uses: actions/upload-artifact@v4
        with: { name: playwright-pdf-report, path: reports/*.pdf }
```

In `playwright.config.ts` set `shardResults: process.env.SHARD_RESULTS_GLOB`. `tests/dummy.spec.ts` can be a single skipped test: live results are ignored when `shardResults` is set.

### GitLab CI

```yaml
playwright-tests:
  image: mcr.microsoft.com/playwright:v1.49.0-jammy
  script:
    - npm ci && npx playwright test
  artifacts:
    paths: [reports/*.pdf]
    when: always
    expire_in: 30 days
```

### Bitbucket Pipelines

```yaml
image: mcr.microsoft.com/playwright:v1.49.0-jammy
pipelines:
  default:
    - step:
        name: Playwright Tests + PDF Report
        caches: [node]
        script:
          - npm ci
          - npx playwright test
        artifacts:
          - playwright-report/*.pdf
```

Set `RF_LICENSE_KEY` in **Repository Settings → Repository variables**.

### Jenkins

```groovy
stage('Playwright Tests') {
  withCredentials([string(credentialsId: 'rf-license', variable: 'RF_LICENSE_KEY')]) {
    sh 'npx playwright test'
  }
  archiveArtifacts artifacts: 'reports/*.pdf', allowEmptyArchive: true
}
```

### Azure DevOps

```yaml
- script: npx playwright test
  env: { RF_LICENSE_KEY: $(RF_LICENSE_KEY) }
- task: PublishPipelineArtifact@1
  inputs:
    targetPath: reports
    artifact: playwright-pdf-report
```

---

## Licensing

ReportForge ships a **hybrid offline-first** model:

1. Paste your `RFSU-…` key into the config once (or set `RF_LICENSE_KEY`).
2. On first run the reporter activates against the license server and caches a signed license at `~/.reportforge/license.json`.
3. Every subsequent run is **fully offline**: the cached license is validated on-device. No network call.
4. The reporter refreshes the cache automatically in the background before it expires.
5. Each subscription allows up to **25 active machines**. Machines that haven't run in 30 days free up automatically.
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.

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.

```bash
export RF_LICENSE_KEY=RFSU-XXXX-XXXX-XXXX-XXXX
npx playwright test
```

Full lifecycle + offline failure modes: [reportforge.org/docs/licensing](https://reportforge.org/docs/licensing).

---

## Requirements Traceability

The `detailed` template generates a requirements matrix from test tags. Tag tests with ticket-shaped IDs to link them to requirements:

```ts
test('user can login', { tag: ['@REQ-101', '@smoke'] }, async ({ page }) => { /* ... */ });
test.describe('payment', { tag: '@PAY-204' }, () => { /* ... */ });
```

Tags matching `requirementTagPattern` (default: ticket shapes like `@REQ-101`, `@ODP-5328`, `JIRA-42`) each get a matrix row — test count, pass/fail counts, and a pass-rate bar — sorted by ID. Category tags (`@smoke`, `@regression`) collapse into a compact tag summary below the matrix instead of repeating one near-identical row per tag. Set `requirementTagPattern: ''` to disable the split and list every tag in the matrix.

---

## Troubleshooting

Set `RF_DEBUG=1` to log the full activation flow and error stack traces:

```bash
RF_DEBUG=1 npx playwright test 2>&1 | grep '\[reportforge\]'
```

### Controlling log output

The `logLevel` option sets how chatty the reporter's `[reportforge]` console lines are:

```ts
['@reportforge/playwright-pdf', { logLevel: 'warn' }]  // warnings and errors only
```

- `'debug'` prints the same diagnostics as `RF_DEBUG=1` — useful in CI where env vars are awkward to set.
- `'silent'` suppresses everything, **including error lines**: a failed PDF then surfaces only as a missing file, because the reporter never fails your test run.
- `RF_DEBUG=1` always forces debug output, overriding `logLevel` — it is the support escape hatch, so "set `RF_DEBUG=1` and send the log" works no matter what the config says.

Common issues:

- **`Chrome/Chromium not found`** → install Google Chrome Stable; don't use the Ubuntu `chromium-browser` snap.
- **Charts blank** → ensure `npm run bundle-chartjs` ran at build; the reporter waits up to 30s for `window.__chartsReady`.
- **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 license cache all skip PDF generation.

Full guide: [reportforge.org/docs/troubleshooting](https://reportforge.org/docs/troubleshooting).

---

## Documentation

Full documentation at [reportforge.org/docs](https://reportforge.org/docs):

- [Quick start](https://reportforge.org/docs/quickstart): install, configure, activate
- [Python](https://reportforge.org/docs/python): the pytest-playwright plugin, same license key
- [Chrome setup](https://reportforge.org/docs/running-tests#chrome-setup): Windows, Linux, macOS
- [CI/CD integration](https://reportforge.org/docs/ci-cd): GitHub Actions, GitLab CI, Bitbucket Pipelines, Jenkins, Azure DevOps
- [Configuration reference](https://reportforge.org/docs/configuration): all options
- [Filename tokens](https://reportforge.org/docs/configuration#tokens): `{date}`, `{branch}`, `{status}`
- [Templates](https://reportforge.org/docs/templates): minimal, detailed, executive
- [Live runs](https://reportforge.org/docs/advanced/live): stream per-test progress to a shared watch link
- [Report Sections](https://reportforge.org/docs/configuration#report-sections): add or remove sections per template
- [Data protection](https://reportforge.org/docs/advanced/security): password-encrypt PDFs and redact credentials/PII from report content
- [License & offline behaviour](https://reportforge.org/docs/licensing): license cache, machine cap, cancellation
- [Troubleshooting](https://reportforge.org/docs/troubleshooting): common issues and debug flags
- [Changelog](https://reportforge.org/changelog): version history and release notes

---

## Contributing

Questions and bug reports: email [support@reportforge.org](mailto:support@reportforge.org).

## Security

Found a vulnerability? Email [support@reportforge.org](mailto:support@reportforge.org) with a subject starting with `[SECURITY]` — please don't open a public issue. Details, scope, and response timelines are in [SECURITY.md](./SECURITY.md); machine-readable contact data is at [reportforge.org/.well-known/security.txt](https://reportforge.org/.well-known/security.txt).

## License

[Elastic License 2.0](./LICENSE). The license does not allow offering the software as a hosted service, bypassing or removing the license-key check, or stripping license or copyright notices. Generating PDF reports requires an active subscription.
