@arghajit/dummy 0.1.0 → 0.1.2-beta-1

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,14 +37,25 @@ 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 convertStatus = (status, testCase) => {
40
+ const ua_parser_js_1 = require("ua-parser-js");
41
+ const os = __importStar(require("os"));
42
+ const convertStatus = (status, testCase, retryCount = 0) => {
43
+ // If a test passes on a retry, it's considered flaky regardless of expected status.
44
+ // This is the most critical check for flaky tests.
45
+ if (status === "passed" && retryCount > 0) {
46
+ return "flaky";
47
+ }
48
+ // Handle expected statuses for the final result.
43
49
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
50
+ // If expected to fail but passed, it's flaky.
51
+ if (status === "passed")
52
+ return "flaky";
44
53
  return "failed";
45
54
  }
46
55
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "skipped") {
47
56
  return "skipped";
48
57
  }
58
+ // Default Playwright status mapping
49
59
  switch (status) {
50
60
  case "passed":
51
61
  return "passed";
@@ -60,17 +70,21 @@ const convertStatus = (status, testCase) => {
60
70
  };
61
71
  const TEMP_SHARD_FILE_PREFIX = ".pulse-shard-results-";
62
72
  const ATTACHMENTS_SUBDIR = "attachments";
73
+ const INDIVIDUAL_REPORTS_SUBDIR = "pulse-results";
63
74
  class PlaywrightPulseReporter {
64
75
  constructor(options = {}) {
65
- var _a, _b;
76
+ var _a, _b, _c;
77
+ // This will now store all individual run attempts for all tests.
66
78
  this.results = [];
67
79
  this.baseOutputFile = "playwright-pulse-report.json";
68
80
  this.isSharded = false;
69
81
  this.shardIndex = undefined;
82
+ this.currentRunId = ""; // Added to store the overall run ID
70
83
  this.options = options;
71
84
  this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
72
85
  this.outputDir = (_b = options.outputDir) !== null && _b !== void 0 ? _b : "pulse-report";
73
86
  this.attachmentsDir = path.join(this.outputDir, ATTACHMENTS_SUBDIR);
87
+ this.resetOnEachRun = (_c = options.resetOnEachRun) !== null && _c !== void 0 ? _c : true;
74
88
  }
75
89
  printsToStdio() {
76
90
  return this.shardIndex === undefined || this.shardIndex === 0;
@@ -80,6 +94,8 @@ class PlaywrightPulseReporter {
80
94
  this.config = config;
81
95
  this.suite = suite;
82
96
  this.runStartTime = Date.now();
97
+ // Generate the overall runId once at the beginning
98
+ this.currentRunId = `run-${this.runStartTime}-${(0, crypto_1.randomUUID)()}`;
83
99
  const configDir = this.config.rootDir;
84
100
  const configFileDir = this.config.configFile
85
101
  ? path.dirname(this.config.configFile)
@@ -94,7 +110,7 @@ class PlaywrightPulseReporter {
94
110
  : undefined;
95
111
  this._ensureDirExists(this.outputDir)
96
112
  .then(() => {
97
- if (this.shardIndex === undefined || this.shardIndex === 0) {
113
+ if (this.printsToStdio()) {
98
114
  console.log(`PlaywrightPulseReporter: Starting test run with ${suite.allTests().length} tests${this.isSharded ? ` across ${totalShards} shards` : ""}. Pulse outputting to ${this.outputDir}`);
99
115
  if (this.shardIndex === undefined ||
100
116
  (this.isSharded && this.shardIndex === 0)) {
@@ -105,9 +121,60 @@ class PlaywrightPulseReporter {
105
121
  .catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
106
122
  }
107
123
  onTestBegin(test) {
108
- // console.log(`Starting test: ${test.title}`);
124
+ // console.log(`Starting test: ${test.title}`); // Removed for brevity in final output
125
+ }
126
+ getBrowserDetails(test) {
127
+ var _a, _b, _c, _d;
128
+ const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
129
+ const projectConfig = project === null || project === void 0 ? void 0 : project.use;
130
+ const userAgent = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.userAgent;
131
+ const configuredBrowserType = (_b = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.browserName) === null || _b === void 0 ? void 0 : _b.toLowerCase();
132
+ const parser = new ua_parser_js_1.UAParser(userAgent);
133
+ const result = parser.getResult();
134
+ let browserName = result.browser.name;
135
+ const browserVersion = result.browser.version
136
+ ? ` v${result.browser.version.split(".")[0]}`
137
+ : "";
138
+ const osName = result.os.name ? ` on ${result.os.name}` : "";
139
+ const osVersion = result.os.version
140
+ ? ` ${result.os.version.split(".")[0]}`
141
+ : "";
142
+ const deviceType = result.device.type;
143
+ let finalString;
144
+ if (browserName === undefined) {
145
+ browserName = configuredBrowserType;
146
+ finalString = `${browserName}`;
147
+ }
148
+ else {
149
+ if (deviceType === "mobile" || deviceType === "tablet") {
150
+ if ((_c = result.os.name) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes("android")) {
151
+ if (browserName.toLowerCase().includes("chrome"))
152
+ browserName = "Chrome Mobile";
153
+ else if (browserName.toLowerCase().includes("firefox"))
154
+ browserName = "Firefox Mobile";
155
+ else if (result.engine.name === "Blink" && !result.browser.name)
156
+ browserName = "Android WebView";
157
+ else if (browserName &&
158
+ !browserName.toLowerCase().includes("mobile")) {
159
+ // Keep it as is
160
+ }
161
+ else {
162
+ browserName = "Android Browser";
163
+ }
164
+ }
165
+ else if ((_d = result.os.name) === null || _d === void 0 ? void 0 : _d.toLowerCase().includes("ios")) {
166
+ browserName = "Mobile Safari";
167
+ }
168
+ }
169
+ else if (browserName === "Electron") {
170
+ browserName = "Electron App";
171
+ }
172
+ finalString = `${browserName}${browserVersion}${osName}${osVersion}`;
173
+ }
174
+ return finalString.trim();
109
175
  }
110
- async processStep(step, testId, browserName, testCase) {
176
+ async processStep(step, testId, browserDetails, testCase, retryCount = 0 // Pass retryCount to convertStatus for steps
177
+ ) {
111
178
  var _a, _b, _c, _d;
112
179
  let stepStatus = "passed";
113
180
  let errorMessage = ((_a = step.error) === null || _a === void 0 ? void 0 : _a.message) || undefined;
@@ -115,7 +182,8 @@ class PlaywrightPulseReporter {
115
182
  stepStatus = "skipped";
116
183
  }
117
184
  else {
118
- stepStatus = convertStatus(step.error ? "failed" : "passed", testCase);
185
+ // Use the extended convertStatus
186
+ stepStatus = convertStatus(step.error ? "failed" : "passed", testCase, retryCount);
119
187
  }
120
188
  const duration = step.duration;
121
189
  const startTime = new Date(step.startTime);
@@ -124,15 +192,14 @@ class PlaywrightPulseReporter {
124
192
  if (step.location) {
125
193
  codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
126
194
  }
127
- let stepTitle = step.title;
128
195
  return {
129
196
  id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}`,
130
- title: stepTitle,
197
+ title: step.title,
131
198
  status: stepStatus,
132
199
  duration: duration,
133
200
  startTime: startTime,
134
201
  endTime: endTime,
135
- browser: browserName,
202
+ browser: browserDetails,
136
203
  errorMessage: errorMessage,
137
204
  stackTrace: ((_d = step.error) === null || _d === void 0 ? void 0 : _d.stack) || undefined,
138
205
  codeLocation: codeLocation || undefined,
@@ -146,21 +213,18 @@ class PlaywrightPulseReporter {
146
213
  };
147
214
  }
148
215
  async onTestEnd(test, result) {
149
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
216
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
150
217
  const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
151
- const browserName = ((_b = project === null || project === void 0 ? void 0 : project.use) === null || _b === void 0 ? void 0 : _b.defaultBrowserType) || (project === null || project === void 0 ? void 0 : project.name) || "unknown";
152
- const testStatus = convertStatus(result.status, test);
218
+ const browserDetails = this.getBrowserDetails(test);
219
+ // Use the extended convertStatus, passing result.retry
220
+ const testStatus = convertStatus(result.status, test, result.retry);
153
221
  const startTime = new Date(result.startTime);
154
222
  const endTime = new Date(startTime.getTime() + result.duration);
155
- const testIdForFiles = test.id ||
156
- `${test
157
- .titlePath()
158
- .join("_")
159
- .replace(/[^a-zA-Z0-9]/g, "_")}_${startTime.getTime()}`;
160
223
  const processAllSteps = async (steps) => {
161
224
  let processed = [];
162
225
  for (const step of steps) {
163
- const processedStep = await this.processStep(step, testIdForFiles, browserName, test);
226
+ const processedStep = await this.processStep(step, test.id, browserDetails, test, result.retry // Pass retryCount to processStep
227
+ );
164
228
  processed.push(processedStep);
165
229
  if (step.steps && step.steps.length > 0) {
166
230
  processedStep.steps = await processAllSteps(step.steps);
@@ -170,7 +234,7 @@ class PlaywrightPulseReporter {
170
234
  };
171
235
  let codeSnippet = undefined;
172
236
  try {
173
- if (((_c = test.location) === null || _c === void 0 ? void 0 : _c.file) && ((_d = test.location) === null || _d === void 0 ? void 0 : _d.line) && ((_e = test.location) === null || _e === void 0 ? void 0 : _e.column)) {
237
+ 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)) {
174
238
  const relativePath = path.relative(this.config.rootDir, test.location.file);
175
239
  codeSnippet = `Test defined at: ${relativePath}:${test.location.line}:${test.location.column}`;
176
240
  }
@@ -178,55 +242,145 @@ class PlaywrightPulseReporter {
178
242
  catch (e) {
179
243
  console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
180
244
  }
181
- const stdoutMessages = [];
182
- if (result.stdout && result.stdout.length > 0) {
183
- result.stdout.forEach((item) => {
184
- stdoutMessages.push(typeof item === "string" ? item : item.toString());
185
- });
186
- }
187
- const stderrMessages = [];
188
- if (result.stderr && result.stderr.length > 0) {
189
- result.stderr.forEach((item) => {
190
- stderrMessages.push(typeof item === "string" ? item : item.toString());
191
- });
192
- }
193
- const uniqueTestId = test.id;
245
+ const stdoutMessages = result.stdout.map((item) => typeof item === "string" ? item : item.toString());
246
+ const stderrMessages = result.stderr.map((item) => typeof item === "string" ? item : item.toString());
247
+ const maxWorkers = this.config.workers;
248
+ let mappedWorkerId = result.workerIndex === -1
249
+ ? -1
250
+ : (result.workerIndex % (maxWorkers > 0 ? maxWorkers : 1)) + 1;
251
+ const testSpecificData = {
252
+ workerId: mappedWorkerId,
253
+ totalWorkers: maxWorkers,
254
+ configFile: this.config.configFile,
255
+ metadata: this.config.metadata
256
+ ? JSON.stringify(this.config.metadata)
257
+ : undefined,
258
+ };
259
+ // Correctly handle the ID for each run. A unique ID per attempt is crucial.
260
+ const testIdWithRunCounter = `${test.id}-run-${result.retry}`;
194
261
  const pulseResult = {
195
- id: uniqueTestId,
196
- runId: "TBD",
262
+ id: testIdWithRunCounter, // Use the modified ID
263
+ runId: this.currentRunId, // Assign the overall run ID
197
264
  name: test.titlePath().join(" > "),
198
- suiteName: (project === null || project === void 0 ? void 0 : project.name) || ((_f = this.config.projects[0]) === null || _f === void 0 ? void 0 : _f.name) || "Default Suite",
265
+ 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",
199
266
  status: testStatus,
200
267
  duration: result.duration,
201
268
  startTime: startTime,
202
269
  endTime: endTime,
203
- browser: browserName,
204
- retries: result.retry,
205
- steps: ((_g = result.steps) === null || _g === void 0 ? void 0 : _g.length) ? await processAllSteps(result.steps) : [],
206
- errorMessage: (_h = result.error) === null || _h === void 0 ? void 0 : _h.message,
207
- stackTrace: (_j = result.error) === null || _j === void 0 ? void 0 : _j.stack,
270
+ browser: browserDetails,
271
+ retries: result.retry, // This is the Playwright retry count (0 for first run, 1 for first retry, etc.)
272
+ runCounter: result.retry, // This is your 'runCounter'
273
+ steps: ((_f = result.steps) === null || _f === void 0 ? void 0 : _f.length) ? await processAllSteps(result.steps) : [],
274
+ errorMessage: (_g = result.error) === null || _g === void 0 ? void 0 : _g.message,
275
+ stackTrace: (_h = result.error) === null || _h === void 0 ? void 0 : _h.stack,
276
+ snippet: (_j = result.error) === null || _j === void 0 ? void 0 : _j.snippet,
208
277
  codeSnippet: codeSnippet,
209
278
  tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
210
279
  screenshots: [],
211
- videoPath: undefined,
280
+ videoPath: [],
212
281
  tracePath: undefined,
282
+ attachments: [],
213
283
  stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
214
284
  stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
285
+ ...testSpecificData,
215
286
  };
216
- try {
217
- (0, attachment_utils_1.attachFiles)(testIdForFiles, result, pulseResult, this.options);
287
+ for (const [index, attachment] of result.attachments.entries()) {
288
+ if (!attachment.path)
289
+ continue;
290
+ try {
291
+ // Use the new testIdWithRunCounter for the subfolder
292
+ const testSubfolder = testIdWithRunCounter.replace(/[^a-zA-Z0-9_-]/g, "_");
293
+ const safeAttachmentName = path
294
+ .basename(attachment.path)
295
+ .replace(/[^a-zA-Z0-9_.-]/g, "_");
296
+ const uniqueFileName = `${index}-${Date.now()}-${safeAttachmentName}`;
297
+ const relativeDestPath = path.join(ATTACHMENTS_SUBDIR, testSubfolder, uniqueFileName);
298
+ const absoluteDestPath = path.join(this.outputDir, relativeDestPath);
299
+ await this._ensureDirExists(path.dirname(absoluteDestPath));
300
+ await fs.copyFile(attachment.path, absoluteDestPath);
301
+ if (attachment.contentType.startsWith("image/")) {
302
+ (_k = pulseResult.screenshots) === null || _k === void 0 ? void 0 : _k.push(relativeDestPath);
303
+ }
304
+ else if (attachment.contentType.startsWith("video/")) {
305
+ (_l = pulseResult.videoPath) === null || _l === void 0 ? void 0 : _l.push(relativeDestPath);
306
+ }
307
+ else if (attachment.name === "trace") {
308
+ pulseResult.tracePath = relativeDestPath;
309
+ }
310
+ else {
311
+ (_m = pulseResult.attachments) === null || _m === void 0 ? void 0 : _m.push({
312
+ name: attachment.name,
313
+ path: relativeDestPath,
314
+ contentType: attachment.contentType,
315
+ });
316
+ }
317
+ }
318
+ catch (err) {
319
+ console.error(`Pulse Reporter: Failed to process attachment "${attachment.name}" for test ${pulseResult.name}. Error: ${err.message}`);
320
+ }
218
321
  }
219
- catch (attachError) {
220
- console.error(`Pulse Reporter: Error processing attachments for test ${pulseResult.name} (ID: ${testIdForFiles}): ${attachError.message}`);
322
+ this.results.push(pulseResult);
323
+ }
324
+ // New method to extract the base test ID, ignoring the run-counter suffix
325
+ _getBaseTestId(testResultId) {
326
+ const parts = testResultId.split("-run-");
327
+ return parts[0];
328
+ }
329
+ _getFinalizedResults(allResults) {
330
+ const finalResultsMap = new Map();
331
+ const allRunsMap = new Map();
332
+ // First, group all run attempts by their base test ID
333
+ for (const result of allResults) {
334
+ const baseTestId = this._getBaseTestId(result.id);
335
+ if (!allRunsMap.has(baseTestId)) {
336
+ allRunsMap.set(baseTestId, []);
337
+ }
338
+ allRunsMap.get(baseTestId).push(result);
221
339
  }
222
- const existingTestIndex = this.results.findIndex((r) => r.id === uniqueTestId);
223
- if (existingTestIndex !== -1) {
224
- if (pulseResult.retries >= this.results[existingTestIndex].retries) {
225
- this.results[existingTestIndex] = pulseResult;
340
+ // Now, iterate through the grouped runs to determine the final state
341
+ for (const [baseTestId, runs] of allRunsMap.entries()) {
342
+ let finalResult = undefined;
343
+ // Sort runs to process them in chronological order
344
+ runs.sort((a, b) => a.runCounter - b.runCounter);
345
+ for (const currentRun of runs) {
346
+ if (!finalResult) {
347
+ finalResult = currentRun;
348
+ }
349
+ else {
350
+ // Compare the current run to the best result found so far
351
+ const currentStatusOrder = this._getStatusOrder(currentRun.status);
352
+ const finalStatusOrder = this._getStatusOrder(finalResult.status);
353
+ if (currentStatusOrder < finalStatusOrder) {
354
+ // Current run is "better" (e.g., passed over failed)
355
+ finalResult = currentRun;
356
+ }
357
+ else if (currentStatusOrder === finalStatusOrder &&
358
+ currentRun.retries > finalResult.retries) {
359
+ // Same status, but prefer the latest attempt
360
+ finalResult = currentRun;
361
+ }
362
+ }
363
+ }
364
+ if (finalResult) {
365
+ // Ensure the ID of the final result is the base test ID for de-duplication
366
+ finalResult.id = baseTestId;
367
+ finalResultsMap.set(baseTestId, finalResult);
226
368
  }
227
369
  }
228
- else {
229
- this.results.push(pulseResult);
370
+ return Array.from(finalResultsMap.values());
371
+ }
372
+ _getStatusOrder(status) {
373
+ switch (status) {
374
+ case "passed":
375
+ return 1;
376
+ case "flaky":
377
+ return 2;
378
+ case "failed":
379
+ return 3;
380
+ case "skipped":
381
+ return 4;
382
+ default:
383
+ return 99; // Unknown status
230
384
  }
231
385
  }
232
386
  onError(error) {
@@ -236,6 +390,20 @@ class PlaywrightPulseReporter {
236
390
  console.error(error.stack);
237
391
  }
238
392
  }
393
+ _getEnvDetails() {
394
+ return {
395
+ host: os.hostname(),
396
+ os: `${os.platform()} ${os.release()}`,
397
+ cpu: {
398
+ model: os.cpus()[0] ? os.cpus()[0].model : "N/A",
399
+ cores: os.cpus().length,
400
+ },
401
+ memory: `${(os.totalmem() / 1024 ** 3).toFixed(2)}GB`,
402
+ node: process.version,
403
+ v8: process.versions.v8,
404
+ cwd: process.cwd(),
405
+ };
406
+ }
239
407
  async _writeShardResults() {
240
408
  if (this.shardIndex === undefined) {
241
409
  return;
@@ -249,15 +417,14 @@ class PlaywrightPulseReporter {
249
417
  }
250
418
  }
251
419
  async _mergeShardResults(finalRunData) {
252
- let allShardProcessedResults = [];
420
+ let allShardRawResults = []; // Store raw results before final de-duplication
253
421
  const totalShards = this.config.shard ? this.config.shard.total : 1;
254
422
  for (let i = 0; i < totalShards; i++) {
255
423
  const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
256
424
  try {
257
425
  const content = await fs.readFile(tempFilePath, "utf-8");
258
426
  const shardResults = JSON.parse(content);
259
- allShardProcessedResults =
260
- allShardProcessedResults.concat(shardResults);
427
+ allShardRawResults = allShardRawResults.concat(shardResults);
261
428
  }
262
429
  catch (error) {
263
430
  if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
@@ -268,18 +435,14 @@ class PlaywrightPulseReporter {
268
435
  }
269
436
  }
270
437
  }
271
- let finalUniqueResultsMap = new Map();
272
- for (const result of allShardProcessedResults) {
273
- const existing = finalUniqueResultsMap.get(result.id);
274
- if (!existing || result.retries >= existing.retries) {
275
- finalUniqueResultsMap.set(result.id, result);
276
- }
277
- }
278
- const finalResultsList = Array.from(finalUniqueResultsMap.values());
438
+ // Apply _getFinalizedResults after all raw shard results are collected
439
+ const finalResultsList = this._getFinalizedResults(allShardRawResults);
279
440
  finalResultsList.forEach((r) => (r.runId = finalRunData.id));
280
441
  finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
281
442
  finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
282
443
  finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
444
+ // Add flaky count
445
+ finalRunData.flaky = finalResultsList.filter((r) => r.status === "flaky").length;
283
446
  finalRunData.totalTests = finalResultsList.length;
284
447
  const reviveDates = (key, value) => {
285
448
  const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
@@ -322,131 +485,177 @@ class PlaywrightPulseReporter {
322
485
  }
323
486
  }
324
487
  async onEnd(result) {
325
- var _a, _b, _c;
326
488
  if (this.shardIndex !== undefined) {
327
489
  await this._writeShardResults();
328
490
  return;
329
491
  }
492
+ // Now, `this.results` contains all individual run attempts.
493
+ // _getFinalizedResults will select the "best" run for each logical test.
494
+ const finalResults = this._getFinalizedResults(this.results);
330
495
  const runEndTime = Date.now();
331
496
  const duration = runEndTime - this.runStartTime;
332
- const runId = `run-${this.runStartTime}-${(0, crypto_1.randomUUID)()}`;
497
+ // Use the stored overall runId
498
+ const runId = this.currentRunId;
499
+ const environmentDetails = this._getEnvDetails();
333
500
  const runData = {
334
501
  id: runId,
335
502
  timestamp: new Date(this.runStartTime),
336
- totalTests: 0,
337
- passed: 0,
338
- failed: 0,
339
- skipped: 0,
503
+ totalTests: finalResults.length,
504
+ passed: finalResults.filter((r) => r.status === "passed").length,
505
+ failed: finalResults.filter((r) => r.status === "failed").length,
506
+ skipped: finalResults.filter((r) => r.status === "skipped").length,
507
+ flaky: finalResults.filter((r) => r.status === "flaky").length, // Add flaky count
340
508
  duration,
509
+ environment: environmentDetails,
341
510
  };
342
- let finalReport = undefined; // Initialize as undefined
511
+ // Ensure all final results have the correct overall runId
512
+ finalResults.forEach((r) => (r.runId = runId));
513
+ let finalReport = undefined;
343
514
  if (this.isSharded) {
515
+ // _mergeShardResults will now perform the final de-duplication across shards
344
516
  finalReport = await this._mergeShardResults(runData);
345
517
  }
346
518
  else {
347
- this.results.forEach((r) => (r.runId = runId));
348
- runData.passed = this.results.filter((r) => r.status === "passed").length;
349
- runData.failed = this.results.filter((r) => r.status === "failed").length;
350
- runData.skipped = this.results.filter((r) => r.status === "skipped").length;
351
- runData.totalTests = this.results.length;
352
- const reviveDates = (key, value) => {
353
- const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
354
- if (typeof value === "string" && isoDateRegex.test(value)) {
355
- const date = new Date(value);
356
- return !isNaN(date.getTime()) ? date : value;
357
- }
358
- return value;
359
- };
360
- const properlyTypedResults = JSON.parse(JSON.stringify(this.results), reviveDates);
361
519
  finalReport = {
362
520
  run: runData,
363
- results: properlyTypedResults,
521
+ results: finalResults, // Use the de-duplicated results for a non-sharded run
364
522
  metadata: { generatedAt: new Date().toISOString() },
365
523
  };
366
524
  }
367
525
  if (!finalReport) {
368
526
  console.error("PlaywrightPulseReporter: CRITICAL - finalReport object was not generated. Cannot create summary.");
369
- const errorSummary = `
370
- PlaywrightPulseReporter: Run Finished
371
- -----------------------------------------
372
- Overall Status: ERROR (Report data missing)
373
- Total Tests: N/A
374
- Passed: N/A
375
- Failed: N/A
376
- Skipped: N/A
377
- Duration: N/A
378
- -----------------------------------------`;
379
- if (this.printsToStdio()) {
380
- console.log(errorSummary);
381
- }
382
- const errorReport = {
383
- run: {
384
- id: runId,
385
- timestamp: new Date(this.runStartTime),
386
- totalTests: 0,
387
- passed: 0,
388
- failed: 0,
389
- skipped: 0,
390
- duration: duration,
391
- },
392
- results: [],
393
- metadata: {
394
- generatedAt: new Date().toISOString(),
395
- },
396
- };
397
- const finalOutputPathOnError = path.join(this.outputDir, this.baseOutputFile);
527
+ return;
528
+ }
529
+ const jsonReplacer = (key, value) => {
530
+ if (value instanceof Date)
531
+ return value.toISOString();
532
+ if (typeof value === "bigint")
533
+ return value.toString();
534
+ return value;
535
+ };
536
+ if (this.resetOnEachRun) {
537
+ const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
398
538
  try {
399
539
  await this._ensureDirExists(this.outputDir);
400
- await fs.writeFile(finalOutputPathOnError, JSON.stringify(errorReport, null, 2));
401
- console.warn(`PlaywrightPulseReporter: Wrote an error report to ${finalOutputPathOnError} as finalReport was missing.`);
540
+ await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, jsonReplacer, 2));
541
+ if (this.printsToStdio()) {
542
+ console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
543
+ }
402
544
  }
403
- catch (writeError) {
404
- console.error(`PlaywrightPulseReporter: Failed to write error report: ${writeError.message}`);
545
+ catch (error) {
546
+ console.error(`Pulse Reporter: Failed to write final JSON report to ${finalOutputPath}. Error: ${error.message}`);
405
547
  }
406
- return;
407
548
  }
408
- const reportRunData = finalReport.run;
409
- const finalRunStatus = ((_a = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.failed) !== null && _a !== void 0 ? _a : 0) > 0
410
- ? "failed"
411
- : ((_b = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.totalTests) !== null && _b !== void 0 ? _b : 0) === 0 && result.status !== "passed"
412
- ? result.status === "interrupted"
413
- ? "interrupted"
414
- : "no tests or error"
415
- : "passed";
416
- const summary = `
417
- PlaywrightPulseReporter: Run Finished
418
- -----------------------------------------
419
- Overall Status: ${finalRunStatus.toUpperCase()}
420
- Total Tests: ${(reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.totalTests) || 0}
421
- Passed: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.passed}
422
- Failed: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.failed}
423
- Skipped: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.skipped}
424
- Duration: ${(((_c = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.duration) !== null && _c !== void 0 ? _c : 0) / 1000).toFixed(2)}s
425
- -----------------------------------------`;
426
- if (this.printsToStdio()) {
427
- console.log(summary);
549
+ else {
550
+ const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
551
+ const individualReportPath = path.join(pulseResultsDir, `playwright-pulse-report-${Date.now()}.json`);
552
+ try {
553
+ await this._ensureDirExists(pulseResultsDir);
554
+ await fs.writeFile(individualReportPath, JSON.stringify(finalReport, jsonReplacer, 2));
555
+ if (this.printsToStdio()) {
556
+ console.log(`PlaywrightPulseReporter: Individual run report for merging written to ${individualReportPath}`);
557
+ }
558
+ await this._mergeAllRunReports();
559
+ }
560
+ catch (error) {
561
+ console.error(`Pulse Reporter: Failed to write or merge report. Error: ${error.message}`);
562
+ }
428
563
  }
564
+ if (this.isSharded) {
565
+ await this._cleanupTemporaryFiles();
566
+ }
567
+ }
568
+ async _mergeAllRunReports() {
569
+ const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
429
570
  const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
571
+ let reportFiles;
572
+ try {
573
+ const allFiles = await fs.readdir(pulseResultsDir);
574
+ reportFiles = allFiles.filter((file) => file.startsWith("playwright-pulse-report-") && file.endsWith(".json"));
575
+ }
576
+ catch (error) {
577
+ if (error.code === "ENOENT") {
578
+ if (this.printsToStdio()) {
579
+ console.log(`Pulse Reporter: No individual reports directory found at ${pulseResultsDir}. Skipping merge.`);
580
+ }
581
+ return;
582
+ }
583
+ console.error(`Pulse Reporter: Error reading report directory ${pulseResultsDir}:`, error);
584
+ return;
585
+ }
586
+ if (reportFiles.length === 0) {
587
+ if (this.printsToStdio()) {
588
+ console.log("Pulse Reporter: No matching JSON report files found to merge.");
589
+ }
590
+ return;
591
+ }
592
+ const allResultsFromAllFiles = [];
593
+ let latestTimestamp = new Date(0);
594
+ let lastRunEnvironment = undefined;
595
+ // We can't simply sum durations across merged files, as the tests might overlap.
596
+ // The final duration will be derived from the range of start/end times in the final results.
597
+ let earliestStartTime = Date.now();
598
+ let latestEndTime = 0;
599
+ for (const file of reportFiles) {
600
+ const filePath = path.join(pulseResultsDir, file);
601
+ try {
602
+ const content = await fs.readFile(filePath, "utf-8");
603
+ const json = JSON.parse(content);
604
+ if (json.run) {
605
+ const runTimestamp = new Date(json.run.timestamp);
606
+ if (runTimestamp > latestTimestamp) {
607
+ latestTimestamp = runTimestamp;
608
+ lastRunEnvironment = json.run.environment || undefined;
609
+ }
610
+ }
611
+ if (json.results) {
612
+ allResultsFromAllFiles.push(...json.results);
613
+ }
614
+ }
615
+ catch (err) {
616
+ console.warn(`Pulse Reporter: Could not parse report file ${filePath}. Skipping. Error: ${err.message}`);
617
+ }
618
+ }
619
+ // De-duplicate the results from ALL merged files using the helper function
620
+ const finalMergedResults = this._getFinalizedResults(allResultsFromAllFiles);
621
+ // Calculate overall duration from the earliest start and latest end of the final merged results
622
+ for (const res of finalMergedResults) {
623
+ if (res.startTime.getTime() < earliestStartTime)
624
+ earliestStartTime = res.startTime.getTime();
625
+ if (res.endTime.getTime() > latestEndTime)
626
+ latestEndTime = res.endTime.getTime();
627
+ }
628
+ const totalDuration = latestEndTime > earliestStartTime ? latestEndTime - earliestStartTime : 0;
629
+ const combinedRun = {
630
+ id: `merged-${Date.now()}`,
631
+ timestamp: latestTimestamp,
632
+ environment: lastRunEnvironment,
633
+ totalTests: finalMergedResults.length,
634
+ passed: finalMergedResults.filter((r) => r.status === "passed").length,
635
+ failed: finalMergedResults.filter((r) => r.status === "failed").length,
636
+ skipped: finalMergedResults.filter((r) => r.status === "skipped").length,
637
+ flaky: finalMergedResults.filter((r) => r.status === "flaky").length, // Add flaky count
638
+ duration: totalDuration,
639
+ };
640
+ const finalReport = {
641
+ run: combinedRun,
642
+ results: finalMergedResults,
643
+ metadata: {
644
+ generatedAt: new Date().toISOString(),
645
+ },
646
+ };
430
647
  try {
431
- await this._ensureDirExists(this.outputDir);
432
648
  await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, (key, value) => {
433
649
  if (value instanceof Date)
434
650
  return value.toISOString();
435
- if (typeof value === "bigint")
436
- return value.toString();
437
651
  return value;
438
652
  }, 2));
439
653
  if (this.printsToStdio()) {
440
- console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
654
+ console.log(`PlaywrightPulseReporter: Merged report with ${finalMergedResults.length} total results saved to ${finalOutputPath}`);
441
655
  }
442
656
  }
443
- catch (error) {
444
- console.error(`Pulse Reporter: Failed to write final JSON report to ${finalOutputPath}. Error: ${error.message}`);
445
- }
446
- finally {
447
- if (this.isSharded) {
448
- await this._cleanupTemporaryFiles();
449
- }
657
+ catch (err) {
658
+ console.error(`Pulse Reporter: Failed to write final merged report to ${finalOutputPath}. Error: ${err.message}`);
450
659
  }
451
660
  }
452
661
  }