@arghajit/dummy 0.1.0 → 0.1.2-beta-1
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 +136 -77
- package/dist/reporter/attachment-utils.js +41 -33
- package/dist/reporter/playwright-pulse-reporter.d.ts +8 -0
- package/dist/reporter/playwright-pulse-reporter.js +360 -151
- package/dist/reporter/tsconfig.reporter.tsbuildinfo +1 -0
- package/dist/types/index.d.ts +31 -4
- package/package.json +17 -6
- package/scripts/generate-email-report.mjs +714 -0
- package/scripts/generate-report.mjs +3034 -0
- package/scripts/generate-static-report.mjs +2201 -1286
- package/scripts/generate-trend.mjs +1 -1
- package/scripts/merge-pulse-report.js +1 -0
- package/scripts/{sendReport.js → sendReport.mjs} +143 -76
- 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
|
@@ -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,14 +37,25 @@ 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
|
|
40
|
+
const ua_parser_js_1 = require("ua-parser-js");
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const convertStatus = (status, testCase, retryCount = 0) => {
|
|
43
|
+
// If a test passes on a retry, it's considered flaky regardless of expected status.
|
|
44
|
+
// This is the most critical check for flaky tests.
|
|
45
|
+
if (status === "passed" && retryCount > 0) {
|
|
46
|
+
return "flaky";
|
|
47
|
+
}
|
|
48
|
+
// Handle expected statuses for the final result.
|
|
43
49
|
if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
|
|
50
|
+
// If expected to fail but passed, it's flaky.
|
|
51
|
+
if (status === "passed")
|
|
52
|
+
return "flaky";
|
|
44
53
|
return "failed";
|
|
45
54
|
}
|
|
46
55
|
if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "skipped") {
|
|
47
56
|
return "skipped";
|
|
48
57
|
}
|
|
58
|
+
// Default Playwright status mapping
|
|
49
59
|
switch (status) {
|
|
50
60
|
case "passed":
|
|
51
61
|
return "passed";
|
|
@@ -60,17 +70,21 @@ const convertStatus = (status, testCase) => {
|
|
|
60
70
|
};
|
|
61
71
|
const TEMP_SHARD_FILE_PREFIX = ".pulse-shard-results-";
|
|
62
72
|
const ATTACHMENTS_SUBDIR = "attachments";
|
|
73
|
+
const INDIVIDUAL_REPORTS_SUBDIR = "pulse-results";
|
|
63
74
|
class PlaywrightPulseReporter {
|
|
64
75
|
constructor(options = {}) {
|
|
65
|
-
var _a, _b;
|
|
76
|
+
var _a, _b, _c;
|
|
77
|
+
// This will now store all individual run attempts for all tests.
|
|
66
78
|
this.results = [];
|
|
67
79
|
this.baseOutputFile = "playwright-pulse-report.json";
|
|
68
80
|
this.isSharded = false;
|
|
69
81
|
this.shardIndex = undefined;
|
|
82
|
+
this.currentRunId = ""; // Added to store the overall run ID
|
|
70
83
|
this.options = options;
|
|
71
84
|
this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
|
|
72
85
|
this.outputDir = (_b = options.outputDir) !== null && _b !== void 0 ? _b : "pulse-report";
|
|
73
86
|
this.attachmentsDir = path.join(this.outputDir, ATTACHMENTS_SUBDIR);
|
|
87
|
+
this.resetOnEachRun = (_c = options.resetOnEachRun) !== null && _c !== void 0 ? _c : true;
|
|
74
88
|
}
|
|
75
89
|
printsToStdio() {
|
|
76
90
|
return this.shardIndex === undefined || this.shardIndex === 0;
|
|
@@ -80,6 +94,8 @@ class PlaywrightPulseReporter {
|
|
|
80
94
|
this.config = config;
|
|
81
95
|
this.suite = suite;
|
|
82
96
|
this.runStartTime = Date.now();
|
|
97
|
+
// Generate the overall runId once at the beginning
|
|
98
|
+
this.currentRunId = `run-${this.runStartTime}-${(0, crypto_1.randomUUID)()}`;
|
|
83
99
|
const configDir = this.config.rootDir;
|
|
84
100
|
const configFileDir = this.config.configFile
|
|
85
101
|
? path.dirname(this.config.configFile)
|
|
@@ -94,7 +110,7 @@ class PlaywrightPulseReporter {
|
|
|
94
110
|
: undefined;
|
|
95
111
|
this._ensureDirExists(this.outputDir)
|
|
96
112
|
.then(() => {
|
|
97
|
-
if (this.
|
|
113
|
+
if (this.printsToStdio()) {
|
|
98
114
|
console.log(`PlaywrightPulseReporter: Starting test run with ${suite.allTests().length} tests${this.isSharded ? ` across ${totalShards} shards` : ""}. Pulse outputting to ${this.outputDir}`);
|
|
99
115
|
if (this.shardIndex === undefined ||
|
|
100
116
|
(this.isSharded && this.shardIndex === 0)) {
|
|
@@ -105,9 +121,60 @@ class PlaywrightPulseReporter {
|
|
|
105
121
|
.catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
|
|
106
122
|
}
|
|
107
123
|
onTestBegin(test) {
|
|
108
|
-
// console.log(`Starting test: ${test.title}`);
|
|
124
|
+
// console.log(`Starting test: ${test.title}`); // Removed for brevity in final output
|
|
125
|
+
}
|
|
126
|
+
getBrowserDetails(test) {
|
|
127
|
+
var _a, _b, _c, _d;
|
|
128
|
+
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
|
|
129
|
+
const projectConfig = project === null || project === void 0 ? void 0 : project.use;
|
|
130
|
+
const userAgent = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.userAgent;
|
|
131
|
+
const configuredBrowserType = (_b = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.browserName) === null || _b === void 0 ? void 0 : _b.toLowerCase();
|
|
132
|
+
const parser = new ua_parser_js_1.UAParser(userAgent);
|
|
133
|
+
const result = parser.getResult();
|
|
134
|
+
let browserName = result.browser.name;
|
|
135
|
+
const browserVersion = result.browser.version
|
|
136
|
+
? ` v${result.browser.version.split(".")[0]}`
|
|
137
|
+
: "";
|
|
138
|
+
const osName = result.os.name ? ` on ${result.os.name}` : "";
|
|
139
|
+
const osVersion = result.os.version
|
|
140
|
+
? ` ${result.os.version.split(".")[0]}`
|
|
141
|
+
: "";
|
|
142
|
+
const deviceType = result.device.type;
|
|
143
|
+
let finalString;
|
|
144
|
+
if (browserName === undefined) {
|
|
145
|
+
browserName = configuredBrowserType;
|
|
146
|
+
finalString = `${browserName}`;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
if (deviceType === "mobile" || deviceType === "tablet") {
|
|
150
|
+
if ((_c = result.os.name) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes("android")) {
|
|
151
|
+
if (browserName.toLowerCase().includes("chrome"))
|
|
152
|
+
browserName = "Chrome Mobile";
|
|
153
|
+
else if (browserName.toLowerCase().includes("firefox"))
|
|
154
|
+
browserName = "Firefox Mobile";
|
|
155
|
+
else if (result.engine.name === "Blink" && !result.browser.name)
|
|
156
|
+
browserName = "Android WebView";
|
|
157
|
+
else if (browserName &&
|
|
158
|
+
!browserName.toLowerCase().includes("mobile")) {
|
|
159
|
+
// Keep it as is
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
browserName = "Android Browser";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if ((_d = result.os.name) === null || _d === void 0 ? void 0 : _d.toLowerCase().includes("ios")) {
|
|
166
|
+
browserName = "Mobile Safari";
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (browserName === "Electron") {
|
|
170
|
+
browserName = "Electron App";
|
|
171
|
+
}
|
|
172
|
+
finalString = `${browserName}${browserVersion}${osName}${osVersion}`;
|
|
173
|
+
}
|
|
174
|
+
return finalString.trim();
|
|
109
175
|
}
|
|
110
|
-
async processStep(step, testId,
|
|
176
|
+
async processStep(step, testId, browserDetails, testCase, retryCount = 0 // Pass retryCount to convertStatus for steps
|
|
177
|
+
) {
|
|
111
178
|
var _a, _b, _c, _d;
|
|
112
179
|
let stepStatus = "passed";
|
|
113
180
|
let errorMessage = ((_a = step.error) === null || _a === void 0 ? void 0 : _a.message) || undefined;
|
|
@@ -115,7 +182,8 @@ class PlaywrightPulseReporter {
|
|
|
115
182
|
stepStatus = "skipped";
|
|
116
183
|
}
|
|
117
184
|
else {
|
|
118
|
-
|
|
185
|
+
// Use the extended convertStatus
|
|
186
|
+
stepStatus = convertStatus(step.error ? "failed" : "passed", testCase, retryCount);
|
|
119
187
|
}
|
|
120
188
|
const duration = step.duration;
|
|
121
189
|
const startTime = new Date(step.startTime);
|
|
@@ -124,15 +192,14 @@ class PlaywrightPulseReporter {
|
|
|
124
192
|
if (step.location) {
|
|
125
193
|
codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
|
|
126
194
|
}
|
|
127
|
-
let stepTitle = step.title;
|
|
128
195
|
return {
|
|
129
196
|
id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}`,
|
|
130
|
-
title:
|
|
197
|
+
title: step.title,
|
|
131
198
|
status: stepStatus,
|
|
132
199
|
duration: duration,
|
|
133
200
|
startTime: startTime,
|
|
134
201
|
endTime: endTime,
|
|
135
|
-
browser:
|
|
202
|
+
browser: browserDetails,
|
|
136
203
|
errorMessage: errorMessage,
|
|
137
204
|
stackTrace: ((_d = step.error) === null || _d === void 0 ? void 0 : _d.stack) || undefined,
|
|
138
205
|
codeLocation: codeLocation || undefined,
|
|
@@ -146,21 +213,18 @@ class PlaywrightPulseReporter {
|
|
|
146
213
|
};
|
|
147
214
|
}
|
|
148
215
|
async onTestEnd(test, result) {
|
|
149
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
216
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
150
217
|
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
|
|
151
|
-
const
|
|
152
|
-
|
|
218
|
+
const browserDetails = this.getBrowserDetails(test);
|
|
219
|
+
// Use the extended convertStatus, passing result.retry
|
|
220
|
+
const testStatus = convertStatus(result.status, test, result.retry);
|
|
153
221
|
const startTime = new Date(result.startTime);
|
|
154
222
|
const endTime = new Date(startTime.getTime() + result.duration);
|
|
155
|
-
const testIdForFiles = test.id ||
|
|
156
|
-
`${test
|
|
157
|
-
.titlePath()
|
|
158
|
-
.join("_")
|
|
159
|
-
.replace(/[^a-zA-Z0-9]/g, "_")}_${startTime.getTime()}`;
|
|
160
223
|
const processAllSteps = async (steps) => {
|
|
161
224
|
let processed = [];
|
|
162
225
|
for (const step of steps) {
|
|
163
|
-
const processedStep = await this.processStep(step,
|
|
226
|
+
const processedStep = await this.processStep(step, test.id, browserDetails, test, result.retry // Pass retryCount to processStep
|
|
227
|
+
);
|
|
164
228
|
processed.push(processedStep);
|
|
165
229
|
if (step.steps && step.steps.length > 0) {
|
|
166
230
|
processedStep.steps = await processAllSteps(step.steps);
|
|
@@ -170,7 +234,7 @@ class PlaywrightPulseReporter {
|
|
|
170
234
|
};
|
|
171
235
|
let codeSnippet = undefined;
|
|
172
236
|
try {
|
|
173
|
-
if (((
|
|
237
|
+
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)) {
|
|
174
238
|
const relativePath = path.relative(this.config.rootDir, test.location.file);
|
|
175
239
|
codeSnippet = `Test defined at: ${relativePath}:${test.location.line}:${test.location.column}`;
|
|
176
240
|
}
|
|
@@ -178,55 +242,145 @@ class PlaywrightPulseReporter {
|
|
|
178
242
|
catch (e) {
|
|
179
243
|
console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
|
|
180
244
|
}
|
|
181
|
-
const stdoutMessages =
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
245
|
+
const stdoutMessages = result.stdout.map((item) => typeof item === "string" ? item : item.toString());
|
|
246
|
+
const stderrMessages = result.stderr.map((item) => typeof item === "string" ? item : item.toString());
|
|
247
|
+
const maxWorkers = this.config.workers;
|
|
248
|
+
let mappedWorkerId = result.workerIndex === -1
|
|
249
|
+
? -1
|
|
250
|
+
: (result.workerIndex % (maxWorkers > 0 ? maxWorkers : 1)) + 1;
|
|
251
|
+
const testSpecificData = {
|
|
252
|
+
workerId: mappedWorkerId,
|
|
253
|
+
totalWorkers: maxWorkers,
|
|
254
|
+
configFile: this.config.configFile,
|
|
255
|
+
metadata: this.config.metadata
|
|
256
|
+
? JSON.stringify(this.config.metadata)
|
|
257
|
+
: undefined,
|
|
258
|
+
};
|
|
259
|
+
// Correctly handle the ID for each run. A unique ID per attempt is crucial.
|
|
260
|
+
const testIdWithRunCounter = `${test.id}-run-${result.retry}`;
|
|
194
261
|
const pulseResult = {
|
|
195
|
-
id:
|
|
196
|
-
runId:
|
|
262
|
+
id: testIdWithRunCounter, // Use the modified ID
|
|
263
|
+
runId: this.currentRunId, // Assign the overall run ID
|
|
197
264
|
name: test.titlePath().join(" > "),
|
|
198
|
-
suiteName: (project === null || project === void 0 ? void 0 : project.name) || ((
|
|
265
|
+
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",
|
|
199
266
|
status: testStatus,
|
|
200
267
|
duration: result.duration,
|
|
201
268
|
startTime: startTime,
|
|
202
269
|
endTime: endTime,
|
|
203
|
-
browser:
|
|
204
|
-
retries: result.retry,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
270
|
+
browser: browserDetails,
|
|
271
|
+
retries: result.retry, // This is the Playwright retry count (0 for first run, 1 for first retry, etc.)
|
|
272
|
+
runCounter: result.retry, // This is your 'runCounter'
|
|
273
|
+
steps: ((_f = result.steps) === null || _f === void 0 ? void 0 : _f.length) ? await processAllSteps(result.steps) : [],
|
|
274
|
+
errorMessage: (_g = result.error) === null || _g === void 0 ? void 0 : _g.message,
|
|
275
|
+
stackTrace: (_h = result.error) === null || _h === void 0 ? void 0 : _h.stack,
|
|
276
|
+
snippet: (_j = result.error) === null || _j === void 0 ? void 0 : _j.snippet,
|
|
208
277
|
codeSnippet: codeSnippet,
|
|
209
278
|
tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
|
|
210
279
|
screenshots: [],
|
|
211
|
-
videoPath:
|
|
280
|
+
videoPath: [],
|
|
212
281
|
tracePath: undefined,
|
|
282
|
+
attachments: [],
|
|
213
283
|
stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
|
|
214
284
|
stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
|
|
285
|
+
...testSpecificData,
|
|
215
286
|
};
|
|
216
|
-
|
|
217
|
-
|
|
287
|
+
for (const [index, attachment] of result.attachments.entries()) {
|
|
288
|
+
if (!attachment.path)
|
|
289
|
+
continue;
|
|
290
|
+
try {
|
|
291
|
+
// Use the new testIdWithRunCounter for the subfolder
|
|
292
|
+
const testSubfolder = testIdWithRunCounter.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
293
|
+
const safeAttachmentName = path
|
|
294
|
+
.basename(attachment.path)
|
|
295
|
+
.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
296
|
+
const uniqueFileName = `${index}-${Date.now()}-${safeAttachmentName}`;
|
|
297
|
+
const relativeDestPath = path.join(ATTACHMENTS_SUBDIR, testSubfolder, uniqueFileName);
|
|
298
|
+
const absoluteDestPath = path.join(this.outputDir, relativeDestPath);
|
|
299
|
+
await this._ensureDirExists(path.dirname(absoluteDestPath));
|
|
300
|
+
await fs.copyFile(attachment.path, absoluteDestPath);
|
|
301
|
+
if (attachment.contentType.startsWith("image/")) {
|
|
302
|
+
(_k = pulseResult.screenshots) === null || _k === void 0 ? void 0 : _k.push(relativeDestPath);
|
|
303
|
+
}
|
|
304
|
+
else if (attachment.contentType.startsWith("video/")) {
|
|
305
|
+
(_l = pulseResult.videoPath) === null || _l === void 0 ? void 0 : _l.push(relativeDestPath);
|
|
306
|
+
}
|
|
307
|
+
else if (attachment.name === "trace") {
|
|
308
|
+
pulseResult.tracePath = relativeDestPath;
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
(_m = pulseResult.attachments) === null || _m === void 0 ? void 0 : _m.push({
|
|
312
|
+
name: attachment.name,
|
|
313
|
+
path: relativeDestPath,
|
|
314
|
+
contentType: attachment.contentType,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
console.error(`Pulse Reporter: Failed to process attachment "${attachment.name}" for test ${pulseResult.name}. Error: ${err.message}`);
|
|
320
|
+
}
|
|
218
321
|
}
|
|
219
|
-
|
|
220
|
-
|
|
322
|
+
this.results.push(pulseResult);
|
|
323
|
+
}
|
|
324
|
+
// New method to extract the base test ID, ignoring the run-counter suffix
|
|
325
|
+
_getBaseTestId(testResultId) {
|
|
326
|
+
const parts = testResultId.split("-run-");
|
|
327
|
+
return parts[0];
|
|
328
|
+
}
|
|
329
|
+
_getFinalizedResults(allResults) {
|
|
330
|
+
const finalResultsMap = new Map();
|
|
331
|
+
const allRunsMap = new Map();
|
|
332
|
+
// First, group all run attempts by their base test ID
|
|
333
|
+
for (const result of allResults) {
|
|
334
|
+
const baseTestId = this._getBaseTestId(result.id);
|
|
335
|
+
if (!allRunsMap.has(baseTestId)) {
|
|
336
|
+
allRunsMap.set(baseTestId, []);
|
|
337
|
+
}
|
|
338
|
+
allRunsMap.get(baseTestId).push(result);
|
|
221
339
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
340
|
+
// Now, iterate through the grouped runs to determine the final state
|
|
341
|
+
for (const [baseTestId, runs] of allRunsMap.entries()) {
|
|
342
|
+
let finalResult = undefined;
|
|
343
|
+
// Sort runs to process them in chronological order
|
|
344
|
+
runs.sort((a, b) => a.runCounter - b.runCounter);
|
|
345
|
+
for (const currentRun of runs) {
|
|
346
|
+
if (!finalResult) {
|
|
347
|
+
finalResult = currentRun;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
// Compare the current run to the best result found so far
|
|
351
|
+
const currentStatusOrder = this._getStatusOrder(currentRun.status);
|
|
352
|
+
const finalStatusOrder = this._getStatusOrder(finalResult.status);
|
|
353
|
+
if (currentStatusOrder < finalStatusOrder) {
|
|
354
|
+
// Current run is "better" (e.g., passed over failed)
|
|
355
|
+
finalResult = currentRun;
|
|
356
|
+
}
|
|
357
|
+
else if (currentStatusOrder === finalStatusOrder &&
|
|
358
|
+
currentRun.retries > finalResult.retries) {
|
|
359
|
+
// Same status, but prefer the latest attempt
|
|
360
|
+
finalResult = currentRun;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (finalResult) {
|
|
365
|
+
// Ensure the ID of the final result is the base test ID for de-duplication
|
|
366
|
+
finalResult.id = baseTestId;
|
|
367
|
+
finalResultsMap.set(baseTestId, finalResult);
|
|
226
368
|
}
|
|
227
369
|
}
|
|
228
|
-
|
|
229
|
-
|
|
370
|
+
return Array.from(finalResultsMap.values());
|
|
371
|
+
}
|
|
372
|
+
_getStatusOrder(status) {
|
|
373
|
+
switch (status) {
|
|
374
|
+
case "passed":
|
|
375
|
+
return 1;
|
|
376
|
+
case "flaky":
|
|
377
|
+
return 2;
|
|
378
|
+
case "failed":
|
|
379
|
+
return 3;
|
|
380
|
+
case "skipped":
|
|
381
|
+
return 4;
|
|
382
|
+
default:
|
|
383
|
+
return 99; // Unknown status
|
|
230
384
|
}
|
|
231
385
|
}
|
|
232
386
|
onError(error) {
|
|
@@ -236,6 +390,20 @@ class PlaywrightPulseReporter {
|
|
|
236
390
|
console.error(error.stack);
|
|
237
391
|
}
|
|
238
392
|
}
|
|
393
|
+
_getEnvDetails() {
|
|
394
|
+
return {
|
|
395
|
+
host: os.hostname(),
|
|
396
|
+
os: `${os.platform()} ${os.release()}`,
|
|
397
|
+
cpu: {
|
|
398
|
+
model: os.cpus()[0] ? os.cpus()[0].model : "N/A",
|
|
399
|
+
cores: os.cpus().length,
|
|
400
|
+
},
|
|
401
|
+
memory: `${(os.totalmem() / 1024 ** 3).toFixed(2)}GB`,
|
|
402
|
+
node: process.version,
|
|
403
|
+
v8: process.versions.v8,
|
|
404
|
+
cwd: process.cwd(),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
239
407
|
async _writeShardResults() {
|
|
240
408
|
if (this.shardIndex === undefined) {
|
|
241
409
|
return;
|
|
@@ -249,15 +417,14 @@ class PlaywrightPulseReporter {
|
|
|
249
417
|
}
|
|
250
418
|
}
|
|
251
419
|
async _mergeShardResults(finalRunData) {
|
|
252
|
-
let
|
|
420
|
+
let allShardRawResults = []; // Store raw results before final de-duplication
|
|
253
421
|
const totalShards = this.config.shard ? this.config.shard.total : 1;
|
|
254
422
|
for (let i = 0; i < totalShards; i++) {
|
|
255
423
|
const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
|
|
256
424
|
try {
|
|
257
425
|
const content = await fs.readFile(tempFilePath, "utf-8");
|
|
258
426
|
const shardResults = JSON.parse(content);
|
|
259
|
-
|
|
260
|
-
allShardProcessedResults.concat(shardResults);
|
|
427
|
+
allShardRawResults = allShardRawResults.concat(shardResults);
|
|
261
428
|
}
|
|
262
429
|
catch (error) {
|
|
263
430
|
if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
|
|
@@ -268,18 +435,14 @@ class PlaywrightPulseReporter {
|
|
|
268
435
|
}
|
|
269
436
|
}
|
|
270
437
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const existing = finalUniqueResultsMap.get(result.id);
|
|
274
|
-
if (!existing || result.retries >= existing.retries) {
|
|
275
|
-
finalUniqueResultsMap.set(result.id, result);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
const finalResultsList = Array.from(finalUniqueResultsMap.values());
|
|
438
|
+
// Apply _getFinalizedResults after all raw shard results are collected
|
|
439
|
+
const finalResultsList = this._getFinalizedResults(allShardRawResults);
|
|
279
440
|
finalResultsList.forEach((r) => (r.runId = finalRunData.id));
|
|
280
441
|
finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
|
|
281
442
|
finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
|
|
282
443
|
finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
|
|
444
|
+
// Add flaky count
|
|
445
|
+
finalRunData.flaky = finalResultsList.filter((r) => r.status === "flaky").length;
|
|
283
446
|
finalRunData.totalTests = finalResultsList.length;
|
|
284
447
|
const reviveDates = (key, value) => {
|
|
285
448
|
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
@@ -322,131 +485,177 @@ class PlaywrightPulseReporter {
|
|
|
322
485
|
}
|
|
323
486
|
}
|
|
324
487
|
async onEnd(result) {
|
|
325
|
-
var _a, _b, _c;
|
|
326
488
|
if (this.shardIndex !== undefined) {
|
|
327
489
|
await this._writeShardResults();
|
|
328
490
|
return;
|
|
329
491
|
}
|
|
492
|
+
// Now, `this.results` contains all individual run attempts.
|
|
493
|
+
// _getFinalizedResults will select the "best" run for each logical test.
|
|
494
|
+
const finalResults = this._getFinalizedResults(this.results);
|
|
330
495
|
const runEndTime = Date.now();
|
|
331
496
|
const duration = runEndTime - this.runStartTime;
|
|
332
|
-
|
|
497
|
+
// Use the stored overall runId
|
|
498
|
+
const runId = this.currentRunId;
|
|
499
|
+
const environmentDetails = this._getEnvDetails();
|
|
333
500
|
const runData = {
|
|
334
501
|
id: runId,
|
|
335
502
|
timestamp: new Date(this.runStartTime),
|
|
336
|
-
totalTests:
|
|
337
|
-
passed:
|
|
338
|
-
failed:
|
|
339
|
-
skipped:
|
|
503
|
+
totalTests: finalResults.length,
|
|
504
|
+
passed: finalResults.filter((r) => r.status === "passed").length,
|
|
505
|
+
failed: finalResults.filter((r) => r.status === "failed").length,
|
|
506
|
+
skipped: finalResults.filter((r) => r.status === "skipped").length,
|
|
507
|
+
flaky: finalResults.filter((r) => r.status === "flaky").length, // Add flaky count
|
|
340
508
|
duration,
|
|
509
|
+
environment: environmentDetails,
|
|
341
510
|
};
|
|
342
|
-
|
|
511
|
+
// Ensure all final results have the correct overall runId
|
|
512
|
+
finalResults.forEach((r) => (r.runId = runId));
|
|
513
|
+
let finalReport = undefined;
|
|
343
514
|
if (this.isSharded) {
|
|
515
|
+
// _mergeShardResults will now perform the final de-duplication across shards
|
|
344
516
|
finalReport = await this._mergeShardResults(runData);
|
|
345
517
|
}
|
|
346
518
|
else {
|
|
347
|
-
this.results.forEach((r) => (r.runId = runId));
|
|
348
|
-
runData.passed = this.results.filter((r) => r.status === "passed").length;
|
|
349
|
-
runData.failed = this.results.filter((r) => r.status === "failed").length;
|
|
350
|
-
runData.skipped = this.results.filter((r) => r.status === "skipped").length;
|
|
351
|
-
runData.totalTests = this.results.length;
|
|
352
|
-
const reviveDates = (key, value) => {
|
|
353
|
-
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
354
|
-
if (typeof value === "string" && isoDateRegex.test(value)) {
|
|
355
|
-
const date = new Date(value);
|
|
356
|
-
return !isNaN(date.getTime()) ? date : value;
|
|
357
|
-
}
|
|
358
|
-
return value;
|
|
359
|
-
};
|
|
360
|
-
const properlyTypedResults = JSON.parse(JSON.stringify(this.results), reviveDates);
|
|
361
519
|
finalReport = {
|
|
362
520
|
run: runData,
|
|
363
|
-
results:
|
|
521
|
+
results: finalResults, // Use the de-duplicated results for a non-sharded run
|
|
364
522
|
metadata: { generatedAt: new Date().toISOString() },
|
|
365
523
|
};
|
|
366
524
|
}
|
|
367
525
|
if (!finalReport) {
|
|
368
526
|
console.error("PlaywrightPulseReporter: CRITICAL - finalReport object was not generated. Cannot create summary.");
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
console.log(errorSummary);
|
|
381
|
-
}
|
|
382
|
-
const errorReport = {
|
|
383
|
-
run: {
|
|
384
|
-
id: runId,
|
|
385
|
-
timestamp: new Date(this.runStartTime),
|
|
386
|
-
totalTests: 0,
|
|
387
|
-
passed: 0,
|
|
388
|
-
failed: 0,
|
|
389
|
-
skipped: 0,
|
|
390
|
-
duration: duration,
|
|
391
|
-
},
|
|
392
|
-
results: [],
|
|
393
|
-
metadata: {
|
|
394
|
-
generatedAt: new Date().toISOString(),
|
|
395
|
-
},
|
|
396
|
-
};
|
|
397
|
-
const finalOutputPathOnError = path.join(this.outputDir, this.baseOutputFile);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const jsonReplacer = (key, value) => {
|
|
530
|
+
if (value instanceof Date)
|
|
531
|
+
return value.toISOString();
|
|
532
|
+
if (typeof value === "bigint")
|
|
533
|
+
return value.toString();
|
|
534
|
+
return value;
|
|
535
|
+
};
|
|
536
|
+
if (this.resetOnEachRun) {
|
|
537
|
+
const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
|
|
398
538
|
try {
|
|
399
539
|
await this._ensureDirExists(this.outputDir);
|
|
400
|
-
await fs.writeFile(
|
|
401
|
-
|
|
540
|
+
await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, jsonReplacer, 2));
|
|
541
|
+
if (this.printsToStdio()) {
|
|
542
|
+
console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
|
|
543
|
+
}
|
|
402
544
|
}
|
|
403
|
-
catch (
|
|
404
|
-
console.error(`
|
|
545
|
+
catch (error) {
|
|
546
|
+
console.error(`Pulse Reporter: Failed to write final JSON report to ${finalOutputPath}. Error: ${error.message}`);
|
|
405
547
|
}
|
|
406
|
-
return;
|
|
407
548
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
Failed: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.failed}
|
|
423
|
-
Skipped: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.skipped}
|
|
424
|
-
Duration: ${(((_c = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.duration) !== null && _c !== void 0 ? _c : 0) / 1000).toFixed(2)}s
|
|
425
|
-
-----------------------------------------`;
|
|
426
|
-
if (this.printsToStdio()) {
|
|
427
|
-
console.log(summary);
|
|
549
|
+
else {
|
|
550
|
+
const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
|
|
551
|
+
const individualReportPath = path.join(pulseResultsDir, `playwright-pulse-report-${Date.now()}.json`);
|
|
552
|
+
try {
|
|
553
|
+
await this._ensureDirExists(pulseResultsDir);
|
|
554
|
+
await fs.writeFile(individualReportPath, JSON.stringify(finalReport, jsonReplacer, 2));
|
|
555
|
+
if (this.printsToStdio()) {
|
|
556
|
+
console.log(`PlaywrightPulseReporter: Individual run report for merging written to ${individualReportPath}`);
|
|
557
|
+
}
|
|
558
|
+
await this._mergeAllRunReports();
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
console.error(`Pulse Reporter: Failed to write or merge report. Error: ${error.message}`);
|
|
562
|
+
}
|
|
428
563
|
}
|
|
564
|
+
if (this.isSharded) {
|
|
565
|
+
await this._cleanupTemporaryFiles();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async _mergeAllRunReports() {
|
|
569
|
+
const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
|
|
429
570
|
const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
|
|
571
|
+
let reportFiles;
|
|
572
|
+
try {
|
|
573
|
+
const allFiles = await fs.readdir(pulseResultsDir);
|
|
574
|
+
reportFiles = allFiles.filter((file) => file.startsWith("playwright-pulse-report-") && file.endsWith(".json"));
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
if (error.code === "ENOENT") {
|
|
578
|
+
if (this.printsToStdio()) {
|
|
579
|
+
console.log(`Pulse Reporter: No individual reports directory found at ${pulseResultsDir}. Skipping merge.`);
|
|
580
|
+
}
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
console.error(`Pulse Reporter: Error reading report directory ${pulseResultsDir}:`, error);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (reportFiles.length === 0) {
|
|
587
|
+
if (this.printsToStdio()) {
|
|
588
|
+
console.log("Pulse Reporter: No matching JSON report files found to merge.");
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const allResultsFromAllFiles = [];
|
|
593
|
+
let latestTimestamp = new Date(0);
|
|
594
|
+
let lastRunEnvironment = undefined;
|
|
595
|
+
// We can't simply sum durations across merged files, as the tests might overlap.
|
|
596
|
+
// The final duration will be derived from the range of start/end times in the final results.
|
|
597
|
+
let earliestStartTime = Date.now();
|
|
598
|
+
let latestEndTime = 0;
|
|
599
|
+
for (const file of reportFiles) {
|
|
600
|
+
const filePath = path.join(pulseResultsDir, file);
|
|
601
|
+
try {
|
|
602
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
603
|
+
const json = JSON.parse(content);
|
|
604
|
+
if (json.run) {
|
|
605
|
+
const runTimestamp = new Date(json.run.timestamp);
|
|
606
|
+
if (runTimestamp > latestTimestamp) {
|
|
607
|
+
latestTimestamp = runTimestamp;
|
|
608
|
+
lastRunEnvironment = json.run.environment || undefined;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (json.results) {
|
|
612
|
+
allResultsFromAllFiles.push(...json.results);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
catch (err) {
|
|
616
|
+
console.warn(`Pulse Reporter: Could not parse report file ${filePath}. Skipping. Error: ${err.message}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// De-duplicate the results from ALL merged files using the helper function
|
|
620
|
+
const finalMergedResults = this._getFinalizedResults(allResultsFromAllFiles);
|
|
621
|
+
// Calculate overall duration from the earliest start and latest end of the final merged results
|
|
622
|
+
for (const res of finalMergedResults) {
|
|
623
|
+
if (res.startTime.getTime() < earliestStartTime)
|
|
624
|
+
earliestStartTime = res.startTime.getTime();
|
|
625
|
+
if (res.endTime.getTime() > latestEndTime)
|
|
626
|
+
latestEndTime = res.endTime.getTime();
|
|
627
|
+
}
|
|
628
|
+
const totalDuration = latestEndTime > earliestStartTime ? latestEndTime - earliestStartTime : 0;
|
|
629
|
+
const combinedRun = {
|
|
630
|
+
id: `merged-${Date.now()}`,
|
|
631
|
+
timestamp: latestTimestamp,
|
|
632
|
+
environment: lastRunEnvironment,
|
|
633
|
+
totalTests: finalMergedResults.length,
|
|
634
|
+
passed: finalMergedResults.filter((r) => r.status === "passed").length,
|
|
635
|
+
failed: finalMergedResults.filter((r) => r.status === "failed").length,
|
|
636
|
+
skipped: finalMergedResults.filter((r) => r.status === "skipped").length,
|
|
637
|
+
flaky: finalMergedResults.filter((r) => r.status === "flaky").length, // Add flaky count
|
|
638
|
+
duration: totalDuration,
|
|
639
|
+
};
|
|
640
|
+
const finalReport = {
|
|
641
|
+
run: combinedRun,
|
|
642
|
+
results: finalMergedResults,
|
|
643
|
+
metadata: {
|
|
644
|
+
generatedAt: new Date().toISOString(),
|
|
645
|
+
},
|
|
646
|
+
};
|
|
430
647
|
try {
|
|
431
|
-
await this._ensureDirExists(this.outputDir);
|
|
432
648
|
await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, (key, value) => {
|
|
433
649
|
if (value instanceof Date)
|
|
434
650
|
return value.toISOString();
|
|
435
|
-
if (typeof value === "bigint")
|
|
436
|
-
return value.toString();
|
|
437
651
|
return value;
|
|
438
652
|
}, 2));
|
|
439
653
|
if (this.printsToStdio()) {
|
|
440
|
-
console.log(`PlaywrightPulseReporter:
|
|
654
|
+
console.log(`PlaywrightPulseReporter: ✅ Merged report with ${finalMergedResults.length} total results saved to ${finalOutputPath}`);
|
|
441
655
|
}
|
|
442
656
|
}
|
|
443
|
-
catch (
|
|
444
|
-
console.error(`Pulse Reporter: Failed to write final
|
|
445
|
-
}
|
|
446
|
-
finally {
|
|
447
|
-
if (this.isSharded) {
|
|
448
|
-
await this._cleanupTemporaryFiles();
|
|
449
|
-
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
console.error(`Pulse Reporter: Failed to write final merged report to ${finalOutputPath}. Error: ${err.message}`);
|
|
450
659
|
}
|
|
451
660
|
}
|
|
452
661
|
}
|