@arghajit/playwright-pulse-report 0.3.0 → 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 +64 -3
- 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 -10
- package/dist/types/index.d.ts +3 -0
- package/package.json +7 -7
- package/scripts/config-reader.mjs +100 -52
- package/scripts/generate-email-report.mjs +60 -10
- package/scripts/generate-report.mjs +491 -52
- package/scripts/generate-static-report.mjs +768 -371
- package/scripts/sendReport.mjs +74 -14
package/README.md
CHANGED
|
@@ -90,13 +90,13 @@ All CLI scripts now support custom output directories, giving you full flexibili
|
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
# Using custom directory
|
|
93
|
-
npx generate-pulse-report --outputDir
|
|
93
|
+
npx generate-pulse-report --outputDir {YOUR_CUSTOM_REPORT_FOLDER}
|
|
94
94
|
npx generate-report -o test-results/e2e
|
|
95
95
|
npx send-email --outputDir custom-pulse-reports
|
|
96
96
|
|
|
97
97
|
# Using nested paths
|
|
98
98
|
npx generate-pulse-report --outputDir reports/integration
|
|
99
|
-
npx merge-pulse-report --outputDir
|
|
99
|
+
npx merge-pulse-report --outputDir {YOUR_CUSTOM_REPORT_FOLDER}
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
**Important:** Make sure your `playwright.config.ts` custom directory matches the CLI script:
|
|
@@ -105,7 +105,7 @@ npx merge-pulse-report --outputDir my-test-reports
|
|
|
105
105
|
import { defineConfig } from "@playwright/test";
|
|
106
106
|
import * as path from "path";
|
|
107
107
|
|
|
108
|
-
const CUSTOM_REPORT_DIR = path.resolve(__dirname, "
|
|
108
|
+
const CUSTOM_REPORT_DIR = path.resolve(__dirname, "{YOUR_CUSTOM_REPORT_FOLDER}");
|
|
109
109
|
|
|
110
110
|
export default defineConfig({
|
|
111
111
|
reporter: [
|
|
@@ -174,6 +174,67 @@ The dashboard includes AI-powered test analysis that provides:
|
|
|
174
174
|
- Failure pattern recognition
|
|
175
175
|
- Suggested optimizations
|
|
176
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
|
+
|
|
177
238
|
## ⚙️ CI/CD Integration
|
|
178
239
|
|
|
179
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, _o;
|
|
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,27 +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,
|
|
264
|
-
annotations: ((
|
|
281
|
+
annotations: ((_m = test.annotations) === null || _m === void 0 ? void 0 : _m.length) > 0 ? test.annotations : undefined,
|
|
265
282
|
...testSpecificData,
|
|
266
283
|
};
|
|
267
284
|
for (const [index, attachment] of result.attachments.entries()) {
|
|
@@ -278,16 +295,16 @@ class PlaywrightPulseReporter {
|
|
|
278
295
|
await this._ensureDirExists(path.dirname(absoluteDestPath));
|
|
279
296
|
await fs.copyFile(attachment.path, absoluteDestPath);
|
|
280
297
|
if (attachment.contentType.startsWith("image/")) {
|
|
281
|
-
(
|
|
298
|
+
(_o = pulseResult.screenshots) === null || _o === void 0 ? void 0 : _o.push(relativeDestPath);
|
|
282
299
|
}
|
|
283
300
|
else if (attachment.contentType.startsWith("video/")) {
|
|
284
|
-
(
|
|
301
|
+
(_p = pulseResult.videoPath) === null || _p === void 0 ? void 0 : _p.push(relativeDestPath);
|
|
285
302
|
}
|
|
286
303
|
else if (attachment.name === "trace") {
|
|
287
304
|
pulseResult.tracePath = relativeDestPath;
|
|
288
305
|
}
|
|
289
306
|
else {
|
|
290
|
-
(
|
|
307
|
+
(_q = pulseResult.attachments) === null || _q === void 0 ? void 0 : _q.push({
|
|
291
308
|
name: attachment.name,
|
|
292
309
|
path: relativeDestPath,
|
|
293
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;
|
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.3.
|
|
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": [
|
|
@@ -30,12 +30,12 @@
|
|
|
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
|
".": {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
import * as fs from "fs";
|
|
3
2
|
import * as path from "path";
|
|
4
3
|
import { pathToFileURL } from "url";
|
|
@@ -24,9 +23,61 @@ async function findPlaywrightConfig() {
|
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
async function extractOutputDirFromConfig(configPath) {
|
|
26
|
+
let fileContent = "";
|
|
27
27
|
try {
|
|
28
|
-
|
|
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
|
+
}
|
|
29
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;
|
|
30
81
|
const configDir = dirname(configPath);
|
|
31
82
|
const originalDirname = global.__dirname;
|
|
32
83
|
const originalFilename = global.__filename;
|
|
@@ -39,73 +90,70 @@ async function extractOutputDirFromConfig(configPath) {
|
|
|
39
90
|
try {
|
|
40
91
|
const { register } = await import("node:module");
|
|
41
92
|
const { pathToFileURL } = await import("node:url");
|
|
42
|
-
|
|
43
93
|
register("ts-node/esm", pathToFileURL("./"));
|
|
44
|
-
|
|
45
94
|
config = await import(pathToFileURL(configPath).href);
|
|
46
95
|
} catch (tsError) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
}
|
|
96
|
+
const tsNode = await import("ts-node");
|
|
97
|
+
tsNode.register({
|
|
98
|
+
transpileOnly: true,
|
|
99
|
+
compilerOptions: { module: "commonjs" },
|
|
100
|
+
});
|
|
101
|
+
config = require(configPath);
|
|
60
102
|
}
|
|
61
103
|
} else {
|
|
104
|
+
// Try dynamic import for JS/MJS
|
|
62
105
|
config = await import(pathToFileURL(configPath).href);
|
|
63
106
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
delete global.__dirname;
|
|
69
|
-
}
|
|
70
|
-
if (originalFilename !== undefined) {
|
|
71
|
-
global.__filename = originalFilename;
|
|
72
|
-
} else {
|
|
73
|
-
delete global.__filename;
|
|
107
|
+
|
|
108
|
+
// Handle Default Export
|
|
109
|
+
if (config && config.default) {
|
|
110
|
+
config = config.default;
|
|
74
111
|
}
|
|
75
|
-
}
|
|
76
112
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
+
}
|
|
97
137
|
}
|
|
98
138
|
}
|
|
99
139
|
}
|
|
140
|
+
|
|
141
|
+
// Check for Global outputDir
|
|
142
|
+
if (config.outputDir) {
|
|
143
|
+
return path.resolve(process.cwd(), config.outputDir);
|
|
144
|
+
}
|
|
100
145
|
}
|
|
146
|
+
} finally {
|
|
147
|
+
// Clean up globals
|
|
148
|
+
global.__dirname = originalDirname;
|
|
149
|
+
global.__filename = originalFilename;
|
|
101
150
|
}
|
|
102
|
-
|
|
103
|
-
console.log("No matching reporter config found with outputDir");
|
|
104
|
-
return null;
|
|
105
151
|
} catch (error) {
|
|
106
|
-
|
|
152
|
+
// SILENT CATCH: Do NOT log anything here.
|
|
107
153
|
return null;
|
|
108
154
|
}
|
|
155
|
+
|
|
156
|
+
return null;
|
|
109
157
|
}
|
|
110
158
|
|
|
111
159
|
export async function getOutputDir(customOutputDirFromArgs = null) {
|
|
@@ -217,6 +217,40 @@ function generateMinifiedHTML(reportData) {
|
|
|
217
217
|
const testFileParts = test.name.split(" > ");
|
|
218
218
|
const testTitle =
|
|
219
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
|
+
|
|
220
254
|
html += `
|
|
221
255
|
<li class="test-item ${getStatusClass(test.status)}"
|
|
222
256
|
data-test-name-min="${sanitizeHTML(testTitle.toLowerCase())}"
|
|
@@ -230,9 +264,9 @@ function generateMinifiedHTML(reportData) {
|
|
|
230
264
|
<span class="test-title-text" title="${sanitizeHTML(
|
|
231
265
|
test.name
|
|
232
266
|
)}">${sanitizeHTML(testTitle)}</span>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
267
|
+
|
|
268
|
+
${severityBadge}
|
|
269
|
+
${tagsBadges}
|
|
236
270
|
</li>
|
|
237
271
|
`;
|
|
238
272
|
});
|
|
@@ -250,9 +284,9 @@ function generateMinifiedHTML(reportData) {
|
|
|
250
284
|
<head>
|
|
251
285
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
252
286
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
253
|
-
<link rel="icon" type="image/png" href="https://
|
|
254
|
-
<link rel="apple-touch-icon" href="https://
|
|
255
|
-
<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>
|
|
256
290
|
<style>
|
|
257
291
|
:root {
|
|
258
292
|
--primary-color: #2c3e50; /* Dark Blue/Grey */
|
|
@@ -492,8 +526,8 @@ function generateMinifiedHTML(reportData) {
|
|
|
492
526
|
<div class="container">
|
|
493
527
|
<header class="report-header">
|
|
494
528
|
<div class="report-header-title">
|
|
495
|
-
<img id="report-logo" src="https://
|
|
496
|
-
<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>
|
|
497
531
|
</div>
|
|
498
532
|
<div class="run-info">
|
|
499
533
|
<strong>Run Date:</strong> ${formatDate(
|
|
@@ -527,8 +561,24 @@ function generateMinifiedHTML(reportData) {
|
|
|
527
561
|
</section>
|
|
528
562
|
|
|
529
563
|
<section class="test-results-section">
|
|
530
|
-
<
|
|
531
|
-
|
|
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>
|
|
532
582
|
<div class="filters-section">
|
|
533
583
|
<input type="text" id="filter-min-name" placeholder="Search by test name...">
|
|
534
584
|
<select id="filter-min-status">
|