@arghajit/playwright-pulse-report 0.2.0 → 0.2.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 +120 -55
- package/dist/reporter/playwright-pulse-reporter.d.ts +2 -0
- package/dist/reporter/playwright-pulse-reporter.js +229 -116
- package/dist/types/index.d.ts +17 -0
- package/package.json +12 -52
- package/scripts/generate-email-report.mjs +714 -0
- package/scripts/generate-report.mjs +2277 -0
- package/scripts/generate-static-report.mjs +1515 -1436
- package/scripts/generate-trend.mjs +165 -0
- package/scripts/merge-pulse-report.js +1 -0
- package/scripts/{sendReport.js → sendReport.mjs} +138 -71
- 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-trend-excel.mjs +0 -273
|
@@ -39,9 +39,11 @@ const fs = __importStar(require("fs/promises"));
|
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const crypto_1 = require("crypto");
|
|
41
41
|
const attachment_utils_1 = require("./attachment-utils"); // Use relative path
|
|
42
|
+
const ua_parser_js_1 = require("ua-parser-js"); // Added UAParser import
|
|
43
|
+
const os = __importStar(require("os"));
|
|
42
44
|
const convertStatus = (status, testCase) => {
|
|
43
45
|
if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
|
|
44
|
-
return
|
|
46
|
+
return "failed";
|
|
45
47
|
}
|
|
46
48
|
if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "skipped") {
|
|
47
49
|
return "skipped";
|
|
@@ -94,24 +96,77 @@ class PlaywrightPulseReporter {
|
|
|
94
96
|
: undefined;
|
|
95
97
|
this._ensureDirExists(this.outputDir)
|
|
96
98
|
.then(() => {
|
|
97
|
-
if (this.shardIndex === undefined) {
|
|
99
|
+
if (this.shardIndex === undefined || this.shardIndex === 0) {
|
|
98
100
|
console.log(`PlaywrightPulseReporter: Starting test run with ${suite.allTests().length} tests${this.isSharded ? ` across ${totalShards} shards` : ""}. Pulse outputting to ${this.outputDir}`);
|
|
99
|
-
|
|
101
|
+
if (this.shardIndex === undefined ||
|
|
102
|
+
(this.isSharded && this.shardIndex === 0)) {
|
|
103
|
+
return this._cleanupTemporaryFiles();
|
|
104
|
+
}
|
|
100
105
|
}
|
|
101
106
|
})
|
|
102
107
|
.catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
|
|
103
108
|
}
|
|
104
109
|
onTestBegin(test) {
|
|
105
|
-
|
|
110
|
+
console.log(`Starting test: ${test.title}`);
|
|
111
|
+
}
|
|
112
|
+
getBrowserDetails(test) {
|
|
113
|
+
var _a, _b, _c, _d;
|
|
114
|
+
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project(); // project() can return undefined if not in a project context
|
|
115
|
+
const projectConfig = project === null || project === void 0 ? void 0 : project.use; // This is where options like userAgent, defaultBrowserType are
|
|
116
|
+
const userAgent = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.userAgent;
|
|
117
|
+
const configuredBrowserType = (_b = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.browserName) === null || _b === void 0 ? void 0 : _b.toLowerCase();
|
|
118
|
+
const parser = new ua_parser_js_1.UAParser(userAgent);
|
|
119
|
+
const result = parser.getResult();
|
|
120
|
+
let browserName = result.browser.name;
|
|
121
|
+
const browserVersion = result.browser.version
|
|
122
|
+
? ` v${result.browser.version.split(".")[0]}`
|
|
123
|
+
: ""; // Major version
|
|
124
|
+
const osName = result.os.name ? ` on ${result.os.name}` : "";
|
|
125
|
+
const osVersion = result.os.version
|
|
126
|
+
? ` ${result.os.version.split(".")[0]}`
|
|
127
|
+
: ""; // Major version
|
|
128
|
+
const deviceType = result.device.type; // "mobile", "tablet", etc.
|
|
129
|
+
let finalString;
|
|
130
|
+
// If UAParser couldn't determine browser name, fallback to configured type
|
|
131
|
+
if (browserName === undefined) {
|
|
132
|
+
browserName = configuredBrowserType;
|
|
133
|
+
finalString = `${browserName}`;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// Specific refinements for mobile based on parsed OS and device type
|
|
137
|
+
if (deviceType === "mobile" || deviceType === "tablet") {
|
|
138
|
+
if ((_c = result.os.name) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes("android")) {
|
|
139
|
+
if (browserName.toLowerCase().includes("chrome"))
|
|
140
|
+
browserName = "Chrome Mobile";
|
|
141
|
+
else if (browserName.toLowerCase().includes("firefox"))
|
|
142
|
+
browserName = "Firefox Mobile";
|
|
143
|
+
else if (result.engine.name === "Blink" && !result.browser.name)
|
|
144
|
+
browserName = "Android WebView";
|
|
145
|
+
else if (browserName &&
|
|
146
|
+
!browserName.toLowerCase().includes("mobile")) {
|
|
147
|
+
// Keep it as is, e.g. "Samsung Browser" is specific enough
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
browserName = "Android Browser"; // default for android if not specific
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if ((_d = result.os.name) === null || _d === void 0 ? void 0 : _d.toLowerCase().includes("ios")) {
|
|
154
|
+
browserName = "Mobile Safari";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (browserName === "Electron") {
|
|
158
|
+
browserName = "Electron App";
|
|
159
|
+
}
|
|
160
|
+
finalString = `${browserName}${browserVersion}${osName}${osVersion}`;
|
|
161
|
+
}
|
|
162
|
+
return finalString.trim();
|
|
106
163
|
}
|
|
107
|
-
async processStep(step, testId,
|
|
108
|
-
testCase) {
|
|
164
|
+
async processStep(step, testId, browserDetails, testCase) {
|
|
109
165
|
var _a, _b, _c, _d;
|
|
110
166
|
let stepStatus = "passed";
|
|
111
167
|
let errorMessage = ((_a = step.error) === null || _a === void 0 ? void 0 : _a.message) || undefined;
|
|
112
168
|
if ((_c = (_b = step.error) === null || _b === void 0 ? void 0 : _b.message) === null || _c === void 0 ? void 0 : _c.startsWith("Test is skipped:")) {
|
|
113
169
|
stepStatus = "skipped";
|
|
114
|
-
errorMessage = "Info: Test is skipped:";
|
|
115
170
|
}
|
|
116
171
|
else {
|
|
117
172
|
stepStatus = convertStatus(step.error ? "failed" : "passed", testCase);
|
|
@@ -124,28 +179,6 @@ class PlaywrightPulseReporter {
|
|
|
124
179
|
codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
|
|
125
180
|
}
|
|
126
181
|
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
|
-
}
|
|
149
182
|
return {
|
|
150
183
|
id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}`,
|
|
151
184
|
title: stepTitle,
|
|
@@ -153,7 +186,7 @@ class PlaywrightPulseReporter {
|
|
|
153
186
|
duration: duration,
|
|
154
187
|
startTime: startTime,
|
|
155
188
|
endTime: endTime,
|
|
156
|
-
browser:
|
|
189
|
+
browser: browserDetails,
|
|
157
190
|
errorMessage: errorMessage,
|
|
158
191
|
stackTrace: ((_d = step.error) === null || _d === void 0 ? void 0 : _d.stack) || undefined,
|
|
159
192
|
codeLocation: codeLocation || undefined,
|
|
@@ -167,12 +200,9 @@ class PlaywrightPulseReporter {
|
|
|
167
200
|
};
|
|
168
201
|
}
|
|
169
202
|
async onTestEnd(test, result) {
|
|
170
|
-
var _a, _b, _c, _d, _e, _f, _g, _h
|
|
203
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
171
204
|
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
|
|
172
|
-
|
|
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";
|
|
205
|
+
const browserDetails = this.getBrowserDetails(test);
|
|
176
206
|
const testStatus = convertStatus(result.status, test);
|
|
177
207
|
const startTime = new Date(result.startTime);
|
|
178
208
|
const endTime = new Date(startTime.getTime() + result.duration);
|
|
@@ -181,14 +211,10 @@ class PlaywrightPulseReporter {
|
|
|
181
211
|
.titlePath()
|
|
182
212
|
.join("_")
|
|
183
213
|
.replace(/[^a-zA-Z0-9]/g, "_")}_${startTime.getTime()}`;
|
|
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
|
-
) => {
|
|
214
|
+
const processAllSteps = async (steps) => {
|
|
188
215
|
let processed = [];
|
|
189
216
|
for (const step of steps) {
|
|
190
|
-
const processedStep = await this.processStep(step, testIdForFiles,
|
|
191
|
-
test);
|
|
217
|
+
const processedStep = await this.processStep(step, testIdForFiles, browserDetails, test);
|
|
192
218
|
processed.push(processedStep);
|
|
193
219
|
if (step.steps && step.steps.length > 0) {
|
|
194
220
|
processedStep.steps = await processAllSteps(step.steps);
|
|
@@ -198,7 +224,7 @@ class PlaywrightPulseReporter {
|
|
|
198
224
|
};
|
|
199
225
|
let codeSnippet = undefined;
|
|
200
226
|
try {
|
|
201
|
-
if (((
|
|
227
|
+
if (((_b = test.location) === null || _b === void 0 ? void 0 : _b.file) && ((_c = test.location) === null || _c === void 0 ? void 0 : _c.line) && ((_d = test.location) === null || _d === void 0 ? void 0 : _d.column)) {
|
|
202
228
|
const relativePath = path.relative(this.config.rootDir, test.location.file);
|
|
203
229
|
codeSnippet = `Test defined at: ${relativePath}:${test.location.line}:${test.location.column}`;
|
|
204
230
|
}
|
|
@@ -206,69 +232,84 @@ class PlaywrightPulseReporter {
|
|
|
206
232
|
catch (e) {
|
|
207
233
|
console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
|
|
208
234
|
}
|
|
209
|
-
// --- Capture stdout and stderr ---
|
|
210
235
|
const stdoutMessages = [];
|
|
211
236
|
if (result.stdout && result.stdout.length > 0) {
|
|
212
237
|
result.stdout.forEach((item) => {
|
|
213
|
-
|
|
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
|
-
}
|
|
238
|
+
stdoutMessages.push(typeof item === "string" ? item : item.toString());
|
|
222
239
|
});
|
|
223
240
|
}
|
|
224
241
|
const stderrMessages = [];
|
|
225
242
|
if (result.stderr && result.stderr.length > 0) {
|
|
226
243
|
result.stderr.forEach((item) => {
|
|
227
|
-
|
|
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
|
-
}
|
|
244
|
+
stderrMessages.push(typeof item === "string" ? item : item.toString());
|
|
235
245
|
});
|
|
236
246
|
}
|
|
237
|
-
|
|
247
|
+
const uniqueTestId = test.id;
|
|
248
|
+
// --- REFINED THIS SECTION for testData ---
|
|
249
|
+
const maxWorkers = this.config.workers;
|
|
250
|
+
let mappedWorkerId;
|
|
251
|
+
// First, check for the special case where a test is not assigned a worker (e.g., global setup failure).
|
|
252
|
+
if (result.workerIndex === -1) {
|
|
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
|
+
}
|
|
265
|
+
const testSpecificData = {
|
|
266
|
+
workerId: mappedWorkerId,
|
|
267
|
+
uniqueWorkerIndex: result.workerIndex, // We'll keep the original for diagnostics
|
|
268
|
+
totalWorkers: maxWorkers,
|
|
269
|
+
configFile: this.config.configFile,
|
|
270
|
+
metadata: this.config.metadata
|
|
271
|
+
? JSON.stringify(this.config.metadata)
|
|
272
|
+
: undefined,
|
|
273
|
+
};
|
|
238
274
|
const pulseResult = {
|
|
239
|
-
id:
|
|
275
|
+
id: uniqueTestId,
|
|
240
276
|
runId: "TBD",
|
|
241
277
|
name: test.titlePath().join(" > "),
|
|
242
|
-
|
|
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",
|
|
278
|
+
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",
|
|
244
279
|
status: testStatus,
|
|
245
280
|
duration: result.duration,
|
|
246
281
|
startTime: startTime,
|
|
247
282
|
endTime: endTime,
|
|
248
|
-
browser:
|
|
283
|
+
browser: browserDetails,
|
|
249
284
|
retries: result.retry,
|
|
250
|
-
steps: ((
|
|
251
|
-
errorMessage: (
|
|
252
|
-
stackTrace: (
|
|
285
|
+
steps: ((_f = result.steps) === null || _f === void 0 ? void 0 : _f.length) ? await processAllSteps(result.steps) : [],
|
|
286
|
+
errorMessage: (_g = result.error) === null || _g === void 0 ? void 0 : _g.message,
|
|
287
|
+
stackTrace: (_h = result.error) === null || _h === void 0 ? void 0 : _h.stack,
|
|
253
288
|
codeSnippet: codeSnippet,
|
|
254
289
|
tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
|
|
255
|
-
screenshots: [],
|
|
290
|
+
screenshots: [],
|
|
256
291
|
videoPath: undefined,
|
|
257
292
|
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
293
|
stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
|
|
262
294
|
stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
|
|
295
|
+
// --- UPDATED THESE LINES from testSpecificData ---
|
|
296
|
+
...testSpecificData,
|
|
263
297
|
};
|
|
264
298
|
try {
|
|
265
|
-
// Pass this.options which should contain the resolved outputDir
|
|
266
299
|
(0, attachment_utils_1.attachFiles)(testIdForFiles, result, pulseResult, this.options);
|
|
267
300
|
}
|
|
268
301
|
catch (attachError) {
|
|
269
302
|
console.error(`Pulse Reporter: Error processing attachments for test ${pulseResult.name} (ID: ${testIdForFiles}): ${attachError.message}`);
|
|
270
303
|
}
|
|
271
|
-
this.results.
|
|
304
|
+
const existingTestIndex = this.results.findIndex((r) => r.id === uniqueTestId);
|
|
305
|
+
if (existingTestIndex !== -1) {
|
|
306
|
+
if (pulseResult.retries >= this.results[existingTestIndex].retries) {
|
|
307
|
+
this.results[existingTestIndex] = pulseResult;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
this.results.push(pulseResult);
|
|
312
|
+
}
|
|
272
313
|
}
|
|
273
314
|
onError(error) {
|
|
274
315
|
var _a;
|
|
@@ -277,9 +318,22 @@ class PlaywrightPulseReporter {
|
|
|
277
318
|
console.error(error.stack);
|
|
278
319
|
}
|
|
279
320
|
}
|
|
321
|
+
_getEnvDetails() {
|
|
322
|
+
return {
|
|
323
|
+
host: os.hostname(),
|
|
324
|
+
os: `${os.platform()} ${os.release()}`,
|
|
325
|
+
cpu: {
|
|
326
|
+
model: os.cpus()[0] ? os.cpus()[0].model : "N/A", // Handle cases with no CPU info
|
|
327
|
+
cores: os.cpus().length,
|
|
328
|
+
},
|
|
329
|
+
memory: `${(os.totalmem() / 1024 ** 3).toFixed(2)}GB`, // Total RAM in GB
|
|
330
|
+
node: process.version,
|
|
331
|
+
v8: process.versions.v8,
|
|
332
|
+
cwd: process.cwd(),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
280
335
|
async _writeShardResults() {
|
|
281
336
|
if (this.shardIndex === undefined) {
|
|
282
|
-
// console.warn("Pulse Reporter: _writeShardResults called unexpectedly in main process. Skipping.");
|
|
283
337
|
return;
|
|
284
338
|
}
|
|
285
339
|
const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${this.shardIndex}.json`);
|
|
@@ -291,29 +345,38 @@ class PlaywrightPulseReporter {
|
|
|
291
345
|
}
|
|
292
346
|
}
|
|
293
347
|
async _mergeShardResults(finalRunData) {
|
|
294
|
-
let
|
|
348
|
+
let allShardProcessedResults = [];
|
|
295
349
|
const totalShards = this.config.shard ? this.config.shard.total : 1;
|
|
296
350
|
for (let i = 0; i < totalShards; i++) {
|
|
297
351
|
const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
|
|
298
352
|
try {
|
|
299
353
|
const content = await fs.readFile(tempFilePath, "utf-8");
|
|
300
354
|
const shardResults = JSON.parse(content);
|
|
301
|
-
|
|
302
|
-
|
|
355
|
+
allShardProcessedResults =
|
|
356
|
+
allShardProcessedResults.concat(shardResults);
|
|
303
357
|
}
|
|
304
358
|
catch (error) {
|
|
305
359
|
if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
|
|
306
|
-
console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}.`);
|
|
360
|
+
console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}. This might be normal if a shard had no tests or failed early.`);
|
|
307
361
|
}
|
|
308
362
|
else {
|
|
309
363
|
console.error(`Pulse Reporter: Could not read/parse results from shard ${i} (${tempFilePath}). Error:`, error);
|
|
310
364
|
}
|
|
311
365
|
}
|
|
312
366
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
367
|
+
let finalUniqueResultsMap = new Map();
|
|
368
|
+
for (const result of allShardProcessedResults) {
|
|
369
|
+
const existing = finalUniqueResultsMap.get(result.id);
|
|
370
|
+
if (!existing || result.retries >= existing.retries) {
|
|
371
|
+
finalUniqueResultsMap.set(result.id, result);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const finalResultsList = Array.from(finalUniqueResultsMap.values());
|
|
375
|
+
finalResultsList.forEach((r) => (r.runId = finalRunData.id));
|
|
376
|
+
finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
|
|
377
|
+
finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
|
|
378
|
+
finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
|
|
379
|
+
finalRunData.totalTests = finalResultsList.length;
|
|
317
380
|
const reviveDates = (key, value) => {
|
|
318
381
|
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
319
382
|
if (typeof value === "string" && isoDateRegex.test(value)) {
|
|
@@ -322,10 +385,10 @@ class PlaywrightPulseReporter {
|
|
|
322
385
|
}
|
|
323
386
|
return value;
|
|
324
387
|
};
|
|
325
|
-
const
|
|
388
|
+
const properlyTypedResults = JSON.parse(JSON.stringify(finalResultsList), reviveDates);
|
|
326
389
|
return {
|
|
327
390
|
run: finalRunData,
|
|
328
|
-
results:
|
|
391
|
+
results: properlyTypedResults,
|
|
329
392
|
metadata: { generatedAt: new Date().toISOString() },
|
|
330
393
|
};
|
|
331
394
|
}
|
|
@@ -339,12 +402,11 @@ class PlaywrightPulseReporter {
|
|
|
339
402
|
}
|
|
340
403
|
catch (error) {
|
|
341
404
|
if ((error === null || error === void 0 ? void 0 : error.code) !== "ENOENT") {
|
|
342
|
-
console.
|
|
405
|
+
console.warn("Pulse Reporter: Warning during cleanup of temporary files:", error.message);
|
|
343
406
|
}
|
|
344
407
|
}
|
|
345
408
|
}
|
|
346
409
|
async _ensureDirExists(dirPath) {
|
|
347
|
-
// Removed 'clean' parameter as it was unused
|
|
348
410
|
try {
|
|
349
411
|
await fs.mkdir(dirPath, { recursive: true });
|
|
350
412
|
}
|
|
@@ -356,15 +418,16 @@ class PlaywrightPulseReporter {
|
|
|
356
418
|
}
|
|
357
419
|
}
|
|
358
420
|
async onEnd(result) {
|
|
359
|
-
var _a, _b, _c
|
|
421
|
+
var _a, _b, _c;
|
|
360
422
|
if (this.shardIndex !== undefined) {
|
|
361
423
|
await this._writeShardResults();
|
|
362
424
|
return;
|
|
363
425
|
}
|
|
364
426
|
const runEndTime = Date.now();
|
|
365
427
|
const duration = runEndTime - this.runStartTime;
|
|
366
|
-
|
|
367
|
-
|
|
428
|
+
const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`; // Need not to change
|
|
429
|
+
// --- CALLING _getEnvDetails HERE ---
|
|
430
|
+
const environmentDetails = this._getEnvDetails();
|
|
368
431
|
const runData = {
|
|
369
432
|
id: runId,
|
|
370
433
|
timestamp: new Date(this.runStartTime),
|
|
@@ -373,10 +436,16 @@ class PlaywrightPulseReporter {
|
|
|
373
436
|
failed: 0,
|
|
374
437
|
skipped: 0,
|
|
375
438
|
duration,
|
|
439
|
+
// --- ADDED environmentDetails HERE ---
|
|
440
|
+
environment: environmentDetails,
|
|
376
441
|
};
|
|
377
|
-
let finalReport;
|
|
442
|
+
let finalReport = undefined; // Initialize as undefined
|
|
378
443
|
if (this.isSharded) {
|
|
379
444
|
finalReport = await this._mergeShardResults(runData);
|
|
445
|
+
// Ensured environment details are on the final merged runData if not already
|
|
446
|
+
if (finalReport && finalReport.run && !finalReport.run.environment) {
|
|
447
|
+
finalReport.run.environment = environmentDetails;
|
|
448
|
+
}
|
|
380
449
|
}
|
|
381
450
|
else {
|
|
382
451
|
this.results.forEach((r) => (r.runId = runId));
|
|
@@ -384,42 +453,84 @@ class PlaywrightPulseReporter {
|
|
|
384
453
|
runData.failed = this.results.filter((r) => r.status === "failed").length;
|
|
385
454
|
runData.skipped = this.results.filter((r) => r.status === "skipped").length;
|
|
386
455
|
runData.totalTests = this.results.length;
|
|
456
|
+
const reviveDates = (key, value) => {
|
|
457
|
+
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
458
|
+
if (typeof value === "string" && isoDateRegex.test(value)) {
|
|
459
|
+
const date = new Date(value);
|
|
460
|
+
return !isNaN(date.getTime()) ? date : value;
|
|
461
|
+
}
|
|
462
|
+
return value;
|
|
463
|
+
};
|
|
464
|
+
const properlyTypedResults = JSON.parse(JSON.stringify(this.results), reviveDates);
|
|
387
465
|
finalReport = {
|
|
388
466
|
run: runData,
|
|
389
|
-
results:
|
|
467
|
+
results: properlyTypedResults,
|
|
390
468
|
metadata: { generatedAt: new Date().toISOString() },
|
|
391
469
|
};
|
|
392
470
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
471
|
+
if (!finalReport) {
|
|
472
|
+
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
|
+
return;
|
|
512
|
+
}
|
|
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
|
|
408
515
|
? "failed"
|
|
409
|
-
: ((
|
|
410
|
-
?
|
|
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"
|
|
411
520
|
: "passed";
|
|
412
521
|
const summary = `
|
|
413
522
|
PlaywrightPulseReporter: Run Finished
|
|
414
523
|
-----------------------------------------
|
|
415
524
|
Overall Status: ${finalRunStatus.toUpperCase()}
|
|
416
|
-
Total Tests: ${(
|
|
417
|
-
Passed: ${
|
|
418
|
-
Failed: ${
|
|
419
|
-
Skipped: ${
|
|
420
|
-
Duration: ${(duration / 1000).toFixed(2)}s
|
|
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
|
|
421
530
|
-----------------------------------------`;
|
|
422
|
-
|
|
531
|
+
if (this.printsToStdio()) {
|
|
532
|
+
console.log(summary);
|
|
533
|
+
}
|
|
423
534
|
const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
|
|
424
535
|
try {
|
|
425
536
|
await this._ensureDirExists(this.outputDir);
|
|
@@ -430,7 +541,9 @@ PlaywrightPulseReporter: Run Finished
|
|
|
430
541
|
return value.toString();
|
|
431
542
|
return value;
|
|
432
543
|
}, 2));
|
|
433
|
-
|
|
544
|
+
if (this.printsToStdio()) {
|
|
545
|
+
console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
|
|
546
|
+
}
|
|
434
547
|
}
|
|
435
548
|
catch (error) {
|
|
436
549
|
console.error(`Pulse Reporter: Failed to write final JSON report to ${finalOutputPath}. Error: ${error.message}`);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -36,6 +36,10 @@ export interface TestResult {
|
|
|
36
36
|
tracePath?: string;
|
|
37
37
|
stdout?: string[];
|
|
38
38
|
stderr?: string[];
|
|
39
|
+
workerId?: number;
|
|
40
|
+
totalWorkers?: number;
|
|
41
|
+
configFile?: string;
|
|
42
|
+
metadata?: string;
|
|
39
43
|
}
|
|
40
44
|
export interface TestRun {
|
|
41
45
|
id: string;
|
|
@@ -45,6 +49,7 @@ export interface TestRun {
|
|
|
45
49
|
failed: number;
|
|
46
50
|
skipped: number;
|
|
47
51
|
duration: number;
|
|
52
|
+
environment?: EnvDetails;
|
|
48
53
|
}
|
|
49
54
|
export interface TrendDataPoint {
|
|
50
55
|
date: string;
|
|
@@ -63,3 +68,15 @@ export interface PlaywrightPulseReporterOptions {
|
|
|
63
68
|
outputDir?: string;
|
|
64
69
|
base64Images?: boolean;
|
|
65
70
|
}
|
|
71
|
+
export interface EnvDetails {
|
|
72
|
+
host: string;
|
|
73
|
+
os: string;
|
|
74
|
+
cpu: {
|
|
75
|
+
model: string;
|
|
76
|
+
cores: number;
|
|
77
|
+
};
|
|
78
|
+
memory: string;
|
|
79
|
+
node: string;
|
|
80
|
+
v8: string;
|
|
81
|
+
cwd: string;
|
|
82
|
+
}
|