@arghajit/dummy 0.3.12 → 0.3.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
@@ -90,13 +90,13 @@ All CLI scripts now support custom output directories, giving you full flexibili
90
90
 
91
91
  ```bash
92
92
  # Using custom directory
93
- npx generate-pulse-report --outputDir my-reports
93
+ npx generate-pulse-report --outputDir {YOUR_CUSTOM_REPORT_FOLDER}
94
94
  npx generate-report -o test-results/e2e
95
95
  npx send-email --outputDir custom-pulse-reports
96
96
 
97
97
  # Using nested paths
98
98
  npx generate-pulse-report --outputDir reports/integration
99
- npx merge-pulse-report --outputDir my-test-reports
99
+ npx merge-pulse-report --outputDir {YOUR_CUSTOM_REPORT_FOLDER}
100
100
  ```
101
101
 
102
102
  **Important:** Make sure your `playwright.config.ts` custom directory matches the CLI script:
@@ -105,7 +105,7 @@ npx merge-pulse-report --outputDir my-test-reports
105
105
  import { defineConfig } from "@playwright/test";
106
106
  import * as path from "path";
107
107
 
108
- const CUSTOM_REPORT_DIR = path.resolve(__dirname, "my-reports");
108
+ const CUSTOM_REPORT_DIR = path.resolve(__dirname, "{YOUR_CUSTOM_REPORT_FOLDER}");
109
109
 
110
110
  export default defineConfig({
111
111
  reporter: [
@@ -174,6 +174,67 @@ The dashboard includes AI-powered test analysis that provides:
174
174
  - Failure pattern recognition
175
175
  - Suggested optimizations
176
176
 
177
+ ## 📧 Send Report to Mail
178
+
179
+ The `send-email` CLI wraps the full email flow:
180
+
181
+ - Generates a lightweight HTML summary (`pulse-email-summary.html`) from the latest `playwright-pulse-report.json`.
182
+ - Builds a stats table (start time, duration, total, passed, failed, skipped, percentages).
183
+ - Sends an email with that summary as both the body and an HTML attachment.
184
+
185
+ ### 1. Configure Recipients
186
+
187
+ Set up to 5 recipients via environment variables:
188
+
189
+ ```bash
190
+ RECIPIENT_EMAIL_1=recipient1@example.com
191
+ RECIPIENT_EMAIL_2=recipient2@example.com
192
+ RECIPIENT_EMAIL_3=recipient3@example.com
193
+ RECIPIENT_EMAIL_4=recipient4@example.com
194
+ RECIPIENT_EMAIL_5=recipient5@example.com
195
+ ```
196
+
197
+ ### 2. Choose Credential Flow
198
+
199
+ The script supports two ways to obtain SMTP credentials:
200
+
201
+ **Flow A – Environment-based credentials (recommended)**
202
+
203
+ Provide mail host and credentials via environment variables:
204
+
205
+ ```bash
206
+ PULSE_MAIL_HOST=gmail # or: outlook
207
+ PULSE_MAIL_USERNAME=you@example.com
208
+ PULSE_MAIL_PASSWORD=your_app_password
209
+ ```
210
+
211
+ - `PULSE_MAIL_HOST` supports `gmail` or `outlook` only.
212
+ - For Gmail/Outlook, use an app password or SMTP-enabled credentials.
213
+
214
+ **Flow B – Default Flow (fallback)**
215
+
216
+ If the above variables are not set, the script fallbacks to default the mail host for compatibility.
217
+
218
+ ### 3. Run the CLI
219
+
220
+ Use the default output directory:
221
+
222
+ ```bash
223
+ npx send-email
224
+ ```
225
+
226
+ Or point to a custom report directory (must contain `playwright-pulse-report.json`):
227
+
228
+ ```bash
229
+ npx send-email --outputDir <YOUR_CUSTOM_REPORT_FOLDER>
230
+ ```
231
+
232
+ Under the hood, this will:
233
+
234
+ - Resolve the report directory (from `--outputDir` or `playwright.config.ts`).
235
+ - Run `generate-email-report.mjs` to create `pulse-email-summary.html`.
236
+ - Use Nodemailer to send the email via the selected provider (Gmail or Outlook).
237
+
177
238
  ## ⚙️ CI/CD Integration
178
239
 
179
240
  ### Basic Workflow
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arghajit/dummy",
3
3
  "author": "Arghajit Singha",
4
- "version": "0.3.12",
4
+ "version": "0.3.13",
5
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
6
  "homepage": "https://playwright-pulse-report.netlify.app/",
7
7
  "keywords": [
@@ -2165,6 +2165,43 @@ function generateSeverityDistributionChart(results) {
2165
2165
  </div>
2166
2166
  `;
2167
2167
  }
2168
+ /**
2169
+ * Helper to generate Lazy Media HTML using the Script Tag pattern.
2170
+ * This prevents the browser from parsing massive Base64 strings on page load.
2171
+ */
2172
+ function createLazyMedia(base64Data, mimeType, type, index, filename) {
2173
+ const uniqueId = `media-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2174
+ const dataUri = `data:${mimeType};base64,${base64Data}`;
2175
+ // Store heavy data in a non-rendering script tag
2176
+ const storage = `<script type="text/plain" id="data-${uniqueId}">${dataUri}</script>`;
2177
+
2178
+ let mediaTag = '';
2179
+ let btnText = '';
2180
+ let icon = '';
2181
+
2182
+ if (type === 'video') {
2183
+ mediaTag = `<video id="${uniqueId}" controls style="display:none; width: 100%; aspect-ratio: 16/9; margin-bottom: 10px;"></video>`;
2184
+ btnText = '▶ Load Video';
2185
+ } else {
2186
+ mediaTag = `<img id="${uniqueId}" alt="Screenshot ${index}" style="display:none; width: 100%; aspect-ratio: 4/3; object-fit: cover; border-bottom: 1px solid var(--border-color);" />`;
2187
+ btnText = '📷 Load Image';
2188
+ }
2189
+
2190
+ return `
2191
+ <div class="attachment-item ${type === 'video' ? 'video-item' : ''}">
2192
+ ${storage}
2193
+ <div class="lazy-placeholder" style="padding: 2rem; text-align: center; background: var(--light-gray-color); display: flex; flex-direction: column; align-items: center; justify-content: center; height: 180px;">
2194
+ <button class="ai-fix-btn" onclick="loadMedia('${uniqueId}', '${type}')" style="margin: 0 auto; font-size: 0.9rem;">${btnText}</button>
2195
+ <span style="font-size: 0.8rem; color: var(--text-color-secondary); margin-top: 8px;">(Click to view)</span>
2196
+ </div>
2197
+ ${mediaTag}
2198
+ <div class="attachment-info">
2199
+ <div class="trace-actions">
2200
+ <a href="#" onclick="event.preventDefault(); downloadMedia('${uniqueId}', '${filename}')" class="download-trace" style="width:100%; text-align:center;">Download</a>
2201
+ </div>
2202
+ </div>
2203
+ </div>`;
2204
+ }
2168
2205
  /**
2169
2206
  * Generates the HTML report.
2170
2207
  * @param {object} reportData - The data for the report.
@@ -2197,17 +2234,25 @@ function generateHTML(reportData, trendData = null) {
2197
2234
  * Generates the HTML for the test cases.
2198
2235
  * @returns {string} The HTML for the test cases.
2199
2236
  */
2200
- function generateTestCasesHTML(subset = results, baseIndex = 0) {
2201
- if (!results || results.length === 0)
2237
+ // MODIFIED: Accepts 'subset' (chunk of tests) and 'offset' (start index)
2238
+ function generateTestCasesHTML(subset, offset = 0) {
2239
+ // Use the subset if provided, otherwise fallback to all results (legacy compatibility)
2240
+ const data = subset || results;
2241
+
2242
+ if (!data || data.length === 0)
2202
2243
  return '<div class="no-tests">No test results found in this run.</div>';
2203
- return subset
2244
+
2245
+ return data
2204
2246
  .map((test, i) => {
2205
- const testIndex = baseIndex + i;
2247
+ // Calculate the global index (essential for unique IDs across chunks)
2248
+ const testIndex = offset + i;
2249
+
2206
2250
  const browser = test.browser || "unknown";
2207
2251
  const testFileParts = test.name.split(" > ");
2208
2252
  const testTitle =
2209
2253
  testFileParts[testFileParts.length - 1] || "Unnamed Test";
2210
- // --- NEW: Severity Logic ---
2254
+
2255
+ // --- Severity Logic ---
2211
2256
  const severity = test.severity || "Medium";
2212
2257
  const getSeverityColor = (level) => {
2213
2258
  switch (level) {
@@ -2226,9 +2271,9 @@ function generateHTML(reportData, trendData = null) {
2226
2271
  }
2227
2272
  };
2228
2273
  const severityColor = getSeverityColor(severity);
2229
- // We reuse 'status-badge' class for size/font consistency, but override background color
2230
2274
  const severityBadge = `<span class="status-badge" style="background-color: ${severityColor}; margin-right: 8px;">${severity}</span>`;
2231
- // ---------------------------
2275
+
2276
+ // --- Step Generation ---
2232
2277
  const generateStepsHTML = (steps, depth = 0) => {
2233
2278
  if (!steps || steps.length === 0)
2234
2279
  return "<div class='no-steps'>No steps recorded for this test.</div>";
@@ -2240,388 +2285,267 @@ function generateHTML(reportData, trendData = null) {
2240
2285
  ? `step-hook step-hook-${step.hookType}`
2241
2286
  : "";
2242
2287
  const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
2243
- return `<div class="step-item" style="--depth: ${depth};"><div class="step-header ${stepClass}" role="button" aria-expanded="false"><span class="step-icon">${getStatusIcon(
2244
- step.status
2245
- )}</span><span class="step-title">${sanitizeHTML(
2288
+ return `
2289
+ <div class="step-item" style="--depth: ${depth};">
2290
+ <div class="step-header ${stepClass}" role="button" aria-expanded="false">
2291
+ <span class="step-icon">${getStatusIcon(step.status)}</span>
2292
+ <span class="step-title">${sanitizeHTML(
2246
2293
  step.title
2247
- )}${hookIndicator}</span><span class="step-duration">${formatDuration(
2294
+ )}${hookIndicator}</span>
2295
+ <span class="step-duration">${formatDuration(
2248
2296
  step.duration
2249
- )}</span></div><div class="step-details" style="display: none;">${
2297
+ )}</span>
2298
+ </div>
2299
+ <div class="step-details" style="display: none;">
2300
+ ${
2250
2301
  step.codeLocation
2251
2302
  ? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
2252
2303
  step.codeLocation
2253
2304
  )}</div>`
2254
2305
  : ""
2255
- }${
2306
+ }
2307
+ ${
2256
2308
  step.errorMessage
2257
- ? `<div class="test-error-summary">${
2258
- step.stackTrace
2259
- ? `<div class="stack-trace">${formatPlaywrightError(
2260
- step.stackTrace
2261
- )}</div>`
2262
- : ""
2263
- }<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
2264
- : ""
2265
- }${(() => {
2266
- if (!step.attachments || step.attachments.length === 0)
2267
- return "";
2268
- return `<div class="attachments-section"><h4>Step Attachments</h4><div class="attachments-grid">${step.attachments
2269
- .map((attachment) => {
2270
- try {
2271
- const attachmentPath = path.resolve(
2272
- DEFAULT_OUTPUT_DIR,
2273
- attachment.path
2274
- );
2275
- if (!fsExistsSync(attachmentPath)) {
2276
- return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
2277
- attachment.name
2278
- )}</div>`;
2309
+ ? `<div class="test-error-summary">
2310
+ ${
2311
+ step.stackTrace
2312
+ ? `<div class="stack-trace">${formatPlaywrightError(
2313
+ step.stackTrace
2314
+ )}</div>`
2315
+ : ""
2279
2316
  }
2280
- const attachmentBase64 =
2281
- readFileSync(attachmentPath).toString("base64");
2282
- const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
2283
- return `<div class="attachment-item generic-attachment">
2284
- <div class="attachment-icon">${getAttachmentIcon(
2285
- attachment.contentType
2286
- )}</div>
2287
- <div class="attachment-caption">
2288
- <span class="attachment-name" title="${sanitizeHTML(
2289
- attachment.name
2290
- )}">${sanitizeHTML(attachment.name)}</span>
2291
- <span class="attachment-type">${sanitizeHTML(
2292
- attachment.contentType
2293
- )}</span>
2294
- </div>
2295
- <div class="attachment-info">
2296
- <div class="trace-actions">
2297
- <a href="#" data-href="${attachmentDataUri}" class="view-full lazy-load-attachment" target="_blank">View</a>
2298
- <a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
2299
- attachment.name
2300
- )}">Download</a>
2301
- </div>
2302
- </div>
2303
- </div>`;
2304
- } catch (e) {
2305
- return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(
2306
- attachment.name
2307
- )}</div>`;
2308
- }
2309
- })
2310
- .join("")}</div></div>`;
2311
- })()}${
2317
+ <button class="copy-error-btn" onclick="copyErrorToClipboard(this)" style="margin-top: 8px; padding: 4px 8px; background: #f0f0f0; border: 2px solid #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; border-color: #8B0000; color: #8B0000;" onmouseover="this.style.background='#e0e0e0'" onmouseout="this.style.background='#f0f0f0'">Copy Error Prompt</button>
2318
+ </div>`
2319
+ : ""
2320
+ }
2321
+ ${
2312
2322
  hasNestedSteps
2313
2323
  ? `<div class="nested-steps">${generateStepsHTML(
2314
2324
  step.steps,
2315
2325
  depth + 1
2316
2326
  )}</div>`
2317
2327
  : ""
2318
- }</div></div>`;
2328
+ }
2329
+ </div>
2330
+ </div>`;
2319
2331
  })
2320
2332
  .join("");
2321
2333
  };
2322
- return `<div class="test-case" data-status="${
2323
- test.status
2324
- }" data-browser="${sanitizeHTML(browser)}" data-tags="${(
2325
- test.tags || []
2326
- )
2334
+
2335
+ return `
2336
+ <div class="test-case" data-status="${
2337
+ test.status
2338
+ }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
2327
2339
  .join(",")
2328
- .toLowerCase()}" data-test-id="${sanitizeHTML(
2329
- String(test.id || testIndex)
2330
- )}">
2331
- <div class="test-case-header" role="button" aria-expanded="false"><div class="test-case-summary"><span class="status-badge ${getStatusClass(
2332
- test.status
2333
- )}">${String(
2340
+ .toLowerCase()}">
2341
+ <div class="test-case-header" role="button" aria-expanded="false">
2342
+ <div class="test-case-summary">
2343
+ <span class="status-badge ${getStatusClass(test.status)}">${String(
2334
2344
  test.status
2335
- ).toUpperCase()}</span><span class="test-case-title" title="${sanitizeHTML(
2336
- test.name
2337
- )}">${sanitizeHTML(
2338
- testTitle
2339
- )}</span><span class="test-case-browser">(${sanitizeHTML(
2340
- browser
2341
- )})</span></div><div class="test-case-meta">
2342
- ${severityBadge}
2343
- ${
2344
- test.tags && test.tags.length > 0
2345
- ? test.tags
2346
- .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
2347
- .join(" ")
2348
- : ""
2349
- }
2350
- <span class="test-duration">${formatDuration(
2351
- test.duration
2352
- )}</span></div></div>
2353
- <div class="test-case-content" style="display: none;">
2354
- <p><strong>Full Path:</strong> ${sanitizeHTML(
2355
- test.name
2356
- )}</p>
2357
- ${
2358
- test.annotations && test.annotations.length > 0
2359
- ? `<div class="annotations-section" style="margin: 12px 0; padding: 12px; background-color: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.3); border-left: 4px solid #8b5cf6; border-radius: 4px;">
2360
- <h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
2361
- ${test.annotations
2362
- .map((annotation, idx) => {
2363
- const isIssueOrBug =
2364
- annotation.type === "issue" ||
2365
- annotation.type === "bug";
2366
- const descriptionText =
2367
- annotation.description || "";
2368
- const typeLabel = sanitizeHTML(
2369
- annotation.type
2370
- );
2371
- const descriptionHtml =
2372
- isIssueOrBug &&
2373
- descriptionText.match(/^[A-Z]+-\d+$/)
2374
- ? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
2375
- descriptionText
2376
- )}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
2377
- descriptionText
2378
- )}</a>`
2379
- : sanitizeHTML(descriptionText);
2380
- const locationText = annotation.location
2381
- ? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
2382
- annotation.location.file
2383
- )}:${annotation.location.line}:${
2384
- annotation.location.column
2385
- }</div>`
2386
- : "";
2387
- return `<div style="margin-bottom: ${
2388
- idx < test.annotations.length - 1
2389
- ? "10px"
2390
- : "0"
2391
- };">
2392
- <strong style="color: #8b5cf6;">Type:</strong> <span style="background-color: rgba(139, 92, 246, 0.2); padding: 2px 8px; border-radius: 4px; font-size: 0.9em;">${typeLabel}</span>
2393
- ${
2394
- descriptionText
2395
- ? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
2396
- : ""
2397
- }
2398
- ${locationText}
2399
- </div>`;
2400
- })
2401
- .join("")}
2402
- </div>`
2403
- : ""
2404
- }
2405
- <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
2406
- test.workerId
2407
- )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
2345
+ ).toUpperCase()}</span>
2346
+ <span class="test-case-title" title="${sanitizeHTML(
2347
+ test.name
2348
+ )}">${sanitizeHTML(testTitle)}</span>
2349
+ <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
2350
+ </div>
2351
+ <div class="test-case-meta">
2352
+ ${severityBadge}
2353
+ ${
2354
+ test.tags && test.tags.length > 0
2355
+ ? test.tags
2356
+ .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
2357
+ .join(" ")
2358
+ : ""
2359
+ }
2360
+ <span class="test-duration">${formatDuration(test.duration)}</span>
2361
+ </div>
2362
+ </div>
2363
+ <div class="test-case-content" style="display: none;">
2364
+ <p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
2365
+ ${
2366
+ test.annotations && test.annotations.length > 0
2367
+ ? `<div class="annotations-section" style="margin: 12px 0; padding: 12px; background-color: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.3); border-left: 4px solid #8b5cf6; border-radius: 4px;">
2368
+ <h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
2369
+ ${test.annotations
2370
+ .map((annotation, idx) => {
2371
+ const isIssueOrBug =
2372
+ annotation.type === "issue" ||
2373
+ annotation.type === "bug";
2374
+ const descriptionText = annotation.description || "";
2375
+ const typeLabel = sanitizeHTML(annotation.type);
2376
+ const descriptionHtml =
2377
+ isIssueOrBug && descriptionText.match(/^[A-Z]+-\d+$/)
2378
+ ? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
2379
+ descriptionText
2380
+ )}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
2381
+ descriptionText
2382
+ )}</a>`
2383
+ : sanitizeHTML(descriptionText);
2384
+ const locationText = annotation.location
2385
+ ? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
2386
+ annotation.location.file
2387
+ )}:${annotation.location.line}:${
2388
+ annotation.location.column
2389
+ }</div>`
2390
+ : "";
2391
+ return `<div style="margin-bottom: ${
2392
+ idx < test.annotations.length - 1 ? "10px" : "0"
2393
+ };"><strong style="color: #8b5cf6;">Type:</strong> <span style="background-color: rgba(139, 92, 246, 0.2); padding: 2px 8px; border-radius: 4px; font-size: 0.9em;">${typeLabel}</span>${
2394
+ descriptionText
2395
+ ? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
2396
+ : ""
2397
+ }${locationText}</div>`;
2398
+ })
2399
+ .join("")}
2400
+ </div>`
2401
+ : ""
2402
+ }
2403
+ <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
2404
+ test.workerId
2405
+ )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
2408
2406
  test.totalWorkers
2409
2407
  )}]</p>
2410
- ${
2411
- test.errorMessage
2412
- ? `<div class="test-error-summary">${formatPlaywrightError(
2413
- test.errorMessage
2414
- )}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
2415
- : ""
2416
- }
2417
- ${
2418
- test.snippet
2419
- ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
2420
- test.snippet
2421
- )}</code></pre></div>`
2422
- : ""
2423
- }
2424
- <h4>Steps</h4><div class="steps-list">${generateStepsHTML(
2425
- test.steps
2426
- )}</div>
2427
- ${(() => {
2428
- if (!test.stdout || test.stdout.length === 0)
2429
- return "";
2430
- // Create a unique ID for the <pre> element to target it for copying
2431
- const logId = `stdout-log-${test.id || testIndex}`;
2432
- return `<div class="console-output-section">
2433
- <h4>Console Output (stdout)
2434
- <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy Console</button>
2435
- </h4>
2436
- <div class="log-wrapper">
2437
- <pre id="${logId}" class="console-log stdout-log" style="background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
2438
- test.stdout
2439
- .map((line) => sanitizeHTML(line))
2440
- .join("\n")
2441
- )}</pre>
2442
- </div>
2443
- </div>`;
2444
- })()}
2445
- ${
2446
- test.stderr && test.stderr.length > 0
2447
- ? (() => {
2448
- const logId = `stderr-log-${
2449
- test.id || testIndex
2450
- }`;
2451
- return `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre id="${logId}" class="console-log stderr-log">${test.stderr
2452
- .map((line) => sanitizeHTML(line))
2453
- .join("\\n")}</pre></div>`;
2454
- })()
2455
- : ""
2456
- }
2457
-
2458
- ${(() => {
2459
- if (
2460
- !test.screenshots ||
2461
- test.screenshots.length === 0
2462
- )
2463
- return "";
2464
- return `<div class="attachments-section"><h4>Screenshots</h4><div class="attachments-grid">${test.screenshots
2465
- .map((screenshotPath, index) => {
2466
- try {
2467
- const imagePath = path.resolve(
2468
- DEFAULT_OUTPUT_DIR,
2469
- screenshotPath
2470
- );
2471
- if (!fsExistsSync(imagePath))
2472
- return `<div class="attachment-item error">Screenshot not found: ${sanitizeHTML(
2473
- screenshotPath
2474
- )}</div>`;
2475
- const base64ImageData =
2476
- readFileSync(imagePath).toString("base64");
2477
- return `<div class="attachment-item"><img src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" data-src="data:image/png;base64,${base64ImageData}" alt="Screenshot ${
2478
- index + 1
2479
- }" class="lazy-load-image"><div class="attachment-info"><div class="trace-actions"><a href="#" data-href="data:image/png;base64,${base64ImageData}" class="lazy-load-attachment" target="_blank" download="screenshot-${index}.png">Download</a></div></div></div>`;
2480
- } catch (e) {
2481
- return `<div class="attachment-item error">Failed to load screenshot: ${sanitizeHTML(
2482
- screenshotPath
2483
- )}</div>`;
2484
- }
2485
- })
2486
- .join("")}</div></div>`;
2487
- })()}
2488
-
2489
- ${(() => {
2490
- if (!test.videoPath || test.videoPath.length === 0)
2491
- return "";
2492
- return `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
2493
- .map((videoPath, index) => {
2494
- try {
2495
- const videoFilePath = path.resolve(
2496
- DEFAULT_OUTPUT_DIR,
2497
- videoPath
2498
- );
2499
- if (!fsExistsSync(videoFilePath))
2500
- return `<div class="attachment-item error">Video not found: ${sanitizeHTML(
2501
- videoPath
2502
- )}</div>`;
2503
- const videoBase64 =
2504
- readFileSync(videoFilePath).toString(
2505
- "base64"
2506
- );
2507
- const fileExtension = path
2508
- .extname(videoPath)
2509
- .slice(1)
2510
- .toLowerCase();
2511
- const mimeType =
2512
- {
2513
- mp4: "video/mp4",
2514
- webm: "video/webm",
2515
- ogg: "video/ogg",
2516
- mov: "video/quicktime",
2517
- avi: "video/x-msvideo",
2518
- }[fileExtension] || "video/mp4";
2519
- const videoDataUri = `data:${mimeType};base64,${videoBase64}`;
2520
- return `<div class="attachment-item video-item"><video controls preload="none" class="lazy-load-video"><source data-src="${videoDataUri}" type="${mimeType}"></video><div class="attachment-info"><div class="trace-actions"><a href="#" data-href="${videoDataUri}" class="lazy-load-attachment" target="_blank" download="video-${index}.${fileExtension}">Download</a></div></div></div>`;
2521
- } catch (e) {
2522
- return `<div class="attachment-item error">Failed to load video: ${sanitizeHTML(
2523
- videoPath
2524
- )}</div>`;
2525
- }
2526
- })
2527
- .join("")}</div></div>`;
2528
- })()}
2529
-
2530
- ${(() => {
2531
- if (!test.tracePath) return "";
2532
- try {
2533
- const traceFilePath = path.resolve(
2534
- DEFAULT_OUTPUT_DIR,
2535
- test.tracePath
2536
- );
2537
- if (!fsExistsSync(traceFilePath))
2538
- return `<div class="attachments-section"><h4>Trace File</h4><div class="attachment-item error">Trace file not found: ${sanitizeHTML(
2539
- test.tracePath
2540
- )}</div></div>`;
2541
- const traceBase64 =
2542
- readFileSync(traceFilePath).toString("base64");
2543
- const traceDataUri = `data:application/zip;base64,${traceBase64}`;
2544
- return `<div class="attachments-section"><h4>Trace File</h4><div class="attachments-grid"><div class="attachment-item generic-attachment"><div class="attachment-icon">📄</div><div class="attachment-caption"><span class="attachment-name">trace.zip</span></div><div class="attachment-info"><div class="trace-actions"><a href="#" data-href="${traceDataUri}" class="lazy-load-attachment" download="trace.zip">Download Trace</a></div></div></div></div></div>`;
2545
- } catch (e) {
2546
- return `<div class="attachments-section"><h4>Trace File</h4><div class="attachment-item error">Failed to load trace file.</div></div>`;
2547
- }
2548
- })()}
2549
-
2550
- ${(() => {
2551
- if (
2552
- !test.attachments ||
2553
- test.attachments.length === 0
2554
- )
2555
- return "";
2556
-
2557
- return `<div class="attachments-section"><h4>Other Attachments</h4><div class="attachments-grid">${test.attachments
2558
- .map((attachment) => {
2559
- try {
2560
- const attachmentPath = path.resolve(
2561
- DEFAULT_OUTPUT_DIR,
2562
- attachment.path
2563
- );
2564
-
2565
- if (!fsExistsSync(attachmentPath)) {
2566
- console.warn(
2567
- `Attachment not found at: ${attachmentPath}`
2568
- );
2569
- return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
2570
- attachment.name
2571
- )}</div>`;
2572
- }
2408
+ ${
2409
+ test.errorMessage
2410
+ ? `<div class="test-error-summary">${formatPlaywrightError(
2411
+ test.errorMessage
2412
+ )}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)" style="margin-top: 8px; padding: 4px 8px; background: #f0f0f0; border: 2px solid #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; border-color: #8B0000; color: #8B0000;" onmouseover="this.style.background='#e0e0e0'" onmouseout="this.style.background='#f0f0f0'">Copy Error Prompt</button></div>`
2413
+ : ""
2414
+ }
2415
+ ${
2416
+ test.snippet
2417
+ ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
2418
+ test.snippet
2419
+ )}</code></pre></div>`
2420
+ : ""
2421
+ }
2422
+ <h4>Steps</h4>
2423
+ <div class="steps-list">${generateStepsHTML(test.steps)}</div>
2424
+
2425
+ ${(() => {
2426
+ if (!test.stdout || test.stdout.length === 0) return "";
2427
+ // FIXED: Now using 'testIndex' which is guaranteed to be defined
2428
+ const logId = `stdout-log-${test.id || testIndex}`;
2429
+ return `<div class="console-output-section"><h4>Console Output (stdout) <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy Console</button></h4><div class="log-wrapper"><pre id="${logId}" class="console-log stdout-log" style="background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
2430
+ test.stdout.map((line) => sanitizeHTML(line)).join("\n")
2431
+ )}</pre></div></div>`;
2432
+ })()}
2433
+
2434
+ ${(() => {
2435
+ if (!test.stderr || test.stderr.length === 0) return "";
2436
+ // FIXED: Using 'testIndex'
2437
+ const logId = `stderr-log-${test.id || testIndex}`;
2438
+ return `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log" style="background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
2439
+ test.stderr.map((line) => sanitizeHTML(line)).join("\n")
2440
+ )}</pre></div>`;
2441
+ })()}
2442
+
2443
+ ${(() => {
2444
+ if (!test.screenshots || test.screenshots.length === 0) return "";
2445
+ const screenshotsHTML = test.screenshots
2446
+ .map((screenshotPath, sIndex) => {
2447
+ try {
2448
+ const imagePath = path.resolve(
2449
+ DEFAULT_OUTPUT_DIR,
2450
+ screenshotPath
2451
+ );
2452
+ if (!fsExistsSync(imagePath))
2453
+ return `<div class="attachment-item error">Screenshot not found</div>`;
2454
+ const base64ImageData =
2455
+ readFileSync(imagePath).toString("base64");
2456
+ // LAZY LOAD: Using helper with unique ID
2457
+ return createLazyMedia(
2458
+ base64ImageData,
2459
+ "image/png",
2460
+ "image",
2461
+ sIndex + 1,
2462
+ `screenshot-${testIndex}-${sIndex}.png`
2463
+ );
2464
+ } catch (e) {
2465
+ return `<div class="attachment-item error">Error loading screenshot</div>`;
2466
+ }
2467
+ })
2468
+ .join("");
2469
+ return `<div class="attachments-section"><h4>Screenshots</h4><div class="attachments-grid">${screenshotsHTML}</div></div>`;
2470
+ })()}
2471
+
2472
+ ${(() => {
2473
+ if (!test.videoPath || test.videoPath.length === 0) return "";
2474
+ const videosHTML = test.videoPath
2475
+ .map((videoPath, vIndex) => {
2476
+ try {
2477
+ const videoFilePath = path.resolve(
2478
+ DEFAULT_OUTPUT_DIR,
2479
+ videoPath
2480
+ );
2481
+ if (!fsExistsSync(videoFilePath))
2482
+ return `<div class="attachment-item error">Video not found</div>`;
2483
+ const videoBase64 =
2484
+ readFileSync(videoFilePath).toString("base64");
2485
+ const ext = path.extname(videoPath).slice(1).toLowerCase();
2486
+ const mime =
2487
+ { mp4: "video/mp4", webm: "video/webm" }[ext] ||
2488
+ "video/mp4";
2489
+ // LAZY LOAD: Using helper with unique ID
2490
+ return createLazyMedia(
2491
+ videoBase64,
2492
+ mime,
2493
+ "video",
2494
+ vIndex + 1,
2495
+ `video-${testIndex}-${vIndex}.${ext}`
2496
+ );
2497
+ } catch (e) {
2498
+ return `<div class="attachment-item error">Error loading video</div>`;
2499
+ }
2500
+ })
2501
+ .join("");
2502
+ return `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${videosHTML}</div></div>`;
2503
+ })()}
2573
2504
 
2574
- const attachmentBase64 =
2575
- readFileSync(attachmentPath).toString(
2576
- "base64"
2577
- );
2578
- const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
2579
-
2580
- return `<div class="attachment-item generic-attachment">
2581
- <div class="attachment-icon">${getAttachmentIcon(
2582
- attachment.contentType
2583
- )}</div>
2584
- <div class="attachment-caption">
2585
- <span class="attachment-name" title="${sanitizeHTML(
2586
- attachment.name
2587
- )}">${sanitizeHTML(
2588
- attachment.name
2589
- )}</span>
2590
- <span class="attachment-type">${sanitizeHTML(
2591
- attachment.contentType
2592
- )}</span>
2593
- </div>
2594
- <div class="attachment-info">
2595
- <div class="trace-actions">
2596
- <a href="#" data-href="${attachmentDataUri}" class="view-full lazy-load-attachment" target="_blank">View</a>
2597
- <a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
2598
- attachment.name
2599
- )}">Download</a>
2600
- </div>
2601
- </div>
2602
- </div>`;
2603
- } catch (e) {
2604
- console.error(
2605
- `Failed to process attachment "${attachment.name}":`,
2606
- e
2607
- );
2608
- return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(
2609
- attachment.name
2610
- )}</div>`;
2611
- }
2612
- })
2613
- .join("")}</div></div>`;
2614
- })()}
2615
-
2616
- ${
2617
- test.codeSnippet
2618
- ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${sanitizeHTML(
2619
- test.codeSnippet
2620
- )}</code></pre></div>`
2621
- : ""
2622
- }
2623
- </div>
2624
- </div>`;
2505
+ ${
2506
+ test.tracePath
2507
+ ? `<div class="attachments-section"><h4>Trace Files</h4><div class="attachments-grid"><div class="attachment-item trace-item"><div class="trace-preview"><span class="trace-icon">📄</span><span class="trace-name">${sanitizeHTML(
2508
+ path.basename(test.tracePath)
2509
+ )}</span></div><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
2510
+ test.tracePath
2511
+ )}" target="_blank" download="${sanitizeHTML(
2512
+ path.basename(test.tracePath)
2513
+ )}" class="download-trace">Download Trace</a></div></div></div></div></div>`
2514
+ : ""
2515
+ }
2516
+ ${
2517
+ test.attachments && test.attachments.length > 0
2518
+ ? `<div class="attachments-section"><h4>Other Attachments</h4><div class="attachments-grid">${test.attachments
2519
+ .map(
2520
+ (attachment) =>
2521
+ `<div class="attachment-item generic-attachment"><div class="attachment-icon">${getAttachmentIcon(
2522
+ attachment.contentType
2523
+ )}</div><div class="attachment-caption"><span class="attachment-name" title="${sanitizeHTML(
2524
+ attachment.name
2525
+ )}">${sanitizeHTML(
2526
+ attachment.name
2527
+ )}</span><span class="attachment-type">${sanitizeHTML(
2528
+ attachment.contentType
2529
+ )}</span></div><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
2530
+ attachment.path
2531
+ )}" target="_blank" class="view-full">View</a><a href="${sanitizeHTML(
2532
+ attachment.path
2533
+ )}" target="_blank" download="${sanitizeHTML(
2534
+ attachment.name
2535
+ )}" class="download-trace">Download</a></div></div></div>`
2536
+ )
2537
+ .join("")}</div></div>`
2538
+ : ""
2539
+ }
2540
+ ${
2541
+ test.codeSnippet
2542
+ ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
2543
+ sanitizeHTML(test.codeSnippet)
2544
+ )}</code></pre></div>`
2545
+ : ""
2546
+ }
2547
+ </div>
2548
+ </div>`;
2625
2549
  })
2626
2550
  .join("");
2627
2551
  }
@@ -3242,6 +3166,41 @@ Code Snippet:
3242
3166
  button.classList.remove('expanded');
3243
3167
  }
3244
3168
  }
3169
+
3170
+ // --- LAZY MEDIA HANDLERS ---
3171
+ window.loadMedia = function(id, type) {
3172
+ const storage = document.getElementById('data-' + id);
3173
+ const element = document.getElementById(id);
3174
+ const placeholder = element.previousElementSibling;
3175
+
3176
+ if (storage && element) {
3177
+ const data = storage.textContent;
3178
+ element.src = data;
3179
+ element.style.display = 'block';
3180
+ if (placeholder) placeholder.style.display = 'none';
3181
+
3182
+ if (type === 'video') {
3183
+ element.play().catch(e => console.log('Autoplay prevented', e));
3184
+ }
3185
+ }
3186
+ };
3187
+
3188
+ window.downloadMedia = function(id, filename) {
3189
+ const storage = document.getElementById('data-' + id);
3190
+ if (storage) {
3191
+ const data = storage.textContent;
3192
+ const link = document.createElement('a');
3193
+ link.href = data;
3194
+ link.download = filename;
3195
+ document.body.appendChild(link);
3196
+ link.click();
3197
+ document.body.removeChild(link);
3198
+ } else {
3199
+ alert("Media data not found.");
3200
+ }
3201
+ };
3202
+
3203
+ // Ensure formatDuration is globally available... (existing code follows)
3245
3204
 
3246
3205
  function initializeReportInteractivity() {
3247
3206
  const tabButtons = document.querySelectorAll('.tab-button');
@@ -3346,10 +3305,25 @@ Code Snippet:
3346
3305
  filterTestHistoryCards();
3347
3306
  });
3348
3307
  // --- Expand/Collapse and Toggle Details Logic ---
3308
+ // --- Expand/Collapse and Toggle Details Logic ---
3349
3309
  function toggleElementDetails(headerElement, contentSelector) {
3350
3310
  let contentElement;
3351
3311
  if (headerElement.classList.contains('test-case-header')) {
3312
+ // Find the content sibling
3352
3313
  contentElement = headerElement.parentElement.querySelector('.test-case-content');
3314
+
3315
+ // --- ALLURE-STYLE LAZY RENDERING ---
3316
+ // If content is empty/not loaded, load it from the template script
3317
+ if (contentElement && !contentElement.getAttribute('data-loaded')) {
3318
+ const testCaseId = contentElement.id.replace('details-', '');
3319
+ const template = document.getElementById('tmpl-' + testCaseId);
3320
+ if (template) {
3321
+ contentElement.innerHTML = template.textContent; // Hydrate HTML
3322
+ contentElement.setAttribute('data-loaded', 'true');
3323
+ }
3324
+ }
3325
+ // -----------------------------------
3326
+
3353
3327
  } else if (headerElement.classList.contains('step-header')) {
3354
3328
  contentElement = headerElement.nextElementSibling;
3355
3329
  if (!contentElement || !contentElement.matches(contentSelector || '.step-details')) {
@@ -252,15 +252,38 @@ const sendEmail = async (credentials, reportDir) => {
252
252
  try {
253
253
  console.log("Starting the sendEmail function...");
254
254
 
255
- const secureTransporter = nodemailer.createTransport({
256
- host: "smtp.gmail.com",
257
- port: 465,
258
- secure: true,
259
- auth: {
260
- user: credentials.username,
261
- pass: credentials.password,
262
- },
263
- });
255
+ let secureTransporter;
256
+ const mailHost = credentials.host
257
+ ? credentials.host.toLowerCase()
258
+ : "gmail";
259
+
260
+ if (mailHost === "gmail") {
261
+ secureTransporter = nodemailer.createTransport({
262
+ service: "gmail",
263
+ auth: {
264
+ user: credentials.username,
265
+ pass: credentials.password,
266
+ },
267
+ });
268
+ } else if (mailHost === "outlook") {
269
+ secureTransporter = nodemailer.createTransport({
270
+ host: "smtp.outlook.com",
271
+ port: 587,
272
+ secure: false,
273
+ auth: {
274
+ user: credentials.username,
275
+ pass: credentials.password,
276
+ },
277
+ });
278
+ } else {
279
+ // Should be caught in main, but safety check here
280
+ console.log(
281
+ chalk.red(
282
+ "Pulse report currently do not support provided mail host, kindly use either outlook mail or, gmail"
283
+ )
284
+ );
285
+ process.exit(1);
286
+ }
264
287
 
265
288
  const reportData = getPulseReportSummary(reportDir);
266
289
  const htmlContent = generateHtmlTable(reportData);
@@ -399,13 +422,50 @@ const main = async () => {
399
422
  );
400
423
  }
401
424
 
402
- const credentials = await fetchCredentials(reportDir);
403
- if (!credentials) {
404
- console.warn(
405
- "Skipping email sending due to missing or failed credential fetch"
425
+ // --- MODIFIED: Credentials Selection Logic ---
426
+ let credentials;
427
+
428
+ // Check if custom environment variables are provided
429
+ if (
430
+ process.env.PULSE_MAIL_HOST &&
431
+ process.env.PULSE_MAIL_USERNAME &&
432
+ process.env.PULSE_MAIL_PASSWORD
433
+ ) {
434
+ const host = process.env.PULSE_MAIL_HOST.toLowerCase();
435
+
436
+ // Validate host immediately
437
+ if (host !== "gmail" && host !== "outlook") {
438
+ console.log(
439
+ chalk.red(
440
+ "Pulse report currently do not support provided mail host, kindly use either outlook mail or, gmail."
441
+ )
442
+ );
443
+ process.exit(1);
444
+ }
445
+
446
+ console.log(
447
+ chalk.blue(
448
+ `Using custom credentials from environment variables for ${host}.`
449
+ )
406
450
  );
407
- return;
451
+ credentials = {
452
+ username: process.env.PULSE_MAIL_USERNAME,
453
+ password: process.env.PULSE_MAIL_PASSWORD,
454
+ host: host,
455
+ };
456
+ } else {
457
+ // Fallback to existing fetch mechanism
458
+ credentials = await fetchCredentials(reportDir);
459
+ if (!credentials) {
460
+ console.warn(
461
+ "Skipping email sending due to missing or failed credential fetch"
462
+ );
463
+ return;
464
+ }
465
+ // Mark fetched credentials as gmail by default for compatibility
466
+ credentials.host = "gmail";
408
467
  }
468
+ // --- END MODIFICATION ---
409
469
  // Removed await delay(10000); // If not strictly needed, remove it.
410
470
  try {
411
471
  await sendEmail(credentials, reportDir);