@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.
@@ -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 attachment_utils_1 = require("./attachment-utils"); // Use relative path
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.shardIndex === undefined || this.shardIndex === 0) {
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(); // project() can return undefined if not in a project context
115
- const projectConfig = project === null || project === void 0 ? void 0 : project.use; // This is where options like userAgent, defaultBrowserType are
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
- : ""; // Major version
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
- : ""; // Major version
128
- const deviceType = result.device.type; // "mobile", "tablet", etc.
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, e.g. "Samsung Browser" is specific enough
145
+ // Keep it as is
148
146
  }
149
147
  else {
150
- browserName = "Android Browser"; // default for android if not specific
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: stepTitle,
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, testIdForFiles, browserDetails, test);
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
- if (result.stdout && result.stdout.length > 0) {
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
- // First, check for the special case where a test is not assigned a worker (e.g., global setup failure).
252
- if (result.workerIndex === -1) {
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: uniqueTestId,
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: undefined,
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
- try {
299
- (0, attachment_utils_1.attachFiles)(testIdForFiles, result, pulseResult, this.options);
300
- }
301
- catch (attachError) {
302
- console.error(`Pulse Reporter: Error processing attachments for test ${pulseResult.name} (ID: ${testIdForFiles}): ${attachError.message}`);
303
- }
304
- const existingTestIndex = this.results.findIndex((r) => r.id === uniqueTestId);
305
- if (existingTestIndex !== -1) {
306
- if (pulseResult.retries >= this.results[existingTestIndex].retries) {
307
- this.results[existingTestIndex] = pulseResult;
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
- else {
311
- this.results.push(pulseResult);
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", // Handle cases with no CPU info
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`, // Total RAM in 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
- let finalUniqueResultsMap = new Map();
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`; // Need not to change
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
- totalTests: 0,
435
- passed: 0,
436
- failed: 0,
437
- skipped: 0,
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
- let finalReport = undefined; // Initialize as undefined
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: properlyTypedResults,
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
- const errorSummary = `
474
- PlaywrightPulseReporter: Run Finished
475
- -----------------------------------------
476
- Overall Status: ERROR (Report data missing)
477
- Total Tests: N/A
478
- Passed: N/A
479
- Failed: N/A
480
- Skipped: N/A
481
- Duration: N/A
482
- -----------------------------------------`;
483
- if (this.printsToStdio()) {
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(finalOutputPathOnError, JSON.stringify(errorReport, null, 2));
506
- console.warn(`PlaywrightPulseReporter: Wrote an error report to ${finalOutputPathOnError} as finalReport was missing.`);
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 (writeError) {
509
- console.error(`PlaywrightPulseReporter: Failed to write error report: ${writeError.message}`);
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
- const reportRunData = finalReport.run;
514
- const finalRunStatus = ((_a = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.failed) !== null && _a !== void 0 ? _a : 0) > 0
515
- ? "failed"
516
- : ((_b = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.totalTests) !== null && _b !== void 0 ? _b : 0) === 0 && result.status !== "passed"
517
- ? result.status === "interrupted"
518
- ? "interrupted"
519
- : "no tests or error"
520
- : "passed";
521
- const summary = `
522
- PlaywrightPulseReporter: Run Finished
523
- -----------------------------------------
524
- Overall Status: ${finalRunStatus.toUpperCase()}
525
- Total Tests: ${(reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.totalTests) || 0}
526
- Passed: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.passed}
527
- Failed: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.failed}
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: JSON report written to ${finalOutputPath}`);
568
+ console.log(`PlaywrightPulseReporter: Merged report with ${finalMergedResults.length} total results saved to ${finalOutputPath}`);
546
569
  }
547
570
  }
548
- catch (error) {
549
- console.error(`Pulse Reporter: Failed to write final JSON report to ${finalOutputPath}. Error: ${error.message}`);
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
  }
@@ -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.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",