@arghajit/playwright-pulse-report 0.2.10 → 0.3.1
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 +97 -0
- package/dist/pulse.d.ts +12 -0
- package/dist/pulse.js +24 -0
- package/dist/reporter/index.d.ts +2 -0
- package/dist/reporter/index.js +5 -1
- package/dist/reporter/playwright-pulse-reporter.d.ts +1 -0
- package/dist/reporter/playwright-pulse-reporter.js +27 -9
- package/dist/types/index.d.ts +12 -0
- package/package.json +8 -8
- package/scripts/config-reader.mjs +180 -0
- package/scripts/generate-email-report.mjs +81 -11
- package/scripts/generate-report.mjs +996 -306
- package/scripts/generate-static-report.mjs +895 -318
- package/scripts/generate-trend.mjs +11 -1
- package/scripts/merge-pulse-report.js +46 -14
- package/scripts/sendReport.mjs +109 -34
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 {YOUR_CUSTOM_REPORT_FOLDER}
|
|
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 {YOUR_CUSTOM_REPORT_FOLDER}
|
|
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, "{YOUR_CUSTOM_REPORT_FOLDER}");
|
|
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)
|
|
@@ -138,6 +174,67 @@ The dashboard includes AI-powered test analysis that provides:
|
|
|
138
174
|
- Failure pattern recognition
|
|
139
175
|
- Suggested optimizations
|
|
140
176
|
|
|
177
|
+
## 📧 Send Report to Mail
|
|
178
|
+
|
|
179
|
+
The `send-email` CLI wraps the full email flow:
|
|
180
|
+
|
|
181
|
+
- Generates a lightweight HTML summary (`pulse-email-summary.html`) from the latest `playwright-pulse-report.json`.
|
|
182
|
+
- Builds a stats table (start time, duration, total, passed, failed, skipped, percentages).
|
|
183
|
+
- Sends an email with that summary as both the body and an HTML attachment.
|
|
184
|
+
|
|
185
|
+
### 1. Configure Recipients
|
|
186
|
+
|
|
187
|
+
Set up to 5 recipients via environment variables:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
RECIPIENT_EMAIL_1=recipient1@example.com
|
|
191
|
+
RECIPIENT_EMAIL_2=recipient2@example.com
|
|
192
|
+
RECIPIENT_EMAIL_3=recipient3@example.com
|
|
193
|
+
RECIPIENT_EMAIL_4=recipient4@example.com
|
|
194
|
+
RECIPIENT_EMAIL_5=recipient5@example.com
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 2. Choose Credential Flow
|
|
198
|
+
|
|
199
|
+
The script supports two ways to obtain SMTP credentials:
|
|
200
|
+
|
|
201
|
+
**Flow A – Environment-based credentials (recommended)**
|
|
202
|
+
|
|
203
|
+
Provide mail host and credentials via environment variables:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
PULSE_MAIL_HOST=gmail # or: outlook
|
|
207
|
+
PULSE_MAIL_USERNAME=you@example.com
|
|
208
|
+
PULSE_MAIL_PASSWORD=your_app_password
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- `PULSE_MAIL_HOST` supports `gmail` or `outlook` only.
|
|
212
|
+
- For Gmail/Outlook, use an app password or SMTP-enabled credentials.
|
|
213
|
+
|
|
214
|
+
**Flow B – Default Flow (fallback)**
|
|
215
|
+
|
|
216
|
+
If the above variables are not set, the script fallbacks to default the mail host for compatibility.
|
|
217
|
+
|
|
218
|
+
### 3. Run the CLI
|
|
219
|
+
|
|
220
|
+
Use the default output directory:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
npx send-email
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Or point to a custom report directory (must contain `playwright-pulse-report.json`):
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
npx send-email --outputDir <YOUR_CUSTOM_REPORT_FOLDER>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Under the hood, this will:
|
|
233
|
+
|
|
234
|
+
- Resolve the report directory (from `--outputDir` or `playwright.config.ts`).
|
|
235
|
+
- Run `generate-email-report.mjs` to create `pulse-email-summary.html`.
|
|
236
|
+
- Use Nodemailer to send the email via the selected provider (Gmail or Outlook).
|
|
237
|
+
|
|
141
238
|
## ⚙️ CI/CD Integration
|
|
142
239
|
|
|
143
240
|
### Basic Workflow
|
package/dist/pulse.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type PulseSeverityLevel = "Minor" | "Low" | "Medium" | "High" | "Critical";
|
|
2
|
+
export declare const pulse: {
|
|
3
|
+
/**
|
|
4
|
+
* Sets the severity level for the current test.
|
|
5
|
+
* * @param level - The severity level ('Minor' | 'Low' | 'Medium' | 'High' | 'Critical')
|
|
6
|
+
* @example
|
|
7
|
+
* test('Login', async () => {
|
|
8
|
+
* pulse.severity('Critical');
|
|
9
|
+
* });
|
|
10
|
+
*/
|
|
11
|
+
severity: (level: PulseSeverityLevel) => void;
|
|
12
|
+
};
|
package/dist/pulse.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pulse = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
exports.pulse = {
|
|
6
|
+
/**
|
|
7
|
+
* Sets the severity level for the current test.
|
|
8
|
+
* * @param level - The severity level ('Minor' | 'Low' | 'Medium' | 'High' | 'Critical')
|
|
9
|
+
* @example
|
|
10
|
+
* test('Login', async () => {
|
|
11
|
+
* pulse.severity('Critical');
|
|
12
|
+
* });
|
|
13
|
+
*/
|
|
14
|
+
severity: (level) => {
|
|
15
|
+
const validLevels = ["Minor", "Low", "Medium", "High", "Critical"];
|
|
16
|
+
// Default to "Medium" if an invalid string is passed
|
|
17
|
+
const selectedLevel = validLevels.includes(level) ? level : "Medium";
|
|
18
|
+
// Add the annotation to Playwright's test info
|
|
19
|
+
test_1.test.info().annotations.push({
|
|
20
|
+
type: "pulse_severity",
|
|
21
|
+
description: selectedLevel,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
};
|
package/dist/reporter/index.d.ts
CHANGED
|
@@ -3,3 +3,5 @@ export default PlaywrightPulseReporter;
|
|
|
3
3
|
export { PlaywrightPulseReporter };
|
|
4
4
|
export type { PlaywrightPulseReport } from "../lib/report-types";
|
|
5
5
|
export type { TestResult, TestRun, TestStep, TestStatus } from "../types";
|
|
6
|
+
export { pulse } from "../pulse";
|
|
7
|
+
export type { PulseSeverityLevel } from "../pulse";
|
package/dist/reporter/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PlaywrightPulseReporter = void 0;
|
|
3
|
+
exports.pulse = exports.PlaywrightPulseReporter = void 0;
|
|
4
4
|
// src/reporter/index.ts
|
|
5
5
|
const playwright_pulse_reporter_1 = require("./playwright-pulse-reporter");
|
|
6
6
|
Object.defineProperty(exports, "PlaywrightPulseReporter", { enumerable: true, get: function () { return playwright_pulse_reporter_1.PlaywrightPulseReporter; } });
|
|
7
7
|
// Export the reporter class as the default export for CommonJS compatibility
|
|
8
8
|
// and also as a named export for potential ES module consumers.
|
|
9
9
|
exports.default = playwright_pulse_reporter_1.PlaywrightPulseReporter;
|
|
10
|
+
// --- NEW: Export the pulse helper ---
|
|
11
|
+
// This allows: import { pulse } from '@arghajit/playwright-pulse-report';
|
|
12
|
+
var pulse_1 = require("../pulse"); // Adjust path based on where you placed pulse.ts
|
|
13
|
+
Object.defineProperty(exports, "pulse", { enumerable: true, get: function () { return pulse_1.pulse; } });
|
|
@@ -16,6 +16,7 @@ export declare class PlaywrightPulseReporter implements Reporter {
|
|
|
16
16
|
printsToStdio(): boolean;
|
|
17
17
|
onBegin(config: FullConfig, suite: Suite): void;
|
|
18
18
|
onTestBegin(test: TestCase): void;
|
|
19
|
+
private _getSeverity;
|
|
19
20
|
private getBrowserDetails;
|
|
20
21
|
private processStep;
|
|
21
22
|
onTestEnd(test: TestCase, result: PwTestResult): Promise<void>;
|
|
@@ -109,6 +109,10 @@ class PlaywrightPulseReporter {
|
|
|
109
109
|
onTestBegin(test) {
|
|
110
110
|
console.log(`Starting test: ${test.title}`);
|
|
111
111
|
}
|
|
112
|
+
_getSeverity(annotations) {
|
|
113
|
+
const severityAnnotation = annotations.find((a) => a.type === "pulse_severity");
|
|
114
|
+
return (severityAnnotation === null || severityAnnotation === void 0 ? void 0 : severityAnnotation.description) || "Medium";
|
|
115
|
+
}
|
|
112
116
|
getBrowserDetails(test) {
|
|
113
117
|
var _a, _b, _c, _d;
|
|
114
118
|
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
|
|
@@ -197,7 +201,7 @@ class PlaywrightPulseReporter {
|
|
|
197
201
|
};
|
|
198
202
|
}
|
|
199
203
|
async onTestEnd(test, result) {
|
|
200
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
204
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
201
205
|
const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
|
|
202
206
|
const browserDetails = this.getBrowserDetails(test);
|
|
203
207
|
const testStatus = convertStatus(result.status, test);
|
|
@@ -224,6 +228,16 @@ class PlaywrightPulseReporter {
|
|
|
224
228
|
catch (e) {
|
|
225
229
|
console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
|
|
226
230
|
}
|
|
231
|
+
// 1. Get Spec File Name
|
|
232
|
+
const specFileName = ((_e = test.location) === null || _e === void 0 ? void 0 : _e.file)
|
|
233
|
+
? path.basename(test.location.file)
|
|
234
|
+
: "n/a";
|
|
235
|
+
// 2. Get Describe Block Name
|
|
236
|
+
// Check if the immediate parent is a 'describe' block
|
|
237
|
+
let describeBlockName = "n/a";
|
|
238
|
+
if (((_f = test.parent) === null || _f === void 0 ? void 0 : _f.type) === "describe") {
|
|
239
|
+
describeBlockName = test.parent.title;
|
|
240
|
+
}
|
|
227
241
|
const stdoutMessages = result.stdout.map((item) => typeof item === "string" ? item : item.toString());
|
|
228
242
|
const stderrMessages = result.stderr.map((item) => typeof item === "string" ? item : item.toString());
|
|
229
243
|
const maxWorkers = this.config.workers;
|
|
@@ -241,26 +255,30 @@ class PlaywrightPulseReporter {
|
|
|
241
255
|
const pulseResult = {
|
|
242
256
|
id: test.id,
|
|
243
257
|
runId: "TBD",
|
|
258
|
+
describe: describeBlockName,
|
|
259
|
+
spec_file: specFileName,
|
|
244
260
|
name: test.titlePath().join(" > "),
|
|
245
|
-
suiteName: (project === null || project === void 0 ? void 0 : project.name) || ((
|
|
261
|
+
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",
|
|
246
262
|
status: testStatus,
|
|
247
263
|
duration: result.duration,
|
|
248
264
|
startTime: startTime,
|
|
249
265
|
endTime: endTime,
|
|
250
266
|
browser: browserDetails,
|
|
251
267
|
retries: result.retry,
|
|
252
|
-
steps: ((
|
|
253
|
-
errorMessage: (
|
|
254
|
-
stackTrace: (
|
|
255
|
-
snippet: (
|
|
268
|
+
steps: ((_h = result.steps) === null || _h === void 0 ? void 0 : _h.length) ? await processAllSteps(result.steps) : [],
|
|
269
|
+
errorMessage: (_j = result.error) === null || _j === void 0 ? void 0 : _j.message,
|
|
270
|
+
stackTrace: (_k = result.error) === null || _k === void 0 ? void 0 : _k.stack,
|
|
271
|
+
snippet: (_l = result.error) === null || _l === void 0 ? void 0 : _l.snippet,
|
|
256
272
|
codeSnippet: codeSnippet,
|
|
257
273
|
tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
|
|
274
|
+
severity: this._getSeverity(test.annotations),
|
|
258
275
|
screenshots: [],
|
|
259
276
|
videoPath: [],
|
|
260
277
|
tracePath: undefined,
|
|
261
278
|
attachments: [],
|
|
262
279
|
stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
|
|
263
280
|
stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
|
|
281
|
+
annotations: ((_m = test.annotations) === null || _m === void 0 ? void 0 : _m.length) > 0 ? test.annotations : undefined,
|
|
264
282
|
...testSpecificData,
|
|
265
283
|
};
|
|
266
284
|
for (const [index, attachment] of result.attachments.entries()) {
|
|
@@ -277,16 +295,16 @@ class PlaywrightPulseReporter {
|
|
|
277
295
|
await this._ensureDirExists(path.dirname(absoluteDestPath));
|
|
278
296
|
await fs.copyFile(attachment.path, absoluteDestPath);
|
|
279
297
|
if (attachment.contentType.startsWith("image/")) {
|
|
280
|
-
(
|
|
298
|
+
(_o = pulseResult.screenshots) === null || _o === void 0 ? void 0 : _o.push(relativeDestPath);
|
|
281
299
|
}
|
|
282
300
|
else if (attachment.contentType.startsWith("video/")) {
|
|
283
|
-
(
|
|
301
|
+
(_p = pulseResult.videoPath) === null || _p === void 0 ? void 0 : _p.push(relativeDestPath);
|
|
284
302
|
}
|
|
285
303
|
else if (attachment.name === "trace") {
|
|
286
304
|
pulseResult.tracePath = relativeDestPath;
|
|
287
305
|
}
|
|
288
306
|
else {
|
|
289
|
-
(
|
|
307
|
+
(_q = pulseResult.attachments) === null || _q === void 0 ? void 0 : _q.push({
|
|
290
308
|
name: attachment.name,
|
|
291
309
|
path: relativeDestPath,
|
|
292
310
|
contentType: attachment.contentType,
|
package/dist/types/index.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface TestStep {
|
|
|
17
17
|
}
|
|
18
18
|
export interface TestResult {
|
|
19
19
|
id: string;
|
|
20
|
+
describe?: string;
|
|
21
|
+
spec_file?: string;
|
|
20
22
|
name: string;
|
|
21
23
|
status: TestStatus;
|
|
22
24
|
duration: number;
|
|
@@ -29,6 +31,7 @@ export interface TestResult {
|
|
|
29
31
|
snippet?: string;
|
|
30
32
|
codeSnippet?: string;
|
|
31
33
|
tags?: string[];
|
|
34
|
+
severity?: "Minor" | "Low" | "Medium" | "High" | "Critical";
|
|
32
35
|
suiteName?: string;
|
|
33
36
|
runId: string;
|
|
34
37
|
browser: string;
|
|
@@ -46,6 +49,15 @@ export interface TestResult {
|
|
|
46
49
|
totalWorkers?: number;
|
|
47
50
|
configFile?: string;
|
|
48
51
|
metadata?: string;
|
|
52
|
+
annotations?: {
|
|
53
|
+
type: string;
|
|
54
|
+
description?: string;
|
|
55
|
+
location?: {
|
|
56
|
+
file: string;
|
|
57
|
+
line: number;
|
|
58
|
+
column: number;
|
|
59
|
+
};
|
|
60
|
+
}[];
|
|
49
61
|
}
|
|
50
62
|
export interface TestRun {
|
|
51
63
|
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.1",
|
|
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,16 +26,16 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|
|
28
28
|
"screenshots",
|
|
29
|
-
"scripts
|
|
29
|
+
"scripts"
|
|
30
30
|
],
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"bin": {
|
|
33
|
-
"generate-pulse-report": "
|
|
34
|
-
"generate-report": "
|
|
35
|
-
"merge-pulse-report": "
|
|
36
|
-
"send-email": "
|
|
37
|
-
"generate-trend": "
|
|
38
|
-
"generate-email-report": "
|
|
33
|
+
"generate-pulse-report": "scripts/generate-static-report.mjs",
|
|
34
|
+
"generate-report": "scripts/generate-report.mjs",
|
|
35
|
+
"merge-pulse-report": "scripts/merge-pulse-report.js",
|
|
36
|
+
"send-email": "scripts/sendReport.mjs",
|
|
37
|
+
"generate-trend": "scripts/generate-trend.mjs",
|
|
38
|
+
"generate-email-report": "scripts/generate-email-report.mjs"
|
|
39
39
|
},
|
|
40
40
|
"exports": {
|
|
41
41
|
".": {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import { dirname } from "path";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
7
|
+
|
|
8
|
+
async function findPlaywrightConfig() {
|
|
9
|
+
const possibleConfigs = [
|
|
10
|
+
"playwright.config.ts",
|
|
11
|
+
"playwright.config.js",
|
|
12
|
+
"playwright.config.mjs",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
for (const configFile of possibleConfigs) {
|
|
16
|
+
const configPath = path.resolve(process.cwd(), configFile);
|
|
17
|
+
if (fs.existsSync(configPath)) {
|
|
18
|
+
return { path: configPath, exists: true };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { path: null, exists: false };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function extractOutputDirFromConfig(configPath) {
|
|
26
|
+
let fileContent = "";
|
|
27
|
+
try {
|
|
28
|
+
fileContent = fs.readFileSync(configPath, "utf-8");
|
|
29
|
+
} catch (e) {
|
|
30
|
+
// If we can't read the file, we can't parse or import it.
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 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
|
+
try {
|
|
38
|
+
// Regex matches: outputDir: "value" or outputDir: 'value'
|
|
39
|
+
const match = fileContent.match(/outputDir:\s*["']([^"']+)["']/);
|
|
40
|
+
|
|
41
|
+
if (match && match[1]) {
|
|
42
|
+
return path.resolve(process.cwd(), match[1]);
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// Ignore text reading errors
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. Safety Check: Detect ESM in CJS to Prevent Node Warnings
|
|
49
|
+
// The warning "To load an ES module..." happens when we try to import()
|
|
50
|
+
// a .js file containing ESM syntax (import/export) in a CJS package.
|
|
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;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
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
|
+
|
|
85
|
+
try {
|
|
86
|
+
global.__dirname = configDir;
|
|
87
|
+
global.__filename = configPath;
|
|
88
|
+
|
|
89
|
+
if (configPath.endsWith(".ts")) {
|
|
90
|
+
try {
|
|
91
|
+
const { register } = await import("node:module");
|
|
92
|
+
const { pathToFileURL } = await import("node:url");
|
|
93
|
+
register("ts-node/esm", pathToFileURL("./"));
|
|
94
|
+
config = await import(pathToFileURL(configPath).href);
|
|
95
|
+
} catch (tsError) {
|
|
96
|
+
const tsNode = await import("ts-node");
|
|
97
|
+
tsNode.register({
|
|
98
|
+
transpileOnly: true,
|
|
99
|
+
compilerOptions: { module: "commonjs" },
|
|
100
|
+
});
|
|
101
|
+
config = require(configPath);
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
// Try dynamic import for JS/MJS
|
|
105
|
+
config = await import(pathToFileURL(configPath).href);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Handle Default Export
|
|
109
|
+
if (config && config.default) {
|
|
110
|
+
config = config.default;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (config) {
|
|
114
|
+
// Check for Reporter Config
|
|
115
|
+
if (config.reporter) {
|
|
116
|
+
const reporters = Array.isArray(config.reporter)
|
|
117
|
+
? config.reporter
|
|
118
|
+
: [config.reporter];
|
|
119
|
+
|
|
120
|
+
for (const reporter of reporters) {
|
|
121
|
+
const reporterName = Array.isArray(reporter)
|
|
122
|
+
? reporter[0]
|
|
123
|
+
: reporter;
|
|
124
|
+
const reporterOptions = Array.isArray(reporter)
|
|
125
|
+
? reporter[1]
|
|
126
|
+
: null;
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
typeof reporterName === "string" &&
|
|
130
|
+
(reporterName.includes("playwright-pulse-report") ||
|
|
131
|
+
reporterName.includes("@arghajit/playwright-pulse-report") ||
|
|
132
|
+
reporterName.includes("@arghajit/dummy"))
|
|
133
|
+
) {
|
|
134
|
+
if (reporterOptions && reporterOptions.outputDir) {
|
|
135
|
+
return path.resolve(process.cwd(), reporterOptions.outputDir);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
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;
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
// SILENT CATCH: Do NOT log anything here.
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function getOutputDir(customOutputDirFromArgs = null) {
|
|
160
|
+
if (customOutputDirFromArgs) {
|
|
161
|
+
console.log(`Using custom outputDir from CLI: ${customOutputDirFromArgs}`);
|
|
162
|
+
return path.resolve(process.cwd(), customOutputDirFromArgs);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { path: configPath, exists } = await findPlaywrightConfig();
|
|
166
|
+
console.log(
|
|
167
|
+
`Config file search result: ${exists ? configPath : "not found"}`
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (exists) {
|
|
171
|
+
const outputDirFromConfig = await extractOutputDirFromConfig(configPath);
|
|
172
|
+
if (outputDirFromConfig) {
|
|
173
|
+
console.log(`Using outputDir from config: ${outputDirFromConfig}`);
|
|
174
|
+
return outputDirFromConfig;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(`Using default outputDir: ${DEFAULT_OUTPUT_DIR}`);
|
|
179
|
+
return path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
180
|
+
}
|
|
@@ -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) => {
|
|
@@ -207,6 +217,40 @@ function generateMinifiedHTML(reportData) {
|
|
|
207
217
|
const testFileParts = test.name.split(" > ");
|
|
208
218
|
const testTitle =
|
|
209
219
|
testFileParts[testFileParts.length - 1] || "Unnamed Test";
|
|
220
|
+
|
|
221
|
+
// --- NEW: Severity Logic ---
|
|
222
|
+
const severity = test.severity || "Medium";
|
|
223
|
+
const getSeverityColor = (level) => {
|
|
224
|
+
switch (level) {
|
|
225
|
+
case "Minor":
|
|
226
|
+
return "#006064";
|
|
227
|
+
case "Low":
|
|
228
|
+
return "#FFA07A";
|
|
229
|
+
case "Medium":
|
|
230
|
+
return "#577A11";
|
|
231
|
+
case "High":
|
|
232
|
+
return "#B71C1C";
|
|
233
|
+
case "Critical":
|
|
234
|
+
return "#64158A";
|
|
235
|
+
default:
|
|
236
|
+
return "#577A11";
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
// We use inline styles here to ensure they render correctly in emails
|
|
240
|
+
const severityBadge = `<span style="background-color: ${getSeverityColor(
|
|
241
|
+
severity
|
|
242
|
+
)}; font-size: 0.8em; font-weight: 600; padding: 3px 8px; border-radius: 4px; color: #fff; margin-left: 10px; white-space: nowrap;">${severity}</span>`;
|
|
243
|
+
|
|
244
|
+
// --- NEW: Tags Logic ---
|
|
245
|
+
const tagsBadges = (test.tags || [])
|
|
246
|
+
.map(
|
|
247
|
+
(tag) =>
|
|
248
|
+
`<span style="background-color: #7f8c8d; font-size: 0.8em; font-weight: 600; padding: 3px 8px; border-radius: 4px; color: #fff; margin-left: 5px; white-space: nowrap;">${sanitizeHTML(
|
|
249
|
+
tag
|
|
250
|
+
)}</span>`
|
|
251
|
+
)
|
|
252
|
+
.join("");
|
|
253
|
+
|
|
210
254
|
html += `
|
|
211
255
|
<li class="test-item ${getStatusClass(test.status)}"
|
|
212
256
|
data-test-name-min="${sanitizeHTML(testTitle.toLowerCase())}"
|
|
@@ -220,9 +264,9 @@ function generateMinifiedHTML(reportData) {
|
|
|
220
264
|
<span class="test-title-text" title="${sanitizeHTML(
|
|
221
265
|
test.name
|
|
222
266
|
)}">${sanitizeHTML(testTitle)}</span>
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
267
|
+
|
|
268
|
+
${severityBadge}
|
|
269
|
+
${tagsBadges}
|
|
226
270
|
</li>
|
|
227
271
|
`;
|
|
228
272
|
});
|
|
@@ -240,9 +284,9 @@ function generateMinifiedHTML(reportData) {
|
|
|
240
284
|
<head>
|
|
241
285
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
242
286
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
243
|
-
<link rel="icon" type="image/png" href="https://
|
|
244
|
-
<link rel="apple-touch-icon" href="https://
|
|
245
|
-
<title>
|
|
287
|
+
<link rel="icon" type="image/png" href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png">
|
|
288
|
+
<link rel="apple-touch-icon" href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png">
|
|
289
|
+
<title>Pulse Summary Report</title>
|
|
246
290
|
<style>
|
|
247
291
|
:root {
|
|
248
292
|
--primary-color: #2c3e50; /* Dark Blue/Grey */
|
|
@@ -482,8 +526,8 @@ function generateMinifiedHTML(reportData) {
|
|
|
482
526
|
<div class="container">
|
|
483
527
|
<header class="report-header">
|
|
484
528
|
<div class="report-header-title">
|
|
485
|
-
<img id="report-logo" src="https://
|
|
486
|
-
<h1>
|
|
529
|
+
<img id="report-logo" src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png" alt="Report Logo">
|
|
530
|
+
<h1>Pulse Summary Report</h1>
|
|
487
531
|
</div>
|
|
488
532
|
<div class="run-info">
|
|
489
533
|
<strong>Run Date:</strong> ${formatDate(
|
|
@@ -517,8 +561,24 @@ function generateMinifiedHTML(reportData) {
|
|
|
517
561
|
</section>
|
|
518
562
|
|
|
519
563
|
<section class="test-results-section">
|
|
520
|
-
<
|
|
521
|
-
|
|
564
|
+
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; margin-top: 30px; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid var(--secondary-color);">
|
|
565
|
+
<h1 style="margin: 0; font-size: 1.5em; color: var(--primary-color);">Test Case Summary</h1>
|
|
566
|
+
<div style="display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 0.75em;">
|
|
567
|
+
<span style="font-weight: 600; color: var(--dark-gray-color);">Legend:</span>
|
|
568
|
+
|
|
569
|
+
<span style="margin-left: 4px; font-weight: 600; color: var(--text-color);">Severity:</span>
|
|
570
|
+
|
|
571
|
+
<span style="background-color: #006064; color: #fff; padding: 2px 6px; border-radius: 3px;">Minor</span>
|
|
572
|
+
<span style="background-color: #FFA07A; color: #fff; padding: 2px 6px; border-radius: 3px;">Low</span>
|
|
573
|
+
<span style="background-color: #577A11; color: #fff; padding: 2px 6px; border-radius: 3px;">Medium</span>
|
|
574
|
+
<span style="background-color: #B71C1C; color: #fff; padding: 2px 6px; border-radius: 3px;">High</span>
|
|
575
|
+
<span style="background-color: #64158A; color: #fff; padding: 2px 6px; border-radius: 3px;">Critical</span>
|
|
576
|
+
|
|
577
|
+
<span style="border-left: 1px solid #ccc; height: 14px; margin: 0 4px;"></span>
|
|
578
|
+
|
|
579
|
+
<span style="background-color: #7f8c8d; color: #fff; padding: 2px 6px; border-radius: 3px;">Tags</span>
|
|
580
|
+
</div>
|
|
581
|
+
</div>
|
|
522
582
|
<div class="filters-section">
|
|
523
583
|
<input type="text" id="filter-min-name" placeholder="Search by test name...">
|
|
524
584
|
<select id="filter-min-status">
|
|
@@ -652,10 +712,20 @@ function generateMinifiedHTML(reportData) {
|
|
|
652
712
|
`;
|
|
653
713
|
}
|
|
654
714
|
async function main() {
|
|
655
|
-
const outputDir =
|
|
715
|
+
const outputDir = await getOutputDir(customOutputDir);
|
|
656
716
|
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE);
|
|
657
717
|
const minifiedReportHtmlPath = path.resolve(outputDir, MINIFIED_HTML_FILE); // Path for the new minified HTML
|
|
658
718
|
|
|
719
|
+
console.log(chalk.blue(`Generating email report...`));
|
|
720
|
+
console.log(chalk.blue(`Output directory set to: ${outputDir}`));
|
|
721
|
+
if (customOutputDir) {
|
|
722
|
+
console.log(chalk.gray(` (from CLI argument)`));
|
|
723
|
+
} else {
|
|
724
|
+
console.log(
|
|
725
|
+
chalk.gray(` (auto-detected from playwright.config or using default)`)
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
659
729
|
// Step 2: Load current run's data
|
|
660
730
|
let currentRunReportData;
|
|
661
731
|
try {
|