@arghajit/dummy
Version:
A Playwright reporter and dashboard for visualizing test results.
628 lines (627 loc) • 31 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;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
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 = __importDefault(require("ua-parser-js"));
const os = __importStar(require("os"));
const compression_utils_1 = require("../utils/compression-utils");
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, _c, _d;
this.results = [];
this._pendingTestEnds = [];
this.baseOutputFile = "playwright-pulse-report.json";
this.individualReportsSubDir = "pulse-results";
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.individualReportsSubDir = (_c = options.individualReportsSubDir) !== null && _c !== void 0 ? _c : "pulse-results";
this.attachmentsDir = path.join(this.outputDir, ATTACHMENTS_SUBDIR);
this.resetOnEachRun = (_d = options.resetOnEachRun) !== null && _d !== void 0 ? _d : true;
}
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(async () => {
if (this.printsToStdio()) {
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)) {
await this._cleanupTemporaryFiles();
}
}
})
.catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
}
onTestBegin(test) {
console.log(`Starting test: ${test.title}`);
}
_getSeverity(annotations) {
const severityAnnotation = annotations.find((a) => a.type === "pulse_severity");
return (severityAnnotation === null || severityAnnotation === void 0 ? void 0 : severityAnnotation.description) || "Medium";
}
extractCodeSnippet(filePath, targetLine, targetColumn) {
var _a;
try {
const fsSync = require("fs");
if (!fsSync.existsSync(filePath)) {
return "";
}
const content = fsSync.readFileSync(filePath, "utf8");
const lines = content.split("\n");
if (targetLine < 1 || targetLine > lines.length) {
return "";
}
return ((_a = lines[targetLine - 1]) === null || _a === void 0 ? void 0 : _a.trim()) || "";
}
catch (e) {
return "";
}
}
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.default(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 = "";
let codeSnippet = "";
if (step.location) {
codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
codeSnippet = this.extractCodeSnippet(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,
codeSnippet: codeSnippet,
isHook: step.category === "hook",
hookType: step.category === "hook"
? step.title.toLowerCase().includes("before")
? "before"
: "after"
: undefined,
steps: [],
};
}
async onTestEnd(test, result) {
// Track this async call so onEnd() can wait for all in-flight processing
// before it reads this.results. This prevents the race condition where
// onEnd() fires before an interrupted test's onTestEnd() has finished
// its async attachment I/O and pushed to this.results.
const p = this._processTestEnd(test, result);
this._pendingTestEnds.push(p);
await p;
}
async _processTestEnd(test, result) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
const browserDetails = this.getBrowserDetails(test);
const uniqueTestId = `${(project === null || project === void 0 ? void 0 : project.name) || "default"}-${test.id}`;
// Captured outcome from Playwright
const outcome = test.outcome();
// Calculate final status based on the last result (Last-Run-Wins)
// result.status in onTestEnd is typically the status of the test run (passed if flaky passed)
// But we double check the last result in test.results just to be sure/consistent
const lastResult = test.results[test.results.length - 1];
const finalStatus = convertStatus(lastResult ? lastResult.status : result.status, test);
// Existing behavior: fail if flaky (implied by user request "existing status field should remain failed")
// If outcome is flaky, status should be 'failed' to indicate initial failure, but final_status is 'passed'
let testStatus = finalStatus;
if (outcome === "flaky") {
testStatus = "flaky";
}
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, uniqueTestId, browserDetails, test);
processed.push(processedStep);
if (step.steps && step.steps.length > 0) {
processedStep.steps = await processAllSteps(step.steps);
}
}
return processed;
};
let codeSnippet = "";
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)) {
codeSnippet = this.extractCodeSnippet(test.location.file, test.location.line, test.location.column);
}
// 1. Get Spec File Name
const specFileName = ((_e = test.location) === null || _e === void 0 ? void 0 : _e.file)
? path.basename(test.location.file)
: "n/a";
// 2. Get Describe Block Name
// Check if the immediate parent is a 'describe' block
let describeBlockName = "n/a";
if (((_f = test.parent) === null || _f === void 0 ? void 0 : _f.type) === "describe") {
describeBlockName = test.parent.title;
}
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: uniqueTestId,
runId: "TBD",
describe: describeBlockName,
spec_file: specFileName,
name: test.titlePath().join(" > "),
suiteName: (project === null || project === void 0 ? void 0 : project.name) || ((_g = this.config.projects[0]) === null || _g === void 0 ? void 0 : _g.name) || "Default Suite",
status: testStatus,
outcome: outcome === "flaky" ? outcome : undefined, // Only Include if flaky
final_status: finalStatus, // New Field
duration: result.duration,
startTime: startTime,
endTime: endTime,
browser: browserDetails,
retries: result.retry,
steps: result.steps ? await processAllSteps(result.steps) : [],
errorMessage: (_h = result.error) === null || _h === void 0 ? void 0 : _h.message,
stackTrace: (_j = result.error) === null || _j === void 0 ? void 0 : _j.stack,
snippet: (_k = result.error) === null || _k === void 0 ? void 0 : _k.snippet,
codeSnippet: codeSnippet,
tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
severity: this._getSeverity(test.annotations),
screenshots: [],
videoPath: [],
tracePath: undefined,
attachments: [],
stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
annotations: ((_l = test.annotations) === null || _l === void 0 ? void 0 : _l.length) > 0 ? test.annotations : undefined,
...testSpecificData,
};
for (const [index, attachment] of result.attachments.entries()) {
if (!attachment.path)
continue;
try {
const testSubfolder = uniqueTestId.replace(/[^a-zA-Z0-9_-]/g, "_");
const safeAttachmentName = path
.basename(attachment.path)
.replace(/[^a-zA-Z0-9_.-]/g, "_");
const uniqueFileName = `${index}-${Date.now()}-${safeAttachmentName}`;
const relativeDestPath = path.join(ATTACHMENTS_SUBDIR, testSubfolder, uniqueFileName);
const absoluteDestPath = path.join(this.outputDir, relativeDestPath);
await this._ensureDirExists(path.dirname(absoluteDestPath));
// Copy file first
await fs.copyFile(attachment.path, absoluteDestPath);
// Compress in-place (preserves path/name)
await (0, compression_utils_1.compressAttachment)(absoluteDestPath, attachment.contentType);
if (attachment.contentType.startsWith("image/")) {
(_m = pulseResult.screenshots) === null || _m === void 0 ? void 0 : _m.push(relativeDestPath);
}
else if (attachment.contentType.startsWith("video/")) {
(_o = pulseResult.videoPath) === null || _o === void 0 ? void 0 : _o.push(relativeDestPath);
}
else if (attachment.name === "trace") {
pulseResult.tracePath = relativeDestPath;
}
else {
(_p = pulseResult.attachments) === null || _p === void 0 ? void 0 : _p.push({
name: attachment.name,
path: relativeDestPath,
contentType: attachment.contentType,
});
}
}
catch (err) {
console.error(`Pulse Reporter: Failed to process attachment "${attachment.name}" for test ${pulseResult.name}. Error: ${err.message}`);
}
}
this.results.push(pulseResult);
}
_getFinalizedResults(allResults) {
const resultsMap = new Map();
for (const result of allResults) {
if (!resultsMap.has(result.id)) {
resultsMap.set(result.id, []);
}
resultsMap.get(result.id).push(result);
}
const finalResults = [];
for (const [testId, attempts] of resultsMap.entries()) {
// Sort by retry count (ASC) then timestamp (DESC) to ensure stable resolution
attempts.sort((a, b) => {
if (a.retries !== b.retries)
return a.retries - b.retries;
return (new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
});
const firstAttempt = attempts[0];
const retryAttempts = attempts.slice(1);
// Only populate retryHistory if there were actual failures that triggered retries
// If all attempts passed, we don't need to show retry history
const hasActualRetries = retryAttempts.length > 0 &&
retryAttempts.some((attempt) => attempt.status === "failed" ||
attempt.status === "flaky" ||
firstAttempt.status === "failed" ||
firstAttempt.status === "flaky");
if (hasActualRetries) {
firstAttempt.retryHistory = retryAttempts;
// Calculate final status and outcome from the last attempt if retries exist
const lastAttempt = attempts[attempts.length - 1];
firstAttempt.final_status = lastAttempt.status;
// If the last attempt was flaky, ensure outcome is set on the main result
if (lastAttempt.outcome === "flaky" || lastAttempt.status === "flaky") {
firstAttempt.outcome = "flaky";
firstAttempt.status = "flaky";
}
}
else {
// If no actual retries (all attempts passed), ensure final_status and retryHistory are removed
delete firstAttempt.final_status;
delete firstAttempt.retryHistory;
}
finalResults.push(firstAttempt);
}
return finalResults;
}
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);
}
}
}
const finalResultsList = this._getFinalizedResults(allShardProcessedResults);
finalResultsList.forEach((r) => (r.runId = finalRunData.id));
finalRunData.passed = finalResultsList.filter((r) => (r.final_status || r.status) === "passed").length;
finalRunData.failed = finalResultsList.filter((r) => (r.final_status || r.status) === "failed").length;
finalRunData.skipped = finalResultsList.filter((r) => (r.final_status || r.status) === "skipped").length;
finalRunData.flaky = finalResultsList.filter((r) => (r.final_status || r.status) === "flaky").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(),
reportDescription: this.options.reportDescription,
logo: this.options.logo,
},
};
}
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);
}
}
}
/**
* Removes all individual run JSON files from the `pulse-results/` directory
* that were left over from previous test sessions.
*
* When `resetOnEachRun: false`, each run writes its own timestamped JSON to
* `pulse-results/` and then `_mergeAllRunReports()` merges them all. However,
* if files from *older* sessions accumulate there (e.g. because a previous run
* was interrupted before the post-merge cleanup, or because the user ran tests
* on a previous day), `_getFinalizedResults()` de-duplicates by `test.id` and
* collapses results from both sessions into a single entry — producing a
* `totalTests` count lower than the actual number of tests that ran.
*
* Cleaning up at `onBegin` time guarantees each run starts with a fresh slate.
*/
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) {
// Wait for ALL in-flight onTestEnd calls to finish before reading this.results.
// This guards against Playwright calling onEnd() concurrently with (or just
// before) the last onTestEnd() finishing its async attachment I/O — which
// would cause that test to be silently dropped from the report.
await Promise.allSettled(this._pendingTestEnds);
if (this.shardIndex !== undefined) {
await this._writeShardResults();
return;
}
// De-duplicate and handle retries here, in a safe, single-threaded context.
const finalResults = this._getFinalizedResults(this.results);
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),
// Use the length of the de-duplicated array for all counts
totalTests: finalResults.length,
passed: finalResults.filter((r) => (r.final_status || r.status) === "passed").length,
failed: finalResults.filter((r) => (r.final_status || r.status) === "failed").length,
skipped: finalResults.filter((r) => (r.final_status || r.status) === "skipped").length,
flaky: finalResults.filter((r) => (r.final_status || r.status) === "flaky").length,
duration,
environment: environmentDetails,
};
finalResults.forEach((r) => (r.runId = runId));
let finalReport = undefined;
if (this.isSharded) {
// The _mergeShardResults method will handle its own de-duplication
finalReport = await this._mergeShardResults(runData);
}
else {
finalReport = {
run: runData,
// Use the de-duplicated results
results: finalResults,
metadata: {
generatedAt: new Date().toISOString(),
reportDescription: this.options.reportDescription,
logo: this.options.logo,
},
};
}
if (!finalReport) {
console.error("PlaywrightPulseReporter: CRITICAL - finalReport object was not generated. Cannot create summary.");
return;
}
const jsonReplacer = (key, value) => {
if (value instanceof Date)
return value.toISOString();
if (typeof value === "bigint")
return value.toString();
return value;
};
if (this.resetOnEachRun) {
const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
try {
await this._ensureDirExists(this.outputDir);
await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, jsonReplacer, 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}`);
}
}
else {
// Logic for appending/merging reports
const pulseResultsDir = path.join(this.outputDir, this.individualReportsSubDir);
const shardPrefix = this.baseOutputFile.replace(".json", "-");
const individualReportPath = path.join(pulseResultsDir, `${shardPrefix}${Date.now()}.json`);
try {
await this._ensureDirExists(pulseResultsDir);
await fs.writeFile(individualReportPath, JSON.stringify(finalReport, jsonReplacer, 2));
if (this.printsToStdio()) {
console.log(`PlaywrightPulseReporter: Individual run report for merging written to ${individualReportPath}`);
}
// DEFERRED MERGING:
// We do not call _mergeAllRunReports() here anymore when resetOnEachRun is false.
// The individual JSON files in pulse-results/ will be collected and merged
// into the main JSON when the user next runs one of the report generator commands.
}
catch (error) {
console.error(`Pulse Reporter: Failed to write report. Error: ${error.message}`);
}
}
if (this.isSharded) {
await this._cleanupTemporaryFiles();
}
}
}
exports.PlaywrightPulseReporter = PlaywrightPulseReporter;
exports.default = PlaywrightPulseReporter;