@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.
- package/LICENSE +21 -0
- package/README.md +98 -88
- package/dist/reporter/playwright-pulse-reporter.d.ts +1 -0
- package/dist/reporter/playwright-pulse-reporter.js +85 -25
- package/dist/types/index.d.ts +8 -2
- package/dist/utils/compression-utils.d.ts +19 -0
- package/dist/utils/compression-utils.js +112 -0
- package/package.json +12 -6
- package/scripts/generate-email-report.mjs +42 -12
- package/scripts/generate-report.mjs +2367 -661
- package/scripts/generate-static-report.mjs +4566 -1065
- package/scripts/generate-trend.mjs +0 -0
- package/scripts/merge-pulse-report.js +160 -36
- package/scripts/sendReport.mjs +153 -65
- package/scripts/terminal-logo.mjs +51 -0
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -26
- package/dist/playwright-pulse-reporter.d.ts +0 -26
- package/dist/playwright-pulse-reporter.js +0 -304
- package/dist/reporter/lib/report-types.d.ts +0 -8
- package/dist/reporter/lib/report-types.js +0 -2
- package/dist/reporter/reporter/playwright-pulse-reporter.d.ts +0 -1
- package/dist/reporter/reporter/playwright-pulse-reporter.js +0 -398
- package/dist/reporter/tsconfig.reporter.tsbuildinfo +0 -1
- package/dist/reporter/types/index.d.ts +0 -52
- package/dist/reporter/types/index.js +0 -2
|
@@ -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.
|
|
4
|
+
"version": "0.3.4",
|
|
5
5
|
"description": "A Playwright reporter and dashboard for visualizing test results.",
|
|
6
|
-
"homepage": "https://playwright-pulse
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
586
|
-
<option value="passed"
|
|
587
|
-
<option value="failed"
|
|
588
|
-
<option value="skipped"
|
|
589
|
-
<option value="
|
|
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
|
|
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
|
-
|
|
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) {
|