@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 +23 -6
- package/dist/reporter/playwright-pulse-reporter.js +148 -118
- package/dist/types/index.d.ts +3 -1
- package/package.json +5 -2
- package/screenshots/127-0-0-1-5500-pulse-report-output-playwright-pulse-static-report-html-i-Phone-14-Pro-Max-1.png +0 -0
- package/screenshots/127-0-0-1-5500-pulse-report-output-playwright-pulse-static-report-html-i-Phone-14-Pro-Max.png +0 -0
- package/screenshots/Email-report.jpg +0 -0
- package/screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html-1.png +0 -0
- package/screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html-2.png +0 -0
- package/screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html.png +0 -0
- package/screenshots/image.png +0 -0
- package/scripts/generate-static-report.mjs +1848 -1262
- package/scripts/generate-trend.mjs +165 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Playwright Pluse Report
|
|
2
2
|
|
|
3
|
-

|
|
4
|
-
*
|
|
3
|
+

|
|
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
|
-
[](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
|
|
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";
|
|
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;
|
|
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);
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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:
|
|
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: [],
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
206
|
-
runId: "TBD",
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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
|
|
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
|
|
267
|
+
console.error(`Pulse Reporter: Could not read/parse results from shard ${i} (${tempFilePath}). Error:`, error);
|
|
281
268
|
}
|
|
282
269
|
}
|
|
283
270
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
294
|
-
return date;
|
|
295
|
-
}
|
|
288
|
+
return !isNaN(date.getTime()) ? date : value;
|
|
296
289
|
}
|
|
297
290
|
return value;
|
|
298
291
|
};
|
|
299
|
-
const
|
|
292
|
+
const properlyTypedResults = JSON.parse(JSON.stringify(finalResultsList), reviveDates);
|
|
300
293
|
return {
|
|
301
294
|
run: finalRunData,
|
|
302
|
-
results:
|
|
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
|
-
|
|
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
|
|
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;
|
|
320
|
+
throw error;
|
|
336
321
|
}
|
|
337
322
|
}
|
|
338
323
|
}
|
|
339
324
|
async onEnd(result) {
|
|
340
|
-
var _a, _b, _c
|
|
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}
|
|
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,
|
|
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
|
-
|
|
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:
|
|
363
|
+
results: properlyTypedResults,
|
|
373
364
|
metadata: { generatedAt: new Date().toISOString() },
|
|
374
365
|
};
|
|
375
366
|
}
|
|
376
|
-
|
|
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
|
-
: ((
|
|
379
|
-
?
|
|
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: ${(
|
|
386
|
-
Passed: ${
|
|
387
|
-
Failed: ${
|
|
388
|
-
Skipped: ${
|
|
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
|
-
|
|
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();
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LucideIcon } from 'lucide-react';
|
|
2
|
-
export type TestStatus =
|
|
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
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|