@arghajit/dummy 0.3.14 → 0.3.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.
@@ -14,9 +14,27 @@ for (let i = 0; i < args.length; i++) {
14
14
 
15
15
  const OUTPUT_FILE = "playwright-pulse-report.json";
16
16
 
17
+ /**
18
+ * Securely resolves the report directory.
19
+ * Prevents Path Traversal by ensuring the output directory
20
+ * is contained within the current working directory.
21
+ */
17
22
  async function getReportDir() {
18
23
  if (customOutputDir) {
19
- return path.resolve(process.cwd(), customOutputDir);
24
+ // 1. Resolve the absolute path
25
+ const resolvedPath = path.resolve(process.cwd(), customOutputDir);
26
+
27
+ // 2. Security Check: Prevent Path Traversal
28
+ // We ensure the resolved path starts with the project root (process.cwd())
29
+ // This blocks inputs like "../../" that try to escape the project.
30
+ if (!resolvedPath.startsWith(process.cwd())) {
31
+ console.error(
32
+ "⛔ Security Error: Custom output directory must be within the current project root.",
33
+ );
34
+ process.exit(1);
35
+ }
36
+
37
+ return resolvedPath;
20
38
  }
21
39
 
22
40
  try {
@@ -28,11 +46,16 @@ async function getReportDir() {
28
46
  }
29
47
 
30
48
  function getReportFiles(dir) {
49
+ // Security Note: 'dir' here is now guaranteed to be safe/sanitized by getReportDir
50
+ if (!fs.existsSync(dir)) {
51
+ return [];
52
+ }
53
+
31
54
  return fs
32
55
  .readdirSync(dir)
33
56
  .filter(
34
57
  (file) =>
35
- file.startsWith("playwright-pulse-report-") && file.endsWith(".json")
58
+ file.startsWith("playwright-pulse-report-") && file.endsWith(".json"),
36
59
  );
37
60
  }
38
61
 
@@ -51,29 +74,36 @@ function mergeReports(files, reportDir) {
51
74
  let latestGeneratedAt = "";
52
75
 
53
76
  for (const file of files) {
77
+ // Security Note: 'file' comes from readdirSync (safe), reportDir is sanitized.
54
78
  const filePath = path.join(reportDir, file);
55
- const json = JSON.parse(fs.readFileSync(filePath, "utf-8"));
56
-
57
- const run = json.run || {};
58
- combinedRun.totalTests += run.totalTests || 0;
59
- combinedRun.passed += run.passed || 0;
60
- combinedRun.failed += run.failed || 0;
61
- combinedRun.skipped += run.skipped || 0;
62
- combinedRun.duration += run.duration || 0;
63
- combinedRun.environment = run.environment;
64
-
65
- if (json.results) {
66
- combinedResults.push(...json.results);
67
- }
68
79
 
69
- if (run.timestamp > latestTimestamp) latestTimestamp = run.timestamp;
70
- if (json.metadata?.generatedAt > latestGeneratedAt)
71
- latestGeneratedAt = json.metadata.generatedAt;
80
+ try {
81
+ const fileContent = fs.readFileSync(filePath, "utf-8");
82
+ const json = JSON.parse(fileContent);
83
+
84
+ const run = json.run || {};
85
+ combinedRun.totalTests += run.totalTests || 0;
86
+ combinedRun.passed += run.passed || 0;
87
+ combinedRun.failed += run.failed || 0;
88
+ combinedRun.skipped += run.skipped || 0;
89
+ combinedRun.duration += run.duration || 0;
90
+ combinedRun.environment = run.environment;
91
+
92
+ if (json.results) {
93
+ combinedResults.push(...json.results);
94
+ }
95
+
96
+ if (run.timestamp > latestTimestamp) latestTimestamp = run.timestamp;
97
+ if (json.metadata?.generatedAt > latestGeneratedAt)
98
+ latestGeneratedAt = json.metadata.generatedAt;
99
+ } catch (e) {
100
+ console.warn(`Warning: Failed to process ${file}: ${e.message}`);
101
+ }
72
102
  }
73
103
 
74
104
  const finalJson = {
75
105
  run: {
76
- id: `merged-${Date.now()}`,
106
+ id: `merged-${Date.now()}-581d5ad8-ce75-4ca5-94a6-ed29c466c815`,
77
107
  timestamp: latestTimestamp,
78
108
  ...combinedRun,
79
109
  },
@@ -106,9 +136,10 @@ function mergeReports(files, reportDir) {
106
136
 
107
137
  const merged = mergeReports(reportFiles, REPORT_DIR);
108
138
 
139
+ // Security Note: REPORT_DIR is sanitized, so writeFileSync is safe.
109
140
  fs.writeFileSync(
110
141
  path.join(REPORT_DIR, OUTPUT_FILE),
111
- JSON.stringify(merged, null, 2)
142
+ JSON.stringify(merged, null, 2),
112
143
  );
113
144
  console.log(`✅ Merged report saved as ${OUTPUT_FILE}`);
114
- })();
145
+ })();
@@ -144,71 +144,156 @@ const generateHtmlTable = (data) => {
144
144
  const startTime = stats.startTime;
145
145
  const durationString = stats.duration; // Already formatted string
146
146
 
147
- return `
148
- <!DOCTYPE html>
149
- <html lang="en">
150
- <head>
151
- <meta charset="UTF-8">
152
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
153
- <title>Test Stats Report</title>
154
- <style>
155
- table {
156
- width: 100%;
157
- border-collapse: collapse;
158
- }
159
- table, th, td {
160
- border: 1px solid black;
161
- }
162
- th, td {
163
- padding: 8px;
164
- text-align: left;
165
- }
166
- th {
167
- background-color: #f2f2f2;
168
- }
169
- </style>
170
- </head>
171
- <body>
172
- <h1>${projectName} Statistics</h1>
173
- <table>
174
- <thead>
175
- <tr>
176
- <th>Metric</th>
177
- <th>Value</th>
178
- </tr>
179
- </thead>
180
- <tbody>
181
- <tr>
182
- <td>Test Start Time</td>
183
- <td>${startTime}</td>
184
- </tr>
185
- <tr>
186
- <td>Test Run Duration</td>
187
- <td>${durationString}</td>
188
- </tr>
189
- <tr>
190
- <td>Total Tests Count</td>
191
- <td>${total}</td>
192
- </tr>
193
- <tr>
194
- <td>Tests Passed</td>
195
- <td>${passedTests} (${passedPercentage}%)</td>
196
- </tr>
197
- <tr>
198
- <td>Skipped Tests</td>
199
- <td>${skippedTests} (${skippedPercentage}%)</td>
200
- </tr>
201
- <tr>
202
- <td>Test Failed</td>
203
- <td>${failedTests} (${failedPercentage}%)</td>
204
- </tr>
205
- </tbody>
206
- </table>
207
- <p>With regards,</p>
208
- <p>QA / SDET</p>
209
- </body>
210
- </html>
211
- `;
147
+ return `
148
+ <!DOCTYPE html>
149
+ <html lang="en">
150
+ <head>
151
+ <meta charset="UTF-8">
152
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
153
+ <title>Test Stats Report</title>
154
+ <style>
155
+ /* ANIMATION KEYFRAMES
156
+ (Supported by Apple Mail, iOS, Outlook Mac, etc.)
157
+ */
158
+
159
+ /* 1. Slide the card up and fade in */
160
+ @keyframes slideUpFade {
161
+ 0% { opacity: 0; transform: translateY(20px); }
162
+ 100% { opacity: 1; transform: translateY(0); }
163
+ }
164
+
165
+ /* 2. Gentle pulse for the logo */
166
+ @keyframes gentlePulse {
167
+ 0% { transform: scale(1); }
168
+ 50% { transform: scale(1.05); }
169
+ 100% { transform: scale(1); }
170
+ }
171
+
172
+ /* 3. Pop in effect for status badges */
173
+ @keyframes popIn {
174
+ 0% { opacity: 0; transform: scale(0.5); }
175
+ 80% { transform: scale(1.1); }
176
+ 100% { opacity: 1; transform: scale(1); }
177
+ }
178
+
179
+ /* CLASSES TO APPLY ANIMATIONS */
180
+ .anim-card {
181
+ animation: slideUpFade 0.8s ease-out forwards;
182
+ }
183
+
184
+ .anim-logo {
185
+ animation: gentlePulse 3s infinite ease-in-out;
186
+ }
187
+
188
+ /* Staggered delays for list items so they cascade in */
189
+ .anim-row-1 { animation: slideUpFade 0.5s ease-out 0.2s backwards; }
190
+ .anim-row-2 { animation: slideUpFade 0.5s ease-out 0.3s backwards; }
191
+ .anim-row-3 { animation: slideUpFade 0.5s ease-out 0.4s backwards; }
192
+ .anim-row-4 { animation: slideUpFade 0.5s ease-out 0.5s backwards; }
193
+
194
+ .anim-badge {
195
+ animation: popIn 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) 0.6s backwards;
196
+ }
197
+ </style>
198
+ </head>
199
+ <body style="margin: 0; padding: 0; background-color: #f3f4f6; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
200
+
201
+ <table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f3f4f6; padding: 40px 0;">
202
+ <tr>
203
+ <td align="center">
204
+
205
+ <table class="anim-card" role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="max-width: 600px; background-color: #ffffff; border-radius: 12px; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); overflow: hidden;">
206
+
207
+ <tr>
208
+ <td height="6" style="background-color: #4f46e5;"></td>
209
+ </tr>
210
+
211
+ <tr>
212
+ <td style="padding: 32px 32px 20px 32px;">
213
+ <table border="0" cellspacing="0" cellpadding="0" width="100%">
214
+ <tr>
215
+ <td width="55" style="vertical-align: middle; padding-right: 16px;">
216
+ <img class="anim-logo" src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png" alt="Report Logo" height="40" style="display: block; border: 0; border-radius: 8px;">
217
+ </td>
218
+ <td style="vertical-align: middle;">
219
+ <h1 style="margin: 0; font-size: 24px; font-weight: 700; color: #111827;">${projectName}</h1>
220
+ <p style="margin: 4px 0 0 0; font-size: 14px; color: #6b7280;">Automated Execution Report</p>
221
+ </td>
222
+ </tr>
223
+ </table>
224
+ </td>
225
+ </tr>
226
+
227
+ <tr>
228
+ <td style="padding: 0 32px 20px 32px;">
229
+ <table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f9fafb; border-radius: 8px; border: 1px solid #e5e7eb;">
230
+ <tr>
231
+ <td style="padding: 16px; border-right: 1px solid #e5e7eb; width: 50%;">
232
+ <p style="margin: 0 0 4px 0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #9ca3af; font-weight: 600;">Start Time</p>
233
+ <p style="margin: 0; font-size: 14px; color: #374151; font-weight: 500;">${startTime}</p>
234
+ </td>
235
+ <td style="padding: 16px; width: 50%;">
236
+ <p style="margin: 0 0 4px 0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #9ca3af; font-weight: 600;">Duration</p>
237
+ <p style="margin: 0; font-size: 14px; color: #374151; font-weight: 500;">${durationString}</p>
238
+ </td>
239
+ </tr>
240
+ </table>
241
+ </td>
242
+ </tr>
243
+
244
+ <tr>
245
+ <td style="padding: 0 32px 32px 32px;">
246
+ <table width="100%" border="0" cellspacing="0" cellpadding="0">
247
+
248
+ <tr class="anim-row-1">
249
+ <td style="padding: 12px 0; border-bottom: 1px solid #f3f4f6; font-size: 14px; color: #4b5563;">Total Tests Executed</td>
250
+ <td style="padding: 12px 0; border-bottom: 1px solid #f3f4f6; text-align: right; font-size: 14px; font-weight: 600; color: #111827;">${total}</td>
251
+ </tr>
252
+
253
+ <tr class="anim-row-2">
254
+ <td style="padding: 12px 0; border-bottom: 1px solid #f3f4f6; font-size: 14px; color: #4b5563;">Tests Passed</td>
255
+ <td style="padding: 12px 0; border-bottom: 1px solid #f3f4f6; text-align: right;">
256
+ <span class="anim-badge" style="background-color: #d1fae5; color: #065f46; padding: 4px 10px; border-radius: 9999px; font-size: 12px; font-weight: 600; display: inline-block; white-space: nowrap;">
257
+ ${passedTests} (${passedPercentage}%)
258
+ </span>
259
+ </td>
260
+ </tr>
261
+
262
+ <tr class="anim-row-3">
263
+ <td style="padding: 12px 0; border-bottom: 1px solid #f3f4f6; font-size: 14px; color: #4b5563;">Tests Failed</td>
264
+ <td style="padding: 12px 0; border-bottom: 1px solid #f3f4f6; text-align: right;">
265
+ <span class="anim-badge" style="background-color: #f3f4f6; color: #991b1b; padding: 4px 10px; border-radius: 9999px; font-size: 12px; font-weight: 600; display: inline-block; white-space: nowrap;">
266
+ ${failedTests} (${failedPercentage}%)
267
+ </span>
268
+ </td>
269
+ </tr>
270
+
271
+ <tr class="anim-row-4">
272
+ <td style="padding: 12px 0; font-size: 14px; color: #4b5563;">Tests Skipped</td>
273
+ <td style="padding: 12px 0; text-align: right;">
274
+ <span class="anim-badge" style="background-color: #fef3c7; color: #92400e; padding: 4px 10px; border-radius: 9999px; font-size: 12px; font-weight: 600; display: inline-block; white-space: nowrap;">
275
+ ${skippedTests} (${skippedPercentage}%)
276
+ </span>
277
+ </td>
278
+ </tr>
279
+
280
+ </table>
281
+ </td>
282
+ </tr>
283
+
284
+ <tr>
285
+ <td style="background-color: #f9fafb; padding: 20px; text-align: center; border-top: 1px solid #e5e7eb;">
286
+ <p style="margin: 0; font-size: 12px; color: #9ca3af;">Generated by Pulse Report</p>
287
+ </td>
288
+ </tr>
289
+
290
+ </table>
291
+ </td>
292
+ </tr>
293
+ </table>
294
+ </body>
295
+ </html>
296
+ `;
212
297
  };
213
298
 
214
299
  const __filename = fileURLToPath(import.meta.url);