@arghajit/dummy
Version:
A Playwright reporter and dashboard for visualizing test results.
498 lines (497 loc) • 23.6 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlaywrightPulseReporter = void 0;
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const crypto_1 = require("crypto");
const ua_parser_js_1 = require("ua-parser-js");
const os = __importStar(require("os"));
const convertStatus = (status, testCase) => {
if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
return "failed";
}
if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "skipped") {
return "skipped";
}
switch (status) {
case "passed":
return "passed";
case "failed":
case "timedOut":
case "interrupted":
return "failed";
case "skipped":
default:
return "skipped";
}
};
const TEMP_SHARD_FILE_PREFIX = ".pulse-shard-results-";
const ATTACHMENTS_SUBDIR = "attachments";
class PlaywrightPulseReporter {
constructor(options = {}) {
var _a, _b;
this.results = [];
this.baseOutputFile = "playwright-pulse-report.json";
this.isSharded = false;
this.shardIndex = undefined;
this.options = options;
this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
this.outputDir = (_b = options.outputDir) !== null && _b !== void 0 ? _b : "pulse-report";
this.attachmentsDir = path.join(this.outputDir, ATTACHMENTS_SUBDIR);
}
printsToStdio() {
return this.shardIndex === undefined || this.shardIndex === 0;
}
onBegin(config, suite) {
var _a;
this.config = config;
this.suite = suite;
this.runStartTime = Date.now();
const configDir = this.config.rootDir;
const configFileDir = this.config.configFile
? path.dirname(this.config.configFile)
: configDir;
this.outputDir = path.resolve(configFileDir, (_a = this.options.outputDir) !== null && _a !== void 0 ? _a : "pulse-report");
this.attachmentsDir = path.resolve(this.outputDir, ATTACHMENTS_SUBDIR);
this.options.outputDir = this.outputDir;
const totalShards = this.config.shard ? this.config.shard.total : 1;
this.isSharded = totalShards > 1;
this.shardIndex = this.config.shard
? this.config.shard.current - 1
: undefined;
this._ensureDirExists(this.outputDir)
.then(() => {
if (this.shardIndex === undefined || this.shardIndex === 0) {
console.log(`PlaywrightPulseReporter: Starting test run with ${suite.allTests().length} tests${this.isSharded ? ` across ${totalShards} shards` : ""}. Pulse outputting to ${this.outputDir}`);
if (this.shardIndex === undefined ||
(this.isSharded && this.shardIndex === 0)) {
return this._cleanupTemporaryFiles();
}
}
})
.catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
}
onTestBegin(test) {
console.log(`Starting test: ${test.title}`);
}
getBrowserDetails(test) {
var _a, _b, _c, _d;
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
const projectConfig = project === null || project === void 0 ? void 0 : project.use;
const userAgent = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.userAgent;
const configuredBrowserType = (_b = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.browserName) === null || _b === void 0 ? void 0 : _b.toLowerCase();
const parser = new ua_parser_js_1.UAParser(userAgent);
const result = parser.getResult();
let browserName = result.browser.name;
const browserVersion = result.browser.version
? ` v${result.browser.version.split(".")[0]}`
: "";
const osName = result.os.name ? ` on ${result.os.name}` : "";
const osVersion = result.os.version
? ` ${result.os.version.split(".")[0]}`
: "";
const deviceType = result.device.type;
let finalString;
if (browserName === undefined) {
browserName = configuredBrowserType;
finalString = `${browserName}`;
}
else {
if (deviceType === "mobile" || deviceType === "tablet") {
if ((_c = result.os.name) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes("android")) {
if (browserName.toLowerCase().includes("chrome"))
browserName = "Chrome Mobile";
else if (browserName.toLowerCase().includes("firefox"))
browserName = "Firefox Mobile";
else if (result.engine.name === "Blink" && !result.browser.name)
browserName = "Android WebView";
else if (browserName &&
!browserName.toLowerCase().includes("mobile")) {
// Keep it as is
}
else {
browserName = "Android Browser";
}
}
else if ((_d = result.os.name) === null || _d === void 0 ? void 0 : _d.toLowerCase().includes("ios")) {
browserName = "Mobile Safari";
}
}
else if (browserName === "Electron") {
browserName = "Electron App";
}
finalString = `${browserName}${browserVersion}${osName}${osVersion}`;
}
return finalString.trim();
}
async processStep(step, testId, browserDetails, testCase) {
var _a, _b, _c, _d;
let stepStatus = "passed";
let errorMessage = ((_a = step.error) === null || _a === void 0 ? void 0 : _a.message) || undefined;
if ((_c = (_b = step.error) === null || _b === void 0 ? void 0 : _b.message) === null || _c === void 0 ? void 0 : _c.startsWith("Test is skipped:")) {
stepStatus = "skipped";
}
else {
stepStatus = convertStatus(step.error ? "failed" : "passed", testCase);
}
const duration = step.duration;
const startTime = new Date(step.startTime);
const endTime = new Date(startTime.getTime() + Math.max(0, duration));
let codeLocation = "";
if (step.location) {
codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
}
return {
id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}`,
title: step.title,
status: stepStatus,
duration: duration,
startTime: startTime,
endTime: endTime,
browser: browserDetails,
errorMessage: errorMessage,
stackTrace: ((_d = step.error) === null || _d === void 0 ? void 0 : _d.stack) || undefined,
codeLocation: codeLocation || undefined,
isHook: step.category === "hook",
hookType: step.category === "hook"
? step.title.toLowerCase().includes("before")
? "before"
: "after"
: undefined,
steps: [],
};
}
async onTestEnd(test, result) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
const browserDetails = this.getBrowserDetails(test);
const testStatus = convertStatus(result.status, test);
const startTime = new Date(result.startTime);
const endTime = new Date(startTime.getTime() + result.duration);
const processAllSteps = async (steps) => {
let processed = [];
for (const step of steps) {
const processedStep = await this.processStep(step, test.id, browserDetails, test);
processed.push(processedStep);
if (step.steps && step.steps.length > 0) {
processedStep.steps = await processAllSteps(step.steps);
}
}
return processed;
};
let codeSnippet = undefined;
try {
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)) {
const relativePath = path.relative(this.config.rootDir, test.location.file);
codeSnippet = `Test defined at: ${relativePath}:${test.location.line}:${test.location.column}`;
}
}
catch (e) {
console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
}
const stdoutMessages = result.stdout.map((item) => typeof item === "string" ? item : item.toString());
const stderrMessages = result.stderr.map((item) => typeof item === "string" ? item : item.toString());
const maxWorkers = this.config.workers;
let mappedWorkerId = result.workerIndex === -1
? -1
: (result.workerIndex % (maxWorkers > 0 ? maxWorkers : 1)) + 1;
const testSpecificData = {
workerId: mappedWorkerId,
totalWorkers: maxWorkers,
configFile: this.config.configFile,
metadata: this.config.metadata
? JSON.stringify(this.config.metadata)
: undefined,
};
const pulseResult = {
id: test.id,
runId: "TBD",
name: test.titlePath().join(" > "),
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",
status: testStatus,
duration: result.duration,
startTime: startTime,
endTime: endTime,
browser: browserDetails,
retries: result.retry,
steps: ((_f = result.steps) === null || _f === void 0 ? void 0 : _f.length) ? await processAllSteps(result.steps) : [],
errorMessage: (_g = result.error) === null || _g === void 0 ? void 0 : _g.message,
stackTrace: (_h = result.error) === null || _h === void 0 ? void 0 : _h.stack,
codeSnippet: codeSnippet,
tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
screenshots: [],
videoPath: [],
tracePath: undefined,
attachments: [],
stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
...testSpecificData,
};
// --- CORRECTED ATTACHMENT PROCESSING LOGIC ---
for (const [index, attachment] of result.attachments.entries()) {
if (!attachment.path)
continue;
try {
// Create a sanitized, unique folder name for this specific test
const testSubfolder = test.id.replace(/[^a-zA-Z0-9_-]/g, "_");
// Sanitize the original attachment name to create a safe filename
const safeAttachmentName = path
.basename(attachment.path)
.replace(/[^a-zA-Z0-9_.-]/g, "_");
// Create a unique filename to prevent collisions, especially in retries
const uniqueFileName = `${index}-${Date.now()}-${safeAttachmentName}`;
// This is the relative path that will be stored in the JSON report
const relativeDestPath = path.join(ATTACHMENTS_SUBDIR, testSubfolder, uniqueFileName);
// This is the absolute path used for the actual file system operation
const absoluteDestPath = path.join(this.outputDir, relativeDestPath);
// Ensure the unique, test-specific attachment directory exists
await this._ensureDirExists(path.dirname(absoluteDestPath));
await fs.copyFile(attachment.path, absoluteDestPath);
// Categorize the attachment based on its content type
if (attachment.contentType.startsWith("image/")) {
(_j = pulseResult.screenshots) === null || _j === void 0 ? void 0 : _j.push(relativeDestPath);
}
else if (attachment.contentType.startsWith("video/")) {
(_k = pulseResult.videoPath) === null || _k === void 0 ? void 0 : _k.push(relativeDestPath);
}
else if (attachment.name === "trace") {
pulseResult.tracePath = relativeDestPath;
}
else {
(_l = pulseResult.attachments) === null || _l === void 0 ? void 0 : _l.push({
name: attachment.name, // The original, human-readable name
path: relativeDestPath, // The safe, relative path for linking
contentType: attachment.contentType,
});
}
}
catch (err) {
console.error(`Pulse Reporter: Failed to process attachment "${attachment.name}" for test ${pulseResult.name}. Error: ${err.message}`);
}
}
const existingTestIndex = this.results.findIndex((r) => r.id === test.id);
if (existingTestIndex !== -1) {
if (pulseResult.retries >= this.results[existingTestIndex].retries) {
this.results[existingTestIndex] = pulseResult;
}
}
else {
this.results.push(pulseResult);
}
}
onError(error) {
var _a;
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);
if (error === null || error === void 0 ? void 0 : error.stack) {
console.error(error.stack);
}
}
_getEnvDetails() {
return {
host: os.hostname(),
os: `${os.platform()} ${os.release()}`,
cpu: {
model: os.cpus()[0] ? os.cpus()[0].model : "N/A",
cores: os.cpus().length,
},
memory: `${(os.totalmem() / 1024 ** 3).toFixed(2)}GB`,
node: process.version,
v8: process.versions.v8,
cwd: process.cwd(),
};
}
async _writeShardResults() {
if (this.shardIndex === undefined) {
return;
}
const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${this.shardIndex}.json`);
try {
await fs.writeFile(tempFilePath, JSON.stringify(this.results, (key, value) => (value instanceof Date ? value.toISOString() : value), 2));
}
catch (error) {
console.error(`Pulse Reporter: Shard ${this.shardIndex} failed to write temporary results to ${tempFilePath}`, error);
}
}
async _mergeShardResults(finalRunData) {
let allShardProcessedResults = [];
const totalShards = this.config.shard ? this.config.shard.total : 1;
for (let i = 0; i < totalShards; i++) {
const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
try {
const content = await fs.readFile(tempFilePath, "utf-8");
const shardResults = JSON.parse(content);
allShardProcessedResults =
allShardProcessedResults.concat(shardResults);
}
catch (error) {
if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}. This might be normal if a shard had no tests or failed early.`);
}
else {
console.error(`Pulse Reporter: Could not read/parse results from shard ${i} (${tempFilePath}). Error:`, error);
}
}
}
let finalUniqueResultsMap = new Map();
for (const result of allShardProcessedResults) {
const existing = finalUniqueResultsMap.get(result.id);
if (!existing || result.retries >= existing.retries) {
finalUniqueResultsMap.set(result.id, result);
}
}
const finalResultsList = Array.from(finalUniqueResultsMap.values());
finalResultsList.forEach((r) => (r.runId = finalRunData.id));
finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
finalRunData.totalTests = finalResultsList.length;
const reviveDates = (key, value) => {
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
if (typeof value === "string" && isoDateRegex.test(value)) {
const date = new Date(value);
return !isNaN(date.getTime()) ? date : value;
}
return value;
};
const properlyTypedResults = JSON.parse(JSON.stringify(finalResultsList), reviveDates);
return {
run: finalRunData,
results: properlyTypedResults,
metadata: { generatedAt: new Date().toISOString() },
};
}
async _cleanupTemporaryFiles() {
try {
const files = await fs.readdir(this.outputDir);
const tempFiles = files.filter((f) => f.startsWith(TEMP_SHARD_FILE_PREFIX));
if (tempFiles.length > 0) {
await Promise.all(tempFiles.map((f) => fs.unlink(path.join(this.outputDir, f))));
}
}
catch (error) {
if ((error === null || error === void 0 ? void 0 : error.code) !== "ENOENT") {
console.warn("Pulse Reporter: Warning during cleanup of temporary files:", error.message);
}
}
}
async _ensureDirExists(dirPath) {
try {
await fs.mkdir(dirPath, { recursive: true });
}
catch (error) {
if (error.code !== "EEXIST") {
console.error(`Pulse Reporter: Failed to ensure directory exists: ${dirPath}`, error);
throw error;
}
}
}
async onEnd(result) {
if (this.shardIndex !== undefined) {
await this._writeShardResults();
return;
}
const runEndTime = Date.now();
const duration = runEndTime - this.runStartTime;
const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`;
const environmentDetails = this._getEnvDetails();
const runData = {
id: runId,
timestamp: new Date(this.runStartTime),
totalTests: 0,
passed: 0,
failed: 0,
skipped: 0,
duration,
environment: environmentDetails,
};
let finalReport = undefined;
if (this.isSharded) {
finalReport = await this._mergeShardResults(runData);
if (finalReport && finalReport.run && !finalReport.run.environment) {
finalReport.run.environment = environmentDetails;
}
}
else {
this.results.forEach((r) => (r.runId = runId));
runData.passed = this.results.filter((r) => r.status === "passed").length;
runData.failed = this.results.filter((r) => r.status === "failed").length;
runData.skipped = this.results.filter((r) => r.status === "skipped").length;
runData.totalTests = this.results.length;
const reviveDates = (key, value) => {
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
if (typeof value === "string" && isoDateRegex.test(value)) {
const date = new Date(value);
return !isNaN(date.getTime()) ? date : value;
}
return value;
};
const properlyTypedResults = JSON.parse(JSON.stringify(this.results), reviveDates);
finalReport = {
run: runData,
results: properlyTypedResults,
metadata: { generatedAt: new Date().toISOString() },
};
}
if (!finalReport) {
console.error("PlaywrightPulseReporter: CRITICAL - finalReport object was not generated. Cannot create summary.");
return;
}
const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
try {
await this._ensureDirExists(this.outputDir);
await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, (key, value) => {
if (value instanceof Date)
return value.toISOString();
if (typeof value === "bigint")
return value.toString();
return value;
}, 2));
if (this.printsToStdio()) {
console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
}
}
catch (error) {
console.error(`Pulse Reporter: Failed to write final JSON report to ${finalOutputPath}. Error: ${error.message}`);
}
finally {
if (this.isSharded) {
await this._cleanupTemporaryFiles();
}
}
}
}
exports.PlaywrightPulseReporter = PlaywrightPulseReporter;
exports.default = PlaywrightPulseReporter;