@checkly/playwright-reporter 0.1.5 → 0.1.7

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/dist/index.js CHANGED
@@ -1 +1,1036 @@
1
- function a6_0x9da0(_0x181701,_0x24254e){_0x181701=_0x181701-0x1c0;var _0x1016ec=a6_0x1016();var _0x9da0d7=_0x1016ec[_0x181701];return _0x9da0d7;}(function(_0x5bc3a0,_0x6585ad){var _0x427d94=a6_0x9da0,_0x196632=_0x5bc3a0();while(!![]){try{var _0x1863df=parseInt(_0x427d94(0x1c7))/0x1*(parseInt(_0x427d94(0x1c9))/0x2)+-parseInt(_0x427d94(0x1c0))/0x3*(parseInt(_0x427d94(0x1c4))/0x4)+parseInt(_0x427d94(0x1c1))/0x5*(-parseInt(_0x427d94(0x1c5))/0x6)+-parseInt(_0x427d94(0x1c8))/0x7+-parseInt(_0x427d94(0x1c3))/0x8+parseInt(_0x427d94(0x1c6))/0x9+parseInt(_0x427d94(0x1c2))/0xa;if(_0x1863df===_0x6585ad)break;else _0x196632['push'](_0x196632['shift']());}catch(_0x6a0cb4){_0x196632['push'](_0x196632['shift']());}}}(a6_0x1016,0x2c5fc));function a6_0x1016(){var _0x3baf3f=['27114qglyOx','814041updBIT','19542XDbSZA','1359253GFKSKw','16UXhciG','293790bpXJMH','270htEmFX','5993960opQwGf','242880kIhRkx','8jGckKj'];a6_0x1016=function(){return _0x3baf3f;};return a6_0x1016();}export{ChecklyReporter}from'./reporter.js';export{AssetCollector}from'./asset-collector.js';export{Zipper}from'./zipper.js';export{ChecklyReporter as default}from'./reporter.js';
1
+ // ../utils/src/asset-collector.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ var AssetCollector = class {
5
+ constructor(testResultsDir) {
6
+ this.testResultsDir = testResultsDir;
7
+ }
8
+ /**
9
+ * Collects assets from test results directory
10
+ * Uses a two-phase approach to support both normal test runs and blob merge scenarios:
11
+ * 1. First, extract attachment paths from the JSON report (works for merge scenarios)
12
+ * 2. Fall back to directory scanning for any additional assets not in the report
13
+ *
14
+ * @param report Optional Playwright JSONReport with attachment paths
15
+ * @returns Array of Asset objects with source and archive paths
16
+ */
17
+ async collectAssets(report) {
18
+ const assets = [];
19
+ const addedPaths = /* @__PURE__ */ new Set();
20
+ if (report) {
21
+ this.collectAssetsFromReport(report, assets, addedPaths);
22
+ }
23
+ if (fs.existsSync(this.testResultsDir)) {
24
+ await this.collectAssetsRecursive(this.testResultsDir, assets, addedPaths);
25
+ }
26
+ return assets;
27
+ }
28
+ /**
29
+ * Extracts assets from JSON report attachment paths
30
+ * Essential for blob merge scenarios where attachments are extracted to temporary locations
31
+ */
32
+ collectAssetsFromReport(report, assets, addedPaths) {
33
+ const processResults = (results) => {
34
+ for (const result of results) {
35
+ if (result.attachments && Array.isArray(result.attachments)) {
36
+ for (const attachment of result.attachments) {
37
+ if (attachment.path && typeof attachment.path === "string") {
38
+ const sourcePath = attachment.path;
39
+ if (addedPaths.has(sourcePath)) continue;
40
+ if (!fs.existsSync(sourcePath)) {
41
+ continue;
42
+ }
43
+ const archivePath = this.determineArchivePath(sourcePath);
44
+ const asset = this.createAsset(sourcePath, archivePath);
45
+ assets.push(asset);
46
+ addedPaths.add(sourcePath);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ };
52
+ const processSuite = (suite) => {
53
+ if (suite.specs) {
54
+ for (const spec of suite.specs) {
55
+ if (spec.tests) {
56
+ for (const test of spec.tests) {
57
+ if (test.results) {
58
+ processResults(test.results);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+ if (suite.suites) {
65
+ for (const nestedSuite of suite.suites) {
66
+ processSuite(nestedSuite);
67
+ }
68
+ }
69
+ };
70
+ if (report.suites) {
71
+ for (const suite of report.suites) {
72
+ processSuite(suite);
73
+ }
74
+ }
75
+ }
76
+ /**
77
+ * Determines the archive path for an asset based on its source path
78
+ * Handles both test-results paths and blob merge extraction paths
79
+ */
80
+ determineArchivePath(sourcePath) {
81
+ const normalizedPath = sourcePath.replace(/\\/g, "/");
82
+ const testResultsIndex = normalizedPath.indexOf("test-results/");
83
+ if (testResultsIndex !== -1) {
84
+ return normalizedPath.substring(testResultsIndex);
85
+ }
86
+ const parts = normalizedPath.split("/");
87
+ if (parts.length >= 2) {
88
+ return `test-results/${parts.slice(-2).join("/")}`;
89
+ }
90
+ return `test-results/${path.basename(sourcePath)}`;
91
+ }
92
+ /**
93
+ * Recursively traverses directories to collect assets
94
+ */
95
+ async collectAssetsRecursive(currentDir, assets, addedPaths) {
96
+ let entries;
97
+ try {
98
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
99
+ } catch {
100
+ return;
101
+ }
102
+ const baseDirName = path.basename(path.resolve(this.testResultsDir));
103
+ for (const entry of entries) {
104
+ const fullPath = path.join(currentDir, entry.name);
105
+ const relativeFromBase = path.relative(this.testResultsDir, fullPath);
106
+ const archivePath = path.join(baseDirName, relativeFromBase);
107
+ if (this.shouldSkipFile(entry.name)) {
108
+ continue;
109
+ }
110
+ if (entry.isDirectory()) {
111
+ await this.collectAssetsRecursive(fullPath, assets, addedPaths);
112
+ } else if (entry.isFile()) {
113
+ if (!addedPaths.has(fullPath)) {
114
+ const asset = this.createAsset(fullPath, archivePath);
115
+ assets.push(asset);
116
+ addedPaths.add(fullPath);
117
+ }
118
+ }
119
+ }
120
+ }
121
+ /**
122
+ * Determines if a file should be skipped
123
+ */
124
+ shouldSkipFile(filename) {
125
+ const skipPatterns = [
126
+ /^\./,
127
+ // Hidden files (.DS_Store, .gitkeep, etc.)
128
+ /\.tmp$/i,
129
+ // Temporary files
130
+ /~$/,
131
+ // Backup files
132
+ /\.swp$/i,
133
+ // Vim swap files
134
+ /\.lock$/i,
135
+ // Lock files
136
+ /^playwright-test-report\.json$/i
137
+ // Skip the JSON report itself
138
+ ];
139
+ return skipPatterns.some((pattern) => pattern.test(filename));
140
+ }
141
+ /**
142
+ * Creates an Asset object from file path
143
+ */
144
+ createAsset(sourcePath, archivePath) {
145
+ const ext = path.extname(sourcePath).toLowerCase();
146
+ const { type, contentType } = this.determineAssetType(ext);
147
+ const normalizedArchivePath = archivePath.split(path.sep).join("/");
148
+ return {
149
+ sourcePath,
150
+ archivePath: normalizedArchivePath,
151
+ type,
152
+ contentType
153
+ };
154
+ }
155
+ /**
156
+ * Determines asset type and content type from file extension
157
+ */
158
+ determineAssetType(ext) {
159
+ if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg"].includes(ext)) {
160
+ return {
161
+ type: "screenshot",
162
+ contentType: this.getImageContentType(ext)
163
+ };
164
+ }
165
+ if ([".webm", ".mp4", ".avi", ".mov"].includes(ext)) {
166
+ return {
167
+ type: "video",
168
+ contentType: this.getVideoContentType(ext)
169
+ };
170
+ }
171
+ if (ext === ".zip") {
172
+ return {
173
+ type: "trace",
174
+ contentType: "application/zip"
175
+ };
176
+ }
177
+ if (ext === ".html" || ext === ".htm") {
178
+ return {
179
+ type: "attachment",
180
+ contentType: "text/html"
181
+ };
182
+ }
183
+ if (ext === ".json") {
184
+ return {
185
+ type: "attachment",
186
+ contentType: "application/json"
187
+ };
188
+ }
189
+ if (ext === ".txt" || ext === ".log") {
190
+ return {
191
+ type: "attachment",
192
+ contentType: "text/plain"
193
+ };
194
+ }
195
+ return {
196
+ type: "other",
197
+ contentType: "application/octet-stream"
198
+ };
199
+ }
200
+ /**
201
+ * Gets content type for image files
202
+ */
203
+ getImageContentType(ext) {
204
+ const contentTypes = {
205
+ ".png": "image/png",
206
+ ".jpg": "image/jpeg",
207
+ ".jpeg": "image/jpeg",
208
+ ".gif": "image/gif",
209
+ ".bmp": "image/bmp",
210
+ ".svg": "image/svg+xml"
211
+ };
212
+ return contentTypes[ext] || "image/png";
213
+ }
214
+ /**
215
+ * Gets content type for video files
216
+ */
217
+ getVideoContentType(ext) {
218
+ const contentTypes = {
219
+ ".webm": "video/webm",
220
+ ".mp4": "video/mp4",
221
+ ".avi": "video/x-msvideo",
222
+ ".mov": "video/quicktime"
223
+ };
224
+ return contentTypes[ext] || "video/webm";
225
+ }
226
+ };
227
+
228
+ // ../utils/src/zipper.ts
229
+ import * as fs2 from "fs";
230
+ import * as os from "os";
231
+ import * as path2 from "path";
232
+ import { ZipArchive } from "archiver";
233
+ var Zipper = class {
234
+ outputPath;
235
+ constructor(options) {
236
+ this.outputPath = options.outputPath;
237
+ }
238
+ /**
239
+ * Creates a ZIP archive containing the JSON report and assets
240
+ * @param reportPath - Path to the JSON report file
241
+ * @param assets - Array of assets to include in the ZIP
242
+ * @returns ZIP creation result with metadata
243
+ */
244
+ async createZip(reportPath, assets) {
245
+ const entries = [];
246
+ return new Promise((resolve2, reject) => {
247
+ try {
248
+ const output = fs2.createWriteStream(this.outputPath);
249
+ const archive = new ZipArchive({
250
+ zlib: { level: 0 }
251
+ });
252
+ archive.on("entry", (entryData) => {
253
+ const entryName = entryData.name.replace(/\\/g, "/");
254
+ const start = entryData._offsets?.contents ?? 0;
255
+ const end = entryData._offsets?.contents + (entryData.csize ?? 0) - 1;
256
+ entries.push({
257
+ name: entryName,
258
+ start,
259
+ end
260
+ });
261
+ });
262
+ output.on("close", () => {
263
+ const zipSize = archive.pointer();
264
+ resolve2({
265
+ zipPath: this.outputPath,
266
+ size: zipSize,
267
+ entryCount: entries.length,
268
+ entries
269
+ });
270
+ });
271
+ archive.on("error", (err) => {
272
+ reject(err);
273
+ });
274
+ output.on("error", (err) => {
275
+ reject(err);
276
+ });
277
+ archive.pipe(output);
278
+ if (!fs2.existsSync(reportPath)) {
279
+ reject(new Error(`Report file not found: ${reportPath}`));
280
+ return;
281
+ }
282
+ const transformedReportPath = this.transformJsonReport(reportPath);
283
+ archive.file(transformedReportPath, { name: "output/playwright-test-report.json" });
284
+ for (const asset of assets) {
285
+ if (!fs2.existsSync(asset.sourcePath)) {
286
+ console.warn(`[Checkly Reporter] Skipping missing asset: ${asset.sourcePath}`);
287
+ continue;
288
+ }
289
+ archive.file(asset.sourcePath, { name: asset.archivePath });
290
+ }
291
+ archive.finalize();
292
+ } catch (error) {
293
+ reject(error);
294
+ }
295
+ });
296
+ }
297
+ /**
298
+ * Transforms the JSON report to use relative paths for attachments
299
+ * This ensures the UI can map attachment paths to ZIP entries
300
+ * @param reportPath - Path to the original JSON report
301
+ * @returns Path to the transformed JSON report (in temp directory)
302
+ */
303
+ transformJsonReport(reportPath) {
304
+ const reportContent = fs2.readFileSync(reportPath, "utf-8");
305
+ const report = JSON.parse(reportContent);
306
+ this.transformAttachmentPaths(report);
307
+ const tempReportPath = path2.join(os.tmpdir(), `playwright-test-report-${Date.now()}.json`);
308
+ fs2.writeFileSync(tempReportPath, JSON.stringify(report, null, 2));
309
+ return tempReportPath;
310
+ }
311
+ /**
312
+ * Recursively transforms attachment paths in the report structure
313
+ * Converts absolute paths to relative paths matching ZIP structure
314
+ * @param obj - Object to transform (mutated in place)
315
+ */
316
+ transformAttachmentPaths(obj) {
317
+ if (typeof obj !== "object" || obj === null) {
318
+ return;
319
+ }
320
+ if (Array.isArray(obj)) {
321
+ obj.forEach((item) => this.transformAttachmentPaths(item));
322
+ return;
323
+ }
324
+ if (obj.attachments && Array.isArray(obj.attachments)) {
325
+ obj.attachments.forEach((attachment) => {
326
+ if (attachment.path && typeof attachment.path === "string") {
327
+ attachment.path = this.normalizeAttachmentPath(attachment.path);
328
+ }
329
+ });
330
+ }
331
+ Object.values(obj).forEach((value) => this.transformAttachmentPaths(value));
332
+ }
333
+ /**
334
+ * Normalizes attachment paths by extracting the relevant snapshot directory portion.
335
+ * Supports Playwright's default and common custom snapshot directory patterns.
336
+ *
337
+ * Priority order (first match wins):
338
+ * 1. test-results/ (highest priority, existing behavior)
339
+ * 2. *-snapshots/ (Playwright default pattern)
340
+ * 3. __screenshots__/ (common custom pattern)
341
+ * 4. __snapshots__/ (common custom pattern)
342
+ * 5. screenshots/ (simple custom pattern)
343
+ * 6. snapshots/ (simple custom pattern)
344
+ *
345
+ * @param attachmentPath - Absolute or relative path to attachment
346
+ * @returns Normalized path starting from the matched directory, or original path if no match
347
+ */
348
+ normalizeAttachmentPath(attachmentPath) {
349
+ const normalizedPath = attachmentPath.replace(/\\/g, "/");
350
+ const testResultsIndex = normalizedPath.indexOf("test-results/");
351
+ if (testResultsIndex !== -1) {
352
+ return normalizedPath.substring(testResultsIndex);
353
+ }
354
+ const snapshotsIndex = normalizedPath.indexOf("-snapshots/");
355
+ if (snapshotsIndex !== -1) {
356
+ const pathBeforeSnapshots = normalizedPath.substring(0, snapshotsIndex);
357
+ const lastSlashIndex = pathBeforeSnapshots.lastIndexOf("/");
358
+ const startIndex = lastSlashIndex !== -1 ? lastSlashIndex + 1 : 0;
359
+ return normalizedPath.substring(startIndex);
360
+ }
361
+ const patterns = ["__screenshots__/", "__snapshots__/", "screenshots/", "snapshots/"];
362
+ for (const pattern of patterns) {
363
+ const matchIndex = normalizedPath.indexOf(pattern);
364
+ if (matchIndex !== -1) {
365
+ return normalizedPath.substring(matchIndex);
366
+ }
367
+ }
368
+ console.warn(`[Checkly Reporter] Could not normalize attachment path: ${attachmentPath}`);
369
+ return attachmentPath;
370
+ }
371
+ };
372
+
373
+ // src/reporter.ts
374
+ import * as fs3 from "fs";
375
+ import { readFileSync as readFileSync3 } from "fs";
376
+ import * as path3 from "path";
377
+ import { dirname, join as join3 } from "path";
378
+ import { fileURLToPath } from "url";
379
+
380
+ // ../clients/src/checkly-client.ts
381
+ import axios from "axios";
382
+
383
+ // ../clients/src/errors.ts
384
+ var ApiError = class extends Error {
385
+ data;
386
+ constructor(data, options) {
387
+ super(data.message, options);
388
+ this.name = this.constructor.name;
389
+ this.data = data;
390
+ }
391
+ };
392
+ var ValidationError = class extends ApiError {
393
+ constructor(data, options) {
394
+ super(data, options);
395
+ }
396
+ };
397
+ var UnauthorizedError = class extends ApiError {
398
+ constructor(data, options) {
399
+ super(data, options);
400
+ }
401
+ };
402
+ var ForbiddenError = class extends ApiError {
403
+ constructor(data, options) {
404
+ super(data, options);
405
+ }
406
+ };
407
+ var NotFoundError = class extends ApiError {
408
+ constructor(data, options) {
409
+ super(data, options);
410
+ }
411
+ };
412
+ var RequestTimeoutError = class extends ApiError {
413
+ constructor(data, options) {
414
+ super(data, options);
415
+ }
416
+ };
417
+ var ConflictError = class extends ApiError {
418
+ constructor(data, options) {
419
+ super(data, options);
420
+ }
421
+ };
422
+ var ServerError = class extends ApiError {
423
+ constructor(data, options) {
424
+ super(data, options);
425
+ }
426
+ };
427
+ var MiscellaneousError = class extends ApiError {
428
+ constructor(data, options) {
429
+ super(data, options);
430
+ }
431
+ };
432
+ var MissingResponseError = class extends Error {
433
+ constructor(message, options) {
434
+ super(message, options);
435
+ this.name = "MissingResponseError";
436
+ }
437
+ };
438
+ function parseErrorData(data, options) {
439
+ if (!data) {
440
+ return void 0;
441
+ }
442
+ if (typeof data === "object" && data.statusCode && data.error && data.message) {
443
+ return {
444
+ statusCode: data.statusCode,
445
+ error: data.error,
446
+ message: data.message,
447
+ errorCode: data.errorCode
448
+ };
449
+ }
450
+ if (typeof data === "object" && data.error && !data.message) {
451
+ return {
452
+ statusCode: options.statusCode,
453
+ error: data.error,
454
+ message: data.error
455
+ };
456
+ }
457
+ if (typeof data === "object" && data.error && data.message) {
458
+ return {
459
+ statusCode: options.statusCode,
460
+ error: data.error,
461
+ message: data.message,
462
+ errorCode: data.errorCode
463
+ };
464
+ }
465
+ if (typeof data === "object" && data.message) {
466
+ return {
467
+ statusCode: options.statusCode,
468
+ error: data.message,
469
+ message: data.message,
470
+ errorCode: data.errorCode
471
+ };
472
+ }
473
+ if (typeof data === "string") {
474
+ return {
475
+ statusCode: options.statusCode,
476
+ error: data,
477
+ message: data
478
+ };
479
+ }
480
+ return void 0;
481
+ }
482
+ function handleErrorResponse(err) {
483
+ if (!err.response) {
484
+ throw new MissingResponseError(err.message || "Network error");
485
+ }
486
+ const { status, data } = err.response;
487
+ const errorData = parseErrorData(data, { statusCode: status });
488
+ if (!errorData) {
489
+ throw new MiscellaneousError({
490
+ statusCode: status,
491
+ error: "Unknown error",
492
+ message: err.message || "An error occurred"
493
+ });
494
+ }
495
+ switch (status) {
496
+ case 400:
497
+ throw new ValidationError(errorData);
498
+ case 401:
499
+ throw new UnauthorizedError(errorData);
500
+ case 403:
501
+ throw new ForbiddenError(errorData);
502
+ case 404:
503
+ throw new NotFoundError(errorData);
504
+ case 408:
505
+ throw new RequestTimeoutError(errorData);
506
+ case 409:
507
+ throw new ConflictError(errorData);
508
+ default:
509
+ if (status >= 500) {
510
+ throw new ServerError(errorData);
511
+ }
512
+ throw new MiscellaneousError(errorData);
513
+ }
514
+ }
515
+
516
+ // ../clients/src/checkly-client.ts
517
+ function getVersion() {
518
+ return "0.1.0";
519
+ }
520
+ function createRequestInterceptor(apiKey, accountId) {
521
+ return (config) => {
522
+ if (config.headers) {
523
+ config.headers.Authorization = `Bearer ${apiKey}`;
524
+ config.headers["x-checkly-account"] = accountId;
525
+ config.headers["User-Agent"] = `@checkly/playwright-reporter/${getVersion()}`;
526
+ }
527
+ return config;
528
+ };
529
+ }
530
+ function createResponseErrorInterceptor() {
531
+ return (error) => {
532
+ handleErrorResponse(error);
533
+ };
534
+ }
535
+ var ChecklyClient = class {
536
+ apiKey;
537
+ baseUrl;
538
+ accountId;
539
+ api;
540
+ constructor(options) {
541
+ this.accountId = options.accountId;
542
+ this.apiKey = options.apiKey;
543
+ this.baseUrl = options.baseUrl;
544
+ this.api = axios.create({
545
+ baseURL: this.baseUrl,
546
+ timeout: 12e4,
547
+ // 120 second timeout for large uploads
548
+ maxContentLength: Number.POSITIVE_INFINITY,
549
+ // Allow large payloads
550
+ maxBodyLength: Number.POSITIVE_INFINITY
551
+ // Allow large request bodies
552
+ });
553
+ this.api.interceptors.request.use(createRequestInterceptor(this.apiKey, this.accountId));
554
+ this.api.interceptors.response.use((response) => response, createResponseErrorInterceptor());
555
+ }
556
+ /**
557
+ * Gets the underlying axios instance
558
+ * Useful for creating resource-specific clients (e.g., TestResults)
559
+ */
560
+ getAxiosInstance() {
561
+ return this.api;
562
+ }
563
+ };
564
+
565
+ // ../clients/src/test-results.ts
566
+ import FormData from "form-data";
567
+ var TestResults = class {
568
+ constructor(api) {
569
+ this.api = api;
570
+ }
571
+ /**
572
+ * Creates a new test session in Checkly
573
+ *
574
+ * @param request Test session creation request
575
+ * @returns Test session response with session ID and test result IDs
576
+ * @throws {ValidationError} If request data is invalid
577
+ * @throws {UnauthorizedError} If authentication fails
578
+ * @throws {ServerError} If server error occurs
579
+ */
580
+ async createTestSession(request) {
581
+ const response = await this.api.post("/next/test-sessions/create", request);
582
+ return response.data;
583
+ }
584
+ /**
585
+ * Step 1: Upload test result assets to S3
586
+ * Streams a ZIP file containing test assets (traces, videos, screenshots)
587
+ *
588
+ * @param testSessionId ID of the test session
589
+ * @param testResultId ID of the test result
590
+ * @param assets Buffer or ReadableStream of the ZIP file
591
+ * @returns Upload response with assetId, region, key, and url
592
+ * @throws {ValidationError} If assets are invalid
593
+ * @throws {UnauthorizedError} If authentication fails
594
+ * @throws {NotFoundError} If test session or result not found
595
+ * @throws {PayloadTooLargeError} If assets exceed 500MB
596
+ * @throws {ServerError} If S3 upload fails
597
+ */
598
+ async uploadTestResultAsset(testSessionId, testResultId, assets) {
599
+ const form = new FormData();
600
+ form.append("assets", assets, {
601
+ filename: "assets.zip",
602
+ contentType: "application/zip"
603
+ });
604
+ const response = await this.api.post(
605
+ `/next/test-sessions/${testSessionId}/results/${testResultId}/assets`,
606
+ form,
607
+ {
608
+ headers: {
609
+ ...form.getHeaders()
610
+ }
611
+ }
612
+ );
613
+ return response.data;
614
+ }
615
+ /**
616
+ * Step 2: Update test result with status and optional asset reference
617
+ * Uses JSON payload for clean, easy-to-validate updates
618
+ *
619
+ * @param testSessionId ID of the test session
620
+ * @param testResultId ID of the test result to update
621
+ * @param request Test result update request (JSON)
622
+ * @returns Test result update response
623
+ * @throws {ValidationError} If request data is invalid
624
+ * @throws {UnauthorizedError} If authentication fails
625
+ * @throws {NotFoundError} If test session or result not found
626
+ * @throws {ServerError} If server error occurs
627
+ */
628
+ async updateTestResult(testSessionId, testResultId, request) {
629
+ const response = await this.api.post(
630
+ `/next/test-sessions/${testSessionId}/results/${testResultId}`,
631
+ request
632
+ );
633
+ return response.data;
634
+ }
635
+ };
636
+
637
+ // src/reporter.ts
638
+ var __filename = fileURLToPath(import.meta.url);
639
+ var __dirname = dirname(__filename);
640
+ var packageJson = JSON.parse(readFileSync3(join3(__dirname, "..", "package.json"), "utf-8"));
641
+ var pkgVersion = packageJson.version;
642
+ var pluralRules = new Intl.PluralRules("en-US");
643
+ var projectForms = {
644
+ zero: "Project",
645
+ one: "Project",
646
+ two: "Projects",
647
+ few: "Projects",
648
+ many: "Projects",
649
+ other: "Projects"
650
+ };
651
+ function getApiUrl(environment) {
652
+ const environments = {
653
+ local: "http://127.0.0.1:3000",
654
+ development: "https://api-dev.checklyhq.com",
655
+ staging: "https://api-test.checklyhq.com",
656
+ production: "https://api.checklyhq.com"
657
+ };
658
+ return environments[environment];
659
+ }
660
+ function getEnvironment(options) {
661
+ const envFromOptions = options?.environment;
662
+ const envFromEnvVar = process.env.CHECKLY_ENV;
663
+ const env = envFromOptions || envFromEnvVar || "production";
664
+ const validEnvironments = ["local", "development", "staging", "production"];
665
+ if (!validEnvironments.includes(env)) {
666
+ console.warn(`[Checkly Reporter] Invalid environment "${env}", using "production"`);
667
+ return "production";
668
+ }
669
+ return env;
670
+ }
671
+ function convertStepToJSON(step) {
672
+ return {
673
+ title: step.title,
674
+ duration: step.duration,
675
+ error: step.error,
676
+ steps: step.steps.length > 0 ? step.steps.map(convertStepToJSON) : void 0
677
+ };
678
+ }
679
+ function getDirectoryName() {
680
+ const cwd = process.cwd();
681
+ let dirName = path3.basename(cwd);
682
+ if (!dirName || dirName === "/" || dirName === ".") {
683
+ dirName = "playwright-tests";
684
+ }
685
+ dirName = dirName.replace(/[<>:"|?*]/g, "-");
686
+ if (dirName.length > 255) {
687
+ dirName = dirName.substring(0, 255);
688
+ }
689
+ return dirName;
690
+ }
691
+ var ChecklyReporter = class {
692
+ options;
693
+ assetCollector;
694
+ zipper;
695
+ testResults;
696
+ testSession;
697
+ startTime;
698
+ testCounts = {
699
+ passed: 0,
700
+ failed: 0,
701
+ flaky: 0
702
+ };
703
+ // Store steps per test result, keyed by "testId:retry"
704
+ stepsMap = /* @__PURE__ */ new Map();
705
+ // Store warnings per test result, keyed by "testId:retry"
706
+ warningsMap = /* @__PURE__ */ new Map();
707
+ constructor(options = {}) {
708
+ const environment = getEnvironment(options);
709
+ const baseUrl = getApiUrl(environment);
710
+ const apiKey = process.env.CHECKLY_API_KEY || options.apiKey;
711
+ const accountId = process.env.CHECKLY_ACCOUNT_ID || options.accountId;
712
+ this.options = {
713
+ accountId,
714
+ apiKey,
715
+ outputPath: options.outputPath ?? "checkly-report.zip",
716
+ jsonReportPath: options.jsonReportPath ?? "test-results/playwright-test-report.json",
717
+ testResultsDir: options.testResultsDir ?? "test-results",
718
+ dryRun: options.dryRun ?? false,
719
+ sessionName: options.sessionName
720
+ };
721
+ this.assetCollector = new AssetCollector(this.options.testResultsDir);
722
+ this.zipper = new Zipper({
723
+ outputPath: this.options.outputPath
724
+ });
725
+ if (!this.options.dryRun && this.options.apiKey && this.options.accountId) {
726
+ const client = new ChecklyClient({
727
+ apiKey: this.options.apiKey,
728
+ accountId: this.options.accountId,
729
+ baseUrl
730
+ });
731
+ this.testResults = new TestResults(client.getAxiosInstance());
732
+ }
733
+ }
734
+ /**
735
+ * Resolves the session name from options
736
+ * Supports string, callback function, or falls back to default
737
+ */
738
+ resolveSessionName(context) {
739
+ const { sessionName } = this.options;
740
+ if (typeof sessionName === "function") {
741
+ return sessionName(context);
742
+ }
743
+ if (typeof sessionName === "string") {
744
+ return sessionName;
745
+ }
746
+ return `Playwright Test Session: ${context.directoryName}`;
747
+ }
748
+ /**
749
+ * Checks if test result has a trace attachment and adds context-aware warning if missing
750
+ * The warning type depends on the trace configuration and test result state
751
+ */
752
+ checkTraceAttachment(test, result) {
753
+ const warningsKey = `${test.id}:${result.retry}`;
754
+ const hasTrace = result.attachments?.some(
755
+ (attachment) => attachment.name === "trace" || attachment.contentType === "application/zip"
756
+ );
757
+ if (hasTrace) {
758
+ return;
759
+ }
760
+ const traceConfig = test.parent?.project()?.use?.trace;
761
+ const traceMode = typeof traceConfig === "object" ? traceConfig.mode : traceConfig;
762
+ const isRetry = result.retry > 0;
763
+ const testPassed = result.status === "passed";
764
+ let warningType;
765
+ let message;
766
+ switch (traceMode) {
767
+ case void 0:
768
+ return;
769
+ case "off":
770
+ warningType = "trace-off";
771
+ message = 'Traces are disabled. Set trace: "on" in playwright.config.ts to capture traces.';
772
+ break;
773
+ case "retain-on-failure":
774
+ if (testPassed) {
775
+ warningType = "trace-retained-on-failure";
776
+ message = 'No trace retained because test passed. Trace mode is "retain-on-failure" which discards traces for passing tests.';
777
+ } else {
778
+ warningType = "trace-missing";
779
+ message = 'Trace should exist but was not found. The test failed with trace: "retain-on-failure".';
780
+ }
781
+ break;
782
+ case "on-first-retry":
783
+ if (!isRetry) {
784
+ warningType = "trace-first-retry-only";
785
+ message = 'No trace for initial attempt. Trace mode is "on-first-retry" which only records traces on the first retry.';
786
+ } else if (result.retry === 1) {
787
+ warningType = "trace-missing";
788
+ message = 'Trace should exist but was not found. This is the first retry with trace: "on-first-retry".';
789
+ } else {
790
+ warningType = "trace-first-retry-only";
791
+ message = `No trace for retry #${result.retry}. Trace mode is "on-first-retry" which only records the first retry.`;
792
+ }
793
+ break;
794
+ case "on-all-retries":
795
+ if (!isRetry) {
796
+ warningType = "trace-retries-only";
797
+ message = 'No trace for initial attempt. Trace mode is "on-all-retries" which only records traces on retries.';
798
+ } else {
799
+ warningType = "trace-missing";
800
+ message = `Trace should exist but was not found. This is retry #${result.retry} with trace: "on-all-retries".`;
801
+ }
802
+ break;
803
+ case "retain-on-first-failure":
804
+ if (testPassed) {
805
+ warningType = "trace-retained-on-first-failure";
806
+ message = 'No trace retained because test passed. Trace mode is "retain-on-first-failure" which discards traces for passing tests.';
807
+ } else if (isRetry) {
808
+ warningType = "trace-retained-on-first-failure";
809
+ message = 'No trace for retries. Trace mode is "retain-on-first-failure" which only records the first run.';
810
+ } else {
811
+ warningType = "trace-missing";
812
+ message = 'Trace should exist but was not found. The test failed on first run with trace: "retain-on-first-failure".';
813
+ }
814
+ break;
815
+ case "on":
816
+ warningType = "trace-missing";
817
+ message = 'Trace should exist but was not found. Trace mode is "on" which should always record traces.';
818
+ break;
819
+ default:
820
+ warningType = "trace-missing";
821
+ message = `No trace found. Trace mode "${traceMode}" may not be generating traces for this result.`;
822
+ }
823
+ const warnings = this.warningsMap.get(warningsKey) || [];
824
+ warnings.push({ type: warningType, message });
825
+ this.warningsMap.set(warningsKey, warnings);
826
+ }
827
+ /**
828
+ * Called once before running tests
829
+ * Creates test session in Checkly if credentials provided
830
+ */
831
+ onBegin(config, suite) {
832
+ this.startTime = /* @__PURE__ */ new Date();
833
+ if (!this.testResults) {
834
+ return;
835
+ }
836
+ try {
837
+ const directoryName = getDirectoryName();
838
+ const sessionName = this.resolveSessionName({ directoryName, config, suite });
839
+ const testResults = [{ name: directoryName }];
840
+ const repoUrl = process.env.GITHUB_REPOSITORY ? `https://github.com/${process.env.GITHUB_REPOSITORY}` : void 0;
841
+ const repoInfo = repoUrl ? {
842
+ repoUrl,
843
+ commitId: process.env.GITHUB_SHA,
844
+ branchName: process.env.GITHUB_REF_NAME,
845
+ commitOwner: process.env.GITHUB_ACTOR,
846
+ commitMessage: process.env.GITHUB_EVENT_NAME
847
+ } : void 0;
848
+ this.testResults.createTestSession({
849
+ name: sessionName,
850
+ environment: process.env.NODE_ENV || "test",
851
+ repoInfo,
852
+ startedAt: this.startTime.getTime(),
853
+ // Required timestamp in milliseconds
854
+ testResults,
855
+ provider: "PW_REPORTER"
856
+ }).then((response) => {
857
+ this.testSession = response;
858
+ }).catch((error) => {
859
+ console.error("[Checkly Reporter] Failed to create test session:", error.message);
860
+ });
861
+ } catch (error) {
862
+ console.error("[Checkly Reporter] Error in onBegin:", error);
863
+ }
864
+ }
865
+ /**
866
+ * Called for each test when it completes
867
+ * Captures steps and warnings, tracks test results for final status calculation
868
+ */
869
+ onTestEnd(test, result) {
870
+ try {
871
+ this.checkTraceAttachment(test, result);
872
+ const stepsKey = `${test.id}:${result.retry}`;
873
+ if (result.steps && result.steps.length > 0) {
874
+ this.stepsMap.set(stepsKey, result.steps.map(convertStepToJSON));
875
+ }
876
+ const outcome = test.outcome();
877
+ const testIsComplete = result.retry === test.retries || outcome !== "unexpected";
878
+ if (!testIsComplete) {
879
+ return;
880
+ }
881
+ const isFlaky = outcome === "flaky";
882
+ if (isFlaky) {
883
+ this.testCounts.flaky++;
884
+ this.testCounts.passed++;
885
+ } else {
886
+ if (result.status === "passed") {
887
+ this.testCounts.passed++;
888
+ } else if (result.status === "failed" || result.status === "timedOut") {
889
+ this.testCounts.failed++;
890
+ }
891
+ }
892
+ } catch (error) {
893
+ console.error("[Checkly Reporter] Error in onTestEnd:", error);
894
+ }
895
+ }
896
+ /**
897
+ * Called after all tests have completed
898
+ * This is where we create the ZIP archive and upload results
899
+ */
900
+ async onEnd() {
901
+ try {
902
+ const jsonReportPath = this.options.jsonReportPath;
903
+ if (!fs3.existsSync(jsonReportPath)) {
904
+ console.error(`[Checkly Reporter] ERROR: JSON report not found at: ${jsonReportPath}`);
905
+ console.error("[Checkly Reporter] Make sure to configure the json reporter before the checkly reporter:");
906
+ console.error(
907
+ " reporter: [\n ['json', { outputFile: 'test-results/playwright-test-report.json' }],\n ['@checkly/playwright-reporter']\n ]"
908
+ );
909
+ return;
910
+ }
911
+ const reportContent = fs3.readFileSync(jsonReportPath, "utf-8");
912
+ const report = JSON.parse(reportContent);
913
+ this.injectDataIntoReport(report);
914
+ fs3.writeFileSync(jsonReportPath, JSON.stringify(report, null, 2), "utf-8");
915
+ const assets = await this.assetCollector.collectAssets(report);
916
+ const result = await this.zipper.createZip(jsonReportPath, assets);
917
+ if (this.testResults && this.testSession) {
918
+ await this.uploadResults(report, result.zipPath, result.entries);
919
+ if (!this.options.dryRun) {
920
+ try {
921
+ fs3.unlinkSync(result.zipPath);
922
+ } catch (cleanupError) {
923
+ console.warn(`[Checkly Reporter] Warning: Could not delete ZIP file: ${cleanupError}`);
924
+ }
925
+ }
926
+ }
927
+ if (this.testResults && this.testSession?.link) {
928
+ this.printSummary(report, this.testSession);
929
+ }
930
+ } catch (error) {
931
+ console.error("[Checkly Reporter] ERROR creating report:", error);
932
+ }
933
+ }
934
+ printSummary(report, testSession) {
935
+ const rule = pluralRules.select(report.config.projects.length);
936
+ console.log("\n======================================================\n");
937
+ console.log(`\u{1F99D} Checkly reporter: ${pkgVersion}`);
938
+ console.log(`\u{1F3AD} Playwright: ${report.config.version}`);
939
+ console.log(`\u{1F4D4} ${projectForms[rule]}: ${report.config.projects.map(({ name }) => name).join(",")}`);
940
+ console.log(`\u{1F517} Test session URL: ${testSession.link}`);
941
+ console.log("\n======================================================");
942
+ }
943
+ /**
944
+ * Injects captured steps and warnings into the JSON report
945
+ * Traverses the report structure and matches by test ID + retry
946
+ */
947
+ injectDataIntoReport(report) {
948
+ const processSuite = (suite) => {
949
+ for (const spec of suite.specs) {
950
+ for (const test of spec.tests) {
951
+ for (const result of test.results) {
952
+ const key = `${spec.id}:${result.retry}`;
953
+ const steps = this.stepsMap.get(key);
954
+ if (steps) {
955
+ result.steps = steps;
956
+ }
957
+ const warnings = this.warningsMap.get(key);
958
+ if (warnings && warnings.length > 0) {
959
+ result._checkly = { warnings };
960
+ }
961
+ }
962
+ }
963
+ }
964
+ if (suite.suites) {
965
+ for (const nestedSuite of suite.suites) {
966
+ processSuite(nestedSuite);
967
+ }
968
+ }
969
+ };
970
+ for (const suite of report.suites) {
971
+ processSuite(suite);
972
+ }
973
+ }
974
+ /**
975
+ * Uploads test results to Checkly API
976
+ */
977
+ async uploadResults(report, zipPath, entries) {
978
+ if (!this.testResults || !this.testSession) {
979
+ return;
980
+ }
981
+ try {
982
+ const { failed: failedCount, flaky: flakyCount } = this.testCounts;
983
+ const overallStatus = failedCount > 0 ? "FAILED" : "PASSED";
984
+ const isDegraded = failedCount === 0 && flakyCount > 0;
985
+ const endTime = /* @__PURE__ */ new Date();
986
+ const responseTime = this.startTime ? Math.max(0, endTime.getTime() - this.startTime.getTime()) : 0;
987
+ const zipSizeBytes = (await fs3.promises.stat(zipPath)).size;
988
+ if (this.testSession.testResults.length > 0) {
989
+ const firstResult = this.testSession.testResults[0];
990
+ let assetId;
991
+ if (zipSizeBytes > 0) {
992
+ try {
993
+ const assets = fs3.createReadStream(zipPath);
994
+ const uploadResponse = await this.testResults.uploadTestResultAsset(
995
+ this.testSession.testSessionId,
996
+ firstResult.testResultId,
997
+ assets
998
+ );
999
+ assetId = uploadResponse.assetId;
1000
+ } catch (error) {
1001
+ const errorMessage = error instanceof Error ? error.message : String(error);
1002
+ console.error("[Checkly Reporter] Asset upload failed:", errorMessage);
1003
+ }
1004
+ }
1005
+ await this.testResults.updateTestResult(this.testSession.testSessionId, firstResult.testResultId, {
1006
+ status: overallStatus,
1007
+ assetEntries: assetId ? entries : void 0,
1008
+ isDegraded,
1009
+ startedAt: this.startTime?.toISOString(),
1010
+ stoppedAt: endTime.toISOString(),
1011
+ responseTime,
1012
+ metadata: {
1013
+ usageData: {
1014
+ s3PostTotalBytes: zipSizeBytes
1015
+ }
1016
+ }
1017
+ });
1018
+ }
1019
+ } catch (error) {
1020
+ const errorMessage = error instanceof Error ? error.message : String(error);
1021
+ console.error("[Checkly Reporter] Failed to upload results:", errorMessage);
1022
+ }
1023
+ }
1024
+ /**
1025
+ * Called when a global error occurs
1026
+ */
1027
+ onError(error) {
1028
+ console.error("[Checkly Reporter] Global error:", error);
1029
+ }
1030
+ };
1031
+ export {
1032
+ AssetCollector,
1033
+ ChecklyReporter,
1034
+ Zipper,
1035
+ ChecklyReporter as default
1036
+ };