playwright-ctrf-json-reporter 0.0.27 → 0.0.28-next-1
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/LICENSE +1 -1
- package/README.md +14 -27
- package/dist/adapter/index.d.ts +2 -0
- package/dist/adapter/index.js +7 -0
- package/dist/adapter/runtime.d.ts +80 -0
- package/dist/adapter/runtime.js +129 -0
- package/dist/generate-report.d.ts +33 -0
- package/dist/generate-report.js +95 -1
- package/dist/generate-report.test.d.ts +4 -0
- package/dist/generate-report.test.js +133 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/package.json +41 -21
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -6,29 +6,20 @@ A Playwright JSON test reporter to create test reports that follow the CTRF stan
|
|
|
6
6
|
|
|
7
7
|
[Common Test Report Format](https://ctrf.io) ensures the generation of uniform JSON test reports, independent of programming languages or test framework in use.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
</div>
|
|
24
|
-
<p style="font-size: 14px; margin: 1rem 0;">
|
|
25
|
-
|
|
26
|
-
Contributions are very welcome! <br/>
|
|
27
|
-
Explore more <a href="https://www.ctrf.io/integrations">integrations</a> <br/>
|
|
28
|
-
<a href="https://app.formbricks.com/s/cmefs524mhlh1tl01gkpvefrb">Let us know your thoughts</a>.
|
|
29
|
-
|
|
30
|
-
</p>
|
|
31
|
-
</div>
|
|
9
|
+
## CTRF Open Standard
|
|
10
|
+
|
|
11
|
+
CTRF is a community-driven open standard for test reporting.
|
|
12
|
+
|
|
13
|
+
By standardizing test results, reports can be validated, merged, compared, and analyzed consistently across languages and frameworks.
|
|
14
|
+
|
|
15
|
+
- **CTRF Specification**: https://github.com/ctrf-io/ctrf
|
|
16
|
+
The official specification defining the format and semantics
|
|
17
|
+
- **Discussions**: https://github.com/orgs/ctrf-io/discussions
|
|
18
|
+
Community forum for questions, ideas, and support
|
|
19
|
+
|
|
20
|
+
> [!NOTE]
|
|
21
|
+
> ⭐ Starring the **CTRF specification repository** (https://github.com/ctrf-io/ctrf)
|
|
22
|
+
> helps support the standard.
|
|
32
23
|
|
|
33
24
|
## Features
|
|
34
25
|
|
|
@@ -259,7 +250,3 @@ CTRF is a universal JSON test report schema that addresses the lack of a standar
|
|
|
259
250
|
**Language and Framework Agnostic:** It provides a universal reporting schema that works seamlessly with any programming language and testing framework.
|
|
260
251
|
|
|
261
252
|
**Facilitates Better Analysis:** With a standardized format, programatically analyzing test outcomes across multiple platforms becomes more straightforward.
|
|
262
|
-
|
|
263
|
-
## Support Us
|
|
264
|
-
|
|
265
|
-
If you find this project useful, consider giving it a GitHub star ⭐ It means a lot to us.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CTRF_RUNTIME_MESSAGE_CONTENT_TYPE = exports.extra = exports.ctrf = void 0;
|
|
4
|
+
var runtime_1 = require("./runtime");
|
|
5
|
+
Object.defineProperty(exports, "ctrf", { enumerable: true, get: function () { return runtime_1.ctrf; } });
|
|
6
|
+
Object.defineProperty(exports, "extra", { enumerable: true, get: function () { return runtime_1.extra; } });
|
|
7
|
+
Object.defineProperty(exports, "CTRF_RUNTIME_MESSAGE_CONTENT_TYPE", { enumerable: true, get: function () { return runtime_1.CTRF_RUNTIME_MESSAGE_CONTENT_TYPE; } });
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CTRF Runtime API for Playwright
|
|
3
|
+
*
|
|
4
|
+
* Enables enriching CTRF test reports with custom metadata at runtime.
|
|
5
|
+
* Metadata is collected via Playwright's native attachment mechanism and
|
|
6
|
+
* consolidated by the reporter into the test's `extra` field.
|
|
7
|
+
*
|
|
8
|
+
* ## Usage
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { ctrf } from 'playwright-ctrf-json-reporter'
|
|
12
|
+
*
|
|
13
|
+
* test('checkout flow', async ({ page }) => {
|
|
14
|
+
* ctrf.extra({ owner: 'checkout-team', priority: 'P1' })
|
|
15
|
+
* // ... test code
|
|
16
|
+
* ctrf.extra({ customMetric: calculateValue() })
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* ## API
|
|
21
|
+
*
|
|
22
|
+
* - `ctrf.extra(data)` - Attach key-value metadata to the current test
|
|
23
|
+
*
|
|
24
|
+
* ## Behavior
|
|
25
|
+
*
|
|
26
|
+
* - Call multiple times; all data is collected and merged
|
|
27
|
+
* - Works from any function in the call stack during test execution
|
|
28
|
+
* - Silently ignored when called outside test context
|
|
29
|
+
* - Deep merge: arrays concatenate, objects merge recursively, primitives overwrite
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Content type used to identify CTRF runtime messages in attachments.
|
|
33
|
+
* The reporter will look for attachments with this content type.
|
|
34
|
+
*/
|
|
35
|
+
export declare const CTRF_RUNTIME_MESSAGE_CONTENT_TYPE = "application/vnd.ctrf.message+json";
|
|
36
|
+
/**
|
|
37
|
+
* Runtime message types
|
|
38
|
+
*/
|
|
39
|
+
export type CtrfRuntimeMessageType = 'metadata';
|
|
40
|
+
/**
|
|
41
|
+
* A runtime message sent from test code to the reporter
|
|
42
|
+
*/
|
|
43
|
+
export interface CtrfRuntimeMessage {
|
|
44
|
+
type: CtrfRuntimeMessageType;
|
|
45
|
+
data: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Attach custom metadata to the current test.
|
|
49
|
+
*
|
|
50
|
+
* @param data - Key-value pairs to include in the CTRF report's `extra` field
|
|
51
|
+
* @returns Promise (can be ignored; completes when attachment is written)
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* - **Arrays**: Concatenated across calls (duplicates allowed)
|
|
55
|
+
* - **Objects**: Deep merged (nested keys preserved)
|
|
56
|
+
* - **Primitives**: Later calls overwrite earlier ones
|
|
57
|
+
* - Safe to call from helper functions - binds to the active test automatically
|
|
58
|
+
* - No-op outside test context (e.g., in global setup)
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // Arrays concatenate
|
|
62
|
+
* ctrf.extra({ tags: ['smoke'] })
|
|
63
|
+
* ctrf.extra({ tags: ['e2e'] })
|
|
64
|
+
* // → { tags: ['smoke', 'e2e'] }
|
|
65
|
+
*
|
|
66
|
+
* // Objects merge deeply
|
|
67
|
+
* ctrf.extra({ build: { id: 'abc' } })
|
|
68
|
+
* ctrf.extra({ build: { url: 'https://...' } })
|
|
69
|
+
* // → { build: { id: 'abc', url: 'https://...' } }
|
|
70
|
+
*
|
|
71
|
+
* // Primitives overwrite
|
|
72
|
+
* ctrf.extra({ owner: 'platform-team' })
|
|
73
|
+
* ctrf.extra({ owner: 'checkout-team' })
|
|
74
|
+
* // → { owner: 'checkout-team' }
|
|
75
|
+
*/
|
|
76
|
+
export declare function extra(data: Record<string, unknown>): Promise<void>;
|
|
77
|
+
/** CTRF runtime API namespace */
|
|
78
|
+
export declare const ctrf: {
|
|
79
|
+
readonly extra: typeof extra;
|
|
80
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CTRF Runtime API for Playwright
|
|
4
|
+
*
|
|
5
|
+
* Enables enriching CTRF test reports with custom metadata at runtime.
|
|
6
|
+
* Metadata is collected via Playwright's native attachment mechanism and
|
|
7
|
+
* consolidated by the reporter into the test's `extra` field.
|
|
8
|
+
*
|
|
9
|
+
* ## Usage
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { ctrf } from 'playwright-ctrf-json-reporter'
|
|
13
|
+
*
|
|
14
|
+
* test('checkout flow', async ({ page }) => {
|
|
15
|
+
* ctrf.extra({ owner: 'checkout-team', priority: 'P1' })
|
|
16
|
+
* // ... test code
|
|
17
|
+
* ctrf.extra({ customMetric: calculateValue() })
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* ## API
|
|
22
|
+
*
|
|
23
|
+
* - `ctrf.extra(data)` - Attach key-value metadata to the current test
|
|
24
|
+
*
|
|
25
|
+
* ## Behavior
|
|
26
|
+
*
|
|
27
|
+
* - Call multiple times; all data is collected and merged
|
|
28
|
+
* - Works from any function in the call stack during test execution
|
|
29
|
+
* - Silently ignored when called outside test context
|
|
30
|
+
* - Deep merge: arrays concatenate, objects merge recursively, primitives overwrite
|
|
31
|
+
*/
|
|
32
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
33
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
34
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
35
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
36
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
37
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
38
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.ctrf = exports.CTRF_RUNTIME_MESSAGE_CONTENT_TYPE = void 0;
|
|
43
|
+
exports.extra = extra;
|
|
44
|
+
const test_1 = require("@playwright/test");
|
|
45
|
+
/**
|
|
46
|
+
* Content type used to identify CTRF runtime messages in attachments.
|
|
47
|
+
* The reporter will look for attachments with this content type.
|
|
48
|
+
*/
|
|
49
|
+
exports.CTRF_RUNTIME_MESSAGE_CONTENT_TYPE = 'application/vnd.ctrf.message+json';
|
|
50
|
+
/**
|
|
51
|
+
* Silent fallback when no test is active - prevents errors in setup/teardown code.
|
|
52
|
+
*/
|
|
53
|
+
const nullTransport = {
|
|
54
|
+
send: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
55
|
+
/* intentionally empty */
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Attaches metadata payloads to the current Playwright test as hidden attachments.
|
|
60
|
+
* The reporter extracts these by content type during report generation.
|
|
61
|
+
*/
|
|
62
|
+
function createPlaywrightTransport() {
|
|
63
|
+
let sequence = 0;
|
|
64
|
+
return {
|
|
65
|
+
send(payload) {
|
|
66
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
67
|
+
const tag = `__ctrf_${++sequence}_${Date.now()}`;
|
|
68
|
+
const data = JSON.stringify(payload);
|
|
69
|
+
yield test_1.test.info().attach(tag, {
|
|
70
|
+
contentType: exports.CTRF_RUNTIME_MESSAGE_CONTENT_TYPE,
|
|
71
|
+
body: Buffer.from(data),
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const activeTransport = createPlaywrightTransport();
|
|
78
|
+
/**
|
|
79
|
+
* Resolves the active transport, or a null transport if outside a test.
|
|
80
|
+
*/
|
|
81
|
+
const resolveTransport = () => {
|
|
82
|
+
try {
|
|
83
|
+
test_1.test.info();
|
|
84
|
+
return activeTransport;
|
|
85
|
+
}
|
|
86
|
+
catch (_a) {
|
|
87
|
+
// Outside test context
|
|
88
|
+
}
|
|
89
|
+
return nullTransport;
|
|
90
|
+
};
|
|
91
|
+
/* ═══════════════════════════════════════════════════════════════════════════
|
|
92
|
+
* Public API
|
|
93
|
+
* ═══════════════════════════════════════════════════════════════════════════ */
|
|
94
|
+
/**
|
|
95
|
+
* Attach custom metadata to the current test.
|
|
96
|
+
*
|
|
97
|
+
* @param data - Key-value pairs to include in the CTRF report's `extra` field
|
|
98
|
+
* @returns Promise (can be ignored; completes when attachment is written)
|
|
99
|
+
*
|
|
100
|
+
* @remarks
|
|
101
|
+
* - **Arrays**: Concatenated across calls (duplicates allowed)
|
|
102
|
+
* - **Objects**: Deep merged (nested keys preserved)
|
|
103
|
+
* - **Primitives**: Later calls overwrite earlier ones
|
|
104
|
+
* - Safe to call from helper functions - binds to the active test automatically
|
|
105
|
+
* - No-op outside test context (e.g., in global setup)
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* // Arrays concatenate
|
|
109
|
+
* ctrf.extra({ tags: ['smoke'] })
|
|
110
|
+
* ctrf.extra({ tags: ['e2e'] })
|
|
111
|
+
* // → { tags: ['smoke', 'e2e'] }
|
|
112
|
+
*
|
|
113
|
+
* // Objects merge deeply
|
|
114
|
+
* ctrf.extra({ build: { id: 'abc' } })
|
|
115
|
+
* ctrf.extra({ build: { url: 'https://...' } })
|
|
116
|
+
* // → { build: { id: 'abc', url: 'https://...' } }
|
|
117
|
+
*
|
|
118
|
+
* // Primitives overwrite
|
|
119
|
+
* ctrf.extra({ owner: 'platform-team' })
|
|
120
|
+
* ctrf.extra({ owner: 'checkout-team' })
|
|
121
|
+
* // → { owner: 'checkout-team' }
|
|
122
|
+
*/
|
|
123
|
+
function extra(data) {
|
|
124
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
125
|
+
yield resolveTransport().send({ type: 'metadata', data });
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** CTRF runtime API namespace */
|
|
129
|
+
exports.ctrf = { extra };
|
|
@@ -43,6 +43,29 @@ declare class GenerateCtrfReport implements Reporter {
|
|
|
43
43
|
setEnvironmentDetails(reporterConfigOptions: ReporterConfigOptions): void;
|
|
44
44
|
hasEnvironmentDetails(environment: CtrfEnvironment): boolean;
|
|
45
45
|
extractMetadata(testResult: TestResult): any;
|
|
46
|
+
/**
|
|
47
|
+
* Deep merge with array concatenation.
|
|
48
|
+
* Arrays are concatenated, objects are recursively merged, primitives overwrite.
|
|
49
|
+
*/
|
|
50
|
+
private deepMerge;
|
|
51
|
+
/**
|
|
52
|
+
* Extract and merge runtime data from CTRF message attachments.
|
|
53
|
+
*
|
|
54
|
+
* Runtime messages are internal transport artifacts used to send data from
|
|
55
|
+
* test code to the reporter via test.info().attach(). They are identified
|
|
56
|
+
* by the CTRF_RUNTIME_MESSAGE_CONTENT_TYPE content type.
|
|
57
|
+
*
|
|
58
|
+
* ## Merge Semantics (deep merge with array concatenation)
|
|
59
|
+
*
|
|
60
|
+
* Messages are processed in attachment order with intelligent merging:
|
|
61
|
+
* - Arrays: Concatenated across calls (duplicates allowed)
|
|
62
|
+
* - Objects: Deep merged (nested keys preserved)
|
|
63
|
+
* - Primitives: Later values overwrite earlier ones
|
|
64
|
+
*
|
|
65
|
+
* @param testResult - The test result containing attachments
|
|
66
|
+
* @returns Merged runtime data or null if no CTRF messages found
|
|
67
|
+
*/
|
|
68
|
+
extractRuntimeData(testResult: TestResult): Record<string, unknown> | null;
|
|
46
69
|
updateStart(startTime: Date): number;
|
|
47
70
|
calculateStopTime(startTime: Date, duration: number): number;
|
|
48
71
|
buildSuitePath(test: TestCase): string;
|
|
@@ -51,6 +74,16 @@ declare class GenerateCtrfReport implements Reporter {
|
|
|
51
74
|
countSuites(suite: Suite): number;
|
|
52
75
|
writeReportToFile(data: CtrfReport): void;
|
|
53
76
|
processStep(test: CtrfTest, step: TestStep): void;
|
|
77
|
+
/**
|
|
78
|
+
* Filter attachments to include in CTRF output.
|
|
79
|
+
*
|
|
80
|
+
* Excludes:
|
|
81
|
+
* - Attachments without a file path (body-only attachments)
|
|
82
|
+
* - CTRF runtime message attachments (internal transport artifacts)
|
|
83
|
+
*
|
|
84
|
+
* Runtime messages exist only to transmit data from test → reporter and
|
|
85
|
+
* are not user-facing attachments.
|
|
86
|
+
*/
|
|
54
87
|
filterValidAttachments(attachments: TestResult['attachments']): CtrfAttachment[];
|
|
55
88
|
}
|
|
56
89
|
export default GenerateCtrfReport;
|
package/dist/generate-report.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const path_1 = __importDefault(require("path"));
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
const adapter_1 = require("./adapter");
|
|
9
10
|
class GenerateCtrfReport {
|
|
10
11
|
constructor(config) {
|
|
11
12
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u;
|
|
@@ -152,6 +153,10 @@ class GenerateCtrfReport {
|
|
|
152
153
|
if (this.reporterConfigOptions.annotations !== undefined) {
|
|
153
154
|
test.extra = { annotations: testCase.annotations };
|
|
154
155
|
}
|
|
156
|
+
const runtimeData = this.extractRuntimeData(testResult);
|
|
157
|
+
if (runtimeData !== null) {
|
|
158
|
+
test.extra = Object.assign(Object.assign({}, test.extra), runtimeData);
|
|
159
|
+
}
|
|
155
160
|
if (testCase.results.length > 1) {
|
|
156
161
|
const retryResults = testCase.results.slice(0, -1);
|
|
157
162
|
test.retryAttempts = [];
|
|
@@ -259,6 +264,77 @@ class GenerateCtrfReport {
|
|
|
259
264
|
}
|
|
260
265
|
return null;
|
|
261
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Deep merge with array concatenation.
|
|
269
|
+
* Arrays are concatenated, objects are recursively merged, primitives overwrite.
|
|
270
|
+
*/
|
|
271
|
+
deepMerge(target, source) {
|
|
272
|
+
const result = Object.assign({}, target);
|
|
273
|
+
for (const [key, sourceValue] of Object.entries(source)) {
|
|
274
|
+
const targetValue = result[key];
|
|
275
|
+
if (Array.isArray(sourceValue)) {
|
|
276
|
+
result[key] = Array.isArray(targetValue)
|
|
277
|
+
? [...targetValue, ...sourceValue]
|
|
278
|
+
: [...sourceValue];
|
|
279
|
+
}
|
|
280
|
+
else if (sourceValue !== null &&
|
|
281
|
+
typeof sourceValue === 'object' &&
|
|
282
|
+
!Array.isArray(sourceValue)) {
|
|
283
|
+
result[key] =
|
|
284
|
+
targetValue !== null &&
|
|
285
|
+
typeof targetValue === 'object' &&
|
|
286
|
+
!Array.isArray(targetValue)
|
|
287
|
+
? this.deepMerge(targetValue, sourceValue)
|
|
288
|
+
: Object.assign({}, sourceValue);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
result[key] = sourceValue;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Extract and merge runtime data from CTRF message attachments.
|
|
298
|
+
*
|
|
299
|
+
* Runtime messages are internal transport artifacts used to send data from
|
|
300
|
+
* test code to the reporter via test.info().attach(). They are identified
|
|
301
|
+
* by the CTRF_RUNTIME_MESSAGE_CONTENT_TYPE content type.
|
|
302
|
+
*
|
|
303
|
+
* ## Merge Semantics (deep merge with array concatenation)
|
|
304
|
+
*
|
|
305
|
+
* Messages are processed in attachment order with intelligent merging:
|
|
306
|
+
* - Arrays: Concatenated across calls (duplicates allowed)
|
|
307
|
+
* - Objects: Deep merged (nested keys preserved)
|
|
308
|
+
* - Primitives: Later values overwrite earlier ones
|
|
309
|
+
*
|
|
310
|
+
* @param testResult - The test result containing attachments
|
|
311
|
+
* @returns Merged runtime data or null if no CTRF messages found
|
|
312
|
+
*/
|
|
313
|
+
extractRuntimeData(testResult) {
|
|
314
|
+
const runtimeData = {};
|
|
315
|
+
let hasData = false;
|
|
316
|
+
for (const attachment of testResult.attachments) {
|
|
317
|
+
if (attachment.contentType !== adapter_1.CTRF_RUNTIME_MESSAGE_CONTENT_TYPE) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (attachment.body === undefined) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const message = JSON.parse(attachment.body.toString('utf-8'));
|
|
325
|
+
if (message.type === 'metadata' && message.data !== null) {
|
|
326
|
+
Object.assign(runtimeData, this.deepMerge(runtimeData, message.data));
|
|
327
|
+
hasData = true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (e) {
|
|
331
|
+
if (e instanceof Error) {
|
|
332
|
+
console.error(`Error parsing CTRF runtime message: ${e.message}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return hasData ? runtimeData : null;
|
|
337
|
+
}
|
|
262
338
|
updateStart(startTime) {
|
|
263
339
|
const date = new Date(startTime);
|
|
264
340
|
const unixEpochTime = Math.floor(date.getTime() / 1000);
|
|
@@ -344,9 +420,27 @@ class GenerateCtrfReport {
|
|
|
344
420
|
});
|
|
345
421
|
}
|
|
346
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Filter attachments to include in CTRF output.
|
|
425
|
+
*
|
|
426
|
+
* Excludes:
|
|
427
|
+
* - Attachments without a file path (body-only attachments)
|
|
428
|
+
* - CTRF runtime message attachments (internal transport artifacts)
|
|
429
|
+
*
|
|
430
|
+
* Runtime messages exist only to transmit data from test → reporter and
|
|
431
|
+
* are not user-facing attachments.
|
|
432
|
+
*/
|
|
347
433
|
filterValidAttachments(attachments) {
|
|
348
434
|
return attachments
|
|
349
|
-
.filter((attachment) =>
|
|
435
|
+
.filter((attachment) => {
|
|
436
|
+
if (attachment.path === undefined) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
if (attachment.contentType === adapter_1.CTRF_RUNTIME_MESSAGE_CONTENT_TYPE) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
return true;
|
|
443
|
+
})
|
|
350
444
|
.map((attachment) => {
|
|
351
445
|
var _a;
|
|
352
446
|
return ({
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for GenerateCtrfReport deep merge functionality
|
|
4
|
+
*/
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
const generate_report_1 = __importDefault(require("./generate-report"));
|
|
10
|
+
describe('GenerateCtrfReport', () => {
|
|
11
|
+
describe('deepMerge', () => {
|
|
12
|
+
let reporter;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
reporter = new generate_report_1.default();
|
|
15
|
+
});
|
|
16
|
+
it('should concatenate arrays', () => {
|
|
17
|
+
const target = { tags: ['smoke'] };
|
|
18
|
+
const source = { tags: ['e2e'] };
|
|
19
|
+
// @ts-expect-error - accessing private method for testing
|
|
20
|
+
const result = reporter.deepMerge(target, source);
|
|
21
|
+
expect(result).toEqual({ tags: ['smoke', 'e2e'] });
|
|
22
|
+
});
|
|
23
|
+
it('should concatenate arrays from empty target', () => {
|
|
24
|
+
const target = {};
|
|
25
|
+
const source = { tags: ['smoke'] };
|
|
26
|
+
// @ts-expect-error - accessing private method for testing
|
|
27
|
+
const result = reporter.deepMerge(target, source);
|
|
28
|
+
expect(result).toEqual({ tags: ['smoke'] });
|
|
29
|
+
});
|
|
30
|
+
it('should merge objects deeply', () => {
|
|
31
|
+
const target = { build: { id: '123' } };
|
|
32
|
+
const source = { build: { branch: 'main' } };
|
|
33
|
+
// @ts-expect-error - accessing private method for testing
|
|
34
|
+
const result = reporter.deepMerge(target, source);
|
|
35
|
+
expect(result).toEqual({ build: { id: '123', branch: 'main' } });
|
|
36
|
+
});
|
|
37
|
+
it('should merge nested objects deeply', () => {
|
|
38
|
+
const target = { meta: { build: { id: '123' } } };
|
|
39
|
+
const source = { meta: { build: { url: 'https://...' } } };
|
|
40
|
+
// @ts-expect-error - accessing private method for testing
|
|
41
|
+
const result = reporter.deepMerge(target, source);
|
|
42
|
+
expect(result).toEqual({
|
|
43
|
+
meta: { build: { id: '123', url: 'https://...' } },
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
it('should overwrite primitives', () => {
|
|
47
|
+
const target = { owner: 'platform-team' };
|
|
48
|
+
const source = { owner: 'checkout-team' };
|
|
49
|
+
// @ts-expect-error - accessing private method for testing
|
|
50
|
+
const result = reporter.deepMerge(target, source);
|
|
51
|
+
expect(result).toEqual({ owner: 'checkout-team' });
|
|
52
|
+
});
|
|
53
|
+
it('should preserve non-overlapping keys', () => {
|
|
54
|
+
const target = { owner: 'platform-team', priority: 'P1' };
|
|
55
|
+
const source = { retries: 3 };
|
|
56
|
+
// @ts-expect-error - accessing private method for testing
|
|
57
|
+
const result = reporter.deepMerge(target, source);
|
|
58
|
+
expect(result).toEqual({
|
|
59
|
+
owner: 'platform-team',
|
|
60
|
+
priority: 'P1',
|
|
61
|
+
retries: 3,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
it('should handle complex nested structures', () => {
|
|
65
|
+
const target = {
|
|
66
|
+
tags: ['smoke'],
|
|
67
|
+
build: { id: '123', metadata: { author: 'alice' } },
|
|
68
|
+
retries: 1,
|
|
69
|
+
};
|
|
70
|
+
const source = {
|
|
71
|
+
tags: ['e2e', 'regression'],
|
|
72
|
+
build: { branch: 'main', metadata: { timestamp: '2026-02-06' } },
|
|
73
|
+
retries: 2,
|
|
74
|
+
owner: 'platform',
|
|
75
|
+
};
|
|
76
|
+
// @ts-expect-error - accessing private method for testing
|
|
77
|
+
const result = reporter.deepMerge(target, source);
|
|
78
|
+
expect(result).toEqual({
|
|
79
|
+
tags: ['smoke', 'e2e', 'regression'],
|
|
80
|
+
build: {
|
|
81
|
+
id: '123',
|
|
82
|
+
branch: 'main',
|
|
83
|
+
metadata: { author: 'alice', timestamp: '2026-02-06' },
|
|
84
|
+
},
|
|
85
|
+
retries: 2,
|
|
86
|
+
owner: 'platform',
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
it('should replace object with primitive', () => {
|
|
90
|
+
const target = { config: { timeout: 5000 } };
|
|
91
|
+
const source = { config: 'default' };
|
|
92
|
+
// @ts-expect-error - accessing private method for testing
|
|
93
|
+
const result = reporter.deepMerge(target, source);
|
|
94
|
+
expect(result).toEqual({ config: 'default' });
|
|
95
|
+
});
|
|
96
|
+
it('should replace primitive with object', () => {
|
|
97
|
+
const target = { config: 'default' };
|
|
98
|
+
const source = { config: { timeout: 5000 } };
|
|
99
|
+
// @ts-expect-error - accessing private method for testing
|
|
100
|
+
const result = reporter.deepMerge(target, source);
|
|
101
|
+
expect(result).toEqual({ config: { timeout: 5000 } });
|
|
102
|
+
});
|
|
103
|
+
it('should replace primitive with array', () => {
|
|
104
|
+
const target = { tags: 'smoke' };
|
|
105
|
+
const source = { tags: ['e2e'] };
|
|
106
|
+
// @ts-expect-error - accessing private method for testing
|
|
107
|
+
const result = reporter.deepMerge(target, source);
|
|
108
|
+
expect(result).toEqual({ tags: ['e2e'] });
|
|
109
|
+
});
|
|
110
|
+
it('should handle null values', () => {
|
|
111
|
+
const target = { value: null };
|
|
112
|
+
const source = { value: 'something' };
|
|
113
|
+
// @ts-expect-error - accessing private method for testing
|
|
114
|
+
const result = reporter.deepMerge(target, source);
|
|
115
|
+
expect(result).toEqual({ value: 'something' });
|
|
116
|
+
});
|
|
117
|
+
it('should handle empty objects', () => {
|
|
118
|
+
const target = {};
|
|
119
|
+
const source = { owner: 'platform' };
|
|
120
|
+
// @ts-expect-error - accessing private method for testing
|
|
121
|
+
const result = reporter.deepMerge(target, source);
|
|
122
|
+
expect(result).toEqual({ owner: 'platform' });
|
|
123
|
+
});
|
|
124
|
+
it('should not mutate original objects', () => {
|
|
125
|
+
const target = { tags: ['smoke'], build: { id: '123' } };
|
|
126
|
+
const source = { tags: ['e2e'], build: { branch: 'main' } };
|
|
127
|
+
// @ts-expect-error - accessing private method for testing
|
|
128
|
+
reporter.deepMerge(target, source);
|
|
129
|
+
expect(target).toEqual({ tags: ['smoke'], build: { id: '123' } });
|
|
130
|
+
expect(source).toEqual({ tags: ['e2e'], build: { branch: 'main' } });
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -3,6 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.default = void 0;
|
|
6
|
+
exports.extra = exports.ctrf = exports.default = void 0;
|
|
7
7
|
var generate_report_1 = require("./generate-report");
|
|
8
8
|
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(generate_report_1).default; } });
|
|
9
|
+
var adapter_1 = require("./adapter");
|
|
10
|
+
Object.defineProperty(exports, "ctrf", { enumerable: true, get: function () { return adapter_1.ctrf; } });
|
|
11
|
+
Object.defineProperty(exports, "extra", { enumerable: true, get: function () { return adapter_1.extra; } });
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwright-ctrf-json-reporter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28-next-1",
|
|
4
4
|
"description": "A Playwright JSON test reporter to create test results reports",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsc",
|
|
8
8
|
"test": "jest",
|
|
9
|
-
"lint": "eslint . --
|
|
10
|
-
"lint
|
|
9
|
+
"lint": "eslint . --fix",
|
|
10
|
+
"lint:check": "eslint .",
|
|
11
11
|
"format": "prettier --write .",
|
|
12
|
-
"format
|
|
13
|
-
"all": "npm run lint && npm run format
|
|
12
|
+
"format:check": "prettier --check .",
|
|
13
|
+
"all": "npm run lint && npm run format:check && npm run test && npm run build"
|
|
14
14
|
},
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
@@ -25,23 +25,43 @@
|
|
|
25
25
|
"dist/",
|
|
26
26
|
"README.md"
|
|
27
27
|
],
|
|
28
|
-
"author": "Matthew
|
|
28
|
+
"author": "Matthew Poulton-White",
|
|
29
29
|
"license": "MIT",
|
|
30
|
+
"keywords": [
|
|
31
|
+
"ctrf",
|
|
32
|
+
"common-test-report-format",
|
|
33
|
+
"test-reporting",
|
|
34
|
+
"test-results",
|
|
35
|
+
"testing",
|
|
36
|
+
"json",
|
|
37
|
+
"ci",
|
|
38
|
+
"playwright",
|
|
39
|
+
"playwright-reporter"
|
|
40
|
+
],
|
|
41
|
+
"security": {
|
|
42
|
+
"email": "security@ctrf.io",
|
|
43
|
+
"url": "https://github.com/ctrf-io/.github/blob/main/SECURITY.md"
|
|
44
|
+
},
|
|
30
45
|
"devDependencies": {
|
|
31
|
-
"@playwright/test": "^1.
|
|
32
|
-
"@types/jest": "^
|
|
33
|
-
"@types/node": "^
|
|
34
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
35
|
-
"eslint": "^8.
|
|
36
|
-
"eslint
|
|
37
|
-
"eslint-config-
|
|
38
|
-
"eslint-plugin-import": "^2.
|
|
39
|
-
"eslint-plugin-n": "^
|
|
40
|
-
"eslint-plugin-prettier": "^5.
|
|
41
|
-
"eslint-plugin-promise": "^
|
|
42
|
-
"jest": "^
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
+
"@playwright/test": "^1.58.2",
|
|
47
|
+
"@types/jest": "^30.0.0",
|
|
48
|
+
"@types/node": "^25.2.2",
|
|
49
|
+
"@typescript-eslint/eslint-plugin": "^8.55.0",
|
|
50
|
+
"@typescript-eslint/parser": "^8.55.0",
|
|
51
|
+
"eslint": "^9.39.2",
|
|
52
|
+
"eslint-config-prettier": "^10.1.8",
|
|
53
|
+
"eslint-plugin-import": "^2.32.0",
|
|
54
|
+
"eslint-plugin-n": "^17.23.2",
|
|
55
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
56
|
+
"eslint-plugin-promise": "^7.2.1",
|
|
57
|
+
"jest": "^30.2.0",
|
|
58
|
+
"jest-ctrf-json-reporter": "^0.0.10",
|
|
59
|
+
"prettier": "^3.8.1",
|
|
60
|
+
"ts-jest": "^29.4.6",
|
|
61
|
+
"typescript": "^5.9.3",
|
|
62
|
+
"typescript-eslint": "^8.55.0"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"ctrf": "^0.2.0-next-0"
|
|
46
66
|
}
|
|
47
67
|
}
|