@arghajit/dummy 0.1.0-beta-13 → 0.1.0-beta-15
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 +32 -8
- package/dist/reporter/playwright-pulse-reporter.d.ts +1 -0
- package/dist/reporter/playwright-pulse-reporter.js +35 -0
- package/dist/types/index.d.ts +17 -0
- package/package.json +4 -4
- package/scripts/generate-email-report.mjs +236 -53
- package/scripts/generate-static-report.mjs +1063 -992
- package/scripts/merge-pulse-report.js +1 -0
package/README.md
CHANGED
|
@@ -11,27 +11,49 @@
|
|
|
11
11
|
|
|
12
12
|
### 🖥️ Desktop View
|
|
13
13
|
|
|
14
|
-
<div align="center" style="display: flex; gap: 20px; justify-content: center; flex-wrap: wrap;">
|
|
14
|
+
<div align="center" style="display: flex; gap: 20px; justify-content: center; flex-wrap: wrap;">
|
|
15
|
+
<a href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//playwright-pulse-static-report-desktop.html.png" target="_blank"> <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//playwright-pulse-static-report-desktop.html.png" alt="Dashboard Overview" width="300"/>
|
|
16
|
+
<p align="center"><strong>Dashboard Overview</strong></p>
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Test-run-desktop.png" target="_blank"> <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Test-run-desktop.png" alt="Test Details" width="300"/>
|
|
19
|
+
<p align="center"><strong>Test Details</strong>
|
|
20
|
+
</p>
|
|
21
|
+
</a>
|
|
22
|
+
<a href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Test-error-desktop.png" target="_blank"> <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Test-run-desktop.png" alt="Test Failure Details" width="300"/>
|
|
23
|
+
<p align="center"><strong>Test Failure Details</strong>
|
|
24
|
+
</p>
|
|
25
|
+
</a>
|
|
26
|
+
<a href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Test-trends-desktop.png" target="_blank"> <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Test-trends-desktop.png" alt="Filter View" width="300"/>
|
|
27
|
+
<p align="center"><strong>Test Trends</strong></p>
|
|
28
|
+
</a>
|
|
29
|
+
</div>
|
|
15
30
|
|
|
16
31
|
### 📱 Mobile View
|
|
17
32
|
|
|
18
33
|
<div align="center" style="display: flex; gap: 20px; justify-content: center; flex-wrap: wrap;">
|
|
19
34
|
|
|
20
35
|
<a href="https://postimg.cc/CzJBLR5N" target="_blank">
|
|
21
|
-
<img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images
|
|
36
|
+
<img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//playwright-pulse-static-report-Dashboard.html.png" alt="Mobile Dashboard Overview" width="300"/>
|
|
22
37
|
<p align="center"><strong>Dashboard Overview</strong></p>
|
|
23
38
|
</a>
|
|
24
39
|
|
|
25
40
|
<a href="https://postimg.cc/G8YTczT8" target="_blank">
|
|
26
|
-
<img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images
|
|
41
|
+
<img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//playwright-pulse-static-report_Test-results.html.png" alt="Test Details" width="300"/>
|
|
27
42
|
<p align="center"><strong>Test Details</strong></p>
|
|
28
43
|
</a>
|
|
29
44
|
|
|
45
|
+
<a href="https://postimg.cc/G8YTczT8" target="_blank">
|
|
46
|
+
<img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//playwright-pulse-static-report-Trends.html.png" alt="Test Trends" width="300"/>
|
|
47
|
+
<p align="center"><strong>Test Trends</strong></p>
|
|
48
|
+
</a>
|
|
49
|
+
|
|
30
50
|
</div>
|
|
31
51
|
|
|
32
52
|
### Email Report Example
|
|
33
53
|
|
|
34
|
-
[](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Email-report-mobile-template.jpeg)
|
|
55
|
+
|
|
56
|
+
[](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//pulse-email-summary.html.png)
|
|
35
57
|
|
|
36
58
|
## 🛠️ How It Works
|
|
37
59
|
|
|
@@ -51,11 +73,11 @@
|
|
|
51
73
|
### 1. Installation
|
|
52
74
|
|
|
53
75
|
```bash
|
|
54
|
-
npm install @arghajit/playwright-pulse-
|
|
76
|
+
npm install @arghajit/playwright-pulse-report@latest --save-dev
|
|
55
77
|
# or
|
|
56
|
-
yarn add @arghajit/playwright-pulse-
|
|
78
|
+
yarn add @arghajit/playwright-pulse-report@latest --dev
|
|
57
79
|
# or
|
|
58
|
-
pnpm add @arghajit/playwright-pulse-
|
|
80
|
+
pnpm add @arghajit/playwright-pulse-report@latest --save-dev
|
|
59
81
|
```
|
|
60
82
|
|
|
61
83
|
### 2. Configure Playwright
|
|
@@ -71,7 +93,7 @@ const PULSE_REPORT_DIR = path.resolve(__dirname, 'pulse-report');
|
|
|
71
93
|
export default defineConfig({
|
|
72
94
|
reporter: [
|
|
73
95
|
['list'],
|
|
74
|
-
['@arghajit/playwright-pulse-
|
|
96
|
+
['@arghajit/playwright-pulse-report', {
|
|
75
97
|
outputDir: PULSE_REPORT_DIR
|
|
76
98
|
}]
|
|
77
99
|
],
|
|
@@ -116,6 +138,8 @@ npx generate-pulse-report
|
|
|
116
138
|
npx send-email
|
|
117
139
|
```
|
|
118
140
|
|
|
141
|
+
NOTE: The email will be send with a light-weight html file, which can be opened in mail preview application.
|
|
142
|
+
|
|
119
143
|
## 🤖 AI Analysis
|
|
120
144
|
|
|
121
145
|
The dashboard includes AI-powered test analysis that provides:
|
|
@@ -19,6 +19,7 @@ export declare class PlaywrightPulseReporter implements Reporter {
|
|
|
19
19
|
private processStep;
|
|
20
20
|
onTestEnd(test: TestCase, result: PwTestResult): Promise<void>;
|
|
21
21
|
onError(error: any): void;
|
|
22
|
+
private _getEnvDetails;
|
|
22
23
|
private _writeShardResults;
|
|
23
24
|
private _mergeShardResults;
|
|
24
25
|
private _cleanupTemporaryFiles;
|
|
@@ -40,6 +40,7 @@ const path = __importStar(require("path"));
|
|
|
40
40
|
const crypto_1 = require("crypto");
|
|
41
41
|
const attachment_utils_1 = require("./attachment-utils"); // Use relative path
|
|
42
42
|
const ua_parser_js_1 = require("ua-parser-js"); // Added UAParser import
|
|
43
|
+
const os = __importStar(require("os"));
|
|
43
44
|
const convertStatus = (status, testCase) => {
|
|
44
45
|
if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
|
|
45
46
|
return "failed";
|
|
@@ -244,6 +245,15 @@ class PlaywrightPulseReporter {
|
|
|
244
245
|
});
|
|
245
246
|
}
|
|
246
247
|
const uniqueTestId = test.id;
|
|
248
|
+
// --- ADDED THIS SECTION for testData ---
|
|
249
|
+
const testSpecificData = {
|
|
250
|
+
workerId: result.workerIndex,
|
|
251
|
+
totalWorkers: this.config.workers,
|
|
252
|
+
configFile: this.config.configFile,
|
|
253
|
+
metadata: this.config.metadata
|
|
254
|
+
? JSON.stringify(this.config.metadata)
|
|
255
|
+
: undefined,
|
|
256
|
+
};
|
|
247
257
|
const pulseResult = {
|
|
248
258
|
id: uniqueTestId,
|
|
249
259
|
runId: "TBD",
|
|
@@ -265,6 +275,8 @@ class PlaywrightPulseReporter {
|
|
|
265
275
|
tracePath: undefined,
|
|
266
276
|
stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
|
|
267
277
|
stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
|
|
278
|
+
// --- ADDED THESE LINES from testSpecificData ---
|
|
279
|
+
...testSpecificData,
|
|
268
280
|
};
|
|
269
281
|
try {
|
|
270
282
|
(0, attachment_utils_1.attachFiles)(testIdForFiles, result, pulseResult, this.options);
|
|
@@ -289,6 +301,20 @@ class PlaywrightPulseReporter {
|
|
|
289
301
|
console.error(error.stack);
|
|
290
302
|
}
|
|
291
303
|
}
|
|
304
|
+
_getEnvDetails() {
|
|
305
|
+
return {
|
|
306
|
+
host: os.hostname(),
|
|
307
|
+
os: `${os.platform()} ${os.release()}`,
|
|
308
|
+
cpu: {
|
|
309
|
+
model: os.cpus()[0] ? os.cpus()[0].model : "N/A", // Handle cases with no CPU info
|
|
310
|
+
cores: os.cpus().length,
|
|
311
|
+
},
|
|
312
|
+
memory: `${(os.totalmem() / 1024 ** 3).toFixed(2)}GB`, // Total RAM in GB
|
|
313
|
+
node: process.version,
|
|
314
|
+
v8: process.versions.v8,
|
|
315
|
+
cwd: process.cwd(),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
292
318
|
async _writeShardResults() {
|
|
293
319
|
if (this.shardIndex === undefined) {
|
|
294
320
|
return;
|
|
@@ -383,6 +409,8 @@ class PlaywrightPulseReporter {
|
|
|
383
409
|
const runEndTime = Date.now();
|
|
384
410
|
const duration = runEndTime - this.runStartTime;
|
|
385
411
|
const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`; // Need not to change
|
|
412
|
+
// --- CALLING _getEnvDetails HERE ---
|
|
413
|
+
const environmentDetails = this._getEnvDetails();
|
|
386
414
|
const runData = {
|
|
387
415
|
id: runId,
|
|
388
416
|
timestamp: new Date(this.runStartTime),
|
|
@@ -391,10 +419,16 @@ class PlaywrightPulseReporter {
|
|
|
391
419
|
failed: 0,
|
|
392
420
|
skipped: 0,
|
|
393
421
|
duration,
|
|
422
|
+
// --- ADDED environmentDetails HERE ---
|
|
423
|
+
environment: environmentDetails,
|
|
394
424
|
};
|
|
395
425
|
let finalReport = undefined; // Initialize as undefined
|
|
396
426
|
if (this.isSharded) {
|
|
397
427
|
finalReport = await this._mergeShardResults(runData);
|
|
428
|
+
// Ensured environment details are on the final merged runData if not already
|
|
429
|
+
if (finalReport && finalReport.run && !finalReport.run.environment) {
|
|
430
|
+
finalReport.run.environment = environmentDetails;
|
|
431
|
+
}
|
|
398
432
|
}
|
|
399
433
|
else {
|
|
400
434
|
this.results.forEach((r) => (r.runId = runId));
|
|
@@ -441,6 +475,7 @@ PlaywrightPulseReporter: Run Finished
|
|
|
441
475
|
failed: 0,
|
|
442
476
|
skipped: 0,
|
|
443
477
|
duration: duration,
|
|
478
|
+
environment: environmentDetails,
|
|
444
479
|
},
|
|
445
480
|
results: [],
|
|
446
481
|
metadata: {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -36,6 +36,10 @@ export interface TestResult {
|
|
|
36
36
|
tracePath?: string;
|
|
37
37
|
stdout?: string[];
|
|
38
38
|
stderr?: string[];
|
|
39
|
+
workerId?: number;
|
|
40
|
+
totalWorkers?: number;
|
|
41
|
+
configFile?: string;
|
|
42
|
+
metadata?: string;
|
|
39
43
|
}
|
|
40
44
|
export interface TestRun {
|
|
41
45
|
id: string;
|
|
@@ -45,6 +49,7 @@ export interface TestRun {
|
|
|
45
49
|
failed: number;
|
|
46
50
|
skipped: number;
|
|
47
51
|
duration: number;
|
|
52
|
+
environment?: EnvDetails;
|
|
48
53
|
}
|
|
49
54
|
export interface TrendDataPoint {
|
|
50
55
|
date: string;
|
|
@@ -63,3 +68,15 @@ export interface PlaywrightPulseReporterOptions {
|
|
|
63
68
|
outputDir?: string;
|
|
64
69
|
base64Images?: boolean;
|
|
65
70
|
}
|
|
71
|
+
export interface EnvDetails {
|
|
72
|
+
host: string;
|
|
73
|
+
os: string;
|
|
74
|
+
cpu: {
|
|
75
|
+
model: string;
|
|
76
|
+
cores: number;
|
|
77
|
+
};
|
|
78
|
+
memory: string;
|
|
79
|
+
node: string;
|
|
80
|
+
v8: string;
|
|
81
|
+
cwd: string;
|
|
82
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arghajit/dummy",
|
|
3
3
|
"author": "Arghajit Singha",
|
|
4
|
-
"version": "0.1.0-beta-
|
|
4
|
+
"version": "0.1.0-beta-15",
|
|
5
5
|
"description": "A Playwright reporter and dashboard for visualizing test results.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"playwright",
|
|
@@ -55,13 +55,13 @@
|
|
|
55
55
|
"dotenv": "^16.5.0",
|
|
56
56
|
"highcharts": "^12.2.0",
|
|
57
57
|
"jsdom": "^26.1.0",
|
|
58
|
+
"lucide-react": "^0.475.0",
|
|
59
|
+
"node-fetch": "^3.3.2",
|
|
58
60
|
"nodemailer": "^7.0.3",
|
|
59
61
|
"patch-package": "^8.0.0",
|
|
60
62
|
"recharts": "^2.15.1",
|
|
61
63
|
"ua-parser-js": "^2.0.3",
|
|
62
|
-
"zod": "^3.24.2"
|
|
63
|
-
"lucide-react": "^0.475.0",
|
|
64
|
-
"node-fetch": "^3.3.2"
|
|
64
|
+
"zod": "^3.24.2"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@types/node": "^20",
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import { fork } from "child_process";
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
7
5
|
|
|
8
6
|
// Use dynamic import for chalk as it's ESM only
|
|
9
7
|
let chalk;
|
|
@@ -21,12 +19,10 @@ try {
|
|
|
21
19
|
};
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
// Default configuration
|
|
25
22
|
const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
26
23
|
const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
|
|
27
24
|
const MINIFIED_HTML_FILE = "pulse-email-summary.html"; // New minified report
|
|
28
25
|
|
|
29
|
-
// Helper functions
|
|
30
26
|
function sanitizeHTML(str) {
|
|
31
27
|
if (str === null || str === undefined) return "";
|
|
32
28
|
return String(str).replace(/[&<>"']/g, (match) => {
|
|
@@ -40,17 +36,93 @@ function sanitizeHTML(str) {
|
|
|
40
36
|
return replacements[match] || match;
|
|
41
37
|
});
|
|
42
38
|
}
|
|
43
|
-
|
|
44
39
|
function capitalize(str) {
|
|
45
40
|
if (!str) return "";
|
|
46
41
|
return str[0].toUpperCase() + str.slice(1).toLowerCase();
|
|
47
42
|
}
|
|
43
|
+
function formatDuration(ms, options = {}) {
|
|
44
|
+
const {
|
|
45
|
+
precision = 1,
|
|
46
|
+
invalidInputReturn = "N/A",
|
|
47
|
+
defaultForNullUndefinedNegative = null,
|
|
48
|
+
} = options;
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
const validPrecision = Math.max(0, Math.floor(precision));
|
|
51
|
+
const zeroWithPrecision = (0).toFixed(validPrecision) + "s";
|
|
52
|
+
const resolvedNullUndefNegReturn =
|
|
53
|
+
defaultForNullUndefinedNegative === null
|
|
54
|
+
? zeroWithPrecision
|
|
55
|
+
: defaultForNullUndefinedNegative;
|
|
56
|
+
|
|
57
|
+
if (ms === undefined || ms === null) {
|
|
58
|
+
return resolvedNullUndefNegReturn;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const numMs = Number(ms);
|
|
62
|
+
|
|
63
|
+
if (Number.isNaN(numMs) || !Number.isFinite(numMs)) {
|
|
64
|
+
return invalidInputReturn;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (numMs < 0) {
|
|
68
|
+
return resolvedNullUndefNegReturn;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (numMs === 0) {
|
|
72
|
+
return zeroWithPrecision;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const MS_PER_SECOND = 1000;
|
|
76
|
+
const SECONDS_PER_MINUTE = 60;
|
|
77
|
+
const MINUTES_PER_HOUR = 60;
|
|
78
|
+
const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
|
|
79
|
+
|
|
80
|
+
const totalRawSeconds = numMs / MS_PER_SECOND;
|
|
81
|
+
|
|
82
|
+
// Decision: Are we going to display hours or minutes?
|
|
83
|
+
// This happens if the duration is inherently >= 1 minute OR
|
|
84
|
+
// if it's < 1 minute but ceiling the seconds makes it >= 1 minute.
|
|
85
|
+
if (
|
|
86
|
+
totalRawSeconds < SECONDS_PER_MINUTE &&
|
|
87
|
+
Math.ceil(totalRawSeconds) < SECONDS_PER_MINUTE
|
|
88
|
+
) {
|
|
89
|
+
// Strictly seconds-only display, use precision.
|
|
90
|
+
return `${totalRawSeconds.toFixed(validPrecision)}s`;
|
|
91
|
+
} else {
|
|
92
|
+
// Display will include minutes and/or hours, or seconds round up to a minute.
|
|
93
|
+
// Seconds part should be an integer (ceiling).
|
|
94
|
+
// Round the total milliseconds UP to the nearest full second.
|
|
95
|
+
const totalMsRoundedUpToSecond =
|
|
96
|
+
Math.ceil(numMs / MS_PER_SECOND) * MS_PER_SECOND;
|
|
97
|
+
|
|
98
|
+
let remainingMs = totalMsRoundedUpToSecond;
|
|
99
|
+
|
|
100
|
+
const h = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_HOUR));
|
|
101
|
+
remainingMs %= MS_PER_SECOND * SECONDS_PER_HOUR;
|
|
102
|
+
|
|
103
|
+
const m = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_MINUTE));
|
|
104
|
+
remainingMs %= MS_PER_SECOND * SECONDS_PER_MINUTE;
|
|
105
|
+
|
|
106
|
+
const s = Math.floor(remainingMs / MS_PER_SECOND); // This will be an integer
|
|
53
107
|
|
|
108
|
+
const parts = [];
|
|
109
|
+
if (h > 0) {
|
|
110
|
+
parts.push(`${h}h`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Show minutes if:
|
|
114
|
+
// - hours are present (e.g., "1h 0m 5s")
|
|
115
|
+
// - OR minutes themselves are > 0 (e.g., "5m 10s")
|
|
116
|
+
// - OR the original duration was >= 1 minute (ensures "1m 0s" for 60000ms)
|
|
117
|
+
if (h > 0 || m > 0 || numMs >= MS_PER_SECOND * SECONDS_PER_MINUTE) {
|
|
118
|
+
parts.push(`${m}m`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
parts.push(`${s}s`);
|
|
122
|
+
|
|
123
|
+
return parts.join(" ");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
54
126
|
function formatDate(dateStrOrDate) {
|
|
55
127
|
if (!dateStrOrDate) return "N/A";
|
|
56
128
|
try {
|
|
@@ -69,7 +141,6 @@ function formatDate(dateStrOrDate) {
|
|
|
69
141
|
return "Invalid Date Format";
|
|
70
142
|
}
|
|
71
143
|
}
|
|
72
|
-
|
|
73
144
|
function getStatusClass(status) {
|
|
74
145
|
switch (String(status).toLowerCase()) {
|
|
75
146
|
case "passed":
|
|
@@ -82,7 +153,6 @@ function getStatusClass(status) {
|
|
|
82
153
|
return "status-unknown";
|
|
83
154
|
}
|
|
84
155
|
}
|
|
85
|
-
|
|
86
156
|
function getStatusIcon(status) {
|
|
87
157
|
switch (String(status).toLowerCase()) {
|
|
88
158
|
case "passed":
|
|
@@ -95,7 +165,6 @@ function getStatusIcon(status) {
|
|
|
95
165
|
return "❓";
|
|
96
166
|
}
|
|
97
167
|
}
|
|
98
|
-
|
|
99
168
|
function generateMinifiedHTML(reportData) {
|
|
100
169
|
const { run, results } = reportData;
|
|
101
170
|
const runSummary = run || {
|
|
@@ -108,9 +177,11 @@ function generateMinifiedHTML(reportData) {
|
|
|
108
177
|
};
|
|
109
178
|
|
|
110
179
|
const testsByBrowser = new Map();
|
|
180
|
+
const allBrowsers = new Set();
|
|
111
181
|
if (results && results.length > 0) {
|
|
112
182
|
results.forEach((test) => {
|
|
113
183
|
const browser = test.browser || "unknown";
|
|
184
|
+
allBrowsers.add(browser);
|
|
114
185
|
if (!testsByBrowser.has(browser)) {
|
|
115
186
|
testsByBrowser.set(browser, []);
|
|
116
187
|
}
|
|
@@ -126,7 +197,9 @@ function generateMinifiedHTML(reportData) {
|
|
|
126
197
|
let html = "";
|
|
127
198
|
testsByBrowser.forEach((tests, browser) => {
|
|
128
199
|
html += `
|
|
129
|
-
<div class="browser-section"
|
|
200
|
+
<div class="browser-section" data-browser-group="${sanitizeHTML(
|
|
201
|
+
browser.toLowerCase()
|
|
202
|
+
)}">
|
|
130
203
|
<h2 class="browser-title">${sanitizeHTML(capitalize(browser))}</h2>
|
|
131
204
|
<ul class="test-list">
|
|
132
205
|
`;
|
|
@@ -135,7 +208,12 @@ function generateMinifiedHTML(reportData) {
|
|
|
135
208
|
const testTitle =
|
|
136
209
|
testFileParts[testFileParts.length - 1] || "Unnamed Test";
|
|
137
210
|
html += `
|
|
138
|
-
<li class="test-item ${getStatusClass(test.status)}"
|
|
211
|
+
<li class="test-item ${getStatusClass(test.status)}"
|
|
212
|
+
data-test-name-min="${sanitizeHTML(testTitle.toLowerCase())}"
|
|
213
|
+
data-status-min="${sanitizeHTML(
|
|
214
|
+
String(test.status).toLowerCase()
|
|
215
|
+
)}"
|
|
216
|
+
data-browser-min="${sanitizeHTML(browser.toLowerCase())}">
|
|
139
217
|
<span class="test-status-icon">${getStatusIcon(
|
|
140
218
|
test.status
|
|
141
219
|
)}</span>
|
|
@@ -157,11 +235,13 @@ function generateMinifiedHTML(reportData) {
|
|
|
157
235
|
}
|
|
158
236
|
|
|
159
237
|
return `
|
|
160
|
-
<!DOCTYPE html>
|
|
161
|
-
<html lang="en">
|
|
238
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
239
|
+
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
|
162
240
|
<head>
|
|
163
|
-
<meta
|
|
241
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
164
242
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
243
|
+
<link rel="icon" type="image/png" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
|
|
244
|
+
<link rel="apple-touch-icon" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
|
|
165
245
|
<title>Playwright Pulse Summary Report</title>
|
|
166
246
|
<style>
|
|
167
247
|
:root {
|
|
@@ -272,6 +352,43 @@ function generateMinifiedHTML(reportData) {
|
|
|
272
352
|
padding-bottom: 10px;
|
|
273
353
|
border-bottom: 2px solid var(--secondary-color);
|
|
274
354
|
}
|
|
355
|
+
|
|
356
|
+
/* Filters Section */
|
|
357
|
+
.filters-section {
|
|
358
|
+
display: flex;
|
|
359
|
+
flex-wrap: wrap;
|
|
360
|
+
gap: 15px;
|
|
361
|
+
margin-bottom: 20px;
|
|
362
|
+
padding: 15px;
|
|
363
|
+
background-color: var(--light-gray-color);
|
|
364
|
+
border-radius: var(--border-radius);
|
|
365
|
+
border: 1px solid var(--border-color);
|
|
366
|
+
}
|
|
367
|
+
.filters-section input[type="text"],
|
|
368
|
+
.filters-section select {
|
|
369
|
+
padding: 8px 12px;
|
|
370
|
+
border: 1px solid var(--medium-gray-color);
|
|
371
|
+
border-radius: 4px;
|
|
372
|
+
font-size: 0.95em;
|
|
373
|
+
flex-grow: 1;
|
|
374
|
+
}
|
|
375
|
+
.filters-section select {
|
|
376
|
+
min-width: 150px;
|
|
377
|
+
}
|
|
378
|
+
.filters-section button {
|
|
379
|
+
padding: 8px 15px;
|
|
380
|
+
font-size: 0.95em;
|
|
381
|
+
background-color: var(--secondary-color);
|
|
382
|
+
color: white;
|
|
383
|
+
border: none;
|
|
384
|
+
border-radius: 4px;
|
|
385
|
+
cursor: pointer;
|
|
386
|
+
transition: background-color 0.2s ease;
|
|
387
|
+
}
|
|
388
|
+
.filters-section button:hover {
|
|
389
|
+
background-color: var(--primary-color);
|
|
390
|
+
}
|
|
391
|
+
|
|
275
392
|
.browser-section {
|
|
276
393
|
margin-bottom: 25px;
|
|
277
394
|
}
|
|
@@ -294,7 +411,7 @@ function generateMinifiedHTML(reportData) {
|
|
|
294
411
|
border: 1px solid var(--border-color);
|
|
295
412
|
border-radius: var(--border-radius);
|
|
296
413
|
background-color: #fff;
|
|
297
|
-
transition: background-color 0.2s ease;
|
|
414
|
+
transition: background-color 0.2s ease, display 0.3s ease-out;
|
|
298
415
|
}
|
|
299
416
|
.test-item:hover {
|
|
300
417
|
background-color: var(--light-gray-color);
|
|
@@ -353,10 +470,11 @@ function generateMinifiedHTML(reportData) {
|
|
|
353
470
|
.report-header { flex-direction: column; align-items: flex-start; gap: 10px; }
|
|
354
471
|
.report-header h1 { font-size: 1.5em; }
|
|
355
472
|
.run-info { text-align: left; }
|
|
356
|
-
.summary-stats { grid-template-columns: 1fr 1fr; }
|
|
473
|
+
.summary-stats { grid-template-columns: 1fr 1fr; }
|
|
474
|
+
.filters-section { flex-direction: column; }
|
|
357
475
|
}
|
|
358
476
|
@media (max-width: 480px) {
|
|
359
|
-
.summary-stats { grid-template-columns: 1fr; }
|
|
477
|
+
.summary-stats { grid-template-columns: 1fr; }
|
|
360
478
|
}
|
|
361
479
|
</style>
|
|
362
480
|
</head>
|
|
@@ -400,30 +518,121 @@ function generateMinifiedHTML(reportData) {
|
|
|
400
518
|
|
|
401
519
|
<section class="test-results-section">
|
|
402
520
|
<h1 class="section-title">Test Case Summary</h1>
|
|
521
|
+
|
|
522
|
+
<div class="filters-section">
|
|
523
|
+
<input type="text" id="filter-min-name" placeholder="Search by test name...">
|
|
524
|
+
<select id="filter-min-status">
|
|
525
|
+
<option value="">All Statuses</option>
|
|
526
|
+
<option value="passed">Passed</option>
|
|
527
|
+
<option value="failed">Failed</option>
|
|
528
|
+
<option value="skipped">Skipped</option>
|
|
529
|
+
<option value="unknown">Unknown</option>
|
|
530
|
+
</select>
|
|
531
|
+
<select id="filter-min-browser">
|
|
532
|
+
<option value="">All Browsers</option>
|
|
533
|
+
${Array.from(allBrowsers)
|
|
534
|
+
.map(
|
|
535
|
+
(browser) =>
|
|
536
|
+
`<option value="${sanitizeHTML(
|
|
537
|
+
browser.toLowerCase()
|
|
538
|
+
)}">${sanitizeHTML(capitalize(browser))}</option>`
|
|
539
|
+
)
|
|
540
|
+
.join("")}
|
|
541
|
+
</select>
|
|
542
|
+
<button id="clear-min-filters">Clear Filters</button>
|
|
543
|
+
</div>
|
|
544
|
+
|
|
403
545
|
${generateTestListHTML()}
|
|
404
546
|
</section>
|
|
405
547
|
|
|
406
548
|
<footer class="report-footer">
|
|
407
549
|
<div style="display: inline-flex; align-items: center; gap: 0.5rem;">
|
|
408
|
-
<span>Created
|
|
550
|
+
<span>Created for</span>
|
|
409
551
|
<a href="https://github.com/Arghajit47" target="_blank" rel="noopener noreferrer">
|
|
410
|
-
|
|
552
|
+
Pulse Email Report
|
|
411
553
|
</a>
|
|
412
554
|
</div>
|
|
413
555
|
<div style="margin-top: 0.3rem; font-size: 0.7rem;">Crafted with precision</div>
|
|
414
556
|
</footer>
|
|
415
557
|
</div>
|
|
416
558
|
<script>
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
559
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
560
|
+
const nameFilterMin = document.getElementById('filter-min-name');
|
|
561
|
+
const statusFilterMin = document.getElementById('filter-min-status');
|
|
562
|
+
const browserFilterMin = document.getElementById('filter-min-browser');
|
|
563
|
+
const clearMinFiltersBtn = document.getElementById('clear-min-filters');
|
|
564
|
+
const testItemsMin = document.querySelectorAll('.test-results-section .test-item');
|
|
565
|
+
const browserSections = document.querySelectorAll('.test-results-section .browser-section');
|
|
566
|
+
|
|
567
|
+
function filterMinifiedTests() {
|
|
568
|
+
const nameValue = nameFilterMin.value.toLowerCase();
|
|
569
|
+
const statusValue = statusFilterMin.value;
|
|
570
|
+
const browserValue = browserFilterMin.value;
|
|
571
|
+
let anyBrowserSectionVisible = false;
|
|
572
|
+
|
|
573
|
+
browserSections.forEach(section => {
|
|
574
|
+
let sectionHasVisibleTests = false;
|
|
575
|
+
const testsInThisSection = section.querySelectorAll('.test-item');
|
|
576
|
+
|
|
577
|
+
testsInThisSection.forEach(testItem => {
|
|
578
|
+
const testName = testItem.getAttribute('data-test-name-min');
|
|
579
|
+
const testStatus = testItem.getAttribute('data-status-min');
|
|
580
|
+
const testBrowser = testItem.getAttribute('data-browser-min');
|
|
581
|
+
|
|
582
|
+
const nameMatch = testName.includes(nameValue);
|
|
583
|
+
const statusMatch = !statusValue || testStatus === statusValue;
|
|
584
|
+
const browserMatch = !browserValue || testBrowser === browserValue;
|
|
585
|
+
|
|
586
|
+
if (nameMatch && statusMatch && browserMatch) {
|
|
587
|
+
testItem.style.display = 'flex';
|
|
588
|
+
sectionHasVisibleTests = true;
|
|
589
|
+
anyBrowserSectionVisible = true;
|
|
590
|
+
} else {
|
|
591
|
+
testItem.style.display = 'none';
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
// Hide browser section if no tests match OR if a specific browser is selected and it's not this one
|
|
595
|
+
if (!sectionHasVisibleTests || (browserValue && section.getAttribute('data-browser-group') !== browserValue)) {
|
|
596
|
+
section.style.display = 'none';
|
|
597
|
+
} else {
|
|
598
|
+
section.style.display = '';
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Show "no tests" message if all sections are hidden
|
|
603
|
+
const noTestsMessage = document.querySelector('.test-results-section .no-tests');
|
|
604
|
+
if (noTestsMessage) {
|
|
605
|
+
noTestsMessage.style.display = anyBrowserSectionVisible ? 'none' : 'block';
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (nameFilterMin) nameFilterMin.addEventListener('input', filterMinifiedTests);
|
|
611
|
+
if (statusFilterMin) statusFilterMin.addEventListener('change', filterMinifiedTests);
|
|
612
|
+
if (browserFilterMin) browserFilterMin.addEventListener('change', filterMinifiedTests);
|
|
613
|
+
|
|
614
|
+
if (clearMinFiltersBtn) {
|
|
615
|
+
clearMinFiltersBtn.addEventListener('click', () => {
|
|
616
|
+
nameFilterMin.value = '';
|
|
617
|
+
statusFilterMin.value = '';
|
|
618
|
+
browserFilterMin.value = '';
|
|
619
|
+
filterMinifiedTests();
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
// Initial filter call in case of pre-filled values (though unlikely here)
|
|
623
|
+
if (testItemsMin.length > 0) { // Only filter if there are items
|
|
624
|
+
filterMinifiedTests();
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Fallback helper functions (though ideally not needed client-side for this minified report)
|
|
420
629
|
if (typeof formatDuration === 'undefined') {
|
|
421
|
-
function formatDuration(ms) {
|
|
630
|
+
function formatDuration(ms) {
|
|
422
631
|
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
423
632
|
return (ms / 1000).toFixed(1) + "s";
|
|
424
633
|
}
|
|
425
634
|
}
|
|
426
|
-
if (typeof formatDate === 'undefined') {
|
|
635
|
+
if (typeof formatDate === 'undefined') {
|
|
427
636
|
function formatDate(dateStrOrDate) {
|
|
428
637
|
if (!dateStrOrDate) return "N/A";
|
|
429
638
|
try {
|
|
@@ -442,31 +651,6 @@ function generateMinifiedHTML(reportData) {
|
|
|
442
651
|
</html>
|
|
443
652
|
`;
|
|
444
653
|
}
|
|
445
|
-
|
|
446
|
-
async function runScript(scriptPath) {
|
|
447
|
-
return new Promise((resolve, reject) => {
|
|
448
|
-
const process = fork(scriptPath, [], {
|
|
449
|
-
stdio: "inherit",
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
process.on("error", (err) => {
|
|
453
|
-
console.error(chalk.red(`Failed to start script: ${scriptPath}`), err);
|
|
454
|
-
reject(err);
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
process.on("exit", (code) => {
|
|
458
|
-
if (code === 0) {
|
|
459
|
-
console.log(chalk.green(`Script ${scriptPath} finished successfully.`));
|
|
460
|
-
resolve();
|
|
461
|
-
} else {
|
|
462
|
-
const errorMessage = `Script ${scriptPath} exited with code ${code}.`;
|
|
463
|
-
console.error(chalk.red(errorMessage));
|
|
464
|
-
reject(new Error(errorMessage));
|
|
465
|
-
}
|
|
466
|
-
});
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
|
|
470
654
|
async function main() {
|
|
471
655
|
const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
472
656
|
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE);
|
|
@@ -521,7 +705,6 @@ async function main() {
|
|
|
521
705
|
process.exit(1);
|
|
522
706
|
}
|
|
523
707
|
}
|
|
524
|
-
|
|
525
708
|
main().catch((err) => {
|
|
526
709
|
console.error(
|
|
527
710
|
chalk.red.bold(`Unhandled error during script execution: ${err.message}`)
|