@checkly/playwright-reporter 0.1.9 → 0.1.10
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.d.ts +91 -2
- package/dist/index.js +434 -27
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -81,6 +81,13 @@ interface ChecklyReporterOptions {
|
|
|
81
81
|
* sessionName: ({ directoryName, config }) => `E2E: ${directoryName} (${config.projects.length} projects)`
|
|
82
82
|
*/
|
|
83
83
|
sessionName?: SessionNameOption;
|
|
84
|
+
/**
|
|
85
|
+
* Enable verbose logging for debugging
|
|
86
|
+
* Logs detailed information about each phase of report generation
|
|
87
|
+
* Can also be enabled via CHECKLY_REPORTER_VERBOSE=true environment variable
|
|
88
|
+
* @default false
|
|
89
|
+
*/
|
|
90
|
+
verbose?: boolean;
|
|
84
91
|
}
|
|
85
92
|
/**
|
|
86
93
|
* Warning types that can be attached to test results
|
|
@@ -99,6 +106,67 @@ interface ChecklyWarning {
|
|
|
99
106
|
*/
|
|
100
107
|
message: string;
|
|
101
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Console message type matching the webapp's CheckRunNavigationTracePage.console type
|
|
111
|
+
*/
|
|
112
|
+
type ConsoleMessageType = 'debug' | 'error' | 'info' | 'log' | 'warning';
|
|
113
|
+
/**
|
|
114
|
+
* Location of a console message in source code
|
|
115
|
+
*/
|
|
116
|
+
interface ConsoleMessageLocation {
|
|
117
|
+
url: string;
|
|
118
|
+
columnNumber: number;
|
|
119
|
+
lineNumber: number;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Console message extracted from Playwright trace
|
|
123
|
+
* Matches the webapp's CheckRunNavigationTracePage.console array item type
|
|
124
|
+
*/
|
|
125
|
+
interface ConsoleMessage {
|
|
126
|
+
/**
|
|
127
|
+
* Unique identifier for the console message
|
|
128
|
+
*/
|
|
129
|
+
id: string;
|
|
130
|
+
/**
|
|
131
|
+
* Source location where the console message was triggered
|
|
132
|
+
*/
|
|
133
|
+
location: ConsoleMessageLocation;
|
|
134
|
+
/**
|
|
135
|
+
* The text content of the console message
|
|
136
|
+
*/
|
|
137
|
+
text: string;
|
|
138
|
+
/**
|
|
139
|
+
* Timestamp when the message was logged (milliseconds)
|
|
140
|
+
*/
|
|
141
|
+
timestamp: number;
|
|
142
|
+
/**
|
|
143
|
+
* Type of console message
|
|
144
|
+
*/
|
|
145
|
+
type: ConsoleMessageType;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Network request extracted from Playwright trace
|
|
149
|
+
* Matches the webapp's CheckRunNavigationTraceNetworkEntry type
|
|
150
|
+
*/
|
|
151
|
+
interface NetworkRequest {
|
|
152
|
+
id: string;
|
|
153
|
+
url: string;
|
|
154
|
+
domain: string;
|
|
155
|
+
method: string;
|
|
156
|
+
resourceType: string;
|
|
157
|
+
statusCode: number;
|
|
158
|
+
statusText: string;
|
|
159
|
+
start: number;
|
|
160
|
+
startedAt: number;
|
|
161
|
+
finishedAt: number;
|
|
162
|
+
time: number;
|
|
163
|
+
hasFinished: boolean;
|
|
164
|
+
hasSucceeded: boolean;
|
|
165
|
+
requestHeaders: Record<string, string>;
|
|
166
|
+
responseHeaders: Record<string, string>;
|
|
167
|
+
transferBytes?: number;
|
|
168
|
+
resourceBytes?: number;
|
|
169
|
+
}
|
|
102
170
|
/**
|
|
103
171
|
* Checkly-specific extensions added to JSONReportTestResult
|
|
104
172
|
*/
|
|
@@ -107,6 +175,14 @@ interface ChecklyTestResultExtensions {
|
|
|
107
175
|
* Warnings about the test result (e.g., missing traces)
|
|
108
176
|
*/
|
|
109
177
|
warnings?: ChecklyWarning[];
|
|
178
|
+
/**
|
|
179
|
+
* Console messages extracted from the Playwright trace
|
|
180
|
+
*/
|
|
181
|
+
console?: ConsoleMessage[];
|
|
182
|
+
/**
|
|
183
|
+
* Network requests extracted from the Playwright trace
|
|
184
|
+
*/
|
|
185
|
+
network?: NetworkRequest[];
|
|
110
186
|
}
|
|
111
187
|
/**
|
|
112
188
|
* Extended JSONReportTestResult with Checkly-specific data
|
|
@@ -168,6 +244,13 @@ declare class ChecklyReporter implements Reporter {
|
|
|
168
244
|
private testCounts;
|
|
169
245
|
private stepsMap;
|
|
170
246
|
private warningsMap;
|
|
247
|
+
private tracePathsMap;
|
|
248
|
+
private consoleMessagesMap;
|
|
249
|
+
private networkRequestsMap;
|
|
250
|
+
/**
|
|
251
|
+
* Log a message if verbose mode is enabled
|
|
252
|
+
*/
|
|
253
|
+
private log;
|
|
171
254
|
constructor(options?: ChecklyReporterOptions);
|
|
172
255
|
/**
|
|
173
256
|
* Resolves the session name from options
|
|
@@ -176,6 +259,7 @@ declare class ChecklyReporter implements Reporter {
|
|
|
176
259
|
private resolveSessionName;
|
|
177
260
|
/**
|
|
178
261
|
* Checks if test result has a trace attachment and adds context-aware warning if missing
|
|
262
|
+
* Also captures trace file path for later console message extraction
|
|
179
263
|
* The warning type depends on the trace configuration and test result state
|
|
180
264
|
*/
|
|
181
265
|
private checkTraceAttachment;
|
|
@@ -196,7 +280,12 @@ declare class ChecklyReporter implements Reporter {
|
|
|
196
280
|
onEnd(): Promise<void>;
|
|
197
281
|
private printSummary;
|
|
198
282
|
/**
|
|
199
|
-
*
|
|
283
|
+
* Extracts console messages and network requests from all captured traces
|
|
284
|
+
* Called before injecting data into the report
|
|
285
|
+
*/
|
|
286
|
+
private extractDataFromTraces;
|
|
287
|
+
/**
|
|
288
|
+
* Injects captured steps, warnings, console messages, and network requests into the JSON report
|
|
200
289
|
* Traverses the report structure and matches by test ID + retry
|
|
201
290
|
*/
|
|
202
291
|
private injectDataIntoReport;
|
|
@@ -216,4 +305,4 @@ declare class ChecklyReporter implements Reporter {
|
|
|
216
305
|
onError(error: TestError): void;
|
|
217
306
|
}
|
|
218
307
|
|
|
219
|
-
export { ChecklyReporter, type ChecklyReporterOptions, type ChecklyTestResultExtensions, type ChecklyWarning, type ChecklyWarningType, type JSONReport, type JSONReportSpec, type JSONReportSuite, type JSONReportTest, type JSONReportTestResult, ChecklyReporter as default };
|
|
308
|
+
export { ChecklyReporter, type ChecklyReporterOptions, type ChecklyTestResultExtensions, type ChecklyWarning, type ChecklyWarningType, type ConsoleMessage, type ConsoleMessageLocation, type ConsoleMessageType, type JSONReport, type JSONReportSpec, type JSONReportSuite, type JSONReportTest, type JSONReportTestResult, type NetworkRequest, ChecklyReporter as default };
|
package/dist/index.js
CHANGED
|
@@ -234,8 +234,261 @@ var AssetCollector = class {
|
|
|
234
234
|
}
|
|
235
235
|
};
|
|
236
236
|
|
|
237
|
-
// ../utils/src/
|
|
237
|
+
// ../utils/src/console-adapter.ts
|
|
238
|
+
import { createHash } from "crypto";
|
|
239
|
+
function normalizeType(messageType) {
|
|
240
|
+
switch (messageType.toLowerCase()) {
|
|
241
|
+
case "debug":
|
|
242
|
+
return "debug";
|
|
243
|
+
case "error":
|
|
244
|
+
return "error";
|
|
245
|
+
case "info":
|
|
246
|
+
return "info";
|
|
247
|
+
case "warning":
|
|
248
|
+
case "warn":
|
|
249
|
+
return "warning";
|
|
250
|
+
default:
|
|
251
|
+
return "log";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function generateId(time, messageType, text, url) {
|
|
255
|
+
return createHash("sha256").update(`${time}-${messageType}-${text}-${url}`).digest("hex").substring(0, 16);
|
|
256
|
+
}
|
|
257
|
+
function toConsoleMessage(event) {
|
|
258
|
+
const url = event.location?.url || "";
|
|
259
|
+
return {
|
|
260
|
+
id: generateId(event.time, event.messageType, event.text, url),
|
|
261
|
+
location: {
|
|
262
|
+
url,
|
|
263
|
+
columnNumber: event.location?.columnNumber || 0,
|
|
264
|
+
lineNumber: event.location?.lineNumber || 0
|
|
265
|
+
},
|
|
266
|
+
text: event.text || "",
|
|
267
|
+
timestamp: event.time,
|
|
268
|
+
type: normalizeType(event.messageType)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ../utils/src/network-adapter.ts
|
|
273
|
+
import { createHash as createHash2 } from "crypto";
|
|
274
|
+
function generateId2(url, method, startedAt) {
|
|
275
|
+
return createHash2("sha256").update(`${url}-${method}-${startedAt}`).digest("hex").substring(0, 16);
|
|
276
|
+
}
|
|
277
|
+
function extractDomain(url) {
|
|
278
|
+
try {
|
|
279
|
+
return new URL(url).hostname;
|
|
280
|
+
} catch {
|
|
281
|
+
return "";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function headersArrayToRecord(headers) {
|
|
285
|
+
const record = {};
|
|
286
|
+
for (const { name, value } of headers) {
|
|
287
|
+
record[name.toLowerCase()] = value;
|
|
288
|
+
}
|
|
289
|
+
return record;
|
|
290
|
+
}
|
|
291
|
+
function isSuccessStatus(status) {
|
|
292
|
+
return status >= 200 && status < 400;
|
|
293
|
+
}
|
|
294
|
+
function determineResourceType(snapshot) {
|
|
295
|
+
if (snapshot._resourceType) {
|
|
296
|
+
return snapshot._resourceType;
|
|
297
|
+
}
|
|
298
|
+
if (snapshot._apiRequest) {
|
|
299
|
+
return "fetch";
|
|
300
|
+
}
|
|
301
|
+
return "other";
|
|
302
|
+
}
|
|
303
|
+
function toNetworkRequest(event) {
|
|
304
|
+
const { snapshot } = event;
|
|
305
|
+
const startedAt = new Date(snapshot.startedDateTime).getTime();
|
|
306
|
+
const time = Math.round(snapshot.time);
|
|
307
|
+
const finishedAt = startedAt + time;
|
|
308
|
+
const statusCode = snapshot.response.status;
|
|
309
|
+
const url = snapshot.request.url;
|
|
310
|
+
const method = snapshot.request.method;
|
|
311
|
+
return {
|
|
312
|
+
id: generateId2(url, method, startedAt),
|
|
313
|
+
url,
|
|
314
|
+
domain: extractDomain(url),
|
|
315
|
+
method,
|
|
316
|
+
resourceType: determineResourceType(snapshot),
|
|
317
|
+
statusCode,
|
|
318
|
+
statusText: snapshot.response.statusText || "",
|
|
319
|
+
start: startedAt,
|
|
320
|
+
startedAt,
|
|
321
|
+
finishedAt,
|
|
322
|
+
time,
|
|
323
|
+
hasFinished: true,
|
|
324
|
+
hasSucceeded: isSuccessStatus(statusCode),
|
|
325
|
+
requestHeaders: headersArrayToRecord(snapshot.request.headers || []),
|
|
326
|
+
responseHeaders: headersArrayToRecord(snapshot.response.headers || []),
|
|
327
|
+
transferBytes: snapshot.response._transferSize,
|
|
328
|
+
resourceBytes: snapshot.response.content?.size
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ../utils/src/trace-reader.ts
|
|
238
333
|
import * as fs2 from "fs";
|
|
334
|
+
|
|
335
|
+
// ../utils/src/zip-reader.ts
|
|
336
|
+
import { promisify } from "util";
|
|
337
|
+
import * as zlib from "zlib";
|
|
338
|
+
var gunzip2 = promisify(zlib.gunzip);
|
|
339
|
+
var inflateRaw2 = promisify(zlib.inflateRaw);
|
|
340
|
+
function parseZipEntries(zipBuffer) {
|
|
341
|
+
const EOCD_SIG = 101010256;
|
|
342
|
+
let eocdOffset = -1;
|
|
343
|
+
for (let i = zipBuffer.length - 22; i >= 0; i--) {
|
|
344
|
+
if (zipBuffer.readUInt32LE(i) === EOCD_SIG) {
|
|
345
|
+
eocdOffset = i;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (eocdOffset === -1) {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
const cdOffset = zipBuffer.readUInt32LE(eocdOffset + 16);
|
|
353
|
+
const cdEntries = zipBuffer.readUInt16LE(eocdOffset + 10);
|
|
354
|
+
const entries = [];
|
|
355
|
+
const CD_SIG = 33639248;
|
|
356
|
+
let offset = cdOffset;
|
|
357
|
+
for (let i = 0; i < cdEntries; i++) {
|
|
358
|
+
if (zipBuffer.readUInt32LE(offset) !== CD_SIG) {
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
const compressionMethod = zipBuffer.readUInt16LE(offset + 10);
|
|
362
|
+
const compressedSize = zipBuffer.readUInt32LE(offset + 20);
|
|
363
|
+
const fileNameLength = zipBuffer.readUInt16LE(offset + 28);
|
|
364
|
+
const extraFieldLength = zipBuffer.readUInt16LE(offset + 30);
|
|
365
|
+
const commentLength = zipBuffer.readUInt16LE(offset + 32);
|
|
366
|
+
const localHeaderOffset = zipBuffer.readUInt32LE(offset + 42);
|
|
367
|
+
const fileName = zipBuffer.subarray(offset + 46, offset + 46 + fileNameLength).toString("utf-8");
|
|
368
|
+
entries.push({
|
|
369
|
+
fileName,
|
|
370
|
+
compressionMethod,
|
|
371
|
+
compressedSize,
|
|
372
|
+
localHeaderOffset
|
|
373
|
+
});
|
|
374
|
+
offset += 46 + fileNameLength + extraFieldLength + commentLength;
|
|
375
|
+
}
|
|
376
|
+
return entries;
|
|
377
|
+
}
|
|
378
|
+
async function readZipEntryContent(zipBuffer, entry) {
|
|
379
|
+
const LOCAL_SIG = 67324752;
|
|
380
|
+
if (zipBuffer.readUInt32LE(entry.localHeaderOffset) !== LOCAL_SIG) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
const localFileNameLength = zipBuffer.readUInt16LE(entry.localHeaderOffset + 26);
|
|
384
|
+
const localExtraLength = zipBuffer.readUInt16LE(entry.localHeaderOffset + 28);
|
|
385
|
+
const dataOffset = entry.localHeaderOffset + 30 + localFileNameLength + localExtraLength;
|
|
386
|
+
const compressedData = zipBuffer.subarray(dataOffset, dataOffset + entry.compressedSize);
|
|
387
|
+
let buffer;
|
|
388
|
+
if (entry.compressionMethod === 0) {
|
|
389
|
+
buffer = compressedData;
|
|
390
|
+
} else if (entry.compressionMethod === 8) {
|
|
391
|
+
buffer = await inflateRaw2(compressedData);
|
|
392
|
+
} else {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
if (buffer.length >= 2 && buffer[0] === 31 && buffer[1] === 139) {
|
|
396
|
+
const decompressed = await gunzip2(buffer);
|
|
397
|
+
return decompressed.toString("utf-8");
|
|
398
|
+
}
|
|
399
|
+
return buffer.toString("utf-8");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ../utils/src/trace-reader.ts
|
|
403
|
+
var TraceReader = class {
|
|
404
|
+
constructor(tracePath) {
|
|
405
|
+
this.tracePath = tracePath;
|
|
406
|
+
}
|
|
407
|
+
zipBuffer = null;
|
|
408
|
+
traceEntries = [];
|
|
409
|
+
async open() {
|
|
410
|
+
if (!fs2.existsSync(this.tracePath)) {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
this.zipBuffer = fs2.readFileSync(this.tracePath);
|
|
415
|
+
const entries = parseZipEntries(this.zipBuffer);
|
|
416
|
+
this.traceEntries = entries.filter(
|
|
417
|
+
(e) => (/^\d+-trace\.trace$/.test(e.fileName) || /^\d+-trace\.network$/.test(e.fileName)) && !e.fileName.includes("/")
|
|
418
|
+
);
|
|
419
|
+
return this.traceEntries.length > 0;
|
|
420
|
+
} catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Extracts events matching a text filter.
|
|
426
|
+
*
|
|
427
|
+
* @param textFilter - Substring to match (e.g., '"type":"console"')
|
|
428
|
+
* @param adapter - Optional adapter to transform events
|
|
429
|
+
*/
|
|
430
|
+
async extractEvents(textFilter, adapter) {
|
|
431
|
+
if (!this.zipBuffer) {
|
|
432
|
+
throw new Error("TraceReader not opened. Call open() first.");
|
|
433
|
+
}
|
|
434
|
+
const results = [];
|
|
435
|
+
for (const traceEntry of this.traceEntries) {
|
|
436
|
+
const content = await readZipEntryContent(this.zipBuffer, traceEntry);
|
|
437
|
+
if (!content) continue;
|
|
438
|
+
for (const line of content.split("\n")) {
|
|
439
|
+
if (line.indexOf(textFilter) !== -1) {
|
|
440
|
+
try {
|
|
441
|
+
const event = JSON.parse(line);
|
|
442
|
+
results.push(adapter ? adapter(event) : event);
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (results.length > 0 && typeof results[0].time === "number") {
|
|
449
|
+
return results.sort((a, b) => a.time - b.time);
|
|
450
|
+
}
|
|
451
|
+
return results;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Extracts all events from the trace.
|
|
455
|
+
*/
|
|
456
|
+
async extractAllEvents(adapter) {
|
|
457
|
+
if (!this.zipBuffer) {
|
|
458
|
+
throw new Error("TraceReader not opened. Call open() first.");
|
|
459
|
+
}
|
|
460
|
+
const results = [];
|
|
461
|
+
for (const traceEntry of this.traceEntries) {
|
|
462
|
+
const content = await readZipEntryContent(this.zipBuffer, traceEntry);
|
|
463
|
+
if (!content) continue;
|
|
464
|
+
for (const line of content.split("\n")) {
|
|
465
|
+
if (line.trim()) {
|
|
466
|
+
try {
|
|
467
|
+
const event = JSON.parse(line);
|
|
468
|
+
results.push(adapter ? adapter(event) : event);
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (results.length > 0 && typeof results[0].time === "number") {
|
|
475
|
+
return results.sort((a, b) => a.time - b.time);
|
|
476
|
+
}
|
|
477
|
+
return results;
|
|
478
|
+
}
|
|
479
|
+
listFiles() {
|
|
480
|
+
if (!this.zipBuffer) {
|
|
481
|
+
return [];
|
|
482
|
+
}
|
|
483
|
+
return parseZipEntries(this.zipBuffer).map((e) => e.fileName);
|
|
484
|
+
}
|
|
485
|
+
isOpen() {
|
|
486
|
+
return this.zipBuffer !== null;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// ../utils/src/zipper.ts
|
|
491
|
+
import * as fs3 from "fs";
|
|
239
492
|
import * as os from "os";
|
|
240
493
|
import * as path2 from "path";
|
|
241
494
|
import { ZipArchive } from "archiver";
|
|
@@ -254,7 +507,7 @@ var Zipper = class {
|
|
|
254
507
|
const entries = [];
|
|
255
508
|
return new Promise((resolve2, reject) => {
|
|
256
509
|
try {
|
|
257
|
-
const output =
|
|
510
|
+
const output = fs3.createWriteStream(this.outputPath);
|
|
258
511
|
const archive = new ZipArchive({
|
|
259
512
|
zlib: { level: 0 }
|
|
260
513
|
});
|
|
@@ -284,14 +537,14 @@ var Zipper = class {
|
|
|
284
537
|
reject(err);
|
|
285
538
|
});
|
|
286
539
|
archive.pipe(output);
|
|
287
|
-
if (!
|
|
540
|
+
if (!fs3.existsSync(reportPath)) {
|
|
288
541
|
reject(new Error(`Report file not found: ${reportPath}`));
|
|
289
542
|
return;
|
|
290
543
|
}
|
|
291
544
|
const transformedReportPath = this.transformJsonReport(reportPath);
|
|
292
545
|
archive.file(transformedReportPath, { name: "output/playwright-test-report.json" });
|
|
293
546
|
for (const asset of assets) {
|
|
294
|
-
if (!
|
|
547
|
+
if (!fs3.existsSync(asset.sourcePath)) {
|
|
295
548
|
console.warn(`[Checkly Reporter] Skipping missing asset: ${asset.sourcePath}`);
|
|
296
549
|
continue;
|
|
297
550
|
}
|
|
@@ -310,11 +563,11 @@ var Zipper = class {
|
|
|
310
563
|
* @returns Path to the transformed JSON report (in temp directory)
|
|
311
564
|
*/
|
|
312
565
|
transformJsonReport(reportPath) {
|
|
313
|
-
const reportContent =
|
|
566
|
+
const reportContent = fs3.readFileSync(reportPath, "utf-8");
|
|
314
567
|
const report = JSON.parse(reportContent);
|
|
315
568
|
this.transformAttachmentPaths(report);
|
|
316
569
|
const tempReportPath = path2.join(os.tmpdir(), `playwright-test-report-${Date.now()}.json`);
|
|
317
|
-
|
|
570
|
+
fs3.writeFileSync(tempReportPath, JSON.stringify(report, null, 2));
|
|
318
571
|
return tempReportPath;
|
|
319
572
|
}
|
|
320
573
|
/**
|
|
@@ -392,8 +645,8 @@ var Zipper = class {
|
|
|
392
645
|
};
|
|
393
646
|
|
|
394
647
|
// src/reporter.ts
|
|
395
|
-
import * as
|
|
396
|
-
import { readFileSync as
|
|
648
|
+
import * as fs4 from "fs";
|
|
649
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
397
650
|
import * as path3 from "path";
|
|
398
651
|
import { dirname, join as join3 } from "path";
|
|
399
652
|
import { fileURLToPath } from "url";
|
|
@@ -658,7 +911,7 @@ var TestResults = class {
|
|
|
658
911
|
// src/reporter.ts
|
|
659
912
|
var __filename = fileURLToPath(import.meta.url);
|
|
660
913
|
var __dirname = dirname(__filename);
|
|
661
|
-
var packageJson = JSON.parse(
|
|
914
|
+
var packageJson = JSON.parse(readFileSync4(join3(__dirname, "..", "package.json"), "utf-8"));
|
|
662
915
|
var pkgVersion = packageJson.version;
|
|
663
916
|
var pluralRules = new Intl.PluralRules("en-US");
|
|
664
917
|
var projectForms = {
|
|
@@ -725,11 +978,30 @@ var ChecklyReporter = class {
|
|
|
725
978
|
stepsMap = /* @__PURE__ */ new Map();
|
|
726
979
|
// Store warnings per test result, keyed by "testId:retry"
|
|
727
980
|
warningsMap = /* @__PURE__ */ new Map();
|
|
981
|
+
// Store trace file paths per test result, keyed by "testId:retry"
|
|
982
|
+
tracePathsMap = /* @__PURE__ */ new Map();
|
|
983
|
+
// Store console messages per test result, keyed by "testId:retry"
|
|
984
|
+
consoleMessagesMap = /* @__PURE__ */ new Map();
|
|
985
|
+
// Store network requests per test result, keyed by "testId:retry"
|
|
986
|
+
networkRequestsMap = /* @__PURE__ */ new Map();
|
|
987
|
+
/**
|
|
988
|
+
* Log a message if verbose mode is enabled
|
|
989
|
+
*/
|
|
990
|
+
log(message, data) {
|
|
991
|
+
if (!this.options.verbose) return;
|
|
992
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
993
|
+
if (data) {
|
|
994
|
+
console.log(`[Checkly Reporter DEBUG ${timestamp}] ${message}`, JSON.stringify(data, null, 2));
|
|
995
|
+
} else {
|
|
996
|
+
console.log(`[Checkly Reporter DEBUG ${timestamp}] ${message}`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
728
999
|
constructor(options = {}) {
|
|
729
1000
|
const environment = getEnvironment(options);
|
|
730
1001
|
const baseUrl = getApiUrl(environment);
|
|
731
1002
|
const apiKey = process.env.CHECKLY_API_KEY || options.apiKey;
|
|
732
1003
|
const accountId = process.env.CHECKLY_ACCOUNT_ID || options.accountId;
|
|
1004
|
+
const verbose = options.verbose ?? process.env.CHECKLY_REPORTER_VERBOSE === "true";
|
|
733
1005
|
this.options = {
|
|
734
1006
|
accountId,
|
|
735
1007
|
apiKey,
|
|
@@ -737,7 +1009,8 @@ var ChecklyReporter = class {
|
|
|
737
1009
|
jsonReportPath: options.jsonReportPath ?? "test-results/playwright-test-report.json",
|
|
738
1010
|
testResultsDir: options.testResultsDir ?? "test-results",
|
|
739
1011
|
dryRun: options.dryRun ?? false,
|
|
740
|
-
sessionName: options.sessionName
|
|
1012
|
+
sessionName: options.sessionName,
|
|
1013
|
+
verbose
|
|
741
1014
|
};
|
|
742
1015
|
this.assetCollector = new AssetCollector(this.options.testResultsDir);
|
|
743
1016
|
this.zipper = new Zipper({
|
|
@@ -751,6 +1024,20 @@ var ChecklyReporter = class {
|
|
|
751
1024
|
});
|
|
752
1025
|
this.testResults = new TestResults(client.getAxiosInstance());
|
|
753
1026
|
}
|
|
1027
|
+
if (verbose) {
|
|
1028
|
+
console.log(`[Checkly Reporter DEBUG] Initialized with options:`, {
|
|
1029
|
+
environment,
|
|
1030
|
+
baseUrl,
|
|
1031
|
+
hasApiKey: !!apiKey,
|
|
1032
|
+
hasAccountId: !!accountId,
|
|
1033
|
+
outputPath: this.options.outputPath,
|
|
1034
|
+
jsonReportPath: this.options.jsonReportPath,
|
|
1035
|
+
testResultsDir: this.options.testResultsDir,
|
|
1036
|
+
dryRun: this.options.dryRun,
|
|
1037
|
+
verbose: this.options.verbose,
|
|
1038
|
+
hasTestResults: !!this.testResults
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
754
1041
|
}
|
|
755
1042
|
/**
|
|
756
1043
|
* Resolves the session name from options
|
|
@@ -768,14 +1055,16 @@ var ChecklyReporter = class {
|
|
|
768
1055
|
}
|
|
769
1056
|
/**
|
|
770
1057
|
* Checks if test result has a trace attachment and adds context-aware warning if missing
|
|
1058
|
+
* Also captures trace file path for later console message extraction
|
|
771
1059
|
* The warning type depends on the trace configuration and test result state
|
|
772
1060
|
*/
|
|
773
1061
|
checkTraceAttachment(test, result) {
|
|
774
|
-
const
|
|
775
|
-
const
|
|
1062
|
+
const key = `${test.id}:${result.retry}`;
|
|
1063
|
+
const traceAttachment = result.attachments?.find(
|
|
776
1064
|
(attachment) => attachment.name === "trace" || attachment.contentType === "application/zip"
|
|
777
1065
|
);
|
|
778
|
-
if (
|
|
1066
|
+
if (traceAttachment?.path) {
|
|
1067
|
+
this.tracePathsMap.set(key, traceAttachment.path);
|
|
779
1068
|
return;
|
|
780
1069
|
}
|
|
781
1070
|
const traceConfig = test.parent?.project()?.use?.trace;
|
|
@@ -841,9 +1130,9 @@ var ChecklyReporter = class {
|
|
|
841
1130
|
warningType = "trace-missing";
|
|
842
1131
|
message = `No trace found. Trace mode "${traceMode}" may not be generating traces for this result.`;
|
|
843
1132
|
}
|
|
844
|
-
const warnings = this.warningsMap.get(
|
|
1133
|
+
const warnings = this.warningsMap.get(key) || [];
|
|
845
1134
|
warnings.push({ type: warningType, message });
|
|
846
|
-
this.warningsMap.set(
|
|
1135
|
+
this.warningsMap.set(key, warnings);
|
|
847
1136
|
}
|
|
848
1137
|
/**
|
|
849
1138
|
* Called once before running tests
|
|
@@ -919,9 +1208,16 @@ var ChecklyReporter = class {
|
|
|
919
1208
|
* This is where we create the ZIP archive and upload results
|
|
920
1209
|
*/
|
|
921
1210
|
async onEnd() {
|
|
1211
|
+
this.log("onEnd started", {
|
|
1212
|
+
testCounts: this.testCounts,
|
|
1213
|
+
stepsMapSize: this.stepsMap.size,
|
|
1214
|
+
warningsMapSize: this.warningsMap.size,
|
|
1215
|
+
hasTestSession: !!this.testSession,
|
|
1216
|
+
testSessionId: this.testSession?.testSessionId
|
|
1217
|
+
});
|
|
922
1218
|
try {
|
|
923
1219
|
const jsonReportPath = this.options.jsonReportPath;
|
|
924
|
-
if (!
|
|
1220
|
+
if (!fs4.existsSync(jsonReportPath)) {
|
|
925
1221
|
console.error(`[Checkly Reporter] ERROR: JSON report not found at: ${jsonReportPath}`);
|
|
926
1222
|
console.error("[Checkly Reporter] Make sure to configure the json reporter before the checkly reporter:");
|
|
927
1223
|
console.error(
|
|
@@ -929,17 +1225,42 @@ var ChecklyReporter = class {
|
|
|
929
1225
|
);
|
|
930
1226
|
return;
|
|
931
1227
|
}
|
|
932
|
-
|
|
1228
|
+
this.log("Reading JSON report", { path: jsonReportPath });
|
|
1229
|
+
const reportContent = fs4.readFileSync(jsonReportPath, "utf-8");
|
|
933
1230
|
const report = JSON.parse(reportContent);
|
|
1231
|
+
this.log("JSON report parsed", {
|
|
1232
|
+
configVersion: report.config?.version,
|
|
1233
|
+
projectsCount: report.config?.projects?.length ?? 0,
|
|
1234
|
+
suitesCount: report.suites?.length ?? 0,
|
|
1235
|
+
rootDir: report.config?.rootDir
|
|
1236
|
+
});
|
|
1237
|
+
await this.extractDataFromTraces();
|
|
934
1238
|
this.injectDataIntoReport(report);
|
|
935
|
-
|
|
1239
|
+
this.log("Data injected into report", {
|
|
1240
|
+
projectsCountAfterReconstruction: report.config?.projects?.length ?? 0
|
|
1241
|
+
});
|
|
1242
|
+
fs4.writeFileSync(jsonReportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
1243
|
+
this.log("Enriched report written to disk");
|
|
1244
|
+
this.log("Collecting assets", { testResultsDir: this.options.testResultsDir });
|
|
936
1245
|
const assets = await this.assetCollector.collectAssets(report);
|
|
1246
|
+
this.log("Assets collected", {
|
|
1247
|
+
count: assets.length,
|
|
1248
|
+
assets: assets.map((a) => ({ source: a.sourcePath, archive: a.archivePath, type: a.type }))
|
|
1249
|
+
});
|
|
1250
|
+
this.log("Creating ZIP archive", { outputPath: this.options.outputPath });
|
|
937
1251
|
const result = await this.zipper.createZip(jsonReportPath, assets);
|
|
1252
|
+
this.log("ZIP created", {
|
|
1253
|
+
zipPath: result.zipPath,
|
|
1254
|
+
zipSize: result.size,
|
|
1255
|
+
entriesCount: result.entries.length,
|
|
1256
|
+
entries: result.entries.map((e) => ({ name: e.name, start: e.start, end: e.end }))
|
|
1257
|
+
});
|
|
938
1258
|
if (this.testResults && this.testSession) {
|
|
1259
|
+
this.log("Uploading results", { testSessionId: this.testSession.testSessionId });
|
|
939
1260
|
await this.uploadResults(report, result.zipPath, result.entries);
|
|
940
1261
|
if (!this.options.dryRun) {
|
|
941
1262
|
try {
|
|
942
|
-
|
|
1263
|
+
fs4.unlinkSync(result.zipPath);
|
|
943
1264
|
} catch (cleanupError) {
|
|
944
1265
|
console.warn(`[Checkly Reporter] Warning: Could not delete ZIP file: ${cleanupError}`);
|
|
945
1266
|
}
|
|
@@ -962,7 +1283,33 @@ var ChecklyReporter = class {
|
|
|
962
1283
|
console.log("\n======================================================");
|
|
963
1284
|
}
|
|
964
1285
|
/**
|
|
965
|
-
*
|
|
1286
|
+
* Extracts console messages and network requests from all captured traces
|
|
1287
|
+
* Called before injecting data into the report
|
|
1288
|
+
*/
|
|
1289
|
+
async extractDataFromTraces() {
|
|
1290
|
+
const extractionPromises = [];
|
|
1291
|
+
for (const [key, tracePath] of this.tracePathsMap.entries()) {
|
|
1292
|
+
extractionPromises.push(
|
|
1293
|
+
(async () => {
|
|
1294
|
+
const reader = new TraceReader(tracePath);
|
|
1295
|
+
if (!await reader.open()) return;
|
|
1296
|
+
const messages = await reader.extractEvents('"type":"console"', toConsoleMessage);
|
|
1297
|
+
if (messages.length > 0) {
|
|
1298
|
+
this.consoleMessagesMap.set(key, messages);
|
|
1299
|
+
}
|
|
1300
|
+
const networkRequests = await reader.extractEvents('"type":"resource-snapshot"', toNetworkRequest);
|
|
1301
|
+
if (networkRequests.length > 0) {
|
|
1302
|
+
this.networkRequestsMap.set(key, networkRequests);
|
|
1303
|
+
}
|
|
1304
|
+
})().catch((error) => {
|
|
1305
|
+
console.error(`[Checkly Reporter] Failed to extract data from trace: ${error}`);
|
|
1306
|
+
})
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
await Promise.all(extractionPromises);
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Injects captured steps, warnings, console messages, and network requests into the JSON report
|
|
966
1313
|
* Traverses the report structure and matches by test ID + retry
|
|
967
1314
|
*/
|
|
968
1315
|
injectDataIntoReport(report) {
|
|
@@ -976,8 +1323,15 @@ var ChecklyReporter = class {
|
|
|
976
1323
|
result.steps = steps;
|
|
977
1324
|
}
|
|
978
1325
|
const warnings = this.warningsMap.get(key);
|
|
979
|
-
|
|
980
|
-
|
|
1326
|
+
const consoleMessages = this.consoleMessagesMap.get(key);
|
|
1327
|
+
const networkRequests = this.networkRequestsMap.get(key);
|
|
1328
|
+
const hasData = warnings && warnings.length > 0 || consoleMessages && consoleMessages.length > 0 || networkRequests && networkRequests.length > 0;
|
|
1329
|
+
if (hasData) {
|
|
1330
|
+
result._checkly = {
|
|
1331
|
+
...warnings && warnings.length > 0 ? { warnings } : {},
|
|
1332
|
+
...consoleMessages && consoleMessages.length > 0 ? { console: consoleMessages } : {},
|
|
1333
|
+
...networkRequests && networkRequests.length > 0 ? { network: networkRequests } : {}
|
|
1334
|
+
};
|
|
981
1335
|
}
|
|
982
1336
|
}
|
|
983
1337
|
}
|
|
@@ -1000,6 +1354,13 @@ var ChecklyReporter = class {
|
|
|
1000
1354
|
*/
|
|
1001
1355
|
reconstructProjectsFromTests(report) {
|
|
1002
1356
|
const projectNames = /* @__PURE__ */ new Set();
|
|
1357
|
+
let testsWithMissingProjectId = 0;
|
|
1358
|
+
const configAny = report.config;
|
|
1359
|
+
const originalProjectsCount = configAny.projects?.length ?? 0;
|
|
1360
|
+
this.log("reconstructProjectsFromTests started", {
|
|
1361
|
+
originalProjectsCount,
|
|
1362
|
+
suitesCount: report.suites?.length ?? 0
|
|
1363
|
+
});
|
|
1003
1364
|
const collectProjectNames = (suite) => {
|
|
1004
1365
|
for (const spec of suite.specs) {
|
|
1005
1366
|
for (const test of spec.tests) {
|
|
@@ -1009,6 +1370,7 @@ var ChecklyReporter = class {
|
|
|
1009
1370
|
}
|
|
1010
1371
|
if (testAny.projectName && !testAny.projectId) {
|
|
1011
1372
|
testAny.projectId = testAny.projectName;
|
|
1373
|
+
testsWithMissingProjectId++;
|
|
1012
1374
|
}
|
|
1013
1375
|
}
|
|
1014
1376
|
}
|
|
@@ -1021,19 +1383,37 @@ var ChecklyReporter = class {
|
|
|
1021
1383
|
for (const suite of report.suites) {
|
|
1022
1384
|
collectProjectNames(suite);
|
|
1023
1385
|
}
|
|
1024
|
-
|
|
1386
|
+
this.log("Project names collected from tests", {
|
|
1387
|
+
uniqueProjectNames: Array.from(projectNames),
|
|
1388
|
+
testsWithMissingProjectId
|
|
1389
|
+
});
|
|
1025
1390
|
if ((!configAny.projects || configAny.projects.length === 0) && projectNames.size > 0) {
|
|
1026
1391
|
configAny.projects = Array.from(projectNames).map((name) => ({
|
|
1027
1392
|
id: name,
|
|
1028
1393
|
name
|
|
1029
1394
|
}));
|
|
1395
|
+
this.log("Reconstructed config.projects", {
|
|
1396
|
+
reconstructedProjectsCount: configAny.projects.length,
|
|
1397
|
+
projects: configAny.projects
|
|
1398
|
+
});
|
|
1399
|
+
} else {
|
|
1400
|
+
this.log("No project reconstruction needed", {
|
|
1401
|
+
reason: configAny.projects?.length > 0 ? "projects already present" : "no project names found in tests"
|
|
1402
|
+
});
|
|
1030
1403
|
}
|
|
1031
1404
|
}
|
|
1032
1405
|
/**
|
|
1033
1406
|
* Uploads test results to Checkly API
|
|
1034
1407
|
*/
|
|
1035
|
-
async uploadResults(
|
|
1408
|
+
async uploadResults(_report, zipPath, entries) {
|
|
1409
|
+
this.log("uploadResults started", {
|
|
1410
|
+
zipPath,
|
|
1411
|
+
entriesCount: entries.length,
|
|
1412
|
+
hasTestResults: !!this.testResults,
|
|
1413
|
+
hasTestSession: !!this.testSession
|
|
1414
|
+
});
|
|
1036
1415
|
if (!this.testResults || !this.testSession) {
|
|
1416
|
+
this.log("uploadResults skipped - missing testResults or testSession");
|
|
1037
1417
|
return;
|
|
1038
1418
|
}
|
|
1039
1419
|
try {
|
|
@@ -1042,25 +1422,42 @@ var ChecklyReporter = class {
|
|
|
1042
1422
|
const isDegraded = failedCount === 0 && flakyCount > 0;
|
|
1043
1423
|
const endTime = /* @__PURE__ */ new Date();
|
|
1044
1424
|
const responseTime = this.startTime ? Math.max(0, endTime.getTime() - this.startTime.getTime()) : 0;
|
|
1045
|
-
const zipSizeBytes = (await
|
|
1425
|
+
const zipSizeBytes = (await fs4.promises.stat(zipPath)).size;
|
|
1426
|
+
this.log("Upload metadata calculated", {
|
|
1427
|
+
testCounts: this.testCounts,
|
|
1428
|
+
overallStatus,
|
|
1429
|
+
isDegraded,
|
|
1430
|
+
responseTime,
|
|
1431
|
+
zipSizeBytes,
|
|
1432
|
+
testSessionId: this.testSession.testSessionId,
|
|
1433
|
+
testResultsCount: this.testSession.testResults.length
|
|
1434
|
+
});
|
|
1046
1435
|
if (this.testSession.testResults.length > 0) {
|
|
1047
1436
|
const firstResult = this.testSession.testResults[0];
|
|
1437
|
+
this.log("Using first test result for upload", {
|
|
1438
|
+
testResultId: firstResult.testResultId
|
|
1439
|
+
});
|
|
1048
1440
|
let assetId;
|
|
1049
1441
|
if (zipSizeBytes > 0) {
|
|
1442
|
+
this.log("Starting S3 asset upload", { zipSizeBytes });
|
|
1050
1443
|
try {
|
|
1051
|
-
const assets =
|
|
1444
|
+
const assets = fs4.createReadStream(zipPath);
|
|
1052
1445
|
const uploadResponse = await this.testResults.uploadTestResultAsset(
|
|
1053
1446
|
this.testSession.testSessionId,
|
|
1054
1447
|
firstResult.testResultId,
|
|
1055
1448
|
assets
|
|
1056
1449
|
);
|
|
1057
1450
|
assetId = uploadResponse.assetId;
|
|
1451
|
+
this.log("S3 asset upload completed", { assetId });
|
|
1058
1452
|
} catch (error) {
|
|
1059
1453
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1060
1454
|
console.error("[Checkly Reporter] Asset upload failed:", errorMessage);
|
|
1455
|
+
this.log("S3 asset upload failed", { error: errorMessage });
|
|
1061
1456
|
}
|
|
1457
|
+
} else {
|
|
1458
|
+
this.log("Skipping S3 upload - ZIP is empty");
|
|
1062
1459
|
}
|
|
1063
|
-
|
|
1460
|
+
const updatePayload = {
|
|
1064
1461
|
status: overallStatus,
|
|
1065
1462
|
assetEntries: assetId ? entries : void 0,
|
|
1066
1463
|
isDegraded,
|
|
@@ -1072,11 +1469,21 @@ var ChecklyReporter = class {
|
|
|
1072
1469
|
s3PostTotalBytes: zipSizeBytes
|
|
1073
1470
|
}
|
|
1074
1471
|
}
|
|
1472
|
+
};
|
|
1473
|
+
this.log("Updating test result", {
|
|
1474
|
+
testResultId: firstResult.testResultId,
|
|
1475
|
+
payload: updatePayload,
|
|
1476
|
+
assetEntriesCount: assetId ? entries.length : 0
|
|
1075
1477
|
});
|
|
1478
|
+
await this.testResults.updateTestResult(this.testSession.testSessionId, firstResult.testResultId, updatePayload);
|
|
1479
|
+
this.log("Test result updated successfully");
|
|
1480
|
+
} else {
|
|
1481
|
+
this.log("No test results in session to update");
|
|
1076
1482
|
}
|
|
1077
1483
|
} catch (error) {
|
|
1078
1484
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1079
1485
|
console.error("[Checkly Reporter] Failed to upload results:", errorMessage);
|
|
1486
|
+
this.log("uploadResults failed", { error: errorMessage });
|
|
1080
1487
|
}
|
|
1081
1488
|
}
|
|
1082
1489
|
/**
|
package/package.json
CHANGED