@arghajit/playwright-pulse-report 0.2.10 → 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.
@@ -5,6 +5,7 @@ import { readFileSync, existsSync as fsExistsSync } from "fs";
5
5
  import path from "path";
6
6
  import { fork } from "child_process";
7
7
  import { fileURLToPath } from "url";
8
+ import { getOutputDir } from "./config-reader.mjs";
8
9
 
9
10
  /**
10
11
  * Dynamically imports the 'chalk' library for terminal string styling.
@@ -393,6 +394,7 @@ function generateTestTrendsChart(trendData) {
393
394
  </script>
394
395
  `;
395
396
  }
397
+ const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
396
398
  /**
397
399
  * Generates HTML and JavaScript for a Highcharts area chart to display test duration trends.
398
400
  * @param {object} trendData Data for duration trends.
@@ -412,8 +414,6 @@ function generateDurationTrendChart(trendData) {
412
414
  )}`;
413
415
  const runs = trendData.overall;
414
416
 
415
- const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
416
-
417
417
  const chartDataString = JSON.stringify(runs.map((run) => run.duration));
418
418
  const categoriesString = JSON.stringify(runs.map((run, i) => `Run ${i + 1}`));
419
419
  const runsForTooltip = runs.map((r) => ({
@@ -776,6 +776,9 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
776
776
  const cardHeight = Math.floor(dashboardHeight * 0.44);
777
777
  const cardContentPadding = 16; // px
778
778
 
779
+ // Logic for Run Context
780
+ const runContext = process.env.CI ? "CI" : "Local Test";
781
+
779
782
  return `
780
783
  <div class="environment-dashboard-wrapper" id="${dashboardId}">
781
784
  <style>
@@ -819,6 +822,20 @@ gap: 20px;
819
822
  font-size: 14px;
820
823
  }
821
824
 
825
+ /* Mobile Responsiveness */
826
+ @media (max-width: 768px) {
827
+ .environment-dashboard-wrapper {
828
+ grid-template-columns: 1fr; /* Stack columns on mobile */
829
+ grid-template-rows: auto;
830
+ padding: 16px;
831
+ height: auto !important; /* Allow height to grow */
832
+ }
833
+ .env-card {
834
+ height: auto !important; /* Allow cards to grow based on content */
835
+ min-height: 200px;
836
+ }
837
+ }
838
+
822
839
  .env-dashboard-header {
823
840
  grid-column: 1 / -1;
824
841
  display: flex;
@@ -827,6 +844,8 @@ align-items: center;
827
844
  border-bottom: 1px solid var(--border-color);
828
845
  padding-bottom: 16px;
829
846
  margin-bottom: 8px;
847
+ flex-wrap: wrap; /* Allow wrapping header items */
848
+ gap: 10px;
830
849
  }
831
850
 
832
851
  .env-dashboard-title {
@@ -1086,7 +1105,7 @@ border-color: var(--border-color);
1086
1105
  </div>
1087
1106
  <div class="env-detail-row">
1088
1107
  <span class="env-detail-label">Run Context</span>
1089
- <span class="env-detail-value">CI/Local Test</span>
1108
+ <span class="env-detail-value">${runContext}</span>
1090
1109
  </div>
1091
1110
  </div>
1092
1111
  </div>
@@ -1720,7 +1739,7 @@ function generateAIFailureAnalyzerTab(results) {
1720
1739
  </div>
1721
1740
  </div>
1722
1741
  <p class="ai-analyzer-description">
1723
- Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for specific failed test.
1742
+ Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for instant analysis or use Copy AI Prompt to analyze with your preferred AI tool.
1724
1743
  </p>
1725
1744
 
1726
1745
  <div class="compact-failure-list">
@@ -1748,9 +1767,14 @@ function generateAIFailureAnalyzerTab(results) {
1748
1767
  )}</span>
1749
1768
  </div>
1750
1769
  </div>
1751
- <button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
1752
- <span class="ai-text">AI Fix</span>
1753
- </button>
1770
+ <div class="ai-buttons-group">
1771
+ <button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
1772
+ <span class="ai-text">AI Fix</span>
1773
+ </button>
1774
+ <button class="copy-prompt-btn" onclick="copyAIPrompt(this)" data-test-json="${testJson}" title="Copy AI Prompt">
1775
+ <span class="copy-prompt-text">Copy AI Prompt</span>
1776
+ </button>
1777
+ </div>
1754
1778
  </div>
1755
1779
  <div class="failure-error-preview">
1756
1780
  <div class="error-snippet">${formatPlaywrightError(
@@ -1775,6 +1799,409 @@ function generateAIFailureAnalyzerTab(results) {
1775
1799
  </div>
1776
1800
  `;
1777
1801
  }
1802
+ /**
1803
+ * Generates a area chart showing the total duration per spec file.
1804
+ * The chart is lazy-loaded and rendered with Highcharts when scrolled into view.
1805
+ *
1806
+ * @param {Array<object>} results - Array of test result objects.
1807
+ * @returns {string} HTML string containing the chart container and lazy-loading script.
1808
+ */
1809
+ function generateSpecDurationChart(results) {
1810
+ if (!results || results.length === 0)
1811
+ return '<div class="no-data">No results available.</div>';
1812
+
1813
+ const specDurations = {};
1814
+ results.forEach((test) => {
1815
+ // Use the dedicated 'spec_file' key
1816
+ const fileName = test.spec_file || "Unknown File";
1817
+
1818
+ if (!specDurations[fileName]) specDurations[fileName] = 0;
1819
+ specDurations[fileName] += test.duration;
1820
+ });
1821
+
1822
+ const categories = Object.keys(specDurations);
1823
+ // We map 'name' here, which we will use in the tooltip later
1824
+ const data = categories.map((cat) => ({
1825
+ y: specDurations[cat],
1826
+ name: cat,
1827
+ }));
1828
+
1829
+ if (categories.length === 0)
1830
+ return '<div class="no-data">No spec data found.</div>';
1831
+
1832
+ const chartId = `specDurChart-${Date.now()}-${Math.random()
1833
+ .toString(36)
1834
+ .substring(2, 7)}`;
1835
+ const renderFunctionName = `renderSpecDurChart_${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 Spec 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: { type: 'area', height: 350, backgroundColor: 'transparent' },
1853
+ title: { text: null },
1854
+ xAxis: {
1855
+ categories: ${categoriesStr},
1856
+ visible: false, // 1. HIDE THE X-AXIS
1857
+ title: { text: null },
1858
+ crosshair: true
1859
+ },
1860
+ yAxis: {
1861
+ min: 0,
1862
+ title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
1863
+ labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
1864
+ },
1865
+ legend: { layout: 'horizontal', align: 'center', verticalAlign: 'bottom', itemStyle: { fontSize: '12px', color: 'var(--text-color)' }},
1866
+ plotOptions: { area: { lineWidth: 2.5, states: { hover: { lineWidthPlus: 0 } }, threshold: null }},
1867
+ tooltip: {
1868
+ shared: true,
1869
+ useHTML: true,
1870
+ backgroundColor: 'rgba(10,10,10,0.92)',
1871
+ borderColor: 'rgba(10,10,10,0.92)',
1872
+ style: { color: '#f5f5f5' },
1873
+ formatter: function() {
1874
+ const point = this.points ? this.points[0].point : this.point;
1875
+ const color = point.color || point.series.color;
1876
+
1877
+ // 2. FIX: Use 'point.name' instead of 'this.x' to get the actual filename
1878
+ return '<span style="color:' + color + '">●</span> <b>File: ' + point.name + '</b><br/>' +
1879
+ 'Duration: <b>' + formatDuration(this.y) + '</b>';
1880
+ }
1881
+ },
1882
+ series: [{
1883
+ name: 'Duration',
1884
+ data: ${dataStr},
1885
+ color: 'var(--accent-color-alt)',
1886
+ type: 'area',
1887
+ marker: { symbol: 'circle', enabled: true, radius: 4, states: { hover: { radius: 6, lineWidthPlus: 0 } } },
1888
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(${accentColorAltRGB}, 0.4)'], [1, 'rgba(${accentColorAltRGB}, 0.05)']] },
1889
+ lineWidth: 2.5
1890
+ }],
1891
+ credits: { enabled: false }
1892
+ });
1893
+ } catch (e) { console.error("Error rendering spec chart:", e); }
1894
+ }
1895
+ };
1896
+ </script>
1897
+ `;
1898
+ }
1899
+ /**
1900
+ * Generates a vertical bar chart showing the total duration of each test describe block.
1901
+ * Tests without a describe block or with "n/a" / empty describe names are ignored.
1902
+ * @param {Array<object>} results - Array of test result objects.
1903
+ * @returns {string} HTML string containing the chart container and lazy-loading script.
1904
+ */
1905
+ function generateDescribeDurationChart(results) {
1906
+ if (!results || results.length === 0)
1907
+ return '<div class="no-data">Seems like there is test describe block available in the executed test suite.</div>';
1908
+
1909
+ const describeMap = new Map();
1910
+ let foundAnyDescribe = false;
1911
+
1912
+ results.forEach((test) => {
1913
+ if (test.describe) {
1914
+ const describeName = test.describe;
1915
+ // Filter out invalid describe blocks
1916
+ if (
1917
+ !describeName ||
1918
+ describeName.trim().toLowerCase() === "n/a" ||
1919
+ describeName.trim() === ""
1920
+ ) {
1921
+ return;
1922
+ }
1923
+
1924
+ foundAnyDescribe = true;
1925
+ const fileName = test.spec_file || "Unknown File";
1926
+ const key = fileName + "::" + describeName;
1927
+
1928
+ if (!describeMap.has(key)) {
1929
+ describeMap.set(key, {
1930
+ duration: 0,
1931
+ file: fileName,
1932
+ describe: describeName,
1933
+ });
1934
+ }
1935
+ describeMap.get(key).duration += test.duration;
1936
+ }
1937
+ });
1938
+
1939
+ if (!foundAnyDescribe) {
1940
+ return '<div class="no-data">No valid test describe blocks found.</div>';
1941
+ }
1942
+
1943
+ const categories = [];
1944
+ const data = [];
1945
+
1946
+ for (const [key, val] of describeMap.entries()) {
1947
+ categories.push(val.describe);
1948
+ data.push({
1949
+ y: val.duration,
1950
+ name: val.describe,
1951
+ custom: {
1952
+ fileName: val.file,
1953
+ describeName: val.describe,
1954
+ },
1955
+ });
1956
+ }
1957
+
1958
+ const chartId = `descDurChart-${Date.now()}-${Math.random()
1959
+ .toString(36)
1960
+ .substring(2, 7)}`;
1961
+ const renderFunctionName = `renderDescDurChart_${chartId.replace(/-/g, "_")}`;
1962
+
1963
+ const categoriesStr = JSON.stringify(categories);
1964
+ const dataStr = JSON.stringify(data);
1965
+
1966
+ return `
1967
+ <div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
1968
+ <div class="no-data">Loading Describe Duration Chart...</div>
1969
+ </div>
1970
+ <script>
1971
+ window.${renderFunctionName} = function() {
1972
+ const chartContainer = document.getElementById('${chartId}');
1973
+ if (!chartContainer) return;
1974
+ if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
1975
+ try {
1976
+ chartContainer.innerHTML = '';
1977
+ Highcharts.chart('${chartId}', {
1978
+ chart: {
1979
+ type: 'column', // 1. CHANGED: 'bar' -> 'column' for vertical bars
1980
+ height: 400, // 2. CHANGED: Fixed height works better for vertical charts
1981
+ backgroundColor: 'transparent'
1982
+ },
1983
+ title: { text: null },
1984
+ xAxis: {
1985
+ categories: ${categoriesStr},
1986
+ visible: false, // Hidden as requested
1987
+ title: { text: null },
1988
+ crosshair: true
1989
+ },
1990
+ yAxis: {
1991
+ min: 0,
1992
+ title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
1993
+ labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
1994
+ },
1995
+ legend: { enabled: false },
1996
+ plotOptions: {
1997
+ series: {
1998
+ borderRadius: 4,
1999
+ borderWidth: 0,
2000
+ states: { hover: { brightness: 0.1 }}
2001
+ },
2002
+ column: { pointPadding: 0.2, groupPadding: 0.1 } // Adjust spacing for columns
2003
+ },
2004
+ tooltip: {
2005
+ shared: true,
2006
+ useHTML: true,
2007
+ backgroundColor: 'rgba(10,10,10,0.92)',
2008
+ borderColor: 'rgba(10,10,10,0.92)',
2009
+ style: { color: '#f5f5f5' },
2010
+ formatter: function() {
2011
+ const point = this.points ? this.points[0].point : this.point;
2012
+ const file = (point.custom && point.custom.fileName) ? point.custom.fileName : 'Unknown';
2013
+ const desc = point.name || 'Unknown';
2014
+ const color = point.color || point.series.color;
2015
+
2016
+ return '<span style="color:' + color + '">●</span> <b>Describe: ' + desc + '</b><br/>' +
2017
+ '<span style="opacity: 0.8; font-size: 0.9em; color: #ddd;">File: ' + file + '</span><br/>' +
2018
+ 'Duration: <b>' + formatDuration(point.y) + '</b>';
2019
+ }
2020
+ },
2021
+ series: [{
2022
+ name: 'Duration',
2023
+ data: ${dataStr},
2024
+ color: 'var(--accent-color-alt)',
2025
+ }],
2026
+ credits: { enabled: false }
2027
+ });
2028
+ } catch (e) { console.error("Error rendering describe chart:", e); }
2029
+ }
2030
+ };
2031
+ </script>
2032
+ `;
2033
+ }
2034
+ /**
2035
+ * Generates a stacked column chart showing test results distributed by severity.
2036
+ * Matches dimensions of the System Environment section (~600px).
2037
+ * Lazy-loaded for performance.
2038
+ */
2039
+ function generateSeverityDistributionChart(results) {
2040
+ if (!results || results.length === 0) {
2041
+ return '<div class="trend-chart" style="height: 600px;"><div class="no-data">No results available for severity distribution.</div></div>';
2042
+ }
2043
+
2044
+ const severityLevels = ["Critical", "High", "Medium", "Low", "Minor"];
2045
+ const data = {
2046
+ passed: [0, 0, 0, 0, 0],
2047
+ failed: [0, 0, 0, 0, 0],
2048
+ skipped: [0, 0, 0, 0, 0],
2049
+ };
2050
+
2051
+ results.forEach((test) => {
2052
+ const sev = test.severity || "Medium";
2053
+ const status = String(test.status).toLowerCase();
2054
+
2055
+ let index = severityLevels.indexOf(sev);
2056
+ if (index === -1) index = 2; // Default to Medium
2057
+
2058
+ if (status === "passed") {
2059
+ data.passed[index]++;
2060
+ } else if (
2061
+ status === "failed" ||
2062
+ status === "timedout" ||
2063
+ status === "interrupted"
2064
+ ) {
2065
+ data.failed[index]++;
2066
+ } else {
2067
+ data.skipped[index]++;
2068
+ }
2069
+ });
2070
+
2071
+ const chartId = `sevDistChart-${Date.now()}-${Math.random()
2072
+ .toString(36)
2073
+ .substring(2, 7)}`;
2074
+ const renderFunctionName = `renderSevDistChart_${chartId.replace(/-/g, "_")}`;
2075
+
2076
+ const seriesData = [
2077
+ { name: "Passed", data: data.passed, color: "var(--success-color)" },
2078
+ { name: "Failed", data: data.failed, color: "var(--danger-color)" },
2079
+ { name: "Skipped", data: data.skipped, color: "var(--warning-color)" },
2080
+ ];
2081
+
2082
+ const seriesDataStr = JSON.stringify(seriesData);
2083
+ const categoriesStr = JSON.stringify(severityLevels);
2084
+
2085
+ return `
2086
+ <div class="trend-chart" style="height: 600px; padding: 28px; box-sizing: border-box;">
2087
+ <h3 class="chart-title-header">Severity Distribution</h3>
2088
+ <div id="${chartId}" class="lazy-load-chart" data-render-function-name="${renderFunctionName}" style="width: 100%; height: 100%;">
2089
+ <div class="no-data">Loading Severity Chart...</div>
2090
+ </div>
2091
+ <script>
2092
+ window.${renderFunctionName} = function() {
2093
+ const chartContainer = document.getElementById('${chartId}');
2094
+ if (!chartContainer) return;
2095
+
2096
+ if (typeof Highcharts !== 'undefined') {
2097
+ try {
2098
+ chartContainer.innerHTML = '';
2099
+ Highcharts.chart('${chartId}', {
2100
+ chart: { type: 'column', backgroundColor: 'transparent' },
2101
+ title: { text: null },
2102
+ xAxis: {
2103
+ categories: ${categoriesStr},
2104
+ crosshair: true,
2105
+ labels: { style: { color: 'var(--text-color-secondary)' } }
2106
+ },
2107
+ yAxis: {
2108
+ min: 0,
2109
+ title: { text: 'Test Count', style: { color: 'var(--text-color)' } },
2110
+ stackLabels: { enabled: true, style: { fontWeight: 'bold', color: 'var(--text-color)' } },
2111
+ labels: { style: { color: 'var(--text-color-secondary)' } }
2112
+ },
2113
+ legend: {
2114
+ itemStyle: { color: 'var(--text-color)' }
2115
+ },
2116
+ tooltip: {
2117
+ shared: true,
2118
+ useHTML: true,
2119
+ backgroundColor: 'rgba(10,10,10,0.92)',
2120
+ style: { color: '#f5f5f5' },
2121
+ formatter: function() {
2122
+ // Custom formatter to HIDE 0 values
2123
+ let tooltip = '';
2124
+ let hasItems = false;
2125
+
2126
+ this.points.forEach(point => {
2127
+ if (point.y > 0) { // ONLY show if count > 0
2128
+ tooltip += '<span style="color:' + point.series.color + '">●</span> ' +
2129
+ point.series.name + ': <b>' + point.y + '</b><br/>';
2130
+ hasItems = true;
2131
+ }
2132
+ });
2133
+
2134
+ if (!hasItems) return false; // Hide tooltip entirely if no data
2135
+
2136
+ // Calculate total from visible points to ensure accuracy or use stackTotal
2137
+ tooltip += 'Total: ' + this.points[0].total;
2138
+ return tooltip;
2139
+ }
2140
+ },
2141
+ plotOptions: {
2142
+ column: {
2143
+ stacking: 'normal',
2144
+ dataLabels: {
2145
+ enabled: true,
2146
+ color: '#fff',
2147
+ style: { textOutline: 'none' },
2148
+ formatter: function() {
2149
+ return (this.y > 0) ? this.y : null; // Hide 0 labels on chart bars
2150
+ }
2151
+ },
2152
+ borderRadius: 3
2153
+ }
2154
+ },
2155
+ series: ${seriesDataStr},
2156
+ credits: { enabled: false }
2157
+ });
2158
+ } catch(e) {
2159
+ console.error("Error rendering severity chart:", e);
2160
+ chartContainer.innerHTML = '<div class="no-data">Error rendering chart.</div>';
2161
+ }
2162
+ }
2163
+ };
2164
+ </script>
2165
+ </div>
2166
+ `;
2167
+ }
2168
+ /**
2169
+ * Helper to generate Lazy Media HTML using the Script Tag pattern.
2170
+ * This prevents the browser from parsing massive Base64 strings on page load.
2171
+ */
2172
+ function createLazyMedia(base64Data, mimeType, type, index, filename) {
2173
+ const uniqueId = `media-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2174
+ const dataUri = `data:${mimeType};base64,${base64Data}`;
2175
+ // Store heavy data in a non-rendering script tag
2176
+ const storage = `<script type="text/plain" id="data-${uniqueId}">${dataUri}</script>`;
2177
+
2178
+ let mediaTag = '';
2179
+ let btnText = '';
2180
+ let icon = '';
2181
+
2182
+ if (type === 'video') {
2183
+ mediaTag = `<video id="${uniqueId}" controls style="display:none; width: 100%; aspect-ratio: 16/9; margin-bottom: 10px;"></video>`;
2184
+ btnText = '▶ Load Video';
2185
+ } else {
2186
+ mediaTag = `<img id="${uniqueId}" alt="Screenshot ${index}" style="display:none; width: 100%; aspect-ratio: 4/3; object-fit: cover; border-bottom: 1px solid var(--border-color);" />`;
2187
+ btnText = '📷 Load Image';
2188
+ }
2189
+
2190
+ return `
2191
+ <div class="attachment-item ${type === 'video' ? 'video-item' : ''}">
2192
+ ${storage}
2193
+ <div class="lazy-placeholder" style="padding: 2rem; text-align: center; background: var(--light-gray-color); display: flex; flex-direction: column; align-items: center; justify-content: center; height: 180px;">
2194
+ <button class="ai-fix-btn" onclick="loadMedia('${uniqueId}', '${type}')" style="margin: 0 auto; font-size: 0.9rem;">${btnText}</button>
2195
+ <span style="font-size: 0.8rem; color: var(--text-color-secondary); margin-top: 8px;">(Click to view)</span>
2196
+ </div>
2197
+ ${mediaTag}
2198
+ <div class="attachment-info">
2199
+ <div class="trace-actions">
2200
+ <a href="#" onclick="event.preventDefault(); downloadMedia('${uniqueId}', '${filename}')" class="download-trace" style="width:100%; text-align:center;">Download</a>
2201
+ </div>
2202
+ </div>
2203
+ </div>`;
2204
+ }
1778
2205
  /**
1779
2206
  * Generates the HTML report.
1780
2207
  * @param {object} reportData - The data for the report.
@@ -1807,16 +2234,46 @@ function generateHTML(reportData, trendData = null) {
1807
2234
  * Generates the HTML for the test cases.
1808
2235
  * @returns {string} The HTML for the test cases.
1809
2236
  */
1810
- function generateTestCasesHTML(subset = results, baseIndex = 0) {
1811
- if (!results || results.length === 0)
2237
+ // MODIFIED: Accepts 'subset' (chunk of tests) and 'offset' (start index)
2238
+ function generateTestCasesHTML(subset, offset = 0) {
2239
+ // Use the subset if provided, otherwise fallback to all results (legacy compatibility)
2240
+ const data = subset || results;
2241
+
2242
+ if (!data || data.length === 0)
1812
2243
  return '<div class="no-tests">No test results found in this run.</div>';
1813
- return subset
2244
+
2245
+ return data
1814
2246
  .map((test, i) => {
1815
- const testIndex = baseIndex + i;
2247
+ // Calculate the global index (essential for unique IDs across chunks)
2248
+ const testIndex = offset + i;
2249
+
1816
2250
  const browser = test.browser || "unknown";
1817
2251
  const testFileParts = test.name.split(" > ");
1818
2252
  const testTitle =
1819
2253
  testFileParts[testFileParts.length - 1] || "Unnamed Test";
2254
+
2255
+ // --- Severity Logic ---
2256
+ const severity = test.severity || "Medium";
2257
+ const getSeverityColor = (level) => {
2258
+ switch (level) {
2259
+ case "Minor":
2260
+ return "#006064";
2261
+ case "Low":
2262
+ return "#FFA07A";
2263
+ case "Medium":
2264
+ return "#577A11";
2265
+ case "High":
2266
+ return "#B71C1C";
2267
+ case "Critical":
2268
+ return "#64158A";
2269
+ default:
2270
+ return "#577A11";
2271
+ }
2272
+ };
2273
+ const severityColor = getSeverityColor(severity);
2274
+ const severityBadge = `<span class="status-badge" style="background-color: ${severityColor}; margin-right: 8px;">${severity}</span>`;
2275
+
2276
+ // --- Step Generation ---
1820
2277
  const generateStepsHTML = (steps, depth = 0) => {
1821
2278
  if (!steps || steps.length === 0)
1822
2279
  return "<div class='no-steps'>No steps recorded for this test.</div>";
@@ -1828,323 +2285,267 @@ function generateHTML(reportData, trendData = null) {
1828
2285
  ? `step-hook step-hook-${step.hookType}`
1829
2286
  : "";
1830
2287
  const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
1831
- return `<div class="step-item" style="--depth: ${depth};"><div class="step-header ${stepClass}" role="button" aria-expanded="false"><span class="step-icon">${getStatusIcon(
1832
- step.status
1833
- )}</span><span class="step-title">${sanitizeHTML(
2288
+ return `
2289
+ <div class="step-item" style="--depth: ${depth};">
2290
+ <div class="step-header ${stepClass}" role="button" aria-expanded="false">
2291
+ <span class="step-icon">${getStatusIcon(step.status)}</span>
2292
+ <span class="step-title">${sanitizeHTML(
1834
2293
  step.title
1835
- )}${hookIndicator}</span><span class="step-duration">${formatDuration(
2294
+ )}${hookIndicator}</span>
2295
+ <span class="step-duration">${formatDuration(
1836
2296
  step.duration
1837
- )}</span></div><div class="step-details" style="display: none;">${
2297
+ )}</span>
2298
+ </div>
2299
+ <div class="step-details" style="display: none;">
2300
+ ${
1838
2301
  step.codeLocation
1839
2302
  ? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
1840
2303
  step.codeLocation
1841
2304
  )}</div>`
1842
2305
  : ""
1843
- }${
2306
+ }
2307
+ ${
1844
2308
  step.errorMessage
1845
- ? `<div class="test-error-summary">${
1846
- step.stackTrace
1847
- ? `<div class="stack-trace">${formatPlaywrightError(
1848
- step.stackTrace
1849
- )}</div>`
1850
- : ""
1851
- }<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
1852
- : ""
1853
- }${
1854
- (() => {
1855
- if (!step.attachments || step.attachments.length === 0) return "";
1856
- return `<div class="attachments-section"><h4>Step Attachments</h4><div class="attachments-grid">${step.attachments
1857
- .map((attachment) => {
1858
- try {
1859
- const attachmentPath = path.resolve(
1860
- DEFAULT_OUTPUT_DIR,
1861
- attachment.path
1862
- );
1863
- if (!fsExistsSync(attachmentPath)) {
1864
- return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
1865
- attachment.name
1866
- )}</div>`;
1867
- }
1868
- const attachmentBase64 = readFileSync(attachmentPath).toString("base64");
1869
- const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
1870
- return `<div class="attachment-item generic-attachment">
1871
- <div class="attachment-icon">${getAttachmentIcon(attachment.contentType)}</div>
1872
- <div class="attachment-caption">
1873
- <span class="attachment-name" title="${sanitizeHTML(attachment.name)}">${sanitizeHTML(attachment.name)}</span>
1874
- <span class="attachment-type">${sanitizeHTML(attachment.contentType)}</span>
1875
- </div>
1876
- <div class="attachment-info">
1877
- <div class="trace-actions">
1878
- <a href="#" data-href="${attachmentDataUri}" class="view-full lazy-load-attachment" target="_blank">View</a>
1879
- <a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(attachment.name)}">Download</a>
1880
- </div>
1881
- </div>
1882
- </div>`;
1883
- } catch (e) {
1884
- return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(attachment.name)}</div>`;
2309
+ ? `<div class="test-error-summary">
2310
+ ${
2311
+ step.stackTrace
2312
+ ? `<div class="stack-trace">${formatPlaywrightError(
2313
+ step.stackTrace
2314
+ )}</div>`
2315
+ : ""
1885
2316
  }
1886
- })
1887
- .join("")}</div></div>`;
1888
- })()
1889
- }${
2317
+ <button class="copy-error-btn" onclick="copyErrorToClipboard(this)" style="margin-top: 8px; padding: 4px 8px; background: #f0f0f0; border: 2px solid #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; border-color: #8B0000; color: #8B0000;" onmouseover="this.style.background='#e0e0e0'" onmouseout="this.style.background='#f0f0f0'">Copy Error Prompt</button>
2318
+ </div>`
2319
+ : ""
2320
+ }
2321
+ ${
1890
2322
  hasNestedSteps
1891
2323
  ? `<div class="nested-steps">${generateStepsHTML(
1892
2324
  step.steps,
1893
2325
  depth + 1
1894
2326
  )}</div>`
1895
2327
  : ""
1896
- }</div></div>`;
2328
+ }
2329
+ </div>
2330
+ </div>`;
1897
2331
  })
1898
2332
  .join("");
1899
2333
  };
1900
- return `<div class="test-case" data-status="${
1901
- test.status
1902
- }" data-browser="${sanitizeHTML(browser)}" data-tags="${(
1903
- test.tags || []
1904
- )
2334
+
2335
+ return `
2336
+ <div class="test-case" data-status="${
2337
+ test.status
2338
+ }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
1905
2339
  .join(",")
1906
- .toLowerCase()}" data-test-id="${sanitizeHTML(String(test.id || testIndex))}">
1907
- <div class="test-case-header" role="button" aria-expanded="false"><div class="test-case-summary"><span class="status-badge ${getStatusClass(
1908
- test.status
1909
- )}">${String(
2340
+ .toLowerCase()}">
2341
+ <div class="test-case-header" role="button" aria-expanded="false">
2342
+ <div class="test-case-summary">
2343
+ <span class="status-badge ${getStatusClass(test.status)}">${String(
1910
2344
  test.status
1911
- ).toUpperCase()}</span><span class="test-case-title" title="${sanitizeHTML(
1912
- test.name
1913
- )}">${sanitizeHTML(
1914
- testTitle
1915
- )}</span><span class="test-case-browser">(${sanitizeHTML(
1916
- browser
1917
- )})</span></div><div class="test-case-meta">${
1918
- test.tags && test.tags.length > 0
1919
- ? test.tags
1920
- .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
1921
- .join(" ")
1922
- : ""
1923
- }<span class="test-duration">${formatDuration(
1924
- test.duration
1925
- )}</span></div></div>
1926
- <div class="test-case-content" style="display: none;">
1927
- <p><strong>Full Path:</strong> ${sanitizeHTML(
1928
- test.name
1929
- )}</p>
1930
- <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
1931
- test.workerId
1932
- )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
2345
+ ).toUpperCase()}</span>
2346
+ <span class="test-case-title" title="${sanitizeHTML(
2347
+ test.name
2348
+ )}">${sanitizeHTML(testTitle)}</span>
2349
+ <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
2350
+ </div>
2351
+ <div class="test-case-meta">
2352
+ ${severityBadge}
2353
+ ${
2354
+ test.tags && test.tags.length > 0
2355
+ ? test.tags
2356
+ .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
2357
+ .join(" ")
2358
+ : ""
2359
+ }
2360
+ <span class="test-duration">${formatDuration(test.duration)}</span>
2361
+ </div>
2362
+ </div>
2363
+ <div class="test-case-content" style="display: none;">
2364
+ <p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
2365
+ ${
2366
+ test.annotations && test.annotations.length > 0
2367
+ ? `<div class="annotations-section" style="margin: 12px 0; padding: 12px; background-color: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.3); border-left: 4px solid #8b5cf6; border-radius: 4px;">
2368
+ <h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
2369
+ ${test.annotations
2370
+ .map((annotation, idx) => {
2371
+ const isIssueOrBug =
2372
+ annotation.type === "issue" ||
2373
+ annotation.type === "bug";
2374
+ const descriptionText = annotation.description || "";
2375
+ const typeLabel = sanitizeHTML(annotation.type);
2376
+ const descriptionHtml =
2377
+ isIssueOrBug && descriptionText.match(/^[A-Z]+-\d+$/)
2378
+ ? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
2379
+ descriptionText
2380
+ )}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
2381
+ descriptionText
2382
+ )}</a>`
2383
+ : sanitizeHTML(descriptionText);
2384
+ const locationText = annotation.location
2385
+ ? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
2386
+ annotation.location.file
2387
+ )}:${annotation.location.line}:${
2388
+ annotation.location.column
2389
+ }</div>`
2390
+ : "";
2391
+ return `<div style="margin-bottom: ${
2392
+ idx < test.annotations.length - 1 ? "10px" : "0"
2393
+ };"><strong style="color: #8b5cf6;">Type:</strong> <span style="background-color: rgba(139, 92, 246, 0.2); padding: 2px 8px; border-radius: 4px; font-size: 0.9em;">${typeLabel}</span>${
2394
+ descriptionText
2395
+ ? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
2396
+ : ""
2397
+ }${locationText}</div>`;
2398
+ })
2399
+ .join("")}
2400
+ </div>`
2401
+ : ""
2402
+ }
2403
+ <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
2404
+ test.workerId
2405
+ )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
1933
2406
  test.totalWorkers
1934
2407
  )}]</p>
1935
- ${
1936
- test.errorMessage
1937
- ? `<div class="test-error-summary">${formatPlaywrightError(
1938
- test.errorMessage
1939
- )}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
1940
- : ""
1941
- }
1942
- ${
1943
- test.snippet
1944
- ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
1945
- test.snippet
1946
- )}</code></pre></div>`
1947
- : ""
1948
- }
1949
- <h4>Steps</h4><div class="steps-list">${generateStepsHTML(
1950
- test.steps
1951
- )}</div>
1952
- ${(() => {
1953
- if (!test.stdout || test.stdout.length === 0)
1954
- return "";
1955
- // Create a unique ID for the <pre> element to target it for copying
1956
- const logId = `stdout-log-${test.id || testIndex}`;
1957
- return `<div class="console-output-section">
1958
- <h4>Console Output (stdout)
1959
- <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy Console</button>
1960
- </h4>
1961
- <div class="log-wrapper">
1962
- <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(
1963
- test.stdout
1964
- .map((line) => sanitizeHTML(line))
1965
- .join("\n")
1966
- )}</pre>
1967
- </div>
1968
- </div>`;
1969
- })()}
1970
- ${
1971
- test.stderr && test.stderr.length > 0
1972
- ? (() => {
1973
- const logId = `stderr-log-${test.id || testIndex}`;
1974
- return `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre id="${logId}" class="console-log stderr-log">${test.stderr
1975
- .map((line) => sanitizeHTML(line))
1976
- .join("\\n")}</pre></div>`;
1977
- })()
1978
- : ""
1979
- }
1980
-
1981
- ${(() => {
1982
- if (
1983
- !test.screenshots ||
1984
- test.screenshots.length === 0
1985
- )
1986
- return "";
1987
- return `<div class="attachments-section"><h4>Screenshots</h4><div class="attachments-grid">${test.screenshots
1988
- .map((screenshotPath, index) => {
1989
- try {
1990
- const imagePath = path.resolve(
1991
- DEFAULT_OUTPUT_DIR,
1992
- screenshotPath
1993
- );
1994
- if (!fsExistsSync(imagePath))
1995
- return `<div class="attachment-item error">Screenshot not found: ${sanitizeHTML(
1996
- screenshotPath
1997
- )}</div>`;
1998
- const base64ImageData =
1999
- readFileSync(imagePath).toString("base64");
2000
- return `<div class="attachment-item"><img src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" data-src="data:image/png;base64,${base64ImageData}" alt="Screenshot ${
2001
- index + 1
2002
- }" class="lazy-load-image"><div class="attachment-info"><div class="trace-actions"><a href="#" data-href="data:image/png;base64,${base64ImageData}" class="lazy-load-attachment" target="_blank" download="screenshot-${index}.png">Download</a></div></div></div>`;
2003
- } catch (e) {
2004
- return `<div class="attachment-item error">Failed to load screenshot: ${sanitizeHTML(
2005
- screenshotPath
2006
- )}</div>`;
2007
- }
2008
- })
2009
- .join("")}</div></div>`;
2010
- })()}
2011
-
2012
- ${(() => {
2013
- if (!test.videoPath || test.videoPath.length === 0)
2014
- return "";
2015
- return `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
2016
- .map((videoPath, index) => {
2017
- try {
2018
- const videoFilePath = path.resolve(
2019
- DEFAULT_OUTPUT_DIR,
2020
- videoPath
2021
- );
2022
- if (!fsExistsSync(videoFilePath))
2023
- return `<div class="attachment-item error">Video not found: ${sanitizeHTML(
2024
- videoPath
2025
- )}</div>`;
2026
- const videoBase64 =
2027
- readFileSync(videoFilePath).toString(
2028
- "base64"
2029
- );
2030
- const fileExtension = path
2031
- .extname(videoPath)
2032
- .slice(1)
2033
- .toLowerCase();
2034
- const mimeType =
2035
- {
2036
- mp4: "video/mp4",
2037
- webm: "video/webm",
2038
- ogg: "video/ogg",
2039
- mov: "video/quicktime",
2040
- avi: "video/x-msvideo",
2041
- }[fileExtension] || "video/mp4";
2042
- const videoDataUri = `data:${mimeType};base64,${videoBase64}`;
2043
- return `<div class="attachment-item video-item"><video controls preload="none" class="lazy-load-video"><source data-src="${videoDataUri}" type="${mimeType}"></video><div class="attachment-info"><div class="trace-actions"><a href="#" data-href="${videoDataUri}" class="lazy-load-attachment" target="_blank" download="video-${index}.${fileExtension}">Download</a></div></div></div>`;
2044
- } catch (e) {
2045
- return `<div class="attachment-item error">Failed to load video: ${sanitizeHTML(
2046
- videoPath
2047
- )}</div>`;
2048
- }
2049
- })
2050
- .join("")}</div></div>`;
2051
- })()}
2052
-
2053
- ${(() => {
2054
- if (!test.tracePath) return "";
2055
- try {
2056
- const traceFilePath = path.resolve(
2057
- DEFAULT_OUTPUT_DIR,
2058
- test.tracePath
2059
- );
2060
- if (!fsExistsSync(traceFilePath))
2061
- return `<div class="attachments-section"><h4>Trace File</h4><div class="attachment-item error">Trace file not found: ${sanitizeHTML(
2062
- test.tracePath
2063
- )}</div></div>`;
2064
- const traceBase64 =
2065
- readFileSync(traceFilePath).toString("base64");
2066
- const traceDataUri = `data:application/zip;base64,${traceBase64}`;
2067
- 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>`;
2068
- } catch (e) {
2069
- return `<div class="attachments-section"><h4>Trace File</h4><div class="attachment-item error">Failed to load trace file.</div></div>`;
2070
- }
2071
- })()}
2072
-
2073
- ${(() => {
2074
- if (
2075
- !test.attachments ||
2076
- test.attachments.length === 0
2077
- )
2078
- return "";
2079
-
2080
- return `<div class="attachments-section"><h4>Other Attachments</h4><div class="attachments-grid">${test.attachments
2081
- .map((attachment) => {
2082
- try {
2083
- const attachmentPath = path.resolve(
2084
- DEFAULT_OUTPUT_DIR,
2085
- attachment.path
2086
- );
2087
-
2088
- if (!fsExistsSync(attachmentPath)) {
2089
- console.warn(
2090
- `Attachment not found at: ${attachmentPath}`
2091
- );
2092
- return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
2093
- attachment.name
2094
- )}</div>`;
2095
- }
2408
+ ${
2409
+ test.errorMessage
2410
+ ? `<div class="test-error-summary">${formatPlaywrightError(
2411
+ test.errorMessage
2412
+ )}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)" style="margin-top: 8px; padding: 4px 8px; background: #f0f0f0; border: 2px solid #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; border-color: #8B0000; color: #8B0000;" onmouseover="this.style.background='#e0e0e0'" onmouseout="this.style.background='#f0f0f0'">Copy Error Prompt</button></div>`
2413
+ : ""
2414
+ }
2415
+ ${
2416
+ test.snippet
2417
+ ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
2418
+ test.snippet
2419
+ )}</code></pre></div>`
2420
+ : ""
2421
+ }
2422
+ <h4>Steps</h4>
2423
+ <div class="steps-list">${generateStepsHTML(test.steps)}</div>
2424
+
2425
+ ${(() => {
2426
+ if (!test.stdout || test.stdout.length === 0) return "";
2427
+ // FIXED: Now using 'testIndex' which is guaranteed to be defined
2428
+ const logId = `stdout-log-${test.id || testIndex}`;
2429
+ return `<div class="console-output-section"><h4>Console Output (stdout) <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy Console</button></h4><div class="log-wrapper"><pre id="${logId}" class="console-log stdout-log" style="background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
2430
+ test.stdout.map((line) => sanitizeHTML(line)).join("\n")
2431
+ )}</pre></div></div>`;
2432
+ })()}
2433
+
2434
+ ${(() => {
2435
+ if (!test.stderr || test.stderr.length === 0) return "";
2436
+ // FIXED: Using 'testIndex'
2437
+ const logId = `stderr-log-${test.id || testIndex}`;
2438
+ return `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log" style="background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
2439
+ test.stderr.map((line) => sanitizeHTML(line)).join("\n")
2440
+ )}</pre></div>`;
2441
+ })()}
2442
+
2443
+ ${(() => {
2444
+ if (!test.screenshots || test.screenshots.length === 0) return "";
2445
+ const screenshotsHTML = test.screenshots
2446
+ .map((screenshotPath, sIndex) => {
2447
+ try {
2448
+ const imagePath = path.resolve(
2449
+ DEFAULT_OUTPUT_DIR,
2450
+ screenshotPath
2451
+ );
2452
+ if (!fsExistsSync(imagePath))
2453
+ return `<div class="attachment-item error">Screenshot not found</div>`;
2454
+ const base64ImageData =
2455
+ readFileSync(imagePath).toString("base64");
2456
+ // LAZY LOAD: Using helper with unique ID
2457
+ return createLazyMedia(
2458
+ base64ImageData,
2459
+ "image/png",
2460
+ "image",
2461
+ sIndex + 1,
2462
+ `screenshot-${testIndex}-${sIndex}.png`
2463
+ );
2464
+ } catch (e) {
2465
+ return `<div class="attachment-item error">Error loading screenshot</div>`;
2466
+ }
2467
+ })
2468
+ .join("");
2469
+ return `<div class="attachments-section"><h4>Screenshots</h4><div class="attachments-grid">${screenshotsHTML}</div></div>`;
2470
+ })()}
2471
+
2472
+ ${(() => {
2473
+ if (!test.videoPath || test.videoPath.length === 0) return "";
2474
+ const videosHTML = test.videoPath
2475
+ .map((videoPath, vIndex) => {
2476
+ try {
2477
+ const videoFilePath = path.resolve(
2478
+ DEFAULT_OUTPUT_DIR,
2479
+ videoPath
2480
+ );
2481
+ if (!fsExistsSync(videoFilePath))
2482
+ return `<div class="attachment-item error">Video not found</div>`;
2483
+ const videoBase64 =
2484
+ readFileSync(videoFilePath).toString("base64");
2485
+ const ext = path.extname(videoPath).slice(1).toLowerCase();
2486
+ const mime =
2487
+ { mp4: "video/mp4", webm: "video/webm" }[ext] ||
2488
+ "video/mp4";
2489
+ // LAZY LOAD: Using helper with unique ID
2490
+ return createLazyMedia(
2491
+ videoBase64,
2492
+ mime,
2493
+ "video",
2494
+ vIndex + 1,
2495
+ `video-${testIndex}-${vIndex}.${ext}`
2496
+ );
2497
+ } catch (e) {
2498
+ return `<div class="attachment-item error">Error loading video</div>`;
2499
+ }
2500
+ })
2501
+ .join("");
2502
+ return `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${videosHTML}</div></div>`;
2503
+ })()}
2096
2504
 
2097
- const attachmentBase64 =
2098
- readFileSync(attachmentPath).toString(
2099
- "base64"
2100
- );
2101
- const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
2102
-
2103
- return `<div class="attachment-item generic-attachment">
2104
- <div class="attachment-icon">${getAttachmentIcon(
2105
- attachment.contentType
2106
- )}</div>
2107
- <div class="attachment-caption">
2108
- <span class="attachment-name" title="${sanitizeHTML(
2109
- attachment.name
2110
- )}">${sanitizeHTML(
2111
- attachment.name
2112
- )}</span>
2113
- <span class="attachment-type">${sanitizeHTML(
2114
- attachment.contentType
2115
- )}</span>
2116
- </div>
2117
- <div class="attachment-info">
2118
- <div class="trace-actions">
2119
- <a href="#" data-href="${attachmentDataUri}" class="view-full lazy-load-attachment" target="_blank">View</a>
2120
- <a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
2121
- attachment.name
2122
- )}">Download</a>
2123
- </div>
2124
- </div>
2125
- </div>`;
2126
- } catch (e) {
2127
- console.error(
2128
- `Failed to process attachment "${attachment.name}":`,
2129
- e
2130
- );
2131
- return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(
2132
- attachment.name
2133
- )}</div>`;
2134
- }
2135
- })
2136
- .join("")}</div></div>`;
2137
- })()}
2138
-
2139
- ${
2140
- test.codeSnippet
2141
- ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${sanitizeHTML(
2142
- test.codeSnippet
2143
- )}</code></pre></div>`
2144
- : ""
2145
- }
2146
- </div>
2147
- </div>`;
2505
+ ${
2506
+ test.tracePath
2507
+ ? `<div class="attachments-section"><h4>Trace Files</h4><div class="attachments-grid"><div class="attachment-item trace-item"><div class="trace-preview"><span class="trace-icon">📄</span><span class="trace-name">${sanitizeHTML(
2508
+ path.basename(test.tracePath)
2509
+ )}</span></div><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
2510
+ test.tracePath
2511
+ )}" target="_blank" download="${sanitizeHTML(
2512
+ path.basename(test.tracePath)
2513
+ )}" class="download-trace">Download Trace</a></div></div></div></div></div>`
2514
+ : ""
2515
+ }
2516
+ ${
2517
+ test.attachments && test.attachments.length > 0
2518
+ ? `<div class="attachments-section"><h4>Other Attachments</h4><div class="attachments-grid">${test.attachments
2519
+ .map(
2520
+ (attachment) =>
2521
+ `<div class="attachment-item generic-attachment"><div class="attachment-icon">${getAttachmentIcon(
2522
+ attachment.contentType
2523
+ )}</div><div class="attachment-caption"><span class="attachment-name" title="${sanitizeHTML(
2524
+ attachment.name
2525
+ )}">${sanitizeHTML(
2526
+ attachment.name
2527
+ )}</span><span class="attachment-type">${sanitizeHTML(
2528
+ attachment.contentType
2529
+ )}</span></div><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
2530
+ attachment.path
2531
+ )}" target="_blank" class="view-full">View</a><a href="${sanitizeHTML(
2532
+ attachment.path
2533
+ )}" target="_blank" download="${sanitizeHTML(
2534
+ attachment.name
2535
+ )}" class="download-trace">Download</a></div></div></div>`
2536
+ )
2537
+ .join("")}</div></div>`
2538
+ : ""
2539
+ }
2540
+ ${
2541
+ test.codeSnippet
2542
+ ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
2543
+ sanitizeHTML(test.codeSnippet)
2544
+ )}</code></pre></div>`
2545
+ : ""
2546
+ }
2547
+ </div>
2548
+ </div>`;
2148
2549
  })
2149
2550
  .join("");
2150
2551
  }
@@ -2154,10 +2555,10 @@ function generateHTML(reportData, trendData = null) {
2154
2555
  <head>
2155
2556
  <meta charset="UTF-8">
2156
2557
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2157
- <link rel="icon" type="image/png" href="https://i.postimg.cc/v817w4sg/logo.png">
2158
- <link rel="apple-touch-icon" href="https://i.postimg.cc/v817w4sg/logo.png">
2558
+ <link rel="icon" type="image/png" href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png">
2559
+ <link rel="apple-touch-icon" href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png">
2159
2560
  <script src="https://code.highcharts.com/highcharts.js" defer></script>
2160
- <title>Playwright Pulse Report (Static Report)</title>
2561
+ <title>Pulse Static Report</title>
2161
2562
 
2162
2563
  <style>
2163
2564
  :root {
@@ -2199,7 +2600,7 @@ body { font-family: var(--font-family); margin: 0; background-color: var(--backg
2199
2600
  .status-passed .value, .stat-passed svg { color: var(--success-color); }
2200
2601
  .status-failed .value, .stat-failed svg { color: var(--danger-color); }
2201
2602
  .status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
2202
- .dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: stretch; }
2603
+ .dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: start; }
2203
2604
  .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; }
2204
2605
  .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); }
2205
2606
  .trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { flex-grow: 1; min-height: 250px; }
@@ -2321,6 +2722,7 @@ aspect-ratio: 16 / 9;
2321
2722
  .status-badge-small.status-failed { background-color: var(--danger-color); }
2322
2723
  .status-badge-small.status-skipped { background-color: var(--warning-color); }
2323
2724
  .status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
2725
+ .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; }
2324
2726
  .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); }
2325
2727
  .no-data-chart {font-size: 0.95em; padding: 18px;}
2326
2728
  .ai-failure-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 22px; }
@@ -2369,9 +2771,14 @@ aspect-ratio: 16 / 9;
2369
2771
  .browser-indicator { background: var(--info-color); color: white; }
2370
2772
  #load-more-tests { font-size: 16px; padding: 4px; background-color: var(--light-gray-color); border-radius: 4px; color: var(--text-color); }
2371
2773
  .duration-indicator { background: var(--medium-gray-color); color: var(--text-color); }
2774
+ .ai-buttons-group { display: flex; gap: 10px; flex-wrap: wrap; }
2372
2775
  .compact-ai-btn { background: linear-gradient(135deg, #374151 0%, #1f2937 100%); color: white; border: none; padding: 12px 18px; border-radius: 6px; cursor: pointer; font-weight: 600; display: flex; align-items: center; gap: 8px; transition: all 0.3s ease; white-space: nowrap;}
2373
2776
  .compact-ai-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(55, 65, 81, 0.4); }
2374
2777
  .ai-text { font-size: 0.95em; }
2778
+ .copy-prompt-btn { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; border: none; padding: 12px 18px; border-radius: 6px; cursor: pointer; font-weight: 600; display: flex; align-items: center; gap: 8px; transition: all 0.3s ease; white-space: nowrap;}
2779
+ .copy-prompt-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(37, 99, 235, 0.4); }
2780
+ .copy-prompt-btn.copied { background: linear-gradient(135deg, #10b981 0%, #059669 100%); }
2781
+ .copy-prompt-text { font-size: 0.95em; }
2375
2782
  .failure-error-preview { padding: 0 20px 18px 20px; border-top: 1px solid var(--light-gray-color);}
2376
2783
  .error-snippet { background: rgba(248, 113, 113, 0.1); border: 1px solid rgba(248, 113, 113, 0.3); border-radius: 6px; padding: 12px; margin-bottom: 12px; font-family: monospace; font-size: 0.9em; color: var(--danger-color); line-height: 1.4;}
2377
2784
  .expand-error-btn { background: none; border: 1px solid var(--border-color); color: var(--text-color-secondary); padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 0.85em; display: flex; align-items: center; gap: 6px; transition: all 0.2s ease;}
@@ -2382,7 +2789,7 @@ aspect-ratio: 16 / 9;
2382
2789
  .full-error-content { background: rgba(248, 113, 113, 0.1); border: 1px solid rgba(248, 113, 113, 0.3); border-radius: 6px; padding: 15px; font-family: monospace; font-size: 0.9em; color: var(--danger-color); line-height: 1.4; max-height: 300px; overflow-y: auto;}
2383
2790
  @media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
2384
2791
  @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; } }
2385
- @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;} .ai-failure-cards-grid { grid-template-columns: 1fr; } .ai-analyzer-stats { flex-direction: column; gap: 15px; text-align: center; } .failure-header { flex-direction: column; align-items: stretch; gap: 15px; } .failure-main-info { text-align: center; } .failure-meta { justify-content: center; } .compact-ai-btn { justify-content: center; padding: 12px 20px; } }
2792
+ @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;} .ai-failure-cards-grid { grid-template-columns: 1fr; } .ai-analyzer-stats { flex-direction: column; gap: 15px; text-align: center; } .failure-header { flex-direction: column; align-items: stretch; gap: 15px; } .failure-main-info { text-align: center; } .failure-meta { justify-content: center; } .ai-buttons-group { flex-direction: column; width: 100%; } .compact-ai-btn, .copy-prompt-btn { justify-content: center; padding: 12px 20px; width: 100%; } }
2386
2793
  @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;} .stat-item .stat-number { font-size: 1.5em; } .failure-header { padding: 15px; } .failure-error-preview, .full-error-details { padding-left: 15px; padding-right: 15px; } }
2387
2794
  .trace-actions a { text-decoration: none; font-weight: 500; font-size: 0.9em; }
2388
2795
  .generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
@@ -2397,8 +2804,8 @@ aspect-ratio: 16 / 9;
2397
2804
  <div class="container">
2398
2805
  <header class="header">
2399
2806
  <div class="header-title">
2400
- <img id="report-logo" src="https://i.postimg.cc/v817w4sg/logo.png" alt="Report Logo">
2401
- <h1>Playwright Pulse Report</h1>
2807
+ <img id="report-logo" src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png" alt="Report Logo">
2808
+ <h1>Pulse Static Report</h1>
2402
2809
  </div>
2403
2810
  <div class="run-info"><strong>Run Date:</strong> ${formatDate(
2404
2811
  runSummary.timestamp
@@ -2449,7 +2856,10 @@ aspect-ratio: 16 / 9;
2449
2856
  : '<div class="no-data">Environment data not available.</div>'
2450
2857
  }
2451
2858
  </div>
2452
- ${generateSuitesWidget(suitesData)}
2859
+ <div style="display: flex; flex-direction: column; gap: 28px;">
2860
+ ${generateSuitesWidget(suitesData)}
2861
+ ${generateSeverityDistributionChart(results)}
2862
+ </div>
2453
2863
  </div>
2454
2864
  </div>
2455
2865
  <div id="test-runs" class="tab-content">
@@ -2501,6 +2911,16 @@ aspect-ratio: 16 / 9;
2501
2911
  }
2502
2912
  </div>
2503
2913
  </div>
2914
+ <div class="trend-charts-row">
2915
+ <div class="trend-chart">
2916
+ <h3 class="chart-title-header">Duration by Spec files</h3>
2917
+ ${generateSpecDurationChart(results)}
2918
+ </div>
2919
+ <div class="trend-chart">
2920
+ <h3 class="chart-title-header">Duration by Test Describe</h3>
2921
+ ${generateDescribeDurationChart(results)}
2922
+ </div>
2923
+ </div>
2504
2924
  <h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
2505
2925
  <div class="trend-charts-row">
2506
2926
  <div class="trend-chart">
@@ -2651,6 +3071,81 @@ aspect-ratio: 16 / 9;
2651
3071
  }
2652
3072
  }
2653
3073
 
3074
+ function copyAIPrompt(button) {
3075
+ try {
3076
+ const testJson = button.dataset.testJson;
3077
+ const test = JSON.parse(atob(testJson));
3078
+
3079
+ const testName = test.name || 'Unknown Test';
3080
+ const failureLogsAndErrors = [
3081
+ 'Error Message:',
3082
+ test.errorMessage || 'Not available.',
3083
+ '\\n\\n--- stdout ---',
3084
+ (test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
3085
+ '\\n\\n--- stderr ---',
3086
+ (test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
3087
+ ].join('\\n');
3088
+ const codeSnippet = test.snippet || '';
3089
+
3090
+ const aiPrompt = \`You are an expert Playwright test automation engineer specializing in debugging test failures.
3091
+
3092
+ INSTRUCTIONS:
3093
+ 1. Analyze the test failure carefully
3094
+ 2. Provide a brief root cause analysis
3095
+ 3. Provide EXACTLY 5 specific, actionable fixes
3096
+ 4. Each fix MUST include a code snippet (codeSnippet field)
3097
+ 5. Return ONLY valid JSON, no markdown or extra text
3098
+
3099
+ REQUIRED JSON FORMAT:
3100
+ {
3101
+ "rootCause": "Brief explanation of why the test failed",
3102
+ "suggestedFixes": [
3103
+ {
3104
+ "description": "Clear explanation of the fix",
3105
+ "codeSnippet": "await page.waitForSelector('.button', { timeout: 5000 });"
3106
+ }
3107
+ ],
3108
+ "affectedTests": ["test1", "test2"]
3109
+ }
3110
+
3111
+ IMPORTANT:
3112
+ - Always return valid JSON only
3113
+ - Always provide exactly 5 fixes in suggestedFixes array
3114
+ - Each fix must have both description and codeSnippet fields
3115
+ - Make code snippets practical and Playwright-specific
3116
+
3117
+ ---
3118
+
3119
+ Test Name: \${testName}
3120
+
3121
+ Failure Logs and Errors:
3122
+ \${failureLogsAndErrors}
3123
+
3124
+ Code Snippet:
3125
+ \${codeSnippet}\`;
3126
+
3127
+ navigator.clipboard.writeText(aiPrompt).then(() => {
3128
+ const originalText = button.querySelector('.copy-prompt-text').textContent;
3129
+ button.querySelector('.copy-prompt-text').textContent = 'Copied!';
3130
+ button.classList.add('copied');
3131
+
3132
+ const shortTestName = testName.split(' > ').pop() || testName;
3133
+ alert(\`AI prompt to generate a suggested fix for "\${shortTestName}" has been copied to your clipboard.\`);
3134
+
3135
+ setTimeout(() => {
3136
+ button.querySelector('.copy-prompt-text').textContent = originalText;
3137
+ button.classList.remove('copied');
3138
+ }, 2000);
3139
+ }).catch(err => {
3140
+ console.error('Failed to copy AI prompt:', err);
3141
+ alert('Failed to copy AI prompt to clipboard. Please try again.');
3142
+ });
3143
+ } catch (e) {
3144
+ console.error('Error processing test data for AI Prompt copy:', e);
3145
+ alert('Could not process test data. Please try again.');
3146
+ }
3147
+ }
3148
+
2654
3149
  function closeAiModal() {
2655
3150
  const modal = document.getElementById('ai-fix-modal');
2656
3151
  if(modal) modal.style.display = 'none';
@@ -2671,6 +3166,41 @@ aspect-ratio: 16 / 9;
2671
3166
  button.classList.remove('expanded');
2672
3167
  }
2673
3168
  }
3169
+
3170
+ // --- LAZY MEDIA HANDLERS ---
3171
+ window.loadMedia = function(id, type) {
3172
+ const storage = document.getElementById('data-' + id);
3173
+ const element = document.getElementById(id);
3174
+ const placeholder = element.previousElementSibling;
3175
+
3176
+ if (storage && element) {
3177
+ const data = storage.textContent;
3178
+ element.src = data;
3179
+ element.style.display = 'block';
3180
+ if (placeholder) placeholder.style.display = 'none';
3181
+
3182
+ if (type === 'video') {
3183
+ element.play().catch(e => console.log('Autoplay prevented', e));
3184
+ }
3185
+ }
3186
+ };
3187
+
3188
+ window.downloadMedia = function(id, filename) {
3189
+ const storage = document.getElementById('data-' + id);
3190
+ if (storage) {
3191
+ const data = storage.textContent;
3192
+ const link = document.createElement('a');
3193
+ link.href = data;
3194
+ link.download = filename;
3195
+ document.body.appendChild(link);
3196
+ link.click();
3197
+ document.body.removeChild(link);
3198
+ } else {
3199
+ alert("Media data not found.");
3200
+ }
3201
+ };
3202
+
3203
+ // Ensure formatDuration is globally available... (existing code follows)
2674
3204
 
2675
3205
  function initializeReportInteractivity() {
2676
3206
  const tabButtons = document.querySelectorAll('.tab-button');
@@ -2775,10 +3305,25 @@ aspect-ratio: 16 / 9;
2775
3305
  filterTestHistoryCards();
2776
3306
  });
2777
3307
  // --- Expand/Collapse and Toggle Details Logic ---
3308
+ // --- Expand/Collapse and Toggle Details Logic ---
2778
3309
  function toggleElementDetails(headerElement, contentSelector) {
2779
3310
  let contentElement;
2780
3311
  if (headerElement.classList.contains('test-case-header')) {
3312
+ // Find the content sibling
2781
3313
  contentElement = headerElement.parentElement.querySelector('.test-case-content');
3314
+
3315
+ // --- ALLURE-STYLE LAZY RENDERING ---
3316
+ // If content is empty/not loaded, load it from the template script
3317
+ if (contentElement && !contentElement.getAttribute('data-loaded')) {
3318
+ const testCaseId = contentElement.id.replace('details-', '');
3319
+ const template = document.getElementById('tmpl-' + testCaseId);
3320
+ if (template) {
3321
+ contentElement.innerHTML = template.textContent; // Hydrate HTML
3322
+ contentElement.setAttribute('data-loaded', 'true');
3323
+ }
3324
+ }
3325
+ // -----------------------------------
3326
+
2782
3327
  } else if (headerElement.classList.contains('step-header')) {
2783
3328
  contentElement = headerElement.nextElementSibling;
2784
3329
  if (!contentElement || !contentElement.matches(contentSelector || '.step-details')) {
@@ -2826,6 +3371,18 @@ aspect-ratio: 16 / 9;
2826
3371
  }
2827
3372
  return;
2828
3373
  }
3374
+ const annotationLink = e.target.closest('a.annotation-link');
3375
+ if (annotationLink) {
3376
+ e.preventDefault();
3377
+ const annotationId = annotationLink.dataset.annotation;
3378
+ if (annotationId) {
3379
+ const jiraUrl = prompt('Enter your JIRA/Ticket system base URL (e.g., https://your-company.atlassian.net/browse/):', 'https://your-company.atlassian.net/browse/');
3380
+ if (jiraUrl) {
3381
+ window.open(jiraUrl + annotationId, '_blank');
3382
+ }
3383
+ }
3384
+ return;
3385
+ }
2829
3386
  const img = e.target.closest('img.lazy-load-image');
2830
3387
  if (img && img.dataset && img.dataset.src) {
2831
3388
  if (e.preventDefault) e.preventDefault();
@@ -2994,10 +3551,10 @@ aspect-ratio: 16 / 9;
2994
3551
  </html>
2995
3552
  `;
2996
3553
  }
2997
- async function runScript(scriptPath) {
3554
+ async function runScript(scriptPath, args = []) {
2998
3555
  return new Promise((resolve, reject) => {
2999
3556
  console.log(chalk.blue(`Executing script: ${scriptPath}...`));
3000
- const process = fork(scriptPath, [], {
3557
+ const process = fork(scriptPath, args, {
3001
3558
  stdio: "inherit",
3002
3559
  });
3003
3560
 
@@ -3027,13 +3584,22 @@ async function main() {
3027
3584
  const __filename = fileURLToPath(import.meta.url);
3028
3585
  const __dirname = path.dirname(__filename);
3029
3586
 
3587
+ const args = process.argv.slice(2);
3588
+ let customOutputDir = null;
3589
+ for (let i = 0; i < args.length; i++) {
3590
+ if (args[i] === "--outputDir" || args[i] === "-o") {
3591
+ customOutputDir = args[i + 1];
3592
+ break;
3593
+ }
3594
+ }
3595
+
3030
3596
  // Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
3031
3597
  const archiveRunScriptPath = path.resolve(
3032
3598
  __dirname,
3033
3599
  "generate-trend.mjs" // Keeping the filename as per your request
3034
3600
  );
3035
3601
 
3036
- const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3602
+ const outputDir = await getOutputDir(customOutputDir);
3037
3603
  const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
3038
3604
  const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
3039
3605
 
@@ -3043,10 +3609,21 @@ async function main() {
3043
3609
 
3044
3610
  console.log(chalk.blue(`Starting static HTML report generation...`));
3045
3611
  console.log(chalk.blue(`Output directory set to: ${outputDir}`));
3612
+ if (customOutputDir) {
3613
+ console.log(chalk.gray(` (from CLI argument)`));
3614
+ } else {
3615
+ const { exists } = await import("./config-reader.mjs").then((m) => ({
3616
+ exists: true,
3617
+ }));
3618
+ console.log(
3619
+ chalk.gray(` (auto-detected from playwright.config or using default)`)
3620
+ );
3621
+ }
3046
3622
 
3047
3623
  // Step 1: Ensure current run data is archived to the history folder
3048
3624
  try {
3049
- await runScript(archiveRunScriptPath); // This script now handles JSON history
3625
+ const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
3626
+ await runScript(archiveRunScriptPath, archiveArgs);
3050
3627
  console.log(
3051
3628
  chalk.green("Current run data archiving to history completed.")
3052
3629
  );
@@ -3213,4 +3790,4 @@ main().catch((err) => {
3213
3790
  );
3214
3791
  console.error(err.stack);
3215
3792
  process.exit(1);
3216
- });
3793
+ });