@arghajit/dummy 0.1.0-beta-18 → 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;
@@ -157,13 +157,13 @@ function sanitizeHTML(str) {
157
157
  "<": "<",
158
158
  ">": ">",
159
159
  '"': '"',
160
- "'": "'", // or '
160
+ "'": "'",
161
161
  };
162
162
  return replacements[match] || match;
163
163
  });
164
164
  }
165
165
  function capitalize(str) {
166
- if (!str) return ""; // Handle empty string
166
+ if (!str) return "";
167
167
  return str[0].toUpperCase() + str.slice(1).toLowerCase();
168
168
  }
169
169
  function formatPlaywrightError(error) {
@@ -171,22 +171,18 @@ function formatPlaywrightError(error) {
171
171
  return convertPlaywrightErrorToHTML(commandOutput);
172
172
  }
173
173
  function convertPlaywrightErrorToHTML(str) {
174
- return (
175
- str
176
- // Convert leading spaces to &nbsp; and tabs to &nbsp;&nbsp;&nbsp;&nbsp;
177
- .replace(/^(\s+)/gm, (match) =>
178
- match.replace(/ /g, "&nbsp;").replace(/\t/g, "&nbsp;&nbsp;")
179
- )
180
- // Color and style replacements
181
- .replace(/<red>/g, '<span style="color: red;">')
182
- .replace(/<green>/g, '<span style="color: green;">')
183
- .replace(/<dim>/g, '<span style="opacity: 0.6;">')
184
- .replace(/<intensity>/g, '<span style="font-weight: bold;">') // Changed to apply bold
185
- .replace(/<\/color>/g, "</span>")
186
- .replace(/<\/intensity>/g, "</span>")
187
- // Convert newlines to <br> after processing other replacements
188
- .replace(/\n/g, "<br>")
189
- );
174
+ if (!str) return "";
175
+ return str
176
+ .replace(/^(\s+)/gm, (match) =>
177
+ match.replace(/ /g, " ").replace(/\t/g, " ")
178
+ )
179
+ .replace(/<red>/g, '<span style="color: red;">')
180
+ .replace(/<green>/g, '<span style="color: green;">')
181
+ .replace(/<dim>/g, '<span style="opacity: 0.6;">')
182
+ .replace(/<intensity>/g, '<span style="font-weight: bold;">')
183
+ .replace(/<\/color>/g, "</span>")
184
+ .replace(/<\/intensity>/g, "</span>")
185
+ .replace(/\n/g, "<br>");
190
186
  }
191
187
  function formatDuration(ms, options = {}) {
192
188
  const {
@@ -227,19 +223,12 @@ function formatDuration(ms, options = {}) {
227
223
 
228
224
  const totalRawSeconds = numMs / MS_PER_SECOND;
229
225
 
230
- // Decision: Are we going to display hours or minutes?
231
- // This happens if the duration is inherently >= 1 minute OR
232
- // if it's < 1 minute but ceiling the seconds makes it >= 1 minute.
233
226
  if (
234
227
  totalRawSeconds < SECONDS_PER_MINUTE &&
235
228
  Math.ceil(totalRawSeconds) < SECONDS_PER_MINUTE
236
229
  ) {
237
- // Strictly seconds-only display, use precision.
238
230
  return `${totalRawSeconds.toFixed(validPrecision)}s`;
239
231
  } else {
240
- // Display will include minutes and/or hours, or seconds round up to a minute.
241
- // Seconds part should be an integer (ceiling).
242
- // Round the total milliseconds UP to the nearest full second.
243
232
  const totalMsRoundedUpToSecond =
244
233
  Math.ceil(numMs / MS_PER_SECOND) * MS_PER_SECOND;
245
234
 
@@ -251,21 +240,15 @@ function formatDuration(ms, options = {}) {
251
240
  const m = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_MINUTE));
252
241
  remainingMs %= MS_PER_SECOND * SECONDS_PER_MINUTE;
253
242
 
254
- const s = Math.floor(remainingMs / MS_PER_SECOND); // This will be an integer
243
+ const s = Math.floor(remainingMs / MS_PER_SECOND);
255
244
 
256
245
  const parts = [];
257
246
  if (h > 0) {
258
247
  parts.push(`${h}h`);
259
248
  }
260
-
261
- // Show minutes if:
262
- // - hours are present (e.g., "1h 0m 5s")
263
- // - OR minutes themselves are > 0 (e.g., "5m 10s")
264
- // - OR the original duration was >= 1 minute (ensures "1m 0s" for 60000ms)
265
249
  if (h > 0 || m > 0 || numMs >= MS_PER_SECOND * SECONDS_PER_MINUTE) {
266
250
  parts.push(`${m}m`);
267
251
  }
268
-
269
252
  parts.push(`${s}s`);
270
253
 
271
254
  return parts.join(" ");
@@ -1283,6 +1266,14 @@ function generateSuitesWidget(suitesData) {
1283
1266
  </div>
1284
1267
  </div>`;
1285
1268
  }
1269
+ function getAttachmentIcon(contentType) {
1270
+ if (contentType.includes("pdf")) return "📄";
1271
+ if (contentType.includes("json")) return "{ }";
1272
+ if (contentType.includes("html") || contentType.includes("xml")) return "</>";
1273
+ if (contentType.includes("csv")) return "📊";
1274
+ if (contentType.startsWith("text/")) return "📝";
1275
+ return "📎";
1276
+ }
1286
1277
  function generateHTML(reportData, trendData = null) {
1287
1278
  const { run, results } = reportData;
1288
1279
  const suitesData = getSuitesData(reportData.results || []);
@@ -1389,16 +1380,6 @@ function generateHTML(reportData, trendData = null) {
1389
1380
  .join("");
1390
1381
  };
1391
1382
 
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
1383
  return `
1403
1384
  <div class="test-case" data-status="${
1404
1385
  test.status
@@ -1459,9 +1440,9 @@ function generateHTML(reportData, trendData = null) {
1459
1440
  <div class="steps-list">${generateStepsHTML(test.steps)}</div>
1460
1441
  ${
1461
1442
  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>`
1443
+ ? `<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;">${formatPlaywrightError(
1444
+ test.stdout.join("\n")
1445
+ )}</pre></div>`
1465
1446
  : ""
1466
1447
  }
1467
1448
  ${
@@ -1474,98 +1455,139 @@ function generateHTML(reportData, trendData = null) {
1474
1455
  ${
1475
1456
  test.screenshots && test.screenshots.length > 0
1476
1457
  ? `
1477
- <div class="attachments-section">
1478
- <h4>Screenshots</h4>
1479
- <div class="attachments-grid">
1480
- ${test.screenshots
1481
- .map(
1482
- (screenshot, index) => `
1483
- <div class="attachment-item">
1484
- <img src="${screenshot}" alt="Screenshot">
1485
- <div class="attachment-info">
1486
- <div class="trace-actions">
1487
- <a href="${screenshot}" target="_blank" class="view-full">View Full Image</a>
1488
- <a href="${screenshot}" target="_blank" download="screenshot-${Date.now()}-${index}-${Math.random()
1489
- .toString(36)
1490
- .substring(2, 7)}.png">Download</a>
1458
+ <div class="attachments-section">
1459
+ <h4>Screenshots</h4>
1460
+ <div class="attachments-grid">
1461
+ ${test.screenshots
1462
+ .map(
1463
+ (screenshot, index) => `
1464
+ <div class="attachment-item">
1465
+ <img src="${screenshot}" alt="Screenshot ${index + 1}">
1466
+ <div class="attachment-info">
1467
+ <div class="trace-actions">
1468
+ <a href="${screenshot}" target="_blank" class="view-full">View Full Image</a>
1469
+ <a href="${screenshot}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
1470
+ </div>
1471
+ </div>
1472
+ </div>
1473
+ `
1474
+ )
1475
+ .join("")}
1476
+ </div>
1491
1477
  </div>
1492
- </div>
1493
- </div>
1494
- `
1495
- )
1496
- .join("")}
1497
- </div>
1498
- </div>
1499
- `
1478
+ `
1500
1479
  : ""
1501
1480
  }
1502
1481
  ${
1503
- test.videoPath
1504
- ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${(() => {
1505
- // Videos
1506
- const videos = Array.isArray(test.videoPath)
1507
- ? test.videoPath
1508
- : [test.videoPath];
1509
- const mimeTypes = {
1510
- mp4: "video/mp4",
1511
- webm: "video/webm",
1512
- ogg: "video/ogg",
1513
- mov: "video/quicktime",
1514
- avi: "video/x-msvideo",
1515
- };
1516
- return videos
1517
- .map((video, index) => {
1518
- const videoUrl =
1519
- typeof video === "object" ? video.url || "" : video;
1520
- const videoName =
1521
- typeof video === "object"
1522
- ? video.name || `Video ${index + 1}`
1523
- : `Video ${index + 1}`;
1524
- const fileExtension = String(videoUrl)
1525
- .split(".")
1526
- .pop()
1527
- .toLowerCase();
1528
- const mimeType = mimeTypes[fileExtension] || "video/mp4";
1529
- return `<div class="attachment-item"><video controls width="100%" height="auto" title="${sanitizeHTML(
1530
- videoName
1531
- )}"><source src="${sanitizeHTML(
1532
- videoUrl
1533
- )}" type="${mimeType}">Your browser does not support the video tag.</video><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
1534
- videoUrl
1535
- )}" target="_blank" download="${sanitizeHTML(
1536
- videoName
1537
- )}.${fileExtension}">Download</a></div></div></div>`;
1538
- })
1539
- .join("");
1540
- })()}</div></div>`
1482
+ test.videoPath && test.videoPath.length > 0
1483
+ ? `
1484
+ <div class="attachments-section">
1485
+ <h4>Videos</h4>
1486
+ <div class="attachments-grid">
1487
+ ${test.videoPath
1488
+ .map((videoUrl, index) => {
1489
+ const fileExtension = String(videoUrl)
1490
+ .split(".")
1491
+ .pop()
1492
+ .toLowerCase();
1493
+ const mimeType =
1494
+ {
1495
+ mp4: "video/mp4",
1496
+ webm: "video/webm",
1497
+ ogg: "video/ogg",
1498
+ mov: "video/quicktime",
1499
+ avi: "video/x-msvideo",
1500
+ }[fileExtension] || "video/mp4";
1501
+ return `
1502
+ <div class="attachment-item video-item">
1503
+ <video controls width="100%" height="auto" title="Video ${
1504
+ index + 1
1505
+ }">
1506
+ <source src="${sanitizeHTML(
1507
+ videoUrl
1508
+ )}" type="${mimeType}">
1509
+ Your browser does not support the video tag.
1510
+ </video>
1511
+ <div class="attachment-info">
1512
+ <div class="trace-actions">
1513
+ <a href="${sanitizeHTML(
1514
+ videoUrl
1515
+ )}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
1516
+ </div>
1517
+ </div>
1518
+ </div>`;
1519
+ })
1520
+ .join("")}
1521
+ </div>
1522
+ </div>
1523
+ `
1541
1524
  : ""
1542
1525
  }
1543
1526
  ${
1544
1527
  test.tracePath
1545
- ? `<div class="attachments-section"><h4>Trace Files</h4><div class="attachments-grid">${(() => {
1546
- // Traces
1547
- const traces = Array.isArray(test.tracePath)
1548
- ? test.tracePath
1549
- : [test.tracePath];
1550
- return traces
1551
- .map((trace, index) => {
1552
- const traceUrl =
1553
- typeof trace === "object" ? trace.url || "" : trace;
1554
- const traceName =
1555
- typeof trace === "object"
1556
- ? trace.name || `Trace ${index + 1}`
1557
- : `Trace ${index + 1}`;
1558
- const traceFileName = String(traceUrl).split("/").pop();
1559
- return `<div class="attachment-item"><div class="trace-preview"><span class="trace-icon">📄</span><span class="trace-name">${sanitizeHTML(
1560
- traceName
1561
- )}</span></div><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
1562
- traceUrl
1563
- )}" target="_blank" download="${sanitizeHTML(
1564
- traceFileName
1565
- )}" class="download-trace">Download</a></div></div></div>`;
1566
- })
1567
- .join("");
1568
- })()}</div></div>`
1528
+ ? `
1529
+ <div class="attachments-section">
1530
+ <h4>Trace Files</h4>
1531
+ <div class="attachments-grid">
1532
+ <div class="attachment-item trace-item">
1533
+ <div class="trace-preview">
1534
+ <span class="trace-icon">📄</span>
1535
+ <span class="trace-name">${sanitizeHTML(
1536
+ path.basename(test.tracePath)
1537
+ )}</span>
1538
+ </div>
1539
+ <div class="attachment-info">
1540
+ <div class="trace-actions">
1541
+ <a href="${sanitizeHTML(
1542
+ test.tracePath
1543
+ )}" target="_blank" download="${sanitizeHTML(
1544
+ path.basename(test.tracePath)
1545
+ )}" class="download-trace">Download Trace</a>
1546
+ </div>
1547
+ </div>
1548
+ </div>
1549
+ </div>
1550
+ </div>
1551
+ `
1552
+ : ""
1553
+ }
1554
+ ${
1555
+ test.attachments && test.attachments.length > 0
1556
+ ? `
1557
+ <div class="attachments-section">
1558
+ <h4>Other Attachments</h4>
1559
+ <div class="attachments-grid">
1560
+ ${test.attachments
1561
+ .map(
1562
+ (attachment) => `
1563
+ <div class="attachment-item generic-attachment">
1564
+ <div class="attachment-icon">${getAttachmentIcon(
1565
+ attachment.contentType
1566
+ )}</div>
1567
+ <div class="attachment-caption">
1568
+ <span class="attachment-name" title="${sanitizeHTML(
1569
+ attachment.name
1570
+ )}">${sanitizeHTML(attachment.name)}</span>
1571
+ <span class="attachment-type">${sanitizeHTML(
1572
+ attachment.contentType
1573
+ )}</span>
1574
+ </div>
1575
+ <div class="attachment-info">
1576
+ <div class="trace-actions">
1577
+ <a href="${sanitizeHTML(
1578
+ attachment.path
1579
+ )}" target="_blank" download="${sanitizeHTML(
1580
+ attachment.name
1581
+ )}" class="download-trace">Download</a>
1582
+ </div>
1583
+ </div>
1584
+ </div>
1585
+ `
1586
+ )
1587
+ .join("")}
1588
+ </div>
1589
+ </div>
1590
+ `
1569
1591
  : ""
1570
1592
  }
1571
1593
  ${
@@ -1601,14 +1623,11 @@ function generateHTML(reportData, trendData = null) {
1601
1623
  }
1602
1624
  .trend-chart-container, .test-history-trend div[id^="testHistoryChart-"] { min-height: 100px; }
1603
1625
  .lazy-load-chart .no-data, .lazy-load-chart .no-data-chart { display: flex; align-items: center; justify-content: center; height: 100%; font-style: italic; color: var(--dark-gray-color); }
1604
-
1605
- /* General Highcharts styling */
1606
1626
  .highcharts-background { fill: transparent; }
1607
1627
  .highcharts-title, .highcharts-subtitle { font-family: var(--font-family); }
1608
1628
  .highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
1609
1629
  .highcharts-axis-title { fill: var(--text-color) !important; }
1610
1630
  .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; }
1611
-
1612
1631
  body { font-family: var(--font-family); margin: 0; background-color: var(--background-color); color: var(--text-color); line-height: 1.65; font-size: 16px; }
1613
1632
  .container { padding: 30px; border-radius: var(--border-radius); box-shadow: var(--box-shadow); background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec); }
1614
1633
  .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; }
@@ -1704,11 +1723,19 @@ function generateHTML(reportData, trendData = null) {
1704
1723
  .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; }
1705
1724
  .attachment-item:hover { transform: translateY(-4px); box-shadow: var(--box-shadow); }
1706
1725
  .attachment-item img { width: 100%; height: 180px; object-fit: cover; display: block; border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease; }
1726
+ .attachment-info { padding: 12px; margin-top: auto; background-color: #fafafa;}
1707
1727
  .attachment-item a:hover img { opacity: 0.85; }
1708
1728
  .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); }
1709
1729
  .video-item a, .trace-item a { display: block; margin-bottom: 8px; color: var(--primary-color); text-decoration: none; font-weight: 500; }
1710
1730
  .video-item a:hover, .trace-item a:hover { text-decoration: underline; }
1711
1731
  .code-section pre { background-color: #2d2d2d; color: #f0f0f0; padding: 20px; border-radius: 6px; overflow-x: auto; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 0.95em; line-height:1.6;}
1732
+ .trace-actions { display: flex; justify-content: center; }
1733
+ .trace-actions a { text-decoration: none; color: var(--primary-color); font-weight: 500; font-size: 0.9em; }
1734
+ .generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
1735
+ .attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
1736
+ .attachment-caption { display: flex; flex-direction: column; }
1737
+ .attachment-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1738
+ .attachment-type { font-size: 0.8rem; color: var(--text-color-secondary); }
1712
1739
  .trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
1713
1740
  .test-history-container h2.tab-main-title { font-size: 1.6em; margin-bottom: 18px; color: var(--primary-color); border-bottom: 1px solid var(--border-color); padding-bottom: 12px;}
1714
1741
  .test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
@@ -2035,24 +2062,64 @@ function generateHTML(reportData, trendData = null) {
2035
2062
  }
2036
2063
  document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
2037
2064
 
2038
- function copyErrorToClipboard(button) {
2039
- const errorContainer = button.closest('.step-error');
2040
- const errorText = errorContainer.querySelector('.stack-trace').textContent;
2041
- const textarea = document.createElement('textarea');
2042
- textarea.value = errorText;
2043
- document.body.appendChild(textarea);
2044
- textarea.select();
2045
- try {
2046
- const successful = document.execCommand('copy');
2047
- const originalText = button.textContent;
2048
- button.textContent = successful ? 'Copied!' : 'Failed to copy';
2049
- setTimeout(() => {
2050
- button.textContent = originalText;
2051
- }, 2000);
2052
- } catch (err) {
2053
- console.error('Failed to copy: ', err);
2054
- button.textContent = 'Failed to copy';
2055
- }
2065
+ function copyErrorToClipboard(button) {
2066
+ // 1. Find the main error container, which should always be present.
2067
+ const errorContainer = button.closest('.step-error');
2068
+ if (!errorContainer) {
2069
+ console.error("Could not find '.step-error' container. The report's HTML structure might have changed.");
2070
+ return;
2071
+ }
2072
+
2073
+ let errorText;
2074
+
2075
+ // 2. First, try to find the preferred .stack-trace element (the "happy path").
2076
+ const stackTraceElement = errorContainer.querySelector('.stack-trace');
2077
+
2078
+ if (stackTraceElement) {
2079
+ // If it exists, use its text content. This handles standard assertion errors.
2080
+ errorText = stackTraceElement.textContent;
2081
+ } else {
2082
+ // 3. FALLBACK: If .stack-trace doesn't exist, this is likely an unstructured error.
2083
+ // We clone the container to avoid manipulating the live DOM or copying the button's own text.
2084
+ const clonedContainer = errorContainer.cloneNode(true);
2085
+
2086
+ // Remove the button from our clone before extracting the text.
2087
+ const buttonInClone = clonedContainer.querySelector('button');
2088
+ if (buttonInClone) {
2089
+ buttonInClone.remove();
2090
+ }
2091
+
2092
+ // Use the text content of the cleaned container as the fallback.
2093
+ errorText = clonedContainer.textContent;
2094
+ }
2095
+
2096
+ // 4. Proceed with the clipboard logic, ensuring text is not null and is trimmed.
2097
+ if (!errorText) {
2098
+ console.error('Could not extract error text.');
2099
+ button.textContent = 'Nothing to copy';
2100
+ setTimeout(() => { button.textContent = 'Copy Error'; }, 2000);
2101
+ return;
2102
+ }
2103
+
2104
+ const textarea = document.createElement('textarea');
2105
+ textarea.value = errorText.trim(); // Trim whitespace for a cleaner copy.
2106
+ textarea.style.position = 'fixed'; // Prevent screen scroll
2107
+ textarea.style.top = '-9999px';
2108
+ document.body.appendChild(textarea);
2109
+ textarea.select();
2110
+
2111
+ try {
2112
+ const successful = document.execCommand('copy');
2113
+ const originalText = button.textContent;
2114
+ button.textContent = successful ? 'Copied!' : 'Failed';
2115
+ setTimeout(() => {
2116
+ button.textContent = originalText;
2117
+ }, 2000);
2118
+ } catch (err) {
2119
+ console.error('Failed to copy: ', err);
2120
+ button.textContent = 'Failed';
2121
+ }
2122
+
2056
2123
  document.body.removeChild(textarea);
2057
2124
  }
2058
2125
  </script>