@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.
- package/README.md +192 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +26 -0
- package/dist/lib/report-types.d.ts +8 -0
- package/dist/lib/report-types.js +2 -0
- package/dist/playwright-pulse-reporter.d.ts +26 -0
- package/dist/playwright-pulse-reporter.js +304 -0
- package/dist/reporter/attachment-utils.d.ts +10 -0
- package/dist/reporter/attachment-utils.js +192 -0
- package/dist/reporter/index.d.ts +5 -0
- package/dist/reporter/index.js +9 -0
- package/dist/reporter/lib/report-types.d.ts +8 -0
- package/dist/reporter/lib/report-types.js +2 -0
- package/dist/reporter/playwright-pulse-reporter.d.ts +27 -0
- package/dist/reporter/playwright-pulse-reporter.js +419 -0
- package/dist/reporter/reporter/playwright-pulse-reporter.d.ts +1 -0
- package/dist/reporter/reporter/playwright-pulse-reporter.js +380 -0
- package/dist/reporter/tsconfig.reporter.tsbuildinfo +1 -0
- package/dist/reporter/types/index.d.ts +52 -0
- package/dist/reporter/types/index.js +2 -0
- package/dist/types/index.d.ts +61 -0
- package/dist/types/index.js +2 -0
- package/package.json +107 -0
- package/scripts/generate-static-report.mjs +1539 -0
|
@@ -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,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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|