@arghajit/playwright-pulse-report 0.1.1 → 0.1.2
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
|
@@ -207,7 +207,7 @@ To work on the reporter or the dashboard itself:
|
|
|
207
207
|
|
|
208
208
|
* This project supports Playwright test execution with Pulse Reporting in GitHub Actions. Here's how Pulse reports are managed:
|
|
209
209
|
|
|
210
|
-
```
|
|
210
|
+
```bash
|
|
211
211
|
# Upload Pulse report from each shard (per matrix.config.type)
|
|
212
212
|
- name: Upload Pulse Report results
|
|
213
213
|
if: success() || failure()
|
|
@@ -241,7 +241,7 @@ To work on the reporter or the dashboard itself:
|
|
|
241
241
|
|
|
242
242
|
* This project supports sharded Playwright test execution with Pulse Reporting in GitHub Actions. Here's how Pulse reports are managed across shards:
|
|
243
243
|
|
|
244
|
-
```
|
|
244
|
+
```bash
|
|
245
245
|
# Upload Pulse report from each shard (per matrix.config.type)
|
|
246
246
|
- name: Upload Pulse Report results
|
|
247
247
|
if: success() || failure()
|
|
@@ -280,6 +280,7 @@ To work on the reporter or the dashboard itself:
|
|
|
280
280
|
name: pulse-report
|
|
281
281
|
path: pulse-report/
|
|
282
282
|
```
|
|
283
|
+
|
|
283
284
|
## 🧠 Notes:
|
|
284
285
|
|
|
285
286
|
* Each shard generates its own playwright-pulse-report.json inside pulse-report/.
|
|
@@ -287,3 +288,8 @@ To work on the reporter or the dashboard itself:
|
|
|
287
288
|
* After the test matrix completes, reports are downloaded, renamed, and merged.
|
|
288
289
|
* merge-report is a custom Node.js script that combines all JSON files into one.
|
|
289
290
|
* generate-report can build a static HTML dashboard if needed.
|
|
291
|
+
|
|
292
|
+
## Fixes:
|
|
293
|
+
|
|
294
|
+
### - "0.1.1" : Added Sharding Support
|
|
295
|
+
### - "0.1.2" : Fixed browser filter and Added Browser Tag in Test Suite Card
|
|
@@ -45,100 +45,62 @@ 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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!fs.existsSync(testAttachmentsDir)) {
|
|
58
|
-
fs.mkdirSync(testAttachmentsDir, { recursive: true });
|
|
59
|
-
}
|
|
60
|
-
} catch (error) {
|
|
61
|
-
console.error(
|
|
62
|
-
`Pulse Reporter: Failed to create attachments directory: ${testAttachmentsDir}`,
|
|
63
|
-
error
|
|
64
|
-
);
|
|
65
|
-
return; // Stop processing if directory creation fails
|
|
66
|
-
}
|
|
67
|
-
if (!pwResult.attachments) return;
|
|
68
|
-
const { base64Images } = config; // Get base64 embedding option
|
|
69
|
-
pulseResult.screenshots = []; // Initialize screenshots array
|
|
70
|
-
pwResult.attachments.forEach((attachment) => {
|
|
71
|
-
const { contentType, name, path: attachmentPath, body } = attachment;
|
|
72
|
-
// Skip attachments without path or body
|
|
73
|
-
if (!attachmentPath && !body) {
|
|
74
|
-
console.warn(
|
|
75
|
-
`Pulse Reporter: Attachment "${name}" for test ${testId} has no path or body. Skipping.`
|
|
76
|
-
);
|
|
77
|
-
return;
|
|
48
|
+
const baseReportDir = config.outputDir || "pulse-report"; // Base output directory
|
|
49
|
+
// Ensure attachments are relative to the main outputDir
|
|
50
|
+
const attachmentsBaseDir = path.resolve(baseReportDir, ATTACHMENTS_SUBDIR); // Absolute path for FS operations
|
|
51
|
+
const attachmentsSubFolder = testId.replace(/[^a-zA-Z0-9_-]/g, "_"); // Sanitize testId for folder name
|
|
52
|
+
const testAttachmentsDir = path.join(attachmentsBaseDir, attachmentsSubFolder); // e.g., pulse-report/attachments/test_id_abc
|
|
53
|
+
try {
|
|
54
|
+
if (!fs.existsSync(testAttachmentsDir)) {
|
|
55
|
+
fs.mkdirSync(testAttachmentsDir, { recursive: true });
|
|
56
|
+
}
|
|
78
57
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
? path.extname(attachmentPath)
|
|
83
|
-
: `.${getFileExtension(contentType)}`;
|
|
84
|
-
const baseFilename = attachmentPath
|
|
85
|
-
? path.basename(attachmentPath, extension)
|
|
86
|
-
: safeName;
|
|
87
|
-
// Ensure unique filename within the test's attachment folder
|
|
88
|
-
const fileName = `${baseFilename}_${Date.now()}${extension}`;
|
|
89
|
-
// Relative path for storing in JSON (relative to baseReportDir)
|
|
90
|
-
const relativePath = path.join(
|
|
91
|
-
ATTACHMENTS_SUBDIR,
|
|
92
|
-
attachmentsSubFolder,
|
|
93
|
-
fileName
|
|
94
|
-
);
|
|
95
|
-
// Full path for file system operations
|
|
96
|
-
const fullPath = path.join(testAttachmentsDir, fileName);
|
|
97
|
-
if (
|
|
98
|
-
contentType === null || contentType === void 0
|
|
99
|
-
? void 0
|
|
100
|
-
: contentType.startsWith("image/")
|
|
101
|
-
) {
|
|
102
|
-
// Handle all image types consistently
|
|
103
|
-
handleImage(
|
|
104
|
-
attachmentPath,
|
|
105
|
-
body,
|
|
106
|
-
base64Images,
|
|
107
|
-
fullPath,
|
|
108
|
-
relativePath,
|
|
109
|
-
pulseResult,
|
|
110
|
-
name
|
|
111
|
-
);
|
|
112
|
-
} else if (
|
|
113
|
-
name === "video" ||
|
|
114
|
-
(contentType === null || contentType === void 0
|
|
115
|
-
? void 0
|
|
116
|
-
: contentType.startsWith("video/"))
|
|
117
|
-
) {
|
|
118
|
-
handleAttachment(
|
|
119
|
-
attachmentPath,
|
|
120
|
-
body,
|
|
121
|
-
fullPath,
|
|
122
|
-
relativePath,
|
|
123
|
-
"videoPath",
|
|
124
|
-
pulseResult
|
|
125
|
-
);
|
|
126
|
-
} else if (name === "trace" || contentType === "application/zip") {
|
|
127
|
-
// Trace files are zips
|
|
128
|
-
handleAttachment(
|
|
129
|
-
attachmentPath,
|
|
130
|
-
body,
|
|
131
|
-
fullPath,
|
|
132
|
-
relativePath,
|
|
133
|
-
"tracePath",
|
|
134
|
-
pulseResult
|
|
135
|
-
);
|
|
136
|
-
} else {
|
|
137
|
-
// Handle other generic attachments if needed (e.g., log files)
|
|
138
|
-
// console.log(`Pulse Reporter: Processing generic attachment "${name}" (Type: ${contentType}) for test ${testId}`);
|
|
139
|
-
// handleAttachment(attachmentPath, body, fullPath, relativePath, 'otherAttachments', pulseResult); // Example for storing other types
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error(`Pulse Reporter: Failed to create attachments directory: ${testAttachmentsDir}`, error);
|
|
60
|
+
return; // Stop processing if directory creation fails
|
|
140
61
|
}
|
|
141
|
-
|
|
62
|
+
if (!pwResult.attachments)
|
|
63
|
+
return;
|
|
64
|
+
const { base64Images } = config; // Get base64 embedding option
|
|
65
|
+
pulseResult.screenshots = []; // Initialize screenshots array
|
|
66
|
+
pwResult.attachments.forEach((attachment) => {
|
|
67
|
+
const { contentType, name, path: attachmentPath, body } = attachment;
|
|
68
|
+
// Skip attachments without path or body
|
|
69
|
+
if (!attachmentPath && !body) {
|
|
70
|
+
console.warn(`Pulse Reporter: Attachment "${name}" for test ${testId} has no path or body. Skipping.`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Determine filename
|
|
74
|
+
const safeName = name.replace(/[^a-zA-Z0-9_.-]/g, "_"); // Sanitize original name
|
|
75
|
+
const extension = attachmentPath
|
|
76
|
+
? path.extname(attachmentPath)
|
|
77
|
+
: `.${getFileExtension(contentType)}`;
|
|
78
|
+
const baseFilename = attachmentPath
|
|
79
|
+
? path.basename(attachmentPath, extension)
|
|
80
|
+
: safeName;
|
|
81
|
+
// Ensure unique filename within the test's attachment folder
|
|
82
|
+
const fileName = `${baseFilename}_${Date.now()}${extension}`;
|
|
83
|
+
// Relative path for storing in JSON (relative to baseReportDir)
|
|
84
|
+
const relativePath = path.join(ATTACHMENTS_SUBDIR, attachmentsSubFolder, fileName);
|
|
85
|
+
// Full path for file system operations
|
|
86
|
+
const fullPath = path.join(testAttachmentsDir, fileName);
|
|
87
|
+
if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("image/")) {
|
|
88
|
+
// Handle all image types consistently
|
|
89
|
+
handleImage(attachmentPath, body, base64Images, fullPath, relativePath, pulseResult, name);
|
|
90
|
+
}
|
|
91
|
+
else if (name === "video" || (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("video/"))) {
|
|
92
|
+
handleAttachment(attachmentPath, body, fullPath, relativePath, "videoPath", pulseResult);
|
|
93
|
+
}
|
|
94
|
+
else if (name === "trace" || contentType === "application/zip") {
|
|
95
|
+
// Trace files are zips
|
|
96
|
+
handleAttachment(attachmentPath, body, fullPath, relativePath, "tracePath", pulseResult);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// Handle other generic attachments if needed (e.g., log files)
|
|
100
|
+
// console.log(`Pulse Reporter: Processing generic attachment "${name}" (Type: ${contentType}) for test ${testId}`);
|
|
101
|
+
// handleAttachment(attachmentPath, body, fullPath, relativePath, 'otherAttachments', pulseResult); // Example for storing other types
|
|
102
|
+
}
|
|
103
|
+
});
|
|
142
104
|
}
|
|
143
105
|
/**
|
|
144
106
|
* Handles image attachments, either embedding as base64 or copying the file.
|
|
@@ -72,10 +72,7 @@ class PlaywrightPulseReporter {
|
|
|
72
72
|
this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
|
|
73
73
|
// Determine outputDir relative to config file or rootDir
|
|
74
74
|
// The actual resolution happens in onBegin where config is available
|
|
75
|
-
this.outputDir =
|
|
76
|
-
(_b = options.outputDir) !== null && _b !== void 0
|
|
77
|
-
? _b
|
|
78
|
-
: "pulse-report";
|
|
75
|
+
this.outputDir = (_b = options.outputDir) !== null && _b !== void 0 ? _b : "pulse-report";
|
|
79
76
|
this.attachmentsDir = path.join(this.outputDir, ATTACHMENTS_SUBDIR); // Initial path, resolved fully in onBegin
|
|
80
77
|
// console.log(`Pulse Reporter Init: Configured outputDir option: ${options.outputDir}, Base file: ${this.baseOutputFile}`);
|
|
81
78
|
}
|
|
@@ -93,12 +90,7 @@ class PlaywrightPulseReporter {
|
|
|
93
90
|
const configFileDir = this.config.configFile
|
|
94
91
|
? path.dirname(this.config.configFile)
|
|
95
92
|
: configDir;
|
|
96
|
-
this.outputDir = path.resolve(
|
|
97
|
-
configFileDir,
|
|
98
|
-
(_a = this.options.outputDir) !== null && _a !== void 0
|
|
99
|
-
? _a
|
|
100
|
-
: "pulse-report"
|
|
101
|
-
);
|
|
93
|
+
this.outputDir = path.resolve(configFileDir, (_a = this.options.outputDir) !== null && _a !== void 0 ? _a : "pulse-report");
|
|
102
94
|
// Resolve attachmentsDir relative to the final outputDir
|
|
103
95
|
this.attachmentsDir = path.resolve(this.outputDir, ATTACHMENTS_SUBDIR);
|
|
104
96
|
// Update options with the resolved absolute path for internal use
|
|
@@ -129,7 +121,7 @@ class PlaywrightPulseReporter {
|
|
|
129
121
|
// Optional: Log test start if needed
|
|
130
122
|
// console.log(`Starting test: ${test.title}`);
|
|
131
123
|
}
|
|
132
|
-
async processStep(step, testId) {
|
|
124
|
+
async processStep(step, testId, browserName) {
|
|
133
125
|
var _a, _b, _c, _d;
|
|
134
126
|
// Determine actual step status (don't inherit from parent)
|
|
135
127
|
let stepStatus = "passed";
|
|
@@ -156,6 +148,7 @@ class PlaywrightPulseReporter {
|
|
|
156
148
|
duration: duration,
|
|
157
149
|
startTime: startTime,
|
|
158
150
|
endTime: endTime,
|
|
151
|
+
browser: browserName,
|
|
159
152
|
errorMessage: errorMessage,
|
|
160
153
|
stackTrace: ((_d = step.error) === null || _d === void 0 ? void 0 : _d.stack) || undefined,
|
|
161
154
|
codeLocation: codeLocation || undefined,
|
|
@@ -169,7 +162,10 @@ class PlaywrightPulseReporter {
|
|
|
169
162
|
};
|
|
170
163
|
}
|
|
171
164
|
async onTestEnd(test, result) {
|
|
172
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
165
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
166
|
+
// Get the most accurate browser name
|
|
167
|
+
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";
|
|
173
169
|
const testStatus = convertStatus(result.status, test);
|
|
174
170
|
const startTime = new Date(result.startTime);
|
|
175
171
|
const endTime = new Date(startTime.getTime() + result.duration);
|
|
@@ -183,7 +179,7 @@ class PlaywrightPulseReporter {
|
|
|
183
179
|
const processAllSteps = async (steps, parentTestStatus) => {
|
|
184
180
|
let processed = [];
|
|
185
181
|
for (const step of steps) {
|
|
186
|
-
const processedStep = await this.processStep(step, testIdForFiles);
|
|
182
|
+
const processedStep = await this.processStep(step, testIdForFiles, browserName);
|
|
187
183
|
processed.push(processedStep);
|
|
188
184
|
if (step.steps && step.steps.length > 0) {
|
|
189
185
|
const nestedSteps = await processAllSteps(step.steps, processedStep.status);
|
|
@@ -196,7 +192,7 @@ class PlaywrightPulseReporter {
|
|
|
196
192
|
// --- Extract Code Snippet ---
|
|
197
193
|
let codeSnippet = undefined;
|
|
198
194
|
try {
|
|
199
|
-
if (((
|
|
195
|
+
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
196
|
const relativePath = path.relative(this.config.rootDir, test.location.file);
|
|
201
197
|
codeSnippet = `Test defined at: ${relativePath}:${test.location.line}:${test.location.column}`;
|
|
202
198
|
}
|
|
@@ -209,17 +205,18 @@ class PlaywrightPulseReporter {
|
|
|
209
205
|
id: test.id || `${test.title}-${startTime.toISOString()}-${(0, crypto_1.randomUUID)()}`, // Use the original ID logic here
|
|
210
206
|
runId: "TBD", // Will be set later
|
|
211
207
|
name: test.titlePath().join(" > "),
|
|
212
|
-
suiteName: ((
|
|
208
|
+
suiteName: ((_f = this.config.projects[0]) === null || _f === void 0 ? void 0 : _f.name) || "Default Suite",
|
|
213
209
|
status: testStatus,
|
|
214
210
|
duration: result.duration,
|
|
215
211
|
startTime: startTime,
|
|
216
212
|
endTime: endTime,
|
|
213
|
+
browser: browserName,
|
|
217
214
|
retries: result.retry,
|
|
218
|
-
steps: ((
|
|
215
|
+
steps: ((_g = result.steps) === null || _g === void 0 ? void 0 : _g.length)
|
|
219
216
|
? await processAllSteps(result.steps, testStatus)
|
|
220
217
|
: [],
|
|
221
|
-
errorMessage: (
|
|
222
|
-
stackTrace: (
|
|
218
|
+
errorMessage: (_h = result.error) === null || _h === void 0 ? void 0 : _h.message,
|
|
219
|
+
stackTrace: (_j = result.error) === null || _j === void 0 ? void 0 : _j.stack,
|
|
223
220
|
codeSnippet: codeSnippet,
|
|
224
221
|
tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
|
|
225
222
|
screenshots: [],
|
package/dist/types/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface TestStep {
|
|
|
7
7
|
duration: number;
|
|
8
8
|
startTime: Date;
|
|
9
9
|
endTime: Date;
|
|
10
|
+
browser: string;
|
|
10
11
|
errorMessage?: string;
|
|
11
12
|
stackTrace?: string;
|
|
12
13
|
codeLocation?: string;
|
|
@@ -29,6 +30,7 @@ export interface TestResult {
|
|
|
29
30
|
tags?: string[];
|
|
30
31
|
suiteName?: string;
|
|
31
32
|
runId: string;
|
|
33
|
+
browser: string;
|
|
32
34
|
screenshots?: string[];
|
|
33
35
|
videoPath?: string;
|
|
34
36
|
tracePath?: string;
|
package/package.json
CHANGED
|
@@ -221,7 +221,7 @@ function getSuitesData(results) {
|
|
|
221
221
|
const suitesMap = new Map();
|
|
222
222
|
|
|
223
223
|
results.forEach((test) => {
|
|
224
|
-
const browser = test.
|
|
224
|
+
const browser = test.browser; // Extract browser (chromium/firefox/webkit)
|
|
225
225
|
const suiteName = test.suiteName;
|
|
226
226
|
const key = `${suiteName}|${browser}`;
|
|
227
227
|
|
|
@@ -231,6 +231,7 @@ function getSuitesData(results) {
|
|
|
231
231
|
name: `${suiteName} (${browser})`,
|
|
232
232
|
status: test.status,
|
|
233
233
|
count: 0,
|
|
234
|
+
browser: browser,
|
|
234
235
|
});
|
|
235
236
|
}
|
|
236
237
|
suitesMap.get(key).count++;
|
|
@@ -267,6 +268,7 @@ function generateSuitesWidget(suitesData) {
|
|
|
267
268
|
suite.count !== 1 ? "s" : ""
|
|
268
269
|
}</span>
|
|
269
270
|
</div>
|
|
271
|
+
<span class="browser-name">${suite.browser}</span>
|
|
270
272
|
</div>
|
|
271
273
|
`
|
|
272
274
|
)
|
|
@@ -469,8 +471,7 @@ function generateHTML(reportData) {
|
|
|
469
471
|
|
|
470
472
|
return results
|
|
471
473
|
.map((test, index) => {
|
|
472
|
-
const
|
|
473
|
-
const browser = browserMatch ? browserMatch[1] : "unknown";
|
|
474
|
+
const browser = test.browser || "unknown";
|
|
474
475
|
const testName = test.name.split(" > ").pop() || test.name;
|
|
475
476
|
|
|
476
477
|
// Generate steps HTML recursively
|
|
@@ -1272,12 +1273,7 @@ function generateHTML(reportData) {
|
|
|
1272
1273
|
<select id="filter-browser">
|
|
1273
1274
|
<option value="">All Browsers</option>
|
|
1274
1275
|
${Array.from(
|
|
1275
|
-
new Set(
|
|
1276
|
-
results.map((test) => {
|
|
1277
|
-
const match = test.name.match(/ > (\w+) > /);
|
|
1278
|
-
return match ? match[1] : "unknown";
|
|
1279
|
-
})
|
|
1280
|
-
)
|
|
1276
|
+
new Set(results.map((test) => test.browser || "unknown"))
|
|
1281
1277
|
)
|
|
1282
1278
|
.map(
|
|
1283
1279
|
(browser) => `
|
|
@@ -1533,4 +1529,4 @@ async function main() {
|
|
|
1533
1529
|
}
|
|
1534
1530
|
|
|
1535
1531
|
// Run the main function
|
|
1536
|
-
main();
|
|
1532
|
+
main();
|