@arghajit/playwright-pulse-report 0.1.6 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Playwright Pluse Report
2
2
 
3
- ![Playwright Pulse Report](screenshots/image.png)
4
- *A powerful custom Playwright reporter with interactive dashboard and standalone HTML reports, now with sharding support!*
3
+ ![Playwright Pulse Report](./screenshots/image.png)
4
+ *The ultimate Playwright reporter Interactive dashboard with historical trend analytics, CI/CD-ready standalone HTML reports, and sharding support for scalable test execution.*
5
5
 
6
6
  <a href="https://pulse-report.netlify.app/" target="_blank"><h3>Live Demo</h3></a>
7
7
 
@@ -20,19 +20,19 @@
20
20
 
21
21
  ### 🖥️ Desktop View
22
22
 
23
- <div align="center" style="display: flex; gap: 20px; justify-content: center; flex-wrap: wrap;"> <a href="https://postimg.cc/180cym6c" target="_blank"> <img src="screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html.png" alt="Dashboard Overview" width="300"/> <p align="center"><strong>Dashboard Overview</strong></p> </a> <a href="https://postimg.cc/V5TFRHmM" target="_blank"> <img src="screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html-1.png" alt="Test Details" width="300"/> <p align="center"><strong>Test Details</strong></p> </a> <a href="https://postimg.cc/XXTwFGkk" target="_blank"> <img src="screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html-2.png" alt="Filter View" width="300"/> <p align="center"><strong>Filter View</strong></p> </a> </div>
23
+ <div align="center" style="display: flex; gap: 20px; justify-content: center; flex-wrap: wrap;"> <a href="https://postimg.cc/180cym6c" target="_blank"> <img src="./screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html.png" alt="Dashboard Overview" width="300"/> <p align="center"><strong>Dashboard Overview</strong></p> </a> <a href="https://postimg.cc/V5TFRHmM" target="_blank"> <img src="./screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html-1.png" alt="Test Details" width="300"/> <p align="center"><strong>Test Details</strong></p> </a> <a href="https://postimg.cc/XXTwFGkk" target="_blank"> <img src="./screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html-2.png" alt="Filter View" width="300"/> <p align="center"><strong>Filter View</strong></p> </a> </div>
24
24
 
25
25
  ### 📱 Mobile View
26
26
 
27
27
  <div align="center" style="display: flex; gap: 20px; justify-content: center; flex-wrap: wrap;">
28
28
 
29
29
  <a href="https://postimg.cc/CzJBLR5N" target="_blank">
30
- <img src="screenshots/127-0-0-1-5500-pulse-report-output-playwright-pulse-static-report-html-i-Phone-14-Pro-Max.png" alt="Mobile Overview" width="300"/>
30
+ <img src="./screenshots/127-0-0-1-5500-pulse-report-output-playwright-pulse-static-report-html-i-Phone-14-Pro-Max.png" alt="Mobile Overview" width="300"/>
31
31
  <p align="center"><strong>Dashboard Overview</strong></p>
32
32
  </a>
33
33
 
34
34
  <a href="https://postimg.cc/G8YTczT8" target="_blank">
35
- <img src="screenshots/127-0-0-1-5500-pulse-report-output-playwright-pulse-static-report-html-i-Phone-14-Pro-Max-1.png" alt="Test Details" width="300"/>
35
+ <img src="./screenshots/127-0-0-1-5500-pulse-report-output-playwright-pulse-static-report-html-i-Phone-14-Pro-Max-1.png" alt="Test Details" width="300"/>
36
36
  <p align="center"><strong>Test Details</strong></p>
37
37
  </a>
38
38
 
@@ -40,7 +40,7 @@
40
40
 
41
41
  ### Email Report Example
42
42
 
43
- [![Email Report](https://i.postimg.cc/X7W1VWqr/Screenshot-2025-05-09-at-2-31-15-AM.png)](https://postimg.cc/DmCPgtqh)
43
+ [![Email Report](./screenshots//Email-report.jpg)](https://postimg.cc/DmCPgtqh)
44
44
 
45
45
  ## 🛠️ How It Works
46
46
 
@@ -227,12 +227,29 @@ playwright-pulse-reporter/
227
227
  │ └── app/ # Next.js dashboard
228
228
  ├── scripts/
229
229
  │ └── generate-static-report.mjs # HTML generator
230
+ | └── generate-trend.mjs # Generate Trends
230
231
  | └── merge-pulse-report.mjs # merge sharded reports
231
232
  | └── sendReport.mjs # Send email report
232
233
  ├── pulse-report/ # Generated reports
233
234
  └── sample-report.json # Example data
234
235
  ```
235
236
 
237
+ ## 🎉 What's New in v0.2.1
238
+
239
+ ### ✨ **Key Improvements**
240
+
241
+ | Feature | Description |
242
+ |---------|-------------|
243
+ | **🎨 Refined UI** | Completely redesigned static HTML reports for better readability and navigation |
244
+ | **📊 History Trends** | Visual analytics for:<br>• Test History for last 15 runs<br>• Test suite pass/fail rates<br>• Duration trends<br>• Individual test flakiness |
245
+ | **🛠️ Project Fixes** | Corrected project name display in test suite components |
246
+
247
+ ### 🚀 **Upgrade Now**
248
+
249
+ ```bash
250
+ npm install @arghajit/playwright-pulse-report@latest
251
+ ```
252
+
236
253
  ## 📬 Support
237
254
 
238
255
  For issues or feature requests, please [Contact Me](mailto:arghajitsingha47@gmail.com).
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ // input_file_0.ts
2
3
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
4
  if (k2 === undefined) k2 = k;
4
5
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -39,11 +40,9 @@ const path = __importStar(require("path"));
39
40
  const crypto_1 = require("crypto");
40
41
  const attachment_utils_1 = require("./attachment-utils"); // Use relative path
41
42
  const convertStatus = (status, testCase) => {
42
- // Special case: test was expected to fail (test.fail())
43
43
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
44
- return status === "failed" ? "passed" : "failed";
44
+ return "failed";
45
45
  }
46
- // Special case: test was expected to skip (test.skip())
47
46
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "skipped") {
48
47
  return "skipped";
49
48
  }
@@ -60,7 +59,7 @@ const convertStatus = (status, testCase) => {
60
59
  }
61
60
  };
62
61
  const TEMP_SHARD_FILE_PREFIX = ".pulse-shard-results-";
63
- const ATTACHMENTS_SUBDIR = "attachments"; // Centralized definition
62
+ const ATTACHMENTS_SUBDIR = "attachments";
64
63
  class PlaywrightPulseReporter {
65
64
  constructor(options = {}) {
66
65
  var _a, _b;
@@ -68,13 +67,10 @@ class PlaywrightPulseReporter {
68
67
  this.baseOutputFile = "playwright-pulse-report.json";
69
68
  this.isSharded = false;
70
69
  this.shardIndex = undefined;
71
- this.options = options; // Store provided options
70
+ this.options = options;
72
71
  this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
73
- // Determine outputDir relative to config file or rootDir
74
- // The actual resolution happens in onBegin where config is available
75
72
  this.outputDir = (_b = options.outputDir) !== null && _b !== void 0 ? _b : "pulse-report";
76
- this.attachmentsDir = path.join(this.outputDir, ATTACHMENTS_SUBDIR); // Initial path, resolved fully in onBegin
77
- // console.log(`Pulse Reporter Init: Configured outputDir option: ${options.outputDir}, Base file: ${this.baseOutputFile}`);
73
+ this.attachmentsDir = path.join(this.outputDir, ATTACHMENTS_SUBDIR);
78
74
  }
79
75
  printsToStdio() {
80
76
  return this.shardIndex === undefined || this.shardIndex === 0;
@@ -84,66 +80,54 @@ class PlaywrightPulseReporter {
84
80
  this.config = config;
85
81
  this.suite = suite;
86
82
  this.runStartTime = Date.now();
87
- // --- Resolve outputDir relative to config file or rootDir ---
88
83
  const configDir = this.config.rootDir;
89
- // Use config file directory if available, otherwise rootDir
90
84
  const configFileDir = this.config.configFile
91
85
  ? path.dirname(this.config.configFile)
92
86
  : configDir;
93
87
  this.outputDir = path.resolve(configFileDir, (_a = this.options.outputDir) !== null && _a !== void 0 ? _a : "pulse-report");
94
- // Resolve attachmentsDir relative to the final outputDir
95
88
  this.attachmentsDir = path.resolve(this.outputDir, ATTACHMENTS_SUBDIR);
96
- // Update options with the resolved absolute path for internal use
97
89
  this.options.outputDir = this.outputDir;
98
- // console.log(`Pulse Reporter onBegin: Final Report Output dir resolved to ${this.outputDir}`);
99
- // console.log(`Pulse Reporter onBegin: Attachments base dir resolved to ${this.attachmentsDir}`);
100
90
  const totalShards = this.config.shard ? this.config.shard.total : 1;
101
91
  this.isSharded = totalShards > 1;
102
92
  this.shardIndex = this.config.shard
103
93
  ? this.config.shard.current - 1
104
94
  : undefined;
105
- // Ensure base output directory exists (attachments handled by attachFiles util)
106
95
  this._ensureDirExists(this.outputDir)
107
96
  .then(() => {
108
- if (this.shardIndex === undefined) {
97
+ if (this.shardIndex === undefined || this.shardIndex === 0) {
109
98
  console.log(`PlaywrightPulseReporter: Starting test run with ${suite.allTests().length} tests${this.isSharded ? ` across ${totalShards} shards` : ""}. Pulse outputting to ${this.outputDir}`);
110
- // Clean up old shard files only in the main process
111
- return this._cleanupTemporaryFiles();
112
- }
113
- else {
114
- // console.log(`Pulse Reporter (Shard ${this.shardIndex + 1}/${totalShards}): Starting. Temp results to ${this.outputDir}`);
115
- return Promise.resolve();
99
+ if (this.shardIndex === undefined ||
100
+ (this.isSharded && this.shardIndex === 0)) {
101
+ return this._cleanupTemporaryFiles();
102
+ }
116
103
  }
117
104
  })
118
105
  .catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
119
106
  }
120
107
  onTestBegin(test) {
121
- // Optional: Log test start if needed
122
108
  // console.log(`Starting test: ${test.title}`);
123
109
  }
124
- async processStep(step, testId, browserName) {
110
+ async processStep(step, testId, browserName, testCase) {
125
111
  var _a, _b, _c, _d;
126
- // Determine actual step status (don't inherit from parent)
127
112
  let stepStatus = "passed";
128
113
  let errorMessage = ((_a = step.error) === null || _a === void 0 ? void 0 : _a.message) || undefined;
129
114
  if ((_c = (_b = step.error) === null || _b === void 0 ? void 0 : _b.message) === null || _c === void 0 ? void 0 : _c.startsWith("Test is skipped:")) {
130
115
  stepStatus = "skipped";
131
- errorMessage = "Info: Test is skipped:";
132
116
  }
133
117
  else {
134
- stepStatus = convertStatus(step.error ? "failed" : "passed");
118
+ stepStatus = convertStatus(step.error ? "failed" : "passed", testCase);
135
119
  }
136
120
  const duration = step.duration;
137
121
  const startTime = new Date(step.startTime);
138
122
  const endTime = new Date(startTime.getTime() + Math.max(0, duration));
139
- // Capture code location if available
140
123
  let codeLocation = "";
141
124
  if (step.location) {
142
125
  codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
143
126
  }
127
+ let stepTitle = step.title;
144
128
  return {
145
129
  id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}`,
146
- title: step.title,
130
+ title: stepTitle,
147
131
  status: stepStatus,
148
132
  duration: duration,
149
133
  startTime: startTime,
@@ -158,38 +142,32 @@ class PlaywrightPulseReporter {
158
142
  ? "before"
159
143
  : "after"
160
144
  : undefined,
161
- steps: [], // Will be populated recursively
145
+ steps: [],
162
146
  };
163
147
  }
164
148
  async onTestEnd(test, result) {
165
149
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
166
- // Get the most accurate browser name
167
150
  const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
168
- const browserName = ((_b = project === null || project === void 0 ? void 0 : project.use) === null || _b === void 0 ? void 0 : _b.defaultBrowserType) || "unknown";
151
+ const browserName = ((_b = project === null || project === void 0 ? void 0 : project.use) === null || _b === void 0 ? void 0 : _b.defaultBrowserType) || (project === null || project === void 0 ? void 0 : project.name) || "unknown";
169
152
  const testStatus = convertStatus(result.status, test);
170
153
  const startTime = new Date(result.startTime);
171
154
  const endTime = new Date(startTime.getTime() + result.duration);
172
- // Generate a slightly more robust ID for attachments, especially if test.id is missing
173
155
  const testIdForFiles = test.id ||
174
156
  `${test
175
157
  .titlePath()
176
158
  .join("_")
177
159
  .replace(/[^a-zA-Z0-9]/g, "_")}_${startTime.getTime()}`;
178
- // --- Process Steps Recursively ---
179
- const processAllSteps = async (steps, parentTestStatus) => {
160
+ const processAllSteps = async (steps) => {
180
161
  let processed = [];
181
162
  for (const step of steps) {
182
- const processedStep = await this.processStep(step, testIdForFiles, browserName);
163
+ const processedStep = await this.processStep(step, testIdForFiles, browserName, test);
183
164
  processed.push(processedStep);
184
165
  if (step.steps && step.steps.length > 0) {
185
- const nestedSteps = await processAllSteps(step.steps, processedStep.status);
186
- // Assign nested steps correctly
187
- processedStep.steps = nestedSteps;
166
+ processedStep.steps = await processAllSteps(step.steps);
188
167
  }
189
168
  }
190
169
  return processed;
191
170
  };
192
- // --- Extract Code Snippet ---
193
171
  let codeSnippet = undefined;
194
172
  try {
195
173
  if (((_c = test.location) === null || _c === void 0 ? void 0 : _c.file) && ((_d = test.location) === null || _d === void 0 ? void 0 : _d.line) && ((_e = test.location) === null || _e === void 0 ? void 0 : _e.column)) {
@@ -200,21 +178,31 @@ class PlaywrightPulseReporter {
200
178
  catch (e) {
201
179
  console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
202
180
  }
203
- // --- Prepare Base TestResult ---
181
+ const stdoutMessages = [];
182
+ if (result.stdout && result.stdout.length > 0) {
183
+ result.stdout.forEach((item) => {
184
+ stdoutMessages.push(typeof item === "string" ? item : item.toString());
185
+ });
186
+ }
187
+ const stderrMessages = [];
188
+ if (result.stderr && result.stderr.length > 0) {
189
+ result.stderr.forEach((item) => {
190
+ stderrMessages.push(typeof item === "string" ? item : item.toString());
191
+ });
192
+ }
193
+ const uniqueTestId = test.id;
204
194
  const pulseResult = {
205
- id: test.id || `${test.title}-${startTime.toISOString()}-${(0, crypto_1.randomUUID)()}`, // Use the original ID logic here
206
- runId: "TBD", // Will be set later
195
+ id: uniqueTestId,
196
+ runId: "TBD",
207
197
  name: test.titlePath().join(" > "),
208
- suiteName: ((_f = this.config.projects[0]) === null || _f === void 0 ? void 0 : _f.name) || "Default Suite",
198
+ suiteName: (project === null || project === void 0 ? void 0 : project.name) || ((_f = this.config.projects[0]) === null || _f === void 0 ? void 0 : _f.name) || "Default Suite",
209
199
  status: testStatus,
210
200
  duration: result.duration,
211
201
  startTime: startTime,
212
202
  endTime: endTime,
213
203
  browser: browserName,
214
204
  retries: result.retry,
215
- steps: ((_g = result.steps) === null || _g === void 0 ? void 0 : _g.length)
216
- ? await processAllSteps(result.steps, testStatus)
217
- : [],
205
+ steps: ((_g = result.steps) === null || _g === void 0 ? void 0 : _g.length) ? await processAllSteps(result.steps) : [],
218
206
  errorMessage: (_h = result.error) === null || _h === void 0 ? void 0 : _h.message,
219
207
  stackTrace: (_j = result.error) === null || _j === void 0 ? void 0 : _j.stack,
220
208
  codeSnippet: codeSnippet,
@@ -222,15 +210,24 @@ class PlaywrightPulseReporter {
222
210
  screenshots: [],
223
211
  videoPath: undefined,
224
212
  tracePath: undefined,
213
+ stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
214
+ stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
225
215
  };
226
- // --- Process Attachments using the new utility ---
227
216
  try {
228
217
  (0, attachment_utils_1.attachFiles)(testIdForFiles, result, pulseResult, this.options);
229
218
  }
230
219
  catch (attachError) {
231
220
  console.error(`Pulse Reporter: Error processing attachments for test ${pulseResult.name} (ID: ${testIdForFiles}): ${attachError.message}`);
232
221
  }
233
- this.results.push(pulseResult);
222
+ const existingTestIndex = this.results.findIndex((r) => r.id === uniqueTestId);
223
+ if (existingTestIndex !== -1) {
224
+ if (pulseResult.retries >= this.results[existingTestIndex].retries) {
225
+ this.results[existingTestIndex] = pulseResult;
226
+ }
227
+ }
228
+ else {
229
+ this.results.push(pulseResult);
230
+ }
234
231
  }
235
232
  onError(error) {
236
233
  var _a;
@@ -241,180 +238,213 @@ class PlaywrightPulseReporter {
241
238
  }
242
239
  async _writeShardResults() {
243
240
  if (this.shardIndex === undefined) {
244
- console.warn("Pulse Reporter: _writeShardResults called unexpectedly in main process. Skipping.");
245
241
  return;
246
242
  }
247
243
  const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${this.shardIndex}.json`);
248
244
  try {
249
- // No need to ensureDirExists here, should be done in onBegin
250
- await fs.writeFile(tempFilePath, JSON.stringify(this.results, (key, value) => {
251
- if (value instanceof Date) {
252
- return value.toISOString();
253
- }
254
- return value;
255
- }, 2));
256
- // console.log(`Pulse Reporter: Shard ${this.shardIndex} wrote ${this.results.length} results to ${tempFilePath}`);
245
+ await fs.writeFile(tempFilePath, JSON.stringify(this.results, (key, value) => (value instanceof Date ? value.toISOString() : value), 2));
257
246
  }
258
247
  catch (error) {
259
248
  console.error(`Pulse Reporter: Shard ${this.shardIndex} failed to write temporary results to ${tempFilePath}`, error);
260
249
  }
261
250
  }
262
251
  async _mergeShardResults(finalRunData) {
263
- // console.log('Pulse Reporter: Merging results from shards...');
264
- let allResults = [];
252
+ let allShardProcessedResults = [];
265
253
  const totalShards = this.config.shard ? this.config.shard.total : 1;
266
254
  for (let i = 0; i < totalShards; i++) {
267
255
  const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
268
256
  try {
269
257
  const content = await fs.readFile(tempFilePath, "utf-8");
270
258
  const shardResults = JSON.parse(content);
271
- shardResults.forEach((r) => (r.runId = finalRunData.id));
272
- allResults = allResults.concat(shardResults);
273
- // console.log(`Pulse Reporter: Successfully merged ${shardResults.length} results from shard ${i}`);
259
+ allShardProcessedResults =
260
+ allShardProcessedResults.concat(shardResults);
274
261
  }
275
262
  catch (error) {
276
263
  if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
277
- console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}. This might happen if shard ${i} had no tests or failed early.`);
264
+ console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}. This might be normal if a shard had no tests or failed early.`);
278
265
  }
279
266
  else {
280
- console.error(`Pulse Reporter: Could not read or parse results from shard ${i} (${tempFilePath}). Error:`, error);
267
+ console.error(`Pulse Reporter: Could not read/parse results from shard ${i} (${tempFilePath}). Error:`, error);
281
268
  }
282
269
  }
283
270
  }
284
- // console.log(`Pulse Reporter: Merged a total of ${allResults.length} results from ${totalShards} shards.`);
285
- finalRunData.passed = allResults.filter((r) => r.status === "passed").length;
286
- finalRunData.failed = allResults.filter((r) => r.status === "failed").length;
287
- finalRunData.skipped = allResults.filter((r) => r.status === "skipped").length;
288
- finalRunData.totalTests = allResults.length;
271
+ let finalUniqueResultsMap = new Map();
272
+ for (const result of allShardProcessedResults) {
273
+ const existing = finalUniqueResultsMap.get(result.id);
274
+ if (!existing || result.retries >= existing.retries) {
275
+ finalUniqueResultsMap.set(result.id, result);
276
+ }
277
+ }
278
+ const finalResultsList = Array.from(finalUniqueResultsMap.values());
279
+ finalResultsList.forEach((r) => (r.runId = finalRunData.id));
280
+ finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
281
+ finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
282
+ finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
283
+ finalRunData.totalTests = finalResultsList.length;
289
284
  const reviveDates = (key, value) => {
290
285
  const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
291
286
  if (typeof value === "string" && isoDateRegex.test(value)) {
292
287
  const date = new Date(value);
293
- if (!isNaN(date.getTime())) {
294
- return date;
295
- }
288
+ return !isNaN(date.getTime()) ? date : value;
296
289
  }
297
290
  return value;
298
291
  };
299
- const finalParsedResults = JSON.parse(JSON.stringify(allResults), reviveDates);
292
+ const properlyTypedResults = JSON.parse(JSON.stringify(finalResultsList), reviveDates);
300
293
  return {
301
294
  run: finalRunData,
302
- results: finalParsedResults,
295
+ results: properlyTypedResults,
303
296
  metadata: { generatedAt: new Date().toISOString() },
304
297
  };
305
298
  }
306
299
  async _cleanupTemporaryFiles() {
307
300
  try {
308
- // No need to ensure dir exists here if handled in onBegin
309
301
  const files = await fs.readdir(this.outputDir);
310
302
  const tempFiles = files.filter((f) => f.startsWith(TEMP_SHARD_FILE_PREFIX));
311
303
  if (tempFiles.length > 0) {
312
- // console.log(`Pulse Reporter: Cleaning up ${tempFiles.length} temporary shard files...`);
313
304
  await Promise.all(tempFiles.map((f) => fs.unlink(path.join(this.outputDir, f))));
314
305
  }
315
306
  }
316
307
  catch (error) {
317
308
  if ((error === null || error === void 0 ? void 0 : error.code) !== "ENOENT") {
318
- // Ignore if the directory doesn't exist
319
- console.error("Pulse Reporter: Error cleaning up temporary files:", error);
309
+ console.warn("Pulse Reporter: Warning during cleanup of temporary files:", error.message);
320
310
  }
321
311
  }
322
312
  }
323
- async _ensureDirExists(dirPath, clean = false) {
313
+ async _ensureDirExists(dirPath) {
324
314
  try {
325
- if (clean) {
326
- // console.log(`Pulse Reporter: Cleaning directory ${dirPath}...`);
327
- await fs.rm(dirPath, { recursive: true, force: true });
328
- }
329
315
  await fs.mkdir(dirPath, { recursive: true });
330
316
  }
331
317
  catch (error) {
332
- // Ignore EEXIST error if the directory already exists
333
318
  if (error.code !== "EEXIST") {
334
319
  console.error(`Pulse Reporter: Failed to ensure directory exists: ${dirPath}`, error);
335
- throw error; // Re-throw other errors
320
+ throw error;
336
321
  }
337
322
  }
338
323
  }
339
324
  async onEnd(result) {
340
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
325
+ var _a, _b, _c;
341
326
  if (this.shardIndex !== undefined) {
342
327
  await this._writeShardResults();
343
- // console.log(`PlaywrightPulseReporter: Shard ${this.shardIndex + 1} finished writing results.`);
344
328
  return;
345
329
  }
346
330
  const runEndTime = Date.now();
347
331
  const duration = runEndTime - this.runStartTime;
348
- const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`;
332
+ const runId = `run-${this.runStartTime}-${(0, crypto_1.randomUUID)()}`;
349
333
  const runData = {
350
334
  id: runId,
351
335
  timestamp: new Date(this.runStartTime),
352
- totalTests: 0, // Will be updated after merging/processing
336
+ totalTests: 0,
353
337
  passed: 0,
354
338
  failed: 0,
355
339
  skipped: 0,
356
340
  duration,
357
341
  };
358
- let finalReport;
342
+ let finalReport = undefined; // Initialize as undefined
359
343
  if (this.isSharded) {
360
- // console.log("Pulse Reporter: Run ended, main process merging shard results...");
361
344
  finalReport = await this._mergeShardResults(runData);
362
345
  }
363
346
  else {
364
- // console.log("Pulse Reporter: Run ended, processing results directly (no sharding)...");
365
- this.results.forEach((r) => (r.runId = runId)); // Assign runId to directly collected results
347
+ this.results.forEach((r) => (r.runId = runId));
366
348
  runData.passed = this.results.filter((r) => r.status === "passed").length;
367
349
  runData.failed = this.results.filter((r) => r.status === "failed").length;
368
350
  runData.skipped = this.results.filter((r) => r.status === "skipped").length;
369
351
  runData.totalTests = this.results.length;
352
+ const reviveDates = (key, value) => {
353
+ const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
354
+ if (typeof value === "string" && isoDateRegex.test(value)) {
355
+ const date = new Date(value);
356
+ return !isNaN(date.getTime()) ? date : value;
357
+ }
358
+ return value;
359
+ };
360
+ const properlyTypedResults = JSON.parse(JSON.stringify(this.results), reviveDates);
370
361
  finalReport = {
371
362
  run: runData,
372
- results: this.results, // Use directly collected results
363
+ results: properlyTypedResults,
373
364
  metadata: { generatedAt: new Date().toISOString() },
374
365
  };
375
366
  }
376
- const finalRunStatus = ((_b = (_a = finalReport.run) === null || _a === void 0 ? void 0 : _a.failed) !== null && _b !== void 0 ? _b : 0 > 0)
367
+ if (!finalReport) {
368
+ console.error("PlaywrightPulseReporter: CRITICAL - finalReport object was not generated. Cannot create summary.");
369
+ const errorSummary = `
370
+ PlaywrightPulseReporter: Run Finished
371
+ -----------------------------------------
372
+ Overall Status: ERROR (Report data missing)
373
+ Total Tests: N/A
374
+ Passed: N/A
375
+ Failed: N/A
376
+ Skipped: N/A
377
+ Duration: N/A
378
+ -----------------------------------------`;
379
+ if (this.printsToStdio()) {
380
+ console.log(errorSummary);
381
+ }
382
+ const errorReport = {
383
+ run: {
384
+ id: runId,
385
+ timestamp: new Date(this.runStartTime),
386
+ totalTests: 0,
387
+ passed: 0,
388
+ failed: 0,
389
+ skipped: 0,
390
+ duration: duration,
391
+ },
392
+ results: [],
393
+ metadata: {
394
+ generatedAt: new Date().toISOString(),
395
+ },
396
+ };
397
+ const finalOutputPathOnError = path.join(this.outputDir, this.baseOutputFile);
398
+ try {
399
+ await this._ensureDirExists(this.outputDir);
400
+ await fs.writeFile(finalOutputPathOnError, JSON.stringify(errorReport, null, 2));
401
+ console.warn(`PlaywrightPulseReporter: Wrote an error report to ${finalOutputPathOnError} as finalReport was missing.`);
402
+ }
403
+ catch (writeError) {
404
+ console.error(`PlaywrightPulseReporter: Failed to write error report: ${writeError.message}`);
405
+ }
406
+ return;
407
+ }
408
+ const reportRunData = finalReport.run;
409
+ const finalRunStatus = ((_a = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.failed) !== null && _a !== void 0 ? _a : 0) > 0
377
410
  ? "failed"
378
- : ((_c = finalReport.run) === null || _c === void 0 ? void 0 : _c.totalTests) === 0
379
- ? "no tests"
411
+ : ((_b = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.totalTests) !== null && _b !== void 0 ? _b : 0) === 0 && result.status !== "passed"
412
+ ? result.status === "interrupted"
413
+ ? "interrupted"
414
+ : "no tests or error"
380
415
  : "passed";
381
416
  const summary = `
382
417
  PlaywrightPulseReporter: Run Finished
383
418
  -----------------------------------------
384
419
  Overall Status: ${finalRunStatus.toUpperCase()}
385
- Total Tests: ${(_e = (_d = finalReport.run) === null || _d === void 0 ? void 0 : _d.totalTests) !== null && _e !== void 0 ? _e : "N/A"}
386
- Passed: ${(_g = (_f = finalReport.run) === null || _f === void 0 ? void 0 : _f.passed) !== null && _g !== void 0 ? _g : "N/A"}
387
- Failed: ${(_j = (_h = finalReport.run) === null || _h === void 0 ? void 0 : _h.failed) !== null && _j !== void 0 ? _j : "N/A"}
388
- Skipped: ${(_l = (_k = finalReport.run) === null || _k === void 0 ? void 0 : _k.skipped) !== null && _l !== void 0 ? _l : "N/A"}
389
- Duration: ${(duration / 1000).toFixed(2)}s
420
+ Total Tests: ${(reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.totalTests) || 0}
421
+ Passed: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.passed}
422
+ Failed: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.failed}
423
+ Skipped: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.skipped}
424
+ Duration: ${(((_c = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.duration) !== null && _c !== void 0 ? _c : 0) / 1000).toFixed(2)}s
390
425
  -----------------------------------------`;
391
- console.log(summary);
426
+ if (this.printsToStdio()) {
427
+ console.log(summary);
428
+ }
392
429
  const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
393
430
  try {
394
- // Ensure directory exists before writing final report
395
431
  await this._ensureDirExists(this.outputDir);
396
- // --- Write Final JSON Report ---
397
432
  await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, (key, value) => {
398
- if (value instanceof Date) {
399
- return value.toISOString(); // Ensure dates are ISO strings in JSON
400
- }
401
- // Handle potential BigInt if used elsewhere, though unlikely here
402
- if (typeof value === "bigint") {
433
+ if (value instanceof Date)
434
+ return value.toISOString();
435
+ if (typeof value === "bigint")
403
436
  return value.toString();
404
- }
405
437
  return value;
406
438
  }, 2));
407
- console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
408
- // REMOVED Static HTML Generation Call
409
- // The reporter's responsibility is now only to create the JSON file.
410
- // The user will run `npx generate-pulse-report` separately.
439
+ if (this.printsToStdio()) {
440
+ console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
441
+ }
411
442
  }
412
443
  catch (error) {
413
444
  console.error(`Pulse Reporter: Failed to write final JSON report to ${finalOutputPath}. Error: ${error.message}`);
414
445
  }
415
446
  finally {
416
447
  if (this.isSharded) {
417
- // console.log("Pulse Reporter: Cleaning up temporary shard files...");
418
448
  await this._cleanupTemporaryFiles();
419
449
  }
420
450
  }
@@ -1,5 +1,5 @@
1
1
  import type { LucideIcon } from 'lucide-react';
2
- export type TestStatus = 'passed' | 'failed' | 'skipped';
2
+ export type TestStatus = "passed" | "failed" | "skipped" | "expected-failure" | "unexpected-success" | "explicitly-skipped";
3
3
  export interface TestStep {
4
4
  id: string;
5
5
  title: string;
@@ -34,6 +34,8 @@ export interface TestResult {
34
34
  screenshots?: string[];
35
35
  videoPath?: string;
36
36
  tracePath?: string;
37
+ stdout?: string[];
38
+ stderr?: string[];
37
39
  }
38
40
  export interface TestRun {
39
41
  id: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arghajit/playwright-pulse-report",
3
3
  "author": "Arghajit Singha",
4
- "version": "0.1.6",
4
+ "version": "0.2.1",
5
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
6
  "keywords": [
7
7
  "playwright",
@@ -20,13 +20,15 @@
20
20
  "types": "dist/reporter/index.d.ts",
21
21
  "files": [
22
22
  "dist",
23
+ "screenshots",
23
24
  "scripts/generate-static-report.mjs"
24
25
  ],
25
26
  "license": "MIT",
26
27
  "bin": {
27
28
  "generate-pulse-report": "./scripts/generate-static-report.mjs",
28
29
  "merge-pulse-report": "./scripts/merge-pulse-report.js",
29
- "send-email": "./scripts/sendReport.js"
30
+ "send-email": "./scripts/sendReport.js",
31
+ "generate-trend": "./scripts/generate-trend.mjs"
30
32
  },
31
33
  "exports": {
32
34
  ".": {
@@ -83,6 +85,7 @@
83
85
  "dotenv": "^16.5.0",
84
86
  "firebase": "^11.3.0",
85
87
  "genkit": "^1.6.2",
88
+ "highcharts": "^12.2.0",
86
89
  "jsdom": "^26.1.0",
87
90
  "lucide-react": "^0.475.0",
88
91
  "next": "15.2.3",
Binary file