@reportforge/playwright-pdf 0.7.2 → 0.8.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 +3 -1
- package/dist/cli/index.js +26 -3
- package/dist/index.d.mts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +208 -8
- package/dist/index.mjs +208 -8
- package/dist/templates/partials/defect-log.hbs +46 -2
- package/dist/templates/partials/failure-card.hbs +8 -0
- package/dist/templates/styles/base.css +73 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ Three templates, one reporter. Every PDF is fully offline — fonts, charts, and
|
|
|
58
58
|
- **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)
|
|
59
59
|
- **Offline failure analysis** — sorts each failure into one of 7 root-cause buckets and renders a per-test root-cause chip inline under the failed test in the `detailed` PDF (dominant-cause one-liner in `executive`). No network, no AI service — a small embedded classifier that updates itself safely over your existing license refresh; pin it any time. Full details at [reportforge.org/docs/advanced/failure-analysis](https://reportforge.org/docs/advanced/failure-analysis).
|
|
60
60
|
- `npx @reportforge/playwright-pdf export-feedback` — exports locally-collected unrecognized failures (tokenized, local-only) for optional manual sharing.
|
|
61
|
+
- **Rich execution capture** — opt-in repro steps, 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` / `console` / `evidence`); off by default.
|
|
61
62
|
- **Configurable sections** — add or remove any report section per template via `sections`.
|
|
62
63
|
- **Hybrid licensing** — one short key unlocks it; reports keep rendering offline via a cached JWT between refreshes
|
|
63
64
|
|
|
@@ -194,6 +195,7 @@ All options are the second element of the reporter tuple.
|
|
|
194
195
|
| `shardResults` | `string \| string[]` | — | Glob or path array of Playwright JSON shard report files to merge into one PDF. See the Shard Merging docs. |
|
|
195
196
|
| `notify` | `object` | — | Notification channels: `slack`, `teams`, `discord` (each `{ url, enabled, on }`), `email` (`{ to, enabled, on, attachPdf }`). See the Notifications docs. |
|
|
196
197
|
| `failureAnalysis` | `object` | `{ enabled: true }` | Offline failure root-cause analysis (embedded Naive Bayes; no runtime network). Fields: `enabled` (default `true`), `maxClusters` (default `10`), `minStrength` (`weak`\|`moderate`\|`strong`, default `weak`), `maxFailuresToAnalyse` (default `500`), `collectUnclassified` (default `true`), `autoUpdateModel` (default `true`). See the Failure Analysis docs. |
|
|
198
|
+
| `capture` | `object` | `{}` (off) | Opt-in rich execution capture for the defect log (reporter-side; no fixtures, no test-code changes). Fields: `steps` (compacted repro-steps trail from the Playwright step tree, default `false`), `apiSteps` (include granular `pw:api` click/fill/goto actions in the trail; off by default the trail shows only `test.step` intent + `expect` assertions + the failing action, 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. |
|
|
197
199
|
| `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. |
|
|
198
200
|
| `historySize` | `number` | `10` | Maximum number of test runs to keep in the history file (integer ≥ 2). Older runs are pruned on append. |
|
|
199
201
|
| `showTrend` | `boolean` | `true` | Show the pass-rate sparkline and delta badge in the `detailed` template. Set to `false` to disable history tracking entirely. |
|
|
@@ -396,7 +398,7 @@ reporter: [['@reportforge/playwright-pdf', {
|
|
|
396
398
|
| `failureDeepDive` | Inline failure detail under each failed test in the breakdown: error message, stack trace, screenshot, retry history |
|
|
397
399
|
| `failureAnalysis` | Inline per-test root-cause chip under each failed test (category + representative error + match strength, from the offline classifier) |
|
|
398
400
|
| `slowTests` | `SLOW` badge on the duration-ranked slowest tests, shown inline in the breakdown |
|
|
399
|
-
| `defectLog` | `DEF-####` numbered failure table (severity,
|
|
401
|
+
| `defectLog` | `DEF-####` numbered failure table (severity, duration) + opt-in repro detail (`capture`) |
|
|
400
402
|
|
|
401
403
|
#### Display modifiers — tune a section that's on
|
|
402
404
|
|
package/dist/cli/index.js
CHANGED
|
@@ -138,6 +138,7 @@ function buildDemoFailures() {
|
|
|
138
138
|
{ attempt: 1, status: "failed", duration: 4900 },
|
|
139
139
|
{ attempt: 2, status: "failed", duration: 4900 }
|
|
140
140
|
],
|
|
141
|
+
durationMs: 4900,
|
|
141
142
|
severity: "critical"
|
|
142
143
|
},
|
|
143
144
|
{
|
|
@@ -153,6 +154,7 @@ function buildDemoFailures() {
|
|
|
153
154
|
{ attempt: 1, status: "failed", duration: 5e3 },
|
|
154
155
|
{ attempt: 2, status: "failed", duration: 4900 }
|
|
155
156
|
],
|
|
157
|
+
durationMs: 4900,
|
|
156
158
|
severity: "high"
|
|
157
159
|
},
|
|
158
160
|
{
|
|
@@ -168,6 +170,7 @@ function buildDemoFailures() {
|
|
|
168
170
|
{ attempt: 1, status: "failed", duration: 4e3 },
|
|
169
171
|
{ attempt: 2, status: "failed", duration: 3900 }
|
|
170
172
|
],
|
|
173
|
+
durationMs: 3900,
|
|
171
174
|
severity: "medium"
|
|
172
175
|
}
|
|
173
176
|
];
|
|
@@ -11828,7 +11831,7 @@ ${issues}`);
|
|
|
11828
11831
|
}
|
|
11829
11832
|
return result.data;
|
|
11830
11833
|
}
|
|
11831
|
-
var hexColor, channelConfigSchema, emailChannelConfigSchema, notifyConfigSchema, failureAnalysisConfigSchema, sectionFlagsShape, sectionFlagsSchema, sectionsSchema, ReporterOptionsSchema;
|
|
11834
|
+
var hexColor, channelConfigSchema, emailChannelConfigSchema, notifyConfigSchema, failureAnalysisConfigSchema, captureConfigSchema, sectionFlagsShape, sectionFlagsSchema, sectionsSchema, ReporterOptionsSchema;
|
|
11832
11835
|
var init_schema = __esm({
|
|
11833
11836
|
"src/config/schema.ts"() {
|
|
11834
11837
|
"use strict";
|
|
@@ -11867,6 +11870,14 @@ var init_schema = __esm({
|
|
|
11867
11870
|
collectUnclassified: external_exports.boolean().default(true),
|
|
11868
11871
|
autoUpdateModel: external_exports.boolean().default(true)
|
|
11869
11872
|
}).default({});
|
|
11873
|
+
captureConfigSchema = external_exports.object({
|
|
11874
|
+
steps: external_exports.boolean().default(false),
|
|
11875
|
+
apiSteps: external_exports.boolean().default(false),
|
|
11876
|
+
console: external_exports.boolean().default(false),
|
|
11877
|
+
evidence: external_exports.boolean().default(false),
|
|
11878
|
+
maxSteps: external_exports.number().int().min(1).max(500).default(50),
|
|
11879
|
+
maxConsoleLines: external_exports.number().int().min(1).max(1e3).default(50)
|
|
11880
|
+
}).default({});
|
|
11870
11881
|
sectionFlagsShape = Object.fromEntries(
|
|
11871
11882
|
SECTION_KEYS.map((k) => [k, external_exports.boolean().optional()])
|
|
11872
11883
|
);
|
|
@@ -11907,6 +11918,7 @@ var init_schema = __esm({
|
|
|
11907
11918
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
11908
11919
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
11909
11920
|
failureAnalysis: failureAnalysisConfigSchema,
|
|
11921
|
+
capture: captureConfigSchema,
|
|
11910
11922
|
templatePath: external_exports.union([
|
|
11911
11923
|
external_exports.string().regex(/\.hbs$/, "templatePath must end with .hbs").refine((v) => v.slice(0, -4).trim().length > 0, "templatePath basename must not be empty"),
|
|
11912
11924
|
external_exports.array(
|
|
@@ -12072,6 +12084,17 @@ var init_constants = __esm({
|
|
|
12072
12084
|
}
|
|
12073
12085
|
});
|
|
12074
12086
|
|
|
12087
|
+
// src/utils/truncate.ts
|
|
12088
|
+
function clampWithEllipsis(text, max) {
|
|
12089
|
+
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
12090
|
+
}
|
|
12091
|
+
var init_truncate = __esm({
|
|
12092
|
+
"src/utils/truncate.ts"() {
|
|
12093
|
+
"use strict";
|
|
12094
|
+
init_cjs_shims();
|
|
12095
|
+
}
|
|
12096
|
+
});
|
|
12097
|
+
|
|
12075
12098
|
// src/analysis/FailureClusterer.ts
|
|
12076
12099
|
function errorHeader(message) {
|
|
12077
12100
|
const beforeCallLog = message.split(/\n\s*Call log:/i)[0];
|
|
@@ -12107,8 +12130,7 @@ function topFrame(stack) {
|
|
|
12107
12130
|
return void 0;
|
|
12108
12131
|
}
|
|
12109
12132
|
function representativeMessage(message) {
|
|
12110
|
-
|
|
12111
|
-
return firstLine.length > MAX_MESSAGE_CHARS ? `${firstLine.slice(0, MAX_MESSAGE_CHARS)}\u2026` : firstLine;
|
|
12133
|
+
return clampWithEllipsis(message.split("\n")[0], MAX_MESSAGE_CHARS);
|
|
12112
12134
|
}
|
|
12113
12135
|
function classifyOne(f, model) {
|
|
12114
12136
|
const frame = topFrame(f.error.stack);
|
|
@@ -12163,6 +12185,7 @@ var init_FailureClusterer = __esm({
|
|
|
12163
12185
|
init_cjs_shims();
|
|
12164
12186
|
init_NaiveBayesClassifier();
|
|
12165
12187
|
init_constants();
|
|
12188
|
+
init_truncate();
|
|
12166
12189
|
MAX_TESTS_PER_CLUSTER = 20;
|
|
12167
12190
|
MAX_MESSAGE_CHARS = 200;
|
|
12168
12191
|
MESSAGE_RULES = [
|
package/dist/index.d.mts
CHANGED
|
@@ -209,6 +209,34 @@ interface ReporterOptions {
|
|
|
209
209
|
*/
|
|
210
210
|
autoUpdateModel?: boolean;
|
|
211
211
|
};
|
|
212
|
+
/**
|
|
213
|
+
* Opt-in rich execution capture for the defect log. Reporter-side only — reads
|
|
214
|
+
* what Playwright already records on each test result (steps, console, trace/
|
|
215
|
+
* video attachments); no fixtures and no test-code changes. Off by default, so
|
|
216
|
+
* reports are unchanged until you enable it. Captured detail renders in the
|
|
217
|
+
* Defect Log section of the `detailed` template.
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* capture: { steps: true, console: true, evidence: true }
|
|
221
|
+
*/
|
|
222
|
+
capture?: {
|
|
223
|
+
/** Capture a compacted repro-steps trail from the Playwright step tree. Default: false */
|
|
224
|
+
steps?: boolean;
|
|
225
|
+
/**
|
|
226
|
+
* Include granular `pw:api` actions (click/fill/goto) in the repro trail. Off by
|
|
227
|
+
* default — the trail shows only `test.step` intent + `expect` assertions + the
|
|
228
|
+
* failing action. Default: false
|
|
229
|
+
*/
|
|
230
|
+
apiSteps?: boolean;
|
|
231
|
+
/** Capture the tail of the test's Node stdout/stderr. Default: false */
|
|
232
|
+
console?: boolean;
|
|
233
|
+
/** Capture file links to the Playwright trace.zip and video. Default: false */
|
|
234
|
+
evidence?: boolean;
|
|
235
|
+
/** Max repro steps retained per failure. Default: 50 */
|
|
236
|
+
maxSteps?: number;
|
|
237
|
+
/** Max console lines retained per stream. Default: 50 */
|
|
238
|
+
maxConsoleLines?: number;
|
|
239
|
+
};
|
|
212
240
|
/**
|
|
213
241
|
* Path to the history JSON file. Defaults to
|
|
214
242
|
* `~/.reportforge/{projectKey}/history.json` where `projectKey` is derived
|
package/dist/index.d.ts
CHANGED
|
@@ -209,6 +209,34 @@ interface ReporterOptions {
|
|
|
209
209
|
*/
|
|
210
210
|
autoUpdateModel?: boolean;
|
|
211
211
|
};
|
|
212
|
+
/**
|
|
213
|
+
* Opt-in rich execution capture for the defect log. Reporter-side only — reads
|
|
214
|
+
* what Playwright already records on each test result (steps, console, trace/
|
|
215
|
+
* video attachments); no fixtures and no test-code changes. Off by default, so
|
|
216
|
+
* reports are unchanged until you enable it. Captured detail renders in the
|
|
217
|
+
* Defect Log section of the `detailed` template.
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* capture: { steps: true, console: true, evidence: true }
|
|
221
|
+
*/
|
|
222
|
+
capture?: {
|
|
223
|
+
/** Capture a compacted repro-steps trail from the Playwright step tree. Default: false */
|
|
224
|
+
steps?: boolean;
|
|
225
|
+
/**
|
|
226
|
+
* Include granular `pw:api` actions (click/fill/goto) in the repro trail. Off by
|
|
227
|
+
* default — the trail shows only `test.step` intent + `expect` assertions + the
|
|
228
|
+
* failing action. Default: false
|
|
229
|
+
*/
|
|
230
|
+
apiSteps?: boolean;
|
|
231
|
+
/** Capture the tail of the test's Node stdout/stderr. Default: false */
|
|
232
|
+
console?: boolean;
|
|
233
|
+
/** Capture file links to the Playwright trace.zip and video. Default: false */
|
|
234
|
+
evidence?: boolean;
|
|
235
|
+
/** Max repro steps retained per failure. Default: 50 */
|
|
236
|
+
maxSteps?: number;
|
|
237
|
+
/** Max console lines retained per stream. Default: 50 */
|
|
238
|
+
maxConsoleLines?: number;
|
|
239
|
+
};
|
|
212
240
|
/**
|
|
213
241
|
* Path to the history JSON file. Defaults to
|
|
214
242
|
* `~/.reportforge/{projectKey}/history.json` where `projectKey` is derived
|
package/dist/index.js
CHANGED
|
@@ -10476,6 +10476,14 @@ var failureAnalysisConfigSchema = external_exports.object({
|
|
|
10476
10476
|
collectUnclassified: external_exports.boolean().default(true),
|
|
10477
10477
|
autoUpdateModel: external_exports.boolean().default(true)
|
|
10478
10478
|
}).default({});
|
|
10479
|
+
var captureConfigSchema = external_exports.object({
|
|
10480
|
+
steps: external_exports.boolean().default(false),
|
|
10481
|
+
apiSteps: external_exports.boolean().default(false),
|
|
10482
|
+
console: external_exports.boolean().default(false),
|
|
10483
|
+
evidence: external_exports.boolean().default(false),
|
|
10484
|
+
maxSteps: external_exports.number().int().min(1).max(500).default(50),
|
|
10485
|
+
maxConsoleLines: external_exports.number().int().min(1).max(1e3).default(50)
|
|
10486
|
+
}).default({});
|
|
10479
10487
|
var sectionFlagsShape = Object.fromEntries(
|
|
10480
10488
|
SECTION_KEYS.map((k) => [k, external_exports.boolean().optional()])
|
|
10481
10489
|
);
|
|
@@ -10516,6 +10524,7 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10516
10524
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
10517
10525
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10518
10526
|
failureAnalysis: failureAnalysisConfigSchema,
|
|
10527
|
+
capture: captureConfigSchema,
|
|
10519
10528
|
templatePath: external_exports.union([
|
|
10520
10529
|
external_exports.string().regex(/\.hbs$/, "templatePath must end with .hbs").refine((v) => v.slice(0, -4).trim().length > 0, "templatePath basename must not be empty"),
|
|
10521
10530
|
external_exports.array(
|
|
@@ -11118,6 +11127,12 @@ var STRENGTH = {
|
|
|
11118
11127
|
var STRENGTH_LEVELS = ["weak", "moderate", "strong"];
|
|
11119
11128
|
var OVERRIDE_MARGIN = 999;
|
|
11120
11129
|
|
|
11130
|
+
// src/utils/truncate.ts
|
|
11131
|
+
init_cjs_shims();
|
|
11132
|
+
function clampWithEllipsis(text, max) {
|
|
11133
|
+
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
11134
|
+
}
|
|
11135
|
+
|
|
11121
11136
|
// src/analysis/FailureClusterer.ts
|
|
11122
11137
|
var MAX_TESTS_PER_CLUSTER = 20;
|
|
11123
11138
|
var MAX_MESSAGE_CHARS = 200;
|
|
@@ -11161,8 +11176,7 @@ function topFrame(stack) {
|
|
|
11161
11176
|
return void 0;
|
|
11162
11177
|
}
|
|
11163
11178
|
function representativeMessage(message) {
|
|
11164
|
-
|
|
11165
|
-
return firstLine.length > MAX_MESSAGE_CHARS ? `${firstLine.slice(0, MAX_MESSAGE_CHARS)}\u2026` : firstLine;
|
|
11179
|
+
return clampWithEllipsis(message.split("\n")[0], MAX_MESSAGE_CHARS);
|
|
11166
11180
|
}
|
|
11167
11181
|
function classifyOne(f, model) {
|
|
11168
11182
|
const frame = topFrame(f.error.stack);
|
|
@@ -11386,6 +11400,170 @@ function stripAnsi(input) {
|
|
|
11386
11400
|
return input.replace(ANSI_PATTERN, "");
|
|
11387
11401
|
}
|
|
11388
11402
|
|
|
11403
|
+
// src/collector/execution-capture.ts
|
|
11404
|
+
init_cjs_shims();
|
|
11405
|
+
var MAX_TITLE_CHARS = 200;
|
|
11406
|
+
var DEFAULT_MAX_STEPS = 50;
|
|
11407
|
+
var DEFAULT_MAX_CONSOLE_LINES = 50;
|
|
11408
|
+
var PW_API_WINDOW = 10;
|
|
11409
|
+
var isTeardown = (category) => category === "hook" || category === "fixture";
|
|
11410
|
+
function flatten(steps) {
|
|
11411
|
+
const out = [];
|
|
11412
|
+
const walk = (nodes, depth, ancestors) => {
|
|
11413
|
+
for (const s of nodes ?? []) {
|
|
11414
|
+
const index = out.length;
|
|
11415
|
+
out.push({
|
|
11416
|
+
raw: s,
|
|
11417
|
+
category: s.category ?? "",
|
|
11418
|
+
depth,
|
|
11419
|
+
hasError: s.error != null,
|
|
11420
|
+
ancestors
|
|
11421
|
+
});
|
|
11422
|
+
if (s.steps?.length) walk(s.steps, depth + 1, [...ancestors, index]);
|
|
11423
|
+
}
|
|
11424
|
+
};
|
|
11425
|
+
walk(steps ?? [], 0, []);
|
|
11426
|
+
return out;
|
|
11427
|
+
}
|
|
11428
|
+
function materialize(n) {
|
|
11429
|
+
const loc = n.raw.location;
|
|
11430
|
+
return {
|
|
11431
|
+
title: clampWithEllipsis(n.raw.title ?? "", MAX_TITLE_CHARS),
|
|
11432
|
+
category: n.category,
|
|
11433
|
+
durationMs: Math.max(0, Math.round(n.raw.duration ?? 0)),
|
|
11434
|
+
depth: n.depth,
|
|
11435
|
+
failed: n.hasError,
|
|
11436
|
+
errorMessage: n.raw.error?.message ? clampWithEllipsis(stripAnsi(n.raw.error.message).split("\n")[0], MAX_TITLE_CHARS) : void 0,
|
|
11437
|
+
location: loc?.file != null ? `${loc.file}:${loc.line ?? 0}` : void 0
|
|
11438
|
+
};
|
|
11439
|
+
}
|
|
11440
|
+
function pickFailingIdx(flat) {
|
|
11441
|
+
const inTeardown = (f) => isTeardown(f.category) || f.ancestors.some((a) => isTeardown(flat[a].category));
|
|
11442
|
+
const pick = (allowTeardown) => {
|
|
11443
|
+
let idx = -1;
|
|
11444
|
+
for (let i = 0; i < flat.length; i++) {
|
|
11445
|
+
const f = flat[i];
|
|
11446
|
+
if (!f.hasError || !allowTeardown && inTeardown(f)) continue;
|
|
11447
|
+
if (idx === -1 || f.depth >= flat[idx].depth) idx = i;
|
|
11448
|
+
}
|
|
11449
|
+
return idx;
|
|
11450
|
+
};
|
|
11451
|
+
const body = pick(false);
|
|
11452
|
+
return body >= 0 ? body : pick(true);
|
|
11453
|
+
}
|
|
11454
|
+
function compactSteps(rawSteps, opts = {}) {
|
|
11455
|
+
const flat = flatten(rawSteps);
|
|
11456
|
+
if (flat.length === 0) return { steps: [] };
|
|
11457
|
+
const maxSteps = opts.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
11458
|
+
const failingIdx = pickFailingIdx(flat);
|
|
11459
|
+
const keep = /* @__PURE__ */ new Set();
|
|
11460
|
+
flat.forEach((f, i) => {
|
|
11461
|
+
if (f.category === "test.step" || f.category === "expect") keep.add(i);
|
|
11462
|
+
});
|
|
11463
|
+
if (failingIdx >= 0) {
|
|
11464
|
+
keep.add(failingIdx);
|
|
11465
|
+
for (const a of flat[failingIdx].ancestors) keep.add(a);
|
|
11466
|
+
if (opts.apiSteps) {
|
|
11467
|
+
const pwApiBefore = [];
|
|
11468
|
+
for (let i = 0; i < failingIdx; i++) {
|
|
11469
|
+
if (flat[i].category === "pw:api") pwApiBefore.push(i);
|
|
11470
|
+
}
|
|
11471
|
+
for (const i of pwApiBefore.slice(-PW_API_WINDOW)) keep.add(i);
|
|
11472
|
+
}
|
|
11473
|
+
}
|
|
11474
|
+
let keepArr = [...keep].sort((a, b) => a - b);
|
|
11475
|
+
if (keepArr.length > maxSteps) {
|
|
11476
|
+
keepArr = keepArr.slice(-maxSteps);
|
|
11477
|
+
if (failingIdx >= 0 && !keepArr.includes(failingIdx)) {
|
|
11478
|
+
keepArr = [failingIdx, ...keepArr.slice(1)];
|
|
11479
|
+
}
|
|
11480
|
+
}
|
|
11481
|
+
const cache = /* @__PURE__ */ new Map();
|
|
11482
|
+
const mat = (i) => {
|
|
11483
|
+
let es = cache.get(i);
|
|
11484
|
+
if (!es) {
|
|
11485
|
+
es = materialize(flat[i]);
|
|
11486
|
+
cache.set(i, es);
|
|
11487
|
+
}
|
|
11488
|
+
return es;
|
|
11489
|
+
};
|
|
11490
|
+
return {
|
|
11491
|
+
steps: keepArr.map(mat),
|
|
11492
|
+
failingStep: failingIdx >= 0 ? mat(failingIdx) : void 0
|
|
11493
|
+
};
|
|
11494
|
+
}
|
|
11495
|
+
function tailConsole(stdout, stderr, maxLines = DEFAULT_MAX_CONSOLE_LINES) {
|
|
11496
|
+
const tail = (chunks) => {
|
|
11497
|
+
if (chunks.length === 0) return { lines: [], truncated: false };
|
|
11498
|
+
const all = chunks.join("").split(/\r?\n/);
|
|
11499
|
+
if (all.length > 0 && all[all.length - 1] === "") all.pop();
|
|
11500
|
+
const lines = all.slice(-maxLines).map(stripAnsi);
|
|
11501
|
+
return { lines, truncated: all.length > lines.length };
|
|
11502
|
+
};
|
|
11503
|
+
const o = tail(stdout);
|
|
11504
|
+
const e = tail(stderr);
|
|
11505
|
+
if (o.lines.length === 0 && e.lines.length === 0) return void 0;
|
|
11506
|
+
return {
|
|
11507
|
+
stdout: o.lines,
|
|
11508
|
+
stderr: e.lines,
|
|
11509
|
+
truncated: o.truncated || e.truncated,
|
|
11510
|
+
stdoutTruncated: o.truncated,
|
|
11511
|
+
stderrTruncated: e.truncated
|
|
11512
|
+
};
|
|
11513
|
+
}
|
|
11514
|
+
function extractEvidence(attachments) {
|
|
11515
|
+
let tracePath;
|
|
11516
|
+
let videoPath;
|
|
11517
|
+
for (const a of attachments) {
|
|
11518
|
+
if (!a.path) continue;
|
|
11519
|
+
const looksLikeTrace = a.name === "trace" || a.contentType === "application/zip" && (a.name ?? "").includes("trace");
|
|
11520
|
+
if (!tracePath && looksLikeTrace) tracePath = a.path;
|
|
11521
|
+
if (!videoPath && a.contentType?.startsWith("video/")) videoPath = a.path;
|
|
11522
|
+
}
|
|
11523
|
+
if (!tracePath && !videoPath) return void 0;
|
|
11524
|
+
return { tracePath, videoPath };
|
|
11525
|
+
}
|
|
11526
|
+
function buildExecution(input, opts, source) {
|
|
11527
|
+
const capture = {};
|
|
11528
|
+
if (opts.steps) {
|
|
11529
|
+
const { steps, failingStep } = compactSteps(input.steps, {
|
|
11530
|
+
maxSteps: opts.maxSteps,
|
|
11531
|
+
apiSteps: opts.apiSteps
|
|
11532
|
+
});
|
|
11533
|
+
if (steps.length > 0) capture.steps = steps;
|
|
11534
|
+
if (failingStep) capture.failingStep = failingStep;
|
|
11535
|
+
}
|
|
11536
|
+
if (opts.console) {
|
|
11537
|
+
const console2 = tailConsole(input.stdout, input.stderr, opts.maxConsoleLines);
|
|
11538
|
+
if (console2) capture.console = console2;
|
|
11539
|
+
}
|
|
11540
|
+
if (opts.evidence) {
|
|
11541
|
+
const evidence = extractEvidence(input.attachments);
|
|
11542
|
+
if (evidence) capture.evidence = evidence;
|
|
11543
|
+
}
|
|
11544
|
+
if (Object.keys(capture).length === 0) return void 0;
|
|
11545
|
+
capture.source = source;
|
|
11546
|
+
return capture;
|
|
11547
|
+
}
|
|
11548
|
+
function chunkToString(chunk) {
|
|
11549
|
+
return typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
11550
|
+
}
|
|
11551
|
+
function decodeShardChunk(c) {
|
|
11552
|
+
return c.text ?? (c.buffer ? Buffer.from(c.buffer, "base64").toString("utf8") : "");
|
|
11553
|
+
}
|
|
11554
|
+
function executionFromResult(result, opts) {
|
|
11555
|
+
return buildExecution(
|
|
11556
|
+
{
|
|
11557
|
+
steps: result.steps,
|
|
11558
|
+
stdout: result.stdout.map(chunkToString),
|
|
11559
|
+
stderr: result.stderr.map(chunkToString),
|
|
11560
|
+
attachments: result.attachments
|
|
11561
|
+
},
|
|
11562
|
+
opts,
|
|
11563
|
+
"reporter"
|
|
11564
|
+
);
|
|
11565
|
+
}
|
|
11566
|
+
|
|
11389
11567
|
// src/collector/SuiteWalker.ts
|
|
11390
11568
|
init_cjs_shims();
|
|
11391
11569
|
|
|
@@ -11595,7 +11773,9 @@ function gitCommit() {
|
|
|
11595
11773
|
|
|
11596
11774
|
// src/collector/DataCollector.ts
|
|
11597
11775
|
var DataCollector = class {
|
|
11598
|
-
|
|
11776
|
+
/** @param captureOpts Per-run rich-capture knobs (steps/console/evidence); all off by default. */
|
|
11777
|
+
constructor(captureOpts = {}) {
|
|
11778
|
+
this.captureOpts = captureOpts;
|
|
11599
11779
|
this.resultMap = /* @__PURE__ */ new Map();
|
|
11600
11780
|
this.failureRecords = [];
|
|
11601
11781
|
this.startTime = Date.now();
|
|
@@ -11665,7 +11845,9 @@ var DataCollector = class {
|
|
|
11665
11845
|
duration: result.duration
|
|
11666
11846
|
}
|
|
11667
11847
|
],
|
|
11668
|
-
|
|
11848
|
+
durationMs: result.duration,
|
|
11849
|
+
severity,
|
|
11850
|
+
execution: executionFromResult(result, this.captureOpts)
|
|
11669
11851
|
};
|
|
11670
11852
|
}
|
|
11671
11853
|
inferSeverity(test) {
|
|
@@ -11771,6 +11953,10 @@ var import_fs3 = require("fs");
|
|
|
11771
11953
|
var import_glob = require("glob");
|
|
11772
11954
|
var import_node_module = require("module");
|
|
11773
11955
|
var ShardMerger = class {
|
|
11956
|
+
/** @param captureOpts Per-run rich-capture knobs; all off by default (parity with DataCollector). */
|
|
11957
|
+
constructor(captureOpts = {}) {
|
|
11958
|
+
this.captureOpts = captureOpts;
|
|
11959
|
+
}
|
|
11774
11960
|
merge(filePaths) {
|
|
11775
11961
|
if (filePaths.length === 0) throw new Error("shardResults: no valid JSON shard files found");
|
|
11776
11962
|
const resolved = this.resolveShardPaths(filePaths);
|
|
@@ -11975,7 +12161,21 @@ var ShardMerger = class {
|
|
|
11975
12161
|
status: r.status,
|
|
11976
12162
|
duration: r.duration
|
|
11977
12163
|
})),
|
|
11978
|
-
|
|
12164
|
+
durationMs: lastResult?.duration ?? 0,
|
|
12165
|
+
severity: "medium",
|
|
12166
|
+
execution: lastResult ? buildExecution(
|
|
12167
|
+
{
|
|
12168
|
+
// Guard each field — a partial / non-conformant / older shard JSON can
|
|
12169
|
+
// omit steps or the stdout/stderr arrays; an unguarded .map/iterate would
|
|
12170
|
+
// throw and (via the merge catch) silently skip the entire PDF.
|
|
12171
|
+
steps: lastResult.steps ?? [],
|
|
12172
|
+
stdout: (lastResult.stdout ?? []).map(decodeShardChunk),
|
|
12173
|
+
stderr: (lastResult.stderr ?? []).map(decodeShardChunk),
|
|
12174
|
+
attachments: lastResult.attachments ?? []
|
|
12175
|
+
},
|
|
12176
|
+
this.captureOpts,
|
|
12177
|
+
"shard"
|
|
12178
|
+
) : void 0
|
|
11979
12179
|
};
|
|
11980
12180
|
}
|
|
11981
12181
|
buildEnvironment(report) {
|
|
@@ -13292,10 +13492,10 @@ var NotificationSender = class {
|
|
|
13292
13492
|
var PdfReporter = class {
|
|
13293
13493
|
constructor(rawOptions = {}) {
|
|
13294
13494
|
this.cwd = process.cwd();
|
|
13295
|
-
this.dataCollector = new DataCollector();
|
|
13296
13495
|
this.pdfGenerator = new PdfGenerator();
|
|
13297
13496
|
this.options = parseOptions(rawOptions);
|
|
13298
|
-
|
|
13497
|
+
this.dataCollector = new DataCollector(this.options.capture);
|
|
13498
|
+
const version = true ? "0.8.0" : "0.x";
|
|
13299
13499
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
13300
13500
|
this.licenseClient = new LicenseClient({
|
|
13301
13501
|
licenseKey: this.options.licenseKey,
|
|
@@ -13342,7 +13542,7 @@ var PdfReporter = class {
|
|
|
13342
13542
|
_collectData(result) {
|
|
13343
13543
|
if (this.options.shardResults && this.options.shardResults.length > 0) {
|
|
13344
13544
|
try {
|
|
13345
|
-
return new ShardMerger().merge(this.options.shardResults);
|
|
13545
|
+
return new ShardMerger(this.options.capture).merge(this.options.shardResults);
|
|
13346
13546
|
} catch (err) {
|
|
13347
13547
|
logger.warn(`shardResults merge failed: ${err.message}. PDF generation skipped.`);
|
|
13348
13548
|
return null;
|
package/dist/index.mjs
CHANGED
|
@@ -10477,6 +10477,14 @@ var failureAnalysisConfigSchema = external_exports.object({
|
|
|
10477
10477
|
collectUnclassified: external_exports.boolean().default(true),
|
|
10478
10478
|
autoUpdateModel: external_exports.boolean().default(true)
|
|
10479
10479
|
}).default({});
|
|
10480
|
+
var captureConfigSchema = external_exports.object({
|
|
10481
|
+
steps: external_exports.boolean().default(false),
|
|
10482
|
+
apiSteps: external_exports.boolean().default(false),
|
|
10483
|
+
console: external_exports.boolean().default(false),
|
|
10484
|
+
evidence: external_exports.boolean().default(false),
|
|
10485
|
+
maxSteps: external_exports.number().int().min(1).max(500).default(50),
|
|
10486
|
+
maxConsoleLines: external_exports.number().int().min(1).max(1e3).default(50)
|
|
10487
|
+
}).default({});
|
|
10480
10488
|
var sectionFlagsShape = Object.fromEntries(
|
|
10481
10489
|
SECTION_KEYS.map((k) => [k, external_exports.boolean().optional()])
|
|
10482
10490
|
);
|
|
@@ -10517,6 +10525,7 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10517
10525
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
10518
10526
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10519
10527
|
failureAnalysis: failureAnalysisConfigSchema,
|
|
10528
|
+
capture: captureConfigSchema,
|
|
10520
10529
|
templatePath: external_exports.union([
|
|
10521
10530
|
external_exports.string().regex(/\.hbs$/, "templatePath must end with .hbs").refine((v) => v.slice(0, -4).trim().length > 0, "templatePath basename must not be empty"),
|
|
10522
10531
|
external_exports.array(
|
|
@@ -11119,6 +11128,12 @@ var STRENGTH = {
|
|
|
11119
11128
|
var STRENGTH_LEVELS = ["weak", "moderate", "strong"];
|
|
11120
11129
|
var OVERRIDE_MARGIN = 999;
|
|
11121
11130
|
|
|
11131
|
+
// src/utils/truncate.ts
|
|
11132
|
+
init_esm_shims();
|
|
11133
|
+
function clampWithEllipsis(text, max) {
|
|
11134
|
+
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
11135
|
+
}
|
|
11136
|
+
|
|
11122
11137
|
// src/analysis/FailureClusterer.ts
|
|
11123
11138
|
var MAX_TESTS_PER_CLUSTER = 20;
|
|
11124
11139
|
var MAX_MESSAGE_CHARS = 200;
|
|
@@ -11162,8 +11177,7 @@ function topFrame(stack) {
|
|
|
11162
11177
|
return void 0;
|
|
11163
11178
|
}
|
|
11164
11179
|
function representativeMessage(message) {
|
|
11165
|
-
|
|
11166
|
-
return firstLine.length > MAX_MESSAGE_CHARS ? `${firstLine.slice(0, MAX_MESSAGE_CHARS)}\u2026` : firstLine;
|
|
11180
|
+
return clampWithEllipsis(message.split("\n")[0], MAX_MESSAGE_CHARS);
|
|
11167
11181
|
}
|
|
11168
11182
|
function classifyOne(f, model) {
|
|
11169
11183
|
const frame = topFrame(f.error.stack);
|
|
@@ -11387,6 +11401,170 @@ function stripAnsi(input) {
|
|
|
11387
11401
|
return input.replace(ANSI_PATTERN, "");
|
|
11388
11402
|
}
|
|
11389
11403
|
|
|
11404
|
+
// src/collector/execution-capture.ts
|
|
11405
|
+
init_esm_shims();
|
|
11406
|
+
var MAX_TITLE_CHARS = 200;
|
|
11407
|
+
var DEFAULT_MAX_STEPS = 50;
|
|
11408
|
+
var DEFAULT_MAX_CONSOLE_LINES = 50;
|
|
11409
|
+
var PW_API_WINDOW = 10;
|
|
11410
|
+
var isTeardown = (category) => category === "hook" || category === "fixture";
|
|
11411
|
+
function flatten(steps) {
|
|
11412
|
+
const out = [];
|
|
11413
|
+
const walk = (nodes, depth, ancestors) => {
|
|
11414
|
+
for (const s of nodes ?? []) {
|
|
11415
|
+
const index = out.length;
|
|
11416
|
+
out.push({
|
|
11417
|
+
raw: s,
|
|
11418
|
+
category: s.category ?? "",
|
|
11419
|
+
depth,
|
|
11420
|
+
hasError: s.error != null,
|
|
11421
|
+
ancestors
|
|
11422
|
+
});
|
|
11423
|
+
if (s.steps?.length) walk(s.steps, depth + 1, [...ancestors, index]);
|
|
11424
|
+
}
|
|
11425
|
+
};
|
|
11426
|
+
walk(steps ?? [], 0, []);
|
|
11427
|
+
return out;
|
|
11428
|
+
}
|
|
11429
|
+
function materialize(n) {
|
|
11430
|
+
const loc = n.raw.location;
|
|
11431
|
+
return {
|
|
11432
|
+
title: clampWithEllipsis(n.raw.title ?? "", MAX_TITLE_CHARS),
|
|
11433
|
+
category: n.category,
|
|
11434
|
+
durationMs: Math.max(0, Math.round(n.raw.duration ?? 0)),
|
|
11435
|
+
depth: n.depth,
|
|
11436
|
+
failed: n.hasError,
|
|
11437
|
+
errorMessage: n.raw.error?.message ? clampWithEllipsis(stripAnsi(n.raw.error.message).split("\n")[0], MAX_TITLE_CHARS) : void 0,
|
|
11438
|
+
location: loc?.file != null ? `${loc.file}:${loc.line ?? 0}` : void 0
|
|
11439
|
+
};
|
|
11440
|
+
}
|
|
11441
|
+
function pickFailingIdx(flat) {
|
|
11442
|
+
const inTeardown = (f) => isTeardown(f.category) || f.ancestors.some((a) => isTeardown(flat[a].category));
|
|
11443
|
+
const pick = (allowTeardown) => {
|
|
11444
|
+
let idx = -1;
|
|
11445
|
+
for (let i = 0; i < flat.length; i++) {
|
|
11446
|
+
const f = flat[i];
|
|
11447
|
+
if (!f.hasError || !allowTeardown && inTeardown(f)) continue;
|
|
11448
|
+
if (idx === -1 || f.depth >= flat[idx].depth) idx = i;
|
|
11449
|
+
}
|
|
11450
|
+
return idx;
|
|
11451
|
+
};
|
|
11452
|
+
const body = pick(false);
|
|
11453
|
+
return body >= 0 ? body : pick(true);
|
|
11454
|
+
}
|
|
11455
|
+
function compactSteps(rawSteps, opts = {}) {
|
|
11456
|
+
const flat = flatten(rawSteps);
|
|
11457
|
+
if (flat.length === 0) return { steps: [] };
|
|
11458
|
+
const maxSteps = opts.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
11459
|
+
const failingIdx = pickFailingIdx(flat);
|
|
11460
|
+
const keep = /* @__PURE__ */ new Set();
|
|
11461
|
+
flat.forEach((f, i) => {
|
|
11462
|
+
if (f.category === "test.step" || f.category === "expect") keep.add(i);
|
|
11463
|
+
});
|
|
11464
|
+
if (failingIdx >= 0) {
|
|
11465
|
+
keep.add(failingIdx);
|
|
11466
|
+
for (const a of flat[failingIdx].ancestors) keep.add(a);
|
|
11467
|
+
if (opts.apiSteps) {
|
|
11468
|
+
const pwApiBefore = [];
|
|
11469
|
+
for (let i = 0; i < failingIdx; i++) {
|
|
11470
|
+
if (flat[i].category === "pw:api") pwApiBefore.push(i);
|
|
11471
|
+
}
|
|
11472
|
+
for (const i of pwApiBefore.slice(-PW_API_WINDOW)) keep.add(i);
|
|
11473
|
+
}
|
|
11474
|
+
}
|
|
11475
|
+
let keepArr = [...keep].sort((a, b) => a - b);
|
|
11476
|
+
if (keepArr.length > maxSteps) {
|
|
11477
|
+
keepArr = keepArr.slice(-maxSteps);
|
|
11478
|
+
if (failingIdx >= 0 && !keepArr.includes(failingIdx)) {
|
|
11479
|
+
keepArr = [failingIdx, ...keepArr.slice(1)];
|
|
11480
|
+
}
|
|
11481
|
+
}
|
|
11482
|
+
const cache = /* @__PURE__ */ new Map();
|
|
11483
|
+
const mat = (i) => {
|
|
11484
|
+
let es = cache.get(i);
|
|
11485
|
+
if (!es) {
|
|
11486
|
+
es = materialize(flat[i]);
|
|
11487
|
+
cache.set(i, es);
|
|
11488
|
+
}
|
|
11489
|
+
return es;
|
|
11490
|
+
};
|
|
11491
|
+
return {
|
|
11492
|
+
steps: keepArr.map(mat),
|
|
11493
|
+
failingStep: failingIdx >= 0 ? mat(failingIdx) : void 0
|
|
11494
|
+
};
|
|
11495
|
+
}
|
|
11496
|
+
function tailConsole(stdout, stderr, maxLines = DEFAULT_MAX_CONSOLE_LINES) {
|
|
11497
|
+
const tail = (chunks) => {
|
|
11498
|
+
if (chunks.length === 0) return { lines: [], truncated: false };
|
|
11499
|
+
const all = chunks.join("").split(/\r?\n/);
|
|
11500
|
+
if (all.length > 0 && all[all.length - 1] === "") all.pop();
|
|
11501
|
+
const lines = all.slice(-maxLines).map(stripAnsi);
|
|
11502
|
+
return { lines, truncated: all.length > lines.length };
|
|
11503
|
+
};
|
|
11504
|
+
const o = tail(stdout);
|
|
11505
|
+
const e = tail(stderr);
|
|
11506
|
+
if (o.lines.length === 0 && e.lines.length === 0) return void 0;
|
|
11507
|
+
return {
|
|
11508
|
+
stdout: o.lines,
|
|
11509
|
+
stderr: e.lines,
|
|
11510
|
+
truncated: o.truncated || e.truncated,
|
|
11511
|
+
stdoutTruncated: o.truncated,
|
|
11512
|
+
stderrTruncated: e.truncated
|
|
11513
|
+
};
|
|
11514
|
+
}
|
|
11515
|
+
function extractEvidence(attachments) {
|
|
11516
|
+
let tracePath;
|
|
11517
|
+
let videoPath;
|
|
11518
|
+
for (const a of attachments) {
|
|
11519
|
+
if (!a.path) continue;
|
|
11520
|
+
const looksLikeTrace = a.name === "trace" || a.contentType === "application/zip" && (a.name ?? "").includes("trace");
|
|
11521
|
+
if (!tracePath && looksLikeTrace) tracePath = a.path;
|
|
11522
|
+
if (!videoPath && a.contentType?.startsWith("video/")) videoPath = a.path;
|
|
11523
|
+
}
|
|
11524
|
+
if (!tracePath && !videoPath) return void 0;
|
|
11525
|
+
return { tracePath, videoPath };
|
|
11526
|
+
}
|
|
11527
|
+
function buildExecution(input, opts, source) {
|
|
11528
|
+
const capture = {};
|
|
11529
|
+
if (opts.steps) {
|
|
11530
|
+
const { steps, failingStep } = compactSteps(input.steps, {
|
|
11531
|
+
maxSteps: opts.maxSteps,
|
|
11532
|
+
apiSteps: opts.apiSteps
|
|
11533
|
+
});
|
|
11534
|
+
if (steps.length > 0) capture.steps = steps;
|
|
11535
|
+
if (failingStep) capture.failingStep = failingStep;
|
|
11536
|
+
}
|
|
11537
|
+
if (opts.console) {
|
|
11538
|
+
const console2 = tailConsole(input.stdout, input.stderr, opts.maxConsoleLines);
|
|
11539
|
+
if (console2) capture.console = console2;
|
|
11540
|
+
}
|
|
11541
|
+
if (opts.evidence) {
|
|
11542
|
+
const evidence = extractEvidence(input.attachments);
|
|
11543
|
+
if (evidence) capture.evidence = evidence;
|
|
11544
|
+
}
|
|
11545
|
+
if (Object.keys(capture).length === 0) return void 0;
|
|
11546
|
+
capture.source = source;
|
|
11547
|
+
return capture;
|
|
11548
|
+
}
|
|
11549
|
+
function chunkToString(chunk) {
|
|
11550
|
+
return typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
11551
|
+
}
|
|
11552
|
+
function decodeShardChunk(c) {
|
|
11553
|
+
return c.text ?? (c.buffer ? Buffer.from(c.buffer, "base64").toString("utf8") : "");
|
|
11554
|
+
}
|
|
11555
|
+
function executionFromResult(result, opts) {
|
|
11556
|
+
return buildExecution(
|
|
11557
|
+
{
|
|
11558
|
+
steps: result.steps,
|
|
11559
|
+
stdout: result.stdout.map(chunkToString),
|
|
11560
|
+
stderr: result.stderr.map(chunkToString),
|
|
11561
|
+
attachments: result.attachments
|
|
11562
|
+
},
|
|
11563
|
+
opts,
|
|
11564
|
+
"reporter"
|
|
11565
|
+
);
|
|
11566
|
+
}
|
|
11567
|
+
|
|
11390
11568
|
// src/collector/SuiteWalker.ts
|
|
11391
11569
|
init_esm_shims();
|
|
11392
11570
|
|
|
@@ -11596,7 +11774,9 @@ function gitCommit() {
|
|
|
11596
11774
|
|
|
11597
11775
|
// src/collector/DataCollector.ts
|
|
11598
11776
|
var DataCollector = class {
|
|
11599
|
-
|
|
11777
|
+
/** @param captureOpts Per-run rich-capture knobs (steps/console/evidence); all off by default. */
|
|
11778
|
+
constructor(captureOpts = {}) {
|
|
11779
|
+
this.captureOpts = captureOpts;
|
|
11600
11780
|
this.resultMap = /* @__PURE__ */ new Map();
|
|
11601
11781
|
this.failureRecords = [];
|
|
11602
11782
|
this.startTime = Date.now();
|
|
@@ -11666,7 +11846,9 @@ var DataCollector = class {
|
|
|
11666
11846
|
duration: result.duration
|
|
11667
11847
|
}
|
|
11668
11848
|
],
|
|
11669
|
-
|
|
11849
|
+
durationMs: result.duration,
|
|
11850
|
+
severity,
|
|
11851
|
+
execution: executionFromResult(result, this.captureOpts)
|
|
11670
11852
|
};
|
|
11671
11853
|
}
|
|
11672
11854
|
inferSeverity(test) {
|
|
@@ -11772,6 +11954,10 @@ import { readFileSync } from "fs";
|
|
|
11772
11954
|
import { globSync } from "glob";
|
|
11773
11955
|
import { createRequire } from "module";
|
|
11774
11956
|
var ShardMerger = class {
|
|
11957
|
+
/** @param captureOpts Per-run rich-capture knobs; all off by default (parity with DataCollector). */
|
|
11958
|
+
constructor(captureOpts = {}) {
|
|
11959
|
+
this.captureOpts = captureOpts;
|
|
11960
|
+
}
|
|
11775
11961
|
merge(filePaths) {
|
|
11776
11962
|
if (filePaths.length === 0) throw new Error("shardResults: no valid JSON shard files found");
|
|
11777
11963
|
const resolved = this.resolveShardPaths(filePaths);
|
|
@@ -11976,7 +12162,21 @@ var ShardMerger = class {
|
|
|
11976
12162
|
status: r.status,
|
|
11977
12163
|
duration: r.duration
|
|
11978
12164
|
})),
|
|
11979
|
-
|
|
12165
|
+
durationMs: lastResult?.duration ?? 0,
|
|
12166
|
+
severity: "medium",
|
|
12167
|
+
execution: lastResult ? buildExecution(
|
|
12168
|
+
{
|
|
12169
|
+
// Guard each field — a partial / non-conformant / older shard JSON can
|
|
12170
|
+
// omit steps or the stdout/stderr arrays; an unguarded .map/iterate would
|
|
12171
|
+
// throw and (via the merge catch) silently skip the entire PDF.
|
|
12172
|
+
steps: lastResult.steps ?? [],
|
|
12173
|
+
stdout: (lastResult.stdout ?? []).map(decodeShardChunk),
|
|
12174
|
+
stderr: (lastResult.stderr ?? []).map(decodeShardChunk),
|
|
12175
|
+
attachments: lastResult.attachments ?? []
|
|
12176
|
+
},
|
|
12177
|
+
this.captureOpts,
|
|
12178
|
+
"shard"
|
|
12179
|
+
) : void 0
|
|
11980
12180
|
};
|
|
11981
12181
|
}
|
|
11982
12182
|
buildEnvironment(report) {
|
|
@@ -13293,10 +13493,10 @@ var NotificationSender = class {
|
|
|
13293
13493
|
var PdfReporter = class {
|
|
13294
13494
|
constructor(rawOptions = {}) {
|
|
13295
13495
|
this.cwd = process.cwd();
|
|
13296
|
-
this.dataCollector = new DataCollector();
|
|
13297
13496
|
this.pdfGenerator = new PdfGenerator();
|
|
13298
13497
|
this.options = parseOptions(rawOptions);
|
|
13299
|
-
|
|
13498
|
+
this.dataCollector = new DataCollector(this.options.capture);
|
|
13499
|
+
const version = true ? "0.8.0" : "0.x";
|
|
13300
13500
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
13301
13501
|
this.licenseClient = new LicenseClient({
|
|
13302
13502
|
licenseKey: this.options.licenseKey,
|
|
@@ -13343,7 +13543,7 @@ var PdfReporter = class {
|
|
|
13343
13543
|
_collectData(result) {
|
|
13344
13544
|
if (this.options.shardResults && this.options.shardResults.length > 0) {
|
|
13345
13545
|
try {
|
|
13346
|
-
return new ShardMerger().merge(this.options.shardResults);
|
|
13546
|
+
return new ShardMerger(this.options.capture).merge(this.options.shardResults);
|
|
13347
13547
|
} catch (err) {
|
|
13348
13548
|
logger.warn(`shardResults merge failed: ${err.message}. PDF generation skipped.`);
|
|
13349
13549
|
return null;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<th>Title</th>
|
|
9
9
|
<th style="width:100px;">Suite</th>
|
|
10
10
|
<th style="width:80px;">Severity</th>
|
|
11
|
-
<th style="width:
|
|
11
|
+
<th style="width:72px;text-align:right;">Duration</th>
|
|
12
12
|
</tr>
|
|
13
13
|
</thead>
|
|
14
14
|
<tbody>
|
|
@@ -18,8 +18,52 @@
|
|
|
18
18
|
<td style="font-weight:500;">{{testTitle}}</td>
|
|
19
19
|
<td style="font-size:9px;color:var(--text-3);font-family:'JetBrains Mono',monospace;">{{lastItem suitePath}}</td>
|
|
20
20
|
<td><span class="severity--{{severity}}">{{upperCase severity}}</span></td>
|
|
21
|
-
<td style="text-align:
|
|
21
|
+
<td style="text-align:right;color:var(--text-3);font-family:'JetBrains Mono',monospace;">{{formatDuration durationMs}}</td>
|
|
22
22
|
</tr>
|
|
23
|
+
{{#if execution}}
|
|
24
|
+
<tr class="defect-detail-row">
|
|
25
|
+
<td colspan="5">
|
|
26
|
+
<div class="defect-detail">
|
|
27
|
+
{{#if error.message}}
|
|
28
|
+
<div class="defect-detail__error">{{error.message}}</div>
|
|
29
|
+
{{/if}}
|
|
30
|
+
|
|
31
|
+
{{#if execution.steps.length}}
|
|
32
|
+
<div class="repro">
|
|
33
|
+
<div class="repro__tag">Steps to Reproduce</div>
|
|
34
|
+
<ol class="repro-steps">
|
|
35
|
+
{{#each execution.steps}}
|
|
36
|
+
<li class="repro-step{{#if failed}} repro-step--failed{{/if}}" style="--d:{{depth}};">
|
|
37
|
+
<span class="repro-step__cat">{{category}}</span>
|
|
38
|
+
<span class="repro-step__title">{{title}}</span>
|
|
39
|
+
{{#if failed}}{{#if location}}<span class="repro-step__loc">{{location}}</span>{{/if}}{{/if}}
|
|
40
|
+
<span class="repro-step__dur">{{formatDuration durationMs}}</span>
|
|
41
|
+
</li>
|
|
42
|
+
{{/each}}
|
|
43
|
+
</ol>
|
|
44
|
+
</div>
|
|
45
|
+
{{/if}}
|
|
46
|
+
|
|
47
|
+
{{#if execution.console}}
|
|
48
|
+
<div class="console-tail">
|
|
49
|
+
<div class="console-tail__tag">Console</div>
|
|
50
|
+
{{#if execution.console.stdoutTruncated}}<div class="console-tail__more">… earlier stdout truncated</div>{{/if}}
|
|
51
|
+
{{#each execution.console.stdout}}<div class="console-tail__line"><span class="console-tail__stream">out</span>{{this}}</div>{{/each}}
|
|
52
|
+
{{#if execution.console.stderrTruncated}}<div class="console-tail__more">… earlier stderr truncated</div>{{/if}}
|
|
53
|
+
{{#each execution.console.stderr}}<div class="console-tail__line console-tail__line--err"><span class="console-tail__stream">err</span>{{this}}</div>{{/each}}
|
|
54
|
+
</div>
|
|
55
|
+
{{/if}}
|
|
56
|
+
|
|
57
|
+
{{#if execution.evidence}}
|
|
58
|
+
<div class="evidence-links">
|
|
59
|
+
{{#if execution.evidence.tracePath}}<span class="evidence-link"><span class="evidence-link__tag">Trace</span><span class="evidence-link__path">{{execution.evidence.tracePath}}</span></span>{{/if}}
|
|
60
|
+
{{#if execution.evidence.videoPath}}<span class="evidence-link"><span class="evidence-link__tag">Video</span><span class="evidence-link__path">{{execution.evidence.videoPath}}</span></span>{{/if}}
|
|
61
|
+
</div>
|
|
62
|
+
{{/if}}
|
|
63
|
+
</div>
|
|
64
|
+
</td>
|
|
65
|
+
</tr>
|
|
66
|
+
{{/if}}
|
|
23
67
|
{{/each}}
|
|
24
68
|
</tbody>
|
|
25
69
|
</table>
|
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
<div class="failure-item__message">{{failure.error.message}}</div>
|
|
12
12
|
{{/if}}
|
|
13
13
|
|
|
14
|
+
{{#if (and showFull failure.execution.failingStep)}}
|
|
15
|
+
<div class="failing-step">
|
|
16
|
+
<span class="failing-step__tag">Failed at</span>
|
|
17
|
+
<span class="failing-step__title">{{failure.execution.failingStep.title}}</span>
|
|
18
|
+
{{#if failure.execution.failingStep.location}}<span class="failing-step__loc">{{failure.execution.failingStep.location}}</span>{{/if}}
|
|
19
|
+
</div>
|
|
20
|
+
{{/if}}
|
|
21
|
+
|
|
14
22
|
{{#if (and showFull failure.screenshotBase64)}}
|
|
15
23
|
<img class="screenshot" src="{{failure.screenshotBase64}}" alt="Screenshot at failure">
|
|
16
24
|
{{/if}}
|
|
@@ -404,6 +404,79 @@ code {
|
|
|
404
404
|
.root-cause-chip--env-config .root-cause-chip__label { color: oklch(48% 0.14 145); }
|
|
405
405
|
.root-cause-chip--unknown .root-cause-chip__label { color: var(--text-2); }
|
|
406
406
|
|
|
407
|
+
/* ── Repro detail for the defect-log section (opt-in `capture`) ──────────────
|
|
408
|
+
Rendered only when the capture option is enabled and data is present. Quiet,
|
|
409
|
+
design-system-consistent: a light bordered card (NO side rail, per the
|
|
410
|
+
side-stripe ban in DESIGN.md), with hairline dividers between sub-blocks —
|
|
411
|
+
mirroring the reworked .failure-detail and .retry-history patterns. */
|
|
412
|
+
.defect-detail-row td { padding: 0; border-top: none; }
|
|
413
|
+
.defect-detail {
|
|
414
|
+
border: 1px solid var(--border-2);
|
|
415
|
+
border-radius: 6px;
|
|
416
|
+
margin: 2px 0 12px;
|
|
417
|
+
padding: 10px 12px 11px;
|
|
418
|
+
}
|
|
419
|
+
.defect-detail__error {
|
|
420
|
+
font-family: 'JetBrains Mono', monospace;
|
|
421
|
+
font-size: 9.5px; color: #8A2E1E; line-height: 1.6; word-break: break-word;
|
|
422
|
+
}
|
|
423
|
+
.failing-step {
|
|
424
|
+
margin-top: 9px; font-size: 9.5px;
|
|
425
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
426
|
+
}
|
|
427
|
+
.failing-step__tag, .repro__tag, .console-tail__tag, .evidence-link__tag {
|
|
428
|
+
font-size: 8px; font-weight: 700; letter-spacing: 0.8px; text-transform: uppercase;
|
|
429
|
+
color: var(--text-3);
|
|
430
|
+
}
|
|
431
|
+
.failing-step__title { font-weight: 700; color: var(--text-1); margin-left: 6px; }
|
|
432
|
+
.failing-step__loc {
|
|
433
|
+
font-family: 'JetBrains Mono', monospace; font-size: 8.5px; color: var(--text-3); margin-left: 6px;
|
|
434
|
+
}
|
|
435
|
+
.repro { margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--border); }
|
|
436
|
+
.repro-steps { list-style: none; margin: 6px 0 0; padding: 0; }
|
|
437
|
+
.repro-step {
|
|
438
|
+
display: flex; align-items: baseline; gap: 8px;
|
|
439
|
+
/* indent by tree depth (capped) — the value comes from `--d` on the element */
|
|
440
|
+
padding: 2px 0 2px min(60px, calc(var(--d, 0) * 12px));
|
|
441
|
+
font-size: 9px; line-height: 1.5;
|
|
442
|
+
page-break-inside: avoid; break-inside: avoid;
|
|
443
|
+
}
|
|
444
|
+
.repro-step__cat {
|
|
445
|
+
flex: none; min-width: 52px;
|
|
446
|
+
font-family: 'JetBrains Mono', monospace; font-size: 8px; color: var(--text-3);
|
|
447
|
+
}
|
|
448
|
+
.repro-step__title { flex: 1; color: var(--text-2); word-break: break-word; }
|
|
449
|
+
.repro-step__loc {
|
|
450
|
+
flex: none; font-family: 'JetBrains Mono', monospace; font-size: 8px; color: var(--text-3);
|
|
451
|
+
}
|
|
452
|
+
.repro-step__dur { flex: none; color: var(--text-3); font-size: 8.5px; }
|
|
453
|
+
.repro-step--failed .repro-step__title { color: #8A2E1E; font-weight: 700; }
|
|
454
|
+
.repro-step--failed .repro-step__cat { color: var(--fail); }
|
|
455
|
+
.console-tail { margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--border); }
|
|
456
|
+
.console-tail__line {
|
|
457
|
+
font-family: 'JetBrains Mono', monospace; font-size: 8.5px; line-height: 1.55;
|
|
458
|
+
color: var(--text-2); word-break: break-word; white-space: pre-wrap;
|
|
459
|
+
}
|
|
460
|
+
/* Non-colour stream cue (out/err) so the two streams read without relying on colour. */
|
|
461
|
+
.console-tail__stream {
|
|
462
|
+
display: inline-block; width: 22px; margin-right: 8px; flex: none;
|
|
463
|
+
font-size: 7.5px; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase;
|
|
464
|
+
color: var(--text-3); vertical-align: baseline;
|
|
465
|
+
}
|
|
466
|
+
.console-tail__line--err { color: #8A2E1E; }
|
|
467
|
+
.console-tail__line--err .console-tail__stream { color: var(--fail); }
|
|
468
|
+
/* Truncation note now leads the tail (earlier lines were dropped — they precede what's shown). */
|
|
469
|
+
.console-tail__more { font-size: 8px; color: var(--text-3); margin: 0 0 4px; font-style: italic; }
|
|
470
|
+
.evidence-links {
|
|
471
|
+
margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--border);
|
|
472
|
+
display: flex; flex-wrap: wrap; gap: 14px;
|
|
473
|
+
}
|
|
474
|
+
.evidence-link { font-size: 9px; }
|
|
475
|
+
.evidence-link__path {
|
|
476
|
+
font-family: 'JetBrains Mono', monospace; font-size: 8.5px; color: var(--text-2);
|
|
477
|
+
margin-left: 5px; word-break: break-all;
|
|
478
|
+
}
|
|
479
|
+
|
|
407
480
|
/* ── History trend card — shared by detailed + executive (charts.hbs) ─────── */
|
|
408
481
|
.trend-card {
|
|
409
482
|
background: var(--surface);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reportforge/playwright-pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Enterprise-ready PDF reports for Playwright Test — minimal, detailed, and executive templates with CI/CD integrations",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ReportForge",
|