@arghajit/dummy 0.1.0-beta-11 → 0.1.0-beta-13

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
@@ -211,16 +211,6 @@ The dashboard includes AI-powered test analysis that provides:
211
211
 
212
212
  ## ![Features](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/structures.svg)
213
213
 
214
- ## 🎉 What's New in v0.2.1
215
-
216
- ### ✨ **Key Improvements**
217
-
218
- | Feature | Description |
219
- |---------|-------------|
220
- | **🎨 Refined UI** | Completely redesigned static HTML reports for better readability and navigation |
221
- | **📊 History Trends** | Visual analytics for:<br>• Test History for last 15 runs<br>• Test suite pass/fail rates<br>• Duration trends<br>• Individual test flakiness |
222
- | **🛠️ Project Fixes** | Corrected project name display in test suite components |
223
-
224
214
  ### 🚀 **Upgrade Now**
225
215
 
226
216
  ```bash
@@ -382,7 +382,7 @@ class PlaywrightPulseReporter {
382
382
  }
383
383
  const runEndTime = Date.now();
384
384
  const duration = runEndTime - this.runStartTime;
385
- const runId = `run-${this.runStartTime}-${(0, crypto_1.randomUUID)()}`;
385
+ const runId = `run-${this.runStartTime}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`; // Need not to change
386
386
  const runData = {
387
387
  id: runId,
388
388
  timestamp: new Date(this.runStartTime),
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-11",
4
+ "version": "0.1.0-beta-13",
5
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
6
  "keywords": [
7
7
  "playwright",
@@ -27,8 +27,9 @@
27
27
  "bin": {
28
28
  "generate-pulse-report": "./scripts/generate-static-report.mjs",
29
29
  "merge-pulse-report": "./scripts/merge-pulse-report.js",
30
- "send-email": "./scripts/sendReport.js",
31
- "generate-trend": "./scripts/generate-trend.mjs"
30
+ "send-email": "./scripts/sendReport.mjs",
31
+ "generate-trend": "./scripts/generate-trend.mjs",
32
+ "generate-email-report": "./scripts/generate-email-report.mjs"
32
33
  },
33
34
  "exports": {
34
35
  ".": {
@@ -42,7 +43,8 @@
42
43
  "prepublishOnly": "npm run build:reporter",
43
44
  "report:static": "node ./scripts/generate-static-report.mjs",
44
45
  "report:merge": "node ./scripts/merge-pulse-report.js",
45
- "report:email": "node ./scripts/sendReport.js"
46
+ "report:email": "node ./scripts/sendReport.mjs",
47
+ "report:minify": "node ./scripts/generate-email-report.mjs"
46
48
  },
47
49
  "dependencies": {
48
50
  "archiver": "^7.0.1",
@@ -58,7 +60,8 @@
58
60
  "recharts": "^2.15.1",
59
61
  "ua-parser-js": "^2.0.3",
60
62
  "zod": "^3.24.2",
61
- "lucide-react": "^0.475.0"
63
+ "lucide-react": "^0.475.0",
64
+ "node-fetch": "^3.3.2"
62
65
  },
63
66
  "devDependencies": {
64
67
  "@types/node": "^20",
@@ -0,0 +1,531 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as fs from "fs/promises";
4
+ import path from "path";
5
+ import { fork } from "child_process";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // Use dynamic import for chalk as it's ESM only
9
+ let chalk;
10
+ try {
11
+ chalk = (await import("chalk")).default;
12
+ } catch (e) {
13
+ console.warn("Chalk could not be imported. Using plain console logs.");
14
+ chalk = {
15
+ green: (text) => text,
16
+ red: (text) => text,
17
+ yellow: (text) => text,
18
+ blue: (text) => text,
19
+ bold: (text) => text,
20
+ gray: (text) => text,
21
+ };
22
+ }
23
+
24
+ // Default configuration
25
+ const DEFAULT_OUTPUT_DIR = "pulse-report";
26
+ const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
27
+ const MINIFIED_HTML_FILE = "pulse-email-summary.html"; // New minified report
28
+
29
+ // Helper functions
30
+ function sanitizeHTML(str) {
31
+ if (str === null || str === undefined) return "";
32
+ return String(str).replace(/[&<>"']/g, (match) => {
33
+ const replacements = {
34
+ "&": "&", // Changed to & for HTML context
35
+ "<": "<",
36
+ ">": ">",
37
+ '"': '"',
38
+ "'": "'",
39
+ };
40
+ return replacements[match] || match;
41
+ });
42
+ }
43
+
44
+ function capitalize(str) {
45
+ if (!str) return "";
46
+ return str[0].toUpperCase() + str.slice(1).toLowerCase();
47
+ }
48
+
49
+ function formatDuration(ms) {
50
+ if (ms === undefined || ms === null || ms < 0) return "0.0s";
51
+ return (ms / 1000).toFixed(1) + "s";
52
+ }
53
+
54
+ function formatDate(dateStrOrDate) {
55
+ if (!dateStrOrDate) return "N/A";
56
+ try {
57
+ const date = new Date(dateStrOrDate);
58
+ if (isNaN(date.getTime())) return "Invalid Date";
59
+ return (
60
+ date.toLocaleDateString(undefined, {
61
+ year: "2-digit",
62
+ month: "2-digit",
63
+ day: "2-digit",
64
+ }) +
65
+ " " +
66
+ date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
67
+ );
68
+ } catch (e) {
69
+ return "Invalid Date Format";
70
+ }
71
+ }
72
+
73
+ function getStatusClass(status) {
74
+ switch (String(status).toLowerCase()) {
75
+ case "passed":
76
+ return "status-passed";
77
+ case "failed":
78
+ return "status-failed";
79
+ case "skipped":
80
+ return "status-skipped";
81
+ default:
82
+ return "status-unknown";
83
+ }
84
+ }
85
+
86
+ function getStatusIcon(status) {
87
+ switch (String(status).toLowerCase()) {
88
+ case "passed":
89
+ return "✅";
90
+ case "failed":
91
+ return "❌";
92
+ case "skipped":
93
+ return "⏭️";
94
+ default:
95
+ return "❓";
96
+ }
97
+ }
98
+
99
+ function generateMinifiedHTML(reportData) {
100
+ const { run, results } = reportData;
101
+ const runSummary = run || {
102
+ totalTests: 0,
103
+ passed: 0,
104
+ failed: 0,
105
+ skipped: 0,
106
+ duration: 0,
107
+ timestamp: new Date().toISOString(),
108
+ };
109
+
110
+ const testsByBrowser = new Map();
111
+ if (results && results.length > 0) {
112
+ results.forEach((test) => {
113
+ const browser = test.browser || "unknown";
114
+ if (!testsByBrowser.has(browser)) {
115
+ testsByBrowser.set(browser, []);
116
+ }
117
+ testsByBrowser.get(browser).push(test);
118
+ });
119
+ }
120
+
121
+ function generateTestListHTML() {
122
+ if (testsByBrowser.size === 0) {
123
+ return '<p class="no-tests">No test results found in this run.</p>';
124
+ }
125
+
126
+ let html = "";
127
+ testsByBrowser.forEach((tests, browser) => {
128
+ html += `
129
+ <div class="browser-section">
130
+ <h2 class="browser-title">${sanitizeHTML(capitalize(browser))}</h2>
131
+ <ul class="test-list">
132
+ `;
133
+ tests.forEach((test) => {
134
+ const testFileParts = test.name.split(" > ");
135
+ const testTitle =
136
+ testFileParts[testFileParts.length - 1] || "Unnamed Test";
137
+ html += `
138
+ <li class="test-item ${getStatusClass(test.status)}">
139
+ <span class="test-status-icon">${getStatusIcon(
140
+ test.status
141
+ )}</span>
142
+ <span class="test-title-text" title="${sanitizeHTML(
143
+ test.name
144
+ )}">${sanitizeHTML(testTitle)}</span>
145
+ <span class="test-status-label">${String(
146
+ test.status
147
+ ).toUpperCase()}</span>
148
+ </li>
149
+ `;
150
+ });
151
+ html += `
152
+ </ul>
153
+ </div>
154
+ `;
155
+ });
156
+ return html;
157
+ }
158
+
159
+ return `
160
+ <!DOCTYPE html>
161
+ <html lang="en">
162
+ <head>
163
+ <meta charset="UTF-8">
164
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
165
+ <title>Playwright Pulse Summary Report</title>
166
+ <style>
167
+ :root {
168
+ --primary-color: #2c3e50; /* Dark Blue/Grey */
169
+ --secondary-color: #3498db; /* Bright Blue */
170
+ --success-color: #2ecc71; /* Green */
171
+ --danger-color: #e74c3c; /* Red */
172
+ --warning-color: #f39c12; /* Orange */
173
+ --light-gray-color: #ecf0f1; /* Light Grey */
174
+ --medium-gray-color: #bdc3c7; /* Medium Grey */
175
+ --dark-gray-color: #7f8c8d; /* Dark Grey */
176
+ --text-color: #34495e; /* Dark Grey/Blue for text */
177
+ --background-color: #f8f9fa;
178
+ --card-background-color: #ffffff;
179
+ --border-color: #dfe6e9;
180
+ --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
181
+ --border-radius: 6px;
182
+ --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
183
+ }
184
+ body {
185
+ font-family: var(--font-family);
186
+ margin: 0;
187
+ background-color: var(--background-color);
188
+ color: var(--text-color);
189
+ line-height: 1.6;
190
+ font-size: 16px;
191
+ padding: 20px;
192
+ }
193
+ .container {
194
+ max-width: 900px;
195
+ margin: 0 auto;
196
+ background-color: var(--card-background-color);
197
+ padding: 25px;
198
+ border-radius: var(--border-radius);
199
+ box-shadow: var(--box-shadow);
200
+ }
201
+ .report-header {
202
+ display: flex;
203
+ justify-content: space-between;
204
+ align-items: center;
205
+ padding-bottom: 20px;
206
+ border-bottom: 1px solid var(--border-color);
207
+ margin-bottom: 25px;
208
+ flex-wrap: wrap;
209
+ }
210
+ .report-header-title {
211
+ display: flex;
212
+ align-items: center;
213
+ gap: 12px;
214
+ }
215
+ .report-header h1 {
216
+ margin: 0;
217
+ font-size: 1.75em;
218
+ font-weight: 600;
219
+ color: var(--primary-color);
220
+ }
221
+ #report-logo {
222
+ height: 36px;
223
+ width: 36px;
224
+ }
225
+ .run-info {
226
+ font-size: 0.9em;
227
+ text-align: right;
228
+ color: var(--dark-gray-color);
229
+ }
230
+ .run-info strong {
231
+ color: var(--text-color);
232
+ }
233
+ .summary-stats {
234
+ display: grid;
235
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
236
+ gap: 20px;
237
+ margin-bottom: 30px;
238
+ }
239
+ .stat-card {
240
+ background-color: var(--card-background-color);
241
+ border: 1px solid var(--border-color);
242
+ border-left-width: 5px;
243
+ border-left-color: var(--primary-color);
244
+ border-radius: var(--border-radius);
245
+ padding: 18px;
246
+ text-align: center;
247
+ }
248
+ .stat-card h3 {
249
+ margin: 0 0 8px;
250
+ font-size: 1em;
251
+ font-weight: 500;
252
+ color: var(--dark-gray-color);
253
+ text-transform: uppercase;
254
+ }
255
+ .stat-card .value {
256
+ font-size: 2em;
257
+ font-weight: 700;
258
+ color: var(--primary-color);
259
+ }
260
+ .stat-card.passed { border-left-color: var(--success-color); }
261
+ .stat-card.passed .value { color: var(--success-color); }
262
+ .stat-card.failed { border-left-color: var(--danger-color); }
263
+ .stat-card.failed .value { color: var(--danger-color); }
264
+ .stat-card.skipped { border-left-color: var(--warning-color); }
265
+ .stat-card.skipped .value { color: var(--warning-color); }
266
+
267
+ .section-title {
268
+ font-size: 1.5em;
269
+ color: var(--primary-color);
270
+ margin-top: 30px;
271
+ margin-bottom: 15px;
272
+ padding-bottom: 10px;
273
+ border-bottom: 2px solid var(--secondary-color);
274
+ }
275
+ .browser-section {
276
+ margin-bottom: 25px;
277
+ }
278
+ .browser-title {
279
+ font-size: 1.25em;
280
+ color: var(--text-color);
281
+ margin-bottom: 10px;
282
+ padding: 8px 0;
283
+ border-bottom: 1px dashed var(--medium-gray-color);
284
+ }
285
+ .test-list {
286
+ list-style-type: none;
287
+ padding-left: 0;
288
+ }
289
+ .test-item {
290
+ display: flex;
291
+ align-items: center;
292
+ padding: 10px 12px;
293
+ margin-bottom: 8px;
294
+ border: 1px solid var(--border-color);
295
+ border-radius: var(--border-radius);
296
+ background-color: #fff;
297
+ transition: background-color 0.2s ease;
298
+ }
299
+ .test-item:hover {
300
+ background-color: var(--light-gray-color);
301
+ }
302
+ .test-status-icon {
303
+ font-size: 1.1em;
304
+ margin-right: 10px;
305
+ }
306
+ .test-title-text {
307
+ flex-grow: 1;
308
+ font-size: 0.95em;
309
+ }
310
+ .test-status-label {
311
+ font-size: 0.8em;
312
+ font-weight: 600;
313
+ padding: 3px 8px;
314
+ border-radius: 4px;
315
+ color: #fff;
316
+ margin-left: 10px;
317
+ min-width: 60px;
318
+ text-align: center;
319
+ }
320
+ .test-item.status-passed .test-status-label { background-color: var(--success-color); }
321
+ .test-item.status-failed .test-status-label { background-color: var(--danger-color); }
322
+ .test-item.status-skipped .test-status-label { background-color: var(--warning-color); }
323
+ .test-item.status-unknown .test-status-label { background-color: var(--dark-gray-color); }
324
+
325
+ .no-tests {
326
+ padding: 20px;
327
+ text-align: center;
328
+ color: var(--dark-gray-color);
329
+ background-color: var(--light-gray-color);
330
+ border-radius: var(--border-radius);
331
+ font-style: italic;
332
+ }
333
+ .report-footer {
334
+ padding: 15px 0;
335
+ margin-top: 30px;
336
+ border-top: 1px solid var(--border-color);
337
+ text-align: center;
338
+ font-size: 0.85em;
339
+ color: var(--dark-gray-color);
340
+ }
341
+ .report-footer a {
342
+ color: var(--secondary-color);
343
+ text-decoration: none;
344
+ font-weight: 600;
345
+ }
346
+ .report-footer a:hover {
347
+ text-decoration: underline;
348
+ }
349
+
350
+ @media (max-width: 768px) {
351
+ body { padding: 10px; font-size: 15px; }
352
+ .container { padding: 20px; }
353
+ .report-header { flex-direction: column; align-items: flex-start; gap: 10px; }
354
+ .report-header h1 { font-size: 1.5em; }
355
+ .run-info { text-align: left; }
356
+ .summary-stats { grid-template-columns: 1fr 1fr; } /* Two cards per row on smaller screens */
357
+ }
358
+ @media (max-width: 480px) {
359
+ .summary-stats { grid-template-columns: 1fr; } /* One card per row on very small screens */
360
+ }
361
+ </style>
362
+ </head>
363
+ <body>
364
+ <div class="container">
365
+ <header class="report-header">
366
+ <div class="report-header-title">
367
+ <img id="report-logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMNCA3bDggNSA4LTUtOC01eiIgZmlsbD0iIzNmNTFiNSIvPjxwYXRoIGQ9Ik0xMiA2TDQgMTFsOCA1IDgtNS04LTV6IiBmaWxsPSIjNDI4NWY0Ii8+PHBhdGggZD0iTTEyIDEwbC04IDUgOCA1IDgtNS04LTV6IiBmaWxsPSIjM2Q1NWI0Ii8+PC9zdmc+" alt="Report Logo">
368
+ <h1>Playwright Pulse Summary</h1>
369
+ </div>
370
+ <div class="run-info">
371
+ <strong>Run Date:</strong> ${formatDate(
372
+ runSummary.timestamp
373
+ )}<br>
374
+ <strong>Total Duration:</strong> ${formatDuration(
375
+ runSummary.duration
376
+ )}
377
+ </div>
378
+ </header>
379
+
380
+ <section class="summary-section">
381
+ <div class="summary-stats">
382
+ <div class="stat-card">
383
+ <h3>Total Tests</h3>
384
+ <div class="value">${runSummary.totalTests}</div>
385
+ </div>
386
+ <div class="stat-card passed">
387
+ <h3>Passed</h3>
388
+ <div class="value">${runSummary.passed}</div>
389
+ </div>
390
+ <div class="stat-card failed">
391
+ <h3>Failed</h3>
392
+ <div class="value">${runSummary.failed}</div>
393
+ </div>
394
+ <div class="stat-card skipped">
395
+ <h3>Skipped</h3>
396
+ <div class="value">${runSummary.skipped || 0}</div>
397
+ </div>
398
+ </div>
399
+ </section>
400
+
401
+ <section class="test-results-section">
402
+ <h1 class="section-title">Test Case Summary</h1>
403
+ ${generateTestListHTML()}
404
+ </section>
405
+
406
+ <footer class="report-footer">
407
+ <div style="display: inline-flex; align-items: center; gap: 0.5rem;">
408
+ <span>Created by</span>
409
+ <a href="https://github.com/Arghajit47" target="_blank" rel="noopener noreferrer">
410
+ Arghajit Singha
411
+ </a>
412
+ </div>
413
+ <div style="margin-top: 0.3rem; font-size: 0.7rem;">Crafted with precision</div>
414
+ </footer>
415
+ </div>
416
+ <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.
420
+ if (typeof formatDuration === 'undefined') {
421
+ function formatDuration(ms) { // Fallback, though should be pre-rendered
422
+ if (ms === undefined || ms === null || ms < 0) return "0.0s";
423
+ return (ms / 1000).toFixed(1) + "s";
424
+ }
425
+ }
426
+ if (typeof formatDate === 'undefined') { // Fallback
427
+ function formatDate(dateStrOrDate) {
428
+ if (!dateStrOrDate) return "N/A";
429
+ try {
430
+ const date = new Date(dateStrOrDate);
431
+ if (isNaN(date.getTime())) return "Invalid Date";
432
+ return (
433
+ date.toLocaleDateString(undefined, { year: "2-digit", month: "2-digit", day: "2-digit" }) +
434
+ " " +
435
+ date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
436
+ );
437
+ } catch (e) { return "Invalid Date Format"; }
438
+ }
439
+ }
440
+ </script>
441
+ </body>
442
+ </html>
443
+ `;
444
+ }
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
+ async function main() {
471
+ const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
472
+ const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE);
473
+ const minifiedReportHtmlPath = path.resolve(outputDir, MINIFIED_HTML_FILE); // Path for the new minified HTML
474
+
475
+ // Step 2: Load current run's data
476
+ let currentRunReportData;
477
+ try {
478
+ const jsonData = await fs.readFile(reportJsonPath, "utf-8");
479
+ currentRunReportData = JSON.parse(jsonData);
480
+ if (
481
+ !currentRunReportData ||
482
+ typeof currentRunReportData !== "object" ||
483
+ !currentRunReportData.results
484
+ ) {
485
+ throw new Error(
486
+ "Invalid report JSON structure. 'results' field is missing or invalid."
487
+ );
488
+ }
489
+ if (!Array.isArray(currentRunReportData.results)) {
490
+ currentRunReportData.results = [];
491
+ console.warn(
492
+ chalk.yellow(
493
+ "Warning: 'results' field in current run JSON was not an array. Treated as empty."
494
+ )
495
+ );
496
+ }
497
+ } catch (error) {
498
+ console.error(
499
+ chalk.red(
500
+ `Critical Error: Could not read or parse main report JSON at ${reportJsonPath}: ${error.message}`
501
+ )
502
+ );
503
+ process.exit(1);
504
+ }
505
+
506
+ // Step 3: Generate and write Minified HTML
507
+ try {
508
+ const htmlContent = generateMinifiedHTML(currentRunReportData); // Use the new generator
509
+ await fs.writeFile(minifiedReportHtmlPath, htmlContent, "utf-8");
510
+ console.log(
511
+ chalk.green.bold(
512
+ `🎉 Minified Pulse summary report generated successfully at: ${minifiedReportHtmlPath}`
513
+ )
514
+ );
515
+ console.log(chalk.gray(`(This HTML file is designed to be lightweight)`));
516
+ } catch (error) {
517
+ console.error(
518
+ chalk.red(`Error generating minified HTML report: ${error.message}`)
519
+ );
520
+ console.error(chalk.red(error.stack));
521
+ process.exit(1);
522
+ }
523
+ }
524
+
525
+ main().catch((err) => {
526
+ console.error(
527
+ chalk.red.bold(`Unhandled error during script execution: ${err.message}`)
528
+ );
529
+ console.error(err.stack);
530
+ process.exit(1);
531
+ });
@@ -1065,113 +1065,97 @@ function generateHTML(reportData, trendData = null) {
1065
1065
  : ""
1066
1066
  }
1067
1067
 
1068
- ${(() => {
1069
- if (
1070
- !test.screenshots ||
1071
- test.screenshots.length === 0
1072
- )
1073
- return "";
1074
-
1075
- // Define base output directory to resolve relative screenshot paths
1076
- // This assumes screenshot paths in your JSON are relative to DEFAULT_OUTPUT_DIR
1077
- const baseOutputDir = path.resolve(
1078
- process.cwd(),
1079
- DEFAULT_OUTPUT_DIR
1080
- );
1081
-
1082
- // Helper to escape HTML special characters (safer than the global sanitizeHTML)
1083
- const escapeHTML = (str) => {
1084
- if (str === null || str === undefined) return "";
1085
- return String(str).replace(
1086
- /[&<>"']/g,
1087
- (match) => {
1088
- const replacements = {
1089
- "&": "&",
1090
- "<": "<",
1091
- ">": ">",
1092
- '"': '"',
1093
- "'": "'",
1094
- };
1095
- return replacements[match] || match;
1096
- }
1097
- );
1098
- };
1099
-
1100
- const renderScreenshot = (
1101
- screenshotPathOrData,
1102
- index
1103
- ) => {
1104
- let base64ImageData = "";
1105
- const uniqueSuffix = `${Date.now()}-${index}-${Math.random()
1106
- .toString(36)
1107
- .substring(2, 7)}`;
1108
-
1109
- try {
1110
- if (
1111
- typeof screenshotPathOrData === "string" &&
1112
- !screenshotPathOrData.startsWith("data:image")
1113
- ) {
1114
- // It's likely a file path, try to read and convert
1115
- const imagePath = path.resolve(
1116
- baseOutputDir,
1117
- screenshotPathOrData
1118
- );
1119
-
1120
- if (fsExistsSync(imagePath)) {
1121
- // Use imported fsExistsSync
1122
- const imageBuffer = readFileSync(imagePath); // Use imported readFileSync
1123
- base64ImageData =
1124
- imageBuffer.toString("base64");
1125
- } else {
1126
- console.warn(
1127
- chalk.yellow(
1128
- `[Reporter] Screenshot file not found: ${imagePath}`
1129
- )
1130
- );
1131
- return `<div class="attachment-item error" style="padding:10px; color:red;">Screenshot not found: ${escapeHTML(
1132
- screenshotPathOrData
1133
- )}</div>`;
1134
- }
1135
- } else if (
1136
- typeof screenshotPathOrData === "string" &&
1137
- screenshotPathOrData.startsWith(
1138
- "data:image/png;base64,"
1139
- )
1140
- ) {
1141
- // It's already a data URI, extract base64 part
1142
- base64ImageData =
1143
- screenshotPathOrData.substring(
1144
- "data:image/png;base64,".length
1145
- );
1146
- } else if (
1147
- typeof screenshotPathOrData === "string"
1148
- ) {
1149
- // Assume it's raw Base64 data if it's a string but not a known path or full data URI
1150
- base64ImageData = screenshotPathOrData;
1151
- } else {
1152
- console.warn(
1153
- chalk.yellow(
1154
- `[Reporter] Invalid screenshot data type for item at index ${index}.`
1155
- )
1156
- );
1157
- return `<div class="attachment-item error" style="padding:10px; color:red;">Invalid screenshot data</div>`;
1158
- }
1159
-
1160
- if (!base64ImageData) {
1161
- // This case should ideally be caught above, but as a fallback:
1162
- console.warn(
1163
- chalk.yellow(
1164
- `[Reporter] Could not obtain base64 data for screenshot: ${escapeHTML(
1165
- String(screenshotPathOrData)
1166
- )}`
1167
- )
1168
- );
1169
- return `<div class="attachment-item error" style="padding:10px; color:red;">Error loading screenshot: ${escapeHTML(
1170
- String(screenshotPathOrData)
1171
- )}</div>`;
1172
- }
1173
-
1174
- return `
1068
+ ${(() => {
1069
+ if (!test.screenshots || test.screenshots.length === 0) return "";
1070
+
1071
+ // Define base output directory to resolve relative screenshot paths
1072
+ // This assumes screenshot paths in your JSON are relative to DEFAULT_OUTPUT_DIR
1073
+ const baseOutputDir = path.resolve(
1074
+ process.cwd(),
1075
+ DEFAULT_OUTPUT_DIR
1076
+ );
1077
+
1078
+ // Helper to escape HTML special characters (safer than the global sanitizeHTML)
1079
+ const escapeHTML = (str) => {
1080
+ if (str === null || str === undefined) return "";
1081
+ return String(str).replace(/[&<>"']/g, (match) => {
1082
+ const replacements = {
1083
+ "&": "&",
1084
+ "<": "<",
1085
+ ">": ">",
1086
+ '"': '"',
1087
+ "'": "'",
1088
+ };
1089
+ return replacements[match] || match;
1090
+ });
1091
+ };
1092
+
1093
+ const renderScreenshot = (screenshotPathOrData, index) => {
1094
+ let base64ImageData = "";
1095
+ const uniqueSuffix = `${Date.now()}-${index}-${Math.random()
1096
+ .toString(36)
1097
+ .substring(2, 7)}`;
1098
+
1099
+ try {
1100
+ if (
1101
+ typeof screenshotPathOrData === "string" &&
1102
+ !screenshotPathOrData.startsWith("data:image")
1103
+ ) {
1104
+ // It's likely a file path, try to read and convert
1105
+ const imagePath = path.resolve(
1106
+ baseOutputDir,
1107
+ screenshotPathOrData
1108
+ );
1109
+
1110
+ if (fsExistsSync(imagePath)) {
1111
+ // Use imported fsExistsSync
1112
+ const imageBuffer = readFileSync(imagePath); // Use imported readFileSync
1113
+ base64ImageData = imageBuffer.toString("base64");
1114
+ } else {
1115
+ console.warn(
1116
+ chalk.yellow(
1117
+ `[Reporter] Screenshot file not found: ${imagePath}`
1118
+ )
1119
+ );
1120
+ return `<div class="attachment-item error" style="padding:10px; color:red;">Screenshot not found: ${escapeHTML(
1121
+ screenshotPathOrData
1122
+ )}</div>`;
1123
+ }
1124
+ } else if (
1125
+ typeof screenshotPathOrData === "string" &&
1126
+ screenshotPathOrData.startsWith("data:image/png;base64,")
1127
+ ) {
1128
+ // It's already a data URI, extract base64 part
1129
+ base64ImageData = screenshotPathOrData.substring(
1130
+ "data:image/png;base64,".length
1131
+ );
1132
+ } else if (typeof screenshotPathOrData === "string") {
1133
+ // Assume it's raw Base64 data if it's a string but not a known path or full data URI
1134
+ base64ImageData = screenshotPathOrData;
1135
+ } else {
1136
+ console.warn(
1137
+ chalk.yellow(
1138
+ `[Reporter] Invalid screenshot data type for item at index ${index}.`
1139
+ )
1140
+ );
1141
+ return `<div class="attachment-item error" style="padding:10px; color:red;">Invalid screenshot data</div>`;
1142
+ }
1143
+
1144
+ if (!base64ImageData) {
1145
+ // This case should ideally be caught above, but as a fallback:
1146
+ console.warn(
1147
+ chalk.yellow(
1148
+ `[Reporter] Could not obtain base64 data for screenshot: ${escapeHTML(
1149
+ String(screenshotPathOrData)
1150
+ )}`
1151
+ )
1152
+ );
1153
+ return `<div class="attachment-item error" style="padding:10px; color:red;">Error loading screenshot: ${escapeHTML(
1154
+ String(screenshotPathOrData)
1155
+ )}</div>`;
1156
+ }
1157
+
1158
+ return `
1175
1159
  <div class="attachment-item">
1176
1160
  <img src="data:image/png;base64,${base64ImageData}"
1177
1161
  alt="Screenshot ${index + 1}"
@@ -1194,21 +1178,21 @@ function generateHTML(reportData, trendData = null) {
1194
1178
  </div>
1195
1179
  </div>
1196
1180
  </div>`;
1197
- } catch (e) {
1198
- console.error(
1199
- chalk.red(
1200
- `[Reporter] Error processing screenshot ${escapeHTML(
1201
- String(screenshotPathOrData)
1202
- )}: ${e.message}`
1203
- )
1204
- );
1205
- return `<div class="attachment-item error" style="padding:10px; color:red;">Failed to load screenshot: ${escapeHTML(
1206
- String(screenshotPathOrData)
1207
- )}</div>`;
1208
- }
1209
- }; // end of renderScreenshot
1210
-
1211
- return `
1181
+ } catch (e) {
1182
+ console.error(
1183
+ chalk.red(
1184
+ `[Reporter] Error processing screenshot ${escapeHTML(
1185
+ String(screenshotPathOrData)
1186
+ )}: ${e.message}`
1187
+ )
1188
+ );
1189
+ return `<div class="attachment-item error" style="padding:10px; color:red;">Failed to load screenshot: ${escapeHTML(
1190
+ String(screenshotPathOrData)
1191
+ )}</div>`;
1192
+ }
1193
+ }; // end of renderScreenshot
1194
+
1195
+ return `
1212
1196
  <div class="attachments-section">
1213
1197
  <h4>Screenshots (${test.screenshots.length})</h4>
1214
1198
  <div class="attachments-grid">
@@ -1216,7 +1200,7 @@ function generateHTML(reportData, trendData = null) {
1216
1200
  </div>
1217
1201
  </div>
1218
1202
  `;
1219
- })()}
1203
+ })()}
1220
1204
 
1221
1205
  ${
1222
1206
  test.videoPath
@@ -1,21 +1,42 @@
1
1
  #!/usr/bin/env node
2
- const nodemailer = require("nodemailer");
3
- const path = require("path");
4
- const archiver = require("archiver");
5
- const fileSystem = require("fs");
6
- const reportDir = "./pulse-report";
2
+ import nodemailer from "nodemailer"; // CHANGED
3
+ import path from "path"; // CHANGED (already was, but good to be explicit)
4
+ import archiver from "archiver"; // CHANGED
5
+ import {
6
+ createWriteStream,
7
+ readFileSync as fsReadFileSync, // Renamed to avoid conflict if fs from fs/promises is used
8
+ existsSync as fsExistsSync, // Renamed
9
+ } from "fs"; // CHANGED for specific functions
10
+ import { fileURLToPath } from "url";
11
+ import { fork } from "child_process"; // This was missing in your sendReport.js but present in generate-email-report.js and needed for runScript
12
+ import "dotenv/config"; // CHANGED for dotenv
13
+
14
+ // Import chalk using top-level await if your Node version supports it (14.8+)
15
+ // or keep the dynamic import if preferred, but ensure chalk is resolved before use.
16
+ let chalk;
17
+ try {
18
+ chalk = (await import("chalk")).default;
19
+ } catch (e) {
20
+ console.warn("Chalk could not be imported. Using plain console logs.");
21
+ chalk = {
22
+ green: (text) => text,
23
+ red: (text) => text,
24
+ yellow: (text) => text,
25
+ blue: (text) => text,
26
+ bold: (text) => text,
27
+ gray: (text) => text,
28
+ };
29
+ }
7
30
 
8
- require("dotenv").config();
31
+ const reportDir = "./pulse-report";
9
32
 
10
33
  let fetch;
11
- import("node-fetch")
12
- .then((module) => {
13
- fetch = module.default;
14
- })
15
- .catch((err) => {
16
- console.error("Failed to import node-fetch:", err);
17
- process.exit(1);
18
- });
34
+ // Ensure fetch is imported and available before it's used in fetchCredentials
35
+ // Using a top-level import is generally cleaner:
36
+ // import fetch from 'node-fetch';
37
+ // However, your dynamic import pattern is also fine if `fetch` is awaited properly.
38
+ // For simplicity, I'll assume the dynamic import is handled and awaited before fetchCredentials is called.
39
+ // The existing dynamic import for fetch is okay.
19
40
 
20
41
  let projectName;
21
42
 
@@ -26,11 +47,12 @@ function getUUID() {
26
47
  );
27
48
  console.log("Report path:", reportPath);
28
49
 
29
- if (!fileSystem.existsSync(reportPath)) {
50
+ if (!fsExistsSync(reportPath)) {
51
+ // CHANGED
30
52
  throw new Error("Pulse report file not found.");
31
53
  }
32
54
 
33
- const content = JSON.parse(fileSystem.readFileSync(reportPath, "utf-8"));
55
+ const content = JSON.parse(fsReadFileSync(reportPath, "utf-8")); // CHANGED
34
56
  const idString = content.run.id;
35
57
  const parts = idString.split("-");
36
58
  const uuid = parts.slice(-5).join("-");
@@ -49,25 +71,25 @@ const formatStartTime = (isoString) => {
49
71
  return date.toLocaleString(); // Default locale
50
72
  };
51
73
 
52
- // Generate test-data from allure report
53
74
  const getPulseReportSummary = () => {
54
75
  const reportPath = path.join(
55
76
  process.cwd(),
56
77
  `${reportDir}/playwright-pulse-report.json`
57
78
  );
58
79
 
59
- if (!fileSystem.existsSync(reportPath)) {
80
+ if (!fsExistsSync(reportPath)) {
81
+ // CHANGED
60
82
  throw new Error("Pulse report file not found.");
61
83
  }
62
84
 
63
- const content = JSON.parse(fileSystem.readFileSync(reportPath, "utf-8"));
85
+ const content = JSON.parse(fsReadFileSync(reportPath, "utf-8")); // CHANGED
64
86
  const run = content.run;
65
87
 
66
88
  const total = run.totalTests || 0;
67
89
  const passed = run.passed || 0;
68
90
  const failed = run.failed || 0;
69
91
  const skipped = run.skipped || 0;
70
- const duration = (run.duration || 0) / 1000; // Convert ms to seconds
92
+ const durationInMs = run.duration || 0; // Keep in ms for formatDuration
71
93
 
72
94
  const readableStartTime = new Date(run.timestamp).toLocaleString();
73
95
 
@@ -80,37 +102,35 @@ const getPulseReportSummary = () => {
80
102
  failedPercentage: total ? ((failed / total) * 100).toFixed(2) : "0.00",
81
103
  skippedPercentage: total ? ((skipped / total) * 100).toFixed(2) : "0.00",
82
104
  startTime: readableStartTime,
83
- duration: formatDuration(duration),
105
+ duration: formatDuration(durationInMs), // Pass ms to formatDuration
84
106
  };
85
107
  };
86
108
 
87
- // sleep function for javascript file
88
109
  const delay = (time) => new Promise((resolve) => setTimeout(resolve, time));
89
- // Function to zip the folder asynchronously using async/await
110
+
90
111
  const zipFolder = async (folderPath, zipPath) => {
91
112
  return new Promise((resolve, reject) => {
92
- const output = fileSystem.createWriteStream(zipPath); // Must use require("fs") directly here
93
- const archive = archiver("zip", { zlib: { level: 9 } });
113
+ const output = createWriteStream(zipPath); // CHANGED
114
+ const archiveInstance = archiver("zip", { zlib: { level: 9 } }); // Renamed to avoid conflict
94
115
 
95
116
  output.on("close", () => {
96
- console.log(`${archive.pointer()} total bytes`);
117
+ console.log(`${archiveInstance.pointer()} total bytes`);
97
118
  console.log("Folder has been zipped successfully.");
98
- resolve(); // Resolve the promise after zipping is complete
119
+ resolve();
99
120
  });
100
121
 
101
- archive.on("error", (err) => {
102
- reject(err); // Reject the promise in case of an error
122
+ archiveInstance.on("error", (err) => {
123
+ reject(err);
103
124
  });
104
125
 
105
- archive.pipe(output);
106
- archive.directory(folderPath, false); // Zip the folder without the parent folder
107
- archive.finalize(); // Finalize the archive
126
+ archiveInstance.pipe(output);
127
+ archiveInstance.directory(folderPath, false);
128
+ archiveInstance.finalize();
108
129
  });
109
130
  };
110
131
 
111
- // Function to convert JSON data to HTML table format
112
132
  const generateHtmlTable = (data) => {
113
- projectName = "Pulse Emailable Report";
133
+ projectName = "Pulse Emailable Report"; // Consider passing projectName as an arg or making it a const
114
134
  const stats = data;
115
135
  const total = stats.passed + stats.failed + stats.skipped;
116
136
  const passedTests = stats.passed;
@@ -120,7 +140,7 @@ const generateHtmlTable = (data) => {
120
140
  const skippedTests = stats.skipped;
121
141
  const skippedPercentage = stats.skippedPercentage;
122
142
  const startTime = stats.startTime;
123
- const durationSeconds = stats.duration;
143
+ const durationString = stats.duration; // Already formatted string
124
144
 
125
145
  return `
126
146
  <!DOCTYPE html>
@@ -161,8 +181,8 @@ const generateHtmlTable = (data) => {
161
181
  <td>${startTime}</td>
162
182
  </tr>
163
183
  <tr>
164
- <td>Test Run Duration (Seconds)</td>
165
- <td>${durationSeconds}</td>
184
+ <td>Test Run Duration</td>
185
+ <td>${durationString}</td>
166
186
  </tr>
167
187
  <tr>
168
188
  <td>Total Tests Count</td>
@@ -183,32 +203,65 @@ const generateHtmlTable = (data) => {
183
203
  </tbody>
184
204
  </table>
185
205
  <p>With regards,</p>
186
- <p>Networks QA Team</p>
206
+ <p>QA / SDET</p>
187
207
  </body>
188
208
  </html>
189
209
  `;
190
210
  };
191
211
 
192
- // Async function to send an email
212
+ const __filename = fileURLToPath(import.meta.url);
213
+ const __dirname = path.dirname(__filename);
214
+
215
+ // Ensure the name here matches the actual file name of input_file_0.js
216
+ // If input_file_0.js is indeed the script, use that name.
217
+ // Using .mjs extension explicitly tells Node to treat it as ESM.
218
+ const archiveRunScriptPath = path.resolve(
219
+ __dirname,
220
+ "generate-email-report.mjs" // Or input_file_0.mjs if you rename it, or input_file_0.js if you configure package.json
221
+ );
222
+
223
+ async function runScript(scriptPath) {
224
+ return new Promise((resolve, reject) => {
225
+ const childProcess = fork(scriptPath, [], {
226
+ // Renamed variable
227
+ stdio: "inherit",
228
+ });
229
+
230
+ childProcess.on("error", (err) => {
231
+ console.error(chalk.red(`Failed to start script: ${scriptPath}`), err);
232
+ reject(err);
233
+ });
234
+
235
+ childProcess.on("exit", (code) => {
236
+ if (code === 0) {
237
+ resolve();
238
+ } else {
239
+ const errorMessage = `Script ${scriptPath} exited with code ${code}.`;
240
+ console.error(chalk.red(errorMessage));
241
+ reject(new Error(errorMessage));
242
+ }
243
+ });
244
+ });
245
+ }
246
+
193
247
  const sendEmail = async (credentials) => {
248
+ await runScript(archiveRunScriptPath);
194
249
  try {
195
250
  console.log("Starting the sendEmail function...");
196
251
 
197
- // Configure nodemailer transporter
198
252
  const secureTransporter = nodemailer.createTransport({
199
253
  host: "smtp.gmail.com",
200
254
  port: 465,
201
- secure: true, // Use SSL/TLS
255
+ secure: true,
202
256
  auth: {
203
257
  user: credentials.username,
204
- pass: credentials.password, // Ensure you use app password or secured token
258
+ pass: credentials.password,
205
259
  },
206
260
  });
207
- // Generate HTML content for email
261
+
208
262
  const reportData = getPulseReportSummary();
209
263
  const htmlContent = generateHtmlTable(reportData);
210
264
 
211
- // Configure mail options
212
265
  const mailOptions = {
213
266
  from: credentials.username,
214
267
  to: [
@@ -217,18 +270,18 @@ const sendEmail = async (credentials) => {
217
270
  process.env.SENDER_EMAIL_3 || "",
218
271
  process.env.SENDER_EMAIL_4 || "",
219
272
  process.env.SENDER_EMAIL_5 || "",
220
- ],
273
+ ].filter((email) => email), // Filter out empty strings
221
274
  subject: "Pulse Report " + new Date().toLocaleString(),
222
275
  html: htmlContent,
223
276
  attachments: [
224
277
  {
225
278
  filename: `report.html`,
226
- path: `${reportDir}/playwright-pulse-static-report.html`, // Attach the zipped folder
279
+ // Make sure this path is correct and the file is generated by archiveRunScriptPath
280
+ path: path.join(reportDir, "pulse-email-summary.html"),
227
281
  },
228
282
  ],
229
283
  };
230
284
 
231
- // Send email
232
285
  const info = await secureTransporter.sendMail(mailOptions);
233
286
  console.log("Email sent: ", info.response);
234
287
  } catch (error) {
@@ -237,29 +290,39 @@ const sendEmail = async (credentials) => {
237
290
  };
238
291
 
239
292
  async function fetchCredentials(retries = 6) {
240
- const timeout = 10000; // 10 seconds timeout
293
+ // Ensure fetch is initialized from the dynamic import before calling this
294
+ if (!fetch) {
295
+ try {
296
+ fetch = (await import("node-fetch")).default;
297
+ } catch (err) {
298
+ console.error(
299
+ "Failed to import node-fetch dynamically for fetchCredentials:",
300
+ err
301
+ );
302
+ return null;
303
+ }
304
+ }
305
+
306
+ const timeout = 10000;
241
307
  const key = getUUID();
242
- // Validate API key exists before making any requests
308
+
243
309
  if (!key) {
244
310
  console.error(
245
- "🔴 Critical: API key not provided - please set EMAIL_KEY in your environment variables"
311
+ "🔴 Critical: API key (UUID from report) not found or invalid."
246
312
  );
247
- console.warn("🟠 Falling back to default credentials (if any)");
248
- return null; // Return null instead of throwing
313
+ return null;
249
314
  }
250
315
 
251
316
  for (let attempt = 1; attempt <= retries; attempt++) {
252
317
  try {
253
- console.log(`🟡 Attempt ${attempt} of ${retries}`);
318
+ console.log(`🟡 Attempt ${attempt} of ${retries} to fetch credentials`);
254
319
 
255
- // Create a timeout promise
256
320
  const timeoutPromise = new Promise((_, reject) => {
257
321
  setTimeout(() => {
258
322
  reject(new Error(`Request timed out after ${timeout}ms`));
259
323
  }, timeout);
260
324
  });
261
325
 
262
- // Create the fetch promise
263
326
  const fetchPromise = fetch(
264
327
  "https://test-dashboard-66zd.onrender.com/api/getcredentials",
265
328
  {
@@ -270,11 +333,9 @@ async function fetchCredentials(retries = 6) {
270
333
  }
271
334
  );
272
335
 
273
- // Race between fetch and timeout
274
336
  const response = await Promise.race([fetchPromise, timeoutPromise]);
275
337
 
276
338
  if (!response.ok) {
277
- // Handle specific HTTP errors with console messages only
278
339
  if (response.status === 401) {
279
340
  console.error("🔴 Invalid API key - authentication failed");
280
341
  } else if (response.status === 404) {
@@ -282,14 +343,17 @@ async function fetchCredentials(retries = 6) {
282
343
  } else {
283
344
  console.error(`🔴 Fetch failed with status: ${response.status}`);
284
345
  }
285
- continue; // Skip to next attempt instead of throwing
346
+ if (attempt < retries)
347
+ await new Promise((resolve) => setTimeout(resolve, 1000));
348
+ continue;
286
349
  }
287
350
 
288
351
  const data = await response.json();
289
352
 
290
- // Validate the response structure
291
353
  if (!data.username || !data.password) {
292
354
  console.error("🔴 Invalid credentials format received from API");
355
+ if (attempt < retries)
356
+ await new Promise((resolve) => setTimeout(resolve, 1000));
293
357
  continue;
294
358
  }
295
359
 
@@ -297,34 +361,37 @@ async function fetchCredentials(retries = 6) {
297
361
  return data;
298
362
  } catch (err) {
299
363
  console.error(`🔴 Attempt ${attempt} failed: ${err.message}`);
300
-
301
364
  if (attempt === retries) {
302
365
  console.error(
303
366
  `🔴 All ${retries} attempts failed. Last error: ${err.message}`
304
367
  );
305
- console.warn(
306
- "🟠 Proceeding without credentials - email sending will be skipped"
307
- );
308
368
  return null;
309
369
  }
310
-
311
370
  await new Promise((resolve) => setTimeout(resolve, 1000));
312
371
  }
313
372
  }
373
+ return null; // Should be unreachable if loop logic is correct
314
374
  }
315
375
 
316
- // Main function to zip the folder and send the email
317
376
  const main = async () => {
318
- await import("node-fetch").then((module) => {
319
- fetch = module.default;
320
- });
377
+ // Ensure fetch is initialized (dynamic import at top or here)
378
+ if (!fetch) {
379
+ try {
380
+ fetch = (await import("node-fetch")).default;
381
+ } catch (err) {
382
+ console.error("Failed to import node-fetch at start of main:", err);
383
+ process.exit(1); // Or handle error appropriately
384
+ }
385
+ }
386
+
321
387
  const credentials = await fetchCredentials();
322
388
  if (!credentials) {
323
- console.warn("Skipping email sending due to missing credentials");
324
- // Continue with pipeline without failing
389
+ console.warn(
390
+ "Skipping email sending due to missing or failed credential fetch"
391
+ );
325
392
  return;
326
393
  }
327
- await delay(10000);
394
+ // Removed await delay(10000); // If not strictly needed, remove it.
328
395
  try {
329
396
  await sendEmail(credentials);
330
397
  } catch (error) {