@arghajit/playwright-pulse-report 0.3.3 → 0.3.5
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 +95 -85
- package/dist/lib/report-types.d.ts +2 -0
- package/dist/reporter/playwright-pulse-reporter.d.ts +18 -1
- package/dist/reporter/playwright-pulse-reporter.js +157 -129
- package/dist/reporter/tsconfig.reporter.tsbuildinfo +1 -0
- package/dist/types/index.d.ts +53 -9
- package/dist/utils/compression-utils.d.ts +19 -0
- package/dist/utils/compression-utils.js +112 -0
- package/package.json +18 -9
- package/scripts/config-reader.mjs +114 -124
- package/scripts/generate-email-report.mjs +96 -21
- package/scripts/generate-report.mjs +1172 -531
- package/scripts/generate-static-report.mjs +1269 -540
- package/scripts/generate-trend.mjs +8 -4
- package/scripts/{merge-pulse-report.js → merge-pulse-report.mjs} +64 -35
- package/scripts/merge-sequential-reports.mjs +172 -0
- package/scripts/sendReport.mjs +156 -202
- package/scripts/terminal-logo.mjs +51 -0
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -26
- package/dist/playwright-pulse-reporter.d.ts +0 -26
- package/dist/playwright-pulse-reporter.js +0 -304
- package/dist/reporter/lib/report-types.d.ts +0 -8
- package/dist/reporter/lib/report-types.js +0 -2
- package/dist/reporter/reporter/playwright-pulse-reporter.d.ts +0 -1
- package/dist/reporter/reporter/playwright-pulse-reporter.js +0 -398
- package/dist/reporter/types/index.d.ts +0 -52
- package/dist/reporter/types/index.js +0 -2
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as fs from "fs/promises";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { getReporterConfig } from "./config-reader.mjs";
|
|
5
|
+
import { mergeSequentialReportsIfNeeded } from "./merge-sequential-reports.mjs";
|
|
5
6
|
|
|
6
7
|
// Use dynamic import for chalk as it's ESM only for prettier console logs
|
|
7
8
|
let chalk;
|
|
@@ -18,7 +19,6 @@ try {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
21
|
-
const CURRENT_RUN_JSON_FILE = "playwright-pulse-report.json"; // Source of the current run data
|
|
22
22
|
const HISTORY_SUBDIR = "history"; // Subdirectory for historical JSON files
|
|
23
23
|
const HISTORY_FILE_PREFIX = "trend-";
|
|
24
24
|
const MAX_HISTORY_FILES = 15; // Store last 15 runs
|
|
@@ -33,8 +33,12 @@ for (let i = 0; i < args.length; i++) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
async function archiveCurrentRunData() {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
36
|
+
const config = await getReporterConfig(customOutputDir);
|
|
37
|
+
const outputDir = config.outputDir;
|
|
38
|
+
const outputFile = config.outputFile;
|
|
39
|
+
|
|
40
|
+
await mergeSequentialReportsIfNeeded(outputDir);
|
|
41
|
+
const currentRunJsonPath = path.join(outputDir, outputFile);
|
|
38
42
|
const historyDir = path.join(outputDir, HISTORY_SUBDIR);
|
|
39
43
|
|
|
40
44
|
try {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { getReporterConfig } from "./config-reader.mjs";
|
|
6
|
+
import { animate } from "./terminal-logo.mjs";
|
|
7
|
+
import { mergeSequentialReportsIfNeeded } from "./merge-sequential-reports.mjs";
|
|
5
8
|
|
|
6
9
|
const args = process.argv.slice(2);
|
|
7
10
|
let customOutputDir = null;
|
|
@@ -13,72 +16,75 @@ for (let i = 0; i < args.length; i++) {
|
|
|
13
16
|
}
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
const OUTPUT_FILE = "playwright-pulse-report.json";
|
|
17
|
-
|
|
18
19
|
/**
|
|
19
|
-
* Securely resolves the report directory.
|
|
20
|
-
* Prevents Path Traversal by ensuring the output directory
|
|
21
|
-
* is contained within the current working directory.
|
|
20
|
+
* Securely resolves the report directory and config.
|
|
22
21
|
*/
|
|
23
|
-
async function
|
|
22
|
+
async function getFullConfig() {
|
|
23
|
+
const config = await getReporterConfig(customOutputDir);
|
|
24
|
+
|
|
24
25
|
if (customOutputDir) {
|
|
25
26
|
const resolvedPath = path.resolve(process.cwd(), customOutputDir);
|
|
26
|
-
|
|
27
27
|
if (!resolvedPath.startsWith(process.cwd())) {
|
|
28
28
|
console.error(
|
|
29
29
|
"⛔ Security Error: Custom output directory must be within the current project root.",
|
|
30
30
|
);
|
|
31
31
|
process.exit(1);
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
return resolvedPath;
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
const { getOutputDir } = await import("./config-reader.mjs");
|
|
39
|
-
return await getOutputDir();
|
|
40
|
-
} catch (error) {
|
|
41
|
-
return path.resolve(process.cwd(), "pulse-report");
|
|
42
|
-
}
|
|
35
|
+
return config;
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
/**
|
|
46
39
|
* Scans the report directory for subdirectories (shards).
|
|
47
40
|
* Returns an array of absolute paths to these subdirectories.
|
|
48
|
-
* Excludes the 'attachments' folder
|
|
41
|
+
* Excludes the 'attachments' folder and non-shard directories.
|
|
49
42
|
*/
|
|
50
|
-
function getShardDirectories(dir) {
|
|
43
|
+
function getShardDirectories(dir, outputFile, individualReportsSubDir) {
|
|
51
44
|
if (!fs.existsSync(dir)) {
|
|
52
45
|
return [];
|
|
53
46
|
}
|
|
54
47
|
|
|
55
48
|
return fs
|
|
56
49
|
.readdirSync(dir, { withFileTypes: true })
|
|
57
|
-
.filter((dirent) =>
|
|
50
|
+
.filter((dirent) => {
|
|
51
|
+
if (!dirent.isDirectory() || dirent.name === "attachments" || dirent.name === individualReportsSubDir) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const shardPath = path.join(dir, dirent.name);
|
|
56
|
+
const hasDirectReport = fs.existsSync(path.join(shardPath, outputFile));
|
|
57
|
+
const hasSequentialResults = fs.existsSync(path.join(shardPath, individualReportsSubDir));
|
|
58
|
+
|
|
59
|
+
// Scenario 3: Only consider directories that have either a report or sequential results
|
|
60
|
+
return hasDirectReport || hasSequentialResults;
|
|
61
|
+
})
|
|
58
62
|
.map((dirent) => path.join(dir, dirent.name));
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
/**
|
|
62
66
|
* Merges JSON reports from all shard directories.
|
|
63
67
|
*/
|
|
64
|
-
function mergeReports(shardDirs) {
|
|
68
|
+
function mergeReports(shardDirs, outputFile) {
|
|
65
69
|
let combinedRun = {
|
|
66
70
|
totalTests: 0,
|
|
67
71
|
passed: 0,
|
|
68
72
|
failed: 0,
|
|
69
73
|
skipped: 0,
|
|
70
74
|
duration: 0,
|
|
75
|
+
flaky: 0
|
|
71
76
|
};
|
|
72
77
|
|
|
73
78
|
let combinedResults = [];
|
|
74
79
|
let latestTimestamp = "";
|
|
75
80
|
let latestGeneratedAt = "";
|
|
81
|
+
let allEnvironments = [];
|
|
76
82
|
|
|
77
83
|
for (const shardDir of shardDirs) {
|
|
78
|
-
const jsonPath = path.join(shardDir,
|
|
84
|
+
const jsonPath = path.join(shardDir, outputFile);
|
|
79
85
|
|
|
80
86
|
if (!fs.existsSync(jsonPath)) {
|
|
81
|
-
console.warn(` Warning: No ${
|
|
87
|
+
console.warn(` Warning: No ${outputFile} found in ${path.basename(shardDir)} after pre-merge attempt.`);
|
|
82
88
|
continue;
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -91,10 +97,11 @@ function mergeReports(shardDirs) {
|
|
|
91
97
|
combinedRun.passed += run.passed || 0;
|
|
92
98
|
combinedRun.failed += run.failed || 0;
|
|
93
99
|
combinedRun.skipped += run.skipped || 0;
|
|
100
|
+
combinedRun.flaky += run.flaky || 0;
|
|
94
101
|
combinedRun.duration += run.duration || 0;
|
|
95
102
|
|
|
96
103
|
if (run.environment) {
|
|
97
|
-
|
|
104
|
+
allEnvironments.push(run.environment);
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
if (json.results) {
|
|
@@ -111,6 +118,10 @@ function mergeReports(shardDirs) {
|
|
|
111
118
|
}
|
|
112
119
|
}
|
|
113
120
|
|
|
121
|
+
if (allEnvironments.length > 0) {
|
|
122
|
+
combinedRun.environment = allEnvironments;
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
const finalJson = {
|
|
115
126
|
run: {
|
|
116
127
|
id: `merged-${Date.now()}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`,
|
|
@@ -175,10 +186,16 @@ function cleanupShardDirectories(shardDirs) {
|
|
|
175
186
|
|
|
176
187
|
// Main execution
|
|
177
188
|
(async () => {
|
|
178
|
-
|
|
189
|
+
await animate();
|
|
190
|
+
|
|
191
|
+
const config = await getFullConfig();
|
|
192
|
+
const REPORT_DIR = config.outputDir;
|
|
193
|
+
const OUTPUT_FILE = config.outputFile;
|
|
194
|
+
const INDIVIDUAL_SUBDIR = config.individualReportsSubDir;
|
|
179
195
|
|
|
180
196
|
console.log(`\n🔄 Playwright Pulse - Merge Reports (Sharding Mode)\n`);
|
|
181
197
|
console.log(` Report directory: ${REPORT_DIR}`);
|
|
198
|
+
console.log(` Output file: ${OUTPUT_FILE}`);
|
|
182
199
|
if (customOutputDir) {
|
|
183
200
|
console.log(` (from CLI argument)`);
|
|
184
201
|
} else {
|
|
@@ -186,13 +203,13 @@ function cleanupShardDirectories(shardDirs) {
|
|
|
186
203
|
}
|
|
187
204
|
console.log();
|
|
188
205
|
|
|
189
|
-
// 1. Get Shard Directories
|
|
190
|
-
const shardDirs = getShardDirectories(REPORT_DIR);
|
|
206
|
+
// 1. Get initial Shard Directories (Scenario 3: filtering non-relevant folders)
|
|
207
|
+
const shardDirs = getShardDirectories(REPORT_DIR, OUTPUT_FILE, INDIVIDUAL_SUBDIR);
|
|
191
208
|
|
|
192
209
|
if (shardDirs.length === 0) {
|
|
193
210
|
console.log("❌ No shard directories found.");
|
|
194
211
|
console.log(
|
|
195
|
-
|
|
212
|
+
` Expected structure: <report-dir>/<shard-folder>/${OUTPUT_FILE} or <report-dir>/<shard-folder>/${INDIVIDUAL_SUBDIR}/`,
|
|
196
213
|
);
|
|
197
214
|
process.exit(0);
|
|
198
215
|
}
|
|
@@ -203,26 +220,38 @@ function cleanupShardDirectories(shardDirs) {
|
|
|
203
220
|
});
|
|
204
221
|
console.log();
|
|
205
222
|
|
|
206
|
-
// 2.
|
|
207
|
-
console.log(
|
|
208
|
-
const
|
|
223
|
+
// 2. Scenario 1: Pre-merge sequential results for EACH shard if needed
|
|
224
|
+
console.log(`⚙️ Checking for sequential results in shards...`);
|
|
225
|
+
for (const shardDir of shardDirs) {
|
|
226
|
+
const hasSequential = fs.existsSync(path.join(shardDir, INDIVIDUAL_SUBDIR));
|
|
227
|
+
if (hasSequential) {
|
|
228
|
+
console.log(` - ${path.basename(shardDir)}: Merging sequential results...`);
|
|
229
|
+
// Force merge because individual shard dirs might not have playwright.config.ts resolving to resetOnEachRun=false
|
|
230
|
+
await mergeSequentialReportsIfNeeded(shardDir, true);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
console.log();
|
|
234
|
+
|
|
235
|
+
// 3. Merge JSON Reports
|
|
236
|
+
console.log(`🔀 Merging reports across shards...`);
|
|
237
|
+
const merged = mergeReports(shardDirs, OUTPUT_FILE);
|
|
209
238
|
console.log(` ✓ Merged ${shardDirs.length} report(s)`);
|
|
210
239
|
console.log();
|
|
211
240
|
|
|
212
|
-
//
|
|
241
|
+
// 4. Copy Attachments
|
|
213
242
|
console.log(`📎 Merging attachments...`);
|
|
214
243
|
mergeAttachments(shardDirs, REPORT_DIR);
|
|
215
244
|
console.log(` ✓ Attachments merged`);
|
|
216
245
|
|
|
217
|
-
//
|
|
246
|
+
// 5. Write Final Merged JSON
|
|
218
247
|
const finalReportPath = path.join(REPORT_DIR, OUTPUT_FILE);
|
|
219
248
|
fs.writeFileSync(finalReportPath, JSON.stringify(merged, null, 2));
|
|
220
249
|
|
|
221
250
|
console.log(`\n✅ Merged report saved as ${OUTPUT_FILE}`);
|
|
222
251
|
console.log(` Total tests: ${merged.run.totalTests}`);
|
|
223
|
-
console.log(` Passed: ${merged.run.passed} | Failed: ${merged.run.failed} | Skipped: ${merged.run.skipped}`);
|
|
252
|
+
console.log(` Passed: ${merged.run.passed} | Failed: ${merged.run.failed} | Skipped: ${merged.run.skipped} | Flaky: ${merged.run.flaky}`);
|
|
224
253
|
|
|
225
|
-
//
|
|
254
|
+
// 6. Cleanup Shard Directories
|
|
226
255
|
cleanupShardDirectories(shardDirs);
|
|
227
256
|
|
|
228
257
|
console.log();
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
import { getReporterConfig } from "./config-reader.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reads all `<outputFile>-*.json` files in the `pulse-results` directory
|
|
8
|
+
* and merges them into a single `<outputFile>.json`.
|
|
9
|
+
* It resolves duplicate tests using exactly the same logic as the reporter.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} customOutputDir The base report directory override (from CLI).
|
|
12
|
+
* @param {boolean} forceMerge Try to merge regardless of config.resetOnEachRun (used by sharded merge).
|
|
13
|
+
*/
|
|
14
|
+
export async function mergeSequentialReportsIfNeeded(customOutputDir, forceMerge = false) {
|
|
15
|
+
const config = await getReporterConfig(customOutputDir);
|
|
16
|
+
|
|
17
|
+
// This logic should ONLY run if resetOnEachRun is disabled, UNLESS we are forcing it
|
|
18
|
+
// (e.g. recovering orphaned shards in merge-pulse-report.mjs).
|
|
19
|
+
if (config.resetOnEachRun && !forceMerge) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const individualReportsSubDir = config.individualReportsSubDir;
|
|
24
|
+
const baseOutputFile = config.outputFile;
|
|
25
|
+
|
|
26
|
+
// If customOutputDir is provided, it might be an absolute path to a shard. Use it directly if it is absolute.
|
|
27
|
+
// Otherwise, fall back to the config's outputDir (which is resolved relative to CWD).
|
|
28
|
+
const outputDir = customOutputDir && path.isAbsolute(customOutputDir)
|
|
29
|
+
? customOutputDir
|
|
30
|
+
: config.outputDir;
|
|
31
|
+
|
|
32
|
+
const pulseResultsDir = path.join(outputDir, individualReportsSubDir);
|
|
33
|
+
const finalOutputPath = path.join(outputDir, baseOutputFile);
|
|
34
|
+
|
|
35
|
+
// Use the actual outputFile name as seed for shard files (e.g. "results.json" -> "results-")
|
|
36
|
+
const shardPrefix = baseOutputFile.replace(".json", "-");
|
|
37
|
+
|
|
38
|
+
let reportFiles;
|
|
39
|
+
try {
|
|
40
|
+
const allFiles = await fs.readdir(pulseResultsDir);
|
|
41
|
+
reportFiles = allFiles.filter(
|
|
42
|
+
(file) =>
|
|
43
|
+
file.startsWith(shardPrefix) && file.endsWith(".json"),
|
|
44
|
+
);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code === "ENOENT") {
|
|
47
|
+
// No individual reports directory found, which is completely fine/normal
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.error(
|
|
51
|
+
`Pulse Reporter: Error reading directory ${pulseResultsDir}:`,
|
|
52
|
+
error,
|
|
53
|
+
);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (reportFiles.length === 0) {
|
|
58
|
+
// No matching JSON report files found to merge
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(
|
|
63
|
+
`\n🔄 Merging ${reportFiles.length} sequential test run(s) from '${individualReportsSubDir}'...`,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const allResultsFromAllFiles = [];
|
|
67
|
+
let latestTimestamp = new Date(0);
|
|
68
|
+
let lastRunEnvironment = undefined;
|
|
69
|
+
let totalDuration = 0;
|
|
70
|
+
|
|
71
|
+
for (const file of reportFiles) {
|
|
72
|
+
const filePath = path.join(pulseResultsDir, file);
|
|
73
|
+
try {
|
|
74
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
75
|
+
const json = JSON.parse(content);
|
|
76
|
+
|
|
77
|
+
let currentRunId = `run-${Date.now()}`;
|
|
78
|
+
if (json.run) {
|
|
79
|
+
if (json.run.id) currentRunId = json.run.id;
|
|
80
|
+
|
|
81
|
+
const runTimestamp = new Date(json.run.timestamp);
|
|
82
|
+
if (runTimestamp > latestTimestamp) {
|
|
83
|
+
latestTimestamp = runTimestamp;
|
|
84
|
+
lastRunEnvironment = json.run.environment || undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (json.results) {
|
|
89
|
+
// Tag each result with its runId to ensure we can sum them up if they have same IDs but different runs
|
|
90
|
+
const resultsWithRunId = json.results.map((r) => ({
|
|
91
|
+
...r,
|
|
92
|
+
runId: currentRunId,
|
|
93
|
+
}));
|
|
94
|
+
allResultsFromAllFiles.push(...resultsWithRunId);
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.warn(
|
|
98
|
+
`Pulse Reporter: Could not parse report file ${filePath}. Skipping. Error: ${err.message}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The results from individual run JSONs are already finalized and deduplicated by their own run's reporter.
|
|
104
|
+
// We simply concatenate them. The runId tag ensures tests across runs remain distinguishable.
|
|
105
|
+
const finalMergedResults = allResultsFromAllFiles;
|
|
106
|
+
|
|
107
|
+
totalDuration = finalMergedResults.reduce(
|
|
108
|
+
(acc, r) => acc + (r.duration || 0),
|
|
109
|
+
0,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const combinedRun = {
|
|
113
|
+
id: `run-${Date.now()}`,
|
|
114
|
+
timestamp: latestTimestamp.toISOString(),
|
|
115
|
+
environment: lastRunEnvironment,
|
|
116
|
+
totalTests: finalMergedResults.length,
|
|
117
|
+
passed: finalMergedResults.filter(
|
|
118
|
+
(r) => (r.final_status || r.status) === "passed",
|
|
119
|
+
).length,
|
|
120
|
+
failed: finalMergedResults.filter(
|
|
121
|
+
(r) => (r.final_status || r.status) === "failed",
|
|
122
|
+
).length,
|
|
123
|
+
skipped: finalMergedResults.filter(
|
|
124
|
+
(r) => (r.final_status || r.status) === "skipped",
|
|
125
|
+
).length,
|
|
126
|
+
flaky: finalMergedResults.filter(
|
|
127
|
+
(r) => (r.final_status || r.status) === "flaky",
|
|
128
|
+
).length,
|
|
129
|
+
duration: totalDuration,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const finalReport = {
|
|
133
|
+
run: combinedRun,
|
|
134
|
+
results: finalMergedResults,
|
|
135
|
+
metadata: {
|
|
136
|
+
generatedAt: new Date().toISOString(),
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await fs.writeFile(
|
|
142
|
+
finalOutputPath,
|
|
143
|
+
JSON.stringify(
|
|
144
|
+
finalReport,
|
|
145
|
+
(key, value) => {
|
|
146
|
+
if (value instanceof Date) return value.toISOString();
|
|
147
|
+
return value;
|
|
148
|
+
},
|
|
149
|
+
2,
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
console.log(
|
|
153
|
+
`✅ Merged report with ${finalMergedResults.length} total results saved to ${finalOutputPath}`,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Clean up the pulse-results directory after a successful merge
|
|
157
|
+
try {
|
|
158
|
+
await fs.rm(pulseResultsDir, { recursive: true, force: true });
|
|
159
|
+
console.log(
|
|
160
|
+
`🧹 Cleaned up temporary reports directory at ${pulseResultsDir}`,
|
|
161
|
+
);
|
|
162
|
+
} catch (cleanupErr) {
|
|
163
|
+
console.warn(
|
|
164
|
+
`Pulse Reporter: Could not clean up individual reports directory. Error: ${cleanupErr.message}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error(
|
|
169
|
+
`Pulse Reporter: Failed to write final merged report to ${finalOutputPath}. Error: ${err.message}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|