@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.
- package/README.md +64 -3
- package/dist/pulse.d.ts +12 -0
- package/dist/pulse.js +24 -0
- package/dist/reporter/index.d.ts +2 -0
- package/dist/reporter/index.js +5 -1
- package/dist/reporter/playwright-pulse-reporter.d.ts +1 -0
- package/dist/reporter/playwright-pulse-reporter.js +27 -10
- package/dist/types/index.d.ts +3 -0
- package/package.json +7 -7
- package/scripts/config-reader.mjs +100 -52
- package/scripts/generate-email-report.mjs +60 -10
- package/scripts/generate-report.mjs +491 -52
- package/scripts/generate-static-report.mjs +768 -371
- package/scripts/sendReport.mjs +74 -14
|
@@ -394,6 +394,7 @@ function generateTestTrendsChart(trendData) {
|
|
|
394
394
|
</script>
|
|
395
395
|
`;
|
|
396
396
|
}
|
|
397
|
+
const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
|
|
397
398
|
/**
|
|
398
399
|
* Generates HTML and JavaScript for a Highcharts area chart to display test duration trends.
|
|
399
400
|
* @param {object} trendData Data for duration trends.
|
|
@@ -413,8 +414,6 @@ function generateDurationTrendChart(trendData) {
|
|
|
413
414
|
)}`;
|
|
414
415
|
const runs = trendData.overall;
|
|
415
416
|
|
|
416
|
-
const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
|
|
417
|
-
|
|
418
417
|
const chartDataString = JSON.stringify(runs.map((run) => run.duration));
|
|
419
418
|
const categoriesString = JSON.stringify(runs.map((run, i) => `Run ${i + 1}`));
|
|
420
419
|
const runsForTooltip = runs.map((r) => ({
|
|
@@ -777,6 +776,9 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
777
776
|
const cardHeight = Math.floor(dashboardHeight * 0.44);
|
|
778
777
|
const cardContentPadding = 16; // px
|
|
779
778
|
|
|
779
|
+
// Logic for Run Context
|
|
780
|
+
const runContext = process.env.CI ? "CI" : "Local Test";
|
|
781
|
+
|
|
780
782
|
return `
|
|
781
783
|
<div class="environment-dashboard-wrapper" id="${dashboardId}">
|
|
782
784
|
<style>
|
|
@@ -820,6 +822,20 @@ gap: 20px;
|
|
|
820
822
|
font-size: 14px;
|
|
821
823
|
}
|
|
822
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
|
+
|
|
823
839
|
.env-dashboard-header {
|
|
824
840
|
grid-column: 1 / -1;
|
|
825
841
|
display: flex;
|
|
@@ -828,6 +844,8 @@ align-items: center;
|
|
|
828
844
|
border-bottom: 1px solid var(--border-color);
|
|
829
845
|
padding-bottom: 16px;
|
|
830
846
|
margin-bottom: 8px;
|
|
847
|
+
flex-wrap: wrap; /* Allow wrapping header items */
|
|
848
|
+
gap: 10px;
|
|
831
849
|
}
|
|
832
850
|
|
|
833
851
|
.env-dashboard-title {
|
|
@@ -1087,7 +1105,7 @@ border-color: var(--border-color);
|
|
|
1087
1105
|
</div>
|
|
1088
1106
|
<div class="env-detail-row">
|
|
1089
1107
|
<span class="env-detail-label">Run Context</span>
|
|
1090
|
-
<span class="env-detail-value"
|
|
1108
|
+
<span class="env-detail-value">${runContext}</span>
|
|
1091
1109
|
</div>
|
|
1092
1110
|
</div>
|
|
1093
1111
|
</div>
|
|
@@ -1781,6 +1799,409 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1781
1799
|
</div>
|
|
1782
1800
|
`;
|
|
1783
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
|
+
}
|
|
1784
2205
|
/**
|
|
1785
2206
|
* Generates the HTML report.
|
|
1786
2207
|
* @param {object} reportData - The data for the report.
|
|
@@ -1813,16 +2234,46 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1813
2234
|
* Generates the HTML for the test cases.
|
|
1814
2235
|
* @returns {string} The HTML for the test cases.
|
|
1815
2236
|
*/
|
|
1816
|
-
|
|
1817
|
-
|
|
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)
|
|
1818
2243
|
return '<div class="no-tests">No test results found in this run.</div>';
|
|
1819
|
-
|
|
2244
|
+
|
|
2245
|
+
return data
|
|
1820
2246
|
.map((test, i) => {
|
|
1821
|
-
|
|
2247
|
+
// Calculate the global index (essential for unique IDs across chunks)
|
|
2248
|
+
const testIndex = offset + i;
|
|
2249
|
+
|
|
1822
2250
|
const browser = test.browser || "unknown";
|
|
1823
2251
|
const testFileParts = test.name.split(" > ");
|
|
1824
2252
|
const testTitle =
|
|
1825
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 ---
|
|
1826
2277
|
const generateStepsHTML = (steps, depth = 0) => {
|
|
1827
2278
|
if (!steps || steps.length === 0)
|
|
1828
2279
|
return "<div class='no-steps'>No steps recorded for this test.</div>";
|
|
@@ -1834,385 +2285,267 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1834
2285
|
? `step-hook step-hook-${step.hookType}`
|
|
1835
2286
|
: "";
|
|
1836
2287
|
const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
|
|
1837
|
-
return
|
|
1838
|
-
|
|
1839
|
-
|
|
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(
|
|
1840
2293
|
step.title
|
|
1841
|
-
)}${hookIndicator}</span
|
|
2294
|
+
)}${hookIndicator}</span>
|
|
2295
|
+
<span class="step-duration">${formatDuration(
|
|
1842
2296
|
step.duration
|
|
1843
|
-
)}</span
|
|
2297
|
+
)}</span>
|
|
2298
|
+
</div>
|
|
2299
|
+
<div class="step-details" style="display: none;">
|
|
2300
|
+
${
|
|
1844
2301
|
step.codeLocation
|
|
1845
2302
|
? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
|
|
1846
2303
|
step.codeLocation
|
|
1847
2304
|
)}</div>`
|
|
1848
2305
|
: ""
|
|
1849
|
-
}
|
|
2306
|
+
}
|
|
2307
|
+
${
|
|
1850
2308
|
step.errorMessage
|
|
1851
|
-
? `<div class="test-error-summary"
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
: ""
|
|
1859
|
-
}${(() => {
|
|
1860
|
-
if (!step.attachments || step.attachments.length === 0)
|
|
1861
|
-
return "";
|
|
1862
|
-
return `<div class="attachments-section"><h4>Step Attachments</h4><div class="attachments-grid">${step.attachments
|
|
1863
|
-
.map((attachment) => {
|
|
1864
|
-
try {
|
|
1865
|
-
const attachmentPath = path.resolve(
|
|
1866
|
-
DEFAULT_OUTPUT_DIR,
|
|
1867
|
-
attachment.path
|
|
1868
|
-
);
|
|
1869
|
-
if (!fsExistsSync(attachmentPath)) {
|
|
1870
|
-
return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
|
|
1871
|
-
attachment.name
|
|
1872
|
-
)}</div>`;
|
|
2309
|
+
? `<div class="test-error-summary">
|
|
2310
|
+
${
|
|
2311
|
+
step.stackTrace
|
|
2312
|
+
? `<div class="stack-trace">${formatPlaywrightError(
|
|
2313
|
+
step.stackTrace
|
|
2314
|
+
)}</div>`
|
|
2315
|
+
: ""
|
|
1873
2316
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
attachment.contentType
|
|
1880
|
-
)}</div>
|
|
1881
|
-
<div class="attachment-caption">
|
|
1882
|
-
<span class="attachment-name" title="${sanitizeHTML(
|
|
1883
|
-
attachment.name
|
|
1884
|
-
)}">${sanitizeHTML(attachment.name)}</span>
|
|
1885
|
-
<span class="attachment-type">${sanitizeHTML(
|
|
1886
|
-
attachment.contentType
|
|
1887
|
-
)}</span>
|
|
1888
|
-
</div>
|
|
1889
|
-
<div class="attachment-info">
|
|
1890
|
-
<div class="trace-actions">
|
|
1891
|
-
<a href="#" data-href="${attachmentDataUri}" class="view-full lazy-load-attachment" target="_blank">View</a>
|
|
1892
|
-
<a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
|
|
1893
|
-
attachment.name
|
|
1894
|
-
)}">Download</a>
|
|
1895
|
-
</div>
|
|
1896
|
-
</div>
|
|
1897
|
-
</div>`;
|
|
1898
|
-
} catch (e) {
|
|
1899
|
-
return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(
|
|
1900
|
-
attachment.name
|
|
1901
|
-
)}</div>`;
|
|
1902
|
-
}
|
|
1903
|
-
})
|
|
1904
|
-
.join("")}</div></div>`;
|
|
1905
|
-
})()}${
|
|
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
|
+
${
|
|
1906
2322
|
hasNestedSteps
|
|
1907
2323
|
? `<div class="nested-steps">${generateStepsHTML(
|
|
1908
2324
|
step.steps,
|
|
1909
2325
|
depth + 1
|
|
1910
2326
|
)}</div>`
|
|
1911
2327
|
: ""
|
|
1912
|
-
}
|
|
2328
|
+
}
|
|
2329
|
+
</div>
|
|
2330
|
+
</div>`;
|
|
1913
2331
|
})
|
|
1914
2332
|
.join("");
|
|
1915
2333
|
};
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2334
|
+
|
|
2335
|
+
return `
|
|
2336
|
+
<div class="test-case" data-status="${
|
|
2337
|
+
test.status
|
|
2338
|
+
}" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
|
|
1921
2339
|
.join(",")
|
|
1922
|
-
.toLowerCase()}"
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
test.status
|
|
1927
|
-
)}">${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(
|
|
1928
2344
|
test.status
|
|
1929
|
-
).toUpperCase()}</span
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
.
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
</div>`;
|
|
1991
|
-
})
|
|
1992
|
-
.join("")}
|
|
1993
|
-
</div>`
|
|
1994
|
-
: ""
|
|
1995
|
-
}
|
|
1996
|
-
<p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
|
|
1997
|
-
test.workerId
|
|
1998
|
-
)} [<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(
|
|
1999
2406
|
test.totalWorkers
|
|
2000
2407
|
)}]</p>
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
);
|
|
2098
|
-
const fileExtension = path
|
|
2099
|
-
.extname(videoPath)
|
|
2100
|
-
.slice(1)
|
|
2101
|
-
.toLowerCase();
|
|
2102
|
-
const mimeType =
|
|
2103
|
-
{
|
|
2104
|
-
mp4: "video/mp4",
|
|
2105
|
-
webm: "video/webm",
|
|
2106
|
-
ogg: "video/ogg",
|
|
2107
|
-
mov: "video/quicktime",
|
|
2108
|
-
avi: "video/x-msvideo",
|
|
2109
|
-
}[fileExtension] || "video/mp4";
|
|
2110
|
-
const videoDataUri = `data:${mimeType};base64,${videoBase64}`;
|
|
2111
|
-
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>`;
|
|
2112
|
-
} catch (e) {
|
|
2113
|
-
return `<div class="attachment-item error">Failed to load video: ${sanitizeHTML(
|
|
2114
|
-
videoPath
|
|
2115
|
-
)}</div>`;
|
|
2116
|
-
}
|
|
2117
|
-
})
|
|
2118
|
-
.join("")}</div></div>`;
|
|
2119
|
-
})()}
|
|
2120
|
-
|
|
2121
|
-
${(() => {
|
|
2122
|
-
if (!test.tracePath) return "";
|
|
2123
|
-
try {
|
|
2124
|
-
const traceFilePath = path.resolve(
|
|
2125
|
-
DEFAULT_OUTPUT_DIR,
|
|
2126
|
-
test.tracePath
|
|
2127
|
-
);
|
|
2128
|
-
if (!fsExistsSync(traceFilePath))
|
|
2129
|
-
return `<div class="attachments-section"><h4>Trace File</h4><div class="attachment-item error">Trace file not found: ${sanitizeHTML(
|
|
2130
|
-
test.tracePath
|
|
2131
|
-
)}</div></div>`;
|
|
2132
|
-
const traceBase64 =
|
|
2133
|
-
readFileSync(traceFilePath).toString("base64");
|
|
2134
|
-
const traceDataUri = `data:application/zip;base64,${traceBase64}`;
|
|
2135
|
-
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>`;
|
|
2136
|
-
} catch (e) {
|
|
2137
|
-
return `<div class="attachments-section"><h4>Trace File</h4><div class="attachment-item error">Failed to load trace file.</div></div>`;
|
|
2138
|
-
}
|
|
2139
|
-
})()}
|
|
2140
|
-
|
|
2141
|
-
${(() => {
|
|
2142
|
-
if (
|
|
2143
|
-
!test.attachments ||
|
|
2144
|
-
test.attachments.length === 0
|
|
2145
|
-
)
|
|
2146
|
-
return "";
|
|
2147
|
-
|
|
2148
|
-
return `<div class="attachments-section"><h4>Other Attachments</h4><div class="attachments-grid">${test.attachments
|
|
2149
|
-
.map((attachment) => {
|
|
2150
|
-
try {
|
|
2151
|
-
const attachmentPath = path.resolve(
|
|
2152
|
-
DEFAULT_OUTPUT_DIR,
|
|
2153
|
-
attachment.path
|
|
2154
|
-
);
|
|
2155
|
-
|
|
2156
|
-
if (!fsExistsSync(attachmentPath)) {
|
|
2157
|
-
console.warn(
|
|
2158
|
-
`Attachment not found at: ${attachmentPath}`
|
|
2159
|
-
);
|
|
2160
|
-
return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
|
|
2161
|
-
attachment.name
|
|
2162
|
-
)}</div>`;
|
|
2163
|
-
}
|
|
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
|
+
})()}
|
|
2164
2504
|
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${sanitizeHTML(
|
|
2210
|
-
test.codeSnippet
|
|
2211
|
-
)}</code></pre></div>`
|
|
2212
|
-
: ""
|
|
2213
|
-
}
|
|
2214
|
-
</div>
|
|
2215
|
-
</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>`;
|
|
2216
2549
|
})
|
|
2217
2550
|
.join("");
|
|
2218
2551
|
}
|
|
@@ -2222,10 +2555,10 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2222
2555
|
<head>
|
|
2223
2556
|
<meta charset="UTF-8">
|
|
2224
2557
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2225
|
-
<link rel="icon" type="image/png" href="https://
|
|
2226
|
-
<link rel="apple-touch-icon" href="https://
|
|
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">
|
|
2227
2560
|
<script src="https://code.highcharts.com/highcharts.js" defer></script>
|
|
2228
|
-
<title>
|
|
2561
|
+
<title>Pulse Static Report</title>
|
|
2229
2562
|
|
|
2230
2563
|
<style>
|
|
2231
2564
|
:root {
|
|
@@ -2267,7 +2600,7 @@ body { font-family: var(--font-family); margin: 0; background-color: var(--backg
|
|
|
2267
2600
|
.status-passed .value, .stat-passed svg { color: var(--success-color); }
|
|
2268
2601
|
.status-failed .value, .stat-failed svg { color: var(--danger-color); }
|
|
2269
2602
|
.status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
|
|
2270
|
-
.dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items:
|
|
2603
|
+
.dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: start; }
|
|
2271
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; }
|
|
2272
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); }
|
|
2273
2606
|
.trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { flex-grow: 1; min-height: 250px; }
|
|
@@ -2389,6 +2722,7 @@ aspect-ratio: 16 / 9;
|
|
|
2389
2722
|
.status-badge-small.status-failed { background-color: var(--danger-color); }
|
|
2390
2723
|
.status-badge-small.status-skipped { background-color: var(--warning-color); }
|
|
2391
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; }
|
|
2392
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); }
|
|
2393
2727
|
.no-data-chart {font-size: 0.95em; padding: 18px;}
|
|
2394
2728
|
.ai-failure-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 22px; }
|
|
@@ -2470,8 +2804,8 @@ aspect-ratio: 16 / 9;
|
|
|
2470
2804
|
<div class="container">
|
|
2471
2805
|
<header class="header">
|
|
2472
2806
|
<div class="header-title">
|
|
2473
|
-
<img id="report-logo" src="https://
|
|
2474
|
-
<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>
|
|
2475
2809
|
</div>
|
|
2476
2810
|
<div class="run-info"><strong>Run Date:</strong> ${formatDate(
|
|
2477
2811
|
runSummary.timestamp
|
|
@@ -2522,7 +2856,10 @@ aspect-ratio: 16 / 9;
|
|
|
2522
2856
|
: '<div class="no-data">Environment data not available.</div>'
|
|
2523
2857
|
}
|
|
2524
2858
|
</div>
|
|
2525
|
-
|
|
2859
|
+
<div style="display: flex; flex-direction: column; gap: 28px;">
|
|
2860
|
+
${generateSuitesWidget(suitesData)}
|
|
2861
|
+
${generateSeverityDistributionChart(results)}
|
|
2862
|
+
</div>
|
|
2526
2863
|
</div>
|
|
2527
2864
|
</div>
|
|
2528
2865
|
<div id="test-runs" class="tab-content">
|
|
@@ -2574,6 +2911,16 @@ aspect-ratio: 16 / 9;
|
|
|
2574
2911
|
}
|
|
2575
2912
|
</div>
|
|
2576
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>
|
|
2577
2924
|
<h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
|
|
2578
2925
|
<div class="trend-charts-row">
|
|
2579
2926
|
<div class="trend-chart">
|
|
@@ -2819,6 +3166,41 @@ Code Snippet:
|
|
|
2819
3166
|
button.classList.remove('expanded');
|
|
2820
3167
|
}
|
|
2821
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)
|
|
2822
3204
|
|
|
2823
3205
|
function initializeReportInteractivity() {
|
|
2824
3206
|
const tabButtons = document.querySelectorAll('.tab-button');
|
|
@@ -2923,10 +3305,25 @@ Code Snippet:
|
|
|
2923
3305
|
filterTestHistoryCards();
|
|
2924
3306
|
});
|
|
2925
3307
|
// --- Expand/Collapse and Toggle Details Logic ---
|
|
3308
|
+
// --- Expand/Collapse and Toggle Details Logic ---
|
|
2926
3309
|
function toggleElementDetails(headerElement, contentSelector) {
|
|
2927
3310
|
let contentElement;
|
|
2928
3311
|
if (headerElement.classList.contains('test-case-header')) {
|
|
3312
|
+
// Find the content sibling
|
|
2929
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
|
+
|
|
2930
3327
|
} else if (headerElement.classList.contains('step-header')) {
|
|
2931
3328
|
contentElement = headerElement.nextElementSibling;
|
|
2932
3329
|
if (!contentElement || !contentElement.matches(contentSelector || '.step-details')) {
|
|
@@ -3393,4 +3790,4 @@ main().catch((err) => {
|
|
|
3393
3790
|
);
|
|
3394
3791
|
console.error(err.stack);
|
|
3395
3792
|
process.exit(1);
|
|
3396
|
-
});
|
|
3793
|
+
});
|