@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.
- package/README.md +97 -0
- 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 -9
- package/dist/types/index.d.ts +12 -0
- package/package.json +8 -8
- package/scripts/config-reader.mjs +180 -0
- package/scripts/generate-email-report.mjs +81 -11
- package/scripts/generate-report.mjs +996 -306
- package/scripts/generate-static-report.mjs +895 -318
- package/scripts/generate-trend.mjs +11 -1
- package/scripts/merge-pulse-report.js +46 -14
- package/scripts/sendReport.mjs +109 -34
|
@@ -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"
|
|
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
|
|
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
|
-
<
|
|
1752
|
-
<
|
|
1753
|
-
|
|
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
|
-
|
|
1811
|
-
|
|
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
|
-
|
|
2244
|
+
|
|
2245
|
+
return data
|
|
1814
2246
|
.map((test, i) => {
|
|
1815
|
-
|
|
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
|
|
1832
|
-
|
|
1833
|
-
|
|
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
|
|
2294
|
+
)}${hookIndicator}</span>
|
|
2295
|
+
<span class="step-duration">${formatDuration(
|
|
1836
2296
|
step.duration
|
|
1837
|
-
)}</span
|
|
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
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
2328
|
+
}
|
|
2329
|
+
</div>
|
|
2330
|
+
</div>`;
|
|
1897
2331
|
})
|
|
1898
2332
|
.join("");
|
|
1899
2333
|
};
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
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()}"
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
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
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
.
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
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
|
-
.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
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
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://
|
|
2158
|
-
<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">
|
|
2159
2560
|
<script src="https://code.highcharts.com/highcharts.js" defer></script>
|
|
2160
|
-
<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:
|
|
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://
|
|
2401
|
-
<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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
+
});
|