@arghajit/playwright-pulse-report 0.2.0 → 0.2.2

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.
@@ -39,9 +39,11 @@ const fs = __importStar(require("fs/promises"));
39
39
  const path = __importStar(require("path"));
40
40
  const crypto_1 = require("crypto");
41
41
  const attachment_utils_1 = require("./attachment-utils"); // Use relative path
42
+ const ua_parser_js_1 = require("ua-parser-js"); // Added UAParser import
43
+ const os = __importStar(require("os"));
42
44
  const convertStatus = (status, testCase) => {
43
45
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
44
- return status === "failed" ? "failed" : "failed";
46
+ return "failed";
45
47
  }
46
48
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "skipped") {
47
49
  return "skipped";
@@ -94,24 +96,77 @@ class PlaywrightPulseReporter {
94
96
  : undefined;
95
97
  this._ensureDirExists(this.outputDir)
96
98
  .then(() => {
97
- if (this.shardIndex === undefined) {
99
+ if (this.shardIndex === undefined || this.shardIndex === 0) {
98
100
  console.log(`PlaywrightPulseReporter: Starting test run with ${suite.allTests().length} tests${this.isSharded ? ` across ${totalShards} shards` : ""}. Pulse outputting to ${this.outputDir}`);
99
- return this._cleanupTemporaryFiles();
101
+ if (this.shardIndex === undefined ||
102
+ (this.isSharded && this.shardIndex === 0)) {
103
+ return this._cleanupTemporaryFiles();
104
+ }
100
105
  }
101
106
  })
102
107
  .catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
103
108
  }
104
109
  onTestBegin(test) {
105
- // console.log(`Starting test: ${test.title}`);
110
+ console.log(`Starting test: ${test.title}`);
111
+ }
112
+ getBrowserDetails(test) {
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
116
+ const userAgent = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.userAgent;
117
+ const configuredBrowserType = (_b = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.browserName) === null || _b === void 0 ? void 0 : _b.toLowerCase();
118
+ const parser = new ua_parser_js_1.UAParser(userAgent);
119
+ const result = parser.getResult();
120
+ let browserName = result.browser.name;
121
+ const browserVersion = result.browser.version
122
+ ? ` v${result.browser.version.split(".")[0]}`
123
+ : ""; // Major version
124
+ const osName = result.os.name ? ` on ${result.os.name}` : "";
125
+ const osVersion = result.os.version
126
+ ? ` ${result.os.version.split(".")[0]}`
127
+ : ""; // Major version
128
+ const deviceType = result.device.type; // "mobile", "tablet", etc.
129
+ let finalString;
130
+ // If UAParser couldn't determine browser name, fallback to configured type
131
+ if (browserName === undefined) {
132
+ browserName = configuredBrowserType;
133
+ finalString = `${browserName}`;
134
+ }
135
+ else {
136
+ // Specific refinements for mobile based on parsed OS and device type
137
+ if (deviceType === "mobile" || deviceType === "tablet") {
138
+ if ((_c = result.os.name) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes("android")) {
139
+ if (browserName.toLowerCase().includes("chrome"))
140
+ browserName = "Chrome Mobile";
141
+ else if (browserName.toLowerCase().includes("firefox"))
142
+ browserName = "Firefox Mobile";
143
+ else if (result.engine.name === "Blink" && !result.browser.name)
144
+ browserName = "Android WebView";
145
+ else if (browserName &&
146
+ !browserName.toLowerCase().includes("mobile")) {
147
+ // Keep it as is, e.g. "Samsung Browser" is specific enough
148
+ }
149
+ else {
150
+ browserName = "Android Browser"; // default for android if not specific
151
+ }
152
+ }
153
+ else if ((_d = result.os.name) === null || _d === void 0 ? void 0 : _d.toLowerCase().includes("ios")) {
154
+ browserName = "Mobile Safari";
155
+ }
156
+ }
157
+ else if (browserName === "Electron") {
158
+ browserName = "Electron App";
159
+ }
160
+ finalString = `${browserName}${browserVersion}${osName}${osVersion}`;
161
+ }
162
+ return finalString.trim();
106
163
  }
107
- async processStep(step, testId, browserName, // Changed from browserName for clarity
108
- testCase) {
164
+ async processStep(step, testId, browserDetails, testCase) {
109
165
  var _a, _b, _c, _d;
110
166
  let stepStatus = "passed";
111
167
  let errorMessage = ((_a = step.error) === null || _a === void 0 ? void 0 : _a.message) || undefined;
112
168
  if ((_c = (_b = step.error) === null || _b === void 0 ? void 0 : _b.message) === null || _c === void 0 ? void 0 : _c.startsWith("Test is skipped:")) {
113
169
  stepStatus = "skipped";
114
- errorMessage = "Info: Test is skipped:";
115
170
  }
116
171
  else {
117
172
  stepStatus = convertStatus(step.error ? "failed" : "passed", testCase);
@@ -124,28 +179,6 @@ class PlaywrightPulseReporter {
124
179
  codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
125
180
  }
126
181
  let stepTitle = step.title;
127
- // This logic had a 'status' variable that was not defined in this scope.
128
- // Assuming it meant to check 'stepStatus' or 'testCase.expectedStatus' related to step.error.
129
- // Corrected to reflect comparison with testCase if step.category is 'test'.
130
- if (step.category === "test" && testCase) {
131
- // If a test step (not a hook) resulted in an error, but the test was expected to fail,
132
- // this specific logic might need refinement based on how you want to report step errors
133
- // within a test that is expected to fail.
134
- // The current convertStatus handles the overall testCase expectedStatus.
135
- // For step-specific error messages when testCase.expectedStatus === 'failed':
136
- if (testCase.expectedStatus === "failed") {
137
- if (step.error) {
138
- // If the step itself has an error
139
- // errorMessage is already set from step.error.message
140
- }
141
- else {
142
- // If a step within an expected-to-fail test passes, it's usually not an error for the step itself.
143
- }
144
- }
145
- else if (testCase.expectedStatus === "skipped") {
146
- // errorMessage is already set if step.error.message started with "Test is skipped:"
147
- }
148
- }
149
182
  return {
150
183
  id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}`,
151
184
  title: stepTitle,
@@ -153,7 +186,7 @@ class PlaywrightPulseReporter {
153
186
  duration: duration,
154
187
  startTime: startTime,
155
188
  endTime: endTime,
156
- browser: browserName,
189
+ browser: browserDetails,
157
190
  errorMessage: errorMessage,
158
191
  stackTrace: ((_d = step.error) === null || _d === void 0 ? void 0 : _d.stack) || undefined,
159
192
  codeLocation: codeLocation || undefined,
@@ -167,12 +200,9 @@ class PlaywrightPulseReporter {
167
200
  };
168
201
  }
169
202
  async onTestEnd(test, result) {
170
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
203
+ var _a, _b, _c, _d, _e, _f, _g, _h;
171
204
  const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
172
- // Use project.name for a user-friendly display name
173
- const browserName = ((_b = project === null || project === void 0 ? void 0 : project.use) === null || _b === void 0 ? void 0 : _b.defaultBrowserType) || "unknown";
174
- // If you need the engine name (chromium, firefox, webkit)
175
- // const browserEngineName = project?.use?.browserName || "unknown_engine";
205
+ const browserDetails = this.getBrowserDetails(test);
176
206
  const testStatus = convertStatus(result.status, test);
177
207
  const startTime = new Date(result.startTime);
178
208
  const endTime = new Date(startTime.getTime() + result.duration);
@@ -181,14 +211,10 @@ class PlaywrightPulseReporter {
181
211
  .titlePath()
182
212
  .join("_")
183
213
  .replace(/[^a-zA-Z0-9]/g, "_")}_${startTime.getTime()}`;
184
- const processAllSteps = async (steps
185
- // parentTestStatus parameter was not used, removed for now.
186
- // If needed for inherited status logic for steps, it can be re-added.
187
- ) => {
214
+ const processAllSteps = async (steps) => {
188
215
  let processed = [];
189
216
  for (const step of steps) {
190
- const processedStep = await this.processStep(step, testIdForFiles, browserName, // Pass display name
191
- test);
217
+ const processedStep = await this.processStep(step, testIdForFiles, browserDetails, test);
192
218
  processed.push(processedStep);
193
219
  if (step.steps && step.steps.length > 0) {
194
220
  processedStep.steps = await processAllSteps(step.steps);
@@ -198,7 +224,7 @@ class PlaywrightPulseReporter {
198
224
  };
199
225
  let codeSnippet = undefined;
200
226
  try {
201
- 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)) {
227
+ 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)) {
202
228
  const relativePath = path.relative(this.config.rootDir, test.location.file);
203
229
  codeSnippet = `Test defined at: ${relativePath}:${test.location.line}:${test.location.column}`;
204
230
  }
@@ -206,69 +232,84 @@ class PlaywrightPulseReporter {
206
232
  catch (e) {
207
233
  console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
208
234
  }
209
- // --- Capture stdout and stderr ---
210
235
  const stdoutMessages = [];
211
236
  if (result.stdout && result.stdout.length > 0) {
212
237
  result.stdout.forEach((item) => {
213
- if (typeof item === "string") {
214
- stdoutMessages.push(item);
215
- }
216
- else {
217
- // If item is not a string, Playwright's typings indicate it's a Buffer (or Buffer-like).
218
- // We must call toString() on it.
219
- // The 'item' here is typed as 'Buffer' from the 'else' branch of '(string | Buffer)[]'
220
- stdoutMessages.push(item.toString());
221
- }
238
+ stdoutMessages.push(typeof item === "string" ? item : item.toString());
222
239
  });
223
240
  }
224
241
  const stderrMessages = [];
225
242
  if (result.stderr && result.stderr.length > 0) {
226
243
  result.stderr.forEach((item) => {
227
- if (typeof item === "string") {
228
- stderrMessages.push(item);
229
- }
230
- else {
231
- // If item is not a string, Playwright's typings indicate it's a Buffer (or Buffer-like).
232
- // We must call toString() on it.
233
- stderrMessages.push(item.toString());
234
- }
244
+ stderrMessages.push(typeof item === "string" ? item : item.toString());
235
245
  });
236
246
  }
237
- // --- End capture stdout and stderr ---
247
+ const uniqueTestId = test.id;
248
+ // --- REFINED THIS SECTION for testData ---
249
+ 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
+ }
265
+ const testSpecificData = {
266
+ workerId: mappedWorkerId,
267
+ uniqueWorkerIndex: result.workerIndex, // We'll keep the original for diagnostics
268
+ totalWorkers: maxWorkers,
269
+ configFile: this.config.configFile,
270
+ metadata: this.config.metadata
271
+ ? JSON.stringify(this.config.metadata)
272
+ : undefined,
273
+ };
238
274
  const pulseResult = {
239
- id: test.id || `${test.title}-${startTime.toISOString()}-${(0, crypto_1.randomUUID)()}`,
275
+ id: uniqueTestId,
240
276
  runId: "TBD",
241
277
  name: test.titlePath().join(" > "),
242
- // Use project.name for suiteName if desired, or fallback
243
- 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",
278
+ 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",
244
279
  status: testStatus,
245
280
  duration: result.duration,
246
281
  startTime: startTime,
247
282
  endTime: endTime,
248
- browser: browserName, // Use the user-friendly project name
283
+ browser: browserDetails,
249
284
  retries: result.retry,
250
- steps: ((_g = result.steps) === null || _g === void 0 ? void 0 : _g.length) ? await processAllSteps(result.steps) : [],
251
- errorMessage: (_h = result.error) === null || _h === void 0 ? void 0 : _h.message,
252
- stackTrace: (_j = result.error) === null || _j === void 0 ? void 0 : _j.stack,
285
+ steps: ((_f = result.steps) === null || _f === void 0 ? void 0 : _f.length) ? await processAllSteps(result.steps) : [],
286
+ errorMessage: (_g = result.error) === null || _g === void 0 ? void 0 : _g.message,
287
+ stackTrace: (_h = result.error) === null || _h === void 0 ? void 0 : _h.stack,
253
288
  codeSnippet: codeSnippet,
254
289
  tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
255
- screenshots: [], // Will be populated by attachFiles
290
+ screenshots: [],
256
291
  videoPath: undefined,
257
292
  tracePath: undefined,
258
- // videoPath and tracePath might be deprecated if using the array versions above
259
- // Depending on attachFiles implementation
260
- // Add the captured console messages
261
293
  stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
262
294
  stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
295
+ // --- UPDATED THESE LINES from testSpecificData ---
296
+ ...testSpecificData,
263
297
  };
264
298
  try {
265
- // Pass this.options which should contain the resolved outputDir
266
299
  (0, attachment_utils_1.attachFiles)(testIdForFiles, result, pulseResult, this.options);
267
300
  }
268
301
  catch (attachError) {
269
302
  console.error(`Pulse Reporter: Error processing attachments for test ${pulseResult.name} (ID: ${testIdForFiles}): ${attachError.message}`);
270
303
  }
271
- this.results.push(pulseResult);
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;
308
+ }
309
+ }
310
+ else {
311
+ this.results.push(pulseResult);
312
+ }
272
313
  }
273
314
  onError(error) {
274
315
  var _a;
@@ -277,9 +318,22 @@ class PlaywrightPulseReporter {
277
318
  console.error(error.stack);
278
319
  }
279
320
  }
321
+ _getEnvDetails() {
322
+ return {
323
+ host: os.hostname(),
324
+ os: `${os.platform()} ${os.release()}`,
325
+ cpu: {
326
+ model: os.cpus()[0] ? os.cpus()[0].model : "N/A", // Handle cases with no CPU info
327
+ cores: os.cpus().length,
328
+ },
329
+ memory: `${(os.totalmem() / 1024 ** 3).toFixed(2)}GB`, // Total RAM in GB
330
+ node: process.version,
331
+ v8: process.versions.v8,
332
+ cwd: process.cwd(),
333
+ };
334
+ }
280
335
  async _writeShardResults() {
281
336
  if (this.shardIndex === undefined) {
282
- // console.warn("Pulse Reporter: _writeShardResults called unexpectedly in main process. Skipping.");
283
337
  return;
284
338
  }
285
339
  const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${this.shardIndex}.json`);
@@ -291,29 +345,38 @@ class PlaywrightPulseReporter {
291
345
  }
292
346
  }
293
347
  async _mergeShardResults(finalRunData) {
294
- let allResults = [];
348
+ let allShardProcessedResults = [];
295
349
  const totalShards = this.config.shard ? this.config.shard.total : 1;
296
350
  for (let i = 0; i < totalShards; i++) {
297
351
  const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
298
352
  try {
299
353
  const content = await fs.readFile(tempFilePath, "utf-8");
300
354
  const shardResults = JSON.parse(content);
301
- shardResults.forEach((r) => (r.runId = finalRunData.id));
302
- allResults = allResults.concat(shardResults);
355
+ allShardProcessedResults =
356
+ allShardProcessedResults.concat(shardResults);
303
357
  }
304
358
  catch (error) {
305
359
  if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
306
- console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}.`);
360
+ console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}. This might be normal if a shard had no tests or failed early.`);
307
361
  }
308
362
  else {
309
363
  console.error(`Pulse Reporter: Could not read/parse results from shard ${i} (${tempFilePath}). Error:`, error);
310
364
  }
311
365
  }
312
366
  }
313
- finalRunData.passed = allResults.filter((r) => r.status === "passed").length;
314
- finalRunData.failed = allResults.filter((r) => r.status === "failed").length;
315
- finalRunData.skipped = allResults.filter((r) => r.status === "skipped").length;
316
- finalRunData.totalTests = allResults.length;
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());
375
+ finalResultsList.forEach((r) => (r.runId = finalRunData.id));
376
+ finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
377
+ finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
378
+ finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
379
+ finalRunData.totalTests = finalResultsList.length;
317
380
  const reviveDates = (key, value) => {
318
381
  const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
319
382
  if (typeof value === "string" && isoDateRegex.test(value)) {
@@ -322,10 +385,10 @@ class PlaywrightPulseReporter {
322
385
  }
323
386
  return value;
324
387
  };
325
- const finalParsedResults = JSON.parse(JSON.stringify(allResults), reviveDates);
388
+ const properlyTypedResults = JSON.parse(JSON.stringify(finalResultsList), reviveDates);
326
389
  return {
327
390
  run: finalRunData,
328
- results: finalParsedResults,
391
+ results: properlyTypedResults,
329
392
  metadata: { generatedAt: new Date().toISOString() },
330
393
  };
331
394
  }
@@ -339,12 +402,11 @@ class PlaywrightPulseReporter {
339
402
  }
340
403
  catch (error) {
341
404
  if ((error === null || error === void 0 ? void 0 : error.code) !== "ENOENT") {
342
- console.error("Pulse Reporter: Error cleaning up temporary files:", error);
405
+ console.warn("Pulse Reporter: Warning during cleanup of temporary files:", error.message);
343
406
  }
344
407
  }
345
408
  }
346
409
  async _ensureDirExists(dirPath) {
347
- // Removed 'clean' parameter as it was unused
348
410
  try {
349
411
  await fs.mkdir(dirPath, { recursive: true });
350
412
  }
@@ -356,15 +418,16 @@ class PlaywrightPulseReporter {
356
418
  }
357
419
  }
358
420
  async onEnd(result) {
359
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
421
+ var _a, _b, _c;
360
422
  if (this.shardIndex !== undefined) {
361
423
  await this._writeShardResults();
362
424
  return;
363
425
  }
364
426
  const runEndTime = Date.now();
365
427
  const duration = runEndTime - this.runStartTime;
366
- // Consider making the UUID part truly random for each run if this ID needs to be globally unique over time
367
- const runId = `run-${this.runStartTime}-${(0, crypto_1.randomUUID)()}`;
428
+ const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`; // Need not to change
429
+ // --- CALLING _getEnvDetails HERE ---
430
+ const environmentDetails = this._getEnvDetails();
368
431
  const runData = {
369
432
  id: runId,
370
433
  timestamp: new Date(this.runStartTime),
@@ -373,10 +436,16 @@ class PlaywrightPulseReporter {
373
436
  failed: 0,
374
437
  skipped: 0,
375
438
  duration,
439
+ // --- ADDED environmentDetails HERE ---
440
+ environment: environmentDetails,
376
441
  };
377
- let finalReport;
442
+ let finalReport = undefined; // Initialize as undefined
378
443
  if (this.isSharded) {
379
444
  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
+ }
380
449
  }
381
450
  else {
382
451
  this.results.forEach((r) => (r.runId = runId));
@@ -384,42 +453,84 @@ class PlaywrightPulseReporter {
384
453
  runData.failed = this.results.filter((r) => r.status === "failed").length;
385
454
  runData.skipped = this.results.filter((r) => r.status === "skipped").length;
386
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);
387
465
  finalReport = {
388
466
  run: runData,
389
- results: this.results,
467
+ results: properlyTypedResults,
390
468
  metadata: { generatedAt: new Date().toISOString() },
391
469
  };
392
470
  }
393
- // This block seems redundant as finalReport is already assigned above.
394
- // if (this.isSharded) {
395
- // finalReport = await this._mergeShardResults(runData);
396
- // } else {
397
- // this.results.forEach((r) => (r.runId = runId));
398
- // runData.passed = this.results.filter((r) => r.status === "passed").length;
399
- // runData.failed = this.results.filter((r) => r.status === "failed").length;
400
- // runData.skipped = this.results.filter((r) => r.status === "skipped").length;
401
- // runData.totalTests = this.results.length;
402
- // finalReport = {
403
- // run: runData, results: this.results,
404
- // metadata: { generatedAt: new Date().toISOString() },
405
- // };
406
- // }
407
- const finalRunStatus = ((_b = (_a = finalReport.run) === null || _a === void 0 ? void 0 : _a.failed) !== null && _b !== void 0 ? _b : 0 > 0)
471
+ if (!finalReport) {
472
+ 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);
503
+ try {
504
+ 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.`);
507
+ }
508
+ catch (writeError) {
509
+ console.error(`PlaywrightPulseReporter: Failed to write error report: ${writeError.message}`);
510
+ }
511
+ return;
512
+ }
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
408
515
  ? "failed"
409
- : ((_c = finalReport.run) === null || _c === void 0 ? void 0 : _c.totalTests) === 0
410
- ? "no tests"
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"
411
520
  : "passed";
412
521
  const summary = `
413
522
  PlaywrightPulseReporter: Run Finished
414
523
  -----------------------------------------
415
524
  Overall Status: ${finalRunStatus.toUpperCase()}
416
- Total Tests: ${(_e = (_d = finalReport.run) === null || _d === void 0 ? void 0 : _d.totalTests) !== null && _e !== void 0 ? _e : "N/A"}
417
- Passed: ${(_g = (_f = finalReport.run) === null || _f === void 0 ? void 0 : _f.passed) !== null && _g !== void 0 ? _g : "N/A"}
418
- Failed: ${(_j = (_h = finalReport.run) === null || _h === void 0 ? void 0 : _h.failed) !== null && _j !== void 0 ? _j : "N/A"}
419
- Skipped: ${(_l = (_k = finalReport.run) === null || _k === void 0 ? void 0 : _k.skipped) !== null && _l !== void 0 ? _l : "N/A"}
420
- Duration: ${(duration / 1000).toFixed(2)}s
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
421
530
  -----------------------------------------`;
422
- console.log(summary);
531
+ if (this.printsToStdio()) {
532
+ console.log(summary);
533
+ }
423
534
  const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
424
535
  try {
425
536
  await this._ensureDirExists(this.outputDir);
@@ -430,7 +541,9 @@ PlaywrightPulseReporter: Run Finished
430
541
  return value.toString();
431
542
  return value;
432
543
  }, 2));
433
- console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
544
+ if (this.printsToStdio()) {
545
+ console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
546
+ }
434
547
  }
435
548
  catch (error) {
436
549
  console.error(`Pulse Reporter: Failed to write final JSON report to ${finalOutputPath}. Error: ${error.message}`);
@@ -36,6 +36,10 @@ export interface TestResult {
36
36
  tracePath?: string;
37
37
  stdout?: string[];
38
38
  stderr?: string[];
39
+ workerId?: number;
40
+ totalWorkers?: number;
41
+ configFile?: string;
42
+ metadata?: string;
39
43
  }
40
44
  export interface TestRun {
41
45
  id: string;
@@ -45,6 +49,7 @@ export interface TestRun {
45
49
  failed: number;
46
50
  skipped: number;
47
51
  duration: number;
52
+ environment?: EnvDetails;
48
53
  }
49
54
  export interface TrendDataPoint {
50
55
  date: string;
@@ -63,3 +68,15 @@ export interface PlaywrightPulseReporterOptions {
63
68
  outputDir?: string;
64
69
  base64Images?: boolean;
65
70
  }
71
+ export interface EnvDetails {
72
+ host: string;
73
+ os: string;
74
+ cpu: {
75
+ model: string;
76
+ cores: number;
77
+ };
78
+ memory: string;
79
+ node: string;
80
+ v8: string;
81
+ cwd: string;
82
+ }