@arghajit/playwright-pulse-report 0.2.2 → 0.2.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.
package/README.md CHANGED
@@ -7,53 +7,7 @@ _The ultimate Playwright reporter — Interactive dashboard with historical tren
7
7
 
8
8
  ## ![Features](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/features.svg)
9
9
 
10
- ## 📸 Screenshots
11
-
12
- ### 🖥️ Desktop View
13
-
14
- <div align="center" style="display: flex; gap: 20px; justify-content: center; flex-wrap: wrap;">
15
- <a href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//playwright-pulse-static-report-desktop.html.png" target="_blank"> <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//playwright-pulse-static-report-desktop.html.png" alt="Dashboard Overview" width="300"/>
16
- <p align="center"><strong>Dashboard Overview</strong></p>
17
- </a>
18
- <a href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Test-run-desktop.png" target="_blank"> <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Test-run-desktop.png" alt="Test Details" width="300"/>
19
- <p align="center"><strong>Test Details</strong>
20
- </p>
21
- </a>
22
- <a href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Test-error-desktop.png" target="_blank"> <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Test-run-desktop.png" alt="Test Failure Details" width="300"/>
23
- <p align="center"><strong>Test Failure Details</strong>
24
- </p>
25
- </a>
26
- <a href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Test-trends-desktop.png" target="_blank"> <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Test-trends-desktop.png" alt="Filter View" width="300"/>
27
- <p align="center"><strong>Test Trends</strong></p>
28
- </a>
29
- </div>
30
-
31
- ### 📱 Mobile View
32
-
33
- <div align="center" style="display: flex; gap: 20px; justify-content: center; flex-wrap: wrap;">
34
-
35
- <a href="https://postimg.cc/CzJBLR5N" target="_blank">
36
- <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//playwright-pulse-static-report-Dashboard.html.png" alt="Mobile Dashboard Overview" width="300"/>
37
- <p align="center"><strong>Dashboard Overview</strong></p>
38
- </a>
39
-
40
- <a href="https://postimg.cc/G8YTczT8" target="_blank">
41
- <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//playwright-pulse-static-report_Test-results.html.png" alt="Test Details" width="300"/>
42
- <p align="center"><strong>Test Details</strong></p>
43
- </a>
44
-
45
- <a href="https://postimg.cc/G8YTczT8" target="_blank">
46
- <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//playwright-pulse-static-report-Trends.html.png" alt="Test Trends" width="300"/>
47
- <p align="center"><strong>Test Trends</strong></p>
48
- </a>
49
-
50
- </div>
51
-
52
- ### Email Report Example
53
-
54
- [![Email Report Template](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Email-report-mobile-template.jpeg)](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Email-report-mobile-template.jpeg)
55
-
56
- [![Email Report](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//pulse-email-summary.html.png)](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//pulse-email-summary.html.png)
10
+ ## **Documentation**: [Pulse Report](https://playwright-pulse-report.netlify.app/)
57
11
 
58
12
  ## Available Scripts
59
13
 
@@ -91,10 +45,6 @@ Run with `npm run <command>`
91
45
 
92
46
  ```bash
93
47
  npm install @arghajit/playwright-pulse-report@latest --save-dev
94
- # or
95
- yarn add @arghajit/playwright-pulse-report@latest --dev
96
- # or
97
- pnpm add @arghajit/playwright-pulse-report@latest --save-dev
98
48
  ```
99
49
 
100
50
  ### 2. Configure Playwright
@@ -161,8 +111,8 @@ npx generate-report
161
111
  1. Configure `.env`:
162
112
 
163
113
  ```bash
164
- SENDER_EMAIL_1=recipient1@example.com
165
- SENDER_EMAIL_2=recipient2@example.com
114
+ RECIPIENT_EMAIL_1=recipient1@example.com
115
+ RECIPIENT_EMAIL_2=recipient2@example.com
166
116
  # ... up to 5 recipients
167
117
  ```
168
118
 
@@ -300,7 +250,7 @@ npm run pulse-dashboard
300
250
 
301
251
  *(Run from project root containing `pulse-report/` directory)*
302
252
 
303
- **NPM Package**: [pulse-dashboard](https://www.npmjs.com/package/pulse-dashboard)
253
+ **NPM Package**: [playwright-pulse-report](https://www.npmjs.com/package/@arghajit/playwright-pulse-report)
304
254
 
305
255
  **Tech Stack**: Next.js, TypeScript, Tailwind CSS, Playwright
306
256
 
@@ -45,11 +45,10 @@ const ATTACHMENTS_SUBDIR = "attachments"; // Consistent subdirectory name
45
45
  * @param config The reporter configuration options.
46
46
  */
47
47
  function attachFiles(testId, pwResult, pulseResult, config) {
48
- const baseReportDir = config.outputDir || "pulse-report"; // Base output directory
49
- // Ensure attachments are relative to the main outputDir
50
- const attachmentsBaseDir = path.resolve(baseReportDir, ATTACHMENTS_SUBDIR); // Absolute path for FS operations
51
- const attachmentsSubFolder = testId.replace(/[^a-zA-Z0-9_-]/g, "_"); // Sanitize testId for folder name
52
- const testAttachmentsDir = path.join(attachmentsBaseDir, attachmentsSubFolder); // e.g., pulse-report/attachments/test_id_abc
48
+ const baseReportDir = config.outputDir || "pulse-report";
49
+ const attachmentsBaseDir = path.resolve(baseReportDir, ATTACHMENTS_SUBDIR);
50
+ const attachmentsSubFolder = testId.replace(/[^a-zA-Z0-9_-]/g, "_");
51
+ const testAttachmentsDir = path.join(attachmentsBaseDir, attachmentsSubFolder);
53
52
  try {
54
53
  if (!fs.existsSync(testAttachmentsDir)) {
55
54
  fs.mkdirSync(testAttachmentsDir, { recursive: true });
@@ -57,53 +56,49 @@ function attachFiles(testId, pwResult, pulseResult, config) {
57
56
  }
58
57
  catch (error) {
59
58
  console.error(`Pulse Reporter: Failed to create attachments directory: ${testAttachmentsDir}`, error);
60
- return; // Stop processing if directory creation fails
59
+ return;
61
60
  }
62
61
  if (!pwResult.attachments)
63
62
  return;
64
- const { base64Images } = config; // Get base64 embedding option
65
- pulseResult.screenshots = []; // Initialize screenshots array
63
+ const { base64Images } = config;
64
+ // --- MODIFICATION: Initialize all attachment arrays to prevent errors ---
65
+ pulseResult.screenshots = [];
66
+ pulseResult.videoPath = [];
67
+ pulseResult.attachments = [];
66
68
  pwResult.attachments.forEach((attachment) => {
67
69
  const { contentType, name, path: attachmentPath, body } = attachment;
68
- // Skip attachments without path or body
69
70
  if (!attachmentPath && !body) {
70
71
  console.warn(`Pulse Reporter: Attachment "${name}" for test ${testId} has no path or body. Skipping.`);
71
72
  return;
72
73
  }
73
- // Determine filename
74
- const safeName = name.replace(/[^a-zA-Z0-9_.-]/g, "_"); // Sanitize original name
74
+ const safeName = name.replace(/[^a-zA-Z0-9_.-]/g, "_");
75
75
  const extension = attachmentPath
76
76
  ? path.extname(attachmentPath)
77
77
  : `.${getFileExtension(contentType)}`;
78
78
  const baseFilename = attachmentPath
79
79
  ? path.basename(attachmentPath, extension)
80
80
  : safeName;
81
- // Ensure unique filename within the test's attachment folder
82
81
  const fileName = `${baseFilename}_${Date.now()}${extension}`;
83
- // Relative path for storing in JSON (relative to baseReportDir)
84
82
  const relativePath = path.join(ATTACHMENTS_SUBDIR, attachmentsSubFolder, fileName);
85
- // Full path for file system operations
86
83
  const fullPath = path.join(testAttachmentsDir, fileName);
87
84
  if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("image/")) {
88
- // Handle all image types consistently
89
85
  handleImage(attachmentPath, body, base64Images, fullPath, relativePath, pulseResult, name);
90
86
  }
91
87
  else if (name === "video" || (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("video/"))) {
92
- handleAttachment(attachmentPath, body, fullPath, relativePath, "videoPath", pulseResult);
88
+ handleAttachment(attachmentPath, body, fullPath, relativePath, "videoPath", pulseResult, attachment);
93
89
  }
94
90
  else if (name === "trace" || contentType === "application/zip") {
95
- // Trace files are zips
96
- handleAttachment(attachmentPath, body, fullPath, relativePath, "tracePath", pulseResult);
91
+ handleAttachment(attachmentPath, body, fullPath, relativePath, "tracePath", pulseResult, attachment);
97
92
  }
98
93
  else {
99
- // Handle other generic attachments if needed (e.g., log files)
100
- // console.log(`Pulse Reporter: Processing generic attachment "${name}" (Type: ${contentType}) for test ${testId}`);
101
- // handleAttachment(attachmentPath, body, fullPath, relativePath, 'otherAttachments', pulseResult); // Example for storing other types
94
+ // --- MODIFICATION: Enabled handling for all other file types ---
95
+ handleAttachment(attachmentPath, body, fullPath, relativePath, "attachments", pulseResult, attachment);
102
96
  }
103
97
  });
104
98
  }
105
99
  /**
106
100
  * Handles image attachments, either embedding as base64 or copying the file.
101
+ * (This function is unchanged)
107
102
  */
108
103
  function handleImage(attachmentPath, body, base64Embed, fullPath, relativePath, pulseResult, attachmentName) {
109
104
  let screenshotData = undefined;
@@ -123,14 +118,10 @@ function handleImage(attachmentPath, body, base64Embed, fullPath, relativePath,
123
118
  }
124
119
  }
125
120
  else if (body) {
126
- // Always embed if only body is available
127
121
  screenshotData = `data:image/${getFileExtension(attachmentName)};base64,${body.toString("base64")}`;
128
122
  if (!base64Embed) {
129
- // Optionally save the buffer to a file even if embedding is off,
130
- // but the primary representation will be base64.
131
123
  try {
132
124
  fs.writeFileSync(fullPath, body);
133
- // console.log(`Pulse Reporter: Saved screenshot buffer to ${fullPath}`);
134
125
  }
135
126
  catch (error) {
136
127
  console.error(`Pulse Reporter: Failed to save screenshot buffer: ${fullPath}. Error: ${error.message}`);
@@ -147,21 +138,36 @@ function handleImage(attachmentPath, body, base64Embed, fullPath, relativePath,
147
138
  /**
148
139
  * Handles non-image attachments by copying the file or writing the buffer.
149
140
  */
150
- function handleAttachment(attachmentPath, body, fullPath, relativePath, resultKey, // Add more keys if needed
151
- pulseResult) {
141
+ function handleAttachment(attachmentPath, body, fullPath, relativePath, resultKey, // MODIFIED: Added 'attachments'
142
+ pulseResult, originalAttachment // MODIFIED: Pass original attachment
143
+ ) {
144
+ var _a, _b;
152
145
  try {
153
146
  if (attachmentPath) {
154
147
  fs.copyFileSync(attachmentPath, fullPath);
155
- pulseResult[resultKey] = relativePath;
156
148
  }
157
149
  else if (body) {
158
150
  fs.writeFileSync(fullPath, body);
159
- pulseResult[resultKey] = relativePath; // Store relative path even if from buffer
151
+ }
152
+ // --- MODIFICATION: Logic to handle different properties correctly ---
153
+ switch (resultKey) {
154
+ case "videoPath":
155
+ (_a = pulseResult.videoPath) === null || _a === void 0 ? void 0 : _a.push(relativePath);
156
+ break;
157
+ case "tracePath":
158
+ pulseResult.tracePath = relativePath;
159
+ break;
160
+ case "attachments":
161
+ (_b = pulseResult.attachments) === null || _b === void 0 ? void 0 : _b.push({
162
+ name: originalAttachment.name,
163
+ path: relativePath,
164
+ contentType: originalAttachment.contentType,
165
+ });
166
+ break;
160
167
  }
161
168
  }
162
169
  catch (error) {
163
170
  console.error(`Pulse Reporter: Failed to copy/write attachment to ${fullPath}. Error: ${error.message}`);
164
- // Don't set the path in pulseResult if saving failed
165
171
  }
166
172
  }
167
173
  /**
@@ -172,8 +178,7 @@ pulseResult) {
172
178
  function getFileExtension(contentType) {
173
179
  var _a;
174
180
  if (!contentType)
175
- return "bin"; // Default binary extension
176
- // More robust mapping
181
+ return "bin";
177
182
  const extensions = {
178
183
  "image/png": "png",
179
184
  "image/jpeg": "jpg",
@@ -182,9 +187,12 @@ function getFileExtension(contentType) {
182
187
  "image/svg+xml": "svg",
183
188
  "video/webm": "webm",
184
189
  "video/mp4": "mp4",
185
- "application/zip": "zip", // For traces
190
+ "application/zip": "zip",
186
191
  "text/plain": "txt",
187
192
  "application/json": "json",
193
+ "text/html": "html",
194
+ "application/pdf": "pdf",
195
+ "text/csv": "csv",
188
196
  };
189
197
  return (extensions[contentType.toLowerCase()] ||
190
198
  ((_a = contentType.split("/")[1]) === null || _a === void 0 ? void 0 : _a.split("+")[0]) ||
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
- // input_file_0.ts
3
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
3
  if (k2 === undefined) k2 = k;
5
4
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -38,8 +37,7 @@ exports.PlaywrightPulseReporter = void 0;
38
37
  const fs = __importStar(require("fs/promises"));
39
38
  const path = __importStar(require("path"));
40
39
  const crypto_1 = require("crypto");
41
- const attachment_utils_1 = require("./attachment-utils"); // Use relative path
42
- const ua_parser_js_1 = require("ua-parser-js"); // Added UAParser import
40
+ const ua_parser_js_1 = require("ua-parser-js");
43
41
  const os = __importStar(require("os"));
44
42
  const convertStatus = (status, testCase) => {
45
43
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
@@ -111,8 +109,8 @@ class PlaywrightPulseReporter {
111
109
  }
112
110
  getBrowserDetails(test) {
113
111
  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
112
+ const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
113
+ const projectConfig = project === null || project === void 0 ? void 0 : project.use;
116
114
  const userAgent = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.userAgent;
117
115
  const configuredBrowserType = (_b = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.browserName) === null || _b === void 0 ? void 0 : _b.toLowerCase();
118
116
  const parser = new ua_parser_js_1.UAParser(userAgent);
@@ -120,20 +118,18 @@ class PlaywrightPulseReporter {
120
118
  let browserName = result.browser.name;
121
119
  const browserVersion = result.browser.version
122
120
  ? ` v${result.browser.version.split(".")[0]}`
123
- : ""; // Major version
121
+ : "";
124
122
  const osName = result.os.name ? ` on ${result.os.name}` : "";
125
123
  const osVersion = result.os.version
126
124
  ? ` ${result.os.version.split(".")[0]}`
127
- : ""; // Major version
128
- const deviceType = result.device.type; // "mobile", "tablet", etc.
125
+ : "";
126
+ const deviceType = result.device.type;
129
127
  let finalString;
130
- // If UAParser couldn't determine browser name, fallback to configured type
131
128
  if (browserName === undefined) {
132
129
  browserName = configuredBrowserType;
133
130
  finalString = `${browserName}`;
134
131
  }
135
132
  else {
136
- // Specific refinements for mobile based on parsed OS and device type
137
133
  if (deviceType === "mobile" || deviceType === "tablet") {
138
134
  if ((_c = result.os.name) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes("android")) {
139
135
  if (browserName.toLowerCase().includes("chrome"))
@@ -144,10 +140,10 @@ class PlaywrightPulseReporter {
144
140
  browserName = "Android WebView";
145
141
  else if (browserName &&
146
142
  !browserName.toLowerCase().includes("mobile")) {
147
- // Keep it as is, e.g. "Samsung Browser" is specific enough
143
+ // Keep it as is
148
144
  }
149
145
  else {
150
- browserName = "Android Browser"; // default for android if not specific
146
+ browserName = "Android Browser";
151
147
  }
152
148
  }
153
149
  else if ((_d = result.os.name) === null || _d === void 0 ? void 0 : _d.toLowerCase().includes("ios")) {
@@ -178,10 +174,9 @@ class PlaywrightPulseReporter {
178
174
  if (step.location) {
179
175
  codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
180
176
  }
181
- let stepTitle = step.title;
182
177
  return {
183
178
  id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}`,
184
- title: stepTitle,
179
+ title: step.title,
185
180
  status: stepStatus,
186
181
  duration: duration,
187
182
  startTime: startTime,
@@ -200,21 +195,16 @@ class PlaywrightPulseReporter {
200
195
  };
201
196
  }
202
197
  async onTestEnd(test, result) {
203
- var _a, _b, _c, _d, _e, _f, _g, _h;
198
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
204
199
  const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
205
200
  const browserDetails = this.getBrowserDetails(test);
206
201
  const testStatus = convertStatus(result.status, test);
207
202
  const startTime = new Date(result.startTime);
208
203
  const endTime = new Date(startTime.getTime() + result.duration);
209
- const testIdForFiles = test.id ||
210
- `${test
211
- .titlePath()
212
- .join("_")
213
- .replace(/[^a-zA-Z0-9]/g, "_")}_${startTime.getTime()}`;
214
204
  const processAllSteps = async (steps) => {
215
205
  let processed = [];
216
206
  for (const step of steps) {
217
- const processedStep = await this.processStep(step, testIdForFiles, browserDetails, test);
207
+ const processedStep = await this.processStep(step, test.id, browserDetails, test);
218
208
  processed.push(processedStep);
219
209
  if (step.steps && step.steps.length > 0) {
220
210
  processedStep.steps = await processAllSteps(step.steps);
@@ -232,39 +222,14 @@ class PlaywrightPulseReporter {
232
222
  catch (e) {
233
223
  console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
234
224
  }
235
- const stdoutMessages = [];
236
- if (result.stdout && result.stdout.length > 0) {
237
- result.stdout.forEach((item) => {
238
- stdoutMessages.push(typeof item === "string" ? item : item.toString());
239
- });
240
- }
241
- const stderrMessages = [];
242
- if (result.stderr && result.stderr.length > 0) {
243
- result.stderr.forEach((item) => {
244
- stderrMessages.push(typeof item === "string" ? item : item.toString());
245
- });
246
- }
247
- const uniqueTestId = test.id;
248
- // --- REFINED THIS SECTION for testData ---
225
+ const stdoutMessages = result.stdout.map((item) => typeof item === "string" ? item : item.toString());
226
+ const stderrMessages = result.stderr.map((item) => typeof item === "string" ? item : item.toString());
249
227
  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
- }
228
+ let mappedWorkerId = result.workerIndex === -1
229
+ ? -1
230
+ : (result.workerIndex % (maxWorkers > 0 ? maxWorkers : 1)) + 1;
265
231
  const testSpecificData = {
266
232
  workerId: mappedWorkerId,
267
- uniqueWorkerIndex: result.workerIndex, // We'll keep the original for diagnostics
268
233
  totalWorkers: maxWorkers,
269
234
  configFile: this.config.configFile,
270
235
  metadata: this.config.metadata
@@ -272,7 +237,7 @@ class PlaywrightPulseReporter {
272
237
  : undefined,
273
238
  };
274
239
  const pulseResult = {
275
- id: uniqueTestId,
240
+ id: test.id,
276
241
  runId: "TBD",
277
242
  name: test.titlePath().join(" > "),
278
243
  suiteName: (project === null || project === void 0 ? void 0 : project.name) || ((_e = this.config.projects[0]) === null || _e === void 0 ? void 0 : _e.name) || "Default Suite",
@@ -288,20 +253,56 @@ class PlaywrightPulseReporter {
288
253
  codeSnippet: codeSnippet,
289
254
  tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
290
255
  screenshots: [],
291
- videoPath: undefined,
256
+ videoPath: [],
292
257
  tracePath: undefined,
258
+ attachments: [],
293
259
  stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
294
260
  stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
295
- // --- UPDATED THESE LINES from testSpecificData ---
296
261
  ...testSpecificData,
297
262
  };
298
- try {
299
- (0, attachment_utils_1.attachFiles)(testIdForFiles, result, pulseResult, this.options);
300
- }
301
- catch (attachError) {
302
- console.error(`Pulse Reporter: Error processing attachments for test ${pulseResult.name} (ID: ${testIdForFiles}): ${attachError.message}`);
263
+ // --- CORRECTED ATTACHMENT PROCESSING LOGIC ---
264
+ for (const [index, attachment] of result.attachments.entries()) {
265
+ if (!attachment.path)
266
+ continue;
267
+ try {
268
+ // Create a sanitized, unique folder name for this specific test
269
+ const testSubfolder = test.id.replace(/[^a-zA-Z0-9_-]/g, "_");
270
+ // Sanitize the original attachment name to create a safe filename
271
+ const safeAttachmentName = path
272
+ .basename(attachment.path)
273
+ .replace(/[^a-zA-Z0-9_.-]/g, "_");
274
+ // Create a unique filename to prevent collisions, especially in retries
275
+ const uniqueFileName = `${index}-${Date.now()}-${safeAttachmentName}`;
276
+ // This is the relative path that will be stored in the JSON report
277
+ const relativeDestPath = path.join(ATTACHMENTS_SUBDIR, testSubfolder, uniqueFileName);
278
+ // This is the absolute path used for the actual file system operation
279
+ const absoluteDestPath = path.join(this.outputDir, relativeDestPath);
280
+ // Ensure the unique, test-specific attachment directory exists
281
+ await this._ensureDirExists(path.dirname(absoluteDestPath));
282
+ await fs.copyFile(attachment.path, absoluteDestPath);
283
+ // Categorize the attachment based on its content type
284
+ if (attachment.contentType.startsWith("image/")) {
285
+ (_j = pulseResult.screenshots) === null || _j === void 0 ? void 0 : _j.push(relativeDestPath);
286
+ }
287
+ else if (attachment.contentType.startsWith("video/")) {
288
+ (_k = pulseResult.videoPath) === null || _k === void 0 ? void 0 : _k.push(relativeDestPath);
289
+ }
290
+ else if (attachment.name === "trace") {
291
+ pulseResult.tracePath = relativeDestPath;
292
+ }
293
+ else {
294
+ (_l = pulseResult.attachments) === null || _l === void 0 ? void 0 : _l.push({
295
+ name: attachment.name, // The original, human-readable name
296
+ path: relativeDestPath, // The safe, relative path for linking
297
+ contentType: attachment.contentType,
298
+ });
299
+ }
300
+ }
301
+ catch (err) {
302
+ console.error(`Pulse Reporter: Failed to process attachment "${attachment.name}" for test ${pulseResult.name}. Error: ${err.message}`);
303
+ }
303
304
  }
304
- const existingTestIndex = this.results.findIndex((r) => r.id === uniqueTestId);
305
+ const existingTestIndex = this.results.findIndex((r) => r.id === test.id);
305
306
  if (existingTestIndex !== -1) {
306
307
  if (pulseResult.retries >= this.results[existingTestIndex].retries) {
307
308
  this.results[existingTestIndex] = pulseResult;
@@ -323,10 +324,10 @@ class PlaywrightPulseReporter {
323
324
  host: os.hostname(),
324
325
  os: `${os.platform()} ${os.release()}`,
325
326
  cpu: {
326
- model: os.cpus()[0] ? os.cpus()[0].model : "N/A", // Handle cases with no CPU info
327
+ model: os.cpus()[0] ? os.cpus()[0].model : "N/A",
327
328
  cores: os.cpus().length,
328
329
  },
329
- memory: `${(os.totalmem() / 1024 ** 3).toFixed(2)}GB`, // Total RAM in GB
330
+ memory: `${(os.totalmem() / 1024 ** 3).toFixed(2)}GB`,
330
331
  node: process.version,
331
332
  v8: process.versions.v8,
332
333
  cwd: process.cwd(),
@@ -418,15 +419,13 @@ class PlaywrightPulseReporter {
418
419
  }
419
420
  }
420
421
  async onEnd(result) {
421
- var _a, _b, _c;
422
422
  if (this.shardIndex !== undefined) {
423
423
  await this._writeShardResults();
424
424
  return;
425
425
  }
426
426
  const runEndTime = Date.now();
427
427
  const duration = runEndTime - this.runStartTime;
428
- const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`; // Need not to change
429
- // --- CALLING _getEnvDetails HERE ---
428
+ const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`;
430
429
  const environmentDetails = this._getEnvDetails();
431
430
  const runData = {
432
431
  id: runId,
@@ -436,13 +435,11 @@ class PlaywrightPulseReporter {
436
435
  failed: 0,
437
436
  skipped: 0,
438
437
  duration,
439
- // --- ADDED environmentDetails HERE ---
440
438
  environment: environmentDetails,
441
439
  };
442
- let finalReport = undefined; // Initialize as undefined
440
+ let finalReport = undefined;
443
441
  if (this.isSharded) {
444
442
  finalReport = await this._mergeShardResults(runData);
445
- // Ensured environment details are on the final merged runData if not already
446
443
  if (finalReport && finalReport.run && !finalReport.run.environment) {
447
444
  finalReport.run.environment = environmentDetails;
448
445
  }
@@ -470,67 +467,8 @@ class PlaywrightPulseReporter {
470
467
  }
471
468
  if (!finalReport) {
472
469
  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
470
  return;
512
471
  }
513
- const reportRunData = finalReport.run;
514
- const finalRunStatus = ((_a = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.failed) !== null && _a !== void 0 ? _a : 0) > 0
515
- ? "failed"
516
- : ((_b = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.totalTests) !== null && _b !== void 0 ? _b : 0) === 0 && result.status !== "passed"
517
- ? result.status === "interrupted"
518
- ? "interrupted"
519
- : "no tests or error"
520
- : "passed";
521
- const summary = `
522
- PlaywrightPulseReporter: Run Finished
523
- -----------------------------------------
524
- Overall Status: ${finalRunStatus.toUpperCase()}
525
- Total Tests: ${(reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.totalTests) || 0}
526
- Passed: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.passed}
527
- Failed: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.failed}
528
- Skipped: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.skipped}
529
- Duration: ${(((_c = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.duration) !== null && _c !== void 0 ? _c : 0) / 1000).toFixed(2)}s
530
- -----------------------------------------`;
531
- if (this.printsToStdio()) {
532
- console.log(summary);
533
- }
534
472
  const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
535
473
  try {
536
474
  await this._ensureDirExists(this.outputDir);
@@ -32,8 +32,13 @@ export interface TestResult {
32
32
  runId: string;
33
33
  browser: string;
34
34
  screenshots?: string[];
35
- videoPath?: string;
35
+ videoPath?: string[];
36
36
  tracePath?: string;
37
+ attachments?: {
38
+ name: string;
39
+ path: string;
40
+ contentType: string;
41
+ }[];
37
42
  stdout?: string[];
38
43
  stderr?: string[];
39
44
  workerId?: number;
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@arghajit/playwright-pulse-report",
3
3
  "author": "Arghajit Singha",
4
- "version": "0.2.2",
4
+ "version": "0.2.3",
5
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
+ "homepage": "https://playwright-pulse-report.netlify.app/",
6
7
  "keywords": [
7
8
  "playwright",
8
9
  "reporter",
@@ -11,10 +12,13 @@
11
12
  "reporting",
12
13
  "nextjs",
13
14
  "playwright-pulse",
15
+ "playwright-pulse-report",
14
16
  "report",
15
17
  "email-report",
16
18
  "send-report",
17
- "email"
19
+ "email",
20
+ "playwright-report",
21
+ "pulse"
18
22
  ],
19
23
  "main": "dist/reporter/index.js",
20
24
  "types": "dist/reporter/index.d.ts",
@@ -46,7 +50,8 @@
46
50
  "report:generate": "node ./scripts/generate-report.mjs",
47
51
  "report:merge": "node ./scripts/merge-pulse-report.js",
48
52
  "report:email": "node ./scripts/sendReport.mjs",
49
- "report:minify": "node ./scripts/generate-email-report.mjs"
53
+ "report:minify": "node ./scripts/generate-email-report.mjs",
54
+ "generate-trend": "node ./scripts/generate-trend.mjs"
50
55
  },
51
56
  "dependencies": {
52
57
  "archiver": "^7.0.1",