@arghajit/dummy 0.1.2-beta-11 → 0.1.3

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,66 +1,51 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.PlaywrightPulseReporter = void 0;
4
- const fs = {
5
- promises: {
6
- writeFile: async (path, data) => {
7
- console.log(`Would write to ${path}`);
8
- },
9
- readFile: async (path, encoding) => {
10
- console.log(`Would read from ${path}`);
11
- return "{}";
12
- },
13
- readdir: async (path) => {
14
- console.log(`Would read directory ${path}`);
15
- return [];
16
- },
17
- mkdir: async (path, options) => {
18
- console.log(`Would create directory ${path}`);
19
- },
20
- unlink: async (path) => {
21
- console.log(`Would delete ${path}`);
22
- },
23
- },
24
- };
25
- const path = {
26
- resolve: (...paths) => paths.join("/"),
27
- join: (...paths) => paths.join("/"),
28
- relative: (from, to) => to,
29
- dirname: (p) => p.split("/").slice(0, -1).join("/"),
30
- basename: (p, ext) => {
31
- const base = p.split("/").pop() || "";
32
- return ext ? base.replace(ext, "") : base;
33
- },
34
- extname: (p) => {
35
- const parts = p.split(".");
36
- return parts.length > 1 ? "." + parts.pop() : "";
37
- },
38
- };
39
- const randomUUID = () => "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
40
- const r = (Math.random() * 16) | 0;
41
- const v = c == "x" ? r : (r & 0x3) | 0x8;
42
- return v.toString(16);
43
- });
44
- const UAParser = class {
45
- getBrowser() {
46
- return { name: "unknown", version: "unknown" };
37
+ const fs = __importStar(require("fs/promises"));
38
+ const path = __importStar(require("path"));
39
+ const crypto_1 = require("crypto");
40
+ const ua_parser_js_1 = require("ua-parser-js");
41
+ const os = __importStar(require("os"));
42
+ const convertStatus = (status, testCase) => {
43
+ if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
44
+ return "failed";
45
+ }
46
+ if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "skipped") {
47
+ return "skipped";
47
48
  }
48
- };
49
- const os = {
50
- type: () => "Linux",
51
- release: () => "5.4.0",
52
- arch: () => "x64",
53
- hostname: () => "localhost",
54
- platform: () => "linux",
55
- cpus: () => [{ model: "Intel" }],
56
- totalmem: () => 8589934592,
57
- };
58
- const process = {
59
- cwd: () => "/current/working/directory",
60
- version: "v18.0.0",
61
- versions: { v8: "10.0.0" },
62
- };
63
- function convertStatus(status) {
64
49
  switch (status) {
65
50
  case "passed":
66
51
  return "passed";
@@ -69,429 +54,525 @@ function convertStatus(status) {
69
54
  case "interrupted":
70
55
  return "failed";
71
56
  case "skipped":
72
- return "skipped";
73
57
  default:
74
- return "failed";
58
+ return "skipped";
75
59
  }
76
- }
77
- function attachFiles(testId, pwResult, pulseResult, config) {
78
- const baseReportDir = config.outputDir || "pulse-report";
79
- const attachmentsBaseDir = path.resolve(baseReportDir, "attachments");
80
- const attachmentsSubFolder = `${testId}-retry-${pwResult.retry || 0}`.replace(/[^a-zA-Z0-9_-]/g, "_");
81
- pulseResult.screenshots = [];
82
- pulseResult.videoPath = [];
83
- pulseResult.attachments = [];
84
- if (!pwResult.attachments)
85
- return;
86
- pwResult.attachments.forEach((attachment) => {
87
- var _a, _b, _c;
88
- const { contentType, name, path: attachmentPath } = attachment;
89
- if (!attachmentPath)
90
- return;
91
- const relativePath = path.join("attachments", attachmentsSubFolder, path.basename(attachmentPath));
92
- if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("image/")) {
93
- (_a = pulseResult.screenshots) === null || _a === void 0 ? void 0 : _a.push(relativePath);
94
- }
95
- else if (name === "video" || (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("video/"))) {
96
- (_b = pulseResult.videoPath) === null || _b === void 0 ? void 0 : _b.push(relativePath);
97
- }
98
- else if (name === "trace" || contentType === "application/zip") {
99
- pulseResult.tracePath = relativePath;
100
- }
101
- else {
102
- (_c = pulseResult.attachments) === null || _c === void 0 ? void 0 : _c.push({
103
- name: attachment.name,
104
- path: relativePath,
105
- contentType: attachment.contentType,
106
- });
107
- }
108
- });
109
- }
60
+ };
61
+ const TEMP_SHARD_FILE_PREFIX = ".pulse-shard-results-";
62
+ const ATTACHMENTS_SUBDIR = "attachments";
63
+ const INDIVIDUAL_REPORTS_SUBDIR = "pulse-results";
110
64
  class PlaywrightPulseReporter {
111
65
  constructor(options = {}) {
112
- this.testResults = [];
113
- this.currentRunId = "";
114
- this.outputDir = "";
115
- this.totalWorkers = 0;
66
+ var _a, _b, _c;
67
+ this.results = [];
68
+ this.baseOutputFile = "playwright-pulse-report.json";
69
+ this.isSharded = false;
70
+ this.shardIndex = undefined;
116
71
  this.options = options;
72
+ this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
73
+ this.outputDir = (_b = options.outputDir) !== null && _b !== void 0 ? _b : "pulse-report";
74
+ this.attachmentsDir = path.join(this.outputDir, ATTACHMENTS_SUBDIR);
75
+ this.resetOnEachRun = (_c = options.resetOnEachRun) !== null && _c !== void 0 ? _c : true;
117
76
  }
118
77
  printsToStdio() {
119
- return false;
78
+ return this.shardIndex === undefined || this.shardIndex === 0;
120
79
  }
121
80
  onBegin(config, suite) {
122
81
  var _a;
123
- this.currentRunId = randomUUID();
124
- this.totalWorkers = config.workers;
125
- this.shardIndex = (_a = config.shard) === null || _a === void 0 ? void 0 : _a.current;
126
- this.outputDir = path.resolve(this.options.outputDir || config.outputDir || "pulse-report");
127
- if (this.options.open === undefined) {
128
- this.options.open = false;
129
- }
130
- if (this.options.base64Images === undefined) {
131
- this.options.base64Images = false;
82
+ this.config = config;
83
+ this.suite = suite;
84
+ this.runStartTime = Date.now();
85
+ const configDir = this.config.rootDir;
86
+ const configFileDir = this.config.configFile
87
+ ? path.dirname(this.config.configFile)
88
+ : configDir;
89
+ this.outputDir = path.resolve(configFileDir, (_a = this.options.outputDir) !== null && _a !== void 0 ? _a : "pulse-report");
90
+ this.attachmentsDir = path.resolve(this.outputDir, ATTACHMENTS_SUBDIR);
91
+ this.options.outputDir = this.outputDir;
92
+ const totalShards = this.config.shard ? this.config.shard.total : 1;
93
+ this.isSharded = totalShards > 1;
94
+ this.shardIndex = this.config.shard
95
+ ? this.config.shard.current - 1
96
+ : undefined;
97
+ this._ensureDirExists(this.outputDir)
98
+ .then(() => {
99
+ if (this.printsToStdio()) {
100
+ console.log(`PlaywrightPulseReporter: Starting test run with ${suite.allTests().length} tests${this.isSharded ? ` across ${totalShards} shards` : ""}. Pulse outputting to ${this.outputDir}`);
101
+ if (this.shardIndex === undefined ||
102
+ (this.isSharded && this.shardIndex === 0)) {
103
+ return this._cleanupTemporaryFiles();
104
+ }
105
+ }
106
+ })
107
+ .catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
108
+ }
109
+ onTestBegin(test) {
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();
115
+ const projectConfig = project === null || project === void 0 ? void 0 : project.use;
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
+ : "";
124
+ const osName = result.os.name ? ` on ${result.os.name}` : "";
125
+ const osVersion = result.os.version
126
+ ? ` ${result.os.version.split(".")[0]}`
127
+ : "";
128
+ const deviceType = result.device.type;
129
+ let finalString;
130
+ if (browserName === undefined) {
131
+ browserName = configuredBrowserType;
132
+ finalString = `${browserName}`;
132
133
  }
133
- console.log(`Pulse Reporter: Starting test run with ID: ${this.currentRunId}`);
134
- if (this.shardIndex !== undefined) {
135
- console.log(`Pulse Reporter: Running shard ${this.shardIndex}`);
134
+ else {
135
+ if (deviceType === "mobile" || deviceType === "tablet") {
136
+ if ((_c = result.os.name) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes("android")) {
137
+ if (browserName.toLowerCase().includes("chrome"))
138
+ browserName = "Chrome Mobile";
139
+ else if (browserName.toLowerCase().includes("firefox"))
140
+ browserName = "Firefox Mobile";
141
+ else if (result.engine.name === "Blink" && !result.browser.name)
142
+ browserName = "Android WebView";
143
+ else if (browserName &&
144
+ !browserName.toLowerCase().includes("mobile")) {
145
+ // Keep it as is
146
+ }
147
+ else {
148
+ browserName = "Android Browser";
149
+ }
150
+ }
151
+ else if ((_d = result.os.name) === null || _d === void 0 ? void 0 : _d.toLowerCase().includes("ios")) {
152
+ browserName = "Mobile Safari";
153
+ }
154
+ }
155
+ else if (browserName === "Electron") {
156
+ browserName = "Electron App";
157
+ }
158
+ finalString = `${browserName}${browserVersion}${osName}${osVersion}`;
136
159
  }
160
+ return finalString.trim();
137
161
  }
138
- getBrowserDetails(project) {
139
- var _a, _b;
140
- if (!project)
141
- return "unknown";
142
- const projectName = project.name || "unknown";
143
- const browserName = ((_a = project.use) === null || _a === void 0 ? void 0 : _a.browserName) || "unknown";
144
- const channel = (_b = project.use) === null || _b === void 0 ? void 0 : _b.channel;
145
- if (channel) {
146
- return `${browserName}-${channel}`;
162
+ async processStep(step, testId, browserDetails, testCase) {
163
+ var _a, _b, _c, _d;
164
+ let stepStatus = "passed";
165
+ let errorMessage = ((_a = step.error) === null || _a === void 0 ? void 0 : _a.message) || undefined;
166
+ if ((_c = (_b = step.error) === null || _b === void 0 ? void 0 : _b.message) === null || _c === void 0 ? void 0 : _c.startsWith("Test is skipped:")) {
167
+ stepStatus = "skipped";
168
+ }
169
+ else {
170
+ stepStatus = convertStatus(step.error ? "failed" : "passed", testCase);
171
+ }
172
+ const duration = step.duration;
173
+ const startTime = new Date(step.startTime);
174
+ const endTime = new Date(startTime.getTime() + Math.max(0, duration));
175
+ let codeLocation = "";
176
+ if (step.location) {
177
+ codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
147
178
  }
148
- return browserName;
149
- }
150
- async processStep(step) {
151
- const stepStatus = convertStatus(step.error ? "failed" : "passed");
152
- const codeLocation = step.location
153
- ? `${step.location.file}:${step.location.line}:${step.location.column}`
154
- : undefined;
155
179
  return {
180
+ id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}`,
156
181
  title: step.title,
157
- category: step.category,
158
- startTime: step.startTime,
159
- duration: step.duration,
160
- error: step.error
161
- ? {
162
- message: step.error.message || "",
163
- stack: step.error.stack,
164
- snippet: step.error.snippet,
165
- }
166
- : undefined,
167
- count: step.count || 0,
168
- location: codeLocation,
169
182
  status: stepStatus,
183
+ duration: duration,
184
+ startTime: startTime,
185
+ endTime: endTime,
186
+ browser: browserDetails,
187
+ errorMessage: errorMessage,
188
+ stackTrace: ((_d = step.error) === null || _d === void 0 ? void 0 : _d.stack) || undefined,
189
+ codeLocation: codeLocation || undefined,
190
+ isHook: step.category === "hook",
191
+ hookType: step.category === "hook"
192
+ ? step.title.toLowerCase().includes("before")
193
+ ? "before"
194
+ : "after"
195
+ : undefined,
196
+ steps: [],
170
197
  };
171
198
  }
172
199
  async onTestEnd(test, result) {
173
- var _a, _b, _c, _d, _e, _f, _g, _h;
200
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
201
+ const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
202
+ const browserDetails = this.getBrowserDetails(test);
203
+ const testStatus = convertStatus(result.status, test);
204
+ const startTime = new Date(result.startTime);
205
+ const endTime = new Date(startTime.getTime() + result.duration);
174
206
  const processAllSteps = async (steps) => {
175
- const processedSteps = [];
207
+ let processed = [];
176
208
  for (const step of steps) {
177
- const processedStep = await this.processStep(step);
178
- processedSteps.push(processedStep);
209
+ const processedStep = await this.processStep(step, test.id, browserDetails, test);
210
+ processed.push(processedStep);
179
211
  if (step.steps && step.steps.length > 0) {
180
- const nestedSteps = await processAllSteps(step.steps);
181
- processedSteps.push(...nestedSteps);
212
+ processedStep.steps = await processAllSteps(step.steps);
182
213
  }
183
214
  }
184
- return processedSteps;
215
+ return processed;
185
216
  };
186
- const project = (_b = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project) === null || _b === void 0 ? void 0 : _b.call(_a);
187
- const relativePath = path.relative(process.cwd(), ((_c = test.location) === null || _c === void 0 ? void 0 : _c.file) || "");
188
- const testStatus = convertStatus(result.status);
189
- const startTime = result.startTime;
190
- const endTime = new Date(startTime.getTime() + result.duration);
191
- const stdoutMessages = result.stdout
192
- .filter((msg) => msg.trim().length > 0)
193
- .map((msg) => msg.toString());
194
- const stderrMessages = result.stderr
195
- .filter((msg) => msg.trim().length > 0)
196
- .map((msg) => msg.toString());
197
- const mappedWorkerId = result.workerIndex !== undefined ? result.workerIndex + 1 : undefined;
217
+ let codeSnippet = undefined;
218
+ try {
219
+ 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)) {
220
+ const relativePath = path.relative(this.config.rootDir, test.location.file);
221
+ codeSnippet = `Test defined at: ${relativePath}:${test.location.line}:${test.location.column}`;
222
+ }
223
+ }
224
+ catch (e) {
225
+ console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
226
+ }
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());
229
+ const maxWorkers = this.config.workers;
230
+ let mappedWorkerId = result.workerIndex === -1
231
+ ? -1
232
+ : (result.workerIndex % (maxWorkers > 0 ? maxWorkers : 1)) + 1;
198
233
  const testSpecificData = {
199
234
  workerId: mappedWorkerId,
200
- totalWorkers: this.totalWorkers,
201
- configFile: relativePath,
202
- metadata: JSON.stringify({ project: project === null || project === void 0 ? void 0 : project.name }),
235
+ totalWorkers: maxWorkers,
236
+ configFile: this.config.configFile,
237
+ metadata: this.config.metadata
238
+ ? JSON.stringify(this.config.metadata)
239
+ : undefined,
203
240
  };
204
241
  const pulseResult = {
205
242
  id: test.id,
206
- runId: this.currentRunId,
207
- name: `${(project === null || project === void 0 ? void 0 : project.name) || "unknown"} > ${((_d = test.parent) === null || _d === void 0 ? void 0 : _d.title) || ""} > ${test.title}`,
208
- suiteName: project === null || project === void 0 ? void 0 : project.name,
243
+ runId: "TBD",
244
+ name: test.titlePath().join(" > "),
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",
209
246
  status: testStatus,
210
247
  duration: result.duration,
211
248
  startTime: startTime,
212
249
  endTime: endTime,
213
- retry: result.retry,
214
- steps: await processAllSteps(result.steps),
215
- errorMessage: (_e = result.error) === null || _e === void 0 ? void 0 : _e.message,
216
- stackTrace: (_f = result.error) === null || _f === void 0 ? void 0 : _f.stack,
217
- snippet: (_g = result.error) === null || _g === void 0 ? void 0 : _g.snippet,
218
- codeSnippet: (_h = result.error) === null || _h === void 0 ? void 0 : _h.snippet,
219
- tags: test.tags,
220
- browser: this.getBrowserDetails(project),
250
+ browser: browserDetails,
251
+ retries: result.retry,
252
+ steps: ((_f = result.steps) === null || _f === void 0 ? void 0 : _f.length) ? await processAllSteps(result.steps) : [],
253
+ errorMessage: (_g = result.error) === null || _g === void 0 ? void 0 : _g.message,
254
+ stackTrace: (_h = result.error) === null || _h === void 0 ? void 0 : _h.stack,
255
+ snippet: (_j = result.error) === null || _j === void 0 ? void 0 : _j.snippet,
256
+ codeSnippet: codeSnippet,
257
+ tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
221
258
  screenshots: [],
222
259
  videoPath: [],
260
+ tracePath: undefined,
223
261
  attachments: [],
224
- stdout: stdoutMessages,
225
- stderr: stderrMessages,
262
+ stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
263
+ stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
226
264
  ...testSpecificData,
227
265
  };
228
- attachFiles(test.id, result, pulseResult, this.options);
229
- this.testResults.push(pulseResult);
230
- }
231
- _getFinalizedResults(allAttempts) {
232
- const groupedResults = new Map();
233
- for (const attempt of allAttempts) {
234
- const baseTestId = attempt.id;
235
- if (!groupedResults.has(baseTestId)) {
236
- groupedResults.set(baseTestId, []);
237
- }
238
- groupedResults.get(baseTestId).push(attempt);
239
- }
240
- const finalResults = [];
241
- for (const [baseId, attempts] of groupedResults.entries()) {
242
- attempts.sort((a, b) => a.retry - b.retry);
243
- let overallStatus = "passed";
244
- const statuses = attempts.map((a) => a.status);
245
- const hasFailures = statuses.some((s) => s === "failed");
246
- const hasPasses = statuses.some((s) => s === "passed");
247
- if (hasFailures && hasPasses) {
248
- overallStatus = "flaky";
249
- }
250
- else if (hasFailures) {
251
- overallStatus = "failed";
252
- }
253
- else if (statuses.some((s) => s === "skipped")) {
254
- overallStatus = "skipped";
266
+ for (const [index, attachment] of result.attachments.entries()) {
267
+ if (!attachment.path)
268
+ continue;
269
+ try {
270
+ const testSubfolder = test.id.replace(/[^a-zA-Z0-9_-]/g, "_");
271
+ const safeAttachmentName = path
272
+ .basename(attachment.path)
273
+ .replace(/[^a-zA-Z0-9_.-]/g, "_");
274
+ const uniqueFileName = `${index}-${Date.now()}-${safeAttachmentName}`;
275
+ const relativeDestPath = path.join(ATTACHMENTS_SUBDIR, testSubfolder, uniqueFileName);
276
+ const absoluteDestPath = path.join(this.outputDir, relativeDestPath);
277
+ await this._ensureDirExists(path.dirname(absoluteDestPath));
278
+ await fs.copyFile(attachment.path, absoluteDestPath);
279
+ if (attachment.contentType.startsWith("image/")) {
280
+ (_k = pulseResult.screenshots) === null || _k === void 0 ? void 0 : _k.push(relativeDestPath);
281
+ }
282
+ else if (attachment.contentType.startsWith("video/")) {
283
+ (_l = pulseResult.videoPath) === null || _l === void 0 ? void 0 : _l.push(relativeDestPath);
284
+ }
285
+ else if (attachment.name === "trace") {
286
+ pulseResult.tracePath = relativeDestPath;
287
+ }
288
+ else {
289
+ (_m = pulseResult.attachments) === null || _m === void 0 ? void 0 : _m.push({
290
+ name: attachment.name,
291
+ path: relativeDestPath,
292
+ contentType: attachment.contentType,
293
+ });
294
+ }
255
295
  }
256
- else {
257
- overallStatus = "passed";
296
+ catch (err) {
297
+ console.error(`Pulse Reporter: Failed to process attachment "${attachment.name}" for test ${pulseResult.name}. Error: ${err.message}`);
258
298
  }
259
- const startTimes = attempts.map((a) => a.startTime.getTime());
260
- const endTimes = attempts.map((a) => a.endTime.getTime());
261
- const overallDuration = Math.max(...endTimes) - Math.min(...startTimes);
262
- const baseAttempt = attempts[0];
263
- finalResults.push({
264
- id: baseId,
265
- name: baseAttempt.name,
266
- suiteName: baseAttempt.suiteName,
267
- status: overallStatus,
268
- duration: overallDuration,
269
- startTime: new Date(Math.min(...startTimes)),
270
- endTime: new Date(Math.max(...endTimes)),
271
- browser: baseAttempt.browser,
272
- tags: baseAttempt.tags,
273
- results: attempts,
274
- });
275
299
  }
276
- return finalResults;
300
+ this.results.push(pulseResult);
277
301
  }
278
- _getSummaryStats(consolidatedResults) {
279
- let passed = 0;
280
- let failed = 0;
281
- let skipped = 0;
282
- let flaky = 0;
283
- for (const result of consolidatedResults) {
284
- switch (result.status) {
285
- case "passed":
286
- passed++;
287
- break;
288
- case "failed":
289
- failed++;
290
- break;
291
- case "skipped":
292
- skipped++;
293
- break;
294
- case "flaky":
295
- flaky++;
296
- break;
302
+ _getFinalizedResults(allResults) {
303
+ const finalResultsMap = new Map();
304
+ for (const result of allResults) {
305
+ const existing = finalResultsMap.get(result.id);
306
+ // Keep the result with the highest retry attempt for each test ID
307
+ if (!existing || result.retries >= existing.retries) {
308
+ finalResultsMap.set(result.id, result);
297
309
  }
298
310
  }
299
- return {
300
- passed,
301
- failed,
302
- skipped,
303
- flaky,
304
- totalTests: consolidatedResults.length,
305
- };
306
- }
307
- _getSummaryStatsFromAttempts(attempts) {
308
- let passed = 0;
309
- let failed = 0;
310
- let skipped = 0;
311
- let flaky = 0;
312
- const groupedByTest = new Map();
313
- for (const attempt of attempts) {
314
- const baseId = attempt.id.replace(/-\d+$/, "");
315
- if (!groupedByTest.has(baseId)) {
316
- groupedByTest.set(baseId, []);
317
- }
318
- groupedByTest.get(baseId).push(attempt);
319
- }
320
- for (const attempt of attempts) {
321
- const baseId = attempt.id.replace(/-\d+$/, "");
322
- const testAttempts = groupedByTest.get(baseId);
323
- const hasFailures = testAttempts.some((a) => a.status === "failed");
324
- const hasPasses = testAttempts.some((a) => a.status === "passed");
325
- if (hasFailures && hasPasses) {
326
- flaky++;
327
- }
328
- else {
329
- switch (attempt.status) {
330
- case "passed":
331
- passed++;
332
- break;
333
- case "failed":
334
- failed++;
335
- break;
336
- case "skipped":
337
- skipped++;
338
- break;
339
- }
340
- }
341
- }
342
- return {
343
- passed,
344
- failed,
345
- skipped,
346
- flaky,
347
- totalTests: attempts.length,
348
- };
311
+ return Array.from(finalResultsMap.values());
349
312
  }
350
313
  onError(error) {
351
- console.error("Pulse Reporter: Error occurred:", error);
314
+ var _a;
315
+ console.error(`PlaywrightPulseReporter: Error encountered (Shard: ${(_a = this.shardIndex) !== null && _a !== void 0 ? _a : "Main"}):`, (error === null || error === void 0 ? void 0 : error.message) || error);
316
+ if (error === null || error === void 0 ? void 0 : error.stack) {
317
+ console.error(error.stack);
318
+ }
352
319
  }
353
320
  _getEnvDetails() {
354
- const parser = new UAParser();
355
321
  return {
356
- os: `${os.type()} ${os.release()}`,
357
- browser: parser.getBrowser(),
358
- cpu: os.arch(),
322
+ host: os.hostname(),
323
+ os: `${os.platform()} ${os.release()}`,
324
+ cpu: {
325
+ model: os.cpus()[0] ? os.cpus()[0].model : "N/A",
326
+ cores: os.cpus().length,
327
+ },
328
+ memory: `${(os.totalmem() / 1024 ** 3).toFixed(2)}GB`,
329
+ node: process.version,
330
+ v8: process.versions.v8,
331
+ cwd: process.cwd(),
359
332
  };
360
333
  }
361
- async _writeShardResults(individualResults) {
362
- const tempFilePath = path.join(this.outputDir, `shard-${this.shardIndex || 0}-results.json`);
334
+ async _writeShardResults() {
335
+ if (this.shardIndex === undefined) {
336
+ return;
337
+ }
338
+ const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${this.shardIndex}.json`);
363
339
  try {
364
- await this._ensureDirExists(path.dirname(tempFilePath));
365
- await fs.promises.writeFile(tempFilePath, JSON.stringify(individualResults, null, 2));
340
+ await fs.writeFile(tempFilePath, JSON.stringify(this.results, (key, value) => (value instanceof Date ? value.toISOString() : value), 2));
366
341
  }
367
342
  catch (error) {
368
- console.error("Pulse Reporter: Error writing shard results:", error);
343
+ console.error(`Pulse Reporter: Shard ${this.shardIndex} failed to write temporary results to ${tempFilePath}`, error);
369
344
  }
370
345
  }
371
- async _mergeShardResults(allShardResults) {
372
- try {
373
- const allAttempts = allShardResults.flat();
374
- const reviveDates = (key, value) => {
375
- if (typeof value === "string" &&
376
- /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) {
377
- return new Date(value);
346
+ async _mergeShardResults(finalRunData) {
347
+ let allShardProcessedResults = [];
348
+ const totalShards = this.config.shard ? this.config.shard.total : 1;
349
+ for (let i = 0; i < totalShards; i++) {
350
+ const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
351
+ try {
352
+ const content = await fs.readFile(tempFilePath, "utf-8");
353
+ const shardResults = JSON.parse(content);
354
+ allShardProcessedResults =
355
+ allShardProcessedResults.concat(shardResults);
356
+ }
357
+ catch (error) {
358
+ if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
359
+ console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}. This might be normal if a shard had no tests or failed early.`);
378
360
  }
379
- return value;
380
- };
381
- const properlyTypedResults = JSON.parse(JSON.stringify(allAttempts), reviveDates);
382
- return properlyTypedResults;
383
- }
384
- catch (error) {
385
- console.error("Pulse Reporter: Error merging shard results:", error);
386
- return [];
361
+ else {
362
+ console.error(`Pulse Reporter: Could not read/parse results from shard ${i} (${tempFilePath}). Error:`, error);
363
+ }
364
+ }
387
365
  }
366
+ const finalResultsList = this._getFinalizedResults(allShardProcessedResults);
367
+ finalResultsList.forEach((r) => (r.runId = finalRunData.id));
368
+ finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
369
+ finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
370
+ finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
371
+ finalRunData.totalTests = finalResultsList.length;
372
+ const reviveDates = (key, value) => {
373
+ const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
374
+ if (typeof value === "string" && isoDateRegex.test(value)) {
375
+ const date = new Date(value);
376
+ return !isNaN(date.getTime()) ? date : value;
377
+ }
378
+ return value;
379
+ };
380
+ const properlyTypedResults = JSON.parse(JSON.stringify(finalResultsList), reviveDates);
381
+ return {
382
+ run: finalRunData,
383
+ results: properlyTypedResults,
384
+ metadata: { generatedAt: new Date().toISOString() },
385
+ };
388
386
  }
389
387
  async _cleanupTemporaryFiles() {
390
388
  try {
391
- const files = await fs.promises.readdir(this.outputDir);
392
- const shardFiles = files.filter((file) => file.startsWith("shard-") && file.endsWith("-results.json"));
393
- for (const file of shardFiles) {
394
- await fs.promises.unlink(path.join(this.outputDir, file));
389
+ const files = await fs.readdir(this.outputDir);
390
+ const tempFiles = files.filter((f) => f.startsWith(TEMP_SHARD_FILE_PREFIX));
391
+ if (tempFiles.length > 0) {
392
+ await Promise.all(tempFiles.map((f) => fs.unlink(path.join(this.outputDir, f))));
395
393
  }
396
394
  }
397
395
  catch (error) {
398
- console.error("Pulse Reporter: Error cleaning up temporary files:", error);
396
+ if ((error === null || error === void 0 ? void 0 : error.code) !== "ENOENT") {
397
+ console.warn("Pulse Reporter: Warning during cleanup of temporary files:", error.message);
398
+ }
399
399
  }
400
400
  }
401
401
  async _ensureDirExists(dirPath) {
402
402
  try {
403
- await fs.promises.mkdir(dirPath, { recursive: true });
403
+ await fs.mkdir(dirPath, { recursive: true });
404
404
  }
405
405
  catch (error) {
406
- console.error(`Pulse Reporter: Error creating directory ${dirPath}:`, error);
406
+ if (error.code !== "EEXIST") {
407
+ console.error(`Pulse Reporter: Failed to ensure directory exists: ${dirPath}`, error);
408
+ throw error;
409
+ }
407
410
  }
408
411
  }
409
412
  async onEnd(result) {
410
- try {
411
- const individualResults = this.testResults;
412
- const stats = this._getSummaryStatsFromAttempts(individualResults);
413
- const runData = {
414
- id: this.currentRunId,
415
- startTime: new Date(),
416
- endTime: new Date(),
417
- duration: result.duration,
418
- status: result.status,
419
- environment: this._getEnvDetails(),
420
- };
421
- const finalReport = {
413
+ if (this.shardIndex !== undefined) {
414
+ await this._writeShardResults();
415
+ return;
416
+ }
417
+ // De-duplicate and handle retries here, in a safe, single-threaded context.
418
+ const finalResults = this._getFinalizedResults(this.results);
419
+ const runEndTime = Date.now();
420
+ const duration = runEndTime - this.runStartTime;
421
+ const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`;
422
+ const environmentDetails = this._getEnvDetails();
423
+ const runData = {
424
+ id: runId,
425
+ timestamp: new Date(this.runStartTime),
426
+ // Use the length of the de-duplicated array for all counts
427
+ totalTests: finalResults.length,
428
+ passed: finalResults.filter((r) => r.status === "passed").length,
429
+ failed: finalResults.filter((r) => r.status === "failed").length,
430
+ skipped: finalResults.filter((r) => r.status === "skipped").length,
431
+ duration,
432
+ environment: environmentDetails,
433
+ };
434
+ finalResults.forEach((r) => (r.runId = runId));
435
+ let finalReport = undefined;
436
+ if (this.isSharded) {
437
+ // The _mergeShardResults method will handle its own de-duplication
438
+ finalReport = await this._mergeShardResults(runData);
439
+ }
440
+ else {
441
+ finalReport = {
422
442
  run: runData,
423
- results: individualResults,
424
- summary: stats,
443
+ // Use the de-duplicated results
444
+ results: finalResults,
445
+ metadata: { generatedAt: new Date().toISOString() },
425
446
  };
426
- const jsonReplacer = (key, value) => {
427
- if (value instanceof Date) {
428
- return value.toISOString();
447
+ }
448
+ if (!finalReport) {
449
+ console.error("PlaywrightPulseReporter: CRITICAL - finalReport object was not generated. Cannot create summary.");
450
+ return;
451
+ }
452
+ const jsonReplacer = (key, value) => {
453
+ if (value instanceof Date)
454
+ return value.toISOString();
455
+ if (typeof value === "bigint")
456
+ return value.toString();
457
+ return value;
458
+ };
459
+ if (this.resetOnEachRun) {
460
+ const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
461
+ try {
462
+ await this._ensureDirExists(this.outputDir);
463
+ await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, jsonReplacer, 2));
464
+ if (this.printsToStdio()) {
465
+ console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
429
466
  }
430
- return value;
431
- };
432
- if (this.shardIndex !== undefined) {
433
- await this._writeShardResults(individualResults);
434
- console.log(`Pulse Reporter: Shard ${this.shardIndex} results written.`);
435
467
  }
436
- else {
437
- const pulseResultsDir = path.join(this.outputDir, "pulse-results");
468
+ catch (error) {
469
+ console.error(`Pulse Reporter: Failed to write final JSON report to ${finalOutputPath}. Error: ${error.message}`);
470
+ }
471
+ }
472
+ else {
473
+ // Logic for appending/merging reports
474
+ const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
475
+ const individualReportPath = path.join(pulseResultsDir, `playwright-pulse-report-${Date.now()}.json`);
476
+ try {
438
477
  await this._ensureDirExists(pulseResultsDir);
439
- const individualReportPath = path.join(pulseResultsDir, `${this.currentRunId}-pulse-report.json`);
440
- await fs.promises.writeFile(individualReportPath, JSON.stringify(finalReport, jsonReplacer, 2));
441
- const mergedReport = await this._mergeAllRunReports();
442
- if (mergedReport) {
443
- const finalReportPath = path.join(this.outputDir, "playwright-pulse-report.json");
444
- await fs.promises.writeFile(finalReportPath, JSON.stringify(mergedReport, jsonReplacer, 2));
445
- console.log(`Pulse Reporter: Final report written to ${finalReportPath}`);
478
+ await fs.writeFile(individualReportPath, JSON.stringify(finalReport, jsonReplacer, 2));
479
+ if (this.printsToStdio()) {
480
+ console.log(`PlaywrightPulseReporter: Individual run report for merging written to ${individualReportPath}`);
446
481
  }
482
+ await this._mergeAllRunReports();
483
+ }
484
+ catch (error) {
485
+ console.error(`Pulse Reporter: Failed to write or merge report. Error: ${error.message}`);
447
486
  }
448
487
  }
449
- catch (error) {
450
- console.error("Pulse Reporter: Error in onEnd:", error);
488
+ if (this.isSharded) {
489
+ await this._cleanupTemporaryFiles();
451
490
  }
452
491
  }
453
492
  async _mergeAllRunReports() {
454
- var _a;
493
+ const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
494
+ const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
495
+ let reportFiles;
455
496
  try {
456
- const pulseResultsDir = path.join(this.outputDir, "pulse-results");
457
- const files = await fs.promises.readdir(pulseResultsDir);
458
- const jsonFiles = files.filter((file) => file.endsWith("-pulse-report.json"));
459
- if (jsonFiles.length === 0) {
460
- return null;
497
+ const allFiles = await fs.readdir(pulseResultsDir);
498
+ reportFiles = allFiles.filter((file) => file.startsWith("playwright-pulse-report-") && file.endsWith(".json"));
499
+ }
500
+ catch (error) {
501
+ if (error.code === "ENOENT") {
502
+ if (this.printsToStdio()) {
503
+ console.log(`Pulse Reporter: No individual reports directory found at ${pulseResultsDir}. Skipping merge.`);
504
+ }
505
+ return;
461
506
  }
462
- const allAttempts = [];
463
- let totalDuration = 0;
464
- for (const file of jsonFiles) {
465
- const filePath = path.join(pulseResultsDir, file);
466
- const content = await fs.promises.readFile(filePath, "utf-8");
467
- const report = JSON.parse(content);
468
- if (report.results) {
469
- allAttempts.push(...report.results);
507
+ console.error(`Pulse Reporter: Error reading report directory ${pulseResultsDir}:`, error);
508
+ return;
509
+ }
510
+ if (reportFiles.length === 0) {
511
+ if (this.printsToStdio()) {
512
+ console.log("Pulse Reporter: No matching JSON report files found to merge.");
513
+ }
514
+ return;
515
+ }
516
+ const allResultsFromAllFiles = [];
517
+ let latestTimestamp = new Date(0);
518
+ let lastRunEnvironment = undefined;
519
+ let totalDuration = 0;
520
+ for (const file of reportFiles) {
521
+ const filePath = path.join(pulseResultsDir, file);
522
+ try {
523
+ const content = await fs.readFile(filePath, "utf-8");
524
+ const json = JSON.parse(content);
525
+ if (json.run) {
526
+ const runTimestamp = new Date(json.run.timestamp);
527
+ if (runTimestamp > latestTimestamp) {
528
+ latestTimestamp = runTimestamp;
529
+ lastRunEnvironment = json.run.environment || undefined;
530
+ }
470
531
  }
471
- if ((_a = report.run) === null || _a === void 0 ? void 0 : _a.duration) {
472
- totalDuration += report.run.duration;
532
+ if (json.results) {
533
+ allResultsFromAllFiles.push(...json.results);
473
534
  }
474
535
  }
475
- const stats = this._getSummaryStatsFromAttempts(allAttempts);
476
- const combinedRun = {
477
- id: this.currentRunId,
478
- startTime: new Date(),
479
- endTime: new Date(),
480
- duration: totalDuration,
481
- status: "passed",
482
- environment: this._getEnvDetails(),
483
- };
484
- const finalReport = {
485
- run: combinedRun,
486
- results: allAttempts,
487
- summary: stats,
488
- };
489
- return finalReport;
536
+ catch (err) {
537
+ console.warn(`Pulse Reporter: Could not parse report file ${filePath}. Skipping. Error: ${err.message}`);
538
+ }
490
539
  }
491
- catch (error) {
492
- console.error("Pulse Reporter: Error merging all run reports:", error);
493
- return null;
540
+ // De-duplicate the results from ALL merged files using the helper function
541
+ const finalMergedResults = this._getFinalizedResults(allResultsFromAllFiles);
542
+ // Sum the duration from the final, de-duplicated list of tests
543
+ totalDuration = finalMergedResults.reduce((acc, r) => acc + (r.duration || 0), 0);
544
+ const combinedRun = {
545
+ id: `merged-${Date.now()}`,
546
+ timestamp: latestTimestamp,
547
+ environment: lastRunEnvironment,
548
+ // Recalculate counts based on the truly final, de-duplicated list
549
+ totalTests: finalMergedResults.length,
550
+ passed: finalMergedResults.filter((r) => r.status === "passed").length,
551
+ failed: finalMergedResults.filter((r) => r.status === "failed").length,
552
+ skipped: finalMergedResults.filter((r) => r.status === "skipped").length,
553
+ duration: totalDuration,
554
+ };
555
+ const finalReport = {
556
+ run: combinedRun,
557
+ results: finalMergedResults, // Use the de-duplicated list
558
+ metadata: {
559
+ generatedAt: new Date().toISOString(),
560
+ },
561
+ };
562
+ try {
563
+ await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, (key, value) => {
564
+ if (value instanceof Date)
565
+ return value.toISOString();
566
+ return value;
567
+ }, 2));
568
+ if (this.printsToStdio()) {
569
+ console.log(`PlaywrightPulseReporter: ✅ Merged report with ${finalMergedResults.length} total results saved to ${finalOutputPath}`);
570
+ }
571
+ }
572
+ catch (err) {
573
+ console.error(`Pulse Reporter: Failed to write final merged report to ${finalOutputPath}. Error: ${err.message}`);
494
574
  }
495
575
  }
496
576
  }
497
577
  exports.PlaywrightPulseReporter = PlaywrightPulseReporter;
578
+ exports.default = PlaywrightPulseReporter;