@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 +4 -54
- package/dist/reporter/attachment-utils.js +41 -33
- package/dist/reporter/playwright-pulse-reporter.js +65 -127
- package/dist/types/index.d.ts +6 -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
|
|
|
@@ -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]) ||
|
|
@@ -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
|
|
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();
|
|
115
|
-
const projectConfig = project === null || project === void 0 ? void 0 : project.use;
|
|
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
|
-
: "";
|
|
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
|
-
: "";
|
|
128
|
-
const deviceType = result.device.type;
|
|
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
|
|
143
|
+
// Keep it as is
|
|
148
144
|
}
|
|
149
145
|
else {
|
|
150
|
-
browserName = "Android Browser";
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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 ===
|
|
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",
|
|
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`,
|
|
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`;
|
|
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;
|
|
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);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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.
|
|
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",
|