@arghajit/playwright-pulse-report 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,192 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.attachFiles = attachFiles;
37
+ const path = __importStar(require("path"));
38
+ const fs = __importStar(require("fs")); // Use synchronous methods for simplicity in this context
39
+ const ATTACHMENTS_SUBDIR = "attachments"; // Consistent subdirectory name
40
+ /**
41
+ * Processes attachments from a Playwright TestResult and updates the PulseTestResult.
42
+ * @param testId A unique identifier for the test, used for folder naming.
43
+ * @param pwResult The TestResult object from Playwright.
44
+ * @param pulseResult The internal test result structure to update.
45
+ * @param config The reporter configuration options.
46
+ */
47
+ function attachFiles(testId, pwResult, pulseResult, config) {
48
+ const baseReportDir = config.outputDir || "pulse-report-output"; // Base output directory
49
+ // Ensure attachments are relative to the main outputDir
50
+ const attachmentsBaseDir = path.resolve(baseReportDir, ATTACHMENTS_SUBDIR); // Absolute path for FS operations
51
+ const attachmentsSubFolder = testId.replace(/[^a-zA-Z0-9_-]/g, "_"); // Sanitize testId for folder name
52
+ const testAttachmentsDir = path.join(attachmentsBaseDir, attachmentsSubFolder); // e.g., pulse-report-output/attachments/test_id_abc
53
+ try {
54
+ if (!fs.existsSync(testAttachmentsDir)) {
55
+ fs.mkdirSync(testAttachmentsDir, { recursive: true });
56
+ }
57
+ }
58
+ catch (error) {
59
+ console.error(`Pulse Reporter: Failed to create attachments directory: ${testAttachmentsDir}`, error);
60
+ return; // Stop processing if directory creation fails
61
+ }
62
+ if (!pwResult.attachments)
63
+ return;
64
+ const { base64Images } = config; // Get base64 embedding option
65
+ pulseResult.screenshots = []; // Initialize screenshots array
66
+ pwResult.attachments.forEach((attachment) => {
67
+ const { contentType, name, path: attachmentPath, body } = attachment;
68
+ // Skip attachments without path or body
69
+ if (!attachmentPath && !body) {
70
+ console.warn(`Pulse Reporter: Attachment "${name}" for test ${testId} has no path or body. Skipping.`);
71
+ return;
72
+ }
73
+ // Determine filename
74
+ const safeName = name.replace(/[^a-zA-Z0-9_.-]/g, "_"); // Sanitize original name
75
+ const extension = attachmentPath
76
+ ? path.extname(attachmentPath)
77
+ : `.${getFileExtension(contentType)}`;
78
+ const baseFilename = attachmentPath
79
+ ? path.basename(attachmentPath, extension)
80
+ : safeName;
81
+ // Ensure unique filename within the test's attachment folder
82
+ const fileName = `${baseFilename}_${Date.now()}${extension}`;
83
+ // Relative path for storing in JSON (relative to baseReportDir)
84
+ const relativePath = path.join(ATTACHMENTS_SUBDIR, attachmentsSubFolder, fileName);
85
+ // Full path for file system operations
86
+ const fullPath = path.join(testAttachmentsDir, fileName);
87
+ if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("image/")) {
88
+ // Handle all image types consistently
89
+ handleImage(attachmentPath, body, base64Images, fullPath, relativePath, pulseResult, name);
90
+ }
91
+ else if (name === "video" || (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("video/"))) {
92
+ handleAttachment(attachmentPath, body, fullPath, relativePath, "videoPath", pulseResult);
93
+ }
94
+ else if (name === "trace" || contentType === "application/zip") {
95
+ // Trace files are zips
96
+ handleAttachment(attachmentPath, body, fullPath, relativePath, "tracePath", pulseResult);
97
+ }
98
+ else {
99
+ // Handle other generic attachments if needed (e.g., log files)
100
+ // console.log(`Pulse Reporter: Processing generic attachment "${name}" (Type: ${contentType}) for test ${testId}`);
101
+ // handleAttachment(attachmentPath, body, fullPath, relativePath, 'otherAttachments', pulseResult); // Example for storing other types
102
+ }
103
+ });
104
+ }
105
+ /**
106
+ * Handles image attachments, either embedding as base64 or copying the file.
107
+ */
108
+ function handleImage(attachmentPath, body, base64Embed, fullPath, relativePath, pulseResult, attachmentName) {
109
+ let screenshotData = undefined;
110
+ if (attachmentPath) {
111
+ try {
112
+ if (base64Embed) {
113
+ const fileContent = fs.readFileSync(attachmentPath, "base64");
114
+ screenshotData = `data:image/${getFileExtension(attachmentName)};base64,${fileContent}`;
115
+ }
116
+ else {
117
+ fs.copyFileSync(attachmentPath, fullPath);
118
+ screenshotData = relativePath;
119
+ }
120
+ }
121
+ catch (error) {
122
+ console.error(`Pulse Reporter: Failed to read/copy screenshot file: ${attachmentPath}. Error: ${error.message}`);
123
+ }
124
+ }
125
+ else if (body) {
126
+ // Always embed if only body is available
127
+ screenshotData = `data:image/${getFileExtension(attachmentName)};base64,${body.toString("base64")}`;
128
+ if (!base64Embed) {
129
+ // Optionally save the buffer to a file even if embedding is off,
130
+ // but the primary representation will be base64.
131
+ try {
132
+ fs.writeFileSync(fullPath, body);
133
+ // console.log(`Pulse Reporter: Saved screenshot buffer to ${fullPath}`);
134
+ }
135
+ catch (error) {
136
+ console.error(`Pulse Reporter: Failed to save screenshot buffer: ${fullPath}. Error: ${error.message}`);
137
+ }
138
+ }
139
+ }
140
+ if (screenshotData) {
141
+ if (!pulseResult.screenshots) {
142
+ pulseResult.screenshots = [];
143
+ }
144
+ pulseResult.screenshots.push(screenshotData);
145
+ }
146
+ }
147
+ /**
148
+ * Handles non-image attachments by copying the file or writing the buffer.
149
+ */
150
+ function handleAttachment(attachmentPath, body, fullPath, relativePath, resultKey, // Add more keys if needed
151
+ pulseResult) {
152
+ try {
153
+ if (attachmentPath) {
154
+ fs.copyFileSync(attachmentPath, fullPath);
155
+ pulseResult[resultKey] = relativePath;
156
+ }
157
+ else if (body) {
158
+ fs.writeFileSync(fullPath, body);
159
+ pulseResult[resultKey] = relativePath; // Store relative path even if from buffer
160
+ }
161
+ }
162
+ catch (error) {
163
+ console.error(`Pulse Reporter: Failed to copy/write attachment to ${fullPath}. Error: ${error.message}`);
164
+ // Don't set the path in pulseResult if saving failed
165
+ }
166
+ }
167
+ /**
168
+ * Determines a file extension based on content type.
169
+ * @param contentType The MIME type string.
170
+ * @returns A file extension string.
171
+ */
172
+ function getFileExtension(contentType) {
173
+ var _a;
174
+ if (!contentType)
175
+ return "bin"; // Default binary extension
176
+ // More robust mapping
177
+ const extensions = {
178
+ "image/png": "png",
179
+ "image/jpeg": "jpg",
180
+ "image/gif": "gif",
181
+ "image/webp": "webp",
182
+ "image/svg+xml": "svg",
183
+ "video/webm": "webm",
184
+ "video/mp4": "mp4",
185
+ "application/zip": "zip", // For traces
186
+ "text/plain": "txt",
187
+ "application/json": "json",
188
+ };
189
+ return (extensions[contentType.toLowerCase()] ||
190
+ ((_a = contentType.split("/")[1]) === null || _a === void 0 ? void 0 : _a.split("+")[0]) ||
191
+ "bin");
192
+ }
@@ -0,0 +1,5 @@
1
+ import { PlaywrightPulseReporter } from "./playwright-pulse-reporter";
2
+ export default PlaywrightPulseReporter;
3
+ export { PlaywrightPulseReporter };
4
+ export type { PlaywrightPulseReport } from "../lib/report-types";
5
+ export type { TestResult, TestRun, TestStep, TestStatus } from "../types";
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PlaywrightPulseReporter = void 0;
4
+ // src/reporter/index.ts
5
+ const playwright_pulse_reporter_1 = require("./playwright-pulse-reporter");
6
+ Object.defineProperty(exports, "PlaywrightPulseReporter", { enumerable: true, get: function () { return playwright_pulse_reporter_1.PlaywrightPulseReporter; } });
7
+ // Export the reporter class as the default export for CommonJS compatibility
8
+ // and also as a named export for potential ES module consumers.
9
+ exports.default = playwright_pulse_reporter_1.PlaywrightPulseReporter;
@@ -0,0 +1,8 @@
1
+ import type { TestResult, TestRun } from '@/types';
2
+ export interface PlaywrightPulseReport {
3
+ run: TestRun | null;
4
+ results: TestResult[];
5
+ metadata: {
6
+ generatedAt: string;
7
+ };
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,27 @@
1
+ import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult as PwTestResult } from "@playwright/test/reporter";
2
+ import type { PlaywrightPulseReporterOptions } from "../types";
3
+ export declare class PlaywrightPulseReporter implements Reporter {
4
+ private config;
5
+ private suite;
6
+ private results;
7
+ private runStartTime;
8
+ private options;
9
+ private outputDir;
10
+ private attachmentsDir;
11
+ private baseOutputFile;
12
+ private isSharded;
13
+ private shardIndex;
14
+ constructor(options?: PlaywrightPulseReporterOptions);
15
+ printsToStdio(): boolean;
16
+ onBegin(config: FullConfig, suite: Suite): void;
17
+ onTestBegin(test: TestCase): void;
18
+ private processStep;
19
+ onTestEnd(test: TestCase, result: PwTestResult): Promise<void>;
20
+ onError(error: any): void;
21
+ private _writeShardResults;
22
+ private _mergeShardResults;
23
+ private _cleanupTemporaryFiles;
24
+ private _ensureDirExists;
25
+ onEnd(result: FullResult): Promise<void>;
26
+ }
27
+ export default PlaywrightPulseReporter;
@@ -0,0 +1,419 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PlaywrightPulseReporter = void 0;
37
+ const fs = __importStar(require("fs/promises"));
38
+ const path = __importStar(require("path"));
39
+ const crypto_1 = require("crypto");
40
+ const attachment_utils_1 = require("./attachment-utils"); // Use relative path
41
+ const convertStatus = (status, testCase) => {
42
+ // Special case: test was expected to fail (test.fail())
43
+ if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
44
+ return status === "failed" ? "passed" : "failed";
45
+ }
46
+ // Special case: test was expected to skip (test.skip())
47
+ if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "skipped") {
48
+ return "skipped";
49
+ }
50
+ switch (status) {
51
+ case "passed":
52
+ return "passed";
53
+ case "failed":
54
+ case "timedOut":
55
+ case "interrupted":
56
+ return "failed";
57
+ case "skipped":
58
+ default:
59
+ return "skipped";
60
+ }
61
+ };
62
+ const TEMP_SHARD_FILE_PREFIX = ".pulse-shard-results-";
63
+ const ATTACHMENTS_SUBDIR = "attachments"; // Centralized definition
64
+ class PlaywrightPulseReporter {
65
+ constructor(options = {}) {
66
+ var _a, _b;
67
+ this.results = [];
68
+ this.baseOutputFile = "playwright-pulse-report.json";
69
+ this.isSharded = false;
70
+ this.shardIndex = undefined;
71
+ this.options = options; // Store provided options
72
+ this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
73
+ // Determine outputDir relative to config file or rootDir
74
+ // The actual resolution happens in onBegin where config is available
75
+ this.outputDir = (_b = options.outputDir) !== null && _b !== void 0 ? _b : "pulse-report-output";
76
+ this.attachmentsDir = path.join(this.outputDir, ATTACHMENTS_SUBDIR); // Initial path, resolved fully in onBegin
77
+ // console.log(`Pulse Reporter Init: Configured outputDir option: ${options.outputDir}, Base file: ${this.baseOutputFile}`);
78
+ }
79
+ printsToStdio() {
80
+ return this.shardIndex === undefined || this.shardIndex === 0;
81
+ }
82
+ onBegin(config, suite) {
83
+ var _a;
84
+ this.config = config;
85
+ this.suite = suite;
86
+ this.runStartTime = Date.now();
87
+ // --- Resolve outputDir relative to config file or rootDir ---
88
+ const configDir = this.config.rootDir;
89
+ // Use config file directory if available, otherwise rootDir
90
+ const configFileDir = this.config.configFile
91
+ ? path.dirname(this.config.configFile)
92
+ : configDir;
93
+ this.outputDir = path.resolve(configFileDir, (_a = this.options.outputDir) !== null && _a !== void 0 ? _a : "pulse-report-output");
94
+ // Resolve attachmentsDir relative to the final outputDir
95
+ this.attachmentsDir = path.resolve(this.outputDir, ATTACHMENTS_SUBDIR);
96
+ // Update options with the resolved absolute path for internal use
97
+ this.options.outputDir = this.outputDir;
98
+ // console.log(`Pulse Reporter onBegin: Final Report Output dir resolved to ${this.outputDir}`);
99
+ // console.log(`Pulse Reporter onBegin: Attachments base dir resolved to ${this.attachmentsDir}`);
100
+ const totalShards = this.config.shard ? this.config.shard.total : 1;
101
+ this.isSharded = totalShards > 1;
102
+ this.shardIndex = this.config.shard
103
+ ? this.config.shard.current - 1
104
+ : undefined;
105
+ // Ensure base output directory exists (attachments handled by attachFiles util)
106
+ this._ensureDirExists(this.outputDir)
107
+ .then(() => {
108
+ if (this.shardIndex === undefined) {
109
+ console.log(`PlaywrightPulseReporter: Starting test run with ${suite.allTests().length} tests${this.isSharded ? ` across ${totalShards} shards` : ""}. Pulse outputting to ${this.outputDir}`);
110
+ // Clean up old shard files only in the main process
111
+ return this._cleanupTemporaryFiles();
112
+ }
113
+ else {
114
+ // console.log(`Pulse Reporter (Shard ${this.shardIndex + 1}/${totalShards}): Starting. Temp results to ${this.outputDir}`);
115
+ return Promise.resolve();
116
+ }
117
+ })
118
+ .catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
119
+ }
120
+ onTestBegin(test) {
121
+ // Optional: Log test start if needed
122
+ // console.log(`Starting test: ${test.title}`);
123
+ }
124
+ async processStep(step, testId) {
125
+ var _a, _b, _c, _d;
126
+ // Determine actual step status (don't inherit from parent)
127
+ let stepStatus = "passed";
128
+ let errorMessage = ((_a = step.error) === null || _a === void 0 ? void 0 : _a.message) || undefined;
129
+ if ((_c = (_b = step.error) === null || _b === void 0 ? void 0 : _b.message) === null || _c === void 0 ? void 0 : _c.startsWith("Test is skipped:")) {
130
+ stepStatus = "skipped";
131
+ errorMessage = "Info: Test is skipped:";
132
+ }
133
+ else {
134
+ stepStatus = convertStatus(step.error ? "failed" : "passed");
135
+ }
136
+ const duration = step.duration;
137
+ const startTime = new Date(step.startTime);
138
+ const endTime = new Date(startTime.getTime() + Math.max(0, duration));
139
+ // Capture code location if available
140
+ let codeLocation = "";
141
+ if (step.location) {
142
+ codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
143
+ }
144
+ return {
145
+ id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}`,
146
+ title: step.title,
147
+ status: stepStatus,
148
+ duration: duration,
149
+ startTime: startTime,
150
+ endTime: endTime,
151
+ errorMessage: errorMessage,
152
+ stackTrace: ((_d = step.error) === null || _d === void 0 ? void 0 : _d.stack) || undefined,
153
+ codeLocation: codeLocation || undefined,
154
+ isHook: step.category === "hook",
155
+ hookType: step.category === "hook"
156
+ ? step.title.toLowerCase().includes("before")
157
+ ? "before"
158
+ : "after"
159
+ : undefined,
160
+ steps: [], // Will be populated recursively
161
+ };
162
+ }
163
+ async onTestEnd(test, result) {
164
+ var _a, _b, _c, _d, _e, _f, _g;
165
+ const testStatus = convertStatus(result.status, test);
166
+ const startTime = new Date(result.startTime);
167
+ const endTime = new Date(startTime.getTime() + result.duration);
168
+ // Generate a slightly more robust ID for attachments, especially if test.id is missing
169
+ const testIdForFiles = test.id ||
170
+ `${test
171
+ .titlePath()
172
+ .join("_")
173
+ .replace(/[^a-zA-Z0-9]/g, "_")}_${startTime.getTime()}`;
174
+ // --- Process Steps Recursively ---
175
+ const processAllSteps = async (steps, parentTestStatus) => {
176
+ let processed = [];
177
+ for (const step of steps) {
178
+ const processedStep = await this.processStep(step, testIdForFiles);
179
+ processed.push(processedStep);
180
+ if (step.steps && step.steps.length > 0) {
181
+ const nestedSteps = await processAllSteps(step.steps, processedStep.status);
182
+ // Assign nested steps correctly
183
+ processedStep.steps = nestedSteps;
184
+ }
185
+ }
186
+ return processed;
187
+ };
188
+ // --- Extract Code Snippet ---
189
+ let codeSnippet = undefined;
190
+ try {
191
+ if (((_a = test.location) === null || _a === void 0 ? void 0 : _a.file) && ((_b = test.location) === null || _b === void 0 ? void 0 : _b.line) && ((_c = test.location) === null || _c === void 0 ? void 0 : _c.column)) {
192
+ const relativePath = path.relative(this.config.rootDir, test.location.file);
193
+ codeSnippet = `Test defined at: ${relativePath}:${test.location.line}:${test.location.column}`;
194
+ }
195
+ }
196
+ catch (e) {
197
+ console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
198
+ }
199
+ // --- Prepare Base TestResult ---
200
+ const pulseResult = {
201
+ id: test.id || `${test.title}-${startTime.toISOString()}-${(0, crypto_1.randomUUID)()}`, // Use the original ID logic here
202
+ runId: "TBD", // Will be set later
203
+ name: test.titlePath().join(" > "),
204
+ suiteName: ((_d = this.config.projects[0]) === null || _d === void 0 ? void 0 : _d.name) || "Default Suite",
205
+ status: testStatus,
206
+ duration: result.duration,
207
+ startTime: startTime,
208
+ endTime: endTime,
209
+ retries: result.retry,
210
+ steps: ((_e = result.steps) === null || _e === void 0 ? void 0 : _e.length)
211
+ ? await processAllSteps(result.steps, testStatus)
212
+ : [],
213
+ errorMessage: (_f = result.error) === null || _f === void 0 ? void 0 : _f.message,
214
+ stackTrace: (_g = result.error) === null || _g === void 0 ? void 0 : _g.stack,
215
+ codeSnippet: codeSnippet,
216
+ tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
217
+ screenshots: [],
218
+ videoPath: undefined,
219
+ tracePath: undefined,
220
+ };
221
+ // --- Process Attachments using the new utility ---
222
+ try {
223
+ (0, attachment_utils_1.attachFiles)(testIdForFiles, result, pulseResult, this.options);
224
+ }
225
+ catch (attachError) {
226
+ console.error(`Pulse Reporter: Error processing attachments for test ${pulseResult.name} (ID: ${testIdForFiles}): ${attachError.message}`);
227
+ }
228
+ this.results.push(pulseResult);
229
+ }
230
+ onError(error) {
231
+ var _a;
232
+ 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);
233
+ if (error === null || error === void 0 ? void 0 : error.stack) {
234
+ console.error(error.stack);
235
+ }
236
+ }
237
+ async _writeShardResults() {
238
+ if (this.shardIndex === undefined) {
239
+ console.warn("Pulse Reporter: _writeShardResults called unexpectedly in main process. Skipping.");
240
+ return;
241
+ }
242
+ const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${this.shardIndex}.json`);
243
+ try {
244
+ // No need to ensureDirExists here, should be done in onBegin
245
+ await fs.writeFile(tempFilePath, JSON.stringify(this.results, (key, value) => {
246
+ if (value instanceof Date) {
247
+ return value.toISOString();
248
+ }
249
+ return value;
250
+ }, 2));
251
+ // console.log(`Pulse Reporter: Shard ${this.shardIndex} wrote ${this.results.length} results to ${tempFilePath}`);
252
+ }
253
+ catch (error) {
254
+ console.error(`Pulse Reporter: Shard ${this.shardIndex} failed to write temporary results to ${tempFilePath}`, error);
255
+ }
256
+ }
257
+ async _mergeShardResults(finalRunData) {
258
+ // console.log('Pulse Reporter: Merging results from shards...');
259
+ let allResults = [];
260
+ const totalShards = this.config.shard ? this.config.shard.total : 1;
261
+ for (let i = 0; i < totalShards; i++) {
262
+ const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
263
+ try {
264
+ const content = await fs.readFile(tempFilePath, "utf-8");
265
+ const shardResults = JSON.parse(content);
266
+ shardResults.forEach((r) => (r.runId = finalRunData.id));
267
+ allResults = allResults.concat(shardResults);
268
+ // console.log(`Pulse Reporter: Successfully merged ${shardResults.length} results from shard ${i}`);
269
+ }
270
+ catch (error) {
271
+ if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
272
+ console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}. This might happen if shard ${i} had no tests or failed early.`);
273
+ }
274
+ else {
275
+ console.error(`Pulse Reporter: Could not read or parse results from shard ${i} (${tempFilePath}). Error:`, error);
276
+ }
277
+ }
278
+ }
279
+ // console.log(`Pulse Reporter: Merged a total of ${allResults.length} results from ${totalShards} shards.`);
280
+ finalRunData.passed = allResults.filter((r) => r.status === "passed").length;
281
+ finalRunData.failed = allResults.filter((r) => r.status === "failed").length;
282
+ finalRunData.skipped = allResults.filter((r) => r.status === "skipped").length;
283
+ finalRunData.totalTests = allResults.length;
284
+ const reviveDates = (key, value) => {
285
+ const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
286
+ if (typeof value === "string" && isoDateRegex.test(value)) {
287
+ const date = new Date(value);
288
+ if (!isNaN(date.getTime())) {
289
+ return date;
290
+ }
291
+ }
292
+ return value;
293
+ };
294
+ const finalParsedResults = JSON.parse(JSON.stringify(allResults), reviveDates);
295
+ return {
296
+ run: finalRunData,
297
+ results: finalParsedResults,
298
+ metadata: { generatedAt: new Date().toISOString() },
299
+ };
300
+ }
301
+ async _cleanupTemporaryFiles() {
302
+ try {
303
+ // No need to ensure dir exists here if handled in onBegin
304
+ const files = await fs.readdir(this.outputDir);
305
+ const tempFiles = files.filter((f) => f.startsWith(TEMP_SHARD_FILE_PREFIX));
306
+ if (tempFiles.length > 0) {
307
+ // console.log(`Pulse Reporter: Cleaning up ${tempFiles.length} temporary shard files...`);
308
+ await Promise.all(tempFiles.map((f) => fs.unlink(path.join(this.outputDir, f))));
309
+ }
310
+ }
311
+ catch (error) {
312
+ if ((error === null || error === void 0 ? void 0 : error.code) !== "ENOENT") {
313
+ // Ignore if the directory doesn't exist
314
+ console.error("Pulse Reporter: Error cleaning up temporary files:", error);
315
+ }
316
+ }
317
+ }
318
+ async _ensureDirExists(dirPath, clean = false) {
319
+ try {
320
+ if (clean) {
321
+ // console.log(`Pulse Reporter: Cleaning directory ${dirPath}...`);
322
+ await fs.rm(dirPath, { recursive: true, force: true });
323
+ }
324
+ await fs.mkdir(dirPath, { recursive: true });
325
+ }
326
+ catch (error) {
327
+ // Ignore EEXIST error if the directory already exists
328
+ if (error.code !== "EEXIST") {
329
+ console.error(`Pulse Reporter: Failed to ensure directory exists: ${dirPath}`, error);
330
+ throw error; // Re-throw other errors
331
+ }
332
+ }
333
+ }
334
+ async onEnd(result) {
335
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
336
+ if (this.shardIndex !== undefined) {
337
+ await this._writeShardResults();
338
+ // console.log(`PlaywrightPulseReporter: Shard ${this.shardIndex + 1} finished writing results.`);
339
+ return;
340
+ }
341
+ const runEndTime = Date.now();
342
+ const duration = runEndTime - this.runStartTime;
343
+ const runId = `run-${this.runStartTime}-${(0, crypto_1.randomUUID)()}`;
344
+ const runData = {
345
+ id: runId,
346
+ timestamp: new Date(this.runStartTime),
347
+ totalTests: 0, // Will be updated after merging/processing
348
+ passed: 0,
349
+ failed: 0,
350
+ skipped: 0,
351
+ duration,
352
+ };
353
+ let finalReport;
354
+ if (this.isSharded) {
355
+ // console.log("Pulse Reporter: Run ended, main process merging shard results...");
356
+ finalReport = await this._mergeShardResults(runData);
357
+ }
358
+ else {
359
+ // console.log("Pulse Reporter: Run ended, processing results directly (no sharding)...");
360
+ this.results.forEach((r) => (r.runId = runId)); // Assign runId to directly collected results
361
+ runData.passed = this.results.filter((r) => r.status === "passed").length;
362
+ runData.failed = this.results.filter((r) => r.status === "failed").length;
363
+ runData.skipped = this.results.filter((r) => r.status === "skipped").length;
364
+ runData.totalTests = this.results.length;
365
+ finalReport = {
366
+ run: runData,
367
+ results: this.results, // Use directly collected results
368
+ metadata: { generatedAt: new Date().toISOString() },
369
+ };
370
+ }
371
+ const finalRunStatus = ((_b = (_a = finalReport.run) === null || _a === void 0 ? void 0 : _a.failed) !== null && _b !== void 0 ? _b : 0 > 0)
372
+ ? "failed"
373
+ : ((_c = finalReport.run) === null || _c === void 0 ? void 0 : _c.totalTests) === 0
374
+ ? "no tests"
375
+ : "passed";
376
+ const summary = `
377
+ PlaywrightPulseReporter: Run Finished
378
+ -----------------------------------------
379
+ Overall Status: ${finalRunStatus.toUpperCase()}
380
+ Total Tests: ${(_e = (_d = finalReport.run) === null || _d === void 0 ? void 0 : _d.totalTests) !== null && _e !== void 0 ? _e : "N/A"}
381
+ Passed: ${(_g = (_f = finalReport.run) === null || _f === void 0 ? void 0 : _f.passed) !== null && _g !== void 0 ? _g : "N/A"}
382
+ Failed: ${(_j = (_h = finalReport.run) === null || _h === void 0 ? void 0 : _h.failed) !== null && _j !== void 0 ? _j : "N/A"}
383
+ Skipped: ${(_l = (_k = finalReport.run) === null || _k === void 0 ? void 0 : _k.skipped) !== null && _l !== void 0 ? _l : "N/A"}
384
+ Duration: ${(duration / 1000).toFixed(2)}s
385
+ -----------------------------------------`;
386
+ console.log(summary);
387
+ const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
388
+ try {
389
+ // Ensure directory exists before writing final report
390
+ await this._ensureDirExists(this.outputDir);
391
+ // --- Write Final JSON Report ---
392
+ await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, (key, value) => {
393
+ if (value instanceof Date) {
394
+ return value.toISOString(); // Ensure dates are ISO strings in JSON
395
+ }
396
+ // Handle potential BigInt if used elsewhere, though unlikely here
397
+ if (typeof value === "bigint") {
398
+ return value.toString();
399
+ }
400
+ return value;
401
+ }, 2));
402
+ console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
403
+ // REMOVED Static HTML Generation Call
404
+ // The reporter's responsibility is now only to create the JSON file.
405
+ // The user will run `npx generate-pulse-report` separately.
406
+ }
407
+ catch (error) {
408
+ console.error(`Pulse Reporter: Failed to write final JSON report to ${finalOutputPath}. Error: ${error.message}`);
409
+ }
410
+ finally {
411
+ if (this.isSharded) {
412
+ // console.log("Pulse Reporter: Cleaning up temporary shard files...");
413
+ await this._cleanupTemporaryFiles();
414
+ }
415
+ }
416
+ }
417
+ }
418
+ exports.PlaywrightPulseReporter = PlaywrightPulseReporter;
419
+ exports.default = PlaywrightPulseReporter;