@arghajit/playwright-pulse-report
Version:
A Playwright reporter and dashboard for visualizing test results.
305 lines (304 loc) • 14 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"));
// Helper to convert Playwright status to Pulse status
const convertStatus = (status) => {
if (status === "passed")
return "passed";
if (status === "failed" || status === "timedOut" || status === "interrupted")
return "failed";
return "skipped";
};
const TEMP_SHARD_FILE_PREFIX = ".pulse-shard-results-";
// Use standard ES module export
class PlaywrightPulseReporter {
constructor(options = {}) {
var _a;
this.results = []; // Holds results *per process* (main or shard)
this.baseOutputFile = "playwright-pulse-report.json";
this.isSharded = false;
this.shardIndex = undefined;
this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
// Initial outputDir setup (will be refined in onBegin)
const baseDir = options.outputDir
? path.resolve(options.outputDir)
: process.cwd();
this.outputDir = baseDir;
console.log(`PlaywrightPulseReporter: Initial Output dir configured to ${this.outputDir}`);
}
printsToStdio() {
return this.shardIndex === undefined || this.shardIndex === 0;
}
onBegin(config, suite) {
this.config = config;
this.suite = suite;
this.runStartTime = Date.now();
const totalShards = parseInt(process.env.PLAYWRIGHT_SHARD_TOTAL || "1", 10);
this.isSharded = totalShards > 1;
if (process.env.PLAYWRIGHT_SHARD_INDEX !== undefined) {
this.shardIndex = parseInt(process.env.PLAYWRIGHT_SHARD_INDEX, 10);
}
const configDir = this.config.rootDir;
this.outputDir = this.outputDir
? path.resolve(configDir, this.outputDir)
: path.resolve(configDir, "pulse-report");
console.log(`PlaywrightPulseReporter: Final Output dir resolved to ${this.outputDir}`);
if (this.shardIndex === undefined) {
// Main process
console.log(`PlaywrightPulseReporter: Starting test run with ${suite.allTests().length} tests${this.isSharded ? ` across ${totalShards} shards` : ""}. Outputting to ${this.outputDir}`);
this._cleanupTemporaryFiles().catch((err) => console.error("Pulse Reporter: Error cleaning up temp files:", err));
}
else {
// Shard process
console.log(`PlaywrightPulseReporter: Shard ${this.shardIndex + 1}/${totalShards} starting. Outputting temp results to ${this.outputDir}`);
}
}
onTestBegin(test) {
// Optional: Log test start
}
processStep(step, parentStatus) {
var _a;
const inherentStatus = parentStatus === "failed" || parentStatus === "skipped"
? parentStatus
: convertStatus(step.error ? "failed" : "passed");
const duration = step.duration;
const startTime = new Date(step.startTime);
const endTime = new Date(startTime.getTime() + Math.max(0, duration));
return {
id: `${step.title}-${startTime.toISOString()}-${duration}-${Math.random()
.toString(16)
.slice(2)}`,
title: step.title,
status: inherentStatus,
duration: duration,
startTime: startTime,
endTime: endTime,
errorMessage: (_a = step.error) === null || _a === void 0 ? void 0 : _a.message,
screenshot: undefined, // Placeholder
};
}
onTestEnd(test, result) {
var _a, _b, _c, _d, _e;
const testStatus = convertStatus(result.status);
const startTime = new Date(result.startTime);
const endTime = new Date(startTime.getTime() + result.duration);
const processAllSteps = (steps, parentTestStatus) => {
let processed = [];
for (const step of steps) {
const processedStep = this.processStep(step, parentTestStatus);
processed.push(processedStep);
if (step.steps.length > 0) {
processed = processed.concat(processAllSteps(step.steps, processedStep.status));
}
}
return processed;
};
let codeSnippet = undefined;
try {
if ((_a = test.location) === null || _a === void 0 ? void 0 : _a.file) {
codeSnippet = `Test defined at: ${test.location.file}:${test.location.line}:${test.location.column}`;
}
}
catch (e) {
console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
}
const pulseResult = {
id: test.id ||
`${test.title}-${startTime.toISOString()}-${Math.random()
.toString(16)
.slice(2)}`,
runId: "TBD",
name: test.titlePath().join(" > "),
suiteName: test.parent.title,
status: testStatus,
duration: result.duration,
startTime: startTime,
endTime: endTime,
retries: result.retry,
steps: processAllSteps(result.steps, testStatus),
errorMessage: (_b = result.error) === null || _b === void 0 ? void 0 : _b.message,
stackTrace: (_c = result.error) === null || _c === void 0 ? void 0 : _c.stack,
codeSnippet: codeSnippet,
screenshot: (_d = result.attachments.find((a) => a.name === "screenshot")) === null || _d === void 0 ? void 0 : _d.path,
video: (_e = result.attachments.find((a) => a.name === "video")) === null || _e === void 0 ? void 0 : _e.path,
tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
};
this.results.push(pulseResult);
}
onError(error) {
var _a;
console.error(`PlaywrightPulseReporter: Error encountered (Shard: ${(_a = this.shardIndex) !== null && _a !== void 0 ? _a : "Main"}):`, error);
}
async _writeShardResults() {
if (this.shardIndex === undefined) {
console.warn("Pulse Reporter: _writeShardResults called in main process. Skipping.");
return;
}
const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${this.shardIndex}.json`);
try {
await this._ensureDirExists(this.outputDir);
await fs.writeFile(tempFilePath, JSON.stringify(this.results, null, 2));
}
catch (error) {
console.error(`Pulse Reporter: Shard ${this.shardIndex} failed to write temporary results to ${tempFilePath}`, error);
}
}
async _mergeShardResults(finalRunData) {
console.log("Pulse Reporter: Merging results from shards...");
let allResults = [];
const totalShards = parseInt(process.env.PLAYWRIGHT_SHARD_TOTAL || "1", 10);
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);
shardResults.forEach((r) => (r.runId = finalRunData.id));
allResults = allResults.concat(shardResults);
}
catch (error) {
if (error && error.code === "ENOENT") {
console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}.`);
}
else {
console.warn(`Pulse Reporter: Could not read or parse results from shard ${i} (${tempFilePath}). Error: ${error}`);
}
}
}
console.log(`Pulse Reporter: Merged a total of ${allResults.length} results from ${totalShards} shards.`);
finalRunData.passed = allResults.filter((r) => r.status === "passed").length;
finalRunData.failed = allResults.filter((r) => r.status === "failed").length;
finalRunData.skipped = allResults.filter((r) => r.status === "skipped").length;
finalRunData.totalTests = allResults.length;
return {
run: finalRunData,
results: allResults,
metadata: { generatedAt: new Date().toISOString() },
};
}
async _cleanupTemporaryFiles() {
try {
await this._ensureDirExists(this.outputDir);
const files = await fs.readdir(this.outputDir);
const tempFiles = files.filter((f) => f.startsWith(TEMP_SHARD_FILE_PREFIX));
if (tempFiles.length > 0) {
console.log(`Pulse Reporter: Cleaning up ${tempFiles.length} temporary shard files...`);
await Promise.all(tempFiles.map((f) => fs.unlink(path.join(this.outputDir, f))));
}
}
catch (error) {
if (error && error.code !== "ENOENT") {
console.error("Pulse Reporter: Error cleaning up temporary files:", error);
}
}
}
async _ensureDirExists(dirPath) {
try {
await fs.mkdir(dirPath, { recursive: true });
}
catch (error) {
if (error && error.code !== "EEXIST") {
console.error(`Pulse Reporter: Failed to ensure directory exists: ${dirPath}`, error);
throw error;
}
}
}
async onEnd(result) {
var _a, _b, _c, _d, _e, _f;
if (this.shardIndex !== undefined) {
await this._writeShardResults();
console.log(`PlaywrightPulseReporter: Shard ${this.shardIndex + 1} finished.`);
return;
}
const runEndTime = Date.now();
const duration = runEndTime - this.runStartTime;
const runId = `run-${this.runStartTime}-${Math.random()
.toString(16)
.slice(2)}`;
const runData = {
id: runId,
timestamp: new Date(this.runStartTime),
totalTests: 0,
passed: 0,
failed: 0,
skipped: 0,
duration,
};
let finalReport;
if (this.isSharded) {
finalReport = await this._mergeShardResults(runData);
}
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;
finalReport = {
run: runData,
results: this.results,
metadata: { generatedAt: new Date().toISOString() },
};
}
const finalRunStatus = ((_b = (_a = finalReport.run) === null || _a === void 0 ? void 0 : _a.failed) !== null && _b !== void 0 ? _b : 0 > 0) ? "failed" : "passed";
console.log(`PlaywrightPulseReporter: Test run finished with overall status: ${finalRunStatus}`);
console.log(` Passed: ${(_c = finalReport.run) === null || _c === void 0 ? void 0 : _c.passed}, Failed: ${(_d = finalReport.run) === null || _d === void 0 ? void 0 : _d.failed}, Skipped: ${(_e = finalReport.run) === null || _e === void 0 ? void 0 : _e.skipped}`);
console.log(` Total tests: ${(_f = finalReport.run) === null || _f === void 0 ? void 0 : _f.totalTests}`);
console.log(` Total time: ${(duration / 1000).toFixed(2)}s`);
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();
}
return value;
}, 2));
console.log(`PlaywrightPulseReporter: Final report written to ${finalOutputPath}`);
}
catch (error) {
console.error(`PlaywrightPulseReporter: Failed to write final report to ${finalOutputPath}`, error);
}
finally {
if (this.isSharded) {
await this._cleanupTemporaryFiles();
}
}
}
}
exports.PlaywrightPulseReporter = PlaywrightPulseReporter;
// No module.exports needed for ES modules