@arghajit/playwright-pulse-report 0.1.5 → 0.2.0
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 +125 -185
- package/dist/reporter/playwright-pulse-reporter.js +115 -93
- package/dist/types/index.d.ts +3 -1
- package/package.json +9 -6
- 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 +1938 -1235
- package/scripts/generate-trend-excel.mjs +273 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// input_file_0.ts
|
|
2
3
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
4
|
if (k2 === undefined) k2 = k;
|
|
4
5
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -39,11 +40,9 @@ const path = __importStar(require("path"));
|
|
|
39
40
|
const crypto_1 = require("crypto");
|
|
40
41
|
const attachment_utils_1 = require("./attachment-utils"); // Use relative path
|
|
41
42
|
const convertStatus = (status, testCase) => {
|
|
42
|
-
// Special case: test was expected to fail (test.fail())
|
|
43
43
|
if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
|
|
44
|
-
return status === "failed" ? "
|
|
44
|
+
return status === "failed" ? "failed" : "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,46 +80,33 @@ 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
97
|
if (this.shardIndex === undefined) {
|
|
109
98
|
console.log(`PlaywrightPulseReporter: Starting test run with ${suite.allTests().length} tests${this.isSharded ? ` across ${totalShards} shards` : ""}. Pulse outputting to ${this.outputDir}`);
|
|
110
|
-
// Clean up old shard files only in the main process
|
|
111
99
|
return this._cleanupTemporaryFiles();
|
|
112
100
|
}
|
|
113
|
-
else {
|
|
114
|
-
// console.log(`Pulse Reporter (Shard ${this.shardIndex + 1}/${totalShards}): Starting. Temp results to ${this.outputDir}`);
|
|
115
|
-
return Promise.resolve();
|
|
116
|
-
}
|
|
117
101
|
})
|
|
118
102
|
.catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
|
|
119
103
|
}
|
|
120
104
|
onTestBegin(test) {
|
|
121
|
-
// Optional: Log test start if needed
|
|
122
105
|
// console.log(`Starting test: ${test.title}`);
|
|
123
106
|
}
|
|
124
|
-
async processStep(step, testId, browserName
|
|
107
|
+
async processStep(step, testId, browserName, // Changed from browserName for clarity
|
|
108
|
+
testCase) {
|
|
125
109
|
var _a, _b, _c, _d;
|
|
126
|
-
// Determine actual step status (don't inherit from parent)
|
|
127
110
|
let stepStatus = "passed";
|
|
128
111
|
let errorMessage = ((_a = step.error) === null || _a === void 0 ? void 0 : _a.message) || undefined;
|
|
129
112
|
if ((_c = (_b = step.error) === null || _b === void 0 ? void 0 : _b.message) === null || _c === void 0 ? void 0 : _c.startsWith("Test is skipped:")) {
|
|
@@ -131,19 +114,41 @@ class PlaywrightPulseReporter {
|
|
|
131
114
|
errorMessage = "Info: Test is skipped:";
|
|
132
115
|
}
|
|
133
116
|
else {
|
|
134
|
-
stepStatus = convertStatus(step.error ? "failed" : "passed");
|
|
117
|
+
stepStatus = convertStatus(step.error ? "failed" : "passed", testCase);
|
|
135
118
|
}
|
|
136
119
|
const duration = step.duration;
|
|
137
120
|
const startTime = new Date(step.startTime);
|
|
138
121
|
const endTime = new Date(startTime.getTime() + Math.max(0, duration));
|
|
139
|
-
// Capture code location if available
|
|
140
122
|
let codeLocation = "";
|
|
141
123
|
if (step.location) {
|
|
142
124
|
codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
|
|
143
125
|
}
|
|
126
|
+
let stepTitle = step.title;
|
|
127
|
+
// This logic had a 'status' variable that was not defined in this scope.
|
|
128
|
+
// Assuming it meant to check 'stepStatus' or 'testCase.expectedStatus' related to step.error.
|
|
129
|
+
// Corrected to reflect comparison with testCase if step.category is 'test'.
|
|
130
|
+
if (step.category === "test" && testCase) {
|
|
131
|
+
// If a test step (not a hook) resulted in an error, but the test was expected to fail,
|
|
132
|
+
// this specific logic might need refinement based on how you want to report step errors
|
|
133
|
+
// within a test that is expected to fail.
|
|
134
|
+
// The current convertStatus handles the overall testCase expectedStatus.
|
|
135
|
+
// For step-specific error messages when testCase.expectedStatus === 'failed':
|
|
136
|
+
if (testCase.expectedStatus === "failed") {
|
|
137
|
+
if (step.error) {
|
|
138
|
+
// If the step itself has an error
|
|
139
|
+
// errorMessage is already set from step.error.message
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// If a step within an expected-to-fail test passes, it's usually not an error for the step itself.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (testCase.expectedStatus === "skipped") {
|
|
146
|
+
// errorMessage is already set if step.error.message started with "Test is skipped:"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
144
149
|
return {
|
|
145
|
-
id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}
|
|
146
|
-
title:
|
|
150
|
+
id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}`,
|
|
151
|
+
title: stepTitle,
|
|
147
152
|
status: stepStatus,
|
|
148
153
|
duration: duration,
|
|
149
154
|
startTime: startTime,
|
|
@@ -158,38 +163,39 @@ class PlaywrightPulseReporter {
|
|
|
158
163
|
? "before"
|
|
159
164
|
: "after"
|
|
160
165
|
: undefined,
|
|
161
|
-
steps: [],
|
|
166
|
+
steps: [],
|
|
162
167
|
};
|
|
163
168
|
}
|
|
164
169
|
async onTestEnd(test, result) {
|
|
165
170
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
166
|
-
// Get the most accurate browser name
|
|
167
171
|
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
|
|
172
|
+
// Use project.name for a user-friendly display name
|
|
168
173
|
const browserName = ((_b = project === null || project === void 0 ? void 0 : project.use) === null || _b === void 0 ? void 0 : _b.defaultBrowserType) || "unknown";
|
|
174
|
+
// If you need the engine name (chromium, firefox, webkit)
|
|
175
|
+
// const browserEngineName = project?.use?.browserName || "unknown_engine";
|
|
169
176
|
const testStatus = convertStatus(result.status, test);
|
|
170
177
|
const startTime = new Date(result.startTime);
|
|
171
178
|
const endTime = new Date(startTime.getTime() + result.duration);
|
|
172
|
-
// Generate a slightly more robust ID for attachments, especially if test.id is missing
|
|
173
179
|
const testIdForFiles = test.id ||
|
|
174
180
|
`${test
|
|
175
181
|
.titlePath()
|
|
176
182
|
.join("_")
|
|
177
183
|
.replace(/[^a-zA-Z0-9]/g, "_")}_${startTime.getTime()}`;
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
const processAllSteps = async (steps
|
|
185
|
+
// parentTestStatus parameter was not used, removed for now.
|
|
186
|
+
// If needed for inherited status logic for steps, it can be re-added.
|
|
187
|
+
) => {
|
|
180
188
|
let processed = [];
|
|
181
189
|
for (const step of steps) {
|
|
182
|
-
const processedStep = await this.processStep(step, testIdForFiles, browserName
|
|
190
|
+
const processedStep = await this.processStep(step, testIdForFiles, browserName, // Pass display name
|
|
191
|
+
test);
|
|
183
192
|
processed.push(processedStep);
|
|
184
193
|
if (step.steps && step.steps.length > 0) {
|
|
185
|
-
|
|
186
|
-
// Assign nested steps correctly
|
|
187
|
-
processedStep.steps = nestedSteps;
|
|
194
|
+
processedStep.steps = await processAllSteps(step.steps);
|
|
188
195
|
}
|
|
189
196
|
}
|
|
190
197
|
return processed;
|
|
191
198
|
};
|
|
192
|
-
// --- Extract Code Snippet ---
|
|
193
199
|
let codeSnippet = undefined;
|
|
194
200
|
try {
|
|
195
201
|
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,31 +206,63 @@ class PlaywrightPulseReporter {
|
|
|
200
206
|
catch (e) {
|
|
201
207
|
console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
|
|
202
208
|
}
|
|
203
|
-
// ---
|
|
209
|
+
// --- Capture stdout and stderr ---
|
|
210
|
+
const stdoutMessages = [];
|
|
211
|
+
if (result.stdout && result.stdout.length > 0) {
|
|
212
|
+
result.stdout.forEach((item) => {
|
|
213
|
+
if (typeof item === "string") {
|
|
214
|
+
stdoutMessages.push(item);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
// If item is not a string, Playwright's typings indicate it's a Buffer (or Buffer-like).
|
|
218
|
+
// We must call toString() on it.
|
|
219
|
+
// The 'item' here is typed as 'Buffer' from the 'else' branch of '(string | Buffer)[]'
|
|
220
|
+
stdoutMessages.push(item.toString());
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const stderrMessages = [];
|
|
225
|
+
if (result.stderr && result.stderr.length > 0) {
|
|
226
|
+
result.stderr.forEach((item) => {
|
|
227
|
+
if (typeof item === "string") {
|
|
228
|
+
stderrMessages.push(item);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
// If item is not a string, Playwright's typings indicate it's a Buffer (or Buffer-like).
|
|
232
|
+
// We must call toString() on it.
|
|
233
|
+
stderrMessages.push(item.toString());
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// --- End capture stdout and stderr ---
|
|
204
238
|
const pulseResult = {
|
|
205
|
-
id: test.id || `${test.title}-${startTime.toISOString()}-${(0, crypto_1.randomUUID)()}`,
|
|
206
|
-
runId: "TBD",
|
|
239
|
+
id: test.id || `${test.title}-${startTime.toISOString()}-${(0, crypto_1.randomUUID)()}`,
|
|
240
|
+
runId: "TBD",
|
|
207
241
|
name: test.titlePath().join(" > "),
|
|
208
|
-
|
|
242
|
+
// Use project.name for suiteName if desired, or fallback
|
|
243
|
+
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
244
|
status: testStatus,
|
|
210
245
|
duration: result.duration,
|
|
211
246
|
startTime: startTime,
|
|
212
247
|
endTime: endTime,
|
|
213
|
-
browser: browserName,
|
|
248
|
+
browser: browserName, // Use the user-friendly project name
|
|
214
249
|
retries: result.retry,
|
|
215
|
-
steps: ((_g = result.steps) === null || _g === void 0 ? void 0 : _g.length)
|
|
216
|
-
? await processAllSteps(result.steps, testStatus)
|
|
217
|
-
: [],
|
|
250
|
+
steps: ((_g = result.steps) === null || _g === void 0 ? void 0 : _g.length) ? await processAllSteps(result.steps) : [],
|
|
218
251
|
errorMessage: (_h = result.error) === null || _h === void 0 ? void 0 : _h.message,
|
|
219
252
|
stackTrace: (_j = result.error) === null || _j === void 0 ? void 0 : _j.stack,
|
|
220
253
|
codeSnippet: codeSnippet,
|
|
221
254
|
tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
|
|
222
|
-
screenshots: [],
|
|
255
|
+
screenshots: [], // Will be populated by attachFiles
|
|
223
256
|
videoPath: undefined,
|
|
224
257
|
tracePath: undefined,
|
|
258
|
+
// videoPath and tracePath might be deprecated if using the array versions above
|
|
259
|
+
// Depending on attachFiles implementation
|
|
260
|
+
// Add the captured console messages
|
|
261
|
+
stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
|
|
262
|
+
stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
|
|
225
263
|
};
|
|
226
|
-
// --- Process Attachments using the new utility ---
|
|
227
264
|
try {
|
|
265
|
+
// Pass this.options which should contain the resolved outputDir
|
|
228
266
|
(0, attachment_utils_1.attachFiles)(testIdForFiles, result, pulseResult, this.options);
|
|
229
267
|
}
|
|
230
268
|
catch (attachError) {
|
|
@@ -241,26 +279,18 @@ class PlaywrightPulseReporter {
|
|
|
241
279
|
}
|
|
242
280
|
async _writeShardResults() {
|
|
243
281
|
if (this.shardIndex === undefined) {
|
|
244
|
-
console.warn("Pulse Reporter: _writeShardResults called unexpectedly in main process. Skipping.");
|
|
282
|
+
// console.warn("Pulse Reporter: _writeShardResults called unexpectedly in main process. Skipping.");
|
|
245
283
|
return;
|
|
246
284
|
}
|
|
247
285
|
const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${this.shardIndex}.json`);
|
|
248
286
|
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}`);
|
|
287
|
+
await fs.writeFile(tempFilePath, JSON.stringify(this.results, (key, value) => (value instanceof Date ? value.toISOString() : value), 2));
|
|
257
288
|
}
|
|
258
289
|
catch (error) {
|
|
259
290
|
console.error(`Pulse Reporter: Shard ${this.shardIndex} failed to write temporary results to ${tempFilePath}`, error);
|
|
260
291
|
}
|
|
261
292
|
}
|
|
262
293
|
async _mergeShardResults(finalRunData) {
|
|
263
|
-
// console.log('Pulse Reporter: Merging results from shards...');
|
|
264
294
|
let allResults = [];
|
|
265
295
|
const totalShards = this.config.shard ? this.config.shard.total : 1;
|
|
266
296
|
for (let i = 0; i < totalShards; i++) {
|
|
@@ -270,18 +300,16 @@ class PlaywrightPulseReporter {
|
|
|
270
300
|
const shardResults = JSON.parse(content);
|
|
271
301
|
shardResults.forEach((r) => (r.runId = finalRunData.id));
|
|
272
302
|
allResults = allResults.concat(shardResults);
|
|
273
|
-
// console.log(`Pulse Reporter: Successfully merged ${shardResults.length} results from shard ${i}`);
|
|
274
303
|
}
|
|
275
304
|
catch (error) {
|
|
276
305
|
if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
|
|
277
|
-
console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}
|
|
306
|
+
console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}.`);
|
|
278
307
|
}
|
|
279
308
|
else {
|
|
280
|
-
console.error(`Pulse Reporter: Could not read
|
|
309
|
+
console.error(`Pulse Reporter: Could not read/parse results from shard ${i} (${tempFilePath}). Error:`, error);
|
|
281
310
|
}
|
|
282
311
|
}
|
|
283
312
|
}
|
|
284
|
-
// console.log(`Pulse Reporter: Merged a total of ${allResults.length} results from ${totalShards} shards.`);
|
|
285
313
|
finalRunData.passed = allResults.filter((r) => r.status === "passed").length;
|
|
286
314
|
finalRunData.failed = allResults.filter((r) => r.status === "failed").length;
|
|
287
315
|
finalRunData.skipped = allResults.filter((r) => r.status === "skipped").length;
|
|
@@ -290,9 +318,7 @@ class PlaywrightPulseReporter {
|
|
|
290
318
|
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
291
319
|
if (typeof value === "string" && isoDateRegex.test(value)) {
|
|
292
320
|
const date = new Date(value);
|
|
293
|
-
|
|
294
|
-
return date;
|
|
295
|
-
}
|
|
321
|
+
return !isNaN(date.getTime()) ? date : value;
|
|
296
322
|
}
|
|
297
323
|
return value;
|
|
298
324
|
};
|
|
@@ -305,34 +331,27 @@ class PlaywrightPulseReporter {
|
|
|
305
331
|
}
|
|
306
332
|
async _cleanupTemporaryFiles() {
|
|
307
333
|
try {
|
|
308
|
-
// No need to ensure dir exists here if handled in onBegin
|
|
309
334
|
const files = await fs.readdir(this.outputDir);
|
|
310
335
|
const tempFiles = files.filter((f) => f.startsWith(TEMP_SHARD_FILE_PREFIX));
|
|
311
336
|
if (tempFiles.length > 0) {
|
|
312
|
-
// console.log(`Pulse Reporter: Cleaning up ${tempFiles.length} temporary shard files...`);
|
|
313
337
|
await Promise.all(tempFiles.map((f) => fs.unlink(path.join(this.outputDir, f))));
|
|
314
338
|
}
|
|
315
339
|
}
|
|
316
340
|
catch (error) {
|
|
317
341
|
if ((error === null || error === void 0 ? void 0 : error.code) !== "ENOENT") {
|
|
318
|
-
// Ignore if the directory doesn't exist
|
|
319
342
|
console.error("Pulse Reporter: Error cleaning up temporary files:", error);
|
|
320
343
|
}
|
|
321
344
|
}
|
|
322
345
|
}
|
|
323
|
-
async _ensureDirExists(dirPath
|
|
346
|
+
async _ensureDirExists(dirPath) {
|
|
347
|
+
// Removed 'clean' parameter as it was unused
|
|
324
348
|
try {
|
|
325
|
-
if (clean) {
|
|
326
|
-
// console.log(`Pulse Reporter: Cleaning directory ${dirPath}...`);
|
|
327
|
-
await fs.rm(dirPath, { recursive: true, force: true });
|
|
328
|
-
}
|
|
329
349
|
await fs.mkdir(dirPath, { recursive: true });
|
|
330
350
|
}
|
|
331
351
|
catch (error) {
|
|
332
|
-
// Ignore EEXIST error if the directory already exists
|
|
333
352
|
if (error.code !== "EEXIST") {
|
|
334
353
|
console.error(`Pulse Reporter: Failed to ensure directory exists: ${dirPath}`, error);
|
|
335
|
-
throw error;
|
|
354
|
+
throw error;
|
|
336
355
|
}
|
|
337
356
|
}
|
|
338
357
|
}
|
|
@@ -340,16 +359,16 @@ class PlaywrightPulseReporter {
|
|
|
340
359
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
341
360
|
if (this.shardIndex !== undefined) {
|
|
342
361
|
await this._writeShardResults();
|
|
343
|
-
// console.log(`PlaywrightPulseReporter: Shard ${this.shardIndex + 1} finished writing results.`);
|
|
344
362
|
return;
|
|
345
363
|
}
|
|
346
364
|
const runEndTime = Date.now();
|
|
347
365
|
const duration = runEndTime - this.runStartTime;
|
|
366
|
+
// Consider making the UUID part truly random for each run if this ID needs to be globally unique over time
|
|
348
367
|
const runId = `run-${this.runStartTime}-${(0, crypto_1.randomUUID)()}`;
|
|
349
368
|
const runData = {
|
|
350
369
|
id: runId,
|
|
351
370
|
timestamp: new Date(this.runStartTime),
|
|
352
|
-
totalTests: 0,
|
|
371
|
+
totalTests: 0,
|
|
353
372
|
passed: 0,
|
|
354
373
|
failed: 0,
|
|
355
374
|
skipped: 0,
|
|
@@ -357,22 +376,34 @@ class PlaywrightPulseReporter {
|
|
|
357
376
|
};
|
|
358
377
|
let finalReport;
|
|
359
378
|
if (this.isSharded) {
|
|
360
|
-
// console.log("Pulse Reporter: Run ended, main process merging shard results...");
|
|
361
379
|
finalReport = await this._mergeShardResults(runData);
|
|
362
380
|
}
|
|
363
381
|
else {
|
|
364
|
-
|
|
365
|
-
this.results.forEach((r) => (r.runId = runId)); // Assign runId to directly collected results
|
|
382
|
+
this.results.forEach((r) => (r.runId = runId));
|
|
366
383
|
runData.passed = this.results.filter((r) => r.status === "passed").length;
|
|
367
384
|
runData.failed = this.results.filter((r) => r.status === "failed").length;
|
|
368
385
|
runData.skipped = this.results.filter((r) => r.status === "skipped").length;
|
|
369
386
|
runData.totalTests = this.results.length;
|
|
370
387
|
finalReport = {
|
|
371
388
|
run: runData,
|
|
372
|
-
results: this.results,
|
|
389
|
+
results: this.results,
|
|
373
390
|
metadata: { generatedAt: new Date().toISOString() },
|
|
374
391
|
};
|
|
375
392
|
}
|
|
393
|
+
// This block seems redundant as finalReport is already assigned above.
|
|
394
|
+
// if (this.isSharded) {
|
|
395
|
+
// finalReport = await this._mergeShardResults(runData);
|
|
396
|
+
// } else {
|
|
397
|
+
// this.results.forEach((r) => (r.runId = runId));
|
|
398
|
+
// runData.passed = this.results.filter((r) => r.status === "passed").length;
|
|
399
|
+
// runData.failed = this.results.filter((r) => r.status === "failed").length;
|
|
400
|
+
// runData.skipped = this.results.filter((r) => r.status === "skipped").length;
|
|
401
|
+
// runData.totalTests = this.results.length;
|
|
402
|
+
// finalReport = {
|
|
403
|
+
// run: runData, results: this.results,
|
|
404
|
+
// metadata: { generatedAt: new Date().toISOString() },
|
|
405
|
+
// };
|
|
406
|
+
// }
|
|
376
407
|
const finalRunStatus = ((_b = (_a = finalReport.run) === null || _a === void 0 ? void 0 : _a.failed) !== null && _b !== void 0 ? _b : 0 > 0)
|
|
377
408
|
? "failed"
|
|
378
409
|
: ((_c = finalReport.run) === null || _c === void 0 ? void 0 : _c.totalTests) === 0
|
|
@@ -391,30 +422,21 @@ PlaywrightPulseReporter: Run Finished
|
|
|
391
422
|
console.log(summary);
|
|
392
423
|
const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
|
|
393
424
|
try {
|
|
394
|
-
// Ensure directory exists before writing final report
|
|
395
425
|
await this._ensureDirExists(this.outputDir);
|
|
396
|
-
// --- Write Final JSON Report ---
|
|
397
426
|
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") {
|
|
427
|
+
if (value instanceof Date)
|
|
428
|
+
return value.toISOString();
|
|
429
|
+
if (typeof value === "bigint")
|
|
403
430
|
return value.toString();
|
|
404
|
-
}
|
|
405
431
|
return value;
|
|
406
432
|
}, 2));
|
|
407
433
|
console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
|
|
408
|
-
// REMOVED Static HTML Generation Call
|
|
409
|
-
// The reporter's responsibility is now only to create the JSON file.
|
|
410
|
-
// The user will run `npx generate-pulse-report` separately.
|
|
411
434
|
}
|
|
412
435
|
catch (error) {
|
|
413
436
|
console.error(`Pulse Reporter: Failed to write final JSON report to ${finalOutputPath}. Error: ${error.message}`);
|
|
414
437
|
}
|
|
415
438
|
finally {
|
|
416
439
|
if (this.isSharded) {
|
|
417
|
-
// console.log("Pulse Reporter: Cleaning up temporary shard files...");
|
|
418
440
|
await this._cleanupTemporaryFiles();
|
|
419
441
|
}
|
|
420
442
|
}
|
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.
|
|
4
|
+
"version": "0.2.0",
|
|
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-excel.mjs"
|
|
30
32
|
},
|
|
31
33
|
"exports": {
|
|
32
34
|
".": {
|
|
@@ -75,15 +77,18 @@
|
|
|
75
77
|
"@radix-ui/react-tooltip": "^1.1.8",
|
|
76
78
|
"@tanstack-query-firebase/react": "^1.0.5",
|
|
77
79
|
"@tanstack/react-query": "^5.66.0",
|
|
80
|
+
"archiver": "^7.0.1",
|
|
78
81
|
"class-variance-authority": "^0.7.1",
|
|
79
82
|
"clsx": "^2.1.1",
|
|
80
83
|
"d3": "^7.9.0",
|
|
81
84
|
"date-fns": "^3.6.0",
|
|
85
|
+
"dotenv": "^16.5.0",
|
|
82
86
|
"firebase": "^11.3.0",
|
|
83
87
|
"genkit": "^1.6.2",
|
|
84
88
|
"jsdom": "^26.1.0",
|
|
85
89
|
"lucide-react": "^0.475.0",
|
|
86
90
|
"next": "15.2.3",
|
|
91
|
+
"nodemailer": "^7.0.3",
|
|
87
92
|
"patch-package": "^8.0.0",
|
|
88
93
|
"react": "^18.3.1",
|
|
89
94
|
"react-day-picker": "^8.10.1",
|
|
@@ -92,10 +97,8 @@
|
|
|
92
97
|
"recharts": "^2.15.1",
|
|
93
98
|
"tailwind-merge": "^3.0.1",
|
|
94
99
|
"tailwindcss-animate": "^1.0.7",
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"dotenv": "^16.5.0",
|
|
98
|
-
"nodemailer": "^7.0.3"
|
|
100
|
+
"xlsx": "^0.18.5",
|
|
101
|
+
"zod": "^3.24.2"
|
|
99
102
|
},
|
|
100
103
|
"devDependencies": {
|
|
101
104
|
"@types/node": "^20",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|