@arghajit/dummy 0.1.2-beta-11 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/reporter/attachment-utils.d.ts +9 -27
- package/dist/reporter/attachment-utils.js +62 -34
- package/dist/reporter/index.d.ts +2 -0
- package/dist/reporter/index.js +3 -0
- package/dist/reporter/playwright-pulse-reporter.d.ts +17 -112
- package/dist/reporter/playwright-pulse-reporter.js +470 -389
- package/dist/types/index.d.ts +2 -4
- package/package.json +8 -5
- package/scripts/generate-static-report.mjs +33 -47
|
@@ -1,66 +1,51 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.PlaywrightPulseReporter = void 0;
|
|
4
|
-
const fs =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return [];
|
|
16
|
-
},
|
|
17
|
-
mkdir: async (path, options) => {
|
|
18
|
-
console.log(`Would create directory ${path}`);
|
|
19
|
-
},
|
|
20
|
-
unlink: async (path) => {
|
|
21
|
-
console.log(`Would delete ${path}`);
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
const path = {
|
|
26
|
-
resolve: (...paths) => paths.join("/"),
|
|
27
|
-
join: (...paths) => paths.join("/"),
|
|
28
|
-
relative: (from, to) => to,
|
|
29
|
-
dirname: (p) => p.split("/").slice(0, -1).join("/"),
|
|
30
|
-
basename: (p, ext) => {
|
|
31
|
-
const base = p.split("/").pop() || "";
|
|
32
|
-
return ext ? base.replace(ext, "") : base;
|
|
33
|
-
},
|
|
34
|
-
extname: (p) => {
|
|
35
|
-
const parts = p.split(".");
|
|
36
|
-
return parts.length > 1 ? "." + parts.pop() : "";
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
const randomUUID = () => "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
|
40
|
-
const r = (Math.random() * 16) | 0;
|
|
41
|
-
const v = c == "x" ? r : (r & 0x3) | 0x8;
|
|
42
|
-
return v.toString(16);
|
|
43
|
-
});
|
|
44
|
-
const UAParser = class {
|
|
45
|
-
getBrowser() {
|
|
46
|
-
return { name: "unknown", version: "unknown" };
|
|
37
|
+
const fs = __importStar(require("fs/promises"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const crypto_1 = require("crypto");
|
|
40
|
+
const ua_parser_js_1 = require("ua-parser-js");
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const convertStatus = (status, testCase) => {
|
|
43
|
+
if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
|
|
44
|
+
return "failed";
|
|
45
|
+
}
|
|
46
|
+
if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "skipped") {
|
|
47
|
+
return "skipped";
|
|
47
48
|
}
|
|
48
|
-
};
|
|
49
|
-
const os = {
|
|
50
|
-
type: () => "Linux",
|
|
51
|
-
release: () => "5.4.0",
|
|
52
|
-
arch: () => "x64",
|
|
53
|
-
hostname: () => "localhost",
|
|
54
|
-
platform: () => "linux",
|
|
55
|
-
cpus: () => [{ model: "Intel" }],
|
|
56
|
-
totalmem: () => 8589934592,
|
|
57
|
-
};
|
|
58
|
-
const process = {
|
|
59
|
-
cwd: () => "/current/working/directory",
|
|
60
|
-
version: "v18.0.0",
|
|
61
|
-
versions: { v8: "10.0.0" },
|
|
62
|
-
};
|
|
63
|
-
function convertStatus(status) {
|
|
64
49
|
switch (status) {
|
|
65
50
|
case "passed":
|
|
66
51
|
return "passed";
|
|
@@ -69,429 +54,525 @@ function convertStatus(status) {
|
|
|
69
54
|
case "interrupted":
|
|
70
55
|
return "failed";
|
|
71
56
|
case "skipped":
|
|
72
|
-
return "skipped";
|
|
73
57
|
default:
|
|
74
|
-
return "
|
|
58
|
+
return "skipped";
|
|
75
59
|
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const attachmentsSubFolder = `${testId}-retry-${pwResult.retry || 0}`.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
81
|
-
pulseResult.screenshots = [];
|
|
82
|
-
pulseResult.videoPath = [];
|
|
83
|
-
pulseResult.attachments = [];
|
|
84
|
-
if (!pwResult.attachments)
|
|
85
|
-
return;
|
|
86
|
-
pwResult.attachments.forEach((attachment) => {
|
|
87
|
-
var _a, _b, _c;
|
|
88
|
-
const { contentType, name, path: attachmentPath } = attachment;
|
|
89
|
-
if (!attachmentPath)
|
|
90
|
-
return;
|
|
91
|
-
const relativePath = path.join("attachments", attachmentsSubFolder, path.basename(attachmentPath));
|
|
92
|
-
if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("image/")) {
|
|
93
|
-
(_a = pulseResult.screenshots) === null || _a === void 0 ? void 0 : _a.push(relativePath);
|
|
94
|
-
}
|
|
95
|
-
else if (name === "video" || (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("video/"))) {
|
|
96
|
-
(_b = pulseResult.videoPath) === null || _b === void 0 ? void 0 : _b.push(relativePath);
|
|
97
|
-
}
|
|
98
|
-
else if (name === "trace" || contentType === "application/zip") {
|
|
99
|
-
pulseResult.tracePath = relativePath;
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
(_c = pulseResult.attachments) === null || _c === void 0 ? void 0 : _c.push({
|
|
103
|
-
name: attachment.name,
|
|
104
|
-
path: relativePath,
|
|
105
|
-
contentType: attachment.contentType,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
}
|
|
60
|
+
};
|
|
61
|
+
const TEMP_SHARD_FILE_PREFIX = ".pulse-shard-results-";
|
|
62
|
+
const ATTACHMENTS_SUBDIR = "attachments";
|
|
63
|
+
const INDIVIDUAL_REPORTS_SUBDIR = "pulse-results";
|
|
110
64
|
class PlaywrightPulseReporter {
|
|
111
65
|
constructor(options = {}) {
|
|
112
|
-
|
|
113
|
-
this.
|
|
114
|
-
this.
|
|
115
|
-
this.
|
|
66
|
+
var _a, _b, _c;
|
|
67
|
+
this.results = [];
|
|
68
|
+
this.baseOutputFile = "playwright-pulse-report.json";
|
|
69
|
+
this.isSharded = false;
|
|
70
|
+
this.shardIndex = undefined;
|
|
116
71
|
this.options = options;
|
|
72
|
+
this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
|
|
73
|
+
this.outputDir = (_b = options.outputDir) !== null && _b !== void 0 ? _b : "pulse-report";
|
|
74
|
+
this.attachmentsDir = path.join(this.outputDir, ATTACHMENTS_SUBDIR);
|
|
75
|
+
this.resetOnEachRun = (_c = options.resetOnEachRun) !== null && _c !== void 0 ? _c : true;
|
|
117
76
|
}
|
|
118
77
|
printsToStdio() {
|
|
119
|
-
return
|
|
78
|
+
return this.shardIndex === undefined || this.shardIndex === 0;
|
|
120
79
|
}
|
|
121
80
|
onBegin(config, suite) {
|
|
122
81
|
var _a;
|
|
123
|
-
this.
|
|
124
|
-
this.
|
|
125
|
-
this.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
this.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
82
|
+
this.config = config;
|
|
83
|
+
this.suite = suite;
|
|
84
|
+
this.runStartTime = Date.now();
|
|
85
|
+
const configDir = this.config.rootDir;
|
|
86
|
+
const configFileDir = this.config.configFile
|
|
87
|
+
? path.dirname(this.config.configFile)
|
|
88
|
+
: configDir;
|
|
89
|
+
this.outputDir = path.resolve(configFileDir, (_a = this.options.outputDir) !== null && _a !== void 0 ? _a : "pulse-report");
|
|
90
|
+
this.attachmentsDir = path.resolve(this.outputDir, ATTACHMENTS_SUBDIR);
|
|
91
|
+
this.options.outputDir = this.outputDir;
|
|
92
|
+
const totalShards = this.config.shard ? this.config.shard.total : 1;
|
|
93
|
+
this.isSharded = totalShards > 1;
|
|
94
|
+
this.shardIndex = this.config.shard
|
|
95
|
+
? this.config.shard.current - 1
|
|
96
|
+
: undefined;
|
|
97
|
+
this._ensureDirExists(this.outputDir)
|
|
98
|
+
.then(() => {
|
|
99
|
+
if (this.printsToStdio()) {
|
|
100
|
+
console.log(`PlaywrightPulseReporter: Starting test run with ${suite.allTests().length} tests${this.isSharded ? ` across ${totalShards} shards` : ""}. Pulse outputting to ${this.outputDir}`);
|
|
101
|
+
if (this.shardIndex === undefined ||
|
|
102
|
+
(this.isSharded && this.shardIndex === 0)) {
|
|
103
|
+
return this._cleanupTemporaryFiles();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
.catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
|
|
108
|
+
}
|
|
109
|
+
onTestBegin(test) {
|
|
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();
|
|
115
|
+
const projectConfig = project === null || project === void 0 ? void 0 : project.use;
|
|
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
|
+
: "";
|
|
124
|
+
const osName = result.os.name ? ` on ${result.os.name}` : "";
|
|
125
|
+
const osVersion = result.os.version
|
|
126
|
+
? ` ${result.os.version.split(".")[0]}`
|
|
127
|
+
: "";
|
|
128
|
+
const deviceType = result.device.type;
|
|
129
|
+
let finalString;
|
|
130
|
+
if (browserName === undefined) {
|
|
131
|
+
browserName = configuredBrowserType;
|
|
132
|
+
finalString = `${browserName}`;
|
|
132
133
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
else {
|
|
135
|
+
if (deviceType === "mobile" || deviceType === "tablet") {
|
|
136
|
+
if ((_c = result.os.name) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes("android")) {
|
|
137
|
+
if (browserName.toLowerCase().includes("chrome"))
|
|
138
|
+
browserName = "Chrome Mobile";
|
|
139
|
+
else if (browserName.toLowerCase().includes("firefox"))
|
|
140
|
+
browserName = "Firefox Mobile";
|
|
141
|
+
else if (result.engine.name === "Blink" && !result.browser.name)
|
|
142
|
+
browserName = "Android WebView";
|
|
143
|
+
else if (browserName &&
|
|
144
|
+
!browserName.toLowerCase().includes("mobile")) {
|
|
145
|
+
// Keep it as is
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
browserName = "Android Browser";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if ((_d = result.os.name) === null || _d === void 0 ? void 0 : _d.toLowerCase().includes("ios")) {
|
|
152
|
+
browserName = "Mobile Safari";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (browserName === "Electron") {
|
|
156
|
+
browserName = "Electron App";
|
|
157
|
+
}
|
|
158
|
+
finalString = `${browserName}${browserVersion}${osName}${osVersion}`;
|
|
136
159
|
}
|
|
160
|
+
return finalString.trim();
|
|
137
161
|
}
|
|
138
|
-
|
|
139
|
-
var _a, _b;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
162
|
+
async processStep(step, testId, browserDetails, testCase) {
|
|
163
|
+
var _a, _b, _c, _d;
|
|
164
|
+
let stepStatus = "passed";
|
|
165
|
+
let errorMessage = ((_a = step.error) === null || _a === void 0 ? void 0 : _a.message) || undefined;
|
|
166
|
+
if ((_c = (_b = step.error) === null || _b === void 0 ? void 0 : _b.message) === null || _c === void 0 ? void 0 : _c.startsWith("Test is skipped:")) {
|
|
167
|
+
stepStatus = "skipped";
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
stepStatus = convertStatus(step.error ? "failed" : "passed", testCase);
|
|
171
|
+
}
|
|
172
|
+
const duration = step.duration;
|
|
173
|
+
const startTime = new Date(step.startTime);
|
|
174
|
+
const endTime = new Date(startTime.getTime() + Math.max(0, duration));
|
|
175
|
+
let codeLocation = "";
|
|
176
|
+
if (step.location) {
|
|
177
|
+
codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
|
|
147
178
|
}
|
|
148
|
-
return browserName;
|
|
149
|
-
}
|
|
150
|
-
async processStep(step) {
|
|
151
|
-
const stepStatus = convertStatus(step.error ? "failed" : "passed");
|
|
152
|
-
const codeLocation = step.location
|
|
153
|
-
? `${step.location.file}:${step.location.line}:${step.location.column}`
|
|
154
|
-
: undefined;
|
|
155
179
|
return {
|
|
180
|
+
id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}`,
|
|
156
181
|
title: step.title,
|
|
157
|
-
category: step.category,
|
|
158
|
-
startTime: step.startTime,
|
|
159
|
-
duration: step.duration,
|
|
160
|
-
error: step.error
|
|
161
|
-
? {
|
|
162
|
-
message: step.error.message || "",
|
|
163
|
-
stack: step.error.stack,
|
|
164
|
-
snippet: step.error.snippet,
|
|
165
|
-
}
|
|
166
|
-
: undefined,
|
|
167
|
-
count: step.count || 0,
|
|
168
|
-
location: codeLocation,
|
|
169
182
|
status: stepStatus,
|
|
183
|
+
duration: duration,
|
|
184
|
+
startTime: startTime,
|
|
185
|
+
endTime: endTime,
|
|
186
|
+
browser: browserDetails,
|
|
187
|
+
errorMessage: errorMessage,
|
|
188
|
+
stackTrace: ((_d = step.error) === null || _d === void 0 ? void 0 : _d.stack) || undefined,
|
|
189
|
+
codeLocation: codeLocation || undefined,
|
|
190
|
+
isHook: step.category === "hook",
|
|
191
|
+
hookType: step.category === "hook"
|
|
192
|
+
? step.title.toLowerCase().includes("before")
|
|
193
|
+
? "before"
|
|
194
|
+
: "after"
|
|
195
|
+
: undefined,
|
|
196
|
+
steps: [],
|
|
170
197
|
};
|
|
171
198
|
}
|
|
172
199
|
async onTestEnd(test, result) {
|
|
173
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
200
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
201
|
+
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
|
|
202
|
+
const browserDetails = this.getBrowserDetails(test);
|
|
203
|
+
const testStatus = convertStatus(result.status, test);
|
|
204
|
+
const startTime = new Date(result.startTime);
|
|
205
|
+
const endTime = new Date(startTime.getTime() + result.duration);
|
|
174
206
|
const processAllSteps = async (steps) => {
|
|
175
|
-
|
|
207
|
+
let processed = [];
|
|
176
208
|
for (const step of steps) {
|
|
177
|
-
const processedStep = await this.processStep(step);
|
|
178
|
-
|
|
209
|
+
const processedStep = await this.processStep(step, test.id, browserDetails, test);
|
|
210
|
+
processed.push(processedStep);
|
|
179
211
|
if (step.steps && step.steps.length > 0) {
|
|
180
|
-
|
|
181
|
-
processedSteps.push(...nestedSteps);
|
|
212
|
+
processedStep.steps = await processAllSteps(step.steps);
|
|
182
213
|
}
|
|
183
214
|
}
|
|
184
|
-
return
|
|
215
|
+
return processed;
|
|
185
216
|
};
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
217
|
+
let codeSnippet = undefined;
|
|
218
|
+
try {
|
|
219
|
+
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)) {
|
|
220
|
+
const relativePath = path.relative(this.config.rootDir, test.location.file);
|
|
221
|
+
codeSnippet = `Test defined at: ${relativePath}:${test.location.line}:${test.location.column}`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
|
|
226
|
+
}
|
|
227
|
+
const stdoutMessages = result.stdout.map((item) => typeof item === "string" ? item : item.toString());
|
|
228
|
+
const stderrMessages = result.stderr.map((item) => typeof item === "string" ? item : item.toString());
|
|
229
|
+
const maxWorkers = this.config.workers;
|
|
230
|
+
let mappedWorkerId = result.workerIndex === -1
|
|
231
|
+
? -1
|
|
232
|
+
: (result.workerIndex % (maxWorkers > 0 ? maxWorkers : 1)) + 1;
|
|
198
233
|
const testSpecificData = {
|
|
199
234
|
workerId: mappedWorkerId,
|
|
200
|
-
totalWorkers:
|
|
201
|
-
configFile:
|
|
202
|
-
metadata:
|
|
235
|
+
totalWorkers: maxWorkers,
|
|
236
|
+
configFile: this.config.configFile,
|
|
237
|
+
metadata: this.config.metadata
|
|
238
|
+
? JSON.stringify(this.config.metadata)
|
|
239
|
+
: undefined,
|
|
203
240
|
};
|
|
204
241
|
const pulseResult = {
|
|
205
242
|
id: test.id,
|
|
206
|
-
runId:
|
|
207
|
-
name:
|
|
208
|
-
suiteName: project === null || project === void 0 ? void 0 : project.name,
|
|
243
|
+
runId: "TBD",
|
|
244
|
+
name: test.titlePath().join(" > "),
|
|
245
|
+
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",
|
|
209
246
|
status: testStatus,
|
|
210
247
|
duration: result.duration,
|
|
211
248
|
startTime: startTime,
|
|
212
249
|
endTime: endTime,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
250
|
+
browser: browserDetails,
|
|
251
|
+
retries: result.retry,
|
|
252
|
+
steps: ((_f = result.steps) === null || _f === void 0 ? void 0 : _f.length) ? await processAllSteps(result.steps) : [],
|
|
253
|
+
errorMessage: (_g = result.error) === null || _g === void 0 ? void 0 : _g.message,
|
|
254
|
+
stackTrace: (_h = result.error) === null || _h === void 0 ? void 0 : _h.stack,
|
|
255
|
+
snippet: (_j = result.error) === null || _j === void 0 ? void 0 : _j.snippet,
|
|
256
|
+
codeSnippet: codeSnippet,
|
|
257
|
+
tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
|
|
221
258
|
screenshots: [],
|
|
222
259
|
videoPath: [],
|
|
260
|
+
tracePath: undefined,
|
|
223
261
|
attachments: [],
|
|
224
|
-
stdout: stdoutMessages,
|
|
225
|
-
stderr: stderrMessages,
|
|
262
|
+
stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
|
|
263
|
+
stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
|
|
226
264
|
...testSpecificData,
|
|
227
265
|
};
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
266
|
+
for (const [index, attachment] of result.attachments.entries()) {
|
|
267
|
+
if (!attachment.path)
|
|
268
|
+
continue;
|
|
269
|
+
try {
|
|
270
|
+
const testSubfolder = test.id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
271
|
+
const safeAttachmentName = path
|
|
272
|
+
.basename(attachment.path)
|
|
273
|
+
.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
274
|
+
const uniqueFileName = `${index}-${Date.now()}-${safeAttachmentName}`;
|
|
275
|
+
const relativeDestPath = path.join(ATTACHMENTS_SUBDIR, testSubfolder, uniqueFileName);
|
|
276
|
+
const absoluteDestPath = path.join(this.outputDir, relativeDestPath);
|
|
277
|
+
await this._ensureDirExists(path.dirname(absoluteDestPath));
|
|
278
|
+
await fs.copyFile(attachment.path, absoluteDestPath);
|
|
279
|
+
if (attachment.contentType.startsWith("image/")) {
|
|
280
|
+
(_k = pulseResult.screenshots) === null || _k === void 0 ? void 0 : _k.push(relativeDestPath);
|
|
281
|
+
}
|
|
282
|
+
else if (attachment.contentType.startsWith("video/")) {
|
|
283
|
+
(_l = pulseResult.videoPath) === null || _l === void 0 ? void 0 : _l.push(relativeDestPath);
|
|
284
|
+
}
|
|
285
|
+
else if (attachment.name === "trace") {
|
|
286
|
+
pulseResult.tracePath = relativeDestPath;
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
(_m = pulseResult.attachments) === null || _m === void 0 ? void 0 : _m.push({
|
|
290
|
+
name: attachment.name,
|
|
291
|
+
path: relativeDestPath,
|
|
292
|
+
contentType: attachment.contentType,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
255
295
|
}
|
|
256
|
-
|
|
257
|
-
|
|
296
|
+
catch (err) {
|
|
297
|
+
console.error(`Pulse Reporter: Failed to process attachment "${attachment.name}" for test ${pulseResult.name}. Error: ${err.message}`);
|
|
258
298
|
}
|
|
259
|
-
const startTimes = attempts.map((a) => a.startTime.getTime());
|
|
260
|
-
const endTimes = attempts.map((a) => a.endTime.getTime());
|
|
261
|
-
const overallDuration = Math.max(...endTimes) - Math.min(...startTimes);
|
|
262
|
-
const baseAttempt = attempts[0];
|
|
263
|
-
finalResults.push({
|
|
264
|
-
id: baseId,
|
|
265
|
-
name: baseAttempt.name,
|
|
266
|
-
suiteName: baseAttempt.suiteName,
|
|
267
|
-
status: overallStatus,
|
|
268
|
-
duration: overallDuration,
|
|
269
|
-
startTime: new Date(Math.min(...startTimes)),
|
|
270
|
-
endTime: new Date(Math.max(...endTimes)),
|
|
271
|
-
browser: baseAttempt.browser,
|
|
272
|
-
tags: baseAttempt.tags,
|
|
273
|
-
results: attempts,
|
|
274
|
-
});
|
|
275
299
|
}
|
|
276
|
-
|
|
300
|
+
this.results.push(pulseResult);
|
|
277
301
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
case "passed":
|
|
286
|
-
passed++;
|
|
287
|
-
break;
|
|
288
|
-
case "failed":
|
|
289
|
-
failed++;
|
|
290
|
-
break;
|
|
291
|
-
case "skipped":
|
|
292
|
-
skipped++;
|
|
293
|
-
break;
|
|
294
|
-
case "flaky":
|
|
295
|
-
flaky++;
|
|
296
|
-
break;
|
|
302
|
+
_getFinalizedResults(allResults) {
|
|
303
|
+
const finalResultsMap = new Map();
|
|
304
|
+
for (const result of allResults) {
|
|
305
|
+
const existing = finalResultsMap.get(result.id);
|
|
306
|
+
// Keep the result with the highest retry attempt for each test ID
|
|
307
|
+
if (!existing || result.retries >= existing.retries) {
|
|
308
|
+
finalResultsMap.set(result.id, result);
|
|
297
309
|
}
|
|
298
310
|
}
|
|
299
|
-
return
|
|
300
|
-
passed,
|
|
301
|
-
failed,
|
|
302
|
-
skipped,
|
|
303
|
-
flaky,
|
|
304
|
-
totalTests: consolidatedResults.length,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
_getSummaryStatsFromAttempts(attempts) {
|
|
308
|
-
let passed = 0;
|
|
309
|
-
let failed = 0;
|
|
310
|
-
let skipped = 0;
|
|
311
|
-
let flaky = 0;
|
|
312
|
-
const groupedByTest = new Map();
|
|
313
|
-
for (const attempt of attempts) {
|
|
314
|
-
const baseId = attempt.id.replace(/-\d+$/, "");
|
|
315
|
-
if (!groupedByTest.has(baseId)) {
|
|
316
|
-
groupedByTest.set(baseId, []);
|
|
317
|
-
}
|
|
318
|
-
groupedByTest.get(baseId).push(attempt);
|
|
319
|
-
}
|
|
320
|
-
for (const attempt of attempts) {
|
|
321
|
-
const baseId = attempt.id.replace(/-\d+$/, "");
|
|
322
|
-
const testAttempts = groupedByTest.get(baseId);
|
|
323
|
-
const hasFailures = testAttempts.some((a) => a.status === "failed");
|
|
324
|
-
const hasPasses = testAttempts.some((a) => a.status === "passed");
|
|
325
|
-
if (hasFailures && hasPasses) {
|
|
326
|
-
flaky++;
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
switch (attempt.status) {
|
|
330
|
-
case "passed":
|
|
331
|
-
passed++;
|
|
332
|
-
break;
|
|
333
|
-
case "failed":
|
|
334
|
-
failed++;
|
|
335
|
-
break;
|
|
336
|
-
case "skipped":
|
|
337
|
-
skipped++;
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return {
|
|
343
|
-
passed,
|
|
344
|
-
failed,
|
|
345
|
-
skipped,
|
|
346
|
-
flaky,
|
|
347
|
-
totalTests: attempts.length,
|
|
348
|
-
};
|
|
311
|
+
return Array.from(finalResultsMap.values());
|
|
349
312
|
}
|
|
350
313
|
onError(error) {
|
|
351
|
-
|
|
314
|
+
var _a;
|
|
315
|
+
console.error(`PlaywrightPulseReporter: Error encountered (Shard: ${(_a = this.shardIndex) !== null && _a !== void 0 ? _a : "Main"}):`, (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
316
|
+
if (error === null || error === void 0 ? void 0 : error.stack) {
|
|
317
|
+
console.error(error.stack);
|
|
318
|
+
}
|
|
352
319
|
}
|
|
353
320
|
_getEnvDetails() {
|
|
354
|
-
const parser = new UAParser();
|
|
355
321
|
return {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
cpu:
|
|
322
|
+
host: os.hostname(),
|
|
323
|
+
os: `${os.platform()} ${os.release()}`,
|
|
324
|
+
cpu: {
|
|
325
|
+
model: os.cpus()[0] ? os.cpus()[0].model : "N/A",
|
|
326
|
+
cores: os.cpus().length,
|
|
327
|
+
},
|
|
328
|
+
memory: `${(os.totalmem() / 1024 ** 3).toFixed(2)}GB`,
|
|
329
|
+
node: process.version,
|
|
330
|
+
v8: process.versions.v8,
|
|
331
|
+
cwd: process.cwd(),
|
|
359
332
|
};
|
|
360
333
|
}
|
|
361
|
-
async _writeShardResults(
|
|
362
|
-
|
|
334
|
+
async _writeShardResults() {
|
|
335
|
+
if (this.shardIndex === undefined) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${this.shardIndex}.json`);
|
|
363
339
|
try {
|
|
364
|
-
await this.
|
|
365
|
-
await fs.promises.writeFile(tempFilePath, JSON.stringify(individualResults, null, 2));
|
|
340
|
+
await fs.writeFile(tempFilePath, JSON.stringify(this.results, (key, value) => (value instanceof Date ? value.toISOString() : value), 2));
|
|
366
341
|
}
|
|
367
342
|
catch (error) {
|
|
368
|
-
console.error(
|
|
343
|
+
console.error(`Pulse Reporter: Shard ${this.shardIndex} failed to write temporary results to ${tempFilePath}`, error);
|
|
369
344
|
}
|
|
370
345
|
}
|
|
371
|
-
async _mergeShardResults(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
346
|
+
async _mergeShardResults(finalRunData) {
|
|
347
|
+
let allShardProcessedResults = [];
|
|
348
|
+
const totalShards = this.config.shard ? this.config.shard.total : 1;
|
|
349
|
+
for (let i = 0; i < totalShards; i++) {
|
|
350
|
+
const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
|
|
351
|
+
try {
|
|
352
|
+
const content = await fs.readFile(tempFilePath, "utf-8");
|
|
353
|
+
const shardResults = JSON.parse(content);
|
|
354
|
+
allShardProcessedResults =
|
|
355
|
+
allShardProcessedResults.concat(shardResults);
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
|
|
359
|
+
console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}. This might be normal if a shard had no tests or failed early.`);
|
|
378
360
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
catch (error) {
|
|
385
|
-
console.error("Pulse Reporter: Error merging shard results:", error);
|
|
386
|
-
return [];
|
|
361
|
+
else {
|
|
362
|
+
console.error(`Pulse Reporter: Could not read/parse results from shard ${i} (${tempFilePath}). Error:`, error);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
387
365
|
}
|
|
366
|
+
const finalResultsList = this._getFinalizedResults(allShardProcessedResults);
|
|
367
|
+
finalResultsList.forEach((r) => (r.runId = finalRunData.id));
|
|
368
|
+
finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
|
|
369
|
+
finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
|
|
370
|
+
finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
|
|
371
|
+
finalRunData.totalTests = finalResultsList.length;
|
|
372
|
+
const reviveDates = (key, value) => {
|
|
373
|
+
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
374
|
+
if (typeof value === "string" && isoDateRegex.test(value)) {
|
|
375
|
+
const date = new Date(value);
|
|
376
|
+
return !isNaN(date.getTime()) ? date : value;
|
|
377
|
+
}
|
|
378
|
+
return value;
|
|
379
|
+
};
|
|
380
|
+
const properlyTypedResults = JSON.parse(JSON.stringify(finalResultsList), reviveDates);
|
|
381
|
+
return {
|
|
382
|
+
run: finalRunData,
|
|
383
|
+
results: properlyTypedResults,
|
|
384
|
+
metadata: { generatedAt: new Date().toISOString() },
|
|
385
|
+
};
|
|
388
386
|
}
|
|
389
387
|
async _cleanupTemporaryFiles() {
|
|
390
388
|
try {
|
|
391
|
-
const files = await fs.
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
await fs.
|
|
389
|
+
const files = await fs.readdir(this.outputDir);
|
|
390
|
+
const tempFiles = files.filter((f) => f.startsWith(TEMP_SHARD_FILE_PREFIX));
|
|
391
|
+
if (tempFiles.length > 0) {
|
|
392
|
+
await Promise.all(tempFiles.map((f) => fs.unlink(path.join(this.outputDir, f))));
|
|
395
393
|
}
|
|
396
394
|
}
|
|
397
395
|
catch (error) {
|
|
398
|
-
|
|
396
|
+
if ((error === null || error === void 0 ? void 0 : error.code) !== "ENOENT") {
|
|
397
|
+
console.warn("Pulse Reporter: Warning during cleanup of temporary files:", error.message);
|
|
398
|
+
}
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
async _ensureDirExists(dirPath) {
|
|
402
402
|
try {
|
|
403
|
-
await fs.
|
|
403
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
404
404
|
}
|
|
405
405
|
catch (error) {
|
|
406
|
-
|
|
406
|
+
if (error.code !== "EEXIST") {
|
|
407
|
+
console.error(`Pulse Reporter: Failed to ensure directory exists: ${dirPath}`, error);
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
407
410
|
}
|
|
408
411
|
}
|
|
409
412
|
async onEnd(result) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
413
|
+
if (this.shardIndex !== undefined) {
|
|
414
|
+
await this._writeShardResults();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
// De-duplicate and handle retries here, in a safe, single-threaded context.
|
|
418
|
+
const finalResults = this._getFinalizedResults(this.results);
|
|
419
|
+
const runEndTime = Date.now();
|
|
420
|
+
const duration = runEndTime - this.runStartTime;
|
|
421
|
+
const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`;
|
|
422
|
+
const environmentDetails = this._getEnvDetails();
|
|
423
|
+
const runData = {
|
|
424
|
+
id: runId,
|
|
425
|
+
timestamp: new Date(this.runStartTime),
|
|
426
|
+
// Use the length of the de-duplicated array for all counts
|
|
427
|
+
totalTests: finalResults.length,
|
|
428
|
+
passed: finalResults.filter((r) => r.status === "passed").length,
|
|
429
|
+
failed: finalResults.filter((r) => r.status === "failed").length,
|
|
430
|
+
skipped: finalResults.filter((r) => r.status === "skipped").length,
|
|
431
|
+
duration,
|
|
432
|
+
environment: environmentDetails,
|
|
433
|
+
};
|
|
434
|
+
finalResults.forEach((r) => (r.runId = runId));
|
|
435
|
+
let finalReport = undefined;
|
|
436
|
+
if (this.isSharded) {
|
|
437
|
+
// The _mergeShardResults method will handle its own de-duplication
|
|
438
|
+
finalReport = await this._mergeShardResults(runData);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
finalReport = {
|
|
422
442
|
run: runData,
|
|
423
|
-
results
|
|
424
|
-
|
|
443
|
+
// Use the de-duplicated results
|
|
444
|
+
results: finalResults,
|
|
445
|
+
metadata: { generatedAt: new Date().toISOString() },
|
|
425
446
|
};
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
447
|
+
}
|
|
448
|
+
if (!finalReport) {
|
|
449
|
+
console.error("PlaywrightPulseReporter: CRITICAL - finalReport object was not generated. Cannot create summary.");
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const jsonReplacer = (key, value) => {
|
|
453
|
+
if (value instanceof Date)
|
|
454
|
+
return value.toISOString();
|
|
455
|
+
if (typeof value === "bigint")
|
|
456
|
+
return value.toString();
|
|
457
|
+
return value;
|
|
458
|
+
};
|
|
459
|
+
if (this.resetOnEachRun) {
|
|
460
|
+
const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
|
|
461
|
+
try {
|
|
462
|
+
await this._ensureDirExists(this.outputDir);
|
|
463
|
+
await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, jsonReplacer, 2));
|
|
464
|
+
if (this.printsToStdio()) {
|
|
465
|
+
console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
|
|
429
466
|
}
|
|
430
|
-
return value;
|
|
431
|
-
};
|
|
432
|
-
if (this.shardIndex !== undefined) {
|
|
433
|
-
await this._writeShardResults(individualResults);
|
|
434
|
-
console.log(`Pulse Reporter: Shard ${this.shardIndex} results written.`);
|
|
435
467
|
}
|
|
436
|
-
|
|
437
|
-
|
|
468
|
+
catch (error) {
|
|
469
|
+
console.error(`Pulse Reporter: Failed to write final JSON report to ${finalOutputPath}. Error: ${error.message}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
// Logic for appending/merging reports
|
|
474
|
+
const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
|
|
475
|
+
const individualReportPath = path.join(pulseResultsDir, `playwright-pulse-report-${Date.now()}.json`);
|
|
476
|
+
try {
|
|
438
477
|
await this._ensureDirExists(pulseResultsDir);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (mergedReport) {
|
|
443
|
-
const finalReportPath = path.join(this.outputDir, "playwright-pulse-report.json");
|
|
444
|
-
await fs.promises.writeFile(finalReportPath, JSON.stringify(mergedReport, jsonReplacer, 2));
|
|
445
|
-
console.log(`Pulse Reporter: Final report written to ${finalReportPath}`);
|
|
478
|
+
await fs.writeFile(individualReportPath, JSON.stringify(finalReport, jsonReplacer, 2));
|
|
479
|
+
if (this.printsToStdio()) {
|
|
480
|
+
console.log(`PlaywrightPulseReporter: Individual run report for merging written to ${individualReportPath}`);
|
|
446
481
|
}
|
|
482
|
+
await this._mergeAllRunReports();
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
console.error(`Pulse Reporter: Failed to write or merge report. Error: ${error.message}`);
|
|
447
486
|
}
|
|
448
487
|
}
|
|
449
|
-
|
|
450
|
-
|
|
488
|
+
if (this.isSharded) {
|
|
489
|
+
await this._cleanupTemporaryFiles();
|
|
451
490
|
}
|
|
452
491
|
}
|
|
453
492
|
async _mergeAllRunReports() {
|
|
454
|
-
|
|
493
|
+
const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
|
|
494
|
+
const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
|
|
495
|
+
let reportFiles;
|
|
455
496
|
try {
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
497
|
+
const allFiles = await fs.readdir(pulseResultsDir);
|
|
498
|
+
reportFiles = allFiles.filter((file) => file.startsWith("playwright-pulse-report-") && file.endsWith(".json"));
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
if (error.code === "ENOENT") {
|
|
502
|
+
if (this.printsToStdio()) {
|
|
503
|
+
console.log(`Pulse Reporter: No individual reports directory found at ${pulseResultsDir}. Skipping merge.`);
|
|
504
|
+
}
|
|
505
|
+
return;
|
|
461
506
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
507
|
+
console.error(`Pulse Reporter: Error reading report directory ${pulseResultsDir}:`, error);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (reportFiles.length === 0) {
|
|
511
|
+
if (this.printsToStdio()) {
|
|
512
|
+
console.log("Pulse Reporter: No matching JSON report files found to merge.");
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const allResultsFromAllFiles = [];
|
|
517
|
+
let latestTimestamp = new Date(0);
|
|
518
|
+
let lastRunEnvironment = undefined;
|
|
519
|
+
let totalDuration = 0;
|
|
520
|
+
for (const file of reportFiles) {
|
|
521
|
+
const filePath = path.join(pulseResultsDir, file);
|
|
522
|
+
try {
|
|
523
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
524
|
+
const json = JSON.parse(content);
|
|
525
|
+
if (json.run) {
|
|
526
|
+
const runTimestamp = new Date(json.run.timestamp);
|
|
527
|
+
if (runTimestamp > latestTimestamp) {
|
|
528
|
+
latestTimestamp = runTimestamp;
|
|
529
|
+
lastRunEnvironment = json.run.environment || undefined;
|
|
530
|
+
}
|
|
470
531
|
}
|
|
471
|
-
if (
|
|
472
|
-
|
|
532
|
+
if (json.results) {
|
|
533
|
+
allResultsFromAllFiles.push(...json.results);
|
|
473
534
|
}
|
|
474
535
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
startTime: new Date(),
|
|
479
|
-
endTime: new Date(),
|
|
480
|
-
duration: totalDuration,
|
|
481
|
-
status: "passed",
|
|
482
|
-
environment: this._getEnvDetails(),
|
|
483
|
-
};
|
|
484
|
-
const finalReport = {
|
|
485
|
-
run: combinedRun,
|
|
486
|
-
results: allAttempts,
|
|
487
|
-
summary: stats,
|
|
488
|
-
};
|
|
489
|
-
return finalReport;
|
|
536
|
+
catch (err) {
|
|
537
|
+
console.warn(`Pulse Reporter: Could not parse report file ${filePath}. Skipping. Error: ${err.message}`);
|
|
538
|
+
}
|
|
490
539
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
540
|
+
// De-duplicate the results from ALL merged files using the helper function
|
|
541
|
+
const finalMergedResults = this._getFinalizedResults(allResultsFromAllFiles);
|
|
542
|
+
// Sum the duration from the final, de-duplicated list of tests
|
|
543
|
+
totalDuration = finalMergedResults.reduce((acc, r) => acc + (r.duration || 0), 0);
|
|
544
|
+
const combinedRun = {
|
|
545
|
+
id: `merged-${Date.now()}`,
|
|
546
|
+
timestamp: latestTimestamp,
|
|
547
|
+
environment: lastRunEnvironment,
|
|
548
|
+
// Recalculate counts based on the truly final, de-duplicated list
|
|
549
|
+
totalTests: finalMergedResults.length,
|
|
550
|
+
passed: finalMergedResults.filter((r) => r.status === "passed").length,
|
|
551
|
+
failed: finalMergedResults.filter((r) => r.status === "failed").length,
|
|
552
|
+
skipped: finalMergedResults.filter((r) => r.status === "skipped").length,
|
|
553
|
+
duration: totalDuration,
|
|
554
|
+
};
|
|
555
|
+
const finalReport = {
|
|
556
|
+
run: combinedRun,
|
|
557
|
+
results: finalMergedResults, // Use the de-duplicated list
|
|
558
|
+
metadata: {
|
|
559
|
+
generatedAt: new Date().toISOString(),
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
try {
|
|
563
|
+
await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, (key, value) => {
|
|
564
|
+
if (value instanceof Date)
|
|
565
|
+
return value.toISOString();
|
|
566
|
+
return value;
|
|
567
|
+
}, 2));
|
|
568
|
+
if (this.printsToStdio()) {
|
|
569
|
+
console.log(`PlaywrightPulseReporter: ✅ Merged report with ${finalMergedResults.length} total results saved to ${finalOutputPath}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch (err) {
|
|
573
|
+
console.error(`Pulse Reporter: Failed to write final merged report to ${finalOutputPath}. Error: ${err.message}`);
|
|
494
574
|
}
|
|
495
575
|
}
|
|
496
576
|
}
|
|
497
577
|
exports.PlaywrightPulseReporter = PlaywrightPulseReporter;
|
|
578
|
+
exports.default = PlaywrightPulseReporter;
|