@arghajit/dummy 0.3.38 → 0.3.40
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/dist/reporter/playwright-pulse-reporter.d.ts +1 -2
- package/dist/reporter/playwright-pulse-reporter.js +10 -127
- package/dist/types/index.d.ts +43 -0
- package/package.json +4 -4
- package/scripts/config-reader.mjs +98 -126
- package/scripts/generate-email-report.mjs +8 -4
- package/scripts/generate-report.mjs +8 -4
- package/scripts/generate-static-report.mjs +14 -15
- package/scripts/generate-trend.mjs +8 -4
- package/scripts/{merge-pulse-report.js → merge-pulse-report.mjs} +22 -25
- package/scripts/merge-sequential-reports.mjs +205 -0
- package/scripts/sendReport.mjs +14 -7
|
@@ -10,6 +10,7 @@ export declare class PlaywrightPulseReporter implements Reporter {
|
|
|
10
10
|
private outputDir;
|
|
11
11
|
private attachmentsDir;
|
|
12
12
|
private baseOutputFile;
|
|
13
|
+
private individualReportsSubDir;
|
|
13
14
|
private isSharded;
|
|
14
15
|
private shardIndex;
|
|
15
16
|
private resetOnEachRun;
|
|
@@ -43,9 +44,7 @@ export declare class PlaywrightPulseReporter implements Reporter {
|
|
|
43
44
|
*
|
|
44
45
|
* Cleaning up at `onBegin` time guarantees each run starts with a fresh slate.
|
|
45
46
|
*/
|
|
46
|
-
private _cleanupStaleRunReports;
|
|
47
47
|
private _ensureDirExists;
|
|
48
48
|
onEnd(result: FullResult): Promise<void>;
|
|
49
|
-
private _mergeAllRunReports;
|
|
50
49
|
}
|
|
51
50
|
export default PlaywrightPulseReporter;
|
|
@@ -64,20 +64,21 @@ const convertStatus = (status, testCase) => {
|
|
|
64
64
|
};
|
|
65
65
|
const TEMP_SHARD_FILE_PREFIX = ".pulse-shard-results-";
|
|
66
66
|
const ATTACHMENTS_SUBDIR = "attachments";
|
|
67
|
-
const INDIVIDUAL_REPORTS_SUBDIR = "pulse-results";
|
|
68
67
|
class PlaywrightPulseReporter {
|
|
69
68
|
constructor(options = {}) {
|
|
70
|
-
var _a, _b, _c;
|
|
69
|
+
var _a, _b, _c, _d;
|
|
71
70
|
this.results = [];
|
|
72
71
|
this._pendingTestEnds = [];
|
|
73
72
|
this.baseOutputFile = "playwright-pulse-report.json";
|
|
73
|
+
this.individualReportsSubDir = "pulse-results";
|
|
74
74
|
this.isSharded = false;
|
|
75
75
|
this.shardIndex = undefined;
|
|
76
76
|
this.options = options;
|
|
77
77
|
this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
|
|
78
78
|
this.outputDir = (_b = options.outputDir) !== null && _b !== void 0 ? _b : "pulse-report";
|
|
79
|
+
this.individualReportsSubDir = (_c = options.individualReportsSubDir) !== null && _c !== void 0 ? _c : "pulse-results";
|
|
79
80
|
this.attachmentsDir = path.join(this.outputDir, ATTACHMENTS_SUBDIR);
|
|
80
|
-
this.resetOnEachRun = (
|
|
81
|
+
this.resetOnEachRun = (_d = options.resetOnEachRun) !== null && _d !== void 0 ? _d : true;
|
|
81
82
|
}
|
|
82
83
|
printsToStdio() {
|
|
83
84
|
return this.shardIndex === undefined || this.shardIndex === 0;
|
|
@@ -108,13 +109,6 @@ class PlaywrightPulseReporter {
|
|
|
108
109
|
await this._cleanupTemporaryFiles();
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
|
-
// When not resetting on each run, clear stale individual run files
|
|
112
|
-
// from previous sessions. Without this, _mergeAllRunReports() reads
|
|
113
|
-
// ALL accumulated files and de-duplicates by test.id, which collapses
|
|
114
|
-
// results from different sessions into fewer entries than expected.
|
|
115
|
-
if (!this.resetOnEachRun) {
|
|
116
|
-
await this._cleanupStaleRunReports();
|
|
117
|
-
}
|
|
118
112
|
})
|
|
119
113
|
.catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
|
|
120
114
|
}
|
|
@@ -523,25 +517,6 @@ class PlaywrightPulseReporter {
|
|
|
523
517
|
*
|
|
524
518
|
* Cleaning up at `onBegin` time guarantees each run starts with a fresh slate.
|
|
525
519
|
*/
|
|
526
|
-
async _cleanupStaleRunReports() {
|
|
527
|
-
const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
|
|
528
|
-
try {
|
|
529
|
-
const files = await fs.readdir(pulseResultsDir);
|
|
530
|
-
const staleFiles = files.filter((f) => f.startsWith("playwright-pulse-report-") && f.endsWith(".json"));
|
|
531
|
-
if (staleFiles.length > 0) {
|
|
532
|
-
await Promise.all(staleFiles.map((f) => fs.unlink(path.join(pulseResultsDir, f))));
|
|
533
|
-
if (this.printsToStdio()) {
|
|
534
|
-
console.log(`PlaywrightPulseReporter: Cleaned up ${staleFiles.length} stale run report(s) from ${pulseResultsDir}`);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
catch (error) {
|
|
539
|
-
// ENOENT simply means no previous runs exist — that's fine
|
|
540
|
-
if ((error === null || error === void 0 ? void 0 : error.code) !== "ENOENT") {
|
|
541
|
-
console.warn("Pulse Reporter: Warning during cleanup of stale run reports:", error.message);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
520
|
async _ensureDirExists(dirPath) {
|
|
546
521
|
try {
|
|
547
522
|
await fs.mkdir(dirPath, { recursive: true });
|
|
@@ -625,7 +600,7 @@ class PlaywrightPulseReporter {
|
|
|
625
600
|
}
|
|
626
601
|
else {
|
|
627
602
|
// Logic for appending/merging reports
|
|
628
|
-
const pulseResultsDir = path.join(this.outputDir,
|
|
603
|
+
const pulseResultsDir = path.join(this.outputDir, this.individualReportsSubDir);
|
|
629
604
|
const individualReportPath = path.join(pulseResultsDir, `playwright-pulse-report-${Date.now()}.json`);
|
|
630
605
|
try {
|
|
631
606
|
await this._ensureDirExists(pulseResultsDir);
|
|
@@ -633,111 +608,19 @@ class PlaywrightPulseReporter {
|
|
|
633
608
|
if (this.printsToStdio()) {
|
|
634
609
|
console.log(`PlaywrightPulseReporter: Individual run report for merging written to ${individualReportPath}`);
|
|
635
610
|
}
|
|
636
|
-
|
|
611
|
+
// DEFERRED MERGING:
|
|
612
|
+
// We do not call _mergeAllRunReports() here anymore when resetOnEachRun is false.
|
|
613
|
+
// The individual JSON files in pulse-results/ will be collected and merged
|
|
614
|
+
// into the main JSON when the user next runs one of the report generator commands.
|
|
637
615
|
}
|
|
638
616
|
catch (error) {
|
|
639
|
-
console.error(`Pulse Reporter: Failed to write
|
|
617
|
+
console.error(`Pulse Reporter: Failed to write report. Error: ${error.message}`);
|
|
640
618
|
}
|
|
641
619
|
}
|
|
642
620
|
if (this.isSharded) {
|
|
643
621
|
await this._cleanupTemporaryFiles();
|
|
644
622
|
}
|
|
645
623
|
}
|
|
646
|
-
async _mergeAllRunReports() {
|
|
647
|
-
const pulseResultsDir = path.join(this.outputDir, INDIVIDUAL_REPORTS_SUBDIR);
|
|
648
|
-
const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
|
|
649
|
-
let reportFiles;
|
|
650
|
-
try {
|
|
651
|
-
const allFiles = await fs.readdir(pulseResultsDir);
|
|
652
|
-
reportFiles = allFiles.filter((file) => file.startsWith("playwright-pulse-report-") && file.endsWith(".json"));
|
|
653
|
-
}
|
|
654
|
-
catch (error) {
|
|
655
|
-
if (error.code === "ENOENT") {
|
|
656
|
-
if (this.printsToStdio()) {
|
|
657
|
-
console.log(`Pulse Reporter: No individual reports directory found at ${pulseResultsDir}. Skipping merge.`);
|
|
658
|
-
}
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
console.error(`Pulse Reporter: Error reading report directory ${pulseResultsDir}:`, error);
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
if (reportFiles.length === 0) {
|
|
665
|
-
if (this.printsToStdio()) {
|
|
666
|
-
console.log("Pulse Reporter: No matching JSON report files found to merge.");
|
|
667
|
-
}
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
const allResultsFromAllFiles = [];
|
|
671
|
-
let latestTimestamp = new Date(0);
|
|
672
|
-
let lastRunEnvironment = undefined;
|
|
673
|
-
let totalDuration = 0;
|
|
674
|
-
for (const file of reportFiles) {
|
|
675
|
-
const filePath = path.join(pulseResultsDir, file);
|
|
676
|
-
try {
|
|
677
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
678
|
-
const json = JSON.parse(content);
|
|
679
|
-
if (json.run) {
|
|
680
|
-
const runTimestamp = new Date(json.run.timestamp);
|
|
681
|
-
if (runTimestamp > latestTimestamp) {
|
|
682
|
-
latestTimestamp = runTimestamp;
|
|
683
|
-
lastRunEnvironment = json.run.environment || undefined;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
if (json.results) {
|
|
687
|
-
allResultsFromAllFiles.push(...json.results);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
catch (err) {
|
|
691
|
-
console.warn(`Pulse Reporter: Could not parse report file ${filePath}. Skipping. Error: ${err.message}`);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
// De-duplicate the results from ALL merged files using the helper function
|
|
695
|
-
const finalMergedResults = this._getFinalizedResults(allResultsFromAllFiles);
|
|
696
|
-
// Sum the duration from the final, de-duplicated list of tests
|
|
697
|
-
totalDuration = finalMergedResults.reduce((acc, r) => acc + (r.duration || 0), 0);
|
|
698
|
-
const combinedRun = {
|
|
699
|
-
id: `merged-${Date.now()}`,
|
|
700
|
-
timestamp: latestTimestamp,
|
|
701
|
-
environment: lastRunEnvironment,
|
|
702
|
-
// Recalculate counts based on the truly final, de-duplicated list
|
|
703
|
-
totalTests: finalMergedResults.length,
|
|
704
|
-
passed: finalMergedResults.filter((r) => (r.final_status || r.status) === "passed").length,
|
|
705
|
-
failed: finalMergedResults.filter((r) => (r.final_status || r.status) === "failed").length,
|
|
706
|
-
skipped: finalMergedResults.filter((r) => (r.final_status || r.status) === "skipped").length,
|
|
707
|
-
flaky: finalMergedResults.filter((r) => (r.final_status || r.status) === "flaky").length,
|
|
708
|
-
duration: totalDuration,
|
|
709
|
-
};
|
|
710
|
-
const finalReport = {
|
|
711
|
-
run: combinedRun,
|
|
712
|
-
results: finalMergedResults, // Use the de-duplicated list
|
|
713
|
-
metadata: {
|
|
714
|
-
generatedAt: new Date().toISOString(),
|
|
715
|
-
},
|
|
716
|
-
};
|
|
717
|
-
try {
|
|
718
|
-
await fs.writeFile(finalOutputPath, JSON.stringify(finalReport, (key, value) => {
|
|
719
|
-
if (value instanceof Date)
|
|
720
|
-
return value.toISOString();
|
|
721
|
-
return value;
|
|
722
|
-
}, 2));
|
|
723
|
-
if (this.printsToStdio()) {
|
|
724
|
-
console.log(`PlaywrightPulseReporter: ✅ Merged report with ${finalMergedResults.length} total results saved to ${finalOutputPath}`);
|
|
725
|
-
}
|
|
726
|
-
// Clean up the pulse-results directory after a successful merge
|
|
727
|
-
try {
|
|
728
|
-
await fs.rm(pulseResultsDir, { recursive: true, force: true });
|
|
729
|
-
if (this.printsToStdio()) {
|
|
730
|
-
console.log(`PlaywrightPulseReporter: Cleaned up individual reports directory at ${pulseResultsDir}`);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
catch (cleanupErr) {
|
|
734
|
-
console.warn(`Pulse Reporter: Could not clean up individual reports directory. Error: ${cleanupErr.message}`);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
catch (err) {
|
|
738
|
-
console.error(`Pulse Reporter: Failed to write final merged report to ${finalOutputPath}. Error: ${err.message}`);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
624
|
}
|
|
742
625
|
exports.PlaywrightPulseReporter = PlaywrightPulseReporter;
|
|
743
626
|
exports.default = PlaywrightPulseReporter;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -81,12 +81,55 @@ export interface TrendDataPoint {
|
|
|
81
81
|
flaky?: number;
|
|
82
82
|
}
|
|
83
83
|
export interface PlaywrightPulseReporterOptions {
|
|
84
|
+
/**
|
|
85
|
+
* The name of the output JSON file. Kindly do not change.
|
|
86
|
+
* @default "playwright-pulse-report.json"
|
|
87
|
+
*/
|
|
84
88
|
outputFile?: string;
|
|
89
|
+
/**
|
|
90
|
+
* The directory where the report files will be generated.
|
|
91
|
+
*
|
|
92
|
+
* Mostly useful while using sharding
|
|
93
|
+
*
|
|
94
|
+
* @default "pulse-report"
|
|
95
|
+
*/
|
|
85
96
|
outputDir?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Whether to embed images directly as base64 strings in the report.
|
|
99
|
+
* @default false
|
|
100
|
+
*/
|
|
86
101
|
base64Images?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Whether to reset the output directory before each run.
|
|
104
|
+
*
|
|
105
|
+
* Mostly useful while running multiple test suites in a single run with `&&` operator.
|
|
106
|
+
*
|
|
107
|
+
* example: `npx playwright test test1.spec.ts && npx playwright test test2.spec.ts`
|
|
108
|
+
*
|
|
109
|
+
* If `resetOnEachRun` is set to `false`, then the report of `test2.spec.ts` will be merged with `test1.spec.ts` report.
|
|
110
|
+
*
|
|
111
|
+
* @default true
|
|
112
|
+
*/
|
|
87
113
|
resetOnEachRun?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* A custom description to embed or display in the report.
|
|
116
|
+
*
|
|
117
|
+
* If not added, the component will not appear in the html reports
|
|
118
|
+
*/
|
|
88
119
|
reportDescription?: string;
|
|
120
|
+
/**
|
|
121
|
+
* Path to a custom logo image file to use in the report, which will be displayed in the header of the html report's logo and favicon.
|
|
122
|
+
*
|
|
123
|
+
* If not added, the default logo will be used.
|
|
124
|
+
*/
|
|
89
125
|
logo?: string;
|
|
126
|
+
/**
|
|
127
|
+
* The subdirectory within `outputDir` where individual run reports are stored.
|
|
128
|
+
* Only used when `resetOnEachRun` is `false`.
|
|
129
|
+
*
|
|
130
|
+
* @default "pulse-results"
|
|
131
|
+
*/
|
|
132
|
+
individualReportsSubDir?: string;
|
|
90
133
|
}
|
|
91
134
|
export interface EnvDetails {
|
|
92
135
|
host: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arghajit/dummy",
|
|
3
3
|
"author": "Arghajit Singha",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.40",
|
|
5
5
|
"description": "A Playwright reporter and dashboard for visualizing test results.",
|
|
6
6
|
"homepage": "https://arghajit47.github.io/playwright-pulse/",
|
|
7
7
|
"repository": {
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
"logo": "node scripts/terminal-logo.mjs",
|
|
39
39
|
"generate-pulse-report": "scripts/generate-static-report.mjs",
|
|
40
40
|
"generate-report": "scripts/generate-report.mjs",
|
|
41
|
-
"merge-pulse-report": "scripts/merge-pulse-report.
|
|
41
|
+
"merge-pulse-report": "scripts/merge-pulse-report.mjs",
|
|
42
42
|
"send-email": "scripts/sendReport.mjs",
|
|
43
|
-
"generate-trend": "scripts/generate-trend.mjs",
|
|
44
43
|
"generate-email-report": "scripts/generate-email-report.mjs",
|
|
44
|
+
"generate-trend": "scripts/generate-trend.mjs",
|
|
45
45
|
"pulse-logo": "scripts/terminal-logo.mjs"
|
|
46
46
|
},
|
|
47
47
|
"exports": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"prepublishOnly": "npm run build:reporter",
|
|
57
57
|
"report:static": "node ./scripts/generate-static-report.mjs",
|
|
58
58
|
"report:generate": "node ./scripts/generate-report.mjs",
|
|
59
|
-
"report:merge": "node ./scripts/merge-pulse-report.
|
|
59
|
+
"report:merge": "node ./scripts/merge-pulse-report.mjs",
|
|
60
60
|
"report:email": "node ./scripts/sendReport.mjs",
|
|
61
61
|
"report:minify": "node ./scripts/generate-email-report.mjs",
|
|
62
62
|
"generate-trend": "node ./scripts/generate-trend.mjs"
|
|
@@ -22,159 +22,131 @@ async function findPlaywrightConfig() {
|
|
|
22
22
|
return { path: null, exists: false };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
async function
|
|
25
|
+
async function extractReporterOptionsFromConfig(configPath) {
|
|
26
26
|
let fileContent = "";
|
|
27
27
|
try {
|
|
28
28
|
fileContent = fs.readFileSync(configPath, "utf-8");
|
|
29
29
|
} catch (e) {
|
|
30
|
-
|
|
31
|
-
return null;
|
|
30
|
+
return {};
|
|
32
31
|
}
|
|
33
32
|
|
|
33
|
+
const options = {};
|
|
34
|
+
|
|
34
35
|
// 1. Strategy: Text Parsing (Safe & Fast)
|
|
35
|
-
// We try to read the file as text first. This finds the outputDir without
|
|
36
|
-
// triggering any Node.js warnings or errors.
|
|
37
36
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
const outputDirMatch = fileContent.match(/outputDir:\s*["']([^"']+)["']/);
|
|
38
|
+
if (outputDirMatch && outputDirMatch[1]) options.outputDir = outputDirMatch[1];
|
|
39
|
+
|
|
40
|
+
const outputFileMatch = fileContent.match(/outputFile:\s*["']([^"']+)["']/);
|
|
41
|
+
if (outputFileMatch && outputFileMatch[1]) options.outputFile = outputFileMatch[1];
|
|
42
|
+
|
|
43
|
+
const resetOnEachRunMatch = fileContent.match(/resetOnEachRun:\s*(true|false)/);
|
|
44
|
+
if (resetOnEachRunMatch) options.resetOnEachRun = resetOnEachRunMatch[1] === "true";
|
|
45
|
+
|
|
46
|
+
const individualReportsSubDirMatch = fileContent.match(/individualReportsSubDir:\s*["']([^"']+)["']/);
|
|
47
|
+
if (individualReportsSubDirMatch && individualReportsSubDirMatch[1]) options.individualReportsSubDir = individualReportsSubDirMatch[1];
|
|
48
|
+
} catch (e) { }
|
|
49
|
+
|
|
50
|
+
// 2. Safety Check and Dynamic Import
|
|
51
|
+
if (Object.keys(options).length < 3) {
|
|
52
|
+
// Check if we can safely import()
|
|
53
|
+
let canImport = true;
|
|
54
|
+
if (configPath.endsWith(".js")) {
|
|
55
|
+
let isModulePackage = false;
|
|
56
|
+
try {
|
|
57
|
+
const pkgPath = path.resolve(process.cwd(), "package.json");
|
|
58
|
+
if (fs.existsSync(pkgPath)) {
|
|
59
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
60
|
+
isModulePackage = pkg.type === "module";
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {}
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// We explicitly check for this and ABORT the import if found.
|
|
52
|
-
if (configPath.endsWith(".js")) {
|
|
53
|
-
let isModulePackage = false;
|
|
54
|
-
try {
|
|
55
|
-
const pkgPath = path.resolve(process.cwd(), "package.json");
|
|
56
|
-
if (fs.existsSync(pkgPath)) {
|
|
57
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
58
|
-
isModulePackage = pkg.type === "module";
|
|
59
|
-
}
|
|
60
|
-
} catch (e) {}
|
|
61
|
-
|
|
62
|
-
if (!isModulePackage) {
|
|
63
|
-
// Heuristic: Check for ESM syntax (import/export at start of lines)
|
|
64
|
-
const hasEsmSyntax =
|
|
65
|
-
/^\s*import\s+/m.test(fileContent) ||
|
|
66
|
-
/^\s*export\s+/m.test(fileContent);
|
|
67
|
-
|
|
68
|
-
if (hasEsmSyntax) {
|
|
69
|
-
// We found ESM syntax in a .js file within a CJS project.
|
|
70
|
-
// Attempting to import this WILL trigger the Node.js warning.
|
|
71
|
-
// Since regex failed to find outputDir, and we can't import safely, we abort now.
|
|
72
|
-
return null;
|
|
64
|
+
if (!isModulePackage) {
|
|
65
|
+
const hasEsmSyntax = /^\s*import\s+/m.test(fileContent) || /^\s*export\s+/m.test(fileContent);
|
|
66
|
+
if (hasEsmSyntax) canImport = false;
|
|
73
67
|
}
|
|
74
68
|
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// 3. Strategy: Dynamic Import
|
|
78
|
-
// If we passed the safety check, we try to import the config.
|
|
79
|
-
try {
|
|
80
|
-
let config;
|
|
81
|
-
const configDir = dirname(configPath);
|
|
82
|
-
const originalDirname = global.__dirname;
|
|
83
|
-
const originalFilename = global.__filename;
|
|
84
69
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
if (canImport) {
|
|
71
|
+
try {
|
|
72
|
+
let config;
|
|
73
|
+
const configDir = dirname(configPath);
|
|
74
|
+
const originalDirname = global.__dirname;
|
|
75
|
+
const originalFilename = global.__filename;
|
|
88
76
|
|
|
89
|
-
if (configPath.endsWith(".ts")) {
|
|
90
77
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// Handle Default Export
|
|
109
|
-
if (config && config.default) {
|
|
110
|
-
config = config.default;
|
|
111
|
-
}
|
|
78
|
+
global.__dirname = configDir;
|
|
79
|
+
global.__filename = configPath;
|
|
80
|
+
|
|
81
|
+
if (configPath.endsWith(".ts")) {
|
|
82
|
+
try {
|
|
83
|
+
const { register } = await import("node:module");
|
|
84
|
+
const { pathToFileURL } = await import("node:url");
|
|
85
|
+
register("ts-node/esm", pathToFileURL("./"));
|
|
86
|
+
config = await import(pathToFileURL(configPath).href);
|
|
87
|
+
} catch (tsError) {
|
|
88
|
+
const tsNode = await import("ts-node");
|
|
89
|
+
tsNode.register({ transpileOnly: true, compilerOptions: { module: "commonjs" } });
|
|
90
|
+
config = require(configPath);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
config = await import(pathToFileURL(configPath).href);
|
|
94
|
+
}
|
|
112
95
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
reporterName.includes("@arghajit/dummy"))
|
|
133
|
-
) {
|
|
134
|
-
if (reporterOptions && reporterOptions.outputDir) {
|
|
135
|
-
return path.resolve(process.cwd(), reporterOptions.outputDir);
|
|
96
|
+
if (config && config.default) config = config.default;
|
|
97
|
+
|
|
98
|
+
if (config) {
|
|
99
|
+
if (config.reporter) {
|
|
100
|
+
const reporters = Array.isArray(config.reporter) ? config.reporter : [config.reporter];
|
|
101
|
+
for (const reporter of reporters) {
|
|
102
|
+
const reporterName = Array.isArray(reporter) ? reporter[0] : reporter;
|
|
103
|
+
const reporterOptions = Array.isArray(reporter) ? reporter[1] : null;
|
|
104
|
+
|
|
105
|
+
if (typeof reporterName === "string" &&
|
|
106
|
+
(reporterName.includes("playwright-pulse-report") ||
|
|
107
|
+
reporterName.includes("@arghajit/playwright-pulse-report"))) {
|
|
108
|
+
if (reporterOptions) {
|
|
109
|
+
if (reporterOptions.outputDir) options.outputDir = reporterOptions.outputDir;
|
|
110
|
+
if (reporterOptions.outputFile) options.outputFile = reporterOptions.outputFile;
|
|
111
|
+
if (reporterOptions.resetOnEachRun !== undefined) options.resetOnEachRun = reporterOptions.resetOnEachRun;
|
|
112
|
+
if (reporterOptions.individualReportsSubDir) options.individualReportsSubDir = reporterOptions.individualReportsSubDir;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
136
115
|
}
|
|
137
116
|
}
|
|
117
|
+
if (config.outputDir && !options.outputDir) options.outputDir = config.outputDir;
|
|
138
118
|
}
|
|
119
|
+
} finally {
|
|
120
|
+
global.__dirname = originalDirname;
|
|
121
|
+
global.__filename = originalFilename;
|
|
139
122
|
}
|
|
140
|
-
|
|
141
|
-
// Check for Global outputDir
|
|
142
|
-
if (config.outputDir) {
|
|
143
|
-
return path.resolve(process.cwd(), config.outputDir);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
} finally {
|
|
147
|
-
// Clean up globals
|
|
148
|
-
global.__dirname = originalDirname;
|
|
149
|
-
global.__filename = originalFilename;
|
|
123
|
+
} catch (error) {}
|
|
150
124
|
}
|
|
151
|
-
} catch (error) {
|
|
152
|
-
// SILENT CATCH: Do NOT log anything here.
|
|
153
|
-
return null;
|
|
154
125
|
}
|
|
155
126
|
|
|
156
|
-
return
|
|
127
|
+
return options;
|
|
157
128
|
}
|
|
158
129
|
|
|
159
|
-
export async function
|
|
160
|
-
if (customOutputDirFromArgs) {
|
|
161
|
-
console.log(`Using custom outputDir from CLI: ${customOutputDirFromArgs}`);
|
|
162
|
-
return path.resolve(process.cwd(), customOutputDirFromArgs);
|
|
163
|
-
}
|
|
164
|
-
|
|
130
|
+
export async function getReporterConfig(customOutputDirFromArgs = null) {
|
|
165
131
|
const { path: configPath, exists } = await findPlaywrightConfig();
|
|
166
|
-
|
|
167
|
-
`Config file search result: ${exists ? configPath : "not found"}`
|
|
168
|
-
);
|
|
132
|
+
let options = {};
|
|
169
133
|
|
|
170
134
|
if (exists) {
|
|
171
|
-
|
|
172
|
-
if (outputDirFromConfig) {
|
|
173
|
-
console.log(`Using outputDir from config: ${outputDirFromConfig}`);
|
|
174
|
-
return outputDirFromConfig;
|
|
175
|
-
}
|
|
135
|
+
options = await extractReporterOptionsFromConfig(configPath);
|
|
176
136
|
}
|
|
177
137
|
|
|
178
|
-
|
|
179
|
-
|
|
138
|
+
const outputDir = customOutputDirFromArgs
|
|
139
|
+
? path.resolve(process.cwd(), customOutputDirFromArgs)
|
|
140
|
+
: path.resolve(process.cwd(), options.outputDir || DEFAULT_OUTPUT_DIR);
|
|
141
|
+
|
|
142
|
+
const outputFile = options.outputFile || "playwright-pulse-report.json";
|
|
143
|
+
const resetOnEachRun = options.resetOnEachRun !== undefined ? options.resetOnEachRun : true;
|
|
144
|
+
const individualReportsSubDir = options.individualReportsSubDir || "pulse-results";
|
|
145
|
+
|
|
146
|
+
return { outputDir, outputFile, resetOnEachRun, individualReportsSubDir };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function getOutputDir(customOutputDirFromArgs = null) {
|
|
150
|
+
const config = await getReporterConfig(customOutputDirFromArgs);
|
|
151
|
+
return config.outputDir;
|
|
180
152
|
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import {
|
|
5
|
+
import { getReporterConfig } from "./config-reader.mjs";
|
|
6
6
|
import { animate } from "./terminal-logo.mjs";
|
|
7
|
+
import { mergeSequentialReportsIfNeeded } from "./merge-sequential-reports.mjs";
|
|
7
8
|
|
|
8
9
|
// Use dynamic import for chalk as it's ESM only
|
|
9
10
|
let chalk;
|
|
@@ -24,7 +25,6 @@ try {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
27
|
-
const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
|
|
28
28
|
const MINIFIED_HTML_FILE = "pulse-email-summary.html"; // New minified report
|
|
29
29
|
|
|
30
30
|
const args = process.argv.slice(2);
|
|
@@ -757,8 +757,12 @@ async function main() {
|
|
|
757
757
|
await animate();
|
|
758
758
|
}
|
|
759
759
|
|
|
760
|
-
const
|
|
761
|
-
const
|
|
760
|
+
const config = await getReporterConfig(customOutputDir);
|
|
761
|
+
const outputDir = config.outputDir;
|
|
762
|
+
const outputFile = config.outputFile;
|
|
763
|
+
|
|
764
|
+
await mergeSequentialReportsIfNeeded(outputDir);
|
|
765
|
+
const reportJsonPath = path.resolve(outputDir, outputFile);
|
|
762
766
|
const minifiedReportHtmlPath = path.resolve(outputDir, MINIFIED_HTML_FILE); // Path for the new minified HTML
|
|
763
767
|
|
|
764
768
|
console.log(chalk.blue(`Generating email report...`));
|
|
@@ -5,8 +5,9 @@ import { readFileSync, existsSync as fsExistsSync } from "fs";
|
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { fork } from "child_process";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
|
-
import {
|
|
8
|
+
import { getReporterConfig } from "./config-reader.mjs";
|
|
9
9
|
import { animate } from "./terminal-logo.mjs";
|
|
10
|
+
import { mergeSequentialReportsIfNeeded } from "./merge-sequential-reports.mjs";
|
|
10
11
|
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
13
|
const __dirname = path.dirname(__filename);
|
|
@@ -52,7 +53,6 @@ try {
|
|
|
52
53
|
}
|
|
53
54
|
// Default configuration
|
|
54
55
|
const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
55
|
-
const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
|
|
56
56
|
const DEFAULT_HTML_FILE = "playwright-pulse-report.html";
|
|
57
57
|
// Helper functions
|
|
58
58
|
export function ansiToHtml(text) {
|
|
@@ -5297,8 +5297,12 @@ async function main() {
|
|
|
5297
5297
|
"generate-trend.mjs", // Keeping the filename as per your request
|
|
5298
5298
|
);
|
|
5299
5299
|
|
|
5300
|
-
const
|
|
5301
|
-
const
|
|
5300
|
+
const config = await getReporterConfig(customOutputDir);
|
|
5301
|
+
const outputDir = config.outputDir;
|
|
5302
|
+
const outputFile = config.outputFile;
|
|
5303
|
+
|
|
5304
|
+
await mergeSequentialReportsIfNeeded(outputDir);
|
|
5305
|
+
const reportJsonPath = path.resolve(outputDir, outputFile); // Current run's main JSON
|
|
5302
5306
|
const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
|
|
5303
5307
|
|
|
5304
5308
|
const historyDir = path.join(outputDir, "history"); // Directory for historical JSON files
|
|
@@ -5,8 +5,9 @@ import { readFileSync, existsSync as fsExistsSync } from "fs";
|
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { fork } from "child_process";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
|
-
import {
|
|
8
|
+
import { getReporterConfig } from "./config-reader.mjs";
|
|
9
9
|
import { animate } from "./terminal-logo.mjs";
|
|
10
|
+
import { mergeSequentialReportsIfNeeded } from "./merge-sequential-reports.mjs";
|
|
10
11
|
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
13
|
const __dirname = path.dirname(__filename);
|
|
@@ -55,18 +56,8 @@ try {
|
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
/**
|
|
59
|
-
* @constant {string} DEFAULT_OUTPUT_DIR
|
|
60
|
-
* The default directory where the report will be generated.
|
|
61
|
-
*/
|
|
62
59
|
const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
63
60
|
|
|
64
|
-
/**
|
|
65
|
-
* @constant {string} DEFAULT_JSON_FILE
|
|
66
|
-
* The default name for the JSON file containing the test data.
|
|
67
|
-
*/
|
|
68
|
-
const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
|
|
69
|
-
|
|
70
61
|
/**
|
|
71
62
|
* @constant {string} DEFAULT_HTML_FILE
|
|
72
63
|
* The default name for the generated HTML report file.
|
|
@@ -6462,11 +6453,13 @@ function generateHTML(reportData, trendData = null) {
|
|
|
6462
6453
|
function switchRetryTab(event, tabId) {
|
|
6463
6454
|
// Find container
|
|
6464
6455
|
const container = event.target.closest('.retry-tabs-container');
|
|
6456
|
+
if (!container) return;
|
|
6465
6457
|
|
|
6466
6458
|
// Update tab buttons
|
|
6467
6459
|
const buttons = container.querySelectorAll('.retry-tab');
|
|
6468
6460
|
buttons.forEach(btn => btn.classList.remove('active'));
|
|
6469
|
-
event.target.
|
|
6461
|
+
const activeBtn = event.target.closest('.retry-tab') || event.target;
|
|
6462
|
+
activeBtn.classList.add('active');
|
|
6470
6463
|
|
|
6471
6464
|
// Update content
|
|
6472
6465
|
const contents = container.querySelectorAll('.retry-tab-content');
|
|
@@ -6475,10 +6468,12 @@ function generateHTML(reportData, trendData = null) {
|
|
|
6475
6468
|
content.classList.remove('active');
|
|
6476
6469
|
});
|
|
6477
6470
|
|
|
6478
|
-
const activeContent =
|
|
6471
|
+
const activeContent = document.getElementById(tabId);
|
|
6479
6472
|
if (activeContent) {
|
|
6480
6473
|
activeContent.style.display = 'block';
|
|
6481
6474
|
activeContent.classList.add('active');
|
|
6475
|
+
} else {
|
|
6476
|
+
console.error('Failed to find retry tab content for id:', tabId);
|
|
6482
6477
|
}
|
|
6483
6478
|
}
|
|
6484
6479
|
|
|
@@ -7158,8 +7153,12 @@ async function main() {
|
|
|
7158
7153
|
"generate-trend.mjs", // Keeping the filename as per your request
|
|
7159
7154
|
);
|
|
7160
7155
|
|
|
7161
|
-
const
|
|
7162
|
-
const
|
|
7156
|
+
const config = await getReporterConfig(customOutputDir);
|
|
7157
|
+
const outputDir = config.outputDir;
|
|
7158
|
+
const outputFile = config.outputFile;
|
|
7159
|
+
|
|
7160
|
+
await mergeSequentialReportsIfNeeded(outputDir);
|
|
7161
|
+
const reportJsonPath = path.resolve(outputDir, outputFile); // Current run's main JSON
|
|
7163
7162
|
const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
|
|
7164
7163
|
|
|
7165
7164
|
const historyDir = path.join(outputDir, "history"); // Directory for historical JSON files
|
|
@@ -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,33 +16,23 @@ 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
|
/**
|
|
@@ -54,14 +47,14 @@ function getShardDirectories(dir) {
|
|
|
54
47
|
|
|
55
48
|
return fs
|
|
56
49
|
.readdirSync(dir, { withFileTypes: true })
|
|
57
|
-
.filter((dirent) => dirent.isDirectory() && dirent.name !== "attachments")
|
|
50
|
+
.filter((dirent) => dirent.isDirectory() && dirent.name !== "attachments" && dirent.name !== "pulse-results")
|
|
58
51
|
.map((dirent) => path.join(dir, dirent.name));
|
|
59
52
|
}
|
|
60
53
|
|
|
61
54
|
/**
|
|
62
55
|
* Merges JSON reports from all shard directories.
|
|
63
56
|
*/
|
|
64
|
-
function mergeReports(shardDirs) {
|
|
57
|
+
function mergeReports(shardDirs, outputFile) {
|
|
65
58
|
let combinedRun = {
|
|
66
59
|
totalTests: 0,
|
|
67
60
|
passed: 0,
|
|
@@ -76,10 +69,10 @@ function mergeReports(shardDirs) {
|
|
|
76
69
|
let allEnvironments = [];
|
|
77
70
|
|
|
78
71
|
for (const shardDir of shardDirs) {
|
|
79
|
-
const jsonPath = path.join(shardDir,
|
|
72
|
+
const jsonPath = path.join(shardDir, outputFile);
|
|
80
73
|
|
|
81
74
|
if (!fs.existsSync(jsonPath)) {
|
|
82
|
-
console.warn(` Warning: No ${
|
|
75
|
+
console.warn(` Warning: No ${outputFile} found in ${path.basename(shardDir)}`);
|
|
83
76
|
continue;
|
|
84
77
|
}
|
|
85
78
|
|
|
@@ -181,13 +174,17 @@ function cleanupShardDirectories(shardDirs) {
|
|
|
181
174
|
|
|
182
175
|
// Main execution
|
|
183
176
|
(async () => {
|
|
184
|
-
const { animate } = await import("./terminal-logo.mjs");
|
|
185
177
|
await animate();
|
|
186
178
|
|
|
187
|
-
const
|
|
179
|
+
const config = await getFullConfig();
|
|
180
|
+
const REPORT_DIR = config.outputDir;
|
|
181
|
+
const OUTPUT_FILE = config.outputFile;
|
|
182
|
+
|
|
183
|
+
await mergeSequentialReportsIfNeeded(REPORT_DIR);
|
|
188
184
|
|
|
189
185
|
console.log(`\n🔄 Playwright Pulse - Merge Reports (Sharding Mode)\n`);
|
|
190
186
|
console.log(` Report directory: ${REPORT_DIR}`);
|
|
187
|
+
console.log(` Output file: ${OUTPUT_FILE}`);
|
|
191
188
|
if (customOutputDir) {
|
|
192
189
|
console.log(` (from CLI argument)`);
|
|
193
190
|
} else {
|
|
@@ -201,7 +198,7 @@ function cleanupShardDirectories(shardDirs) {
|
|
|
201
198
|
if (shardDirs.length === 0) {
|
|
202
199
|
console.log("❌ No shard directories found.");
|
|
203
200
|
console.log(
|
|
204
|
-
|
|
201
|
+
` Expected structure: <report-dir>/<shard-folder>/${OUTPUT_FILE}`,
|
|
205
202
|
);
|
|
206
203
|
process.exit(0);
|
|
207
204
|
}
|
|
@@ -214,7 +211,7 @@ function cleanupShardDirectories(shardDirs) {
|
|
|
214
211
|
|
|
215
212
|
// 2. Merge JSON Reports
|
|
216
213
|
console.log(`🔀 Merging reports...`);
|
|
217
|
-
const merged = mergeReports(shardDirs);
|
|
214
|
+
const merged = mergeReports(shardDirs, OUTPUT_FILE);
|
|
218
215
|
console.log(` ✓ Merged ${shardDirs.length} report(s)`);
|
|
219
216
|
console.log();
|
|
220
217
|
|
|
@@ -0,0 +1,205 @@
|
|
|
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 `playwright-pulse-report-*.json` files in the `pulse-results` directory
|
|
8
|
+
* and merges them into a single `playwright-pulse-report.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
|
+
*/
|
|
13
|
+
export async function mergeSequentialReportsIfNeeded(customOutputDir) {
|
|
14
|
+
const config = await getReporterConfig(customOutputDir);
|
|
15
|
+
|
|
16
|
+
// This logic should ONLY run if resetOnEachRun is disabled.
|
|
17
|
+
if (config.resetOnEachRun) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const individualReportsSubDir = config.individualReportsSubDir;
|
|
22
|
+
const baseOutputFile = config.outputFile;
|
|
23
|
+
const outputDir = config.outputDir;
|
|
24
|
+
|
|
25
|
+
const pulseResultsDir = path.join(outputDir, individualReportsSubDir);
|
|
26
|
+
const finalOutputPath = path.join(outputDir, baseOutputFile);
|
|
27
|
+
|
|
28
|
+
let reportFiles;
|
|
29
|
+
try {
|
|
30
|
+
const allFiles = await fs.readdir(pulseResultsDir);
|
|
31
|
+
reportFiles = allFiles.filter(
|
|
32
|
+
(file) =>
|
|
33
|
+
file.startsWith("playwright-pulse-report-") && file.endsWith(".json"),
|
|
34
|
+
);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error.code === "ENOENT") {
|
|
37
|
+
// No individual reports directory found, which is completely fine/normal
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
console.error(
|
|
41
|
+
`Pulse Reporter: Error reading directory ${pulseResultsDir}:`,
|
|
42
|
+
error,
|
|
43
|
+
);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (reportFiles.length === 0) {
|
|
48
|
+
// No matching JSON report files found to merge
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(
|
|
53
|
+
`\n🔄 Merging ${reportFiles.length} sequential test run(s) from '${individualReportsSubDir}'...`,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const allResultsFromAllFiles = [];
|
|
57
|
+
let latestTimestamp = new Date(0);
|
|
58
|
+
let lastRunEnvironment = undefined;
|
|
59
|
+
let totalDuration = 0;
|
|
60
|
+
|
|
61
|
+
for (const file of reportFiles) {
|
|
62
|
+
const filePath = path.join(pulseResultsDir, file);
|
|
63
|
+
try {
|
|
64
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
65
|
+
const json = JSON.parse(content);
|
|
66
|
+
|
|
67
|
+
if (json.run) {
|
|
68
|
+
const runTimestamp = new Date(json.run.timestamp);
|
|
69
|
+
if (runTimestamp > latestTimestamp) {
|
|
70
|
+
latestTimestamp = runTimestamp;
|
|
71
|
+
lastRunEnvironment = json.run.environment || undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (json.results) {
|
|
75
|
+
allResultsFromAllFiles.push(...json.results);
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.warn(
|
|
79
|
+
`Pulse Reporter: Could not parse report file ${filePath}. Skipping. Error: ${err.message}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// De-duplicate the results from ALL merged files using the same logic as the reporter
|
|
85
|
+
const finalMergedResults = getFinalizedResults(allResultsFromAllFiles);
|
|
86
|
+
|
|
87
|
+
totalDuration = finalMergedResults.reduce(
|
|
88
|
+
(acc, r) => acc + (r.duration || 0),
|
|
89
|
+
0,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const combinedRun = {
|
|
93
|
+
id: `run-${Date.now()}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`,
|
|
94
|
+
timestamp: latestTimestamp.toISOString(),
|
|
95
|
+
environment: lastRunEnvironment,
|
|
96
|
+
totalTests: finalMergedResults.length,
|
|
97
|
+
passed: finalMergedResults.filter(
|
|
98
|
+
(r) => (r.final_status || r.status) === "passed",
|
|
99
|
+
).length,
|
|
100
|
+
failed: finalMergedResults.filter(
|
|
101
|
+
(r) => (r.final_status || r.status) === "failed",
|
|
102
|
+
).length,
|
|
103
|
+
skipped: finalMergedResults.filter(
|
|
104
|
+
(r) => (r.final_status || r.status) === "skipped",
|
|
105
|
+
).length,
|
|
106
|
+
flaky: finalMergedResults.filter(
|
|
107
|
+
(r) => (r.final_status || r.status) === "flaky",
|
|
108
|
+
).length,
|
|
109
|
+
duration: totalDuration,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const finalReport = {
|
|
113
|
+
run: combinedRun,
|
|
114
|
+
results: finalMergedResults,
|
|
115
|
+
metadata: {
|
|
116
|
+
generatedAt: new Date().toISOString(),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await fs.writeFile(
|
|
122
|
+
finalOutputPath,
|
|
123
|
+
JSON.stringify(
|
|
124
|
+
finalReport,
|
|
125
|
+
(key, value) => {
|
|
126
|
+
if (value instanceof Date) return value.toISOString();
|
|
127
|
+
return value;
|
|
128
|
+
},
|
|
129
|
+
2,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
console.log(
|
|
133
|
+
`✅ Merged report with ${finalMergedResults.length} total results saved to ${finalOutputPath}`,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Clean up the pulse-results directory after a successful merge
|
|
137
|
+
try {
|
|
138
|
+
await fs.rm(pulseResultsDir, { recursive: true, force: true });
|
|
139
|
+
console.log(
|
|
140
|
+
`🧹 Cleaned up temporary reports directory at ${pulseResultsDir}`,
|
|
141
|
+
);
|
|
142
|
+
} catch (cleanupErr) {
|
|
143
|
+
console.warn(
|
|
144
|
+
`Pulse Reporter: Could not clean up individual reports directory. Error: ${cleanupErr.message}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error(
|
|
149
|
+
`Pulse Reporter: Failed to write final merged report to ${finalOutputPath}. Error: ${err.message}`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getFinalizedResults(allResults) {
|
|
155
|
+
const resultsMap = new Map();
|
|
156
|
+
|
|
157
|
+
for (const result of allResults) {
|
|
158
|
+
if (!resultsMap.has(result.id)) {
|
|
159
|
+
resultsMap.set(result.id, []);
|
|
160
|
+
}
|
|
161
|
+
resultsMap.get(result.id).push(result);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const finalResults = [];
|
|
165
|
+
|
|
166
|
+
for (const [testId, attempts] of resultsMap.entries()) {
|
|
167
|
+
// Sort by retry count (ASC) then timestamp (DESC) to ensure stable resolution
|
|
168
|
+
attempts.sort((a, b) => {
|
|
169
|
+
if (a.retries !== b.retries) return a.retries - b.retries;
|
|
170
|
+
return new Date(b.startTime).getTime() - new Date(a.startTime).getTime();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const firstAttempt = attempts[0];
|
|
174
|
+
const retryAttempts = attempts.slice(1);
|
|
175
|
+
|
|
176
|
+
const hasActualRetries =
|
|
177
|
+
retryAttempts.length > 0 &&
|
|
178
|
+
retryAttempts.some(
|
|
179
|
+
(attempt) =>
|
|
180
|
+
attempt.status === "failed" ||
|
|
181
|
+
attempt.status === "flaky" ||
|
|
182
|
+
firstAttempt.status === "failed" ||
|
|
183
|
+
firstAttempt.status === "flaky"
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
if (hasActualRetries) {
|
|
187
|
+
firstAttempt.retryHistory = retryAttempts;
|
|
188
|
+
|
|
189
|
+
const lastAttempt = attempts[attempts.length - 1];
|
|
190
|
+
firstAttempt.final_status = lastAttempt.status;
|
|
191
|
+
|
|
192
|
+
if (lastAttempt.outcome === "flaky" || lastAttempt.status === "flaky") {
|
|
193
|
+
firstAttempt.outcome = "flaky";
|
|
194
|
+
firstAttempt.status = "flaky";
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
delete firstAttempt.final_status;
|
|
198
|
+
delete firstAttempt.retryHistory;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
finalResults.push(firstAttempt);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return finalResults;
|
|
205
|
+
}
|
package/scripts/sendReport.mjs
CHANGED
|
@@ -11,7 +11,8 @@ import { fileURLToPath } from "url";
|
|
|
11
11
|
import { animate } from "./terminal-logo.mjs";
|
|
12
12
|
import { fork } from "child_process";
|
|
13
13
|
import "dotenv/config";
|
|
14
|
-
import {
|
|
14
|
+
import { getReporterConfig } from "./config-reader.mjs";
|
|
15
|
+
import { mergeSequentialReportsIfNeeded } from "./merge-sequential-reports.mjs";
|
|
15
16
|
|
|
16
17
|
let chalk;
|
|
17
18
|
let logo =
|
|
@@ -42,8 +43,10 @@ for (let i = 0; i < args.length; i++) {
|
|
|
42
43
|
let fetch;
|
|
43
44
|
let projectName;
|
|
44
45
|
|
|
45
|
-
function getUUID(reportDir) {
|
|
46
|
-
const
|
|
46
|
+
async function getUUID(reportDir) {
|
|
47
|
+
const config = await getReporterConfig(customOutputDir);
|
|
48
|
+
const outputFile = config.outputFile;
|
|
49
|
+
const reportPath = path.join(reportDir, outputFile);
|
|
47
50
|
console.log("Report path:", reportPath);
|
|
48
51
|
|
|
49
52
|
if (!fsExistsSync(reportPath)) {
|
|
@@ -65,8 +68,10 @@ function formatDuration(ms) {
|
|
|
65
68
|
return `${(ms / 3600000).toFixed(1)}h`;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
const getPulseReportSummary = (reportDir) => {
|
|
69
|
-
const
|
|
71
|
+
const getPulseReportSummary = async (reportDir) => {
|
|
72
|
+
const config = await getReporterConfig(customOutputDir);
|
|
73
|
+
const outputFile = config.outputFile;
|
|
74
|
+
const reportPath = path.join(reportDir, outputFile);
|
|
70
75
|
|
|
71
76
|
if (!fsExistsSync(reportPath)) {
|
|
72
77
|
throw new Error("Pulse report file not found.");
|
|
@@ -286,8 +291,9 @@ const sendEmail = async (credentials, reportDir) => {
|
|
|
286
291
|
|
|
287
292
|
try {
|
|
288
293
|
console.log("Starting the sendEmail function...");
|
|
289
|
-
const
|
|
290
|
-
const
|
|
294
|
+
const uuid = await getUUID(reportDir);
|
|
295
|
+
const summary = await getPulseReportSummary(reportDir);
|
|
296
|
+
const htmlContent = generateHtmlTable(summary);
|
|
291
297
|
|
|
292
298
|
const recipients = [
|
|
293
299
|
process.env.RECIPIENT_EMAIL_1 || "",
|
|
@@ -460,6 +466,7 @@ const main = async () => {
|
|
|
460
466
|
}
|
|
461
467
|
|
|
462
468
|
const reportDir = await getOutputDir(customOutputDir);
|
|
469
|
+
await mergeSequentialReportsIfNeeded(reportDir);
|
|
463
470
|
console.log(chalk.blue(`Preparing to send email report...`));
|
|
464
471
|
console.log(chalk.blue(`Report directory set to: ${reportDir}`));
|
|
465
472
|
|