@arghajit/playwright-pulse-report 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -54
- package/dist/reporter/attachment-utils.js +41 -33
- package/dist/reporter/playwright-pulse-reporter.d.ts +3 -0
- package/dist/reporter/playwright-pulse-reporter.js +190 -172
- package/dist/types/index.d.ts +7 -1
- package/package.json +8 -3
- package/scripts/generate-report.mjs +222 -158
- package/scripts/generate-static-report.mjs +324 -374
- package/scripts/generate-trend.mjs +1 -1
- package/scripts/sendReport.mjs +5 -5
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// input_file_0.ts
|
|
3
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
3
|
if (k2 === undefined) k2 = k;
|
|
5
4
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -38,8 +37,7 @@ exports.PlaywrightPulseReporter = void 0;
|
|
|
38
37
|
const fs = __importStar(require("fs/promises"));
|
|
39
38
|
const path = __importStar(require("path"));
|
|
40
39
|
const crypto_1 = require("crypto");
|
|
41
|
-
const
|
|
42
|
-
const ua_parser_js_1 = require("ua-parser-js"); // Added UAParser import
|
|
40
|
+
const ua_parser_js_1 = require("ua-parser-js");
|
|
43
41
|
const os = __importStar(require("os"));
|
|
44
42
|
const convertStatus = (status, testCase) => {
|
|
45
43
|
if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
|
|
@@ -62,9 +60,10 @@ const convertStatus = (status, testCase) => {
|
|
|
62
60
|
};
|
|
63
61
|
const TEMP_SHARD_FILE_PREFIX = ".pulse-shard-results-";
|
|
64
62
|
const ATTACHMENTS_SUBDIR = "attachments";
|
|
63
|
+
const INDIVIDUAL_REPORTS_SUBDIR = "pulse-results";
|
|
65
64
|
class PlaywrightPulseReporter {
|
|
66
65
|
constructor(options = {}) {
|
|
67
|
-
var _a, _b;
|
|
66
|
+
var _a, _b, _c;
|
|
68
67
|
this.results = [];
|
|
69
68
|
this.baseOutputFile = "playwright-pulse-report.json";
|
|
70
69
|
this.isSharded = false;
|
|
@@ -73,6 +72,7 @@ class PlaywrightPulseReporter {
|
|
|
73
72
|
this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
|
|
74
73
|
this.outputDir = (_b = options.outputDir) !== null && _b !== void 0 ? _b : "pulse-report";
|
|
75
74
|
this.attachmentsDir = path.join(this.outputDir, ATTACHMENTS_SUBDIR);
|
|
75
|
+
this.resetOnEachRun = (_c = options.resetOnEachRun) !== null && _c !== void 0 ? _c : true;
|
|
76
76
|
}
|
|
77
77
|
printsToStdio() {
|
|
78
78
|
return this.shardIndex === undefined || this.shardIndex === 0;
|
|
@@ -96,7 +96,7 @@ class PlaywrightPulseReporter {
|
|
|
96
96
|
: undefined;
|
|
97
97
|
this._ensureDirExists(this.outputDir)
|
|
98
98
|
.then(() => {
|
|
99
|
-
if (this.
|
|
99
|
+
if (this.printsToStdio()) {
|
|
100
100
|
console.log(`PlaywrightPulseReporter: Starting test run with ${suite.allTests().length} tests${this.isSharded ? ` across ${totalShards} shards` : ""}. Pulse outputting to ${this.outputDir}`);
|
|
101
101
|
if (this.shardIndex === undefined ||
|
|
102
102
|
(this.isSharded && this.shardIndex === 0)) {
|
|
@@ -111,8 +111,8 @@ class PlaywrightPulseReporter {
|
|
|
111
111
|
}
|
|
112
112
|
getBrowserDetails(test) {
|
|
113
113
|
var _a, _b, _c, _d;
|
|
114
|
-
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
|
|
115
|
-
const projectConfig = project === null || project === void 0 ? void 0 : project.use;
|
|
114
|
+
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
|
|
115
|
+
const projectConfig = project === null || project === void 0 ? void 0 : project.use;
|
|
116
116
|
const userAgent = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.userAgent;
|
|
117
117
|
const configuredBrowserType = (_b = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.browserName) === null || _b === void 0 ? void 0 : _b.toLowerCase();
|
|
118
118
|
const parser = new ua_parser_js_1.UAParser(userAgent);
|
|
@@ -120,20 +120,18 @@ class PlaywrightPulseReporter {
|
|
|
120
120
|
let browserName = result.browser.name;
|
|
121
121
|
const browserVersion = result.browser.version
|
|
122
122
|
? ` v${result.browser.version.split(".")[0]}`
|
|
123
|
-
: "";
|
|
123
|
+
: "";
|
|
124
124
|
const osName = result.os.name ? ` on ${result.os.name}` : "";
|
|
125
125
|
const osVersion = result.os.version
|
|
126
126
|
? ` ${result.os.version.split(".")[0]}`
|
|
127
|
-
: "";
|
|
128
|
-
const deviceType = result.device.type;
|
|
127
|
+
: "";
|
|
128
|
+
const deviceType = result.device.type;
|
|
129
129
|
let finalString;
|
|
130
|
-
// If UAParser couldn't determine browser name, fallback to configured type
|
|
131
130
|
if (browserName === undefined) {
|
|
132
131
|
browserName = configuredBrowserType;
|
|
133
132
|
finalString = `${browserName}`;
|
|
134
133
|
}
|
|
135
134
|
else {
|
|
136
|
-
// Specific refinements for mobile based on parsed OS and device type
|
|
137
135
|
if (deviceType === "mobile" || deviceType === "tablet") {
|
|
138
136
|
if ((_c = result.os.name) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes("android")) {
|
|
139
137
|
if (browserName.toLowerCase().includes("chrome"))
|
|
@@ -144,10 +142,10 @@ class PlaywrightPulseReporter {
|
|
|
144
142
|
browserName = "Android WebView";
|
|
145
143
|
else if (browserName &&
|
|
146
144
|
!browserName.toLowerCase().includes("mobile")) {
|
|
147
|
-
// Keep it as is
|
|
145
|
+
// Keep it as is
|
|
148
146
|
}
|
|
149
147
|
else {
|
|
150
|
-
browserName = "Android Browser";
|
|
148
|
+
browserName = "Android Browser";
|
|
151
149
|
}
|
|
152
150
|
}
|
|
153
151
|
else if ((_d = result.os.name) === null || _d === void 0 ? void 0 : _d.toLowerCase().includes("ios")) {
|
|
@@ -178,10 +176,9 @@ class PlaywrightPulseReporter {
|
|
|
178
176
|
if (step.location) {
|
|
179
177
|
codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
|
|
180
178
|
}
|
|
181
|
-
let stepTitle = step.title;
|
|
182
179
|
return {
|
|
183
180
|
id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}`,
|
|
184
|
-
title:
|
|
181
|
+
title: step.title,
|
|
185
182
|
status: stepStatus,
|
|
186
183
|
duration: duration,
|
|
187
184
|
startTime: startTime,
|
|
@@ -200,21 +197,16 @@ class PlaywrightPulseReporter {
|
|
|
200
197
|
};
|
|
201
198
|
}
|
|
202
199
|
async onTestEnd(test, result) {
|
|
203
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
200
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
204
201
|
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
|
|
205
202
|
const browserDetails = this.getBrowserDetails(test);
|
|
206
203
|
const testStatus = convertStatus(result.status, test);
|
|
207
204
|
const startTime = new Date(result.startTime);
|
|
208
205
|
const endTime = new Date(startTime.getTime() + result.duration);
|
|
209
|
-
const testIdForFiles = test.id ||
|
|
210
|
-
`${test
|
|
211
|
-
.titlePath()
|
|
212
|
-
.join("_")
|
|
213
|
-
.replace(/[^a-zA-Z0-9]/g, "_")}_${startTime.getTime()}`;
|
|
214
206
|
const processAllSteps = async (steps) => {
|
|
215
207
|
let processed = [];
|
|
216
208
|
for (const step of steps) {
|
|
217
|
-
const processedStep = await this.processStep(step,
|
|
209
|
+
const processedStep = await this.processStep(step, test.id, browserDetails, test);
|
|
218
210
|
processed.push(processedStep);
|
|
219
211
|
if (step.steps && step.steps.length > 0) {
|
|
220
212
|
processedStep.steps = await processAllSteps(step.steps);
|
|
@@ -232,39 +224,14 @@ class PlaywrightPulseReporter {
|
|
|
232
224
|
catch (e) {
|
|
233
225
|
console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
|
|
234
226
|
}
|
|
235
|
-
const stdoutMessages =
|
|
236
|
-
|
|
237
|
-
result.stdout.forEach((item) => {
|
|
238
|
-
stdoutMessages.push(typeof item === "string" ? item : item.toString());
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
const stderrMessages = [];
|
|
242
|
-
if (result.stderr && result.stderr.length > 0) {
|
|
243
|
-
result.stderr.forEach((item) => {
|
|
244
|
-
stderrMessages.push(typeof item === "string" ? item : item.toString());
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
const uniqueTestId = test.id;
|
|
248
|
-
// --- REFINED THIS SECTION for testData ---
|
|
227
|
+
const stdoutMessages = result.stdout.map((item) => typeof item === "string" ? item : item.toString());
|
|
228
|
+
const stderrMessages = result.stderr.map((item) => typeof item === "string" ? item : item.toString());
|
|
249
229
|
const maxWorkers = this.config.workers;
|
|
250
|
-
let mappedWorkerId
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
mappedWorkerId = -1; // Keep it as -1 to clearly identify this special case.
|
|
254
|
-
}
|
|
255
|
-
else if (maxWorkers && maxWorkers > 0) {
|
|
256
|
-
// If there's a valid worker, map it to the concurrency slot...
|
|
257
|
-
const zeroBasedId = result.workerIndex % maxWorkers;
|
|
258
|
-
// ...and then shift it to be 1-based (1 to n).
|
|
259
|
-
mappedWorkerId = zeroBasedId + 1;
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
// Fallback for when maxWorkers is not defined: just use the original index (and shift to 1-based).
|
|
263
|
-
mappedWorkerId = result.workerIndex + 1;
|
|
264
|
-
}
|
|
230
|
+
let mappedWorkerId = result.workerIndex === -1
|
|
231
|
+
? -1
|
|
232
|
+
: (result.workerIndex % (maxWorkers > 0 ? maxWorkers : 1)) + 1;
|
|
265
233
|
const testSpecificData = {
|
|
266
234
|
workerId: mappedWorkerId,
|
|
267
|
-
uniqueWorkerIndex: result.workerIndex, // We'll keep the original for diagnostics
|
|
268
235
|
totalWorkers: maxWorkers,
|
|
269
236
|
configFile: this.config.configFile,
|
|
270
237
|
metadata: this.config.metadata
|
|
@@ -272,7 +239,7 @@ class PlaywrightPulseReporter {
|
|
|
272
239
|
: undefined,
|
|
273
240
|
};
|
|
274
241
|
const pulseResult = {
|
|
275
|
-
id:
|
|
242
|
+
id: test.id,
|
|
276
243
|
runId: "TBD",
|
|
277
244
|
name: test.titlePath().join(" > "),
|
|
278
245
|
suiteName: (project === null || project === void 0 ? void 0 : project.name) || ((_e = this.config.projects[0]) === null || _e === void 0 ? void 0 : _e.name) || "Default Suite",
|
|
@@ -288,28 +255,59 @@ class PlaywrightPulseReporter {
|
|
|
288
255
|
codeSnippet: codeSnippet,
|
|
289
256
|
tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
|
|
290
257
|
screenshots: [],
|
|
291
|
-
videoPath:
|
|
258
|
+
videoPath: [],
|
|
292
259
|
tracePath: undefined,
|
|
260
|
+
attachments: [],
|
|
293
261
|
stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
|
|
294
262
|
stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
|
|
295
|
-
// --- UPDATED THESE LINES from testSpecificData ---
|
|
296
263
|
...testSpecificData,
|
|
297
264
|
};
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
265
|
+
for (const [index, attachment] of result.attachments.entries()) {
|
|
266
|
+
if (!attachment.path)
|
|
267
|
+
continue;
|
|
268
|
+
try {
|
|
269
|
+
const testSubfolder = test.id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
270
|
+
const safeAttachmentName = path
|
|
271
|
+
.basename(attachment.path)
|
|
272
|
+
.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
273
|
+
const uniqueFileName = `${index}-${Date.now()}-${safeAttachmentName}`;
|
|
274
|
+
const relativeDestPath = path.join(ATTACHMENTS_SUBDIR, testSubfolder, uniqueFileName);
|
|
275
|
+
const absoluteDestPath = path.join(this.outputDir, relativeDestPath);
|
|
276
|
+
await this._ensureDirExists(path.dirname(absoluteDestPath));
|
|
277
|
+
await fs.copyFile(attachment.path, absoluteDestPath);
|
|
278
|
+
if (attachment.contentType.startsWith("image/")) {
|
|
279
|
+
(_j = pulseResult.screenshots) === null || _j === void 0 ? void 0 : _j.push(relativeDestPath);
|
|
280
|
+
}
|
|
281
|
+
else if (attachment.contentType.startsWith("video/")) {
|
|
282
|
+
(_k = pulseResult.videoPath) === null || _k === void 0 ? void 0 : _k.push(relativeDestPath);
|
|
283
|
+
}
|
|
284
|
+
else if (attachment.name === "trace") {
|
|
285
|
+
pulseResult.tracePath = relativeDestPath;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
(_l = pulseResult.attachments) === null || _l === void 0 ? void 0 : _l.push({
|
|
289
|
+
name: attachment.name,
|
|
290
|
+
path: relativeDestPath,
|
|
291
|
+
contentType: attachment.contentType,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
console.error(`Pulse Reporter: Failed to process attachment "${attachment.name}" for test ${pulseResult.name}. Error: ${err.message}`);
|
|
308
297
|
}
|
|
309
298
|
}
|
|
310
|
-
|
|
311
|
-
|
|
299
|
+
this.results.push(pulseResult);
|
|
300
|
+
}
|
|
301
|
+
_getFinalizedResults(allResults) {
|
|
302
|
+
const finalResultsMap = new Map();
|
|
303
|
+
for (const result of allResults) {
|
|
304
|
+
const existing = finalResultsMap.get(result.id);
|
|
305
|
+
// Keep the result with the highest retry attempt for each test ID
|
|
306
|
+
if (!existing || result.retries >= existing.retries) {
|
|
307
|
+
finalResultsMap.set(result.id, result);
|
|
308
|
+
}
|
|
312
309
|
}
|
|
310
|
+
return Array.from(finalResultsMap.values());
|
|
313
311
|
}
|
|
314
312
|
onError(error) {
|
|
315
313
|
var _a;
|
|
@@ -323,10 +321,10 @@ class PlaywrightPulseReporter {
|
|
|
323
321
|
host: os.hostname(),
|
|
324
322
|
os: `${os.platform()} ${os.release()}`,
|
|
325
323
|
cpu: {
|
|
326
|
-
model: os.cpus()[0] ? os.cpus()[0].model : "N/A",
|
|
324
|
+
model: os.cpus()[0] ? os.cpus()[0].model : "N/A",
|
|
327
325
|
cores: os.cpus().length,
|
|
328
326
|
},
|
|
329
|
-
memory: `${(os.totalmem() / 1024 ** 3).toFixed(2)}GB`,
|
|
327
|
+
memory: `${(os.totalmem() / 1024 ** 3).toFixed(2)}GB`,
|
|
330
328
|
node: process.version,
|
|
331
329
|
v8: process.versions.v8,
|
|
332
330
|
cwd: process.cwd(),
|
|
@@ -364,14 +362,7 @@ class PlaywrightPulseReporter {
|
|
|
364
362
|
}
|
|
365
363
|
}
|
|
366
364
|
}
|
|
367
|
-
|
|
368
|
-
for (const result of allShardProcessedResults) {
|
|
369
|
-
const existing = finalUniqueResultsMap.get(result.id);
|
|
370
|
-
if (!existing || result.retries >= existing.retries) {
|
|
371
|
-
finalUniqueResultsMap.set(result.id, result);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
const finalResultsList = Array.from(finalUniqueResultsMap.values());
|
|
365
|
+
const finalResultsList = this._getFinalizedResults(allShardProcessedResults);
|
|
375
366
|
finalResultsList.forEach((r) => (r.runId = finalRunData.id));
|
|
376
367
|
finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
|
|
377
368
|
finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
|
|
@@ -418,140 +409,167 @@ class PlaywrightPulseReporter {
|
|
|
418
409
|
}
|
|
419
410
|
}
|
|
420
411
|
async onEnd(result) {
|
|
421
|
-
var _a, _b, _c;
|
|
422
412
|
if (this.shardIndex !== undefined) {
|
|
423
413
|
await this._writeShardResults();
|
|
424
414
|
return;
|
|
425
415
|
}
|
|
416
|
+
// De-duplicate and handle retries here, in a safe, single-threaded context.
|
|
417
|
+
const finalResults = this._getFinalizedResults(this.results);
|
|
426
418
|
const runEndTime = Date.now();
|
|
427
419
|
const duration = runEndTime - this.runStartTime;
|
|
428
|
-
const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`;
|
|
429
|
-
// --- CALLING _getEnvDetails HERE ---
|
|
420
|
+
const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`;
|
|
430
421
|
const environmentDetails = this._getEnvDetails();
|
|
431
422
|
const runData = {
|
|
432
423
|
id: runId,
|
|
433
424
|
timestamp: new Date(this.runStartTime),
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
425
|
+
// Use the length of the de-duplicated array for all counts
|
|
426
|
+
totalTests: finalResults.length,
|
|
427
|
+
passed: finalResults.filter((r) => r.status === "passed").length,
|
|
428
|
+
failed: finalResults.filter((r) => r.status === "failed").length,
|
|
429
|
+
skipped: finalResults.filter((r) => r.status === "skipped").length,
|
|
438
430
|
duration,
|
|
439
|
-
// --- ADDED environmentDetails HERE ---
|
|
440
431
|
environment: environmentDetails,
|
|
441
432
|
};
|
|
442
|
-
|
|
433
|
+
finalResults.forEach((r) => (r.runId = runId));
|
|
434
|
+
let finalReport = undefined;
|
|
443
435
|
if (this.isSharded) {
|
|
436
|
+
// The _mergeShardResults method will handle its own de-duplication
|
|
444
437
|
finalReport = await this._mergeShardResults(runData);
|
|
445
|
-
// Ensured environment details are on the final merged runData if not already
|
|
446
|
-
if (finalReport && finalReport.run && !finalReport.run.environment) {
|
|
447
|
-
finalReport.run.environment = environmentDetails;
|
|
448
|
-
}
|
|
449
438
|
}
|
|
450
439
|
else {
|
|
451
|
-
this.results.forEach((r) => (r.runId = runId));
|
|
452
|
-
runData.passed = this.results.filter((r) => r.status === "passed").length;
|
|
453
|
-
runData.failed = this.results.filter((r) => r.status === "failed").length;
|
|
454
|
-
runData.skipped = this.results.filter((r) => r.status === "skipped").length;
|
|
455
|
-
runData.totalTests = this.results.length;
|
|
456
|
-
const reviveDates = (key, value) => {
|
|
457
|
-
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
458
|
-
if (typeof value === "string" && isoDateRegex.test(value)) {
|
|
459
|
-
const date = new Date(value);
|
|
460
|
-
return !isNaN(date.getTime()) ? date : value;
|
|
461
|
-
}
|
|
462
|
-
return value;
|
|
463
|
-
};
|
|
464
|
-
const properlyTypedResults = JSON.parse(JSON.stringify(this.results), reviveDates);
|
|
465
440
|
finalReport = {
|
|
466
441
|
run: runData,
|
|
467
|
-
results
|
|
442
|
+
// Use the de-duplicated results
|
|
443
|
+
results: finalResults,
|
|
468
444
|
metadata: { generatedAt: new Date().toISOString() },
|
|
469
445
|
};
|
|
470
446
|
}
|
|
471
447
|
if (!finalReport) {
|
|
472
448
|
console.error("PlaywrightPulseReporter: CRITICAL - finalReport object was not generated. Cannot create summary.");
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
console.log(errorSummary);
|
|
485
|
-
}
|
|
486
|
-
const errorReport = {
|
|
487
|
-
run: {
|
|
488
|
-
id: runId,
|
|
489
|
-
timestamp: new Date(this.runStartTime),
|
|
490
|
-
totalTests: 0,
|
|
491
|
-
passed: 0,
|
|
492
|
-
failed: 0,
|
|
493
|
-
skipped: 0,
|
|
494
|
-
duration: duration,
|
|
495
|
-
environment: environmentDetails,
|
|
496
|
-
},
|
|
497
|
-
results: [],
|
|
498
|
-
metadata: {
|
|
499
|
-
generatedAt: new Date().toISOString(),
|
|
500
|
-
},
|
|
501
|
-
};
|
|
502
|
-
const finalOutputPathOnError = path.join(this.outputDir, this.baseOutputFile);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const jsonReplacer = (key, value) => {
|
|
452
|
+
if (value instanceof Date)
|
|
453
|
+
return value.toISOString();
|
|
454
|
+
if (typeof value === "bigint")
|
|
455
|
+
return value.toString();
|
|
456
|
+
return value;
|
|
457
|
+
};
|
|
458
|
+
if (this.resetOnEachRun) {
|
|
459
|
+
const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
|
|
503
460
|
try {
|
|
504
461
|
await this._ensureDirExists(this.outputDir);
|
|
505
|
-
await fs.writeFile(
|
|
506
|
-
|
|
462
|
+
await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, jsonReplacer, 2));
|
|
463
|
+
if (this.printsToStdio()) {
|
|
464
|
+
console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
|
|
465
|
+
}
|
|
507
466
|
}
|
|
508
|
-
catch (
|
|
509
|
-
console.error(`
|
|
467
|
+
catch (error) {
|
|
468
|
+
console.error(`Pulse Reporter: Failed to write final JSON report to ${finalOutputPath}. Error: ${error.message}`);
|
|
510
469
|
}
|
|
511
|
-
return;
|
|
512
470
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
Skipped: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.skipped}
|
|
529
|
-
Duration: ${(((_c = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.duration) !== null && _c !== void 0 ? _c : 0) / 1000).toFixed(2)}s
|
|
530
|
-
-----------------------------------------`;
|
|
531
|
-
if (this.printsToStdio()) {
|
|
532
|
-
console.log(summary);
|
|
471
|
+
else {
|
|
472
|
+
// Logic for appending/merging reports
|
|
473
|
+
const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
|
|
474
|
+
const individualReportPath = path.join(pulseResultsDir, `playwright-pulse-report-${Date.now()}.json`);
|
|
475
|
+
try {
|
|
476
|
+
await this._ensureDirExists(pulseResultsDir);
|
|
477
|
+
await fs.writeFile(individualReportPath, JSON.stringify(finalReport, jsonReplacer, 2));
|
|
478
|
+
if (this.printsToStdio()) {
|
|
479
|
+
console.log(`PlaywrightPulseReporter: Individual run report for merging written to ${individualReportPath}`);
|
|
480
|
+
}
|
|
481
|
+
await this._mergeAllRunReports();
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
console.error(`Pulse Reporter: Failed to write or merge report. Error: ${error.message}`);
|
|
485
|
+
}
|
|
533
486
|
}
|
|
487
|
+
if (this.isSharded) {
|
|
488
|
+
await this._cleanupTemporaryFiles();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async _mergeAllRunReports() {
|
|
492
|
+
const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
|
|
534
493
|
const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
|
|
494
|
+
let reportFiles;
|
|
495
|
+
try {
|
|
496
|
+
const allFiles = await fs.readdir(pulseResultsDir);
|
|
497
|
+
reportFiles = allFiles.filter((file) => file.startsWith("playwright-pulse-report-") && file.endsWith(".json"));
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
if (error.code === "ENOENT") {
|
|
501
|
+
if (this.printsToStdio()) {
|
|
502
|
+
console.log(`Pulse Reporter: No individual reports directory found at ${pulseResultsDir}. Skipping merge.`);
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
console.error(`Pulse Reporter: Error reading report directory ${pulseResultsDir}:`, error);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (reportFiles.length === 0) {
|
|
510
|
+
if (this.printsToStdio()) {
|
|
511
|
+
console.log("Pulse Reporter: No matching JSON report files found to merge.");
|
|
512
|
+
}
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const allResultsFromAllFiles = [];
|
|
516
|
+
let latestTimestamp = new Date(0);
|
|
517
|
+
let lastRunEnvironment = undefined;
|
|
518
|
+
let totalDuration = 0;
|
|
519
|
+
for (const file of reportFiles) {
|
|
520
|
+
const filePath = path.join(pulseResultsDir, file);
|
|
521
|
+
try {
|
|
522
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
523
|
+
const json = JSON.parse(content);
|
|
524
|
+
if (json.run) {
|
|
525
|
+
const runTimestamp = new Date(json.run.timestamp);
|
|
526
|
+
if (runTimestamp > latestTimestamp) {
|
|
527
|
+
latestTimestamp = runTimestamp;
|
|
528
|
+
lastRunEnvironment = json.run.environment || undefined;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (json.results) {
|
|
532
|
+
allResultsFromAllFiles.push(...json.results);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
console.warn(`Pulse Reporter: Could not parse report file ${filePath}. Skipping. Error: ${err.message}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// De-duplicate the results from ALL merged files using the helper function
|
|
540
|
+
const finalMergedResults = this._getFinalizedResults(allResultsFromAllFiles);
|
|
541
|
+
// Sum the duration from the final, de-duplicated list of tests
|
|
542
|
+
totalDuration = finalMergedResults.reduce((acc, r) => acc + (r.duration || 0), 0);
|
|
543
|
+
const combinedRun = {
|
|
544
|
+
id: `merged-${Date.now()}`,
|
|
545
|
+
timestamp: latestTimestamp,
|
|
546
|
+
environment: lastRunEnvironment,
|
|
547
|
+
// Recalculate counts based on the truly final, de-duplicated list
|
|
548
|
+
totalTests: finalMergedResults.length,
|
|
549
|
+
passed: finalMergedResults.filter((r) => r.status === "passed").length,
|
|
550
|
+
failed: finalMergedResults.filter((r) => r.status === "failed").length,
|
|
551
|
+
skipped: finalMergedResults.filter((r) => r.status === "skipped").length,
|
|
552
|
+
duration: totalDuration,
|
|
553
|
+
};
|
|
554
|
+
const finalReport = {
|
|
555
|
+
run: combinedRun,
|
|
556
|
+
results: finalMergedResults, // Use the de-duplicated list
|
|
557
|
+
metadata: {
|
|
558
|
+
generatedAt: new Date().toISOString(),
|
|
559
|
+
},
|
|
560
|
+
};
|
|
535
561
|
try {
|
|
536
|
-
await this._ensureDirExists(this.outputDir);
|
|
537
562
|
await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, (key, value) => {
|
|
538
563
|
if (value instanceof Date)
|
|
539
564
|
return value.toISOString();
|
|
540
|
-
if (typeof value === "bigint")
|
|
541
|
-
return value.toString();
|
|
542
565
|
return value;
|
|
543
566
|
}, 2));
|
|
544
567
|
if (this.printsToStdio()) {
|
|
545
|
-
console.log(`PlaywrightPulseReporter:
|
|
568
|
+
console.log(`PlaywrightPulseReporter: ✅ Merged report with ${finalMergedResults.length} total results saved to ${finalOutputPath}`);
|
|
546
569
|
}
|
|
547
570
|
}
|
|
548
|
-
catch (
|
|
549
|
-
console.error(`Pulse Reporter: Failed to write final
|
|
550
|
-
}
|
|
551
|
-
finally {
|
|
552
|
-
if (this.isSharded) {
|
|
553
|
-
await this._cleanupTemporaryFiles();
|
|
554
|
-
}
|
|
571
|
+
catch (err) {
|
|
572
|
+
console.error(`Pulse Reporter: Failed to write final merged report to ${finalOutputPath}. Error: ${err.message}`);
|
|
555
573
|
}
|
|
556
574
|
}
|
|
557
575
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -32,8 +32,13 @@ export interface TestResult {
|
|
|
32
32
|
runId: string;
|
|
33
33
|
browser: string;
|
|
34
34
|
screenshots?: string[];
|
|
35
|
-
videoPath?: string;
|
|
35
|
+
videoPath?: string[];
|
|
36
36
|
tracePath?: string;
|
|
37
|
+
attachments?: {
|
|
38
|
+
name: string;
|
|
39
|
+
path: string;
|
|
40
|
+
contentType: string;
|
|
41
|
+
}[];
|
|
37
42
|
stdout?: string[];
|
|
38
43
|
stderr?: string[];
|
|
39
44
|
workerId?: number;
|
|
@@ -67,6 +72,7 @@ export interface PlaywrightPulseReporterOptions {
|
|
|
67
72
|
outputFile?: string;
|
|
68
73
|
outputDir?: string;
|
|
69
74
|
base64Images?: boolean;
|
|
75
|
+
resetOnEachRun?: boolean;
|
|
70
76
|
}
|
|
71
77
|
export interface EnvDetails {
|
|
72
78
|
host: string;
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arghajit/playwright-pulse-report",
|
|
3
3
|
"author": "Arghajit Singha",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.4",
|
|
5
5
|
"description": "A Playwright reporter and dashboard for visualizing test results.",
|
|
6
|
+
"homepage": "https://playwright-pulse-report.netlify.app/",
|
|
6
7
|
"keywords": [
|
|
7
8
|
"playwright",
|
|
8
9
|
"reporter",
|
|
@@ -11,10 +12,13 @@
|
|
|
11
12
|
"reporting",
|
|
12
13
|
"nextjs",
|
|
13
14
|
"playwright-pulse",
|
|
15
|
+
"playwright-pulse-report",
|
|
14
16
|
"report",
|
|
15
17
|
"email-report",
|
|
16
18
|
"send-report",
|
|
17
|
-
"email"
|
|
19
|
+
"email",
|
|
20
|
+
"playwright-report",
|
|
21
|
+
"pulse"
|
|
18
22
|
],
|
|
19
23
|
"main": "dist/reporter/index.js",
|
|
20
24
|
"types": "dist/reporter/index.d.ts",
|
|
@@ -46,7 +50,8 @@
|
|
|
46
50
|
"report:generate": "node ./scripts/generate-report.mjs",
|
|
47
51
|
"report:merge": "node ./scripts/merge-pulse-report.js",
|
|
48
52
|
"report:email": "node ./scripts/sendReport.mjs",
|
|
49
|
-
"report:minify": "node ./scripts/generate-email-report.mjs"
|
|
53
|
+
"report:minify": "node ./scripts/generate-email-report.mjs",
|
|
54
|
+
"generate-trend": "node ./scripts/generate-trend.mjs"
|
|
50
55
|
},
|
|
51
56
|
"dependencies": {
|
|
52
57
|
"archiver": "^7.0.1",
|