@arghajit/dummy 0.1.0-beta-12 → 0.1.0-beta-14

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,576 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as fs from "fs/promises";
4
+ import path from "path";
5
+
6
+ // Use dynamic import for chalk as it's ESM only
7
+ let chalk;
8
+ try {
9
+ chalk = (await import("chalk")).default;
10
+ } catch (e) {
11
+ console.warn("Chalk could not be imported. Using plain console logs.");
12
+ chalk = {
13
+ green: (text) => text,
14
+ red: (text) => text,
15
+ yellow: (text) => text,
16
+ blue: (text) => text,
17
+ bold: (text) => text,
18
+ gray: (text) => text,
19
+ };
20
+ }
21
+
22
+ const DEFAULT_OUTPUT_DIR = "pulse-report";
23
+ const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
24
+ const MINIFIED_HTML_FILE = "pulse-email-summary.html"; // New minified report
25
+
26
+ function sanitizeHTML(str) {
27
+ if (str === null || str === undefined) return "";
28
+ return String(str).replace(/[&<>"']/g, (match) => {
29
+ const replacements = {
30
+ "&": "&", // Changed to & for HTML context
31
+ "<": "<",
32
+ ">": ">",
33
+ '"': '"',
34
+ "'": "'",
35
+ };
36
+ return replacements[match] || match;
37
+ });
38
+ }
39
+ function capitalize(str) {
40
+ if (!str) return "";
41
+ return str[0].toUpperCase() + str.slice(1).toLowerCase();
42
+ }
43
+ function formatDuration(ms, options = {}) {
44
+ const {
45
+ precision = 1,
46
+ invalidInputReturn = "N/A",
47
+ defaultForNullUndefinedNegative = null,
48
+ } = options;
49
+
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
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
+ }
126
+ function formatDate(dateStrOrDate) {
127
+ if (!dateStrOrDate) return "N/A";
128
+ try {
129
+ const date = new Date(dateStrOrDate);
130
+ if (isNaN(date.getTime())) return "Invalid Date";
131
+ return (
132
+ date.toLocaleDateString(undefined, {
133
+ year: "2-digit",
134
+ month: "2-digit",
135
+ day: "2-digit",
136
+ }) +
137
+ " " +
138
+ date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
139
+ );
140
+ } catch (e) {
141
+ return "Invalid Date Format";
142
+ }
143
+ }
144
+ function getStatusClass(status) {
145
+ switch (String(status).toLowerCase()) {
146
+ case "passed":
147
+ return "status-passed";
148
+ case "failed":
149
+ return "status-failed";
150
+ case "skipped":
151
+ return "status-skipped";
152
+ default:
153
+ return "status-unknown";
154
+ }
155
+ }
156
+ function getStatusIcon(status) {
157
+ switch (String(status).toLowerCase()) {
158
+ case "passed":
159
+ return "✅";
160
+ case "failed":
161
+ return "❌";
162
+ case "skipped":
163
+ return "⏭️";
164
+ default:
165
+ return "❓";
166
+ }
167
+ }
168
+ function generateMinifiedHTML(reportData) {
169
+ const { run, results } = reportData;
170
+ const runSummary = run || {
171
+ totalTests: 0,
172
+ passed: 0,
173
+ failed: 0,
174
+ skipped: 0,
175
+ duration: 0,
176
+ timestamp: new Date().toISOString(),
177
+ };
178
+
179
+ const testsByBrowser = new Map();
180
+ if (results && results.length > 0) {
181
+ results.forEach((test) => {
182
+ const browser = test.browser || "unknown";
183
+ if (!testsByBrowser.has(browser)) {
184
+ testsByBrowser.set(browser, []);
185
+ }
186
+ testsByBrowser.get(browser).push(test);
187
+ });
188
+ }
189
+
190
+ function generateTestListHTML() {
191
+ if (testsByBrowser.size === 0) {
192
+ return '<p class="no-tests">No test results found in this run.</p>';
193
+ }
194
+
195
+ let html = "";
196
+ testsByBrowser.forEach((tests, browser) => {
197
+ html += `
198
+ <div class="browser-section">
199
+ <h2 class="browser-title">${sanitizeHTML(capitalize(browser))}</h2>
200
+ <ul class="test-list">
201
+ `;
202
+ tests.forEach((test) => {
203
+ const testFileParts = test.name.split(" > ");
204
+ const testTitle =
205
+ testFileParts[testFileParts.length - 1] || "Unnamed Test";
206
+ html += `
207
+ <li class="test-item ${getStatusClass(test.status)}">
208
+ <span class="test-status-icon">${getStatusIcon(
209
+ test.status
210
+ )}</span>
211
+ <span class="test-title-text" title="${sanitizeHTML(
212
+ test.name
213
+ )}">${sanitizeHTML(testTitle)}</span>
214
+ <span class="test-status-label">${String(
215
+ test.status
216
+ ).toUpperCase()}</span>
217
+ </li>
218
+ `;
219
+ });
220
+ html += `
221
+ </ul>
222
+ </div>
223
+ `;
224
+ });
225
+ return html;
226
+ }
227
+
228
+ return `
229
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
230
+ <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
231
+ <head>
232
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
233
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
234
+ <link rel="icon" type="image/png" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
235
+ <link rel="apple-touch-icon" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
236
+ <title>Playwright Pulse Summary Report</title>
237
+ <style>
238
+ :root {
239
+ --primary-color: #2c3e50; /* Dark Blue/Grey */
240
+ --secondary-color: #3498db; /* Bright Blue */
241
+ --success-color: #2ecc71; /* Green */
242
+ --danger-color: #e74c3c; /* Red */
243
+ --warning-color: #f39c12; /* Orange */
244
+ --light-gray-color: #ecf0f1; /* Light Grey */
245
+ --medium-gray-color: #bdc3c7; /* Medium Grey */
246
+ --dark-gray-color: #7f8c8d; /* Dark Grey */
247
+ --text-color: #34495e; /* Dark Grey/Blue for text */
248
+ --background-color: #f8f9fa;
249
+ --card-background-color: #ffffff;
250
+ --border-color: #dfe6e9;
251
+ --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
252
+ --border-radius: 6px;
253
+ --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
254
+ }
255
+ body {
256
+ font-family: var(--font-family);
257
+ margin: 0;
258
+ background-color: var(--background-color);
259
+ color: var(--text-color);
260
+ line-height: 1.6;
261
+ font-size: 16px;
262
+ padding: 20px;
263
+ }
264
+ .container {
265
+ max-width: 900px;
266
+ margin: 0 auto;
267
+ background-color: var(--card-background-color);
268
+ padding: 25px;
269
+ border-radius: var(--border-radius);
270
+ box-shadow: var(--box-shadow);
271
+ }
272
+ .report-header {
273
+ display: flex;
274
+ justify-content: space-between;
275
+ align-items: center;
276
+ padding-bottom: 20px;
277
+ border-bottom: 1px solid var(--border-color);
278
+ margin-bottom: 25px;
279
+ flex-wrap: wrap;
280
+ }
281
+ .report-header-title {
282
+ display: flex;
283
+ align-items: center;
284
+ gap: 12px;
285
+ }
286
+ .report-header h1 {
287
+ margin: 0;
288
+ font-size: 1.75em;
289
+ font-weight: 600;
290
+ color: var(--primary-color);
291
+ }
292
+ #report-logo {
293
+ height: 36px;
294
+ width: 36px;
295
+ }
296
+ .run-info {
297
+ font-size: 0.9em;
298
+ text-align: right;
299
+ color: var(--dark-gray-color);
300
+ }
301
+ .run-info strong {
302
+ color: var(--text-color);
303
+ }
304
+ .summary-stats {
305
+ display: grid;
306
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
307
+ gap: 20px;
308
+ margin-bottom: 30px;
309
+ }
310
+ .stat-card {
311
+ background-color: var(--card-background-color);
312
+ border: 1px solid var(--border-color);
313
+ border-left-width: 5px;
314
+ border-left-color: var(--primary-color);
315
+ border-radius: var(--border-radius);
316
+ padding: 18px;
317
+ text-align: center;
318
+ }
319
+ .stat-card h3 {
320
+ margin: 0 0 8px;
321
+ font-size: 1em;
322
+ font-weight: 500;
323
+ color: var(--dark-gray-color);
324
+ text-transform: uppercase;
325
+ }
326
+ .stat-card .value {
327
+ font-size: 2em;
328
+ font-weight: 700;
329
+ color: var(--primary-color);
330
+ }
331
+ .stat-card.passed { border-left-color: var(--success-color); }
332
+ .stat-card.passed .value { color: var(--success-color); }
333
+ .stat-card.failed { border-left-color: var(--danger-color); }
334
+ .stat-card.failed .value { color: var(--danger-color); }
335
+ .stat-card.skipped { border-left-color: var(--warning-color); }
336
+ .stat-card.skipped .value { color: var(--warning-color); }
337
+
338
+ .section-title {
339
+ font-size: 1.5em;
340
+ color: var(--primary-color);
341
+ margin-top: 30px;
342
+ margin-bottom: 15px;
343
+ padding-bottom: 10px;
344
+ border-bottom: 2px solid var(--secondary-color);
345
+ }
346
+ .browser-section {
347
+ margin-bottom: 25px;
348
+ }
349
+ .browser-title {
350
+ font-size: 1.25em;
351
+ color: var(--text-color);
352
+ margin-bottom: 10px;
353
+ padding: 8px 0;
354
+ border-bottom: 1px dashed var(--medium-gray-color);
355
+ }
356
+ .test-list {
357
+ list-style-type: none;
358
+ padding-left: 0;
359
+ }
360
+ .test-item {
361
+ display: flex;
362
+ align-items: center;
363
+ padding: 10px 12px;
364
+ margin-bottom: 8px;
365
+ border: 1px solid var(--border-color);
366
+ border-radius: var(--border-radius);
367
+ background-color: #fff;
368
+ transition: background-color 0.2s ease;
369
+ }
370
+ .test-item:hover {
371
+ background-color: var(--light-gray-color);
372
+ }
373
+ .test-status-icon {
374
+ font-size: 1.1em;
375
+ margin-right: 10px;
376
+ }
377
+ .test-title-text {
378
+ flex-grow: 1;
379
+ font-size: 0.95em;
380
+ }
381
+ .test-status-label {
382
+ font-size: 0.8em;
383
+ font-weight: 600;
384
+ padding: 3px 8px;
385
+ border-radius: 4px;
386
+ color: #fff;
387
+ margin-left: 10px;
388
+ min-width: 60px;
389
+ text-align: center;
390
+ }
391
+ .test-item.status-passed .test-status-label { background-color: var(--success-color); }
392
+ .test-item.status-failed .test-status-label { background-color: var(--danger-color); }
393
+ .test-item.status-skipped .test-status-label { background-color: var(--warning-color); }
394
+ .test-item.status-unknown .test-status-label { background-color: var(--dark-gray-color); }
395
+
396
+ .no-tests {
397
+ padding: 20px;
398
+ text-align: center;
399
+ color: var(--dark-gray-color);
400
+ background-color: var(--light-gray-color);
401
+ border-radius: var(--border-radius);
402
+ font-style: italic;
403
+ }
404
+ .report-footer {
405
+ padding: 15px 0;
406
+ margin-top: 30px;
407
+ border-top: 1px solid var(--border-color);
408
+ text-align: center;
409
+ font-size: 0.85em;
410
+ color: var(--dark-gray-color);
411
+ }
412
+ .report-footer a {
413
+ color: var(--secondary-color);
414
+ text-decoration: none;
415
+ font-weight: 600;
416
+ }
417
+ .report-footer a:hover {
418
+ text-decoration: underline;
419
+ }
420
+
421
+ @media (max-width: 768px) {
422
+ body { padding: 10px; font-size: 15px; }
423
+ .container { padding: 20px; }
424
+ .report-header { flex-direction: column; align-items: flex-start; gap: 10px; }
425
+ .report-header h1 { font-size: 1.5em; }
426
+ .run-info { text-align: left; }
427
+ .summary-stats { grid-template-columns: 1fr 1fr; } /* Two cards per row on smaller screens */
428
+ }
429
+ @media (max-width: 480px) {
430
+ .summary-stats { grid-template-columns: 1fr; } /* One card per row on very small screens */
431
+ }
432
+ </style>
433
+ </head>
434
+ <body>
435
+ <div class="container">
436
+ <header class="report-header">
437
+ <div class="report-header-title">
438
+ <img id="report-logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMNCA3bDggNSA4LTUtOC01eiIgZmlsbD0iIzNmNTFiNSIvPjxwYXRoIGQ9Ik0xMiA2TDQgMTFsOCA1IDgtNS04LTV6IiBmaWxsPSIjNDI4NWY0Ii8+PHBhdGggZD0iTTEyIDEwbC04IDUgOCA1IDgtNS04LTV6IiBmaWxsPSIjM2Q1NWI0Ii8+PC9zdmc+" alt="Report Logo">
439
+ <h1>Playwright Pulse Summary</h1>
440
+ </div>
441
+ <div class="run-info">
442
+ <strong>Run Date:</strong> ${formatDate(
443
+ runSummary.timestamp
444
+ )}<br>
445
+ <strong>Total Duration:</strong> ${formatDuration(
446
+ runSummary.duration
447
+ )}
448
+ </div>
449
+ </header>
450
+
451
+ <section class="summary-section">
452
+ <div class="summary-stats">
453
+ <div class="stat-card">
454
+ <h3>Total Tests</h3>
455
+ <div class="value">${runSummary.totalTests}</div>
456
+ </div>
457
+ <div class="stat-card passed">
458
+ <h3>Passed</h3>
459
+ <div class="value">${runSummary.passed}</div>
460
+ </div>
461
+ <div class="stat-card failed">
462
+ <h3>Failed</h3>
463
+ <div class="value">${runSummary.failed}</div>
464
+ </div>
465
+ <div class="stat-card skipped">
466
+ <h3>Skipped</h3>
467
+ <div class="value">${runSummary.skipped || 0}</div>
468
+ </div>
469
+ </div>
470
+ </section>
471
+
472
+ <section class="test-results-section">
473
+ <h1 class="section-title">Test Case Summary</h1>
474
+ ${generateTestListHTML()}
475
+ </section>
476
+
477
+ <footer class="report-footer">
478
+ <div style="display: inline-flex; align-items: center; gap: 0.5rem;">
479
+ <span>Created for</span>
480
+ <a href="https://github.com/Arghajit47" target="_blank" rel="noopener noreferrer">
481
+ Pulse Email Report
482
+ </a>
483
+ </div>
484
+ <div style="margin-top: 0.3rem; font-size: 0.7rem;">Crafted with precision</div>
485
+ </footer>
486
+ </div>
487
+ <script>
488
+ // Global helper functions needed by the template (if any complex ones were used)
489
+ // For this minified version, formatDuration and formatDate are primarily used during HTML generation server-side.
490
+ // No client-side interactivity scripts are needed for this simple report.
491
+ if (typeof formatDuration === 'undefined') {
492
+ function formatDuration(ms) { // Fallback, though should be pre-rendered
493
+ if (ms === undefined || ms === null || ms < 0) return "0.0s";
494
+ return (ms / 1000).toFixed(1) + "s";
495
+ }
496
+ }
497
+ if (typeof formatDate === 'undefined') { // Fallback
498
+ function formatDate(dateStrOrDate) {
499
+ if (!dateStrOrDate) return "N/A";
500
+ try {
501
+ const date = new Date(dateStrOrDate);
502
+ if (isNaN(date.getTime())) return "Invalid Date";
503
+ return (
504
+ date.toLocaleDateString(undefined, { year: "2-digit", month: "2-digit", day: "2-digit" }) +
505
+ " " +
506
+ date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
507
+ );
508
+ } catch (e) { return "Invalid Date Format"; }
509
+ }
510
+ }
511
+ </script>
512
+ </body>
513
+ </html>
514
+ `;
515
+ }
516
+ async function main() {
517
+ const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
518
+ const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE);
519
+ const minifiedReportHtmlPath = path.resolve(outputDir, MINIFIED_HTML_FILE); // Path for the new minified HTML
520
+
521
+ // Step 2: Load current run's data
522
+ let currentRunReportData;
523
+ try {
524
+ const jsonData = await fs.readFile(reportJsonPath, "utf-8");
525
+ currentRunReportData = JSON.parse(jsonData);
526
+ if (
527
+ !currentRunReportData ||
528
+ typeof currentRunReportData !== "object" ||
529
+ !currentRunReportData.results
530
+ ) {
531
+ throw new Error(
532
+ "Invalid report JSON structure. 'results' field is missing or invalid."
533
+ );
534
+ }
535
+ if (!Array.isArray(currentRunReportData.results)) {
536
+ currentRunReportData.results = [];
537
+ console.warn(
538
+ chalk.yellow(
539
+ "Warning: 'results' field in current run JSON was not an array. Treated as empty."
540
+ )
541
+ );
542
+ }
543
+ } catch (error) {
544
+ console.error(
545
+ chalk.red(
546
+ `Critical Error: Could not read or parse main report JSON at ${reportJsonPath}: ${error.message}`
547
+ )
548
+ );
549
+ process.exit(1);
550
+ }
551
+
552
+ // Step 3: Generate and write Minified HTML
553
+ try {
554
+ const htmlContent = generateMinifiedHTML(currentRunReportData); // Use the new generator
555
+ await fs.writeFile(minifiedReportHtmlPath, htmlContent, "utf-8");
556
+ console.log(
557
+ chalk.green.bold(
558
+ `🎉 Minified Pulse summary report generated successfully at: ${minifiedReportHtmlPath}`
559
+ )
560
+ );
561
+ console.log(chalk.gray(`(This HTML file is designed to be lightweight)`));
562
+ } catch (error) {
563
+ console.error(
564
+ chalk.red(`Error generating minified HTML report: ${error.message}`)
565
+ );
566
+ console.error(chalk.red(error.stack));
567
+ process.exit(1);
568
+ }
569
+ }
570
+ main().catch((err) => {
571
+ console.error(
572
+ chalk.red.bold(`Unhandled error during script execution: ${err.message}`)
573
+ );
574
+ console.error(err.stack);
575
+ process.exit(1);
576
+ });