@checkly/playwright-reporter 0.1.8 → 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 +97 -2
- package/dist/index.js +470 -26
- 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,10 +280,21 @@ 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;
|
|
292
|
+
/**
|
|
293
|
+
* Reconstructs config.projects and test.projectId from test data
|
|
294
|
+
* This is necessary for blob merge scenarios where Playwright's JSON reporter
|
|
295
|
+
* doesn't populate projects array or projectId fields
|
|
296
|
+
*/
|
|
297
|
+
private reconstructProjectsFromTests;
|
|
203
298
|
/**
|
|
204
299
|
* Uploads test results to Checkly API
|
|
205
300
|
*/
|
|
@@ -210,4 +305,4 @@ declare class ChecklyReporter implements Reporter {
|
|
|
210
305
|
onError(error: TestError): void;
|
|
211
306
|
}
|
|
212
307
|
|
|
213
|
-
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
|
}
|
|
@@ -991,12 +1345,75 @@ var ChecklyReporter = class {
|
|
|
991
1345
|
for (const suite of report.suites) {
|
|
992
1346
|
processSuite(suite);
|
|
993
1347
|
}
|
|
1348
|
+
this.reconstructProjectsFromTests(report);
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Reconstructs config.projects and test.projectId from test data
|
|
1352
|
+
* This is necessary for blob merge scenarios where Playwright's JSON reporter
|
|
1353
|
+
* doesn't populate projects array or projectId fields
|
|
1354
|
+
*/
|
|
1355
|
+
reconstructProjectsFromTests(report) {
|
|
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
|
+
});
|
|
1364
|
+
const collectProjectNames = (suite) => {
|
|
1365
|
+
for (const spec of suite.specs) {
|
|
1366
|
+
for (const test of spec.tests) {
|
|
1367
|
+
const testAny = test;
|
|
1368
|
+
if (testAny.projectName) {
|
|
1369
|
+
projectNames.add(testAny.projectName);
|
|
1370
|
+
}
|
|
1371
|
+
if (testAny.projectName && !testAny.projectId) {
|
|
1372
|
+
testAny.projectId = testAny.projectName;
|
|
1373
|
+
testsWithMissingProjectId++;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (suite.suites) {
|
|
1378
|
+
for (const nestedSuite of suite.suites) {
|
|
1379
|
+
collectProjectNames(nestedSuite);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1383
|
+
for (const suite of report.suites) {
|
|
1384
|
+
collectProjectNames(suite);
|
|
1385
|
+
}
|
|
1386
|
+
this.log("Project names collected from tests", {
|
|
1387
|
+
uniqueProjectNames: Array.from(projectNames),
|
|
1388
|
+
testsWithMissingProjectId
|
|
1389
|
+
});
|
|
1390
|
+
if ((!configAny.projects || configAny.projects.length === 0) && projectNames.size > 0) {
|
|
1391
|
+
configAny.projects = Array.from(projectNames).map((name) => ({
|
|
1392
|
+
id: name,
|
|
1393
|
+
name
|
|
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
|
+
});
|
|
1403
|
+
}
|
|
994
1404
|
}
|
|
995
1405
|
/**
|
|
996
1406
|
* Uploads test results to Checkly API
|
|
997
1407
|
*/
|
|
998
|
-
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
|
+
});
|
|
999
1415
|
if (!this.testResults || !this.testSession) {
|
|
1416
|
+
this.log("uploadResults skipped - missing testResults or testSession");
|
|
1000
1417
|
return;
|
|
1001
1418
|
}
|
|
1002
1419
|
try {
|
|
@@ -1005,25 +1422,42 @@ var ChecklyReporter = class {
|
|
|
1005
1422
|
const isDegraded = failedCount === 0 && flakyCount > 0;
|
|
1006
1423
|
const endTime = /* @__PURE__ */ new Date();
|
|
1007
1424
|
const responseTime = this.startTime ? Math.max(0, endTime.getTime() - this.startTime.getTime()) : 0;
|
|
1008
|
-
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
|
+
});
|
|
1009
1435
|
if (this.testSession.testResults.length > 0) {
|
|
1010
1436
|
const firstResult = this.testSession.testResults[0];
|
|
1437
|
+
this.log("Using first test result for upload", {
|
|
1438
|
+
testResultId: firstResult.testResultId
|
|
1439
|
+
});
|
|
1011
1440
|
let assetId;
|
|
1012
1441
|
if (zipSizeBytes > 0) {
|
|
1442
|
+
this.log("Starting S3 asset upload", { zipSizeBytes });
|
|
1013
1443
|
try {
|
|
1014
|
-
const assets =
|
|
1444
|
+
const assets = fs4.createReadStream(zipPath);
|
|
1015
1445
|
const uploadResponse = await this.testResults.uploadTestResultAsset(
|
|
1016
1446
|
this.testSession.testSessionId,
|
|
1017
1447
|
firstResult.testResultId,
|
|
1018
1448
|
assets
|
|
1019
1449
|
);
|
|
1020
1450
|
assetId = uploadResponse.assetId;
|
|
1451
|
+
this.log("S3 asset upload completed", { assetId });
|
|
1021
1452
|
} catch (error) {
|
|
1022
1453
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1023
1454
|
console.error("[Checkly Reporter] Asset upload failed:", errorMessage);
|
|
1455
|
+
this.log("S3 asset upload failed", { error: errorMessage });
|
|
1024
1456
|
}
|
|
1457
|
+
} else {
|
|
1458
|
+
this.log("Skipping S3 upload - ZIP is empty");
|
|
1025
1459
|
}
|
|
1026
|
-
|
|
1460
|
+
const updatePayload = {
|
|
1027
1461
|
status: overallStatus,
|
|
1028
1462
|
assetEntries: assetId ? entries : void 0,
|
|
1029
1463
|
isDegraded,
|
|
@@ -1035,11 +1469,21 @@ var ChecklyReporter = class {
|
|
|
1035
1469
|
s3PostTotalBytes: zipSizeBytes
|
|
1036
1470
|
}
|
|
1037
1471
|
}
|
|
1472
|
+
};
|
|
1473
|
+
this.log("Updating test result", {
|
|
1474
|
+
testResultId: firstResult.testResultId,
|
|
1475
|
+
payload: updatePayload,
|
|
1476
|
+
assetEntriesCount: assetId ? entries.length : 0
|
|
1038
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");
|
|
1039
1482
|
}
|
|
1040
1483
|
} catch (error) {
|
|
1041
1484
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1042
1485
|
console.error("[Checkly Reporter] Failed to upload results:", errorMessage);
|
|
1486
|
+
this.log("uploadResults failed", { error: errorMessage });
|
|
1043
1487
|
}
|
|
1044
1488
|
}
|
|
1045
1489
|
/**
|
package/package.json
CHANGED