@arghajit/playwright-pulse-report 0.3.0 → 0.3.1

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.
@@ -350,6 +350,7 @@ function generateTestTrendsChart(trendData) {
350
350
  </script>
351
351
  `;
352
352
  }
353
+ const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
353
354
  function generateDurationTrendChart(trendData) {
354
355
  if (!trendData || !trendData.overall || trendData.overall.length === 0) {
355
356
  return '<div class="no-data">No overall trend data available for durations.</div>';
@@ -363,8 +364,6 @@ function generateDurationTrendChart(trendData) {
363
364
  )}`;
364
365
  const runs = trendData.overall;
365
366
 
366
- const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
367
-
368
367
  const chartDataString = JSON.stringify(runs.map((run) => run.duration));
369
368
  const categoriesString = JSON.stringify(runs.map((run, i) => `Run ${i + 1}`));
370
369
  const runsForTooltip = runs.map((r) => ({
@@ -703,6 +702,9 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
703
702
  const cardHeight = Math.floor(dashboardHeight * 0.44);
704
703
  const cardContentPadding = 16; // px
705
704
 
705
+ // Logic for Run Context
706
+ const runContext = process.env.CI ? "CI" : "Local Test";
707
+
706
708
  return `
707
709
  <div class="environment-dashboard-wrapper" id="${dashboardId}">
708
710
  <style>
@@ -745,6 +747,20 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
745
747
  gap: 20px;
746
748
  font-size: 14px;
747
749
  }
750
+
751
+ /* Mobile Responsiveness */
752
+ @media (max-width: 768px) {
753
+ .environment-dashboard-wrapper {
754
+ grid-template-columns: 1fr; /* Stack columns on mobile */
755
+ grid-template-rows: auto;
756
+ padding: 16px;
757
+ height: auto !important; /* Allow height to grow */
758
+ }
759
+ .env-card {
760
+ height: auto !important; /* Allow cards to grow based on content */
761
+ min-height: 200px;
762
+ }
763
+ }
748
764
 
749
765
  .env-dashboard-header {
750
766
  grid-column: 1 / -1;
@@ -754,6 +770,8 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
754
770
  border-bottom: 1px solid var(--border-color);
755
771
  padding-bottom: 16px;
756
772
  margin-bottom: 8px;
773
+ flex-wrap: wrap; /* Allow wrapping header items */
774
+ gap: 10px;
757
775
  }
758
776
 
759
777
  .env-dashboard-title {
@@ -811,6 +829,8 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
811
829
  padding: 10px 0;
812
830
  border-bottom: 1px solid var(--border-light-color);
813
831
  font-size: 0.875rem;
832
+ flex-wrap: wrap; /* Allow details to wrap on very small screens */
833
+ gap: 8px;
814
834
  }
815
835
 
816
836
  .env-detail-row:last-child {
@@ -828,6 +848,7 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
828
848
  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
829
849
  text-align: right;
830
850
  word-break: break-all;
851
+ margin-left: auto; /* Push to right */
831
852
  }
832
853
 
833
854
  .env-chip {
@@ -1013,7 +1034,7 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
1013
1034
  </div>
1014
1035
  <div class="env-detail-row">
1015
1036
  <span class="env-detail-label">Run Context</span>
1016
- <span class="env-detail-value">CI/Local Test</span>
1037
+ <span class="env-detail-value">${runContext}</span>
1017
1038
  </div>
1018
1039
  </div>
1019
1040
  </div>
@@ -1467,11 +1488,14 @@ function getSuitesData(results) {
1467
1488
  }
1468
1489
  function generateSuitesWidget(suitesData) {
1469
1490
  if (!suitesData || suitesData.length === 0) {
1470
- return `<div class="suites-widget"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
1491
+ // Maintain height consistency even if empty
1492
+ return `<div class="suites-widget" style="height: 450px;"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
1471
1493
  }
1494
+
1495
+ // Added inline styles for height consistency with Pie Chart (approx 450px) and scrolling
1472
1496
  return `
1473
- <div class="suites-widget">
1474
- <div class="suites-header">
1497
+ <div class="suites-widget" style="height: 450px; display: flex; flex-direction: column;">
1498
+ <div class="suites-header" style="flex-shrink: 0;">
1475
1499
  <h2>Test Suites</h2>
1476
1500
  <span class="summary-badge">${
1477
1501
  suitesData.length
@@ -1480,44 +1504,49 @@ function generateSuitesWidget(suitesData) {
1480
1504
  0
1481
1505
  )} tests</span>
1482
1506
  </div>
1483
- <div class="suites-grid">
1484
- ${suitesData
1485
- .map(
1486
- (suite) => `
1487
- <div class="suite-card status-${suite.statusOverall}">
1488
- <div class="suite-card-header">
1489
- <h3 class="suite-name" title="${sanitizeHTML(
1490
- suite.name
1491
- )} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
1492
- </div>
1493
- <div>🖥️ <span class="browser-tag">${sanitizeHTML(
1494
- suite.browser
1495
- )}</span></div>
1496
- <div class="suite-card-body">
1497
- <span class="test-count">${suite.count} test${
1498
- suite.count !== 1 ? "s" : ""
1499
- }</span>
1500
- <div class="suite-stats">
1501
- ${
1502
- suite.passed > 0
1503
- ? `<span class="stat-passed" title="Passed"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/></svg> ${suite.passed}</span>`
1504
- : ""
1505
- }
1506
- ${
1507
- suite.failed > 0
1508
- ? `<span class="stat-failed" title="Failed"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/></svg> ${suite.failed}</span>`
1509
- : ""
1510
- }
1511
- ${
1512
- suite.skipped > 0
1513
- ? `<span class="stat-skipped" title="Skipped"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-exclamation-triangle-fill" viewBox="0 0 16 16"><path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/></svg> ${suite.skipped}</span>`
1514
- : ""
1515
- }
1516
- </div>
1507
+
1508
+ <div class="suites-grid-container" style="flex-grow: 1; overflow-y: auto; padding-right: 5px;">
1509
+ <div class="suites-grid">
1510
+ ${suitesData
1511
+ .map(
1512
+ (suite) => `
1513
+ <div class="suite-card status-${suite.statusOverall}">
1514
+ <div class="suite-card-header">
1515
+ <h3 class="suite-name" title="${sanitizeHTML(
1516
+ suite.name
1517
+ )} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(
1518
+ suite.name
1519
+ )}</h3>
1520
+ </div>
1521
+ <div>🖥️ <span class="browser-tag">${sanitizeHTML(
1522
+ suite.browser
1523
+ )}</span></div>
1524
+ <div class="suite-card-body">
1525
+ <span class="test-count">${suite.count} test${
1526
+ suite.count !== 1 ? "s" : ""
1527
+ }</span>
1528
+ <div class="suite-stats">
1529
+ ${
1530
+ suite.passed > 0
1531
+ ? `<span class="stat-passed" title="Passed"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/></svg> ${suite.passed}</span>`
1532
+ : ""
1533
+ }
1534
+ ${
1535
+ suite.failed > 0
1536
+ ? `<span class="stat-failed" title="Failed"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/></svg> ${suite.failed}</span>`
1537
+ : ""
1538
+ }
1539
+ ${
1540
+ suite.skipped > 0
1541
+ ? `<span class="stat-skipped" title="Skipped"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-exclamation-triangle-fill" viewBox="0 0 16 16"><path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/></svg> ${suite.skipped}</span>`
1542
+ : ""
1543
+ }
1544
+ </div>
1545
+ </div>
1546
+ </div>`
1547
+ )
1548
+ .join("")}
1517
1549
  </div>
1518
- </div>`
1519
- )
1520
- .join("")}
1521
1550
  </div>
1522
1551
  </div>`;
1523
1552
  }
@@ -1644,6 +1673,378 @@ function generateAIFailureAnalyzerTab(results) {
1644
1673
  </div>
1645
1674
  `;
1646
1675
  }
1676
+ /**
1677
+ * Generates a area chart showing the total duration per spec file.
1678
+ * The chart is lazy-loaded and rendered with Highcharts when scrolled into view.
1679
+ *
1680
+ * @param {Array<object>} results - Array of test result objects.
1681
+ * @returns {string} HTML string containing the chart container and lazy-loading script.
1682
+ */
1683
+ function generateSpecDurationChart(results) {
1684
+ if (!results || results.length === 0)
1685
+ return '<div class="no-data">No results available.</div>';
1686
+
1687
+ const specDurations = {};
1688
+ results.forEach((test) => {
1689
+ // Use the dedicated 'spec_file' key
1690
+ const fileName = test.spec_file || "Unknown File";
1691
+
1692
+ if (!specDurations[fileName]) specDurations[fileName] = 0;
1693
+ specDurations[fileName] += test.duration;
1694
+ });
1695
+
1696
+ const categories = Object.keys(specDurations);
1697
+ // We map 'name' here, which we will use in the tooltip later
1698
+ const data = categories.map((cat) => ({
1699
+ y: specDurations[cat],
1700
+ name: cat,
1701
+ }));
1702
+
1703
+ if (categories.length === 0)
1704
+ return '<div class="no-data">No spec data found.</div>';
1705
+
1706
+ const chartId = `specDurChart-${Date.now()}-${Math.random()
1707
+ .toString(36)
1708
+ .substring(2, 7)}`;
1709
+ const renderFunctionName = `renderSpecDurChart_${chartId.replace(/-/g, "_")}`;
1710
+
1711
+ const categoriesStr = JSON.stringify(categories);
1712
+ const dataStr = JSON.stringify(data);
1713
+
1714
+ return `
1715
+ <div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
1716
+ <div class="no-data">Loading Spec Duration Chart...</div>
1717
+ </div>
1718
+ <script>
1719
+ window.${renderFunctionName} = function() {
1720
+ const chartContainer = document.getElementById('${chartId}');
1721
+ if (!chartContainer) return;
1722
+ if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
1723
+ try {
1724
+ chartContainer.innerHTML = '';
1725
+ Highcharts.chart('${chartId}', {
1726
+ chart: { type: 'area', height: 350, backgroundColor: 'transparent' },
1727
+ title: { text: null },
1728
+ xAxis: {
1729
+ categories: ${categoriesStr},
1730
+ visible: false, // 1. HIDE THE X-AXIS
1731
+ title: { text: null },
1732
+ crosshair: true
1733
+ },
1734
+ yAxis: {
1735
+ min: 0,
1736
+ title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
1737
+ labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
1738
+ },
1739
+ legend: { layout: 'horizontal', align: 'center', verticalAlign: 'bottom', itemStyle: { fontSize: '12px', color: 'var(--text-color)' }},
1740
+ plotOptions: { area: { lineWidth: 2.5, states: { hover: { lineWidthPlus: 0 } }, threshold: null }},
1741
+ tooltip: {
1742
+ shared: true,
1743
+ useHTML: true,
1744
+ backgroundColor: 'rgba(10,10,10,0.92)',
1745
+ borderColor: 'rgba(10,10,10,0.92)',
1746
+ style: { color: '#f5f5f5' },
1747
+ formatter: function() {
1748
+ const point = this.points ? this.points[0].point : this.point;
1749
+ const color = point.color || point.series.color;
1750
+
1751
+ // 2. FIX: Use 'point.name' instead of 'this.x' to get the actual filename
1752
+ return '<span style="color:' + color + '">●</span> <b>File: ' + point.name + '</b><br/>' +
1753
+ 'Duration: <b>' + formatDuration(this.y) + '</b>';
1754
+ }
1755
+ },
1756
+ series: [{
1757
+ name: 'Duration',
1758
+ data: ${dataStr},
1759
+ color: 'var(--accent-color-alt)',
1760
+ type: 'area',
1761
+ marker: { symbol: 'circle', enabled: true, radius: 4, states: { hover: { radius: 6, lineWidthPlus: 0 } } },
1762
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(${accentColorAltRGB}, 0.4)'], [1, 'rgba(${accentColorAltRGB}, 0.05)']] },
1763
+ lineWidth: 2.5
1764
+ }],
1765
+ credits: { enabled: false }
1766
+ });
1767
+ } catch (e) { console.error("Error rendering spec chart:", e); }
1768
+ }
1769
+ };
1770
+ </script>
1771
+ `;
1772
+ }
1773
+ /**
1774
+ * Generates a vertical bar chart showing the total duration of each test describe block.
1775
+ * Tests without a describe block or with "n/a" / empty describe names are ignored.
1776
+ * @param {Array<object>} results - Array of test result objects.
1777
+ * @returns {string} HTML string containing the chart container and lazy-loading script.
1778
+ */
1779
+ function generateDescribeDurationChart(results) {
1780
+ if (!results || results.length === 0)
1781
+ return '<div class="no-data">Seems like there is test describe block available in the executed test suite.</div>';
1782
+
1783
+ const describeMap = new Map();
1784
+ let foundAnyDescribe = false;
1785
+
1786
+ results.forEach((test) => {
1787
+ if (test.describe) {
1788
+ const describeName = test.describe;
1789
+ // Filter out invalid describe blocks
1790
+ if (
1791
+ !describeName ||
1792
+ describeName.trim().toLowerCase() === "n/a" ||
1793
+ describeName.trim() === ""
1794
+ ) {
1795
+ return;
1796
+ }
1797
+
1798
+ foundAnyDescribe = true;
1799
+ const fileName = test.spec_file || "Unknown File";
1800
+ const key = fileName + "::" + describeName;
1801
+
1802
+ if (!describeMap.has(key)) {
1803
+ describeMap.set(key, {
1804
+ duration: 0,
1805
+ file: fileName,
1806
+ describe: describeName,
1807
+ });
1808
+ }
1809
+ describeMap.get(key).duration += test.duration;
1810
+ }
1811
+ });
1812
+
1813
+ if (!foundAnyDescribe) {
1814
+ return '<div class="no-data">No valid test describe blocks found.</div>';
1815
+ }
1816
+
1817
+ const categories = [];
1818
+ const data = [];
1819
+
1820
+ for (const [key, val] of describeMap.entries()) {
1821
+ categories.push(val.describe);
1822
+ data.push({
1823
+ y: val.duration,
1824
+ name: val.describe,
1825
+ custom: {
1826
+ fileName: val.file,
1827
+ describeName: val.describe,
1828
+ },
1829
+ });
1830
+ }
1831
+
1832
+ const chartId = `descDurChart-${Date.now()}-${Math.random()
1833
+ .toString(36)
1834
+ .substring(2, 7)}`;
1835
+ const renderFunctionName = `renderDescDurChart_${chartId.replace(/-/g, "_")}`;
1836
+
1837
+ const categoriesStr = JSON.stringify(categories);
1838
+ const dataStr = JSON.stringify(data);
1839
+
1840
+ return `
1841
+ <div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
1842
+ <div class="no-data">Loading Describe Duration Chart...</div>
1843
+ </div>
1844
+ <script>
1845
+ window.${renderFunctionName} = function() {
1846
+ const chartContainer = document.getElementById('${chartId}');
1847
+ if (!chartContainer) return;
1848
+ if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
1849
+ try {
1850
+ chartContainer.innerHTML = '';
1851
+ Highcharts.chart('${chartId}', {
1852
+ chart: {
1853
+ type: 'column', // 1. CHANGED: 'bar' -> 'column' for vertical bars
1854
+ height: 400, // 2. CHANGED: Fixed height works better for vertical charts
1855
+ backgroundColor: 'transparent'
1856
+ },
1857
+ title: { text: null },
1858
+ xAxis: {
1859
+ categories: ${categoriesStr},
1860
+ visible: false, // Hidden as requested
1861
+ title: { text: null },
1862
+ crosshair: true
1863
+ },
1864
+ yAxis: {
1865
+ min: 0,
1866
+ title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
1867
+ labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
1868
+ },
1869
+ legend: { enabled: false },
1870
+ plotOptions: {
1871
+ series: {
1872
+ borderRadius: 4,
1873
+ borderWidth: 0,
1874
+ states: { hover: { brightness: 0.1 }}
1875
+ },
1876
+ column: { pointPadding: 0.2, groupPadding: 0.1 } // Adjust spacing for columns
1877
+ },
1878
+ tooltip: {
1879
+ shared: true,
1880
+ useHTML: true,
1881
+ backgroundColor: 'rgba(10,10,10,0.92)',
1882
+ borderColor: 'rgba(10,10,10,0.92)',
1883
+ style: { color: '#f5f5f5' },
1884
+ formatter: function() {
1885
+ const point = this.points ? this.points[0].point : this.point;
1886
+ const file = (point.custom && point.custom.fileName) ? point.custom.fileName : 'Unknown';
1887
+ const desc = point.name || 'Unknown';
1888
+ const color = point.color || point.series.color;
1889
+
1890
+ return '<span style="color:' + color + '">●</span> <b>Describe: ' + desc + '</b><br/>' +
1891
+ '<span style="opacity: 0.8; font-size: 0.9em; color: #ddd;">File: ' + file + '</span><br/>' +
1892
+ 'Duration: <b>' + formatDuration(point.y) + '</b>';
1893
+ }
1894
+ },
1895
+ series: [{
1896
+ name: 'Duration',
1897
+ data: ${dataStr},
1898
+ color: 'var(--accent-color-alt)',
1899
+ }],
1900
+ credits: { enabled: false }
1901
+ });
1902
+ } catch (e) { console.error("Error rendering describe chart:", e); }
1903
+ }
1904
+ };
1905
+ </script>
1906
+ `;
1907
+ }
1908
+ /**
1909
+ * Generates a stacked column chart showing test results distributed by severity.
1910
+ * Matches dimensions of the System Environment section (~600px).
1911
+ * Lazy-loaded for performance.
1912
+ */
1913
+ function generateSeverityDistributionChart(results) {
1914
+ if (!results || results.length === 0) {
1915
+ return '<div class="trend-chart" style="height: 600px;"><div class="no-data">No results available for severity distribution.</div></div>';
1916
+ }
1917
+
1918
+ const severityLevels = ["Critical", "High", "Medium", "Low", "Minor"];
1919
+ const data = {
1920
+ passed: [0, 0, 0, 0, 0],
1921
+ failed: [0, 0, 0, 0, 0],
1922
+ skipped: [0, 0, 0, 0, 0],
1923
+ };
1924
+
1925
+ results.forEach((test) => {
1926
+ const sev = test.severity || "Medium";
1927
+ const status = String(test.status).toLowerCase();
1928
+
1929
+ let index = severityLevels.indexOf(sev);
1930
+ if (index === -1) index = 2; // Default to Medium
1931
+
1932
+ if (status === "passed") {
1933
+ data.passed[index]++;
1934
+ } else if (
1935
+ status === "failed" ||
1936
+ status === "timedout" ||
1937
+ status === "interrupted"
1938
+ ) {
1939
+ data.failed[index]++;
1940
+ } else {
1941
+ data.skipped[index]++;
1942
+ }
1943
+ });
1944
+
1945
+ const chartId = `sevDistChart-${Date.now()}-${Math.random()
1946
+ .toString(36)
1947
+ .substring(2, 7)}`;
1948
+ const renderFunctionName = `renderSevDistChart_${chartId.replace(/-/g, "_")}`;
1949
+
1950
+ const seriesData = [
1951
+ { name: "Passed", data: data.passed, color: "var(--success-color)" },
1952
+ { name: "Failed", data: data.failed, color: "var(--danger-color)" },
1953
+ { name: "Skipped", data: data.skipped, color: "var(--warning-color)" },
1954
+ ];
1955
+
1956
+ const seriesDataStr = JSON.stringify(seriesData);
1957
+ const categoriesStr = JSON.stringify(severityLevels);
1958
+
1959
+ return `
1960
+ <div class="trend-chart" style="height: 600px; padding: 28px; box-sizing: border-box;">
1961
+ <h3 class="chart-title-header">Severity Distribution</h3>
1962
+ <div id="${chartId}" class="lazy-load-chart" data-render-function-name="${renderFunctionName}" style="width: 100%; height: 100%;">
1963
+ <div class="no-data">Loading Severity Chart...</div>
1964
+ </div>
1965
+ <script>
1966
+ window.${renderFunctionName} = function() {
1967
+ const chartContainer = document.getElementById('${chartId}');
1968
+ if (!chartContainer) return;
1969
+
1970
+ if (typeof Highcharts !== 'undefined') {
1971
+ try {
1972
+ chartContainer.innerHTML = '';
1973
+ Highcharts.chart('${chartId}', {
1974
+ chart: { type: 'column', backgroundColor: 'transparent' },
1975
+ title: { text: null },
1976
+ xAxis: {
1977
+ categories: ${categoriesStr},
1978
+ crosshair: true,
1979
+ labels: { style: { color: 'var(--text-color-secondary)' } }
1980
+ },
1981
+ yAxis: {
1982
+ min: 0,
1983
+ title: { text: 'Test Count', style: { color: 'var(--text-color)' } },
1984
+ stackLabels: { enabled: true, style: { fontWeight: 'bold', color: 'var(--text-color)' } },
1985
+ labels: { style: { color: 'var(--text-color-secondary)' } }
1986
+ },
1987
+ legend: {
1988
+ itemStyle: { color: 'var(--text-color)' }
1989
+ },
1990
+ tooltip: {
1991
+ shared: true,
1992
+ useHTML: true,
1993
+ backgroundColor: 'rgba(10,10,10,0.92)',
1994
+ style: { color: '#f5f5f5' },
1995
+ formatter: function() {
1996
+ // Custom formatter to HIDE 0 values
1997
+ let tooltip = '';
1998
+ let hasItems = false;
1999
+
2000
+ this.points.forEach(point => {
2001
+ if (point.y > 0) { // ONLY show if count > 0
2002
+ tooltip += '<span style="color:' + point.series.color + '">●</span> ' +
2003
+ point.series.name + ': <b>' + point.y + '</b><br/>';
2004
+ hasItems = true;
2005
+ }
2006
+ });
2007
+
2008
+ if (!hasItems) return false; // Hide tooltip entirely if no data
2009
+
2010
+ // Calculate total from visible points to ensure accuracy or use stackTotal
2011
+ tooltip += 'Total: ' + this.points[0].total;
2012
+ return tooltip;
2013
+ }
2014
+ },
2015
+ plotOptions: {
2016
+ column: {
2017
+ stacking: 'normal',
2018
+ dataLabels: {
2019
+ enabled: true,
2020
+ color: '#fff',
2021
+ style: { textOutline: 'none' },
2022
+ formatter: function() {
2023
+ return (this.y > 0) ? this.y : null; // Hide 0 labels on chart bars
2024
+ }
2025
+ },
2026
+ borderRadius: 3
2027
+ }
2028
+ },
2029
+ series: ${seriesDataStr},
2030
+ credits: { enabled: false }
2031
+ });
2032
+ } catch(e) {
2033
+ console.error("Error rendering severity chart:", e);
2034
+ chartContainer.innerHTML = '<div class="no-data">Error rendering chart.</div>';
2035
+ }
2036
+ }
2037
+ };
2038
+ </script>
2039
+ </div>
2040
+ `;
2041
+ }
2042
+ /**
2043
+ * Generates the HTML content for the report.
2044
+ * @param {object} reportData - The report data object containing run and results.
2045
+ * @param {object} trendData - Optional trend data object for additional trends.
2046
+ * @returns {string} HTML string for the report.
2047
+ */
1647
2048
  function generateHTML(reportData, trendData = null) {
1648
2049
  const { run, results } = reportData;
1649
2050
  const suitesData = getSuitesData(reportData.results || []);
@@ -1681,6 +2082,28 @@ function generateHTML(reportData, trendData = null) {
1681
2082
  const testFileParts = test.name.split(" > ");
1682
2083
  const testTitle =
1683
2084
  testFileParts[testFileParts.length - 1] || "Unnamed Test";
2085
+ // --- NEW: Severity Logic ---
2086
+ const severity = test.severity || "Medium";
2087
+ const getSeverityColor = (level) => {
2088
+ switch (level) {
2089
+ case "Minor":
2090
+ return "#006064";
2091
+ case "Low":
2092
+ return "#FFA07A";
2093
+ case "Medium":
2094
+ return "#577A11";
2095
+ case "High":
2096
+ return "#B71C1C";
2097
+ case "Critical":
2098
+ return "#64158A";
2099
+ default:
2100
+ return "#577A11";
2101
+ }
2102
+ };
2103
+ const severityColor = getSeverityColor(severity);
2104
+ // We reuse 'status-badge' class for size/font consistency, but override background color
2105
+ const severityBadge = `<span class="status-badge" style="background-color: ${severityColor}; margin-right: 8px;">${severity}</span>`;
2106
+ // ---------------------------
1684
2107
  const generateStepsHTML = (steps, depth = 0) => {
1685
2108
  if (!steps || steps.length === 0)
1686
2109
  return "<div class='no-steps'>No steps recorded for this test.</div>";
@@ -1774,6 +2197,7 @@ function generateHTML(reportData, trendData = null) {
1774
2197
  <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
1775
2198
  </div>
1776
2199
  <div class="test-case-meta">
2200
+ ${severityBadge}
1777
2201
  ${
1778
2202
  test.tags && test.tags.length > 0
1779
2203
  ? test.tags
@@ -2049,10 +2473,10 @@ function generateHTML(reportData, trendData = null) {
2049
2473
  <head>
2050
2474
  <meta charset="UTF-8">
2051
2475
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2052
- <link rel="icon" type="image/png" href="https://i.postimg.cc/v817w4sg/logo.png">
2053
- <link rel="apple-touch-icon" href="https://i.postimg.cc/v817w4sg/logo.png">
2476
+ <link rel="icon" type="image/png" href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png">
2477
+ <link rel="apple-touch-icon" href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png">
2054
2478
  <script src="https://code.highcharts.com/highcharts.js" defer></script>
2055
- <title>Playwright Pulse Report</title>
2479
+ <title>Pulse Report</title>
2056
2480
  <style>
2057
2481
  :root {
2058
2482
  --primary-color: #3f51b5; --secondary-color: #ff4081; --accent-color: #673ab7; --accent-color-alt: #FF9800;
@@ -2093,7 +2517,7 @@ function generateHTML(reportData, trendData = null) {
2093
2517
  .status-passed .value, .stat-passed svg { color: var(--success-color); }
2094
2518
  .status-failed .value, .stat-failed svg { color: var(--danger-color); }
2095
2519
  .status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
2096
- .dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: stretch; }
2520
+ .dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: start; }
2097
2521
  .pie-chart-wrapper, .suites-widget, .trend-chart { background-color: var(--card-background-color); padding: 28px; border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); display: flex; flex-direction: column; }
2098
2522
  .pie-chart-wrapper h3, .suites-header h2, .trend-chart h3 { text-align: center; margin-top: 0; margin-bottom: 25px; font-size: 1.25em; font-weight: 600; color: var(--text-color); }
2099
2523
  .trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { flex-grow: 1; min-height: 250px; }
@@ -2196,6 +2620,7 @@ function generateHTML(reportData, trendData = null) {
2196
2620
  .status-badge-small.status-failed { background-color: var(--danger-color); }
2197
2621
  .status-badge-small.status-skipped { background-color: var(--warning-color); }
2198
2622
  .status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
2623
+ .badge-severity { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; color: white; text-transform: uppercase; margin-right: 8px; vertical-align: middle; }
2199
2624
  .no-data, .no-tests, .no-steps, .no-data-chart { padding: 28px; text-align: center; color: var(--dark-gray-color); font-style: italic; font-size:1.1em; background-color: var(--light-gray-color); border-radius: var(--border-radius); margin: 18px 0; border: 1px dashed var(--medium-gray-color); }
2200
2625
  .no-data-chart {font-size: 0.95em; padding: 18px;}
2201
2626
  .ai-failure-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 22px; }
@@ -2479,8 +2904,8 @@ function generateHTML(reportData, trendData = null) {
2479
2904
  <div class="container">
2480
2905
  <header class="header">
2481
2906
  <div class="header-title">
2482
- <img id="report-logo" src="https://i.postimg.cc/v817w4sg/logo.png" alt="Report Logo">
2483
- <h1>Playwright Pulse Report</h1>
2907
+ <img id="report-logo" src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png" alt="Report Logo">
2908
+ <h1>Pulse Report</h1>
2484
2909
  </div>
2485
2910
  <div class="run-info"><strong>Run Date:</strong> ${formatDate(
2486
2911
  runSummary.timestamp
@@ -2514,7 +2939,7 @@ function generateHTML(reportData, trendData = null) {
2514
2939
  )}</div></div>
2515
2940
  </div>
2516
2941
  <div class="dashboard-bottom-row">
2517
- <div style="display: grid; gap: 20px">
2942
+ <div style="display: flex; flex-direction: column; gap: 28px;">
2518
2943
  ${generatePieChart(
2519
2944
  [
2520
2945
  { label: "Passed", value: runSummary.passed },
@@ -2531,9 +2956,13 @@ function generateHTML(reportData, trendData = null) {
2531
2956
  : '<div class="no-data">Environment data not available.</div>'
2532
2957
  }
2533
2958
  </div>
2959
+
2960
+ <div style="display: flex; flex-direction: column; gap: 28px;">
2534
2961
  ${generateSuitesWidget(suitesData)}
2962
+ ${generateSeverityDistributionChart(results)}
2963
+ </div>
2535
2964
  </div>
2536
- </div>
2965
+ </div>
2537
2966
  <div id="test-runs" class="tab-content">
2538
2967
  <div class="filters">
2539
2968
  <input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
@@ -2572,6 +3001,16 @@ function generateHTML(reportData, trendData = null) {
2572
3001
  }
2573
3002
  </div>
2574
3003
  </div>
3004
+ <div class="trend-charts-row">
3005
+ <div class="trend-chart">
3006
+ <h3 class="chart-title-header">Duration by Spec files</h3>
3007
+ ${generateSpecDurationChart(results)}
3008
+ </div>
3009
+ <div class="trend-chart">
3010
+ <h3 class="chart-title-header">Duration by Test Describe</h3>
3011
+ ${generateDescribeDurationChart(results)}
3012
+ </div>
3013
+ </div>
2575
3014
  <h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
2576
3015
  <div class="trend-charts-row">
2577
3016
  <div class="trend-chart">
@@ -3282,4 +3721,4 @@ main().catch((err) => {
3282
3721
  );
3283
3722
  console.error(err.stack);
3284
3723
  process.exit(1);
3285
- });
3724
+ });