@reportforge/playwright-pdf 0.28.0 → 0.29.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 +9 -4
- package/dist/cli/index.js +4 -0
- package/dist/index.d.mts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +121 -9
- package/dist/index.mjs +121 -9
- package/dist/templates/layouts/detailed.hbs +2 -0
- package/dist/templates/layouts/executive.hbs +2 -0
- package/dist/templates/layouts/minimal.hbs +2 -0
- package/dist/templates/partials/browser-matrix.hbs +34 -0
- package/dist/templates/styles/base.css +24 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,6 +59,7 @@ Three templates, one reporter. Every PDF is fully offline: fonts, charts, and sc
|
|
|
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
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
|
|
62
|
+
- **Cross-browser comparison**: multi-project runs get a per-test matrix across projects in the `detailed` template — divergent outcomes first (fails-only-in-webkit at a glance), identical tests collapsed to a count; works for Node.js multi-project configs and pytest runs with multiple `--browser` values
|
|
62
63
|
- **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).
|
|
63
64
|
- **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).
|
|
64
65
|
- `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.
|
|
@@ -179,7 +180,7 @@ template = "detailed"
|
|
|
179
180
|
outputFile = "reports/{date}-{branch}-report.pdf"
|
|
180
181
|
```
|
|
181
182
|
|
|
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
|
+
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. Passing `--browser` more than once splits the report into one project per browser and unlocks the cross-browser comparison section. Live runs and step trees are Node.js-only today. Full guide: [reportforge.org/docs/python](https://reportforge.org/docs/python).
|
|
183
184
|
|
|
184
185
|
---
|
|
185
186
|
|
|
@@ -194,7 +195,7 @@ set RF_LICENSE_KEY=RFSU-XXXX-XXXX-XXXX-XXXX
|
|
|
194
195
|
dotnet test --logger "reportforge;output=reports/run.pdf;template=detailed"
|
|
195
196
|
```
|
|
196
197
|
|
|
197
|
-
Full options come from a JSON file with the same camelCase names as this reporter: `--logger "reportforge;optionsFile=rf-options.json"`. The logger reuses the Node runtime Microsoft.Playwright places in your test output and a Playwright-managed Chromium, so there is nothing else to install. Retried tests render as flaky with their attempt history; categories become tags.
|
|
198
|
+
Full options come from a JSON file with the same camelCase names as this reporter: `--logger "reportforge;optionsFile=rf-options.json"`. The logger reuses the Node runtime Microsoft.Playwright places in your test output and a Playwright-managed Chromium, so there is nothing else to install. Retried tests render as flaky with their attempt history; categories become tags. A `dotnet test` run drives one browser, so the cross-browser comparison section stays hidden here; live runs and step trees are Node.js-only today. Full guide: [reportforge.org/docs/dotnet](https://reportforge.org/docs/dotnet).
|
|
198
199
|
|
|
199
200
|
---
|
|
200
201
|
|
|
@@ -255,7 +256,7 @@ All options are the second element of the reporter tuple.
|
|
|
255
256
|
| `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). |
|
|
256
257
|
| `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. |
|
|
257
258
|
| `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. |
|
|
258
|
-
| `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.
|
|
259
|
+
| `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. 21 keys total. Block toggles: `coverPage`, `analysisOneliner`, `releaseGate`, `summary`, `charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`, `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`, `briefBand`, `runDiff`, `browserMatrix`. 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. `browserMatrix` (block toggle, on by default in `detailed`) compares per-test outcomes across the run's projects and appears only when the run has 2+ projects. See the Report Sections docs. |
|
|
259
260
|
<!-- AUTOGEN:options-table END -->
|
|
260
261
|
|
|
261
262
|
Full reference: [reportforge.org/docs/configuration](https://reportforge.org/docs/configuration).
|
|
@@ -542,6 +543,8 @@ reporter: [['@reportforge/playwright-pdf', {
|
|
|
542
543
|
| `slowTests` | `SLOW` badge on the duration-ranked slowest tests, shown inline in the breakdown |
|
|
543
544
|
| `defectLog` | `DEF-####` numbered failure table (severity, duration) + opt-in repro detail (`capture`) |
|
|
544
545
|
| `briefBand` | Executive brief band: one plain-language sentence (trend delta + root causes) and a hero pass-rate stat |
|
|
546
|
+
| `runDiff` | Since Last Run: new failures, fixed tests, and still-failing tests vs. the previous run on the same branch |
|
|
547
|
+
| `browserMatrix` | Cross-browser comparison: per-test outcomes side by side across the run's projects — divergent tests first, identical ones collapsed to a count. Appears only when the run has 2+ projects |
|
|
545
548
|
|
|
546
549
|
#### Display modifiers: tune a section that's on
|
|
547
550
|
|
|
@@ -573,6 +576,8 @@ Omit `sections` entirely and each template renders exactly this (✓ = on):
|
|
|
573
576
|
| `slowTests` | – | ✓ | ✓ |
|
|
574
577
|
| `defectLog` | – | ✓ | – |
|
|
575
578
|
| `briefBand` | – | – | ✓ |
|
|
579
|
+
| `runDiff` | ✓ | ✓ | ✓ |
|
|
580
|
+
| `browserMatrix` | – | ✓ | – |
|
|
576
581
|
| `passRate` | ✓ | ✓ | ✓ |
|
|
577
582
|
| `fullEnvironment` | – | ✓ | – |
|
|
578
583
|
| `retries` | ✓ | ✓ | – |
|
|
@@ -583,7 +588,7 @@ Omit `sections` entirely and each template renders exactly this (✓ = on):
|
|
|
583
588
|
|
|
584
589
|
- **`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.
|
|
585
590
|
- **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.
|
|
586
|
-
- **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.
|
|
591
|
+
- **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. `browserMatrix` is data-gated too: it needs 2+ projects in the run (multiple Playwright projects, or `pytest --browser` given twice), so single-browser runs never show it.
|
|
587
592
|
- **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).
|
|
588
593
|
- **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.
|
|
589
594
|
|
package/dist/cli/index.js
CHANGED
|
@@ -6518,6 +6518,7 @@ var init_sections = __esm({
|
|
|
6518
6518
|
"defectLog",
|
|
6519
6519
|
"briefBand",
|
|
6520
6520
|
"runDiff",
|
|
6521
|
+
"browserMatrix",
|
|
6521
6522
|
// display modifiers
|
|
6522
6523
|
"passRate",
|
|
6523
6524
|
"fullEnvironment",
|
|
@@ -6542,6 +6543,7 @@ var init_sections = __esm({
|
|
|
6542
6543
|
defectLog: false,
|
|
6543
6544
|
briefBand: false,
|
|
6544
6545
|
runDiff: true,
|
|
6546
|
+
browserMatrix: false,
|
|
6545
6547
|
passRate: true,
|
|
6546
6548
|
fullEnvironment: false,
|
|
6547
6549
|
retries: true,
|
|
@@ -6564,6 +6566,7 @@ var init_sections = __esm({
|
|
|
6564
6566
|
defectLog: true,
|
|
6565
6567
|
briefBand: false,
|
|
6566
6568
|
runDiff: true,
|
|
6569
|
+
browserMatrix: true,
|
|
6567
6570
|
passRate: true,
|
|
6568
6571
|
fullEnvironment: true,
|
|
6569
6572
|
retries: true,
|
|
@@ -6591,6 +6594,7 @@ var init_sections = __esm({
|
|
|
6591
6594
|
defectLog: false,
|
|
6592
6595
|
briefBand: true,
|
|
6593
6596
|
runDiff: true,
|
|
6597
|
+
browserMatrix: false,
|
|
6594
6598
|
passRate: true,
|
|
6595
6599
|
fullEnvironment: false,
|
|
6596
6600
|
retries: false,
|
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.
|
|
4
|
+
* Canonical section catalog. 16 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", "runDiff", "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", "browserMatrix", "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,18 +406,22 @@ 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
|
-
*
|
|
409
|
+
* 21 keys total: 16 block toggles, 5 display modifiers.
|
|
410
410
|
*
|
|
411
411
|
* Block toggles: `coverPage`, `analysisOneliner`, `releaseGate`, `summary`,
|
|
412
412
|
* `charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`,
|
|
413
413
|
* `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`, `briefBand`,
|
|
414
|
-
* `runDiff`.
|
|
414
|
+
* `runDiff`, `browserMatrix`.
|
|
415
415
|
* Display modifiers: `passRate`, `fullEnvironment`, `retries`, `fullFailures`,
|
|
416
416
|
* `stackTraces`.
|
|
417
417
|
*
|
|
418
418
|
* `runDiff` shows new failures, fixed tests, and still-failing tests against
|
|
419
419
|
* the previous run on the same branch; on by default in all three templates.
|
|
420
420
|
*
|
|
421
|
+
* `browserMatrix` compares per-test outcomes across the run's projects
|
|
422
|
+
* (divergent tests first, identical ones collapsed to a count); on by default
|
|
423
|
+
* in `detailed` and self-hides when the run has fewer than two projects.
|
|
424
|
+
*
|
|
421
425
|
* `trend` renders only when `charts` is also on and history data exists (`showTrend` enabled). Sections gated on data
|
|
422
426
|
* (`failureAnalysis`, `requirementsMatrix`, `slowTests`, `defectLog`) appear
|
|
423
427
|
* only when matching data exists — this option never changes data collection.
|
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.
|
|
4
|
+
* Canonical section catalog. 16 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", "runDiff", "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", "browserMatrix", "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,18 +406,22 @@ 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
|
-
*
|
|
409
|
+
* 21 keys total: 16 block toggles, 5 display modifiers.
|
|
410
410
|
*
|
|
411
411
|
* Block toggles: `coverPage`, `analysisOneliner`, `releaseGate`, `summary`,
|
|
412
412
|
* `charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`,
|
|
413
413
|
* `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`, `briefBand`,
|
|
414
|
-
* `runDiff`.
|
|
414
|
+
* `runDiff`, `browserMatrix`.
|
|
415
415
|
* Display modifiers: `passRate`, `fullEnvironment`, `retries`, `fullFailures`,
|
|
416
416
|
* `stackTraces`.
|
|
417
417
|
*
|
|
418
418
|
* `runDiff` shows new failures, fixed tests, and still-failing tests against
|
|
419
419
|
* the previous run on the same branch; on by default in all three templates.
|
|
420
420
|
*
|
|
421
|
+
* `browserMatrix` compares per-test outcomes across the run's projects
|
|
422
|
+
* (divergent tests first, identical ones collapsed to a count); on by default
|
|
423
|
+
* in `detailed` and self-hides when the run has fewer than two projects.
|
|
424
|
+
*
|
|
421
425
|
* `trend` renders only when `charts` is also on and history data exists (`showTrend` enabled). Sections gated on data
|
|
422
426
|
* (`failureAnalysis`, `requirementsMatrix`, `slowTests`, `defectLog`) appear
|
|
423
427
|
* only when matching data exists — this option never changes data collection.
|
package/dist/index.js
CHANGED
|
@@ -10355,6 +10355,7 @@ var SECTION_KEYS = [
|
|
|
10355
10355
|
"defectLog",
|
|
10356
10356
|
"briefBand",
|
|
10357
10357
|
"runDiff",
|
|
10358
|
+
"browserMatrix",
|
|
10358
10359
|
// display modifiers
|
|
10359
10360
|
"passRate",
|
|
10360
10361
|
"fullEnvironment",
|
|
@@ -10379,6 +10380,7 @@ var SECTION_DEFAULTS = {
|
|
|
10379
10380
|
defectLog: false,
|
|
10380
10381
|
briefBand: false,
|
|
10381
10382
|
runDiff: true,
|
|
10383
|
+
browserMatrix: false,
|
|
10382
10384
|
passRate: true,
|
|
10383
10385
|
fullEnvironment: false,
|
|
10384
10386
|
retries: true,
|
|
@@ -10401,6 +10403,7 @@ var SECTION_DEFAULTS = {
|
|
|
10401
10403
|
defectLog: true,
|
|
10402
10404
|
briefBand: false,
|
|
10403
10405
|
runDiff: true,
|
|
10406
|
+
browserMatrix: true,
|
|
10404
10407
|
passRate: true,
|
|
10405
10408
|
fullEnvironment: true,
|
|
10406
10409
|
retries: true,
|
|
@@ -10428,6 +10431,7 @@ var SECTION_DEFAULTS = {
|
|
|
10428
10431
|
defectLog: false,
|
|
10429
10432
|
briefBand: true,
|
|
10430
10433
|
runDiff: true,
|
|
10434
|
+
browserMatrix: false,
|
|
10431
10435
|
passRate: true,
|
|
10432
10436
|
fullEnvironment: false,
|
|
10433
10437
|
retries: false,
|
|
@@ -11296,7 +11300,7 @@ var isTopLevel = (flat, f) => !f.ancestors.some((a) => {
|
|
|
11296
11300
|
var testStepDepth = (flat, f) => f.ancestors.reduce((n, a) => n + (flat[a].category === "test.step" ? 1 : 0), 0);
|
|
11297
11301
|
function flatten(steps) {
|
|
11298
11302
|
const out = [];
|
|
11299
|
-
const
|
|
11303
|
+
const walk2 = (nodes, depth, ancestors) => {
|
|
11300
11304
|
for (const s of nodes ?? []) {
|
|
11301
11305
|
const index = out.length;
|
|
11302
11306
|
out.push({
|
|
@@ -11306,10 +11310,10 @@ function flatten(steps) {
|
|
|
11306
11310
|
hasError: s.error != null,
|
|
11307
11311
|
ancestors
|
|
11308
11312
|
});
|
|
11309
|
-
if (s.steps?.length)
|
|
11313
|
+
if (s.steps?.length) walk2(s.steps, depth + 1, [...ancestors, index]);
|
|
11310
11314
|
}
|
|
11311
11315
|
};
|
|
11312
|
-
|
|
11316
|
+
walk2(steps ?? [], 0, []);
|
|
11313
11317
|
return out;
|
|
11314
11318
|
}
|
|
11315
11319
|
function materialize(n, isFailing, depth) {
|
|
@@ -11917,7 +11921,7 @@ var ShardMerger = class {
|
|
|
11917
11921
|
merge(filePaths) {
|
|
11918
11922
|
if (filePaths.length === 0) throw new Error("shardResults: no valid JSON shard files found");
|
|
11919
11923
|
const resolved = this.resolveShardPaths(filePaths);
|
|
11920
|
-
const reports = this.loadReports(resolved);
|
|
11924
|
+
const reports = this.loadReports(resolved).map((r) => this.normalizeReport(r));
|
|
11921
11925
|
const projects = this.mergeProjects(reports);
|
|
11922
11926
|
const stats = this.mergeStats(reports, projects);
|
|
11923
11927
|
const failures = sortBySeverity(this.extractFailures(reports));
|
|
@@ -11955,6 +11959,49 @@ var ShardMerger = class {
|
|
|
11955
11959
|
return report;
|
|
11956
11960
|
});
|
|
11957
11961
|
}
|
|
11962
|
+
/**
|
|
11963
|
+
* Two top-level shapes reach the merger. Our language adapters (Python,
|
|
11964
|
+
* .NET) and the engine fixtures emit PROJECT wrapper suites (no `file`,
|
|
11965
|
+
* children are the file suites). Playwright's own --reporter=json emits
|
|
11966
|
+
* FILE suites at top level and hangs the project on each
|
|
11967
|
+
* spec.tests[].projectName instead. Everything downstream (mergeProjects,
|
|
11968
|
+
* extractFailures) assumes the wrapper shape, so real Playwright reports
|
|
11969
|
+
* are pivoted into it here: one wrapper per project, file trees filtered
|
|
11970
|
+
* to that project's tests. An unnamed project ('') renders as 'default',
|
|
11971
|
+
* matching the live reporter path.
|
|
11972
|
+
*/
|
|
11973
|
+
normalizeReport(report) {
|
|
11974
|
+
if (report.suites.every((s) => s.file === void 0)) return report;
|
|
11975
|
+
const wrappers = this.collectProjectNames(report).map((name) => ({
|
|
11976
|
+
title: name === "" ? "default" : name,
|
|
11977
|
+
specs: [],
|
|
11978
|
+
suites: report.suites.map((fileSuite) => this.filterSuiteForProject(fileSuite, name)).filter((s) => s !== null)
|
|
11979
|
+
})).filter((w) => w.suites.length > 0);
|
|
11980
|
+
return { ...report, suites: wrappers };
|
|
11981
|
+
}
|
|
11982
|
+
/** Config order first, then any project discovered only on the tests. */
|
|
11983
|
+
collectProjectNames(report) {
|
|
11984
|
+
const names = [];
|
|
11985
|
+
const push = (n) => {
|
|
11986
|
+
if (!names.includes(n)) names.push(n);
|
|
11987
|
+
};
|
|
11988
|
+
for (const p of report.config?.projects ?? []) push(p.name ?? "");
|
|
11989
|
+
const walk2 = (suite) => {
|
|
11990
|
+
for (const spec of suite.specs ?? []) for (const t of spec.tests ?? []) push(t.projectName ?? "");
|
|
11991
|
+
for (const child of suite.suites ?? []) walk2(child);
|
|
11992
|
+
};
|
|
11993
|
+
for (const s of report.suites) walk2(s);
|
|
11994
|
+
return names.length > 0 ? names : [""];
|
|
11995
|
+
}
|
|
11996
|
+
filterSuiteForProject(suite, project) {
|
|
11997
|
+
const specs = (suite.specs ?? []).map((spec) => ({
|
|
11998
|
+
...spec,
|
|
11999
|
+
tests: (spec.tests ?? []).filter((t) => (t.projectName ?? "") === project)
|
|
12000
|
+
})).filter((spec) => spec.tests.length > 0);
|
|
12001
|
+
const children = (suite.suites ?? []).map((child) => this.filterSuiteForProject(child, project)).filter((c) => c !== null);
|
|
12002
|
+
if (specs.length === 0 && children.length === 0) return null;
|
|
12003
|
+
return { ...suite, specs, suites: children };
|
|
12004
|
+
}
|
|
11958
12005
|
mergeProjects(reports) {
|
|
11959
12006
|
const projectMap = /* @__PURE__ */ new Map();
|
|
11960
12007
|
for (const report of reports) {
|
|
@@ -13735,6 +13782,66 @@ function sourceTally(classified) {
|
|
|
13735
13782
|
return ["rule", "retry", "local", "base"].filter((s) => counts.has(s)).map((s) => `${counts.get(s)} ${s}`).join(" / ");
|
|
13736
13783
|
}
|
|
13737
13784
|
|
|
13785
|
+
// src/collector/BrowserMatrix.ts
|
|
13786
|
+
init_cjs_shims();
|
|
13787
|
+
var DEFAULT_ROW_CAP = 40;
|
|
13788
|
+
function buildBrowserMatrix(projects, cap = DEFAULT_ROW_CAP) {
|
|
13789
|
+
if (projects.length < 2) return void 0;
|
|
13790
|
+
let projectNames = projects.map((p, i) => p.name || `project ${i + 1}`);
|
|
13791
|
+
const byTest = /* @__PURE__ */ new Map();
|
|
13792
|
+
projects.forEach((project, column) => {
|
|
13793
|
+
walk(project.suites, [], (path15, test) => {
|
|
13794
|
+
const key = [...path15, test.title].join(" > ");
|
|
13795
|
+
let statuses = byTest.get(key);
|
|
13796
|
+
if (!statuses) {
|
|
13797
|
+
statuses = projectNames.map(() => null);
|
|
13798
|
+
byTest.set(key, statuses);
|
|
13799
|
+
}
|
|
13800
|
+
statuses[column] = test.status;
|
|
13801
|
+
});
|
|
13802
|
+
});
|
|
13803
|
+
const divergent = [];
|
|
13804
|
+
let identicalCount = 0;
|
|
13805
|
+
const contributing = /* @__PURE__ */ new Set();
|
|
13806
|
+
for (const [title, statuses] of byTest) {
|
|
13807
|
+
const present = statuses.filter((s) => s !== null);
|
|
13808
|
+
if (present.length < 2) continue;
|
|
13809
|
+
statuses.forEach((s, i) => {
|
|
13810
|
+
if (s !== null) contributing.add(i);
|
|
13811
|
+
});
|
|
13812
|
+
if (present.every((s) => s === present[0])) {
|
|
13813
|
+
identicalCount++;
|
|
13814
|
+
} else {
|
|
13815
|
+
divergent.push({ title, statuses });
|
|
13816
|
+
}
|
|
13817
|
+
}
|
|
13818
|
+
if (divergent.length === 0 && identicalCount === 0) return void 0;
|
|
13819
|
+
if (contributing.size < projectNames.length) {
|
|
13820
|
+
const keep = [...contributing].sort((a, b) => a - b);
|
|
13821
|
+
projectNames = keep.map((i) => projectNames[i]);
|
|
13822
|
+
for (const row of divergent) row.statuses = keep.map((i) => row.statuses[i]);
|
|
13823
|
+
}
|
|
13824
|
+
divergent.sort((a, b) => severityRank(b) - severityRank(a) || a.title.localeCompare(b.title));
|
|
13825
|
+
return {
|
|
13826
|
+
projects: projectNames,
|
|
13827
|
+
rows: divergent.slice(0, cap),
|
|
13828
|
+
divergentCount: divergent.length,
|
|
13829
|
+
identicalCount,
|
|
13830
|
+
truncated: divergent.length > cap,
|
|
13831
|
+
hasFailures: divergent.some((r) => severityRank(r) > 0)
|
|
13832
|
+
};
|
|
13833
|
+
}
|
|
13834
|
+
function severityRank(row) {
|
|
13835
|
+
return row.statuses.some((s) => s === "failed" || s === "timedOut") ? 1 : 0;
|
|
13836
|
+
}
|
|
13837
|
+
function walk(suites, path15, visit) {
|
|
13838
|
+
for (const suite of suites) {
|
|
13839
|
+
const next = [...path15, suite.title];
|
|
13840
|
+
for (const test of suite.tests) visit(next, test);
|
|
13841
|
+
walk(suite.suites, next, visit);
|
|
13842
|
+
}
|
|
13843
|
+
}
|
|
13844
|
+
|
|
13738
13845
|
// src/redact/redact-report.ts
|
|
13739
13846
|
init_cjs_shims();
|
|
13740
13847
|
function redactReportData(data, r) {
|
|
@@ -13765,6 +13872,10 @@ function redactReportData(data, r) {
|
|
|
13765
13872
|
data.briefSentence = rd(data.briefSentence);
|
|
13766
13873
|
data.charts.suiteResults.forEach((s) => s.label = r.redactText(s.label));
|
|
13767
13874
|
data.charts.flakinessTable?.forEach((row) => row.title = r.redactText(row.title));
|
|
13875
|
+
if (data.browserMatrix) {
|
|
13876
|
+
data.browserMatrix.projects = data.browserMatrix.projects.map((p) => r.redactText(p));
|
|
13877
|
+
data.browserMatrix.rows.forEach((row) => row.title = r.redactText(row.title));
|
|
13878
|
+
}
|
|
13768
13879
|
data.environment.buildUrl = r.redactText(data.environment.buildUrl);
|
|
13769
13880
|
}
|
|
13770
13881
|
function redactStep(s, r) {
|
|
@@ -14505,7 +14616,8 @@ function buildReportData(collected, licenseInfo, hooks) {
|
|
|
14505
14616
|
failures,
|
|
14506
14617
|
environment,
|
|
14507
14618
|
charts,
|
|
14508
|
-
runDiff
|
|
14619
|
+
runDiff,
|
|
14620
|
+
browserMatrix: buildBrowserMatrix(projects)
|
|
14509
14621
|
};
|
|
14510
14622
|
}
|
|
14511
14623
|
function resolveProjectNameFromPkg(cwd) {
|
|
@@ -14980,7 +15092,7 @@ var MAX_ERR_SNIPPET = 4096;
|
|
|
14980
15092
|
var isHook = (c) => c === "hook" || c === "fixture";
|
|
14981
15093
|
function flatten2(steps) {
|
|
14982
15094
|
const out = [];
|
|
14983
|
-
const
|
|
15095
|
+
const walk2 = (nodes, depth, ancestors) => {
|
|
14984
15096
|
for (const s of nodes ?? []) {
|
|
14985
15097
|
const index = out.length;
|
|
14986
15098
|
out.push({
|
|
@@ -14990,10 +15102,10 @@ function flatten2(steps) {
|
|
|
14990
15102
|
hasError: s.error != null,
|
|
14991
15103
|
ancestors
|
|
14992
15104
|
});
|
|
14993
|
-
if (s.steps?.length)
|
|
15105
|
+
if (s.steps?.length) walk2(s.steps, depth + 1, [...ancestors, index]);
|
|
14994
15106
|
}
|
|
14995
15107
|
};
|
|
14996
|
-
|
|
15108
|
+
walk2(steps ?? [], 0, []);
|
|
14997
15109
|
return out;
|
|
14998
15110
|
}
|
|
14999
15111
|
var inHookSubtree2 = (flat, f) => isHook(f.category) || f.ancestors.some((a) => isHook(flat[a].category));
|
|
@@ -15257,7 +15369,7 @@ var PdfReporter = class {
|
|
|
15257
15369
|
if (this.options.redact.enabled) {
|
|
15258
15370
|
this.redactor = new Redactor(this.options.redact);
|
|
15259
15371
|
}
|
|
15260
|
-
this.version = true ? "0.
|
|
15372
|
+
this.version = true ? "0.29.0" : "0.x";
|
|
15261
15373
|
logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
|
|
15262
15374
|
this.licenseClient = new LicenseClient({
|
|
15263
15375
|
licenseKey: this.options.licenseKey,
|
package/dist/index.mjs
CHANGED
|
@@ -10356,6 +10356,7 @@ var SECTION_KEYS = [
|
|
|
10356
10356
|
"defectLog",
|
|
10357
10357
|
"briefBand",
|
|
10358
10358
|
"runDiff",
|
|
10359
|
+
"browserMatrix",
|
|
10359
10360
|
// display modifiers
|
|
10360
10361
|
"passRate",
|
|
10361
10362
|
"fullEnvironment",
|
|
@@ -10380,6 +10381,7 @@ var SECTION_DEFAULTS = {
|
|
|
10380
10381
|
defectLog: false,
|
|
10381
10382
|
briefBand: false,
|
|
10382
10383
|
runDiff: true,
|
|
10384
|
+
browserMatrix: false,
|
|
10383
10385
|
passRate: true,
|
|
10384
10386
|
fullEnvironment: false,
|
|
10385
10387
|
retries: true,
|
|
@@ -10402,6 +10404,7 @@ var SECTION_DEFAULTS = {
|
|
|
10402
10404
|
defectLog: true,
|
|
10403
10405
|
briefBand: false,
|
|
10404
10406
|
runDiff: true,
|
|
10407
|
+
browserMatrix: true,
|
|
10405
10408
|
passRate: true,
|
|
10406
10409
|
fullEnvironment: true,
|
|
10407
10410
|
retries: true,
|
|
@@ -10429,6 +10432,7 @@ var SECTION_DEFAULTS = {
|
|
|
10429
10432
|
defectLog: false,
|
|
10430
10433
|
briefBand: true,
|
|
10431
10434
|
runDiff: true,
|
|
10435
|
+
browserMatrix: false,
|
|
10432
10436
|
passRate: true,
|
|
10433
10437
|
fullEnvironment: false,
|
|
10434
10438
|
retries: false,
|
|
@@ -11297,7 +11301,7 @@ var isTopLevel = (flat, f) => !f.ancestors.some((a) => {
|
|
|
11297
11301
|
var testStepDepth = (flat, f) => f.ancestors.reduce((n, a) => n + (flat[a].category === "test.step" ? 1 : 0), 0);
|
|
11298
11302
|
function flatten(steps) {
|
|
11299
11303
|
const out = [];
|
|
11300
|
-
const
|
|
11304
|
+
const walk2 = (nodes, depth, ancestors) => {
|
|
11301
11305
|
for (const s of nodes ?? []) {
|
|
11302
11306
|
const index = out.length;
|
|
11303
11307
|
out.push({
|
|
@@ -11307,10 +11311,10 @@ function flatten(steps) {
|
|
|
11307
11311
|
hasError: s.error != null,
|
|
11308
11312
|
ancestors
|
|
11309
11313
|
});
|
|
11310
|
-
if (s.steps?.length)
|
|
11314
|
+
if (s.steps?.length) walk2(s.steps, depth + 1, [...ancestors, index]);
|
|
11311
11315
|
}
|
|
11312
11316
|
};
|
|
11313
|
-
|
|
11317
|
+
walk2(steps ?? [], 0, []);
|
|
11314
11318
|
return out;
|
|
11315
11319
|
}
|
|
11316
11320
|
function materialize(n, isFailing, depth) {
|
|
@@ -11918,7 +11922,7 @@ var ShardMerger = class {
|
|
|
11918
11922
|
merge(filePaths) {
|
|
11919
11923
|
if (filePaths.length === 0) throw new Error("shardResults: no valid JSON shard files found");
|
|
11920
11924
|
const resolved = this.resolveShardPaths(filePaths);
|
|
11921
|
-
const reports = this.loadReports(resolved);
|
|
11925
|
+
const reports = this.loadReports(resolved).map((r) => this.normalizeReport(r));
|
|
11922
11926
|
const projects = this.mergeProjects(reports);
|
|
11923
11927
|
const stats = this.mergeStats(reports, projects);
|
|
11924
11928
|
const failures = sortBySeverity(this.extractFailures(reports));
|
|
@@ -11956,6 +11960,49 @@ var ShardMerger = class {
|
|
|
11956
11960
|
return report;
|
|
11957
11961
|
});
|
|
11958
11962
|
}
|
|
11963
|
+
/**
|
|
11964
|
+
* Two top-level shapes reach the merger. Our language adapters (Python,
|
|
11965
|
+
* .NET) and the engine fixtures emit PROJECT wrapper suites (no `file`,
|
|
11966
|
+
* children are the file suites). Playwright's own --reporter=json emits
|
|
11967
|
+
* FILE suites at top level and hangs the project on each
|
|
11968
|
+
* spec.tests[].projectName instead. Everything downstream (mergeProjects,
|
|
11969
|
+
* extractFailures) assumes the wrapper shape, so real Playwright reports
|
|
11970
|
+
* are pivoted into it here: one wrapper per project, file trees filtered
|
|
11971
|
+
* to that project's tests. An unnamed project ('') renders as 'default',
|
|
11972
|
+
* matching the live reporter path.
|
|
11973
|
+
*/
|
|
11974
|
+
normalizeReport(report) {
|
|
11975
|
+
if (report.suites.every((s) => s.file === void 0)) return report;
|
|
11976
|
+
const wrappers = this.collectProjectNames(report).map((name) => ({
|
|
11977
|
+
title: name === "" ? "default" : name,
|
|
11978
|
+
specs: [],
|
|
11979
|
+
suites: report.suites.map((fileSuite) => this.filterSuiteForProject(fileSuite, name)).filter((s) => s !== null)
|
|
11980
|
+
})).filter((w) => w.suites.length > 0);
|
|
11981
|
+
return { ...report, suites: wrappers };
|
|
11982
|
+
}
|
|
11983
|
+
/** Config order first, then any project discovered only on the tests. */
|
|
11984
|
+
collectProjectNames(report) {
|
|
11985
|
+
const names = [];
|
|
11986
|
+
const push = (n) => {
|
|
11987
|
+
if (!names.includes(n)) names.push(n);
|
|
11988
|
+
};
|
|
11989
|
+
for (const p of report.config?.projects ?? []) push(p.name ?? "");
|
|
11990
|
+
const walk2 = (suite) => {
|
|
11991
|
+
for (const spec of suite.specs ?? []) for (const t of spec.tests ?? []) push(t.projectName ?? "");
|
|
11992
|
+
for (const child of suite.suites ?? []) walk2(child);
|
|
11993
|
+
};
|
|
11994
|
+
for (const s of report.suites) walk2(s);
|
|
11995
|
+
return names.length > 0 ? names : [""];
|
|
11996
|
+
}
|
|
11997
|
+
filterSuiteForProject(suite, project) {
|
|
11998
|
+
const specs = (suite.specs ?? []).map((spec) => ({
|
|
11999
|
+
...spec,
|
|
12000
|
+
tests: (spec.tests ?? []).filter((t) => (t.projectName ?? "") === project)
|
|
12001
|
+
})).filter((spec) => spec.tests.length > 0);
|
|
12002
|
+
const children = (suite.suites ?? []).map((child) => this.filterSuiteForProject(child, project)).filter((c) => c !== null);
|
|
12003
|
+
if (specs.length === 0 && children.length === 0) return null;
|
|
12004
|
+
return { ...suite, specs, suites: children };
|
|
12005
|
+
}
|
|
11959
12006
|
mergeProjects(reports) {
|
|
11960
12007
|
const projectMap = /* @__PURE__ */ new Map();
|
|
11961
12008
|
for (const report of reports) {
|
|
@@ -13736,6 +13783,66 @@ function sourceTally(classified) {
|
|
|
13736
13783
|
return ["rule", "retry", "local", "base"].filter((s) => counts.has(s)).map((s) => `${counts.get(s)} ${s}`).join(" / ");
|
|
13737
13784
|
}
|
|
13738
13785
|
|
|
13786
|
+
// src/collector/BrowserMatrix.ts
|
|
13787
|
+
init_esm_shims();
|
|
13788
|
+
var DEFAULT_ROW_CAP = 40;
|
|
13789
|
+
function buildBrowserMatrix(projects, cap = DEFAULT_ROW_CAP) {
|
|
13790
|
+
if (projects.length < 2) return void 0;
|
|
13791
|
+
let projectNames = projects.map((p, i) => p.name || `project ${i + 1}`);
|
|
13792
|
+
const byTest = /* @__PURE__ */ new Map();
|
|
13793
|
+
projects.forEach((project, column) => {
|
|
13794
|
+
walk(project.suites, [], (path16, test) => {
|
|
13795
|
+
const key = [...path16, test.title].join(" > ");
|
|
13796
|
+
let statuses = byTest.get(key);
|
|
13797
|
+
if (!statuses) {
|
|
13798
|
+
statuses = projectNames.map(() => null);
|
|
13799
|
+
byTest.set(key, statuses);
|
|
13800
|
+
}
|
|
13801
|
+
statuses[column] = test.status;
|
|
13802
|
+
});
|
|
13803
|
+
});
|
|
13804
|
+
const divergent = [];
|
|
13805
|
+
let identicalCount = 0;
|
|
13806
|
+
const contributing = /* @__PURE__ */ new Set();
|
|
13807
|
+
for (const [title, statuses] of byTest) {
|
|
13808
|
+
const present = statuses.filter((s) => s !== null);
|
|
13809
|
+
if (present.length < 2) continue;
|
|
13810
|
+
statuses.forEach((s, i) => {
|
|
13811
|
+
if (s !== null) contributing.add(i);
|
|
13812
|
+
});
|
|
13813
|
+
if (present.every((s) => s === present[0])) {
|
|
13814
|
+
identicalCount++;
|
|
13815
|
+
} else {
|
|
13816
|
+
divergent.push({ title, statuses });
|
|
13817
|
+
}
|
|
13818
|
+
}
|
|
13819
|
+
if (divergent.length === 0 && identicalCount === 0) return void 0;
|
|
13820
|
+
if (contributing.size < projectNames.length) {
|
|
13821
|
+
const keep = [...contributing].sort((a, b) => a - b);
|
|
13822
|
+
projectNames = keep.map((i) => projectNames[i]);
|
|
13823
|
+
for (const row of divergent) row.statuses = keep.map((i) => row.statuses[i]);
|
|
13824
|
+
}
|
|
13825
|
+
divergent.sort((a, b) => severityRank(b) - severityRank(a) || a.title.localeCompare(b.title));
|
|
13826
|
+
return {
|
|
13827
|
+
projects: projectNames,
|
|
13828
|
+
rows: divergent.slice(0, cap),
|
|
13829
|
+
divergentCount: divergent.length,
|
|
13830
|
+
identicalCount,
|
|
13831
|
+
truncated: divergent.length > cap,
|
|
13832
|
+
hasFailures: divergent.some((r) => severityRank(r) > 0)
|
|
13833
|
+
};
|
|
13834
|
+
}
|
|
13835
|
+
function severityRank(row) {
|
|
13836
|
+
return row.statuses.some((s) => s === "failed" || s === "timedOut") ? 1 : 0;
|
|
13837
|
+
}
|
|
13838
|
+
function walk(suites, path16, visit) {
|
|
13839
|
+
for (const suite of suites) {
|
|
13840
|
+
const next = [...path16, suite.title];
|
|
13841
|
+
for (const test of suite.tests) visit(next, test);
|
|
13842
|
+
walk(suite.suites, next, visit);
|
|
13843
|
+
}
|
|
13844
|
+
}
|
|
13845
|
+
|
|
13739
13846
|
// src/redact/redact-report.ts
|
|
13740
13847
|
init_esm_shims();
|
|
13741
13848
|
function redactReportData(data, r) {
|
|
@@ -13766,6 +13873,10 @@ function redactReportData(data, r) {
|
|
|
13766
13873
|
data.briefSentence = rd(data.briefSentence);
|
|
13767
13874
|
data.charts.suiteResults.forEach((s) => s.label = r.redactText(s.label));
|
|
13768
13875
|
data.charts.flakinessTable?.forEach((row) => row.title = r.redactText(row.title));
|
|
13876
|
+
if (data.browserMatrix) {
|
|
13877
|
+
data.browserMatrix.projects = data.browserMatrix.projects.map((p) => r.redactText(p));
|
|
13878
|
+
data.browserMatrix.rows.forEach((row) => row.title = r.redactText(row.title));
|
|
13879
|
+
}
|
|
13769
13880
|
data.environment.buildUrl = r.redactText(data.environment.buildUrl);
|
|
13770
13881
|
}
|
|
13771
13882
|
function redactStep(s, r) {
|
|
@@ -14506,7 +14617,8 @@ function buildReportData(collected, licenseInfo, hooks) {
|
|
|
14506
14617
|
failures,
|
|
14507
14618
|
environment,
|
|
14508
14619
|
charts,
|
|
14509
|
-
runDiff
|
|
14620
|
+
runDiff,
|
|
14621
|
+
browserMatrix: buildBrowserMatrix(projects)
|
|
14510
14622
|
};
|
|
14511
14623
|
}
|
|
14512
14624
|
function resolveProjectNameFromPkg(cwd) {
|
|
@@ -14981,7 +15093,7 @@ var MAX_ERR_SNIPPET = 4096;
|
|
|
14981
15093
|
var isHook = (c) => c === "hook" || c === "fixture";
|
|
14982
15094
|
function flatten2(steps) {
|
|
14983
15095
|
const out = [];
|
|
14984
|
-
const
|
|
15096
|
+
const walk2 = (nodes, depth, ancestors) => {
|
|
14985
15097
|
for (const s of nodes ?? []) {
|
|
14986
15098
|
const index = out.length;
|
|
14987
15099
|
out.push({
|
|
@@ -14991,10 +15103,10 @@ function flatten2(steps) {
|
|
|
14991
15103
|
hasError: s.error != null,
|
|
14992
15104
|
ancestors
|
|
14993
15105
|
});
|
|
14994
|
-
if (s.steps?.length)
|
|
15106
|
+
if (s.steps?.length) walk2(s.steps, depth + 1, [...ancestors, index]);
|
|
14995
15107
|
}
|
|
14996
15108
|
};
|
|
14997
|
-
|
|
15109
|
+
walk2(steps ?? [], 0, []);
|
|
14998
15110
|
return out;
|
|
14999
15111
|
}
|
|
15000
15112
|
var inHookSubtree2 = (flat, f) => isHook(f.category) || f.ancestors.some((a) => isHook(flat[a].category));
|
|
@@ -15258,7 +15370,7 @@ var PdfReporter = class {
|
|
|
15258
15370
|
if (this.options.redact.enabled) {
|
|
15259
15371
|
this.redactor = new Redactor(this.options.redact);
|
|
15260
15372
|
}
|
|
15261
|
-
this.version = true ? "0.
|
|
15373
|
+
this.version = true ? "0.29.0" : "0.x";
|
|
15262
15374
|
logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
|
|
15263
15375
|
this.licenseClient = new LicenseClient({
|
|
15264
15376
|
licenseKey: this.options.licenseKey,
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
{{#if sections.runDiff}}{{> run-diff variant='full'}}{{/if}}
|
|
28
28
|
|
|
29
|
+
{{#if sections.browserMatrix}}{{> browser-matrix}}{{/if}}
|
|
30
|
+
|
|
29
31
|
{{#if sections.requirementsMatrix}}{{> requirements-matrix }}{{/if}}
|
|
30
32
|
|
|
31
33
|
{{#if sections.ciEnvironment}}{{> ci-environment options=(obj showFullEnvironment=sections.fullEnvironment) }}{{/if}}
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
{{#if sections.charts}}{{> charts options=(obj showCharts=true showTrend=sections.trend heroTrend=true) }}{{/if}}
|
|
28
28
|
|
|
29
|
+
{{#if sections.browserMatrix}}{{> browser-matrix}}{{/if}}
|
|
30
|
+
|
|
29
31
|
{{#if sections.requirementsMatrix}}{{> requirements-matrix }}{{/if}}
|
|
30
32
|
|
|
31
33
|
{{#if sections.defectLog}}{{> defect-log }}{{/if}}
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
{{#if sections.charts}}{{> charts options=(obj showCharts=true showTrend=sections.trend) }}{{/if}}
|
|
28
28
|
|
|
29
|
+
{{#if sections.browserMatrix}}{{> browser-matrix}}{{/if}}
|
|
30
|
+
|
|
29
31
|
{{#if sections.requirementsMatrix}}{{> requirements-matrix }}{{/if}}
|
|
30
32
|
|
|
31
33
|
{{#if sections.suiteBreakdown}}{{> suite-breakdown options=(obj showRetries=sections.retries showFullFailures=sections.fullFailures showStackTraces=sections.stackTraces) }}{{/if}}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{{#if browserMatrix}}
|
|
2
|
+
<div class="section section--browser-matrix">
|
|
3
|
+
<h2>Cross-Browser Comparison</h2>
|
|
4
|
+
{{#if browserMatrix.rows.length}}
|
|
5
|
+
<p class="browser-matrix__intro">{{browserMatrix.divergentCount}} {{pluralise browserMatrix.divergentCount "test behaves" "tests behave"}} differently across the run's {{browserMatrix.projects.length}} projects.{{#if browserMatrix.hasFailures}} Failures first.{{/if}}</p>
|
|
6
|
+
<table class="browser-matrix__table">
|
|
7
|
+
<thead>
|
|
8
|
+
<tr>
|
|
9
|
+
<th>Test</th>
|
|
10
|
+
{{#each browserMatrix.projects}}<th class="browser-matrix__col">{{this}}</th>{{/each}}
|
|
11
|
+
</tr>
|
|
12
|
+
</thead>
|
|
13
|
+
<tbody>
|
|
14
|
+
{{#each browserMatrix.rows}}
|
|
15
|
+
<tr class="no-break">
|
|
16
|
+
<td class="browser-matrix__title">{{title}}</td>
|
|
17
|
+
{{#each statuses}}
|
|
18
|
+
<td class="browser-matrix__cell">{{#if this}}{{statusBadge this}}{{else}}<span class="browser-matrix__na">n/a</span>{{/if}}</td>
|
|
19
|
+
{{/each}}
|
|
20
|
+
</tr>
|
|
21
|
+
{{/each}}
|
|
22
|
+
</tbody>
|
|
23
|
+
</table>
|
|
24
|
+
{{#if browserMatrix.truncated}}
|
|
25
|
+
<div class="browser-matrix__note">+ {{subtract browserMatrix.divergentCount browserMatrix.rows.length}} more divergent {{pluralise (subtract browserMatrix.divergentCount browserMatrix.rows.length) "test" "tests"}} not shown</div>
|
|
26
|
+
{{/if}}
|
|
27
|
+
{{#if browserMatrix.identicalCount}}
|
|
28
|
+
<div class="browser-matrix__note">{{browserMatrix.identicalCount}} {{pluralise browserMatrix.identicalCount "test behaves" "tests behave"}} the same in every project.</div>
|
|
29
|
+
{{/if}}
|
|
30
|
+
{{else}}
|
|
31
|
+
<p class="browser-matrix__intro">All {{browserMatrix.identicalCount}} comparable {{pluralise browserMatrix.identicalCount "test behaves" "tests behave"}} the same across the run's {{browserMatrix.projects.length}} projects.</p>
|
|
32
|
+
{{/if}}
|
|
33
|
+
</div>
|
|
34
|
+
{{/if}}
|
|
@@ -954,3 +954,27 @@ code {
|
|
|
954
954
|
/* The aggregate failure-analysis cluster section was retired — root causes now
|
|
955
955
|
render as inline per-test chips in the suite breakdown (.root-cause-chip above).
|
|
956
956
|
The .analysis-oneliner one-liner lives further up and is still used by executive. */
|
|
957
|
+
|
|
958
|
+
/* ── Cross-browser comparison matrix (sections.browserMatrix) ───────────── */
|
|
959
|
+
/* Divergent tests only; identical tests collapse into the note line. Cells
|
|
960
|
+
reuse the .badge--* status palette so the grid reads like the rest of the
|
|
961
|
+
report. Lives here (not a template css) because the section is toggle-able
|
|
962
|
+
across all three layouts. */
|
|
963
|
+
.browser-matrix__intro {
|
|
964
|
+
font-size: 10px; color: var(--text-3); margin-bottom: 12px;
|
|
965
|
+
}
|
|
966
|
+
.browser-matrix__table th.browser-matrix__col {
|
|
967
|
+
width: 88px; text-align: center; text-transform: capitalize;
|
|
968
|
+
}
|
|
969
|
+
.browser-matrix__title {
|
|
970
|
+
font-size: 9.5px; color: var(--text-1); word-break: break-word;
|
|
971
|
+
}
|
|
972
|
+
.browser-matrix__cell { text-align: center; }
|
|
973
|
+
.browser-matrix__cell .badge { min-width: 0; font-size: 7.5px; }
|
|
974
|
+
.browser-matrix__na {
|
|
975
|
+
font-family: 'JetBrains Mono', monospace;
|
|
976
|
+
font-size: 8.5px; color: var(--text-3);
|
|
977
|
+
}
|
|
978
|
+
.browser-matrix__note {
|
|
979
|
+
font-size: 9px; color: var(--text-3); margin-top: 6px;
|
|
980
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reportforge/playwright-pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Playwright Test reporter that generates designed PDF reports: minimal, detailed, and executive templates with CI/CD integrations",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"author": "ReportForge",
|