playwright-ctrf-json-reporter 0.0.28 → 0.0.30-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -165,6 +165,64 @@ The test object in the report includes the following [CTRF properties](https://c
165
165
  | `screenshot` | String | Optional | A base64 encoded screenshot taken during the test. |
166
166
  | `steps` | Array of Objects | Optional | Individual steps in the test, especially for BDD-style testing. |
167
167
 
168
+ ## Extra
169
+
170
+ The `extra` field lets you attach custom metadata to individual test results at runtime. This data is merged into the `extra` field of each test in the CTRF report.
171
+
172
+ See the [CTRF extra specification](https://www.ctrf.io/docs/specification/extra) for full details.
173
+
174
+ ### Usage
175
+
176
+ Import `ctrf` from the reporter and call `ctrf.extra()` inside any test:
177
+
178
+ ```typescript
179
+ import { test, expect } from '@playwright/test'
180
+ import { ctrf } from 'playwright-ctrf-json-reporter'
181
+
182
+ test('checkout flow', async ({ page }) => {
183
+ ctrf.extra({ owner: 'checkout-team', priority: 'P1' })
184
+
185
+ // ... test logic ...
186
+ })
187
+ ```
188
+
189
+ You can call it multiple times in a single test:
190
+
191
+ ```typescript
192
+ test('search results', async ({ page }) => {
193
+ ctrf.extra({ owner: 'search-team' })
194
+ ctrf.extra({ feature: 'search', environment: 'staging' })
195
+
196
+ // ... test logic ...
197
+
198
+ ctrf.extra({ customMetric: 'some-value' })
199
+ })
200
+ ```
201
+
202
+ The resulting `extra` field in the CTRF report:
203
+
204
+ ```json
205
+ {
206
+ "name": "search results",
207
+ "status": "passed",
208
+ "duration": 300,
209
+ "extra": {
210
+ "owner": "search-team",
211
+ "feature": "search",
212
+ "environment": "staging",
213
+ "customMetric": "some-value"
214
+ }
215
+ }
216
+ ```
217
+
218
+ ### Merge behaviour
219
+
220
+ | Data type | Behaviour | Example |
221
+ | ---------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------- |
222
+ | Primitives | Later call overwrites earlier | `extra({ owner: 'a' })` then `extra({ owner: 'b' })` → `{ owner: 'b' }` |
223
+ | Objects | Deep merged - nested keys preserved | `extra({ build: { id: '1' } })` then `extra({ build: { url: '...' } })` → `{ build: { id: '1', url: '...' } }` |
224
+ | Arrays | Concatenated across calls | `extra({ tags: ['smoke'] })` then `extra({ tags: ['e2e'] })` → `{ tags: ['smoke', 'e2e'] }` |
225
+
168
226
  ## Advanced usage
169
227
 
170
228
  Some features require additional setup or usage considerations.
package/dist/index.cjs ADDED
@@ -0,0 +1,534 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ ctrf: () => ctrf,
34
+ default: () => generate_report_default,
35
+ extra: () => extra
36
+ });
37
+ module.exports = __toCommonJS(src_exports);
38
+
39
+ // src/generate-report.ts
40
+ var import_node_path = __toESM(require("path"), 1);
41
+ var import_node_fs = __toESM(require("fs"), 1);
42
+ var import_node_crypto = __toESM(require("crypto"), 1);
43
+
44
+ // src/adapter/runtime.ts
45
+ var import_test = require("@playwright/test");
46
+ var CTRF_RUNTIME_MESSAGE_CONTENT_TYPE = "application/vnd.ctrf.message+json";
47
+ var nullTransport = {
48
+ send: async () => {
49
+ }
50
+ };
51
+ function createPlaywrightTransport() {
52
+ let sequence = 0;
53
+ return {
54
+ async send(payload) {
55
+ const tag = `__ctrf_${++sequence}_${Date.now()}`;
56
+ const data = JSON.stringify(payload);
57
+ await import_test.test.info().attach(tag, {
58
+ contentType: CTRF_RUNTIME_MESSAGE_CONTENT_TYPE,
59
+ body: Buffer.from(data)
60
+ });
61
+ }
62
+ };
63
+ }
64
+ var activeTransport = createPlaywrightTransport();
65
+ var resolveTransport = () => {
66
+ try {
67
+ import_test.test.info();
68
+ return activeTransport;
69
+ } catch {
70
+ }
71
+ return nullTransport;
72
+ };
73
+ async function extra(data) {
74
+ await resolveTransport().send({ type: "metadata", data });
75
+ }
76
+ var ctrf = { extra };
77
+
78
+ // src/generate-report.ts
79
+ var GenerateCtrfReport = class {
80
+ ctrfReport;
81
+ ctrfEnvironment;
82
+ reporterConfigOptions;
83
+ reporterName = "playwright-ctrf-json-reporter";
84
+ defaultOutputFile = "ctrf-report.json";
85
+ defaultOutputDir = "ctrf";
86
+ suite;
87
+ startTime;
88
+ constructor(config) {
89
+ this.reporterConfigOptions = {
90
+ outputFile: config?.outputFile ?? this.defaultOutputFile,
91
+ outputDir: config?.outputDir ?? this.defaultOutputDir,
92
+ minimal: config?.minimal ?? false,
93
+ screenshot: config?.screenshot ?? false,
94
+ annotations: config?.annotations ?? false,
95
+ testType: config?.testType ?? "e2e",
96
+ appName: config?.appName ?? void 0,
97
+ appVersion: config?.appVersion ?? void 0,
98
+ osPlatform: config?.osPlatform ?? void 0,
99
+ osRelease: config?.osRelease ?? void 0,
100
+ osVersion: config?.osVersion ?? void 0,
101
+ buildName: config?.buildName ?? void 0,
102
+ buildNumber: config?.buildNumber ?? void 0,
103
+ buildUrl: config?.buildUrl ?? void 0,
104
+ repositoryName: config?.repositoryName ?? void 0,
105
+ repositoryUrl: config?.repositoryUrl ?? void 0,
106
+ branchName: config?.branchName ?? void 0,
107
+ commit: config?.commit ?? void 0,
108
+ testEnvironment: config?.testEnvironment ?? void 0
109
+ };
110
+ this.ctrfReport = {
111
+ reportFormat: "CTRF",
112
+ specVersion: "0.0.0",
113
+ reportId: import_node_crypto.default.randomUUID(),
114
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
115
+ generatedBy: "playwright-ctrf-json-reporter",
116
+ results: {
117
+ tool: {
118
+ name: "playwright"
119
+ },
120
+ summary: {
121
+ tests: 0,
122
+ passed: 0,
123
+ failed: 0,
124
+ pending: 0,
125
+ skipped: 0,
126
+ other: 0,
127
+ start: 0,
128
+ stop: 0
129
+ },
130
+ tests: []
131
+ }
132
+ };
133
+ this.ctrfEnvironment = {};
134
+ }
135
+ onBegin(_config, suite) {
136
+ this.suite = suite;
137
+ this.startTime = Date.now();
138
+ this.ctrfReport.results.summary.start = this.startTime;
139
+ if (!import_node_fs.default.existsSync(
140
+ this.reporterConfigOptions.outputDir ?? this.defaultOutputDir
141
+ )) {
142
+ import_node_fs.default.mkdirSync(
143
+ this.reporterConfigOptions.outputDir ?? this.defaultOutputDir,
144
+ { recursive: true }
145
+ );
146
+ }
147
+ this.setEnvironmentDetails(this.reporterConfigOptions);
148
+ if (this.hasEnvironmentDetails(this.ctrfEnvironment)) {
149
+ this.ctrfReport.results.environment = this.ctrfEnvironment;
150
+ }
151
+ this.setFilename(
152
+ this.reporterConfigOptions.outputFile ?? this.defaultOutputFile
153
+ );
154
+ }
155
+ onEnd() {
156
+ this.ctrfReport.results.summary.stop = Date.now();
157
+ if (this.suite !== void 0) {
158
+ if (this.suite.allTests().length > 0) {
159
+ this.processSuite(this.suite);
160
+ this.ctrfReport.results.summary.suites = this.countSuites(this.suite);
161
+ }
162
+ }
163
+ this.writeReportToFile(this.ctrfReport);
164
+ }
165
+ printsToStdio() {
166
+ return false;
167
+ }
168
+ processSuite(suite) {
169
+ for (const test2 of suite.tests) {
170
+ this.processTest(test2);
171
+ }
172
+ for (const childSuite of suite.suites) {
173
+ this.processSuite(childSuite);
174
+ }
175
+ }
176
+ processTest(testCase) {
177
+ if (testCase.results.length === 0) {
178
+ return;
179
+ }
180
+ const latestResult = testCase.results[testCase.results.length - 1];
181
+ if (latestResult !== void 0) {
182
+ this.updateCtrfTestResultsFromTestResult(
183
+ testCase,
184
+ latestResult,
185
+ this.ctrfReport
186
+ );
187
+ this.updateSummaryFromTestResult(latestResult, this.ctrfReport);
188
+ }
189
+ }
190
+ setFilename(filename) {
191
+ if (filename.endsWith(".json")) {
192
+ this.reporterConfigOptions.outputFile = filename;
193
+ } else {
194
+ this.reporterConfigOptions.outputFile = `${filename}.json`;
195
+ }
196
+ }
197
+ updateCtrfTestResultsFromTestResult(testCase, testResult, ctrfReport) {
198
+ const test2 = {
199
+ name: testCase.title,
200
+ status: testResult.status === testCase.expectedStatus && testResult.status !== "skipped" ? "passed" : this.mapPlaywrightStatusToCtrf(testResult.status),
201
+ duration: testResult.duration
202
+ };
203
+ if (this.reporterConfigOptions.minimal === false) {
204
+ test2.start = this.updateStart(testResult.startTime);
205
+ test2.stop = this.calculateStopTime(
206
+ testResult.startTime,
207
+ testResult.duration
208
+ );
209
+ test2.message = this.extractFailureDetails(testResult).message;
210
+ test2.trace = this.extractFailureDetails(testResult).trace;
211
+ test2.snippet = this.extractFailureDetails(testResult).snippet;
212
+ test2.rawStatus = testResult.status;
213
+ test2.tags = testCase.tags ?? [];
214
+ test2.type = this.reporterConfigOptions.testType ?? "e2e";
215
+ test2.filePath = testCase.location.file;
216
+ test2.retries = testResult.retry;
217
+ test2.flaky = testResult.status === "passed" && testResult.retry > 0;
218
+ test2.steps = [];
219
+ if (testResult.steps.length > 0) {
220
+ testResult.steps.forEach((step) => {
221
+ this.processStep(test2, step);
222
+ });
223
+ }
224
+ if (this.reporterConfigOptions.screenshot === true) {
225
+ test2.screenshot = this.extractScreenshotBase64(testResult);
226
+ }
227
+ test2.suite = this.buildSuitePath(testCase);
228
+ if (this.extractMetadata(testResult)?.name !== void 0 || this.extractMetadata(testResult)?.version !== void 0)
229
+ test2.browser = `${this.extractMetadata(testResult)?.name} ${this.extractMetadata(testResult)?.version}`;
230
+ test2.attachments = this.filterValidAttachments(testResult.attachments);
231
+ test2.stdout = testResult.stdout.map(
232
+ (item) => Buffer.isBuffer(item) ? item.toString() : String(item)
233
+ );
234
+ test2.stderr = testResult.stderr.map(
235
+ (item) => Buffer.isBuffer(item) ? item.toString() : String(item)
236
+ );
237
+ if (this.reporterConfigOptions.annotations !== void 0) {
238
+ test2.extra = { annotations: testCase.annotations };
239
+ }
240
+ const runtimeData = this.extractRuntimeData(testResult);
241
+ if (runtimeData !== null) {
242
+ test2.extra = { ...test2.extra, ...runtimeData };
243
+ }
244
+ if (testCase.results.length > 1) {
245
+ const retryResults = testCase.results.slice(0, -1);
246
+ test2.retryAttempts = [];
247
+ for (let i = 0; i < retryResults.length; i++) {
248
+ const retryResult = retryResults[i];
249
+ const retryAttempt = {
250
+ attempt: i + 1,
251
+ status: this.mapPlaywrightStatusToCtrf(retryResult.status),
252
+ duration: retryResult.duration,
253
+ message: this.extractFailureDetails(retryResult).message,
254
+ trace: this.extractFailureDetails(retryResult).trace,
255
+ snippet: this.extractFailureDetails(retryResult).snippet
256
+ };
257
+ test2.retryAttempts.push(retryAttempt);
258
+ }
259
+ }
260
+ }
261
+ ctrfReport.results.tests.push(test2);
262
+ }
263
+ updateSummaryFromTestResult(testResult, ctrfReport) {
264
+ ctrfReport.results.summary.tests++;
265
+ const ctrfStatus = this.mapPlaywrightStatusToCtrf(testResult.status);
266
+ if (ctrfStatus in ctrfReport.results.summary) {
267
+ ctrfReport.results.summary[ctrfStatus]++;
268
+ } else {
269
+ ctrfReport.results.summary.other++;
270
+ }
271
+ }
272
+ mapPlaywrightStatusToCtrf(testStatus) {
273
+ switch (testStatus) {
274
+ case "passed":
275
+ return "passed";
276
+ case "failed":
277
+ case "timedOut":
278
+ case "interrupted":
279
+ return "failed";
280
+ case "skipped":
281
+ return "skipped";
282
+ case "pending":
283
+ return "pending";
284
+ default:
285
+ return "other";
286
+ }
287
+ }
288
+ setEnvironmentDetails(reporterConfigOptions) {
289
+ if (reporterConfigOptions.appName !== void 0) {
290
+ this.ctrfEnvironment.appName = reporterConfigOptions.appName;
291
+ }
292
+ if (reporterConfigOptions.appVersion !== void 0) {
293
+ this.ctrfEnvironment.appVersion = reporterConfigOptions.appVersion;
294
+ }
295
+ if (reporterConfigOptions.osPlatform !== void 0) {
296
+ this.ctrfEnvironment.osPlatform = reporterConfigOptions.osPlatform;
297
+ }
298
+ if (reporterConfigOptions.osRelease !== void 0) {
299
+ this.ctrfEnvironment.osRelease = reporterConfigOptions.osRelease;
300
+ }
301
+ if (reporterConfigOptions.osVersion !== void 0) {
302
+ this.ctrfEnvironment.osVersion = reporterConfigOptions.osVersion;
303
+ }
304
+ if (reporterConfigOptions.buildName !== void 0) {
305
+ this.ctrfEnvironment.buildName = reporterConfigOptions.buildName;
306
+ }
307
+ if (reporterConfigOptions.buildNumber !== void 0) {
308
+ this.ctrfEnvironment.buildNumber = Number(
309
+ reporterConfigOptions.buildNumber
310
+ );
311
+ }
312
+ if (reporterConfigOptions.buildUrl !== void 0) {
313
+ this.ctrfEnvironment.buildUrl = reporterConfigOptions.buildUrl;
314
+ }
315
+ if (reporterConfigOptions.repositoryName !== void 0) {
316
+ this.ctrfEnvironment.repositoryName = reporterConfigOptions.repositoryName;
317
+ }
318
+ if (reporterConfigOptions.repositoryUrl !== void 0) {
319
+ this.ctrfEnvironment.repositoryUrl = reporterConfigOptions.repositoryUrl;
320
+ }
321
+ if (reporterConfigOptions.branchName !== void 0) {
322
+ this.ctrfEnvironment.branchName = reporterConfigOptions.branchName;
323
+ }
324
+ if (reporterConfigOptions.commit !== void 0) {
325
+ this.ctrfEnvironment.commit = reporterConfigOptions.commit;
326
+ }
327
+ if (reporterConfigOptions.testEnvironment !== void 0) {
328
+ this.ctrfEnvironment.testEnvironment = reporterConfigOptions.testEnvironment;
329
+ }
330
+ }
331
+ hasEnvironmentDetails(environment) {
332
+ return Object.keys(environment).length > 0;
333
+ }
334
+ extractMetadata(testResult) {
335
+ const metadataAttachment = testResult.attachments.find(
336
+ (attachment) => attachment.name === "metadata.json"
337
+ );
338
+ if (metadataAttachment?.body !== null && metadataAttachment?.body !== void 0) {
339
+ try {
340
+ const metadataRaw = metadataAttachment.body.toString("utf-8");
341
+ return JSON.parse(metadataRaw);
342
+ } catch (e) {
343
+ if (e instanceof Error) {
344
+ console.error(`Error parsing browser metadata: ${e.message}`);
345
+ } else {
346
+ console.error(
347
+ "An unknown error occurred in parsing browser metadata"
348
+ );
349
+ }
350
+ }
351
+ }
352
+ return null;
353
+ }
354
+ /**
355
+ * Deep merge with array concatenation.
356
+ * Arrays are concatenated, objects are recursively merged, primitives overwrite.
357
+ */
358
+ deepMerge(target, source) {
359
+ const result = { ...target };
360
+ for (const [key, sourceValue] of Object.entries(source)) {
361
+ const targetValue = result[key];
362
+ if (Array.isArray(sourceValue)) {
363
+ result[key] = Array.isArray(targetValue) ? [...targetValue, ...sourceValue] : [...sourceValue];
364
+ } else if (sourceValue !== null && typeof sourceValue === "object" && !Array.isArray(sourceValue)) {
365
+ result[key] = targetValue !== null && typeof targetValue === "object" && !Array.isArray(targetValue) ? this.deepMerge(
366
+ targetValue,
367
+ sourceValue
368
+ ) : { ...sourceValue };
369
+ } else {
370
+ result[key] = sourceValue;
371
+ }
372
+ }
373
+ return result;
374
+ }
375
+ /**
376
+ * Extract and merge runtime data from CTRF message attachments.
377
+ *
378
+ * Runtime messages are internal transport artifacts used to send data from
379
+ * test code to the reporter via test.info().attach(). They are identified
380
+ * by the CTRF_RUNTIME_MESSAGE_CONTENT_TYPE content type.
381
+ *
382
+ * ## Merge Semantics (deep merge with array concatenation)
383
+ *
384
+ * Messages are processed in attachment order with intelligent merging:
385
+ * - Arrays: Concatenated across calls (duplicates allowed)
386
+ * - Objects: Deep merged (nested keys preserved)
387
+ * - Primitives: Later values overwrite earlier ones
388
+ *
389
+ * @param testResult - The test result containing attachments
390
+ * @returns Merged runtime data or null if no CTRF messages found
391
+ */
392
+ extractRuntimeData(testResult) {
393
+ const runtimeData = {};
394
+ let hasData = false;
395
+ for (const attachment of testResult.attachments) {
396
+ if (attachment.contentType !== CTRF_RUNTIME_MESSAGE_CONTENT_TYPE) {
397
+ continue;
398
+ }
399
+ if (attachment.body === void 0) {
400
+ continue;
401
+ }
402
+ try {
403
+ const message = JSON.parse(
404
+ attachment.body.toString("utf-8")
405
+ );
406
+ if (message.type === "metadata" && message.data !== null) {
407
+ Object.assign(runtimeData, this.deepMerge(runtimeData, message.data));
408
+ hasData = true;
409
+ }
410
+ } catch (e) {
411
+ if (e instanceof Error) {
412
+ console.error(`Error parsing CTRF runtime message: ${e.message}`);
413
+ }
414
+ }
415
+ }
416
+ return hasData ? runtimeData : null;
417
+ }
418
+ updateStart(startTime) {
419
+ const date = new Date(startTime);
420
+ const unixEpochTime = Math.floor(date.getTime() / 1e3);
421
+ return unixEpochTime;
422
+ }
423
+ calculateStopTime(startTime, duration) {
424
+ const startDate = new Date(startTime);
425
+ const stopDate = new Date(startDate.getTime() + duration);
426
+ return Math.floor(stopDate.getTime() / 1e3);
427
+ }
428
+ // TODO(v1): change return type to string[] and update Test.suite to match canonical ctrf type.
429
+ buildSuitePath(test2) {
430
+ const pathComponents = [];
431
+ let currentSuite = test2.parent;
432
+ while (currentSuite !== void 0) {
433
+ if (currentSuite.title !== "") {
434
+ pathComponents.unshift(currentSuite.title);
435
+ }
436
+ currentSuite = currentSuite.parent;
437
+ }
438
+ return pathComponents.join(" > ");
439
+ }
440
+ extractScreenshotBase64(testResult) {
441
+ const screenshotAttachment = testResult.attachments.find(
442
+ (attachment) => attachment.name === "screenshot" && (attachment.contentType === "image/jpeg" || attachment.contentType === "image/png")
443
+ );
444
+ return screenshotAttachment?.body?.toString("base64");
445
+ }
446
+ extractFailureDetails(testResult) {
447
+ if ((testResult.status === "failed" || testResult.status === "timedOut" || testResult.status === "interrupted") && testResult.error !== void 0) {
448
+ const failureDetails = {};
449
+ if (testResult.error.message !== void 0) {
450
+ failureDetails.message = testResult.error.message;
451
+ }
452
+ if (testResult.error.stack !== void 0) {
453
+ failureDetails.trace = testResult.error.stack;
454
+ }
455
+ if (testResult.error.snippet !== void 0) {
456
+ failureDetails.snippet = testResult.error.snippet;
457
+ }
458
+ return failureDetails;
459
+ }
460
+ return {};
461
+ }
462
+ countSuites(suite) {
463
+ let count = 0;
464
+ suite.suites.forEach((childSuite) => {
465
+ count += this.countSuites(childSuite);
466
+ });
467
+ return count;
468
+ }
469
+ writeReportToFile(data) {
470
+ const filePath = import_node_path.default.join(
471
+ this.reporterConfigOptions.outputDir ?? this.defaultOutputDir,
472
+ this.reporterConfigOptions.outputFile ?? this.defaultOutputFile
473
+ );
474
+ const str = JSON.stringify(data, null, 2);
475
+ try {
476
+ import_node_fs.default.writeFileSync(filePath, `${str}
477
+ `);
478
+ console.log(
479
+ `${this.reporterName}: successfully written ctrf json to %s/%s`,
480
+ this.reporterConfigOptions.outputDir,
481
+ this.reporterConfigOptions.outputFile
482
+ );
483
+ } catch (error) {
484
+ console.error(`Error writing ctrf json report:, ${String(error)}`);
485
+ }
486
+ }
487
+ processStep(test2, step) {
488
+ if (step.category === "test.step") {
489
+ const stepStatus = step.error === void 0 ? this.mapPlaywrightStatusToCtrf("passed") : this.mapPlaywrightStatusToCtrf("failed");
490
+ const currentStep = {
491
+ name: step.title,
492
+ status: stepStatus
493
+ };
494
+ test2.steps?.push(currentStep);
495
+ }
496
+ const childSteps = step.steps;
497
+ if (childSteps.length > 0) {
498
+ childSteps.forEach((cStep) => {
499
+ this.processStep(test2, cStep);
500
+ });
501
+ }
502
+ }
503
+ /**
504
+ * Filter attachments to include in CTRF output.
505
+ *
506
+ * Excludes:
507
+ * - Attachments without a file path (body-only attachments)
508
+ * - CTRF runtime message attachments (internal transport artifacts)
509
+ *
510
+ * Runtime messages exist only to transmit data from test → reporter and
511
+ * are not user-facing attachments.
512
+ */
513
+ filterValidAttachments(attachments) {
514
+ return attachments.filter((attachment) => {
515
+ if (attachment.path === void 0) {
516
+ return false;
517
+ }
518
+ if (attachment.contentType === CTRF_RUNTIME_MESSAGE_CONTENT_TYPE) {
519
+ return false;
520
+ }
521
+ return true;
522
+ }).map((attachment) => ({
523
+ name: attachment.name,
524
+ contentType: attachment.contentType,
525
+ path: attachment.path ?? ""
526
+ }));
527
+ }
528
+ };
529
+ var generate_report_default = GenerateCtrfReport;
530
+ // Annotate the CommonJS export names for ESM import in node:
531
+ 0 && (module.exports = {
532
+ ctrf,
533
+ extra
534
+ });