@reportforge/playwright-pdf 0.29.0 → 0.30.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 +8 -4
- package/dist/cli/index.js +16 -2
- package/dist/index.d.mts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +160 -72
- package/dist/index.mjs +166 -78
- package/dist/templates/partials/charts.hbs +9 -1
- package/dist/templates/partials/defect-log.hbs +2 -2
- package/dist/templates/partials/run-diff.hbs +2 -1
- package/dist/templates/styles/base.css +31 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Three templates, one reporter. Every PDF is fully offline: fonts, charts, and sc
|
|
|
63
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).
|
|
64
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).
|
|
65
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.
|
|
66
|
-
- **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.
|
|
66
|
+
- **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. Set `evidenceUrlTemplate` to turn those trace/video paths into clickable CI artifact links with a QR code beside each one, so even a printed report jumps straight to the trace viewer.
|
|
67
67
|
- **Configurable sections**: add or remove any report section per template via `sections`.
|
|
68
68
|
- **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
|
|
69
69
|
- **Data protection**: AES-256 password-protected PDFs (no external tools) and opt-in redaction of credentials/PII in report content and live streams
|
|
@@ -180,7 +180,7 @@ template = "detailed"
|
|
|
180
180
|
outputFile = "reports/{date}-{branch}-report.pdf"
|
|
181
181
|
```
|
|
182
182
|
|
|
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
|
+
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, and trace/video artifacts from `--tracing` / `--video` feed the defect log's evidence links (pair with `evidenceUrlTemplate` for clickable CI links). 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).
|
|
184
184
|
|
|
185
185
|
---
|
|
186
186
|
|
|
@@ -195,7 +195,7 @@ set RF_LICENSE_KEY=RFSU-XXXX-XXXX-XXXX-XXXX
|
|
|
195
195
|
dotnet test --logger "reportforge;output=reports/run.pdf;template=detailed"
|
|
196
196
|
```
|
|
197
197
|
|
|
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
|
+
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. Trace archives and videos attached via `TestContext.AddTestAttachment` feed the defect log's evidence links (pair with `evidenceUrlTemplate` for clickable CI links). 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).
|
|
199
199
|
|
|
200
200
|
---
|
|
201
201
|
|
|
@@ -253,6 +253,8 @@ All options are the second element of the reporter tuple.
|
|
|
253
253
|
| `showTrend` | `boolean` | `true` | Show the pass-rate sparkline and delta badge in the `detailed` template. Set to `false` to disable history tracking entirely. |
|
|
254
254
|
| `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. |
|
|
255
255
|
| `flakinessTopN` | `number` | `5` | Maximum flaky tests to show in the flakiness table (`detailed` template). Set to `0` to disable the table entirely. |
|
|
256
|
+
| `quarantineThreshold` | `number` | `40` | Flake-rate percentage (0-100) at which a test gets a QUARANTINE chip in the flakiness table, plus a callout with the candidate count. Applies once a test has 3+ history runs with flakiness data. Set to `0` to disable flagging. |
|
|
257
|
+
| `evidenceUrlTemplate` | `string` | n/a | URL template that turns local trace/video artifact paths (from `capture.evidence`) into clickable CI links with a QR code in the defect log. Must contain `{path}`, replaced with the forward-slashed artifact path, e.g. `https://ci.example.com/job/123/artifacts/{path}`. Unset = plain paths as before. |
|
|
256
258
|
| `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). |
|
|
257
259
|
| `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. |
|
|
258
260
|
| `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. |
|
|
@@ -409,7 +411,9 @@ Full details at [reportforge.org/docs/advanced/security](https://reportforge.org
|
|
|
409
411
|
|
|
410
412
|
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.
|
|
411
413
|
|
|
412
|
-
**
|
|
414
|
+
**Flaky intelligence**: the flakiness table (`detailed` template) flags quarantine candidates — tests whose flake rate reaches `quarantineThreshold` (default 40%) across 3+ recorded runs get a QUARANTINE chip and a callout with the candidate count, and every row shows its current streak (consecutive latest runs flaky). Set `quarantineThreshold: 0` to turn flagging off.
|
|
415
|
+
|
|
416
|
+
**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, newly flaky tests, 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.
|
|
413
417
|
|
|
414
418
|
**Local dev (zero config)**: history is written automatically to `~/.reportforge/{projectKey}/history.json`. The sparkline appears once two or more runs have been recorded.
|
|
415
419
|
|
package/dist/cli/index.js
CHANGED
|
@@ -293,20 +293,26 @@ function buildDemoCharts(passed, failed, skipped, flaky, total) {
|
|
|
293
293
|
delta: 71 - 76
|
|
294
294
|
// -5 vs previous run
|
|
295
295
|
},
|
|
296
|
+
flakinessQuarantineTotal: 1,
|
|
297
|
+
flakinessQuarantineThreshold: 40,
|
|
296
298
|
flakinessTable: [
|
|
297
299
|
{
|
|
298
300
|
title: "should handle session timeout gracefully",
|
|
299
301
|
flakeRate: 60,
|
|
300
302
|
flakeCount: 3,
|
|
301
303
|
totalRuns: 5,
|
|
302
|
-
sparkline: [true, false, true, false, true]
|
|
304
|
+
sparkline: [true, false, true, false, true],
|
|
305
|
+
streak: 1,
|
|
306
|
+
quarantine: true
|
|
303
307
|
},
|
|
304
308
|
{
|
|
305
309
|
title: "should apply valid promo code",
|
|
306
310
|
flakeRate: 20,
|
|
307
311
|
flakeCount: 1,
|
|
308
312
|
totalRuns: 5,
|
|
309
|
-
sparkline: [false, false, false, true, false]
|
|
313
|
+
sparkline: [false, false, false, true, false],
|
|
314
|
+
streak: 0,
|
|
315
|
+
quarantine: false
|
|
310
316
|
}
|
|
311
317
|
]
|
|
312
318
|
};
|
|
@@ -320,6 +326,9 @@ function buildDemoRunDiff() {
|
|
|
320
326
|
stillFailing: [
|
|
321
327
|
{ id: "co-3", title: "should reject expired promo code" }
|
|
322
328
|
],
|
|
329
|
+
newlyFlaky: [
|
|
330
|
+
{ id: "auth-4", title: "should handle session timeout gracefully" }
|
|
331
|
+
],
|
|
323
332
|
fixed: [
|
|
324
333
|
{ id: "co-5", title: "should complete Stripe checkout" }
|
|
325
334
|
],
|
|
@@ -6381,6 +6390,7 @@ var init_defaults = __esm({
|
|
|
6381
6390
|
historySize: 10,
|
|
6382
6391
|
showTrend: true,
|
|
6383
6392
|
flakinessTopN: 5,
|
|
6393
|
+
quarantineThreshold: 40,
|
|
6384
6394
|
slowTestThreshold: 10,
|
|
6385
6395
|
// Ticket-ID shape: @ODP-5328, REQ-001, JIRA-123. Tags matching this land in
|
|
6386
6396
|
// the Requirements Traceability matrix; the rest fall to the tag summary.
|
|
@@ -12250,6 +12260,10 @@ var init_schema = __esm({
|
|
|
12250
12260
|
// local history file is wiped every run. Aggregate numbers only.
|
|
12251
12261
|
remoteHistory: external_exports.boolean().default(false),
|
|
12252
12262
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
12263
|
+
quarantineThreshold: external_exports.number().int().min(0, "quarantineThreshold must be between 0 and 100").max(100, "quarantineThreshold must be between 0 and 100").optional().default(DEFAULT_OPTIONS.quarantineThreshold),
|
|
12264
|
+
evidenceUrlTemplate: external_exports.string().refine((s) => s.includes("{path}"), {
|
|
12265
|
+
message: "evidenceUrlTemplate must contain the {path} token"
|
|
12266
|
+
}).optional(),
|
|
12253
12267
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
12254
12268
|
requirementTagPattern: external_exports.string().refine((v) => {
|
|
12255
12269
|
try {
|
package/dist/index.d.mts
CHANGED
|
@@ -368,6 +368,24 @@ interface ReporterOptions {
|
|
|
368
368
|
* @default 5
|
|
369
369
|
*/
|
|
370
370
|
flakinessTopN?: number;
|
|
371
|
+
/**
|
|
372
|
+
* Flake-rate percentage (0-100) at which a test is flagged as a quarantine
|
|
373
|
+
* candidate in the flakiness table: a QUARANTINE chip on the row plus a
|
|
374
|
+
* callout with the candidate count. A test qualifies once its flake rate
|
|
375
|
+
* over the stored history reaches the threshold across at least 3 runs
|
|
376
|
+
* that carry flakiness data. Set to 0 to disable flagging.
|
|
377
|
+
* @default 40
|
|
378
|
+
*/
|
|
379
|
+
quarantineThreshold?: number;
|
|
380
|
+
/**
|
|
381
|
+
* URL template that turns local trace/video artifact paths (surfaced by
|
|
382
|
+
* `capture.evidence`) into clickable CI links with a QR code in the defect
|
|
383
|
+
* log. Must contain the `{path}` token, replaced with the forward-slashed
|
|
384
|
+
* artifact path (each segment percent-encoded), e.g.
|
|
385
|
+
* `https://ci.example.com/job/123/artifacts/{path}`.
|
|
386
|
+
* Unset = evidence renders as plain paths, exactly as before.
|
|
387
|
+
*/
|
|
388
|
+
evidenceUrlTemplate?: string;
|
|
371
389
|
/**
|
|
372
390
|
* Minimum number of timed tests before `SLOW` badges appear in the suite
|
|
373
391
|
* breakdown — below this, every test is trivially the "slowest", so badges
|
package/dist/index.d.ts
CHANGED
|
@@ -368,6 +368,24 @@ interface ReporterOptions {
|
|
|
368
368
|
* @default 5
|
|
369
369
|
*/
|
|
370
370
|
flakinessTopN?: number;
|
|
371
|
+
/**
|
|
372
|
+
* Flake-rate percentage (0-100) at which a test is flagged as a quarantine
|
|
373
|
+
* candidate in the flakiness table: a QUARANTINE chip on the row plus a
|
|
374
|
+
* callout with the candidate count. A test qualifies once its flake rate
|
|
375
|
+
* over the stored history reaches the threshold across at least 3 runs
|
|
376
|
+
* that carry flakiness data. Set to 0 to disable flagging.
|
|
377
|
+
* @default 40
|
|
378
|
+
*/
|
|
379
|
+
quarantineThreshold?: number;
|
|
380
|
+
/**
|
|
381
|
+
* URL template that turns local trace/video artifact paths (surfaced by
|
|
382
|
+
* `capture.evidence`) into clickable CI links with a QR code in the defect
|
|
383
|
+
* log. Must contain the `{path}` token, replaced with the forward-slashed
|
|
384
|
+
* artifact path (each segment percent-encoded), e.g.
|
|
385
|
+
* `https://ci.example.com/job/123/artifacts/{path}`.
|
|
386
|
+
* Unset = evidence renders as plain paths, exactly as before.
|
|
387
|
+
*/
|
|
388
|
+
evidenceUrlTemplate?: string;
|
|
371
389
|
/**
|
|
372
390
|
* Minimum number of timed tests before `SLOW` badges appear in the suite
|
|
373
391
|
* breakdown — below this, every test is trivially the "slowest", so badges
|
package/dist/index.js
CHANGED
|
@@ -1221,13 +1221,13 @@ var require_ast = __commonJS({
|
|
|
1221
1221
|
helperExpression: function helperExpression(node) {
|
|
1222
1222
|
return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
|
|
1223
1223
|
},
|
|
1224
|
-
scopedId: function scopedId(
|
|
1225
|
-
return /^\.|this\b/.test(
|
|
1224
|
+
scopedId: function scopedId(path16) {
|
|
1225
|
+
return /^\.|this\b/.test(path16.original);
|
|
1226
1226
|
},
|
|
1227
1227
|
// an ID is simple if it only has one part, and that part is not
|
|
1228
1228
|
// `..` or `this`.
|
|
1229
|
-
simpleId: function simpleId(
|
|
1230
|
-
return
|
|
1229
|
+
simpleId: function simpleId(path16) {
|
|
1230
|
+
return path16.parts.length === 1 && !AST.helpers.scopedId(path16) && !path16.depth;
|
|
1231
1231
|
}
|
|
1232
1232
|
}
|
|
1233
1233
|
};
|
|
@@ -2301,12 +2301,12 @@ var require_helpers2 = __commonJS({
|
|
|
2301
2301
|
loc
|
|
2302
2302
|
};
|
|
2303
2303
|
}
|
|
2304
|
-
function prepareMustache(
|
|
2304
|
+
function prepareMustache(path16, params, hash, open, strip, locInfo) {
|
|
2305
2305
|
var escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== "{" && escapeFlag !== "&";
|
|
2306
2306
|
var decorator = /\*/.test(open);
|
|
2307
2307
|
return {
|
|
2308
2308
|
type: decorator ? "Decorator" : "MustacheStatement",
|
|
2309
|
-
path:
|
|
2309
|
+
path: path16,
|
|
2310
2310
|
params,
|
|
2311
2311
|
hash,
|
|
2312
2312
|
escaped,
|
|
@@ -2626,9 +2626,9 @@ var require_compiler = __commonJS({
|
|
|
2626
2626
|
},
|
|
2627
2627
|
DecoratorBlock: function DecoratorBlock(decorator) {
|
|
2628
2628
|
var program = decorator.program && this.compileProgram(decorator.program);
|
|
2629
|
-
var params = this.setupFullMustacheParams(decorator, program, void 0),
|
|
2629
|
+
var params = this.setupFullMustacheParams(decorator, program, void 0), path16 = decorator.path;
|
|
2630
2630
|
this.useDecorators = true;
|
|
2631
|
-
this.opcode("registerDecorator", params.length,
|
|
2631
|
+
this.opcode("registerDecorator", params.length, path16.original);
|
|
2632
2632
|
},
|
|
2633
2633
|
PartialStatement: function PartialStatement(partial) {
|
|
2634
2634
|
this.usePartial = true;
|
|
@@ -2692,46 +2692,46 @@ var require_compiler = __commonJS({
|
|
|
2692
2692
|
}
|
|
2693
2693
|
},
|
|
2694
2694
|
ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) {
|
|
2695
|
-
var
|
|
2696
|
-
this.opcode("getContext",
|
|
2695
|
+
var path16 = sexpr.path, name = path16.parts[0], isBlock = program != null || inverse != null;
|
|
2696
|
+
this.opcode("getContext", path16.depth);
|
|
2697
2697
|
this.opcode("pushProgram", program);
|
|
2698
2698
|
this.opcode("pushProgram", inverse);
|
|
2699
|
-
|
|
2700
|
-
this.accept(
|
|
2699
|
+
path16.strict = true;
|
|
2700
|
+
this.accept(path16);
|
|
2701
2701
|
this.opcode("invokeAmbiguous", name, isBlock);
|
|
2702
2702
|
},
|
|
2703
2703
|
simpleSexpr: function simpleSexpr(sexpr) {
|
|
2704
|
-
var
|
|
2705
|
-
|
|
2706
|
-
this.accept(
|
|
2704
|
+
var path16 = sexpr.path;
|
|
2705
|
+
path16.strict = true;
|
|
2706
|
+
this.accept(path16);
|
|
2707
2707
|
this.opcode("resolvePossibleLambda");
|
|
2708
2708
|
},
|
|
2709
2709
|
helperSexpr: function helperSexpr(sexpr, program, inverse) {
|
|
2710
|
-
var params = this.setupFullMustacheParams(sexpr, program, inverse),
|
|
2710
|
+
var params = this.setupFullMustacheParams(sexpr, program, inverse), path16 = sexpr.path, name = path16.parts[0];
|
|
2711
2711
|
if (this.options.knownHelpers[name]) {
|
|
2712
2712
|
this.opcode("invokeKnownHelper", params.length, name);
|
|
2713
2713
|
} else if (this.options.knownHelpersOnly) {
|
|
2714
2714
|
throw new _exception2["default"]("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
|
|
2715
2715
|
} else {
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
this.accept(
|
|
2719
|
-
this.opcode("invokeHelper", params.length,
|
|
2716
|
+
path16.strict = true;
|
|
2717
|
+
path16.falsy = true;
|
|
2718
|
+
this.accept(path16);
|
|
2719
|
+
this.opcode("invokeHelper", params.length, path16.original, _ast2["default"].helpers.simpleId(path16));
|
|
2720
2720
|
}
|
|
2721
2721
|
},
|
|
2722
|
-
PathExpression: function PathExpression(
|
|
2723
|
-
this.addDepth(
|
|
2724
|
-
this.opcode("getContext",
|
|
2725
|
-
var name =
|
|
2722
|
+
PathExpression: function PathExpression(path16) {
|
|
2723
|
+
this.addDepth(path16.depth);
|
|
2724
|
+
this.opcode("getContext", path16.depth);
|
|
2725
|
+
var name = path16.parts[0], scoped = _ast2["default"].helpers.scopedId(path16), blockParamId = !path16.depth && !scoped && this.blockParamIndex(name);
|
|
2726
2726
|
if (blockParamId) {
|
|
2727
|
-
this.opcode("lookupBlockParam", blockParamId,
|
|
2727
|
+
this.opcode("lookupBlockParam", blockParamId, path16.parts);
|
|
2728
2728
|
} else if (!name) {
|
|
2729
2729
|
this.opcode("pushContext");
|
|
2730
|
-
} else if (
|
|
2730
|
+
} else if (path16.data) {
|
|
2731
2731
|
this.options.data = true;
|
|
2732
|
-
this.opcode("lookupData",
|
|
2732
|
+
this.opcode("lookupData", path16.depth, path16.parts, path16.strict);
|
|
2733
2733
|
} else {
|
|
2734
|
-
this.opcode("lookupOnContext",
|
|
2734
|
+
this.opcode("lookupOnContext", path16.parts, path16.falsy, path16.strict, scoped);
|
|
2735
2735
|
}
|
|
2736
2736
|
},
|
|
2737
2737
|
StringLiteral: function StringLiteral(string) {
|
|
@@ -3087,16 +3087,16 @@ var require_util = __commonJS({
|
|
|
3087
3087
|
}
|
|
3088
3088
|
exports2.urlGenerate = urlGenerate;
|
|
3089
3089
|
function normalize(aPath) {
|
|
3090
|
-
var
|
|
3090
|
+
var path16 = aPath;
|
|
3091
3091
|
var url = urlParse(aPath);
|
|
3092
3092
|
if (url) {
|
|
3093
3093
|
if (!url.path) {
|
|
3094
3094
|
return aPath;
|
|
3095
3095
|
}
|
|
3096
|
-
|
|
3096
|
+
path16 = url.path;
|
|
3097
3097
|
}
|
|
3098
|
-
var isAbsolute = exports2.isAbsolute(
|
|
3099
|
-
var parts =
|
|
3098
|
+
var isAbsolute = exports2.isAbsolute(path16);
|
|
3099
|
+
var parts = path16.split(/\/+/);
|
|
3100
3100
|
for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
|
|
3101
3101
|
part = parts[i];
|
|
3102
3102
|
if (part === ".") {
|
|
@@ -3113,15 +3113,15 @@ var require_util = __commonJS({
|
|
|
3113
3113
|
}
|
|
3114
3114
|
}
|
|
3115
3115
|
}
|
|
3116
|
-
|
|
3117
|
-
if (
|
|
3118
|
-
|
|
3116
|
+
path16 = parts.join("/");
|
|
3117
|
+
if (path16 === "") {
|
|
3118
|
+
path16 = isAbsolute ? "/" : ".";
|
|
3119
3119
|
}
|
|
3120
3120
|
if (url) {
|
|
3121
|
-
url.path =
|
|
3121
|
+
url.path = path16;
|
|
3122
3122
|
return urlGenerate(url);
|
|
3123
3123
|
}
|
|
3124
|
-
return
|
|
3124
|
+
return path16;
|
|
3125
3125
|
}
|
|
3126
3126
|
exports2.normalize = normalize;
|
|
3127
3127
|
function join(aRoot, aPath) {
|
|
@@ -5924,8 +5924,8 @@ var require_printer = __commonJS({
|
|
|
5924
5924
|
return this.accept(sexpr.path) + " " + params + hash;
|
|
5925
5925
|
};
|
|
5926
5926
|
PrintVisitor.prototype.PathExpression = function(id) {
|
|
5927
|
-
var
|
|
5928
|
-
return (id.data ? "@" : "") + "PATH:" +
|
|
5927
|
+
var path16 = id.parts.join("/");
|
|
5928
|
+
return (id.data ? "@" : "") + "PATH:" + path16;
|
|
5929
5929
|
};
|
|
5930
5930
|
PrintVisitor.prototype.StringLiteral = function(string) {
|
|
5931
5931
|
return '"' + string.value + '"';
|
|
@@ -6045,7 +6045,7 @@ var require_win32 = __commonJS({
|
|
|
6045
6045
|
"node_modules/chrome-finder/lib/win32.js"(exports2, module2) {
|
|
6046
6046
|
"use strict";
|
|
6047
6047
|
init_cjs_shims();
|
|
6048
|
-
var
|
|
6048
|
+
var path16 = require("path");
|
|
6049
6049
|
var { canAccess } = require_util2();
|
|
6050
6050
|
function win32() {
|
|
6051
6051
|
const installations = [];
|
|
@@ -6059,7 +6059,7 @@ var require_win32 = __commonJS({
|
|
|
6059
6059
|
const prefixes = [process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"]];
|
|
6060
6060
|
prefixes.forEach((prefix) => suffixes.forEach((suffix) => {
|
|
6061
6061
|
if (prefix) {
|
|
6062
|
-
const chromePath =
|
|
6062
|
+
const chromePath = path16.join(prefix, suffix);
|
|
6063
6063
|
if (canAccess(chromePath)) {
|
|
6064
6064
|
installations.push(chromePath);
|
|
6065
6065
|
}
|
|
@@ -6077,7 +6077,7 @@ var require_darwin = __commonJS({
|
|
|
6077
6077
|
"use strict";
|
|
6078
6078
|
init_cjs_shims();
|
|
6079
6079
|
var { execSync: execSync3 } = require("child_process");
|
|
6080
|
-
var
|
|
6080
|
+
var path16 = require("path");
|
|
6081
6081
|
var { canAccess, newLineRegex, sort } = require_util2();
|
|
6082
6082
|
function darwin() {
|
|
6083
6083
|
const suffixes = ["/Contents/MacOS/Google Chrome Canary", "/Contents/MacOS/Google Chrome", "/Contents/MacOS/Chromium"];
|
|
@@ -6087,7 +6087,7 @@ var require_darwin = __commonJS({
|
|
|
6087
6087
|
${LSREGISTER} -dump | grep -E -i -o '/.+(google chrome( canary)?|chromium)\\.app(\\s|$)' | grep -E -v 'Caches|TimeMachine|Temporary|/Volumes|\\.Trash'
|
|
6088
6088
|
`).toString().split(newLineRegex).forEach((inst) => {
|
|
6089
6089
|
suffixes.forEach((suffix) => {
|
|
6090
|
-
const execPath =
|
|
6090
|
+
const execPath = path16.join(inst.trim(), suffix);
|
|
6091
6091
|
if (canAccess(execPath)) {
|
|
6092
6092
|
installations.push(execPath);
|
|
6093
6093
|
}
|
|
@@ -6116,7 +6116,7 @@ var require_linux = __commonJS({
|
|
|
6116
6116
|
"use strict";
|
|
6117
6117
|
init_cjs_shims();
|
|
6118
6118
|
var { execSync: execSync3, execFileSync } = require("child_process");
|
|
6119
|
-
var
|
|
6119
|
+
var path16 = require("path");
|
|
6120
6120
|
var fs16 = require("fs");
|
|
6121
6121
|
var { canAccess, sort, isExecutable, newLineRegex } = require_util2();
|
|
6122
6122
|
function findChromeExecutablesForLinuxDesktop(folder) {
|
|
@@ -6134,7 +6134,7 @@ var require_linux = __commonJS({
|
|
|
6134
6134
|
function linux() {
|
|
6135
6135
|
let installations = [];
|
|
6136
6136
|
const desktopInstallationFolders = [
|
|
6137
|
-
|
|
6137
|
+
path16.join(require("os").homedir(), ".local/share/applications/"),
|
|
6138
6138
|
"/usr/share/applications/"
|
|
6139
6139
|
];
|
|
6140
6140
|
desktopInstallationFolders.forEach((folder) => {
|
|
@@ -6749,8 +6749,8 @@ function getErrorMap() {
|
|
|
6749
6749
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
6750
6750
|
init_cjs_shims();
|
|
6751
6751
|
var makeIssue = (params) => {
|
|
6752
|
-
const { data, path:
|
|
6753
|
-
const fullPath = [...
|
|
6752
|
+
const { data, path: path16, errorMaps, issueData } = params;
|
|
6753
|
+
const fullPath = [...path16, ...issueData.path || []];
|
|
6754
6754
|
const fullIssue = {
|
|
6755
6755
|
...issueData,
|
|
6756
6756
|
path: fullPath
|
|
@@ -6870,11 +6870,11 @@ var errorUtil;
|
|
|
6870
6870
|
|
|
6871
6871
|
// node_modules/zod/v3/types.js
|
|
6872
6872
|
var ParseInputLazyPath = class {
|
|
6873
|
-
constructor(parent, value,
|
|
6873
|
+
constructor(parent, value, path16, key) {
|
|
6874
6874
|
this._cachedPath = [];
|
|
6875
6875
|
this.parent = parent;
|
|
6876
6876
|
this.data = value;
|
|
6877
|
-
this._path =
|
|
6877
|
+
this._path = path16;
|
|
6878
6878
|
this._key = key;
|
|
6879
6879
|
}
|
|
6880
6880
|
get path() {
|
|
@@ -10330,6 +10330,7 @@ var DEFAULT_OPTIONS = {
|
|
|
10330
10330
|
historySize: 10,
|
|
10331
10331
|
showTrend: true,
|
|
10332
10332
|
flakinessTopN: 5,
|
|
10333
|
+
quarantineThreshold: 40,
|
|
10333
10334
|
slowTestThreshold: 10,
|
|
10334
10335
|
// Ticket-ID shape: @ODP-5328, REQ-001, JIRA-123. Tags matching this land in
|
|
10335
10336
|
// the Requirements Traceability matrix; the rest fall to the tag summary.
|
|
@@ -10584,6 +10585,10 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10584
10585
|
// local history file is wiped every run. Aggregate numbers only.
|
|
10585
10586
|
remoteHistory: external_exports.boolean().default(false),
|
|
10586
10587
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
10588
|
+
quarantineThreshold: external_exports.number().int().min(0, "quarantineThreshold must be between 0 and 100").max(100, "quarantineThreshold must be between 0 and 100").optional().default(DEFAULT_OPTIONS.quarantineThreshold),
|
|
10589
|
+
evidenceUrlTemplate: external_exports.string().refine((s) => s.includes("{path}"), {
|
|
10590
|
+
message: "evidenceUrlTemplate must contain the {path} token"
|
|
10591
|
+
}).optional(),
|
|
10587
10592
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10588
10593
|
requirementTagPattern: external_exports.string().refine((v) => {
|
|
10589
10594
|
try {
|
|
@@ -13790,8 +13795,8 @@ function buildBrowserMatrix(projects, cap = DEFAULT_ROW_CAP) {
|
|
|
13790
13795
|
let projectNames = projects.map((p, i) => p.name || `project ${i + 1}`);
|
|
13791
13796
|
const byTest = /* @__PURE__ */ new Map();
|
|
13792
13797
|
projects.forEach((project, column) => {
|
|
13793
|
-
walk(project.suites, [], (
|
|
13794
|
-
const key = [...
|
|
13798
|
+
walk(project.suites, [], (path16, test) => {
|
|
13799
|
+
const key = [...path16, test.title].join(" > ");
|
|
13795
13800
|
let statuses = byTest.get(key);
|
|
13796
13801
|
if (!statuses) {
|
|
13797
13802
|
statuses = projectNames.map(() => null);
|
|
@@ -13834,14 +13839,66 @@ function buildBrowserMatrix(projects, cap = DEFAULT_ROW_CAP) {
|
|
|
13834
13839
|
function severityRank(row) {
|
|
13835
13840
|
return row.statuses.some((s) => s === "failed" || s === "timedOut") ? 1 : 0;
|
|
13836
13841
|
}
|
|
13837
|
-
function walk(suites,
|
|
13842
|
+
function walk(suites, path16, visit) {
|
|
13838
13843
|
for (const suite of suites) {
|
|
13839
|
-
const next = [...
|
|
13844
|
+
const next = [...path16, suite.title];
|
|
13840
13845
|
for (const test of suite.tests) visit(next, test);
|
|
13841
13846
|
walk(suite.suites, next, visit);
|
|
13842
13847
|
}
|
|
13843
13848
|
}
|
|
13844
13849
|
|
|
13850
|
+
// src/utils/evidence-links.ts
|
|
13851
|
+
init_cjs_shims();
|
|
13852
|
+
var import_node_path6 = __toESM(require("path"));
|
|
13853
|
+
var import_qrcode_generator = __toESM(require("qrcode-generator"));
|
|
13854
|
+
function applyEvidenceUrls(data, template, cwd) {
|
|
13855
|
+
let warned = false;
|
|
13856
|
+
const warnOnce = (err) => {
|
|
13857
|
+
if (warned) return;
|
|
13858
|
+
warned = true;
|
|
13859
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13860
|
+
logger.warn(`evidenceUrlTemplate: link generation failed for at least one artifact \u2014 ${message}`);
|
|
13861
|
+
};
|
|
13862
|
+
for (const failure of data.failures) {
|
|
13863
|
+
const evidence = failure.execution?.evidence;
|
|
13864
|
+
if (!evidence) continue;
|
|
13865
|
+
if (evidence.tracePath) {
|
|
13866
|
+
try {
|
|
13867
|
+
evidence.traceUrl = expandTemplate(template, relativizeArtifact(evidence.tracePath, cwd));
|
|
13868
|
+
evidence.traceQr = qrDataUri(evidence.traceUrl);
|
|
13869
|
+
} catch (err) {
|
|
13870
|
+
warnOnce(err);
|
|
13871
|
+
}
|
|
13872
|
+
}
|
|
13873
|
+
if (evidence.videoPath) {
|
|
13874
|
+
try {
|
|
13875
|
+
evidence.videoUrl = expandTemplate(template, relativizeArtifact(evidence.videoPath, cwd));
|
|
13876
|
+
evidence.videoQr = qrDataUri(evidence.videoUrl);
|
|
13877
|
+
} catch (err) {
|
|
13878
|
+
warnOnce(err);
|
|
13879
|
+
}
|
|
13880
|
+
}
|
|
13881
|
+
}
|
|
13882
|
+
}
|
|
13883
|
+
function relativizeArtifact(artifactPath, cwd) {
|
|
13884
|
+
if (!import_node_path6.default.isAbsolute(artifactPath)) return artifactPath;
|
|
13885
|
+
const rel = import_node_path6.default.relative(cwd, artifactPath);
|
|
13886
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path6.default.sep}`) || import_node_path6.default.isAbsolute(rel)) return artifactPath;
|
|
13887
|
+
return rel;
|
|
13888
|
+
}
|
|
13889
|
+
function expandTemplate(template, artifactPath) {
|
|
13890
|
+
const normalized = artifactPath.replace(/\\/g, "/");
|
|
13891
|
+
const encoded = normalized.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
13892
|
+
return template.split("{path}").join(encoded);
|
|
13893
|
+
}
|
|
13894
|
+
function qrDataUri(url) {
|
|
13895
|
+
const qr = (0, import_qrcode_generator.default)(0, "M");
|
|
13896
|
+
qr.addData(url);
|
|
13897
|
+
qr.make();
|
|
13898
|
+
const svg = qr.createSvgTag({ cellSize: 2, margin: 2, scalable: true });
|
|
13899
|
+
return `data:image/svg+xml;base64,${Buffer.from(svg, "utf8").toString("base64")}`;
|
|
13900
|
+
}
|
|
13901
|
+
|
|
13845
13902
|
// src/redact/redact-report.ts
|
|
13846
13903
|
init_cjs_shims();
|
|
13847
13904
|
function redactReportData(data, r) {
|
|
@@ -14007,7 +14064,7 @@ function splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses
|
|
|
14007
14064
|
return { fixed, noLongerRun };
|
|
14008
14065
|
}
|
|
14009
14066
|
function computeRunDiff(args) {
|
|
14010
|
-
const { currentFailing, currentStatuses, priorEntries, branch } = args;
|
|
14067
|
+
const { currentFailing, currentFlaky = [], currentStatuses, priorEntries, branch } = args;
|
|
14011
14068
|
const baseline = priorEntries.find((e) => e.branch === branch && Array.isArray(e.failedTests));
|
|
14012
14069
|
if (!baseline) return null;
|
|
14013
14070
|
const baselineFailed = /* @__PURE__ */ new Map();
|
|
@@ -14016,7 +14073,15 @@ function computeRunDiff(args) {
|
|
|
14016
14073
|
}
|
|
14017
14074
|
const { newFailures, stillFailing, currentFailedIds } = splitCurrentFailing(currentFailing, baselineFailed);
|
|
14018
14075
|
const { fixed, noLongerRun } = splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses);
|
|
14019
|
-
|
|
14076
|
+
const baselineFlakyIds = new Set((baseline.flakyTests ?? []).map((t) => t.id));
|
|
14077
|
+
const newlyFlaky = [];
|
|
14078
|
+
const seenFlaky = /* @__PURE__ */ new Set();
|
|
14079
|
+
for (const item of currentFlaky) {
|
|
14080
|
+
if (seenFlaky.has(item.id) || baselineFlakyIds.has(item.id)) continue;
|
|
14081
|
+
seenFlaky.add(item.id);
|
|
14082
|
+
newlyFlaky.push(item);
|
|
14083
|
+
}
|
|
14084
|
+
for (const bucket of [newFailures, stillFailing, fixed, noLongerRun, newlyFlaky]) {
|
|
14020
14085
|
bucket.sort((a, b) => a.title.localeCompare(b.title));
|
|
14021
14086
|
}
|
|
14022
14087
|
return {
|
|
@@ -14024,6 +14089,7 @@ function computeRunDiff(args) {
|
|
|
14024
14089
|
stillFailing,
|
|
14025
14090
|
fixed,
|
|
14026
14091
|
noLongerRun,
|
|
14092
|
+
newlyFlaky,
|
|
14027
14093
|
baselineRunId: baseline.runId,
|
|
14028
14094
|
baselineTimestamp: baseline.timestamp,
|
|
14029
14095
|
baselineTruncated: baseline.failedTestsTruncated === true
|
|
@@ -14032,10 +14098,11 @@ function computeRunDiff(args) {
|
|
|
14032
14098
|
|
|
14033
14099
|
// src/history/FlakinessAnalyser.ts
|
|
14034
14100
|
init_cjs_shims();
|
|
14035
|
-
|
|
14036
|
-
|
|
14101
|
+
var QUARANTINE_MIN_RUNS = 3;
|
|
14102
|
+
function computeTopN(entries, topN, quarantineThreshold = 0) {
|
|
14103
|
+
if (topN === 0) return { rows: [], quarantineTotal: 0 };
|
|
14037
14104
|
const qualifying = entries.filter((e) => Array.isArray(e.flakyTests));
|
|
14038
|
-
if (qualifying.length === 0) return [];
|
|
14105
|
+
if (qualifying.length === 0) return { rows: [], quarantineTotal: 0 };
|
|
14039
14106
|
const map = /* @__PURE__ */ new Map();
|
|
14040
14107
|
qualifying.forEach((entry, idx) => {
|
|
14041
14108
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -14053,17 +14120,26 @@ function computeTopN(entries, topN) {
|
|
|
14053
14120
|
const originalIdx = qualifying.length - 1 - reversedIdx;
|
|
14054
14121
|
return hits.has(originalIdx);
|
|
14055
14122
|
});
|
|
14123
|
+
let streak = 0;
|
|
14124
|
+
while (streak < totalRuns && hits.has(streak)) streak++;
|
|
14125
|
+
const flakeRate = Math.round(flakeCount / totalRuns * 100);
|
|
14056
14126
|
return {
|
|
14057
14127
|
title,
|
|
14058
|
-
flakeRate
|
|
14128
|
+
flakeRate,
|
|
14059
14129
|
flakeCount,
|
|
14060
14130
|
totalRuns,
|
|
14061
|
-
sparkline
|
|
14131
|
+
sparkline,
|
|
14132
|
+
streak,
|
|
14133
|
+
quarantine: quarantineThreshold > 0 && totalRuns >= QUARANTINE_MIN_RUNS && flakeRate >= quarantineThreshold
|
|
14062
14134
|
};
|
|
14063
14135
|
});
|
|
14064
|
-
|
|
14065
|
-
|
|
14066
|
-
|
|
14136
|
+
const quarantineTotal = rows.filter((r) => r.quarantine).length;
|
|
14137
|
+
return {
|
|
14138
|
+
rows: rows.sort(
|
|
14139
|
+
(a, b) => b.flakeRate - a.flakeRate || b.flakeCount - a.flakeCount || a.title.localeCompare(b.title)
|
|
14140
|
+
).slice(0, topN),
|
|
14141
|
+
quarantineTotal
|
|
14142
|
+
};
|
|
14067
14143
|
}
|
|
14068
14144
|
|
|
14069
14145
|
// src/notify/index.ts
|
|
@@ -14506,6 +14582,9 @@ async function runReportPipeline(hooks) {
|
|
|
14506
14582
|
}
|
|
14507
14583
|
}
|
|
14508
14584
|
const reportData = buildReportData(collected, licenseInfo, hooks);
|
|
14585
|
+
if (options.evidenceUrlTemplate) {
|
|
14586
|
+
applyEvidenceUrls(reportData, options.evidenceUrlTemplate, hooks.cwd);
|
|
14587
|
+
}
|
|
14509
14588
|
runFailureAnalysis(reportData, options, hooks.cwd);
|
|
14510
14589
|
reportData.briefSentence = buildBriefSentence(reportData.stats, reportData.analysis, reportData.charts.trend);
|
|
14511
14590
|
if (hooks.redactor) {
|
|
@@ -14554,7 +14633,7 @@ async function appendTrend(collected, licenseInfo, hooks) {
|
|
|
14554
14633
|
branch: collected.environment.branch
|
|
14555
14634
|
};
|
|
14556
14635
|
const localEntries = hm.append(entry, options.historySize);
|
|
14557
|
-
collected.runDiff = resolveRunDiff(collected.projects, redactedFailing, localEntries.slice(1), collected.environment.branch);
|
|
14636
|
+
collected.runDiff = resolveRunDiff(collected.projects, redactedFailing, localEntries.slice(1), collected.environment.branch, flakyTests);
|
|
14558
14637
|
let trendEntries = localEntries;
|
|
14559
14638
|
if (options.remoteHistory && licenseInfo.jwt) {
|
|
14560
14639
|
const serverUrl = options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL;
|
|
@@ -14587,9 +14666,17 @@ function populateHistoryCharts(trendEntries, localEntries, collected, options) {
|
|
|
14587
14666
|
};
|
|
14588
14667
|
}
|
|
14589
14668
|
if (options.flakinessTopN > 0) {
|
|
14590
|
-
const
|
|
14591
|
-
|
|
14592
|
-
|
|
14669
|
+
const { rows, quarantineTotal } = computeTopN(
|
|
14670
|
+
localEntries,
|
|
14671
|
+
options.flakinessTopN,
|
|
14672
|
+
options.quarantineThreshold
|
|
14673
|
+
);
|
|
14674
|
+
if (rows.length > 0) {
|
|
14675
|
+
collected.charts.flakinessTable = rows;
|
|
14676
|
+
if (quarantineTotal > 0) {
|
|
14677
|
+
collected.charts.flakinessQuarantineTotal = quarantineTotal;
|
|
14678
|
+
collected.charts.flakinessQuarantineThreshold = options.quarantineThreshold;
|
|
14679
|
+
}
|
|
14593
14680
|
}
|
|
14594
14681
|
}
|
|
14595
14682
|
}
|
|
@@ -14726,9 +14813,10 @@ function buildFailedTestsFields(projects, redactor) {
|
|
|
14726
14813
|
const failedTests = redactedFailing.slice(0, FAILED_TESTS_CAP);
|
|
14727
14814
|
return redactedFailing.length > FAILED_TESTS_CAP ? { redactedFailing, failedTests, failedTestsTruncated: true } : { redactedFailing, failedTests };
|
|
14728
14815
|
}
|
|
14729
|
-
function resolveRunDiff(projects, currentFailing, priorEntries, branch) {
|
|
14816
|
+
function resolveRunDiff(projects, currentFailing, priorEntries, branch, currentFlaky) {
|
|
14730
14817
|
return computeRunDiff({
|
|
14731
14818
|
currentFailing,
|
|
14819
|
+
currentFlaky,
|
|
14732
14820
|
currentStatuses: collectCurrentStatuses(projects),
|
|
14733
14821
|
priorEntries,
|
|
14734
14822
|
branch
|
|
@@ -15248,9 +15336,9 @@ var MAX_FILE_CHARS = 300;
|
|
|
15248
15336
|
var MAX_PROJECT_CHARS = 100;
|
|
15249
15337
|
var MAX_SUITE_SEGMENTS = 10;
|
|
15250
15338
|
function suiteIdentity(test) {
|
|
15251
|
-
const
|
|
15252
|
-
if (!Array.isArray(
|
|
15253
|
-
const [, project, file, ...rest] =
|
|
15339
|
+
const path16 = typeof test.titlePath === "function" ? test.titlePath() : [];
|
|
15340
|
+
if (!Array.isArray(path16) || path16.length < 4) return {};
|
|
15341
|
+
const [, project, file, ...rest] = path16;
|
|
15254
15342
|
const describes = rest.slice(0, -1);
|
|
15255
15343
|
const out = {};
|
|
15256
15344
|
if (typeof file === "string" && file) {
|
|
@@ -15369,7 +15457,7 @@ var PdfReporter = class {
|
|
|
15369
15457
|
if (this.options.redact.enabled) {
|
|
15370
15458
|
this.redactor = new Redactor(this.options.redact);
|
|
15371
15459
|
}
|
|
15372
|
-
this.version = true ? "0.
|
|
15460
|
+
this.version = true ? "0.30.0" : "0.x";
|
|
15373
15461
|
logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
|
|
15374
15462
|
this.licenseClient = new LicenseClient({
|
|
15375
15463
|
licenseKey: this.options.licenseKey,
|