@arghajit/playwright-pulse-report 0.2.2 → 0.2.4
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 +50 -54
- package/dist/reporter/attachment-utils.js +41 -33
- package/dist/reporter/playwright-pulse-reporter.d.ts +3 -0
- package/dist/reporter/playwright-pulse-reporter.js +190 -172
- package/dist/types/index.d.ts +7 -1
- package/package.json +8 -3
- package/scripts/generate-report.mjs +222 -158
- package/scripts/generate-static-report.mjs +324 -374
- package/scripts/generate-trend.mjs +1 -1
- package/scripts/sendReport.mjs +5 -5
package/README.md
CHANGED
|
@@ -7,53 +7,7 @@ _The ultimate Playwright reporter — Interactive dashboard with historical tren
|
|
|
7
7
|
|
|
8
8
|
## 
|
|
9
9
|
|
|
10
|
-
##
|
|
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
|
-
[](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Email-report-mobile-template.jpeg)
|
|
55
|
-
|
|
56
|
-
[](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
|
-
|
|
165
|
-
|
|
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-
|
|
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
|
|
|
@@ -308,6 +258,52 @@ npm run pulse-dashboard
|
|
|
308
258
|
|
|
309
259
|
---
|
|
310
260
|
|
|
261
|
+
## ⚙️ Advanced Configuration
|
|
262
|
+
|
|
263
|
+
### Handling Sequential Test Runs
|
|
264
|
+
|
|
265
|
+
By default, the reporter will overwrite the `playwright-pulse-report.json` file on each new test run. This is usually what we want. However, if we run tests sequentially in the same job, like this:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
npx playwright test test1.spec.ts && npx playwright test test2.spec.ts
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
By default, In this above scenario, the report from test1 will be lost. To solve this, you can use the resetOnEachRun option.
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
// playwright.config.ts
|
|
275
|
+
import { defineConfig } from "@playwright/test";
|
|
276
|
+
import * as path from "path";
|
|
277
|
+
|
|
278
|
+
// Define where the final report JSON and HTML should go
|
|
279
|
+
const PULSE_REPORT_DIR = path.resolve(__dirname, "pulse-report"); // Example: a directory in your project root
|
|
280
|
+
|
|
281
|
+
export default defineConfig({
|
|
282
|
+
reporter: [
|
|
283
|
+
["list"],
|
|
284
|
+
[
|
|
285
|
+
"@arghajit/playwright-pulse-report",
|
|
286
|
+
{
|
|
287
|
+
outputDir: PULSE_REPORT_DIR,
|
|
288
|
+
// Add this option
|
|
289
|
+
resetOnEachRun: false, // Default is true
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
],
|
|
293
|
+
// ...
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**How it works when resetOnEachRun: false:**
|
|
298
|
+
|
|
299
|
+
- On the first run, it saves report-1.json to a pulse-report/pulse-results directory and creates the main playwright-pulse-report.json from it.
|
|
300
|
+
- On the second run, it saves report-2.json to the same directory.
|
|
301
|
+
- It then automatically reads both report-1.json and report-2.json, merges them, and updates the main playwright-pulse-report.json with the combined results.
|
|
302
|
+
|
|
303
|
+
***This ensures your final report is always a complete summary of all sequential test runs.***
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
311
307
|
## 📬 Support
|
|
312
308
|
|
|
313
309
|
For issues or feature requests, please [Contact Me](mailto:arghajitsingha47@gmail.com).
|
|
@@ -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";
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const
|
|
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;
|
|
59
|
+
return;
|
|
61
60
|
}
|
|
62
61
|
if (!pwResult.attachments)
|
|
63
62
|
return;
|
|
64
|
-
const { base64Images } = config;
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
handleAttachment(attachmentPath, body, fullPath, relativePath, "tracePath", pulseResult);
|
|
91
|
+
handleAttachment(attachmentPath, body, fullPath, relativePath, "tracePath", pulseResult, attachment);
|
|
97
92
|
}
|
|
98
93
|
else {
|
|
99
|
-
//
|
|
100
|
-
|
|
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, //
|
|
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
|
-
|
|
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";
|
|
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",
|
|
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]) ||
|
|
@@ -11,6 +11,7 @@ export declare class PlaywrightPulseReporter implements Reporter {
|
|
|
11
11
|
private baseOutputFile;
|
|
12
12
|
private isSharded;
|
|
13
13
|
private shardIndex;
|
|
14
|
+
private resetOnEachRun;
|
|
14
15
|
constructor(options?: PlaywrightPulseReporterOptions);
|
|
15
16
|
printsToStdio(): boolean;
|
|
16
17
|
onBegin(config: FullConfig, suite: Suite): void;
|
|
@@ -18,6 +19,7 @@ export declare class PlaywrightPulseReporter implements Reporter {
|
|
|
18
19
|
private getBrowserDetails;
|
|
19
20
|
private processStep;
|
|
20
21
|
onTestEnd(test: TestCase, result: PwTestResult): Promise<void>;
|
|
22
|
+
private _getFinalizedResults;
|
|
21
23
|
onError(error: any): void;
|
|
22
24
|
private _getEnvDetails;
|
|
23
25
|
private _writeShardResults;
|
|
@@ -25,5 +27,6 @@ export declare class PlaywrightPulseReporter implements Reporter {
|
|
|
25
27
|
private _cleanupTemporaryFiles;
|
|
26
28
|
private _ensureDirExists;
|
|
27
29
|
onEnd(result: FullResult): Promise<void>;
|
|
30
|
+
private _mergeAllRunReports;
|
|
28
31
|
}
|
|
29
32
|
export default PlaywrightPulseReporter;
|