playwright-ctrf-json-reporter 0.0.29 → 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 +58 -0
- package/dist/index.cjs +534 -0
- package/dist/{generate-report.d.ts → index.d.cts} +60 -13
- package/dist/index.d.ts +136 -2
- package/dist/index.js +496 -11
- package/package.json +72 -65
- package/dist/adapter/index.d.ts +0 -2
- package/dist/adapter/index.js +0 -7
- package/dist/adapter/runtime.d.ts +0 -80
- package/dist/adapter/runtime.js +0 -129
- package/dist/generate-report.js +0 -454
- package/dist/generate-report.test.d.ts +0 -4
- package/dist/generate-report.test.js +0 -133
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
|
+
});
|