@arghajit/dummy 0.1.0-beta-17 → 0.1.0-beta-19

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.
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import * as fs from "fs/promises";
4
- import { readFileSync, existsSync as fsExistsSync } from "fs"; // ADD THIS LINE
4
+ import { readFileSync, existsSync as fsExistsSync } from "fs";
5
5
  import path from "path";
6
- import { fork } from "child_process"; // Add this
7
- import { fileURLToPath } from "url"; // Add this for resolving path in ESM
6
+ import { fork } from "child_process";
7
+ import { fileURLToPath } from "url";
8
8
 
9
9
  // Use dynamic import for chalk as it's ESM only
10
10
  let chalk;
@@ -21,16 +21,17 @@ try {
21
21
  gray: (text) => text,
22
22
  };
23
23
  }
24
+
24
25
  // Default configuration
25
26
  const DEFAULT_OUTPUT_DIR = "pulse-report";
26
27
  const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
27
28
  const DEFAULT_HTML_FILE = "playwright-pulse-static-report.html";
29
+
28
30
  // Helper functions
29
31
  export function ansiToHtml(text) {
30
32
  if (!text) {
31
33
  return "";
32
34
  }
33
-
34
35
  const codes = {
35
36
  0: "color:inherit;font-weight:normal;font-style:normal;text-decoration:none;opacity:1;background-color:inherit;",
36
37
  1: "font-weight:bold",
@@ -82,18 +83,14 @@ export function ansiToHtml(text) {
82
83
  }
83
84
  }
84
85
  };
85
-
86
86
  const resetAndApplyNewCodes = (newCodesStr) => {
87
87
  const newCodes = newCodesStr.split(";");
88
-
89
88
  if (newCodes.includes("0")) {
90
89
  currentStylesArray = [];
91
90
  if (codes["0"]) currentStylesArray.push(codes["0"]);
92
91
  }
93
-
94
92
  for (const code of newCodes) {
95
93
  if (code === "0") continue;
96
-
97
94
  if (codes[code]) {
98
95
  if (code === "39") {
99
96
  currentStylesArray = currentStylesArray.filter(
@@ -123,12 +120,9 @@ export function ansiToHtml(text) {
123
120
  }
124
121
  applyStyles();
125
122
  };
126
-
127
123
  const segments = text.split(/(\x1b\[[0-9;]*m)/g);
128
-
129
124
  for (const segment of segments) {
130
125
  if (!segment) continue;
131
-
132
126
  if (segment.startsWith("\x1b[") && segment.endsWith("m")) {
133
127
  const command = segment.slice(2, -1);
134
128
  resetAndApplyNewCodes(command);
@@ -142,28 +136,23 @@ export function ansiToHtml(text) {
142
136
  html += escapedContent;
143
137
  }
144
138
  }
145
-
146
139
  if (openSpan) {
147
140
  html += "</span>";
148
141
  }
149
-
150
142
  return html;
151
143
  }
144
+
152
145
  function sanitizeHTML(str) {
153
146
  if (str === null || str === undefined) return "";
154
- return String(str).replace(/[&<>"']/g, (match) => {
155
- const replacements = {
156
- "&": "&",
157
- "<": "<",
158
- ">": ">",
159
- '"': '"',
160
- "'": "'", // or '
161
- };
162
- return replacements[match] || match;
163
- });
147
+ return String(str).replace(
148
+ /[&<>"']/g,
149
+ (match) =>
150
+ ({ "&": "&", "<": "<", ">": ">", '"': '"', "'": "'" }[match] || match)
151
+ );
164
152
  }
153
+
165
154
  function capitalize(str) {
166
- if (!str) return ""; // Handle empty string
155
+ if (!str) return "";
167
156
  return str[0].toUpperCase() + str.slice(1).toLowerCase();
168
157
  }
169
158
  function formatPlaywrightError(error) {
@@ -1304,11 +1293,12 @@ function generateHTML(reportData, trendData = null) {
1304
1293
  runSummary.totalTests > 0
1305
1294
  ? formatDuration(runSummary.duration / runSummary.totalTests)
1306
1295
  : "0.0s";
1296
+
1307
1297
  function generateTestCasesHTML() {
1308
1298
  if (!results || results.length === 0)
1309
1299
  return '<div class="no-tests">No test results found in this run.</div>';
1310
1300
  return results
1311
- .map((test, index) => {
1301
+ .map((test) => {
1312
1302
  const browser = test.browser || "unknown";
1313
1303
  const testFileParts = test.name.split(" > ");
1314
1304
  const testTitle =
@@ -1324,319 +1314,229 @@ function generateHTML(reportData, trendData = null) {
1324
1314
  ? `step-hook step-hook-${step.hookType}`
1325
1315
  : "";
1326
1316
  const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
1327
- return `
1328
- <div class="step-item" style="--depth: ${depth};">
1329
- <div class="step-header ${stepClass}" role="button" aria-expanded="false">
1330
- <span class="step-icon">${getStatusIcon(step.status)}</span>
1331
- <span class="step-title">${sanitizeHTML(
1317
+ return `<div class="step-item" style="--depth: ${depth};"><div class="step-header ${stepClass}" role="button" aria-expanded="false"><span class="step-icon">${getStatusIcon(
1318
+ step.status
1319
+ )}</span><span class="step-title">${sanitizeHTML(
1332
1320
  step.title
1333
- )}${hookIndicator}</span>
1334
- <span class="step-duration">${formatDuration(
1321
+ )}${hookIndicator}</span><span class="step-duration">${formatDuration(
1335
1322
  step.duration
1336
- )}</span>
1337
- </div>
1338
- <div class="step-details" style="display: none;">
1339
- ${
1323
+ )}</span></div><div class="step-details" style="display: none;">${
1340
1324
  step.codeLocation
1341
1325
  ? `<div class="step-info"><strong>Location:</strong> ${sanitizeHTML(
1342
1326
  step.codeLocation
1343
1327
  )}</div>`
1344
1328
  : ""
1345
- }
1346
- ${
1329
+ }${
1347
1330
  step.errorMessage
1348
- ? `<div class="step-error">
1349
- ${
1350
- step.stackTrace
1351
- ? `<div class="stack-trace">${formatPlaywrightError(
1352
- step.stackTrace
1353
- )}</div>`
1354
- : ""
1355
- }
1356
- <button
1357
- class="copy-error-btn"
1358
- onclick="copyErrorToClipboard(this)"
1359
- style="
1360
- margin-top: 8px;
1361
- padding: 4px 8px;
1362
- background: #f0f0f0;
1363
- border: 2px solid #ccc;
1364
- border-radius: 4px;
1365
- cursor: pointer;
1366
- font-size: 12px;
1367
- border-color: #8B0000;
1368
- color: #8B0000;
1369
- "
1370
- onmouseover="this.style.background='#e0e0e0'"
1371
- onmouseout="this.style.background='#f0f0f0'"
1372
- >
1373
- Copy Error Prompt
1374
- </button>
1375
- </div>`
1331
+ ? `<div class="step-error">${
1332
+ step.stackTrace
1333
+ ? `<div class="stack-trace">${formatPlaywrightError(
1334
+ step.stackTrace
1335
+ )}</div>`
1336
+ : ""
1337
+ }<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
1376
1338
  : ""
1377
- }
1378
- ${
1339
+ }${
1379
1340
  hasNestedSteps
1380
1341
  ? `<div class="nested-steps">${generateStepsHTML(
1381
1342
  step.steps,
1382
1343
  depth + 1
1383
1344
  )}</div>`
1384
1345
  : ""
1385
- }
1386
- </div>
1387
- </div>`;
1346
+ }</div></div>`;
1388
1347
  })
1389
1348
  .join("");
1390
1349
  };
1391
-
1392
- // Local escapeHTML for screenshot rendering part, ensuring it uses proper entities
1393
- const escapeHTMLForScreenshots = (str) => {
1394
- if (str === null || str === undefined) return "";
1395
- return String(str).replace(
1396
- /[&<>"']/g,
1397
- (match) =>
1398
- ({ "&": "&", "<": "<", ">": ">", '"': '"', "'": "'" }[match] ||
1399
- match)
1400
- );
1401
- };
1402
- return `
1403
- <div class="test-case" data-status="${
1404
- test.status
1405
- }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
1350
+ return `<div class="test-case" data-status="${
1351
+ test.status
1352
+ }" data-browser="${sanitizeHTML(browser)}" data-tags="${(
1353
+ test.tags || []
1354
+ )
1406
1355
  .join(",")
1407
1356
  .toLowerCase()}">
1408
- <div class="test-case-header" role="button" aria-expanded="false">
1409
- <div class="test-case-summary">
1410
- <span class="status-badge ${getStatusClass(test.status)}">${String(
1357
+ <div class="test-case-header" role="button" aria-expanded="false"><div class="test-case-summary"><span class="status-badge ${getStatusClass(
1358
+ test.status
1359
+ )}">${String(
1411
1360
  test.status
1412
- ).toUpperCase()}</span>
1413
- <span class="test-case-title" title="${sanitizeHTML(
1414
- test.name
1415
- )}">${sanitizeHTML(testTitle)}</span>
1416
- <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
1417
- </div>
1418
- <div class="test-case-meta">
1419
- ${
1420
- test.tags && test.tags.length > 0
1421
- ? test.tags
1422
- .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
1423
- .join(" ")
1424
- : ""
1425
- }
1426
- <span class="test-duration">${formatDuration(test.duration)}</span>
1427
- </div>
1428
- </div>
1429
- <div class="test-case-content" style="display: none;">
1430
- <p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
1431
- ${
1432
- test.errorMessage
1433
- ? `<div class="test-error-summary">${formatPlaywrightError(
1434
- test.errorMessage
1435
- )}
1436
- <button
1437
- class="copy-error-btn"
1438
- onclick="copyErrorToClipboard(this)"
1439
- style="
1440
- margin-top: 8px;
1441
- padding: 4px 8px;
1442
- background: #f0f0f0;
1443
- border: 2px solid #ccc;
1444
- border-radius: 4px;
1445
- cursor: pointer;
1446
- font-size: 12px;
1447
- border-color: #8B0000;
1448
- color: #8B0000;
1449
- "
1450
- onmouseover="this.style.background='#e0e0e0'"
1451
- onmouseout="this.style.background='#f0f0f0'"
1452
- >
1453
- Copy Error Prompt
1454
- </button>
1455
- </div>`
1456
- : ""
1457
- }
1458
- <h4>Steps</h4>
1459
- <div class="steps-list">${generateStepsHTML(test.steps)}</div>
1460
- ${
1461
- test.stdout && test.stdout.length > 0
1462
- ? `<div class="console-output-section"><h4>Console Output (stdout)</h4><pre class="console-log stdout-log" style="background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${test.stdout
1463
- .map((line) => sanitizeHTML(line))
1464
- .join("\n")}</pre></div>`
1465
- : ""
1466
- }
1467
- ${
1468
- test.stderr && test.stderr.length > 0
1469
- ? `<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;">${test.stderr
1470
- .map((line) => sanitizeHTML(line))
1471
- .join("\n")}</pre></div>`
1472
- : ""
1473
- }
1474
- ${(() => {
1475
- // Screenshots
1476
- if (!test.screenshots || test.screenshots.length === 0) return "";
1477
- const baseOutputDir = path.resolve(
1478
- process.cwd(),
1479
- DEFAULT_OUTPUT_DIR
1480
- );
1481
-
1482
- const renderScreenshot = (screenshotPathOrData, index) => {
1483
- let base64ImageData = "";
1484
- const uniqueSuffix = `${Date.now()}-${index}-${Math.random()
1485
- .toString(36)
1486
- .substring(2, 7)}`;
1487
- try {
1488
- if (
1489
- typeof screenshotPathOrData === "string" &&
1490
- !screenshotPathOrData.startsWith("data:image")
1491
- ) {
1492
- const imagePath = path.resolve(
1493
- baseOutputDir,
1494
- screenshotPathOrData
1495
- );
1496
- if (fsExistsSync(imagePath))
1497
- base64ImageData =
1498
- readFileSync(imagePath).toString("base64");
1499
- else {
1500
- console.warn(
1501
- chalk.yellow(
1502
- `[Reporter] Screenshot file not found: ${imagePath}`
1503
- )
1504
- );
1505
- return `<div class="attachment-item error" style="padding:10px; color:red;">Screenshot not found: ${escapeHTMLForScreenshots(
1506
- screenshotPathOrData
1507
- )}</div>`;
1508
- }
1509
- } else if (
1510
- typeof screenshotPathOrData === "string" &&
1511
- screenshotPathOrData.startsWith("data:image/png;base64,")
1512
- )
1513
- base64ImageData = screenshotPathOrData.substring(
1514
- "data:image/png;base64,".length
1515
- );
1516
- else if (typeof screenshotPathOrData === "string")
1517
- base64ImageData = screenshotPathOrData;
1518
- else {
1519
- console.warn(
1520
- chalk.yellow(
1521
- `[Reporter] Invalid screenshot data type for item at index ${index}.`
1522
- )
1523
- );
1524
- return `<div class="attachment-item error" style="padding:10px; color:red;">Invalid screenshot data</div>`;
1525
- }
1526
- if (!base64ImageData) {
1527
- console.warn(
1528
- chalk.yellow(
1529
- `[Reporter] Could not obtain base64 data for screenshot: ${escapeHTMLForScreenshots(
1530
- String(screenshotPathOrData)
1531
- )}`
1532
- )
1533
- );
1534
- return `<div class="attachment-item error" style="padding:10px; color:red;">Error loading screenshot: ${escapeHTMLForScreenshots(
1535
- String(screenshotPathOrData)
1536
- )}</div>`;
1537
- }
1538
- return `<div class="attachment-item"><img src="data:image/png;base64,${base64ImageData}" alt="Screenshot ${
1539
- index + 1
1540
- }" loading="lazy" onerror="this.alt='Error displaying embedded image'; this.style.display='none'; this.parentElement.innerHTML='<p style=\\'color:red;padding:10px;\\'>Error displaying screenshot ${
1541
- index + 1
1542
- }.</p>';"><div class="attachment-info"><div class="trace-actions"><a href="data:image/png;base64,${base64ImageData}" target="_blank" class="view-full">View Full Image</a><a href="data:image/png;base64,${base64ImageData}" target="_blank" download="screenshot-${uniqueSuffix}.png">Download</a></div></div></div>`;
1543
- } catch (e) {
1544
- console.error(
1545
- chalk.red(
1546
- `[Reporter] Error processing screenshot ${escapeHTMLForScreenshots(
1547
- String(screenshotPathOrData)
1548
- )}: ${e.message}`
1549
- )
1550
- );
1551
- return `<div class="attachment-item error" style="padding:10px; color:red;">Failed to load screenshot: ${escapeHTMLForScreenshots(
1552
- String(screenshotPathOrData)
1553
- )}</div>`;
1554
- }
1555
- };
1556
- return `<div class="attachments-section"><h4>Screenshots (${
1557
- test.screenshots.length
1558
- })</h4><div class="attachments-grid">${test.screenshots
1559
- .map(renderScreenshot)
1560
- .join("")}</div></div>`;
1561
- })()}
1562
- ${
1563
- test.videoPath
1564
- ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${(() => {
1565
- // Videos
1566
- const videos = Array.isArray(test.videoPath)
1567
- ? test.videoPath
1568
- : [test.videoPath];
1569
- const mimeTypes = {
1570
- mp4: "video/mp4",
1571
- webm: "video/webm",
1572
- ogg: "video/ogg",
1573
- mov: "video/quicktime",
1574
- avi: "video/x-msvideo",
1575
- };
1576
- return videos
1577
- .map((video, index) => {
1578
- const videoUrl =
1579
- typeof video === "object" ? video.url || "" : video;
1580
- const videoName =
1581
- typeof video === "object"
1582
- ? video.name || `Video ${index + 1}`
1583
- : `Video ${index + 1}`;
1584
- const fileExtension = String(videoUrl)
1585
- .split(".")
1586
- .pop()
1587
- .toLowerCase();
1588
- const mimeType = mimeTypes[fileExtension] || "video/mp4";
1589
- return `<div class="attachment-item"><video controls width="100%" height="auto" title="${sanitizeHTML(
1590
- videoName
1591
- )}"><source src="${sanitizeHTML(
1592
- videoUrl
1593
- )}" type="${mimeType}">Your browser does not support the video tag.</video><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
1594
- videoUrl
1595
- )}" target="_blank" download="${sanitizeHTML(
1596
- videoName
1597
- )}.${fileExtension}">Download</a></div></div></div>`;
1598
- })
1599
- .join("");
1600
- })()}</div></div>`
1601
- : ""
1602
- }
1603
- ${
1604
- test.tracePath
1605
- ? `<div class="attachments-section"><h4>Trace Files</h4><div class="attachments-grid">${(() => {
1606
- // Traces
1607
- const traces = Array.isArray(test.tracePath)
1608
- ? test.tracePath
1609
- : [test.tracePath];
1610
- return traces
1611
- .map((trace, index) => {
1612
- const traceUrl =
1613
- typeof trace === "object" ? trace.url || "" : trace;
1614
- const traceName =
1615
- typeof trace === "object"
1616
- ? trace.name || `Trace ${index + 1}`
1617
- : `Trace ${index + 1}`;
1618
- const traceFileName = String(traceUrl).split("/").pop();
1619
- return `<div class="attachment-item"><div class="trace-preview"><span class="trace-icon">📄</span><span class="trace-name">${sanitizeHTML(
1620
- traceName
1621
- )}</span></div><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
1622
- traceUrl
1623
- )}" target="_blank" download="${sanitizeHTML(
1624
- traceFileName
1625
- )}" class="download-trace">Download</a></div></div></div>`;
1626
- })
1627
- .join("");
1628
- })()}</div></div>`
1629
- : ""
1630
- }
1631
- ${
1632
- test.codeSnippet
1633
- ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${sanitizeHTML(
1634
- test.codeSnippet
1635
- )}</code></pre></div>`
1636
- : ""
1637
- }
1638
- </div>
1639
- </div>`;
1361
+ ).toUpperCase()}</span><span class="test-case-title" title="${sanitizeHTML(
1362
+ test.name
1363
+ )}">${sanitizeHTML(
1364
+ testTitle
1365
+ )}</span><span class="test-case-browser">(${sanitizeHTML(
1366
+ browser
1367
+ )})</span></div><div class="test-case-meta">${
1368
+ test.tags && test.tags.length > 0
1369
+ ? test.tags
1370
+ .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
1371
+ .join(" ")
1372
+ : ""
1373
+ }<span class="test-duration">${formatDuration(
1374
+ test.duration
1375
+ )}</span></div></div>
1376
+ <div class="test-case-content" style="display: none;">
1377
+ <p><strong>Full Path:</strong> ${sanitizeHTML(
1378
+ test.name
1379
+ )}</p>
1380
+ ${
1381
+ test.errorMessage
1382
+ ? `<div class="test-error-summary">${formatPlaywrightError(
1383
+ test.errorMessage
1384
+ )}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
1385
+ : ""
1386
+ }
1387
+ <h4>Steps</h4><div class="steps-list">${generateStepsHTML(
1388
+ test.steps
1389
+ )}</div>
1390
+ ${
1391
+ test.stdout && test.stdout.length > 0
1392
+ ? `<div class="console-output-section"><h4>Console Output (stdout)</h4><pre class="console-log stdout-log">${formatPlaywrightError(
1393
+ test.stdout.join("\\n")
1394
+ )}</pre></div>`
1395
+ : ""
1396
+ }
1397
+ ${
1398
+ test.stderr && test.stderr.length > 0
1399
+ ? `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log">${test.stderr
1400
+ .map((line) => sanitizeHTML(line))
1401
+ .join("\\n")}</pre></div>`
1402
+ : ""
1403
+ }
1404
+
1405
+ ${(() => {
1406
+ if (
1407
+ !test.screenshots ||
1408
+ test.screenshots.length === 0
1409
+ )
1410
+ return "";
1411
+ return `<div class="attachments-section"><h4>Screenshots</h4><div class="attachments-grid">${test.screenshots
1412
+ .map((screenshotPath, index) => {
1413
+ try {
1414
+ const imagePath = path.resolve(
1415
+ DEFAULT_OUTPUT_DIR,
1416
+ screenshotPath
1417
+ );
1418
+ if (!fsExistsSync(imagePath))
1419
+ return `<div class="attachment-item error">Screenshot not found: ${sanitizeHTML(
1420
+ screenshotPath
1421
+ )}</div>`;
1422
+ const base64ImageData =
1423
+ readFileSync(imagePath).toString("base64");
1424
+ return `<div class="attachment-item"><img src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" data-src="data:image/png;base64,${base64ImageData}" alt="Screenshot ${
1425
+ index + 1
1426
+ }" class="lazy-load-image"><div class="attachment-info"><div class="trace-actions"><a href="data:image/png;base64,${base64ImageData}" target="_blank" download="screenshot-${index}.png">Download</a></div></div></div>`;
1427
+ } catch (e) {
1428
+ return `<div class="attachment-item error">Failed to load screenshot: ${sanitizeHTML(
1429
+ screenshotPath
1430
+ )}</div>`;
1431
+ }
1432
+ })
1433
+ .join("")}</div></div>`;
1434
+ })()}
1435
+
1436
+ ${(() => {
1437
+ if (!test.videoPath || test.videoPath.length === 0)
1438
+ return "";
1439
+ return `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
1440
+ .map((videoPath, index) => {
1441
+ try {
1442
+ const videoFilePath = path.resolve(
1443
+ DEFAULT_OUTPUT_DIR,
1444
+ videoPath
1445
+ );
1446
+ if (!fsExistsSync(videoFilePath))
1447
+ return `<div class="attachment-item error">Video not found: ${sanitizeHTML(
1448
+ videoPath
1449
+ )}</div>`;
1450
+ const videoBase64 =
1451
+ readFileSync(videoFilePath).toString(
1452
+ "base64"
1453
+ );
1454
+ const fileExtension = path
1455
+ .extname(videoPath)
1456
+ .slice(1)
1457
+ .toLowerCase();
1458
+ const mimeType =
1459
+ {
1460
+ mp4: "video/mp4",
1461
+ webm: "video/webm",
1462
+ ogg: "video/ogg",
1463
+ mov: "video/quicktime",
1464
+ avi: "video/x-msvideo",
1465
+ }[fileExtension] || "video/mp4";
1466
+ const videoDataUri = `data:${mimeType};base64,${videoBase64}`;
1467
+ 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="${videoDataUri}" target="_blank" download="video-${index}.${fileExtension}">Download</a></div></div></div>`;
1468
+ } catch (e) {
1469
+ return `<div class="attachment-item error">Failed to load video: ${sanitizeHTML(
1470
+ videoPath
1471
+ )}</div>`;
1472
+ }
1473
+ })
1474
+ .join("")}</div></div>`;
1475
+ })()}
1476
+
1477
+ ${(() => {
1478
+ if (!test.tracePath) return "";
1479
+ try {
1480
+ const traceFilePath = path.resolve(
1481
+ DEFAULT_OUTPUT_DIR,
1482
+ test.tracePath
1483
+ );
1484
+ if (!fsExistsSync(traceFilePath))
1485
+ return `<div class="attachments-section"><h4>Trace File</h4><div class="attachment-item error">Trace file not found: ${sanitizeHTML(
1486
+ test.tracePath
1487
+ )}</div></div>`;
1488
+ const traceBase64 =
1489
+ readFileSync(traceFilePath).toString("base64");
1490
+ const traceDataUri = `data:application/zip;base64,${traceBase64}`;
1491
+ 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>`;
1492
+ } catch (e) {
1493
+ return `<div class="attachments-section"><h4>Trace File</h4><div class="attachment-item error">Failed to load trace file.</div></div>`;
1494
+ }
1495
+ })()}
1496
+
1497
+ ${(() => {
1498
+ if (
1499
+ !test.attachments ||
1500
+ test.attachments.length === 0
1501
+ )
1502
+ return "";
1503
+ return `<div class="attachments-section"><h4>Other Attachments</h4><div class="attachments-grid">${test.attachments
1504
+ .map((attachment) => {
1505
+ try {
1506
+ const attachmentPath = path.resolve(
1507
+ DEFAULT_OUTPUT_DIR,
1508
+ attachment.path
1509
+ );
1510
+ if (!fsExistsSync(attachmentPath))
1511
+ return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
1512
+ attachment.name
1513
+ )}</div>`;
1514
+ const attachmentBase64 =
1515
+ readFileSync(attachmentPath).toString(
1516
+ "base64"
1517
+ );
1518
+ const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
1519
+ return `<div class="attachment-item generic-attachment"><div class="attachment-icon">${getAttachmentIcon(
1520
+ attachment.contentType
1521
+ )}</div><div class="attachment-caption"><span class="attachment-name" title="${sanitizeHTML(
1522
+ attachment.name
1523
+ )}">${sanitizeHTML(
1524
+ attachment.name
1525
+ )}</span><span class="attachment-type">${sanitizeHTML(
1526
+ attachment.contentType
1527
+ )}</span></div><div class="attachment-info"><div class="trace-actions"><a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
1528
+ attachment.name
1529
+ )}">Download</a></div></div></div>`;
1530
+ } catch (e) {
1531
+ return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(
1532
+ attachment.name
1533
+ )}</div>`;
1534
+ }
1535
+ })
1536
+ .join("")}</div></div>`;
1537
+ })()}
1538
+ </div>
1539
+ </div>`;
1640
1540
  })
1641
1541
  .join("");
1642
1542
  }
@@ -1668,7 +1568,6 @@ function generateHTML(reportData, trendData = null) {
1668
1568
  .highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
1669
1569
  .highcharts-axis-title { fill: var(--text-color) !important; }
1670
1570
  .highcharts-tooltip > span { background-color: rgba(10,10,10,0.92) !important; border-color: rgba(10,10,10,0.92) !important; color: #f5f5f5 !important; padding: 10px !important; border-radius: 6px !important; }
1671
-
1672
1571
  body { font-family: var(--font-family); margin: 0; background-color: var(--background-color); color: var(--text-color); line-height: 1.65; font-size: 16px; }
1673
1572
  .container { padding: 30px; border-radius: var(--border-radius); box-shadow: var(--box-shadow); background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec); }
1674
1573
  .header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; padding-bottom: 25px; border-bottom: 1px solid var(--border-color); margin-bottom: 25px; }
@@ -1763,7 +1662,8 @@ function generateHTML(reportData, trendData = null) {
1763
1662
  .attachments-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 22px; }
1764
1663
  .attachment-item { border: 1px solid var(--border-color); border-radius: var(--border-radius); background-color: #fff; box-shadow: var(--box-shadow-light); overflow: hidden; display: flex; flex-direction: column; transition: transform 0.2s ease-out, box-shadow 0.2s ease-out; }
1765
1664
  .attachment-item:hover { transform: translateY(-4px); box-shadow: var(--box-shadow); }
1766
- .attachment-item img { width: 100%; height: 180px; object-fit: cover; display: block; border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease; }
1665
+ .attachment-item img, .attachment-item video { width: 100%; height: 180px; object-fit: cover; display: block; background-color: #eee; border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease; }
1666
+ .attachment-info { padding: 12px; margin-top: auto; background-color: #fafafa;}
1767
1667
  .attachment-item a:hover img { opacity: 0.85; }
1768
1668
  .attachment-caption { padding: 12px 15px; font-size: 0.9em; text-align: center; color: var(--text-color-secondary); word-break: break-word; background-color: var(--light-gray-color); }
1769
1669
  .video-item a, .trace-item a { display: block; margin-bottom: 8px; color: var(--primary-color); text-decoration: none; font-weight: 500; }
@@ -1807,6 +1707,12 @@ function generateHTML(reportData, trendData = null) {
1807
1707
  @media (max-width: 992px) { .dashboard-bottom-row { grid-template-columns: 1fr; } .pie-chart-wrapper div[id^="pieChart-"] { max-width: 350px; margin: 0 auto; } .filters input { min-width: 180px; } .filters select { min-width: 150px; } }
1808
1708
  @media (max-width: 768px) { body { font-size: 15px; } .container { margin: 10px; padding: 20px; } .header { flex-direction: column; align-items: flex-start; gap: 15px; } .header h1 { font-size: 1.6em; } .run-info { text-align: left; font-size:0.9em; } .tabs { margin-bottom: 25px;} .tab-button { padding: 12px 20px; font-size: 1.05em;} .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;} .summary-card .value {font-size: 2em;} .summary-card h3 {font-size: 0.95em;} .filters { flex-direction: column; padding: 18px; gap: 12px;} .filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;} .test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; } .test-case-summary {gap: 10px;} .test-case-title {font-size: 1.05em;} .test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;} .attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;} .test-history-grid {grid-template-columns: 1fr;} .pie-chart-wrapper {min-height: auto;} }
1809
1709
  @media (max-width: 480px) { body {font-size: 14px;} .container {padding: 15px;} .header h1 {font-size: 1.4em;} #report-logo { height: 35px; width: 35px; } .tab-button {padding: 10px 15px; font-size: 1em;} .summary-card .value {font-size: 1.8em;} .attachments-grid {grid-template-columns: 1fr;} .step-item {padding-left: calc(var(--depth, 0) * 18px);} .test-case-content, .step-details {padding: 15px;} .trend-charts-row {gap: 20px;} .trend-chart {padding: 20px;} }
1710
+ .trace-actions a { text-decoration: none; color: var(--primary-color); font-weight: 500; font-size: 0.9em; }
1711
+ .generic-attachment { text-align: center; padding: 1rem; justify-content: center; align-items: center; }
1712
+ .attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
1713
+ .attachment-caption { display: flex; flex-direction: column; align-items: center; justify-content: center; flex-grow: 1; }
1714
+ .attachment-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
1715
+ .attachment-type { font-size: 0.8rem; color: var(--text-color-secondary); }
1810
1716
  </style>
1811
1717
  </head>
1812
1718
  <body>
@@ -2035,84 +1941,115 @@ function generateHTML(reportData, trendData = null) {
2035
1941
  if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
2036
1942
  if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
2037
1943
  // --- Intersection Observer for Lazy Loading ---
2038
- const lazyLoadElements = document.querySelectorAll('.lazy-load-chart, .lazy-load-iframe');
1944
+ const lazyLoadElements = document.querySelectorAll('.lazy-load-chart, .lazy-load-iframe, .lazy-load-image, .lazy-load-video, .lazy-load-attachment');
2039
1945
  if ('IntersectionObserver' in window) {
2040
1946
  let lazyObserver = new IntersectionObserver((entries, observer) => {
2041
1947
  entries.forEach(entry => {
2042
1948
  if (entry.isIntersecting) {
2043
1949
  const element = entry.target;
2044
- if (element.classList.contains('lazy-load-iframe')) {
1950
+ if (element.classList.contains('lazy-load-image')) {
2045
1951
  if (element.dataset.src) {
2046
1952
  element.src = element.dataset.src;
2047
- element.removeAttribute('data-src'); // Optional: remove data-src after loading
2048
- console.log('Lazy loaded iframe:', element.title || 'Untitled Iframe');
1953
+ element.removeAttribute('data-src');
1954
+ }
1955
+ } else if (element.classList.contains('lazy-load-video')) {
1956
+ const source = element.querySelector('source');
1957
+ if (source && source.dataset.src) {
1958
+ source.src = source.dataset.src;
1959
+ source.removeAttribute('data-src');
1960
+ element.load();
1961
+ }
1962
+ } else if (element.classList.contains('lazy-load-attachment')) {
1963
+ if (element.dataset.href) {
1964
+ element.href = element.dataset.href;
1965
+ element.removeAttribute('data-href');
1966
+ }
1967
+ } else if (element.classList.contains('lazy-load-iframe')) {
1968
+ if (element.dataset.src) {
1969
+ element.src = element.dataset.src;
1970
+ element.removeAttribute('data-src');
2049
1971
  }
2050
1972
  } else if (element.classList.contains('lazy-load-chart')) {
2051
1973
  const renderFunctionName = element.dataset.renderFunctionName;
2052
1974
  if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
2053
- try {
2054
- console.log('Lazy loading chart with function:', renderFunctionName);
2055
- window[renderFunctionName](); // Call the render function
2056
- } catch (e) {
2057
- console.error(\`Error lazy-loading chart \${element.id} using \${renderFunctionName}:\`, e);
2058
- element.innerHTML = '<div class="no-data-chart">Error lazy-loading chart.</div>';
2059
- }
2060
- } else {
2061
- console.warn(\`Render function \${renderFunctionName} not found or not a function for chart:\`, element.id);
1975
+ window[renderFunctionName]();
2062
1976
  }
2063
1977
  }
2064
- observer.unobserve(element); // Important: stop observing once loaded
1978
+ observer.unobserve(element);
2065
1979
  }
2066
1980
  });
2067
- }, {
2068
- rootMargin: "0px 0px 200px 0px" // Start loading when element is 200px from viewport bottom
2069
- });
2070
-
2071
- lazyLoadElements.forEach(el => {
2072
- lazyObserver.observe(el);
2073
- });
1981
+ }, { rootMargin: "0px 0px 200px 0px" });
1982
+ lazyLoadElements.forEach(el => lazyObserver.observe(el));
2074
1983
  } else { // Fallback for browsers without IntersectionObserver
2075
- console.warn("IntersectionObserver not supported. Loading all items immediately.");
2076
1984
  lazyLoadElements.forEach(element => {
2077
- if (element.classList.contains('lazy-load-iframe')) {
2078
- if (element.dataset.src) {
2079
- element.src = element.dataset.src;
2080
- element.removeAttribute('data-src');
2081
- }
2082
- } else if (element.classList.contains('lazy-load-chart')) {
2083
- const renderFunctionName = element.dataset.renderFunctionName;
2084
- if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
2085
- try {
2086
- window[renderFunctionName]();
2087
- } catch (e) {
2088
- console.error(\`Error loading chart (fallback) \${element.id} using \${renderFunctionName}:\`, e);
2089
- element.innerHTML = '<div class="no-data-chart">Error loading chart (fallback).</div>';
2090
- }
2091
- }
2092
- }
1985
+ if (element.classList.contains('lazy-load-image') && element.dataset.src) element.src = element.dataset.src;
1986
+ else if (element.classList.contains('lazy-load-video')) { const source = element.querySelector('source'); if (source && source.dataset.src) { source.src = source.dataset.src; element.load(); } }
1987
+ else if (element.classList.contains('lazy-load-attachment') && element.dataset.href) element.href = element.dataset.href;
1988
+ else if (element.classList.contains('lazy-load-iframe') && element.dataset.src) element.src = element.dataset.src;
1989
+ else if (element.classList.contains('lazy-load-chart')) { const renderFn = element.dataset.renderFunctionName; if(renderFn && window[renderFn]) window[renderFn](); }
2093
1990
  });
2094
1991
  }
2095
1992
  }
2096
1993
  document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
2097
1994
 
2098
1995
  function copyErrorToClipboard(button) {
2099
- const errorContainer = button.closest('.step-error');
2100
- const errorText = errorContainer.querySelector('.stack-trace').textContent;
2101
- const textarea = document.createElement('textarea');
2102
- textarea.value = errorText;
2103
- document.body.appendChild(textarea);
2104
- textarea.select();
2105
- try {
2106
- const successful = document.execCommand('copy');
2107
- const originalText = button.textContent;
2108
- button.textContent = successful ? 'Copied!' : 'Failed to copy';
2109
- setTimeout(() => {
2110
- button.textContent = originalText;
2111
- }, 2000);
2112
- } catch (err) {
2113
- console.error('Failed to copy: ', err);
2114
- button.textContent = 'Failed to copy';
2115
- }
1996
+ // 1. Find the main error container, which should always be present.
1997
+ const errorContainer = button.closest('.step-error');
1998
+ if (!errorContainer) {
1999
+ console.error("Could not find '.step-error' container. The report's HTML structure might have changed.");
2000
+ return;
2001
+ }
2002
+
2003
+ let errorText;
2004
+
2005
+ // 2. First, try to find the preferred .stack-trace element (the "happy path").
2006
+ const stackTraceElement = errorContainer.querySelector('.stack-trace');
2007
+
2008
+ if (stackTraceElement) {
2009
+ // If it exists, use its text content. This handles standard assertion errors.
2010
+ errorText = stackTraceElement.textContent;
2011
+ } else {
2012
+ // 3. FALLBACK: If .stack-trace doesn't exist, this is likely an unstructured error.
2013
+ // We clone the container to avoid manipulating the live DOM or copying the button's own text.
2014
+ const clonedContainer = errorContainer.cloneNode(true);
2015
+
2016
+ // Remove the button from our clone before extracting the text.
2017
+ const buttonInClone = clonedContainer.querySelector('button');
2018
+ if (buttonInClone) {
2019
+ buttonInClone.remove();
2020
+ }
2021
+
2022
+ // Use the text content of the cleaned container as the fallback.
2023
+ errorText = clonedContainer.textContent;
2024
+ }
2025
+
2026
+ // 4. Proceed with the clipboard logic, ensuring text is not null and is trimmed.
2027
+ if (!errorText) {
2028
+ console.error('Could not extract error text.');
2029
+ button.textContent = 'Nothing to copy';
2030
+ setTimeout(() => { button.textContent = 'Copy Error'; }, 2000);
2031
+ return;
2032
+ }
2033
+
2034
+ const textarea = document.createElement('textarea');
2035
+ textarea.value = errorText.trim(); // Trim whitespace for a cleaner copy.
2036
+ textarea.style.position = 'fixed'; // Prevent screen scroll
2037
+ textarea.style.top = '-9999px';
2038
+ document.body.appendChild(textarea);
2039
+ textarea.select();
2040
+
2041
+ try {
2042
+ const successful = document.execCommand('copy');
2043
+ const originalText = button.textContent;
2044
+ button.textContent = successful ? 'Copied!' : 'Failed';
2045
+ setTimeout(() => {
2046
+ button.textContent = originalText;
2047
+ }, 2000);
2048
+ } catch (err) {
2049
+ console.error('Failed to copy: ', err);
2050
+ button.textContent = 'Failed';
2051
+ }
2052
+
2116
2053
  document.body.removeChild(textarea);
2117
2054
  }
2118
2055
  </script>