@arghajit/dummy 0.1.0-beta-24 → 0.1.0-beta-26

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.
@@ -141,7 +141,6 @@ export function ansiToHtml(text) {
141
141
  }
142
142
  return html;
143
143
  }
144
-
145
144
  function sanitizeHTML(str) {
146
145
  if (str === null || str === undefined) return "";
147
146
  return String(str).replace(
@@ -150,7 +149,6 @@ function sanitizeHTML(str) {
150
149
  ({ "&": "&", "<": "<", ">": ">", '"': '"', "'": "'" }[match] || match)
151
150
  );
152
151
  }
153
-
154
152
  function capitalize(str) {
155
153
  if (!str) return "";
156
154
  return str[0].toUpperCase() + str.slice(1).toLowerCase();
@@ -1216,6 +1214,19 @@ function getSuitesData(results) {
1216
1214
  });
1217
1215
  return Array.from(suitesMap.values());
1218
1216
  }
1217
+ function getAttachmentIcon(contentType) {
1218
+ if (!contentType) return "📎"; // Handle undefined/null
1219
+
1220
+ const normalizedType = contentType.toLowerCase();
1221
+
1222
+ if (normalizedType.includes("pdf")) return "📄";
1223
+ if (normalizedType.includes("json")) return "{ }";
1224
+ if (/html/.test(normalizedType)) return "🌐"; // Fixed: regex for any HTML type
1225
+ if (normalizedType.includes("xml")) return "<>";
1226
+ if (normalizedType.includes("csv")) return "📊";
1227
+ if (normalizedType.startsWith("text/")) return "📝";
1228
+ return "📎";
1229
+ }
1219
1230
  function generateSuitesWidget(suitesData) {
1220
1231
  if (!suitesData || suitesData.length === 0) {
1221
1232
  return `<div class="suites-widget"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
@@ -1272,6 +1283,260 @@ function generateSuitesWidget(suitesData) {
1272
1283
  </div>
1273
1284
  </div>`;
1274
1285
  }
1286
+ function generateWorkerDistributionChart(results) {
1287
+ if (!results || results.length === 0) {
1288
+ return '<div class="no-data">No test results data available to display worker distribution.</div>';
1289
+ }
1290
+
1291
+ // 1. Sort results by startTime to ensure chronological order
1292
+ const sortedResults = [...results].sort((a, b) => {
1293
+ const timeA = a.startTime ? new Date(a.startTime).getTime() : 0;
1294
+ const timeB = b.startTime ? new Date(b.startTime).getTime() : 0;
1295
+ return timeA - timeB;
1296
+ });
1297
+
1298
+ const workerData = sortedResults.reduce((acc, test) => {
1299
+ const workerId =
1300
+ typeof test.workerId !== "undefined" ? test.workerId : "N/A";
1301
+ if (!acc[workerId]) {
1302
+ acc[workerId] = { passed: 0, failed: 0, skipped: 0, tests: [] };
1303
+ }
1304
+
1305
+ const status = String(test.status).toLowerCase();
1306
+ if (status === "passed" || status === "failed" || status === "skipped") {
1307
+ acc[workerId][status]++;
1308
+ }
1309
+
1310
+ const testTitleParts = test.name.split(" > ");
1311
+ const testTitle =
1312
+ testTitleParts[testTitleParts.length - 1] || "Unnamed Test";
1313
+ // Store both name and status for each test
1314
+ acc[workerId].tests.push({ name: testTitle, status: status });
1315
+
1316
+ return acc;
1317
+ }, {});
1318
+
1319
+ const workerIds = Object.keys(workerData).sort((a, b) => {
1320
+ if (a === "N/A") return 1;
1321
+ if (b === "N/A") return -1;
1322
+ return parseInt(a, 10) - parseInt(b, 10);
1323
+ });
1324
+
1325
+ if (workerIds.length === 0) {
1326
+ return '<div class="no-data">Could not determine worker distribution from test data.</div>';
1327
+ }
1328
+
1329
+ const chartId = `workerDistChart-${Date.now()}-${Math.random()
1330
+ .toString(36)
1331
+ .substring(2, 7)}`;
1332
+ const renderFunctionName = `renderWorkerDistChart_${chartId.replace(
1333
+ /-/g,
1334
+ "_"
1335
+ )}`;
1336
+ const modalJsNamespace = `modal_funcs_${chartId.replace(/-/g, "_")}`;
1337
+
1338
+ // The categories now just need the name for the axis labels
1339
+ const categories = workerIds.map((id) => `Worker ${id}`);
1340
+
1341
+ // We pass the full data separately to the script
1342
+ const fullWorkerData = workerIds.map((id) => ({
1343
+ id: id,
1344
+ name: `Worker ${id}`,
1345
+ tests: workerData[id].tests,
1346
+ }));
1347
+
1348
+ const passedData = workerIds.map((id) => workerData[id].passed);
1349
+ const failedData = workerIds.map((id) => workerData[id].failed);
1350
+ const skippedData = workerIds.map((id) => workerData[id].skipped);
1351
+
1352
+ const categoriesString = JSON.stringify(categories);
1353
+ const fullDataString = JSON.stringify(fullWorkerData);
1354
+ const seriesString = JSON.stringify([
1355
+ { name: "Passed", data: passedData, color: "var(--success-color)" },
1356
+ { name: "Failed", data: failedData, color: "var(--danger-color)" },
1357
+ { name: "Skipped", data: skippedData, color: "var(--warning-color)" },
1358
+ ]);
1359
+
1360
+ // The HTML now includes the chart container, the modal, and styles for the modal
1361
+ return `
1362
+ <style>
1363
+ .worker-modal-overlay {
1364
+ position: fixed; z-index: 1050; left: 0; top: 0; width: 100%; height: 100%;
1365
+ overflow: auto; background-color: rgba(0,0,0,0.6);
1366
+ display: none; align-items: center; justify-content: center;
1367
+ }
1368
+ .worker-modal-content {
1369
+ background-color: #3d4043;
1370
+ color: var(--card-background-color);
1371
+ margin: auto; padding: 20px; border: 1px solid var(--border-color, #888);
1372
+ width: 80%; max-width: 700px; border-radius: 8px;
1373
+ position: relative; box-shadow: 0 5px 15px rgba(0,0,0,0.5);
1374
+ }
1375
+ .worker-modal-close {
1376
+ position: absolute; top: 10px; right: 20px;
1377
+ font-size: 28px; font-weight: bold; cursor: pointer;
1378
+ line-height: 1;
1379
+ }
1380
+ .worker-modal-close:hover, .worker-modal-close:focus {
1381
+ color: var(--text-color, #000);
1382
+ }
1383
+ #worker-modal-body-${chartId} ul {
1384
+ list-style-type: none; padding-left: 0; margin-top: 15px; max-height: 45vh; overflow-y: auto;
1385
+ }
1386
+ #worker-modal-body-${chartId} li {
1387
+ padding: 8px 5px; border-bottom: 1px solid var(--border-color, #eee);
1388
+ font-size: 0.9em;
1389
+ }
1390
+ #worker-modal-body-${chartId} li:last-child {
1391
+ border-bottom: none;
1392
+ }
1393
+ #worker-modal-body-${chartId} li > span {
1394
+ display: inline-block;
1395
+ width: 70px;
1396
+ font-weight: bold;
1397
+ text-align: right;
1398
+ margin-right: 10px;
1399
+ }
1400
+ </style>
1401
+
1402
+ <div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}" style="min-height: 350px;">
1403
+ <div class="no-data">Loading Worker Distribution Chart...</div>
1404
+ </div>
1405
+
1406
+ <div id="worker-modal-${chartId}" class="worker-modal-overlay">
1407
+ <div class="worker-modal-content">
1408
+ <span class="worker-modal-close">×</span>
1409
+ <h3 id="worker-modal-title-${chartId}" style="text-align: center; margin-top: 0; margin-bottom: 25px; font-size: 1.25em; font-weight: 600; color: #fff"></h3>
1410
+ <div id="worker-modal-body-${chartId}"></div>
1411
+ </div>
1412
+ </div>
1413
+
1414
+ <script>
1415
+ // Namespace for modal functions to avoid global scope pollution
1416
+ window.${modalJsNamespace} = {};
1417
+
1418
+ window.${renderFunctionName} = function() {
1419
+ const chartContainer = document.getElementById('${chartId}');
1420
+ if (!chartContainer) { console.error("Chart container ${chartId} not found."); return; }
1421
+
1422
+ // --- Modal Setup ---
1423
+ const modal = document.getElementById('worker-modal-${chartId}');
1424
+ const modalTitle = document.getElementById('worker-modal-title-${chartId}');
1425
+ const modalBody = document.getElementById('worker-modal-body-${chartId}');
1426
+ const closeModalBtn = modal.querySelector('.worker-modal-close');
1427
+
1428
+ window.${modalJsNamespace}.open = function(worker) {
1429
+ if (!worker) return;
1430
+ modalTitle.textContent = 'Test Details for ' + worker.name;
1431
+
1432
+ let testListHtml = '<ul>';
1433
+ if (worker.tests && worker.tests.length > 0) {
1434
+ worker.tests.forEach(test => {
1435
+ let color = 'inherit';
1436
+ if (test.status === 'passed') color = 'var(--success-color)';
1437
+ else if (test.status === 'failed') color = 'var(--danger-color)';
1438
+ else if (test.status === 'skipped') color = 'var(--warning-color)';
1439
+
1440
+ const escapedName = test.name.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
1441
+ testListHtml += \`<li style="color: \${color};"><span style="color: \${color}">[\${test.status.toUpperCase()}]</span> \${escapedName}</li>\`;
1442
+ });
1443
+ } else {
1444
+ testListHtml += '<li>No detailed test data available for this worker.</li>';
1445
+ }
1446
+ testListHtml += '</ul>';
1447
+
1448
+ modalBody.innerHTML = testListHtml;
1449
+ modal.style.display = 'flex';
1450
+ };
1451
+
1452
+ const closeModal = function() {
1453
+ modal.style.display = 'none';
1454
+ };
1455
+
1456
+ closeModalBtn.onclick = closeModal;
1457
+ modal.onclick = function(event) {
1458
+ // Close if clicked on the dark overlay background
1459
+ if (event.target == modal) {
1460
+ closeModal();
1461
+ }
1462
+ };
1463
+
1464
+
1465
+ // --- Highcharts Setup ---
1466
+ if (typeof Highcharts !== 'undefined') {
1467
+ try {
1468
+ chartContainer.innerHTML = '';
1469
+ const fullData = ${fullDataString};
1470
+
1471
+ const chartOptions = {
1472
+ chart: { type: 'bar', height: 350, backgroundColor: 'transparent' },
1473
+ title: { text: null },
1474
+ xAxis: {
1475
+ categories: ${categoriesString},
1476
+ title: { text: 'Worker ID' },
1477
+ labels: { style: { color: 'var(--text-color-secondary)' }}
1478
+ },
1479
+ yAxis: {
1480
+ min: 0,
1481
+ title: { text: 'Number of Tests' },
1482
+ labels: { style: { color: 'var(--text-color-secondary)' }},
1483
+ stackLabels: { enabled: true, style: { fontWeight: 'bold', color: 'var(--text-color)' } }
1484
+ },
1485
+ legend: { reversed: true, itemStyle: { fontSize: "12px", color: 'var(--text-color)' } },
1486
+ plotOptions: {
1487
+ series: {
1488
+ stacking: 'normal',
1489
+ cursor: 'pointer',
1490
+ point: {
1491
+ events: {
1492
+ click: function () {
1493
+ // 'this.x' is the index of the category
1494
+ const workerData = fullData[this.x];
1495
+ window.${modalJsNamespace}.open(workerData);
1496
+ }
1497
+ }
1498
+ }
1499
+ }
1500
+ },
1501
+ tooltip: {
1502
+ shared: true,
1503
+ headerFormat: '<b>{point.key}</b> (Click for details)<br/>',
1504
+ pointFormat: '<span style="color:{series.color}">●</span> {series.name}: <b>{point.y}</b><br/>',
1505
+ footerFormat: 'Total: <b>{point.total}</b>'
1506
+ },
1507
+ series: ${seriesString},
1508
+ credits: { enabled: false }
1509
+ };
1510
+ Highcharts.chart('${chartId}', chartOptions);
1511
+ } catch (e) {
1512
+ console.error("Error rendering chart ${chartId}:", e);
1513
+ chartContainer.innerHTML = '<div class="no-data">Error rendering worker distribution chart.</div>';
1514
+ }
1515
+ } else {
1516
+ chartContainer.innerHTML = '<div class="no-data">Charting library not available for worker distribution.</div>';
1517
+ }
1518
+ };
1519
+ </script>
1520
+ `;
1521
+ }
1522
+ const infoTooltip = `
1523
+ <span class="info-tooltip" style="display: inline-block; margin-left: 8px;">
1524
+ <span class="info-icon"
1525
+ style="cursor: pointer; font-size: 1.25rem;"
1526
+ onclick="window.workerInfoPrompt()">ℹ️</span>
1527
+ </span>
1528
+ <script>
1529
+ window.workerInfoPrompt = function() {
1530
+ const message = 'Why is worker -1 special?\\n\\n' +
1531
+ 'Playwright assigns skipped tests to worker -1 because:\\n' +
1532
+ '1. They don\\'t require browser execution\\n' +
1533
+ '2. This keeps real workers focused on actual tests\\n' +
1534
+ '3. Maintains clean reporting\\n\\n' +
1535
+ 'This is an intentional optimization by Playwright.';
1536
+ alert(message);
1537
+ }
1538
+ </script>
1539
+ `;
1275
1540
  function generateHTML(reportData, trendData = null) {
1276
1541
  const { run, results } = reportData;
1277
1542
  const suitesData = getSuitesData(reportData.results || []);
@@ -1322,13 +1587,13 @@ function generateHTML(reportData, trendData = null) {
1322
1587
  step.duration
1323
1588
  )}</span></div><div class="step-details" style="display: none;">${
1324
1589
  step.codeLocation
1325
- ? `<div class="step-info"><strong>Location:</strong> ${sanitizeHTML(
1590
+ ? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
1326
1591
  step.codeLocation
1327
1592
  )}</div>`
1328
1593
  : ""
1329
1594
  }${
1330
1595
  step.errorMessage
1331
- ? `<div class="step-error">${
1596
+ ? `<div class="test-error-summary">${
1332
1597
  step.stackTrace
1333
1598
  ? `<div class="stack-trace">${formatPlaywrightError(
1334
1599
  step.stackTrace
@@ -1392,13 +1657,24 @@ function generateHTML(reportData, trendData = null) {
1392
1657
  <h4>Steps</h4><div class="steps-list">${generateStepsHTML(
1393
1658
  test.steps
1394
1659
  )}</div>
1395
- ${
1396
- test.stdout && test.stdout.length > 0
1397
- ? `<div class="console-output-section"><h4>Console Output (stdout)</h4><pre class="console-log stdout-log">${formatPlaywrightError(
1398
- test.stdout.join("\\n")
1399
- )}</pre></div>`
1400
- : ""
1401
- }
1660
+ ${(() => {
1661
+ if (!test.stdout || test.stdout.length === 0)
1662
+ return "";
1663
+ // Create a unique ID for the <pre> element to target it for copying
1664
+ const logId = `stdout-log-${test.id || testIndex}`;
1665
+ return `<div class="console-output-section">
1666
+ <h4>Console Output (stdout)
1667
+ <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy Console</button>
1668
+ </h4>
1669
+ <div class="log-wrapper">
1670
+ <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(
1671
+ test.stdout
1672
+ .map((line) => sanitizeHTML(line))
1673
+ .join("\n")
1674
+ )}</pre>
1675
+ </div>
1676
+ </div>`;
1677
+ })()}
1402
1678
  ${
1403
1679
  test.stderr && test.stderr.length > 0
1404
1680
  ? `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log">${test.stderr
@@ -1505,6 +1781,7 @@ function generateHTML(reportData, trendData = null) {
1505
1781
  test.attachments.length === 0
1506
1782
  )
1507
1783
  return "";
1784
+
1508
1785
  return `<div class="attachments-section"><h4>Other Attachments</h4><div class="attachments-grid">${test.attachments
1509
1786
  .map((attachment) => {
1510
1787
  try {
@@ -1512,27 +1789,50 @@ function generateHTML(reportData, trendData = null) {
1512
1789
  DEFAULT_OUTPUT_DIR,
1513
1790
  attachment.path
1514
1791
  );
1515
- if (!fsExistsSync(attachmentPath))
1792
+
1793
+ if (!fsExistsSync(attachmentPath)) {
1794
+ console.warn(
1795
+ `Attachment not found at: ${attachmentPath}`
1796
+ );
1516
1797
  return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
1517
1798
  attachment.name
1518
1799
  )}</div>`;
1800
+ }
1801
+
1519
1802
  const attachmentBase64 =
1520
1803
  readFileSync(attachmentPath).toString(
1521
1804
  "base64"
1522
1805
  );
1523
1806
  const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
1524
- return `<div class="attachment-item generic-attachment"><div class="attachment-icon">${getAttachmentIcon(
1525
- attachment.contentType
1526
- )}</div><div class="attachment-caption"><span class="attachment-name" title="${sanitizeHTML(
1527
- attachment.name
1528
- )}">${sanitizeHTML(
1807
+
1808
+ return `<div class="attachment-item generic-attachment">
1809
+ <div class="attachment-icon">${getAttachmentIcon(
1810
+ attachment.contentType
1811
+ )}</div>
1812
+ <div class="attachment-caption">
1813
+ <span class="attachment-name" title="${sanitizeHTML(
1814
+ attachment.name
1815
+ )}">${sanitizeHTML(
1529
1816
  attachment.name
1530
- )}</span><span class="attachment-type">${sanitizeHTML(
1531
- attachment.contentType
1532
- )}</span></div><div class="attachment-info"><div class="trace-actions"><a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
1817
+ )}</span>
1818
+ <span class="attachment-type">${sanitizeHTML(
1819
+ attachment.contentType
1820
+ )}</span>
1821
+ </div>
1822
+ <div class="attachment-info">
1823
+ <div class="trace-actions">
1824
+ <a href="${attachmentDataUri}" target="_blank" class="view-full">View</a>
1825
+ <a href="${attachmentDataUri}" download="${sanitizeHTML(
1533
1826
  attachment.name
1534
- )}">Download</a></div></div></div>`;
1827
+ )}">Download</a>
1828
+ </div>
1829
+ </div>
1830
+ </div>`;
1535
1831
  } catch (e) {
1832
+ console.error(
1833
+ `Failed to process attachment "${attachment.name}":`,
1834
+ e
1835
+ );
1536
1836
  return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(
1537
1837
  attachment.name
1538
1838
  )}</div>`;
@@ -1559,8 +1859,8 @@ function generateHTML(reportData, trendData = null) {
1559
1859
  <head>
1560
1860
  <meta charset="UTF-8">
1561
1861
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1562
- <link rel="icon" type="image/png" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
1563
- <link rel="apple-touch-icon" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
1862
+ <link rel="icon" type="image/png" href="https://i.postimg.cc/v817w4sg/logo.png">
1863
+ <link rel="apple-touch-icon" href="https://i.postimg.cc/v817w4sg/logo.png">
1564
1864
  <script src="https://code.highcharts.com/highcharts.js" defer></script>
1565
1865
  <title>Playwright Pulse Report</title>
1566
1866
  <style>
@@ -1586,7 +1886,7 @@ function generateHTML(reportData, trendData = null) {
1586
1886
  .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; }
1587
1887
  .header-title { display: flex; align-items: center; gap: 15px; }
1588
1888
  .header h1 { margin: 0; font-size: 1.85em; font-weight: 600; color: var(--primary-color); }
1589
- #report-logo { height: 40px; width: 40px; border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.1);}
1889
+ #report-logo { height: 40px; width: 55px; }
1590
1890
  .run-info { font-size: 0.9em; text-align: right; color: var(--text-color-secondary); line-height:1.5;}
1591
1891
  .run-info strong { color: var(--text-color); }
1592
1892
  .tabs { display: flex; border-bottom: 2px solid var(--border-color); margin-bottom: 30px; overflow-x: auto; }
@@ -1665,8 +1965,8 @@ function generateHTML(reportData, trendData = null) {
1665
1965
  .step-duration { color: var(--dark-gray-color); font-size: 0.9em; }
1666
1966
  .step-details { display: none; padding: 14px; margin-top: 8px; background: #fdfdfd; border-radius: 6px; font-size: 0.95em; border: 1px solid var(--light-gray-color); }
1667
1967
  .step-info { margin-bottom: 8px; }
1668
- .step-error { color: var(--danger-color); margin-top: 12px; padding: 14px; background: rgba(244,67,54,0.05); border-radius: 4px; font-size: 0.95em; border-left: 3px solid var(--danger-color); }
1669
- .step-error pre.stack-trace { margin-top: 10px; padding: 12px; background-color: rgba(0,0,0,0.03); border-radius: 4px; font-size:0.9em; max-height: 280px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; }
1968
+ .test-error-summary { color: var(--danger-color); margin-top: 12px; padding: 14px; background: rgba(244,67,54,0.05); border-radius: 4px; font-size: 0.95em; border-left: 3px solid var(--danger-color); }
1969
+ .test-error-summary pre.stack-trace { margin-top: 10px; padding: 12px; background-color: rgba(0,0,0,0.03); border-radius: 4px; font-size:0.9em; max-height: 280px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; }
1670
1970
  .step-hook { background-color: rgba(33,150,243,0.04); border-left: 3px solid var(--info-color) !important; }
1671
1971
  .step-hook .step-title { font-style: italic; color: var(--info-color)}
1672
1972
  .nested-steps { margin-top: 12px; }
@@ -1716,12 +2016,13 @@ function generateHTML(reportData, trendData = null) {
1716
2016
  .download-trace:hover { background: #cbd5e0; }
1717
2017
  .filters button.clear-filters-btn { background-color: var(--medium-gray-color); color: var(--text-color); }
1718
2018
  .filters button.clear-filters-btn:hover { background-color: var(--dark-gray-color); color: #fff; }
2019
+ .copy-btn {color: var(--primary-color); background: #fefefe; border-radius: 8px; cursor: pointer; border-color: var(--primary-color); font-size: 1em; margin-left: 93%; font-weight: 600;}
1719
2020
  @media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
1720
2021
  @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; } }
1721
2022
  @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;} }
1722
- @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;} }
2023
+ @media (max-width: 480px) { body {font-size: 14px;} .container {padding: 15px;} .header h1 {font-size: 1.4em;} #report-logo { height: 35px; width: 45px; } .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;} }
1723
2024
  .trace-actions a { text-decoration: none; color: var(--primary-color); font-weight: 500; font-size: 0.9em; }
1724
- .generic-attachment { text-align: center; padding: 1rem; justify-content: center; align-items: center; }
2025
+ .generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
1725
2026
  .attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
1726
2027
  .attachment-caption { display: flex; flex-direction: column; align-items: center; justify-content: center; flex-grow: 1; }
1727
2028
  .attachment-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
@@ -1732,7 +2033,7 @@ function generateHTML(reportData, trendData = null) {
1732
2033
  <div class="container">
1733
2034
  <header class="header">
1734
2035
  <div class="header-title">
1735
- <img id="report-logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMNCA3bDggNSA4LTUtOC01eiIgZmlsbD0iIzNmNTFiNSIvPjxwYXRoIGQ9Ik0xMiA2TDQgMTFsOCA1IDgtNS04LTV6IiBmaWxsPSIjNDI4NWY0Ii8+PHBhdGggZD0iTTEyIDEwbC04IDUgOCA1IDgtNS04LTV6IiBmaWxsPSIjM2Q1NWI0Ii8+PC9zdmc+" alt="Report Logo">
2036
+ <img id="report-logo" src="https://i.postimg.cc/v817w4sg/logo.png" alt="Report Logo">
1736
2037
  <h1>Playwright Pulse Report</h1>
1737
2038
  </div>
1738
2039
  <div class="run-info"><strong>Run Date:</strong> ${formatDate(
@@ -1823,6 +2124,12 @@ function generateHTML(reportData, trendData = null) {
1823
2124
  ? generateDurationTrendChart(trendData)
1824
2125
  : '<div class="no-data">Overall trend data not available for durations.</div>'
1825
2126
  }
2127
+ </div>
2128
+ </div>
2129
+ <h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
2130
+ <div class="trend-charts-row">
2131
+ <div class="trend-chart">
2132
+ ${generateWorkerDistributionChart(results)}
1826
2133
  </div>
1827
2134
  </div>
1828
2135
  <h2 class="tab-main-title">Individual Test History</h2>
@@ -1839,7 +2146,6 @@ function generateHTML(reportData, trendData = null) {
1839
2146
  </div>
1840
2147
  <footer style="padding: 0.5rem; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); text-align: center; font-family: 'Segoe UI', system-ui, sans-serif;">
1841
2148
  <div style="display: inline-flex; align-items: center; gap: 0.5rem; color: #333; font-size: 0.9rem; font-weight: 600; letter-spacing: 0.5px;">
1842
- <img width="48" height="48" src="https://img.icons8.com/emoji/48/index-pointing-at-the-viewer-light-skin-tone-emoji.png" alt="index-pointing-at-the-viewer-light-skin-tone-emoji"/>
1843
2149
  <span>Created by</span>
1844
2150
  <a href="https://github.com/Arghajit47" target="_blank" rel="noopener noreferrer" style="color: #7737BF; font-weight: 700; font-style: italic; text-decoration: none; transition: all 0.2s ease;" onmouseover="this.style.color='#BF5C37'" onmouseout="this.style.color='#7737BF'">Arghajit Singha</a>
1845
2151
  </div>
@@ -1854,6 +2160,21 @@ function generateHTML(reportData, trendData = null) {
1854
2160
  return (ms / 1000).toFixed(1) + "s";
1855
2161
  }
1856
2162
  }
2163
+ function copyLogContent(elementId, button) {
2164
+ const logElement = document.getElementById(elementId);
2165
+ if (!logElement) {
2166
+ console.error('Could not find log element with ID:', elementId);
2167
+ return;
2168
+ }
2169
+ navigator.clipboard.writeText(logElement.innerText).then(() => {
2170
+ button.textContent = 'Copied!';
2171
+ setTimeout(() => { button.textContent = 'Copy'; }, 2000);
2172
+ }).catch(err => {
2173
+ console.error('Failed to copy log content:', err);
2174
+ button.textContent = 'Failed';
2175
+ setTimeout(() => { button.textContent = 'Copy'; }, 2000);
2176
+ });
2177
+ }
1857
2178
  function initializeReportInteractivity() {
1858
2179
  const tabButtons = document.querySelectorAll('.tab-button');
1859
2180
  const tabContents = document.querySelectorAll('.tab-content');
@@ -2007,9 +2328,9 @@ function generateHTML(reportData, trendData = null) {
2007
2328
 
2008
2329
  function copyErrorToClipboard(button) {
2009
2330
  // 1. Find the main error container, which should always be present.
2010
- const errorContainer = button.closest('.step-error');
2331
+ const errorContainer = button.closest('.test-error-summary');
2011
2332
  if (!errorContainer) {
2012
- console.error("Could not find '.step-error' container. The report's HTML structure might have changed.");
2333
+ console.error("Could not find '.test-error-summary' container. The report's HTML structure might have changed.");
2013
2334
  return;
2014
2335
  }
2015
2336