@arghajit/playwright-pulse-report 0.3.2 → 0.3.4

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.
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ // src/utils/compression-utils.ts
3
+ /**
4
+ * Compression utilities for images
5
+ * Uses sharp for image compression (works cross-platform with no external dependencies)
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.compressImage = compressImage;
42
+ exports.compressAttachment = compressAttachment;
43
+ const fs = __importStar(require("fs/promises"));
44
+ const path = __importStar(require("path"));
45
+ /**
46
+ * Compress an image file in-place
47
+ * @param filePath - Absolute path to the image file
48
+ * @param options - Compression options
49
+ */
50
+ async function compressImage(filePath, options = {}) {
51
+ try {
52
+ const sharp = require('sharp');
53
+ const quality = options.quality || 75;
54
+ const ext = path.extname(filePath).toLowerCase();
55
+ // Read original file
56
+ const imageBuffer = await fs.readFile(filePath);
57
+ let compressedBuffer;
58
+ if (ext === '.png') {
59
+ // Compress PNG
60
+ compressedBuffer = await sharp(imageBuffer)
61
+ .png({ quality, compressionLevel: 9 })
62
+ .toBuffer();
63
+ }
64
+ else if (ext === '.jpg' || ext === '.jpeg') {
65
+ // Compress JPEG
66
+ compressedBuffer = await sharp(imageBuffer)
67
+ .jpeg({ quality, mozjpeg: true })
68
+ .toBuffer();
69
+ }
70
+ else if (ext === '.webp') {
71
+ // Compress WebP
72
+ compressedBuffer = await sharp(imageBuffer)
73
+ .webp({ quality })
74
+ .toBuffer();
75
+ }
76
+ else if (ext === '.gif') {
77
+ // Compress GIF
78
+ compressedBuffer = await sharp(imageBuffer)
79
+ .gif()
80
+ .toBuffer();
81
+ }
82
+ else if (ext === '.tiff' || ext === '.tif') {
83
+ // Compress TIFF
84
+ compressedBuffer = await sharp(imageBuffer)
85
+ .tiff({ quality, compression: 'lzw' })
86
+ .toBuffer();
87
+ }
88
+ else {
89
+ // Unsupported format, skip compression
90
+ return;
91
+ }
92
+ // Only overwrite if compression actually reduced size
93
+ if (compressedBuffer.length < imageBuffer.length) {
94
+ await fs.writeFile(filePath, compressedBuffer);
95
+ }
96
+ }
97
+ catch (error) {
98
+ // File remains unchanged
99
+ }
100
+ }
101
+ /**
102
+ * Compress an attachment file (auto-detects type)
103
+ * Note: Only compresses images. Videos are already compressed by Playwright.
104
+ * @param filePath - Absolute path to the file
105
+ * @param contentType - MIME content type
106
+ */
107
+ async function compressAttachment(filePath, contentType) {
108
+ if (contentType.startsWith('image/')) {
109
+ await compressImage(filePath, { quality: 75 });
110
+ }
111
+ // Videos are skipped - already compressed by Playwright as WebM
112
+ }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@arghajit/playwright-pulse-report",
3
3
  "author": "Arghajit Singha",
4
- "version": "0.3.2",
4
+ "version": "0.3.4",
5
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
- "homepage": "https://playwright-pulse-report.netlify.app/",
6
+ "homepage": "https://arghajit47.github.io/playwright-pulse/",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/Arghajit47/playwright-pulse"
@@ -34,12 +34,14 @@
34
34
  ],
35
35
  "license": "MIT",
36
36
  "bin": {
37
+ "logo": "node scripts/terminal-logo.mjs",
37
38
  "generate-pulse-report": "scripts/generate-static-report.mjs",
38
39
  "generate-report": "scripts/generate-report.mjs",
39
40
  "merge-pulse-report": "scripts/merge-pulse-report.js",
40
41
  "send-email": "scripts/sendReport.mjs",
41
42
  "generate-trend": "scripts/generate-trend.mjs",
42
- "generate-email-report": "scripts/generate-email-report.mjs"
43
+ "generate-email-report": "scripts/generate-email-report.mjs",
44
+ "pulse-logo": "scripts/terminal-logo.mjs"
43
45
  },
44
46
  "exports": {
45
47
  ".": {
@@ -71,7 +73,7 @@
71
73
  "node-fetch": "^3.3.2",
72
74
  "nodemailer": "^7.0.3",
73
75
  "patch-package": "^8.0.0",
74
- "recharts": "^2.15.1",
76
+ "sharp": "^0.33.5",
75
77
  "ua-parser-js": "^1.0.41",
76
78
  "zod": "^3.24.2"
77
79
  },
@@ -88,6 +90,10 @@
88
90
  "@playwright/test": ">=1.40.0"
89
91
  },
90
92
  "overrides": {
91
- "glob": "^13.0.0"
93
+ "brace-expansion": "^2.0.2",
94
+ "@isaacs/brace-expansion": "^5.0.1",
95
+ "glob": "^13.0.0",
96
+ "minimatch": "^10.1.2",
97
+ "lodash": "^4.17.23"
92
98
  }
93
- }
99
+ }
@@ -3,6 +3,7 @@
3
3
  import * as fs from "fs/promises";
4
4
  import path from "path";
5
5
  import { getOutputDir } from "./config-reader.mjs";
6
+ import { animate } from "./terminal-logo.mjs";
6
7
 
7
8
  // Use dynamic import for chalk as it's ESM only
8
9
  let chalk;
@@ -159,6 +160,8 @@ function getStatusClass(status) {
159
160
  return "status-failed";
160
161
  case "skipped":
161
162
  return "status-skipped";
163
+ case "flaky":
164
+ return "status-flaky";
162
165
  default:
163
166
  return "status-unknown";
164
167
  }
@@ -171,6 +174,8 @@ function getStatusIcon(status) {
171
174
  return "❌";
172
175
  case "skipped":
173
176
  return "⏭️";
177
+ case "flaky":
178
+ return "⚠";
174
179
  default:
175
180
  return "❓";
176
181
  }
@@ -241,6 +246,20 @@ function generateMinifiedHTML(reportData) {
241
246
  severity
242
247
  )}; font-size: 0.8em; font-weight: 600; padding: 3px 8px; border-radius: 4px; color: #fff; margin-left: 10px; white-space: nowrap;">${severity}</span>`;
243
248
 
249
+ // --- NEW: Retry Count Badge ---
250
+ const unsuccessfulRetries = (test.retryHistory || []).filter(attempt =>
251
+ attempt.status === 'failed' || attempt.status === 'timedout' || attempt.status === 'flaky'
252
+ );
253
+ const retryCountBadge = (unsuccessfulRetries.length > 0)
254
+ ? `<span style="background-color: #f59e0b; border: 1px solid #d97706; font-size: 0.8em; font-weight: 700; padding: 4px 10px; border-radius: 50px; color: #fff; margin-left: 10px; white-space: nowrap; display: inline-flex; align-items: center; gap: 4px;">
255
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle;">
256
+ <path d="M1 4v6h6"/>
257
+ <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
258
+ </svg>
259
+ Retry Count: ${unsuccessfulRetries.length}
260
+ </span>`
261
+ : '';
262
+
244
263
  // --- NEW: Tags Logic ---
245
264
  const tagsBadges = (test.tags || [])
246
265
  .map(
@@ -265,6 +284,7 @@ function generateMinifiedHTML(reportData) {
265
284
  test.name
266
285
  )}">${sanitizeHTML(testTitle)}</span>
267
286
 
287
+ ${retryCountBadge}
268
288
  ${severityBadge}
269
289
  ${tagsBadges}
270
290
  </li>
@@ -297,6 +317,7 @@ function generateMinifiedHTML(reportData) {
297
317
  --light-gray-color: #ecf0f1; /* Light Grey */
298
318
  --medium-gray-color: #bdc3c7; /* Medium Grey */
299
319
  --dark-gray-color: #7f8c8d; /* Dark Grey */
320
+ --flaky-color: #00ccd3; /* Cyan/Teal for Flaky */
300
321
  --text-color: #34495e; /* Dark Grey/Blue for text */
301
322
  --background-color: #f8f9fa;
302
323
  --card-background-color: #ffffff;
@@ -387,6 +408,8 @@ function generateMinifiedHTML(reportData) {
387
408
  .stat-card.failed .value { color: var(--danger-color); }
388
409
  .stat-card.skipped { border-left-color: var(--warning-color); }
389
410
  .stat-card.skipped .value { color: var(--warning-color); }
411
+ .stat-card.flaky { border-left-color: var(--flaky-color); }
412
+ .stat-card.flaky .value { color: var(--flaky-color); }
390
413
 
391
414
  .section-title {
392
415
  font-size: 1.5em;
@@ -481,6 +504,7 @@ function generateMinifiedHTML(reportData) {
481
504
  .test-item.status-passed .test-status-label { background-color: var(--success-color); }
482
505
  .test-item.status-failed .test-status-label { background-color: var(--danger-color); }
483
506
  .test-item.status-skipped .test-status-label { background-color: var(--warning-color); }
507
+ .test-item.status-flaky .test-status-label { background-color: var(--flaky-color); }
484
508
  .test-item.status-unknown .test-status-label { background-color: var(--dark-gray-color); }
485
509
 
486
510
  .no-tests {
@@ -531,10 +555,10 @@ function generateMinifiedHTML(reportData) {
531
555
  </div>
532
556
  <div class="run-info">
533
557
  <strong>Run Date:</strong> ${formatDate(
534
- runSummary.timestamp
558
+ runSummary.timestamp,
535
559
  )}<br>
536
560
  <strong>Total Duration:</strong> ${formatDuration(
537
- runSummary.duration
561
+ runSummary.duration,
538
562
  )}
539
563
  </div>
540
564
  </header>
@@ -557,6 +581,10 @@ function generateMinifiedHTML(reportData) {
557
581
  <h3>Skipped</h3>
558
582
  <div class="value">${runSummary.skipped || 0}</div>
559
583
  </div>
584
+ <div class="stat-card flaky">
585
+ <h3>Flaky</h3>
586
+ <div class="value">${runSummary.flaky || 0}</div>
587
+ </div>
560
588
  </div>
561
589
  </section>
562
590
 
@@ -582,11 +610,11 @@ function generateMinifiedHTML(reportData) {
582
610
  <div class="filters-section">
583
611
  <input type="text" id="filter-min-name" placeholder="Search by test name...">
584
612
  <select id="filter-min-status">
585
- <option value="">All Statuses</option>
586
- <option value="passed">Passed</option>
587
- <option value="failed">Failed</option>
588
- <option value="skipped">Skipped</option>
589
- <option value="unknown">Unknown</option>
613
+ <option value="">All Status</option>
614
+ <option value="passed">✅ Passed</option>
615
+ <option value="failed">❌ Failed</option>
616
+ <option value="skipped">⏭️ Skipped</option>
617
+ <option value="flaky">⚠ Flaky</option>
590
618
  </select>
591
619
  <select id="filter-min-browser">
592
620
  <option value="">All Browsers</option>
@@ -594,8 +622,8 @@ function generateMinifiedHTML(reportData) {
594
622
  .map(
595
623
  (browser) =>
596
624
  `<option value="${sanitizeHTML(
597
- browser.toLowerCase()
598
- )}">${sanitizeHTML(capitalize(browser))}</option>`
625
+ browser.toLowerCase(),
626
+ )}">${sanitizeHTML(capitalize(browser))}</option>`,
599
627
  )
600
628
  .join("")}
601
629
  </select>
@@ -608,7 +636,7 @@ function generateMinifiedHTML(reportData) {
608
636
  <footer class="report-footer">
609
637
  <div style="display: inline-flex; align-items: center; gap: 0.5rem;">
610
638
  <span>Created for</span>
611
- <a href="https://playwright-pulse-report.netlify.app/" target="_blank" rel="noopener noreferrer">
639
+ <a href="https://arghajit47.github.io/playwright-pulse/" target="_blank" rel="noopener noreferrer">
612
640
  Pulse Email Report
613
641
  </a>
614
642
  </div>
@@ -712,6 +740,8 @@ function generateMinifiedHTML(reportData) {
712
740
  `;
713
741
  }
714
742
  async function main() {
743
+ await animate();
744
+
715
745
  const outputDir = await getOutputDir(customOutputDir);
716
746
  const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE);
717
747
  const minifiedReportHtmlPath = path.resolve(outputDir, MINIFIED_HTML_FILE); // Path for the new minified HTML
@@ -763,8 +793,8 @@ async function main() {
763
793
  await fs.writeFile(minifiedReportHtmlPath, htmlContent, "utf-8");
764
794
  console.log(
765
795
  chalk.green.bold(
766
- `🎉 Minified Pulse summary report generated successfully at: ${minifiedReportHtmlPath}`
767
- )
796
+ `Minified Pulse summary report generated successfully at: ${minifiedReportHtmlPath}`,
797
+ ),
768
798
  );
769
799
  console.log(chalk.gray(`(This HTML file is designed to be lightweight)`));
770
800
  } catch (error) {