@arghajit/playwright-pulse-report 0.2.10 → 0.3.0
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 +36 -0
- package/dist/reporter/playwright-pulse-reporter.js +5 -4
- package/dist/types/index.d.ts +9 -0
- package/package.json +2 -2
- package/scripts/config-reader.mjs +132 -0
- package/scripts/generate-email-report.mjs +21 -1
- package/scripts/generate-report.mjs +525 -274
- package/scripts/generate-static-report.mjs +227 -47
- package/scripts/generate-trend.mjs +11 -1
- package/scripts/merge-pulse-report.js +46 -14
- package/scripts/sendReport.mjs +36 -21
package/README.md
CHANGED
|
@@ -84,6 +84,42 @@ npx generate-pulse-report # Generates static HTML
|
|
|
84
84
|
npx send-email # Sends email report
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
### 4. Custom Output Directory (Optional)
|
|
88
|
+
|
|
89
|
+
All CLI scripts now support custom output directories, giving you full flexibility over where reports are generated:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Using custom directory
|
|
93
|
+
npx generate-pulse-report --outputDir my-reports
|
|
94
|
+
npx generate-report -o test-results/e2e
|
|
95
|
+
npx send-email --outputDir custom-pulse-reports
|
|
96
|
+
|
|
97
|
+
# Using nested paths
|
|
98
|
+
npx generate-pulse-report --outputDir reports/integration
|
|
99
|
+
npx merge-pulse-report --outputDir my-test-reports
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Important:** Make sure your `playwright.config.ts` custom directory matches the CLI script:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { defineConfig } from "@playwright/test";
|
|
106
|
+
import * as path from "path";
|
|
107
|
+
|
|
108
|
+
const CUSTOM_REPORT_DIR = path.resolve(__dirname, "my-reports");
|
|
109
|
+
|
|
110
|
+
export default defineConfig({
|
|
111
|
+
reporter: [
|
|
112
|
+
["list"],
|
|
113
|
+
[
|
|
114
|
+
"@arghajit/playwright-pulse-report",
|
|
115
|
+
{
|
|
116
|
+
outputDir: CUSTOM_REPORT_DIR, // Must match CLI --outputDir
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
87
123
|
## 📊 Report Options
|
|
88
124
|
|
|
89
125
|
### Option 1: Static HTML Report (Embedded Attachments)
|
|
@@ -197,7 +197,7 @@ class PlaywrightPulseReporter {
|
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
199
|
async onTestEnd(test, result) {
|
|
200
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
200
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
201
201
|
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
|
|
202
202
|
const browserDetails = this.getBrowserDetails(test);
|
|
203
203
|
const testStatus = convertStatus(result.status, test);
|
|
@@ -261,6 +261,7 @@ class PlaywrightPulseReporter {
|
|
|
261
261
|
attachments: [],
|
|
262
262
|
stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
|
|
263
263
|
stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
|
|
264
|
+
annotations: ((_k = test.annotations) === null || _k === void 0 ? void 0 : _k.length) > 0 ? test.annotations : undefined,
|
|
264
265
|
...testSpecificData,
|
|
265
266
|
};
|
|
266
267
|
for (const [index, attachment] of result.attachments.entries()) {
|
|
@@ -277,16 +278,16 @@ class PlaywrightPulseReporter {
|
|
|
277
278
|
await this._ensureDirExists(path.dirname(absoluteDestPath));
|
|
278
279
|
await fs.copyFile(attachment.path, absoluteDestPath);
|
|
279
280
|
if (attachment.contentType.startsWith("image/")) {
|
|
280
|
-
(
|
|
281
|
+
(_l = pulseResult.screenshots) === null || _l === void 0 ? void 0 : _l.push(relativeDestPath);
|
|
281
282
|
}
|
|
282
283
|
else if (attachment.contentType.startsWith("video/")) {
|
|
283
|
-
(
|
|
284
|
+
(_m = pulseResult.videoPath) === null || _m === void 0 ? void 0 : _m.push(relativeDestPath);
|
|
284
285
|
}
|
|
285
286
|
else if (attachment.name === "trace") {
|
|
286
287
|
pulseResult.tracePath = relativeDestPath;
|
|
287
288
|
}
|
|
288
289
|
else {
|
|
289
|
-
(
|
|
290
|
+
(_o = pulseResult.attachments) === null || _o === void 0 ? void 0 : _o.push({
|
|
290
291
|
name: attachment.name,
|
|
291
292
|
path: relativeDestPath,
|
|
292
293
|
contentType: attachment.contentType,
|
package/dist/types/index.d.ts
CHANGED
|
@@ -46,6 +46,15 @@ export interface TestResult {
|
|
|
46
46
|
totalWorkers?: number;
|
|
47
47
|
configFile?: string;
|
|
48
48
|
metadata?: string;
|
|
49
|
+
annotations?: {
|
|
50
|
+
type: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
location?: {
|
|
53
|
+
file: string;
|
|
54
|
+
line: number;
|
|
55
|
+
column: number;
|
|
56
|
+
};
|
|
57
|
+
}[];
|
|
49
58
|
}
|
|
50
59
|
export interface TestRun {
|
|
51
60
|
id: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arghajit/playwright-pulse-report",
|
|
3
3
|
"author": "Arghajit Singha",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "A Playwright reporter and dashboard for visualizing test results.",
|
|
6
6
|
"homepage": "https://playwright-pulse-report.netlify.app/",
|
|
7
7
|
"keywords": [
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|
|
28
28
|
"screenshots",
|
|
29
|
-
"scripts
|
|
29
|
+
"scripts"
|
|
30
30
|
],
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"bin": {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
import { dirname } from "path";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
8
|
+
|
|
9
|
+
async function findPlaywrightConfig() {
|
|
10
|
+
const possibleConfigs = [
|
|
11
|
+
"playwright.config.ts",
|
|
12
|
+
"playwright.config.js",
|
|
13
|
+
"playwright.config.mjs",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
for (const configFile of possibleConfigs) {
|
|
17
|
+
const configPath = path.resolve(process.cwd(), configFile);
|
|
18
|
+
if (fs.existsSync(configPath)) {
|
|
19
|
+
return { path: configPath, exists: true };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { path: null, exists: false };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function extractOutputDirFromConfig(configPath) {
|
|
27
|
+
try {
|
|
28
|
+
let config;
|
|
29
|
+
|
|
30
|
+
const configDir = dirname(configPath);
|
|
31
|
+
const originalDirname = global.__dirname;
|
|
32
|
+
const originalFilename = global.__filename;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
global.__dirname = configDir;
|
|
36
|
+
global.__filename = configPath;
|
|
37
|
+
|
|
38
|
+
if (configPath.endsWith(".ts")) {
|
|
39
|
+
try {
|
|
40
|
+
const { register } = await import("node:module");
|
|
41
|
+
const { pathToFileURL } = await import("node:url");
|
|
42
|
+
|
|
43
|
+
register("ts-node/esm", pathToFileURL("./"));
|
|
44
|
+
|
|
45
|
+
config = await import(pathToFileURL(configPath).href);
|
|
46
|
+
} catch (tsError) {
|
|
47
|
+
try {
|
|
48
|
+
const tsNode = await import("ts-node");
|
|
49
|
+
tsNode.register({
|
|
50
|
+
transpileOnly: true,
|
|
51
|
+
compilerOptions: {
|
|
52
|
+
module: "ESNext",
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
config = await import(pathToFileURL(configPath).href);
|
|
56
|
+
} catch (fallbackError) {
|
|
57
|
+
console.error("Failed to load TypeScript config:", fallbackError);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
config = await import(pathToFileURL(configPath).href);
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
if (originalDirname !== undefined) {
|
|
66
|
+
global.__dirname = originalDirname;
|
|
67
|
+
} else {
|
|
68
|
+
delete global.__dirname;
|
|
69
|
+
}
|
|
70
|
+
if (originalFilename !== undefined) {
|
|
71
|
+
global.__filename = originalFilename;
|
|
72
|
+
} else {
|
|
73
|
+
delete global.__filename;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const playwrightConfig = config.default || config;
|
|
78
|
+
|
|
79
|
+
if (playwrightConfig && Array.isArray(playwrightConfig.reporter)) {
|
|
80
|
+
for (const reporterConfig of playwrightConfig.reporter) {
|
|
81
|
+
if (Array.isArray(reporterConfig)) {
|
|
82
|
+
const [reporterPath, options] = reporterConfig;
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
typeof reporterPath === "string" &&
|
|
86
|
+
(reporterPath.includes("playwright-pulse-report") ||
|
|
87
|
+
reporterPath.includes("@arghajit/playwright-pulse-report") ||
|
|
88
|
+
reporterPath.includes("@arghajit/dummy"))
|
|
89
|
+
) {
|
|
90
|
+
if (options && options.outputDir) {
|
|
91
|
+
const resolvedPath =
|
|
92
|
+
typeof options.outputDir === "string"
|
|
93
|
+
? options.outputDir
|
|
94
|
+
: options.outputDir;
|
|
95
|
+
console.log(`Found outputDir in config: ${resolvedPath}`);
|
|
96
|
+
return path.resolve(process.cwd(), resolvedPath);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log("No matching reporter config found with outputDir");
|
|
104
|
+
return null;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error("Error extracting outputDir from config:", error);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function getOutputDir(customOutputDirFromArgs = null) {
|
|
112
|
+
if (customOutputDirFromArgs) {
|
|
113
|
+
console.log(`Using custom outputDir from CLI: ${customOutputDirFromArgs}`);
|
|
114
|
+
return path.resolve(process.cwd(), customOutputDirFromArgs);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { path: configPath, exists } = await findPlaywrightConfig();
|
|
118
|
+
console.log(
|
|
119
|
+
`Config file search result: ${exists ? configPath : "not found"}`
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (exists) {
|
|
123
|
+
const outputDirFromConfig = await extractOutputDirFromConfig(configPath);
|
|
124
|
+
if (outputDirFromConfig) {
|
|
125
|
+
console.log(`Using outputDir from config: ${outputDirFromConfig}`);
|
|
126
|
+
return outputDirFromConfig;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(`Using default outputDir: ${DEFAULT_OUTPUT_DIR}`);
|
|
131
|
+
return path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
132
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
|
+
import { getOutputDir } from "./config-reader.mjs";
|
|
5
6
|
|
|
6
7
|
// Use dynamic import for chalk as it's ESM only
|
|
7
8
|
let chalk;
|
|
@@ -23,6 +24,15 @@ const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
|
23
24
|
const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
|
|
24
25
|
const MINIFIED_HTML_FILE = "pulse-email-summary.html"; // New minified report
|
|
25
26
|
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
let customOutputDir = null;
|
|
29
|
+
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
if (args[i] === "--outputDir" || args[i] === "-o") {
|
|
31
|
+
customOutputDir = args[i + 1];
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
function sanitizeHTML(str) {
|
|
27
37
|
if (str === null || str === undefined) return "";
|
|
28
38
|
return String(str).replace(/[&<>"']/g, (match) => {
|
|
@@ -652,10 +662,20 @@ function generateMinifiedHTML(reportData) {
|
|
|
652
662
|
`;
|
|
653
663
|
}
|
|
654
664
|
async function main() {
|
|
655
|
-
const outputDir =
|
|
665
|
+
const outputDir = await getOutputDir(customOutputDir);
|
|
656
666
|
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE);
|
|
657
667
|
const minifiedReportHtmlPath = path.resolve(outputDir, MINIFIED_HTML_FILE); // Path for the new minified HTML
|
|
658
668
|
|
|
669
|
+
console.log(chalk.blue(`Generating email report...`));
|
|
670
|
+
console.log(chalk.blue(`Output directory set to: ${outputDir}`));
|
|
671
|
+
if (customOutputDir) {
|
|
672
|
+
console.log(chalk.gray(` (from CLI argument)`));
|
|
673
|
+
} else {
|
|
674
|
+
console.log(
|
|
675
|
+
chalk.gray(` (auto-detected from playwright.config or using default)`)
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
659
679
|
// Step 2: Load current run's data
|
|
660
680
|
let currentRunReportData;
|
|
661
681
|
try {
|