@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 +64 -3
- package/package.json +1 -1
- package/scripts/generate-static-report.mjs +340 -366
- package/scripts/sendReport.mjs +74 -14
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
|
|
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
|
|
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, "
|
|
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.
|
|
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
|
-
|
|
2201
|
-
|
|
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
|
-
|
|
2244
|
+
|
|
2245
|
+
return data
|
|
2204
2246
|
.map((test, i) => {
|
|
2205
|
-
|
|
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
|
-
|
|
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
|
|
2244
|
-
|
|
2245
|
-
|
|
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
|
|
2294
|
+
)}${hookIndicator}</span>
|
|
2295
|
+
<span class="step-duration">${formatDuration(
|
|
2248
2296
|
step.duration
|
|
2249
|
-
)}</span
|
|
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
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
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
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
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
|
-
}
|
|
2328
|
+
}
|
|
2329
|
+
</div>
|
|
2330
|
+
</div>`;
|
|
2319
2331
|
})
|
|
2320
2332
|
.join("");
|
|
2321
2333
|
};
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
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()}"
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
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
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
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
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
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
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
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')) {
|
package/scripts/sendReport.mjs
CHANGED
|
@@ -252,15 +252,38 @@ const sendEmail = async (credentials, reportDir) => {
|
|
|
252
252
|
try {
|
|
253
253
|
console.log("Starting the sendEmail function...");
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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);
|