@arghajit/dummy 0.3.34 → 0.3.37

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.
@@ -4,6 +4,7 @@ export declare class PlaywrightPulseReporter implements Reporter {
4
4
  private config;
5
5
  private suite;
6
6
  private results;
7
+ private _pendingTestEnds;
7
8
  private runStartTime;
8
9
  private options;
9
10
  private outputDir;
@@ -21,12 +22,28 @@ export declare class PlaywrightPulseReporter implements Reporter {
21
22
  private getBrowserDetails;
22
23
  private processStep;
23
24
  onTestEnd(test: TestCase, result: PwTestResult): Promise<void>;
25
+ private _processTestEnd;
24
26
  private _getFinalizedResults;
25
27
  onError(error: any): void;
26
28
  private _getEnvDetails;
27
29
  private _writeShardResults;
28
30
  private _mergeShardResults;
29
31
  private _cleanupTemporaryFiles;
32
+ /**
33
+ * Removes all individual run JSON files from the `pulse-results/` directory
34
+ * that were left over from previous test sessions.
35
+ *
36
+ * When `resetOnEachRun: false`, each run writes its own timestamped JSON to
37
+ * `pulse-results/` and then `_mergeAllRunReports()` merges them all. However,
38
+ * if files from *older* sessions accumulate there (e.g. because a previous run
39
+ * was interrupted before the post-merge cleanup, or because the user ran tests
40
+ * on a previous day), `_getFinalizedResults()` de-duplicates by `test.id` and
41
+ * collapses results from both sessions into a single entry — producing a
42
+ * `totalTests` count lower than the actual number of tests that ran.
43
+ *
44
+ * Cleaning up at `onBegin` time guarantees each run starts with a fresh slate.
45
+ */
46
+ private _cleanupStaleRunReports;
30
47
  private _ensureDirExists;
31
48
  onEnd(result: FullResult): Promise<void>;
32
49
  private _mergeAllRunReports;
@@ -69,6 +69,7 @@ class PlaywrightPulseReporter {
69
69
  constructor(options = {}) {
70
70
  var _a, _b, _c;
71
71
  this.results = [];
72
+ this._pendingTestEnds = [];
72
73
  this.baseOutputFile = "playwright-pulse-report.json";
73
74
  this.isSharded = false;
74
75
  this.shardIndex = undefined;
@@ -99,14 +100,21 @@ class PlaywrightPulseReporter {
99
100
  ? this.config.shard.current - 1
100
101
  : undefined;
101
102
  this._ensureDirExists(this.outputDir)
102
- .then(() => {
103
+ .then(async () => {
103
104
  if (this.printsToStdio()) {
104
105
  console.log(`PlaywrightPulseReporter: Starting test run with ${suite.allTests().length} tests${this.isSharded ? ` across ${totalShards} shards` : ""}. Pulse outputting to ${this.outputDir}`);
105
106
  if (this.shardIndex === undefined ||
106
107
  (this.isSharded && this.shardIndex === 0)) {
107
- return this._cleanupTemporaryFiles();
108
+ await this._cleanupTemporaryFiles();
108
109
  }
109
110
  }
111
+ // When not resetting on each run, clear stale individual run files
112
+ // from previous sessions. Without this, _mergeAllRunReports() reads
113
+ // ALL accumulated files and de-duplicates by test.id, which collapses
114
+ // results from different sessions into fewer entries than expected.
115
+ if (!this.resetOnEachRun) {
116
+ await this._cleanupStaleRunReports();
117
+ }
110
118
  })
111
119
  .catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
112
120
  }
@@ -120,19 +128,19 @@ class PlaywrightPulseReporter {
120
128
  extractCodeSnippet(filePath, targetLine, targetColumn) {
121
129
  var _a;
122
130
  try {
123
- const fsSync = require('fs');
131
+ const fsSync = require("fs");
124
132
  if (!fsSync.existsSync(filePath)) {
125
- return '';
133
+ return "";
126
134
  }
127
- const content = fsSync.readFileSync(filePath, 'utf8');
128
- const lines = content.split('\n');
135
+ const content = fsSync.readFileSync(filePath, "utf8");
136
+ const lines = content.split("\n");
129
137
  if (targetLine < 1 || targetLine > lines.length) {
130
- return '';
138
+ return "";
131
139
  }
132
- return ((_a = lines[targetLine - 1]) === null || _a === void 0 ? void 0 : _a.trim()) || '';
140
+ return ((_a = lines[targetLine - 1]) === null || _a === void 0 ? void 0 : _a.trim()) || "";
133
141
  }
134
142
  catch (e) {
135
- return '';
143
+ return "";
136
144
  }
137
145
  }
138
146
  getBrowserDetails(test) {
@@ -199,7 +207,7 @@ class PlaywrightPulseReporter {
199
207
  const startTime = new Date(step.startTime);
200
208
  const endTime = new Date(startTime.getTime() + Math.max(0, duration));
201
209
  let codeLocation = "";
202
- let codeSnippet = '';
210
+ let codeSnippet = "";
203
211
  if (step.location) {
204
212
  codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
205
213
  codeSnippet = this.extractCodeSnippet(step.location.file, step.location.line, step.location.column);
@@ -226,9 +234,19 @@ class PlaywrightPulseReporter {
226
234
  };
227
235
  }
228
236
  async onTestEnd(test, result) {
237
+ // Track this async call so onEnd() can wait for all in-flight processing
238
+ // before it reads this.results. This prevents the race condition where
239
+ // onEnd() fires before an interrupted test's onTestEnd() has finished
240
+ // its async attachment I/O and pushed to this.results.
241
+ const p = this._processTestEnd(test, result);
242
+ this._pendingTestEnds.push(p);
243
+ await p;
244
+ }
245
+ async _processTestEnd(test, result) {
229
246
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
230
247
  const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
231
248
  const browserDetails = this.getBrowserDetails(test);
249
+ const uniqueTestId = `${(project === null || project === void 0 ? void 0 : project.name) || "default"}-${test.id}`;
232
250
  // Captured outcome from Playwright
233
251
  const outcome = test.outcome();
234
252
  // Calculate final status based on the last result (Last-Run-Wins)
@@ -239,15 +257,15 @@ class PlaywrightPulseReporter {
239
257
  // Existing behavior: fail if flaky (implied by user request "existing status field should remain failed")
240
258
  // If outcome is flaky, status should be 'failed' to indicate initial failure, but final_status is 'passed'
241
259
  let testStatus = finalStatus;
242
- if (outcome === 'flaky') {
243
- testStatus = 'flaky';
260
+ if (outcome === "flaky") {
261
+ testStatus = "flaky";
244
262
  }
245
263
  const startTime = new Date(result.startTime);
246
264
  const endTime = new Date(startTime.getTime() + result.duration);
247
265
  const processAllSteps = async (steps) => {
248
266
  let processed = [];
249
267
  for (const step of steps) {
250
- const processedStep = await this.processStep(step, pulseResult.id, browserDetails, test);
268
+ const processedStep = await this.processStep(step, uniqueTestId, browserDetails, test);
251
269
  processed.push(processedStep);
252
270
  if (step.steps && step.steps.length > 0) {
253
271
  processedStep.steps = await processAllSteps(step.steps);
@@ -255,7 +273,7 @@ class PlaywrightPulseReporter {
255
273
  }
256
274
  return processed;
257
275
  };
258
- let codeSnippet = '';
276
+ let codeSnippet = "";
259
277
  if (((_b = test.location) === null || _b === void 0 ? void 0 : _b.file) && ((_c = test.location) === null || _c === void 0 ? void 0 : _c.line) && ((_d = test.location) === null || _d === void 0 ? void 0 : _d.column)) {
260
278
  codeSnippet = this.extractCodeSnippet(test.location.file, test.location.line, test.location.column);
261
279
  }
@@ -284,7 +302,7 @@ class PlaywrightPulseReporter {
284
302
  : undefined,
285
303
  };
286
304
  const pulseResult = {
287
- id: `${(project === null || project === void 0 ? void 0 : project.name) || "default"}-${test.id}`,
305
+ id: uniqueTestId,
288
306
  runId: "TBD",
289
307
  describe: describeBlockName,
290
308
  spec_file: specFileName,
@@ -318,7 +336,7 @@ class PlaywrightPulseReporter {
318
336
  if (!attachment.path)
319
337
  continue;
320
338
  try {
321
- const testSubfolder = pulseResult.id.replace(/[^a-zA-Z0-9_-]/g, "_");
339
+ const testSubfolder = uniqueTestId.replace(/[^a-zA-Z0-9_-]/g, "_");
322
340
  const safeAttachmentName = path
323
341
  .basename(attachment.path)
324
342
  .replace(/[^a-zA-Z0-9_.-]/g, "_");
@@ -487,6 +505,39 @@ class PlaywrightPulseReporter {
487
505
  }
488
506
  }
489
507
  }
508
+ /**
509
+ * Removes all individual run JSON files from the `pulse-results/` directory
510
+ * that were left over from previous test sessions.
511
+ *
512
+ * When `resetOnEachRun: false`, each run writes its own timestamped JSON to
513
+ * `pulse-results/` and then `_mergeAllRunReports()` merges them all. However,
514
+ * if files from *older* sessions accumulate there (e.g. because a previous run
515
+ * was interrupted before the post-merge cleanup, or because the user ran tests
516
+ * on a previous day), `_getFinalizedResults()` de-duplicates by `test.id` and
517
+ * collapses results from both sessions into a single entry — producing a
518
+ * `totalTests` count lower than the actual number of tests that ran.
519
+ *
520
+ * Cleaning up at `onBegin` time guarantees each run starts with a fresh slate.
521
+ */
522
+ async _cleanupStaleRunReports() {
523
+ const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
524
+ try {
525
+ const files = await fs.readdir(pulseResultsDir);
526
+ const staleFiles = files.filter((f) => f.startsWith("playwright-pulse-report-") && f.endsWith(".json"));
527
+ if (staleFiles.length > 0) {
528
+ await Promise.all(staleFiles.map((f) => fs.unlink(path.join(pulseResultsDir, f))));
529
+ if (this.printsToStdio()) {
530
+ console.log(`PlaywrightPulseReporter: Cleaned up ${staleFiles.length} stale run report(s) from ${pulseResultsDir}`);
531
+ }
532
+ }
533
+ }
534
+ catch (error) {
535
+ // ENOENT simply means no previous runs exist — that's fine
536
+ if ((error === null || error === void 0 ? void 0 : error.code) !== "ENOENT") {
537
+ console.warn("Pulse Reporter: Warning during cleanup of stale run reports:", error.message);
538
+ }
539
+ }
540
+ }
490
541
  async _ensureDirExists(dirPath) {
491
542
  try {
492
543
  await fs.mkdir(dirPath, { recursive: true });
@@ -499,6 +550,11 @@ class PlaywrightPulseReporter {
499
550
  }
500
551
  }
501
552
  async onEnd(result) {
553
+ // Wait for ALL in-flight onTestEnd calls to finish before reading this.results.
554
+ // This guards against Playwright calling onEnd() concurrently with (or just
555
+ // before) the last onTestEnd() finishing its async attachment I/O — which
556
+ // would cause that test to be silently dropped from the report.
557
+ await Promise.allSettled(this._pendingTestEnds);
502
558
  if (this.shardIndex !== undefined) {
503
559
  await this._writeShardResults();
504
560
  return;
@@ -659,6 +715,16 @@ class PlaywrightPulseReporter {
659
715
  if (this.printsToStdio()) {
660
716
  console.log(`PlaywrightPulseReporter: ✅ Merged report with ${finalMergedResults.length} total results saved to ${finalOutputPath}`);
661
717
  }
718
+ // Clean up the pulse-results directory after a successful merge
719
+ try {
720
+ await fs.rm(pulseResultsDir, { recursive: true, force: true });
721
+ if (this.printsToStdio()) {
722
+ console.log(`PlaywrightPulseReporter: Cleaned up individual reports directory at ${pulseResultsDir}`);
723
+ }
724
+ }
725
+ catch (cleanupErr) {
726
+ console.warn(`Pulse Reporter: Could not clean up individual reports directory. Error: ${cleanupErr.message}`);
727
+ }
662
728
  }
663
729
  catch (err) {
664
730
  console.error(`Pulse Reporter: Failed to write final merged report to ${finalOutputPath}. Error: ${err.message}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arghajit/dummy",
3
3
  "author": "Arghajit Singha",
4
- "version": "0.3.34",
4
+ "version": "0.3.37",
5
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
6
  "homepage": "https://arghajit47.github.io/playwright-pulse/",
7
7
  "repository": {