@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 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;"> <a href="https://postimg.cc/180cym6c" target="_blank"> <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html.png" alt="Dashboard Overview" width="300"/> <p align="center"><strong>Dashboard Overview</strong></p> </a> <a href="https://postimg.cc/V5TFRHmM" target="_blank"> <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html-1.png" alt="Test Details" width="300"/> <p align="center"><strong>Test Details</strong></p> </a> <a href="https://postimg.cc/XXTwFGkk" target="_blank"> <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html-2.png" alt="Filter View" width="300"/> <p align="center"><strong>Filter View</strong></p> </a> </div>
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/127-0-0-1-5500-pulse-report-output-playwright-pulse-static-report-html-i-Phone-14-Pro-Max.png" alt="Mobile Overview" width="300"/>
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/127-0-0-1-5500-pulse-report-output-playwright-pulse-static-report-html-i-Phone-14-Pro-Max-1.png" alt="Test Details" width="300"/>
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
- [![Email Report](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Email-report.jpg)](https://postimg.cc/DmCPgtqh)
54
+ [![Email Report Template](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Email-report-mobile-template.jpeg)](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//Email-report-mobile-template.jpeg)
55
+
56
+ [![Email Report](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//pulse-email-summary.html.png)](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-reporter@latest --save-dev
76
+ npm install @arghajit/playwright-pulse-report@latest --save-dev
55
77
  # or
56
- yarn add @arghajit/playwright-pulse-reporter@latest --dev
78
+ yarn add @arghajit/playwright-pulse-report@latest --dev
57
79
  # or
58
- pnpm add @arghajit/playwright-pulse-reporter@latest --save-dev
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-reporter', {
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: {
@@ -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-13",
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
- function formatDuration(ms) {
50
- if (ms === undefined || ms === null || ms < 0) return "0.0s";
51
- return (ms / 1000).toFixed(1) + "s";
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 charset="UTF-8">
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; } /* Two cards per row on smaller screens */
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; } /* One card per row on very small screens */
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 by</span>
550
+ <span>Created for</span>
409
551
  <a href="https://github.com/Arghajit47" target="_blank" rel="noopener noreferrer">
410
- Arghajit Singha
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
- // Global helper functions needed by the template (if any complex ones were used)
418
- // For this minified version, formatDuration and formatDate are primarily used during HTML generation server-side.
419
- // No client-side interactivity scripts are needed for this simple report.
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) { // Fallback, though should be pre-rendered
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') { // Fallback
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}`)