@arghajit/playwright-pulse-report 0.2.4 → 0.2.6
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 +33 -34
- package/dist/reporter/playwright-pulse-reporter.js +5 -4
- package/dist/reporter/tsconfig.reporter.tsbuildinfo +1 -0
- package/dist/types/index.d.ts +2 -1
- package/package.json +1 -1
- package/scripts/generate-email-report.mjs +6 -6
- package/scripts/generate-report.mjs +1017 -324
- package/scripts/generate-static-report.mjs +1390 -497
|
@@ -615,9 +615,8 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
615
615
|
chart: {
|
|
616
616
|
type: 'pie',
|
|
617
617
|
width: ${chartWidth},
|
|
618
|
-
height: ${
|
|
619
|
-
|
|
620
|
-
}, // Adjusted height to make space for legend if chartHeight is for the whole wrapper
|
|
618
|
+
height: ${chartHeight - 40
|
|
619
|
+
}, // Adjusted height to make space for legend if chartHeight is for the whole wrapper
|
|
621
620
|
backgroundColor: 'transparent',
|
|
622
621
|
plotShadow: false,
|
|
623
622
|
spacingBottom: 40 // Ensure space for legend
|
|
@@ -669,9 +668,8 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
669
668
|
return `
|
|
670
669
|
<div class="pie-chart-wrapper" style="align-items: center; max-height: 450px">
|
|
671
670
|
<div style="display: flex; align-items: start; width: 100%;"><h3>Test Distribution</h3></div>
|
|
672
|
-
<div id="${chartId}" style="width: ${chartWidth}px; height: ${
|
|
673
|
-
|
|
674
|
-
}px;"></div>
|
|
671
|
+
<div id="${chartId}" style="width: ${chartWidth}px; height: ${chartHeight - 40
|
|
672
|
+
}px;"></div>
|
|
675
673
|
<script>
|
|
676
674
|
document.addEventListener('DOMContentLoaded', function() {
|
|
677
675
|
if (typeof Highcharts !== 'undefined') {
|
|
@@ -899,15 +897,14 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
899
897
|
<span class="env-detail-value">
|
|
900
898
|
<div class="env-cpu-cores">
|
|
901
899
|
${Array.from(
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
).join("")}
|
|
900
|
+
{ length: Math.max(0, environment.cpu.cores || 0) },
|
|
901
|
+
(_, i) =>
|
|
902
|
+
`<div class="env-core-indicator ${i >=
|
|
903
|
+
(environment.cpu.cores >= 8 ? 8 : environment.cpu.cores)
|
|
904
|
+
? "inactive"
|
|
905
|
+
: ""
|
|
906
|
+
}" title="Core ${i + 1}"></div>`
|
|
907
|
+
).join("")}
|
|
911
908
|
<span>${environment.cpu.cores || "N/A"} cores</span>
|
|
912
909
|
</div>
|
|
913
910
|
</span>
|
|
@@ -927,23 +924,20 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
927
924
|
<div class="env-card-content">
|
|
928
925
|
<div class="env-detail-row">
|
|
929
926
|
<span class="env-detail-label">OS Type</span>
|
|
930
|
-
<span class="env-detail-value">${
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
}</span>
|
|
927
|
+
<span class="env-detail-value">${environment.os.split(" ")[0] === "darwin"
|
|
928
|
+
? "darwin (macOS)"
|
|
929
|
+
: environment.os.split(" ")[0] || "Unknown"
|
|
930
|
+
}</span>
|
|
935
931
|
</div>
|
|
936
932
|
<div class="env-detail-row">
|
|
937
933
|
<span class="env-detail-label">OS Version</span>
|
|
938
|
-
<span class="env-detail-value">${
|
|
939
|
-
|
|
940
|
-
}</span>
|
|
934
|
+
<span class="env-detail-value">${environment.os.split(" ")[1] || "N/A"
|
|
935
|
+
}</span>
|
|
941
936
|
</div>
|
|
942
937
|
<div class="env-detail-row">
|
|
943
938
|
<span class="env-detail-label">Hostname</span>
|
|
944
|
-
<span class="env-detail-value" title="${environment.host}">${
|
|
945
|
-
|
|
946
|
-
}</span>
|
|
939
|
+
<span class="env-detail-value" title="${environment.host}">${environment.host
|
|
940
|
+
}</span>
|
|
947
941
|
</div>
|
|
948
942
|
</div>
|
|
949
943
|
</div>
|
|
@@ -964,11 +958,10 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
964
958
|
</div>
|
|
965
959
|
<div class="env-detail-row">
|
|
966
960
|
<span class="env-detail-label">Working Dir</span>
|
|
967
|
-
<span class="env-detail-value" title="${environment.cwd}">${
|
|
968
|
-
environment.cwd.length > 25
|
|
961
|
+
<span class="env-detail-value" title="${environment.cwd}">${environment.cwd.length > 25
|
|
969
962
|
? "..." + environment.cwd.slice(-22)
|
|
970
963
|
: environment.cwd
|
|
971
|
-
|
|
964
|
+
}</span>
|
|
972
965
|
</div>
|
|
973
966
|
</div>
|
|
974
967
|
</div>
|
|
@@ -982,33 +975,30 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
982
975
|
<div class="env-detail-row">
|
|
983
976
|
<span class="env-detail-label">Platform Arch</span>
|
|
984
977
|
<span class="env-detail-value">
|
|
985
|
-
<span class="env-chip ${
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
: "x86/Other"
|
|
999
|
-
}
|
|
978
|
+
<span class="env-chip ${environment.os.includes("darwin") &&
|
|
979
|
+
environment.cpu.model.toLowerCase().includes("apple")
|
|
980
|
+
? "env-chip-success"
|
|
981
|
+
: "env-chip-warning"
|
|
982
|
+
}">
|
|
983
|
+
${environment.os.includes("darwin") &&
|
|
984
|
+
environment.cpu.model.toLowerCase().includes("apple")
|
|
985
|
+
? "Apple Silicon"
|
|
986
|
+
: environment.cpu.model.toLowerCase().includes("arm") ||
|
|
987
|
+
environment.cpu.model.toLowerCase().includes("aarch64")
|
|
988
|
+
? "ARM-based"
|
|
989
|
+
: "x86/Other"
|
|
990
|
+
}
|
|
1000
991
|
</span>
|
|
1001
992
|
</span>
|
|
1002
993
|
</div>
|
|
1003
994
|
<div class="env-detail-row">
|
|
1004
995
|
<span class="env-detail-label">Memory per Core</span>
|
|
1005
|
-
<span class="env-detail-value">${
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}</span>
|
|
996
|
+
<span class="env-detail-value">${environment.cpu.cores > 0
|
|
997
|
+
? (
|
|
998
|
+
parseFloat(environment.memory) / environment.cpu.cores
|
|
999
|
+
).toFixed(2) + " GB"
|
|
1000
|
+
: "N/A"
|
|
1001
|
+
}</span>
|
|
1012
1002
|
</div>
|
|
1013
1003
|
<div class="env-detail-row">
|
|
1014
1004
|
<span class="env-detail-label">Run Context</span>
|
|
@@ -1019,6 +1009,260 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
1019
1009
|
</div>
|
|
1020
1010
|
`;
|
|
1021
1011
|
}
|
|
1012
|
+
function generateWorkerDistributionChart(results) {
|
|
1013
|
+
if (!results || results.length === 0) {
|
|
1014
|
+
return '<div class="no-data">No test results data available to display worker distribution.</div>';
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// 1. Sort results by startTime to ensure chronological order
|
|
1018
|
+
const sortedResults = [...results].sort((a, b) => {
|
|
1019
|
+
const timeA = a.startTime ? new Date(a.startTime).getTime() : 0;
|
|
1020
|
+
const timeB = b.startTime ? new Date(b.startTime).getTime() : 0;
|
|
1021
|
+
return timeA - timeB;
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
const workerData = sortedResults.reduce((acc, test) => {
|
|
1025
|
+
const workerId =
|
|
1026
|
+
typeof test.workerId !== "undefined" ? test.workerId : "N/A";
|
|
1027
|
+
if (!acc[workerId]) {
|
|
1028
|
+
acc[workerId] = { passed: 0, failed: 0, skipped: 0, tests: [] };
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const status = String(test.status).toLowerCase();
|
|
1032
|
+
if (status === "passed" || status === "failed" || status === "skipped") {
|
|
1033
|
+
acc[workerId][status]++;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const testTitleParts = test.name.split(" > ");
|
|
1037
|
+
const testTitle =
|
|
1038
|
+
testTitleParts[testTitleParts.length - 1] || "Unnamed Test";
|
|
1039
|
+
// Store both name and status for each test
|
|
1040
|
+
acc[workerId].tests.push({ name: testTitle, status: status });
|
|
1041
|
+
|
|
1042
|
+
return acc;
|
|
1043
|
+
}, {});
|
|
1044
|
+
|
|
1045
|
+
const workerIds = Object.keys(workerData).sort((a, b) => {
|
|
1046
|
+
if (a === "N/A") return 1;
|
|
1047
|
+
if (b === "N/A") return -1;
|
|
1048
|
+
return parseInt(a, 10) - parseInt(b, 10);
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
if (workerIds.length === 0) {
|
|
1052
|
+
return '<div class="no-data">Could not determine worker distribution from test data.</div>';
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const chartId = `workerDistChart-${Date.now()}-${Math.random()
|
|
1056
|
+
.toString(36)
|
|
1057
|
+
.substring(2, 7)}`;
|
|
1058
|
+
const renderFunctionName = `renderWorkerDistChart_${chartId.replace(
|
|
1059
|
+
/-/g,
|
|
1060
|
+
"_"
|
|
1061
|
+
)}`;
|
|
1062
|
+
const modalJsNamespace = `modal_funcs_${chartId.replace(/-/g, "_")}`;
|
|
1063
|
+
|
|
1064
|
+
// The categories now just need the name for the axis labels
|
|
1065
|
+
const categories = workerIds.map((id) => `Worker ${id}`);
|
|
1066
|
+
|
|
1067
|
+
// We pass the full data separately to the script
|
|
1068
|
+
const fullWorkerData = workerIds.map((id) => ({
|
|
1069
|
+
id: id,
|
|
1070
|
+
name: `Worker ${id}`,
|
|
1071
|
+
tests: workerData[id].tests,
|
|
1072
|
+
}));
|
|
1073
|
+
|
|
1074
|
+
const passedData = workerIds.map((id) => workerData[id].passed);
|
|
1075
|
+
const failedData = workerIds.map((id) => workerData[id].failed);
|
|
1076
|
+
const skippedData = workerIds.map((id) => workerData[id].skipped);
|
|
1077
|
+
|
|
1078
|
+
const categoriesString = JSON.stringify(categories);
|
|
1079
|
+
const fullDataString = JSON.stringify(fullWorkerData);
|
|
1080
|
+
const seriesString = JSON.stringify([
|
|
1081
|
+
{ name: "Passed", data: passedData, color: "var(--success-color)" },
|
|
1082
|
+
{ name: "Failed", data: failedData, color: "var(--danger-color)" },
|
|
1083
|
+
{ name: "Skipped", data: skippedData, color: "var(--warning-color)" },
|
|
1084
|
+
]);
|
|
1085
|
+
|
|
1086
|
+
// The HTML now includes the chart container, the modal, and styles for the modal
|
|
1087
|
+
return `
|
|
1088
|
+
<style>
|
|
1089
|
+
.worker-modal-overlay {
|
|
1090
|
+
position: fixed; z-index: 1050; left: 0; top: 0; width: 100%; height: 100%;
|
|
1091
|
+
overflow: auto; background-color: rgba(0,0,0,0.6);
|
|
1092
|
+
display: none; align-items: center; justify-content: center;
|
|
1093
|
+
}
|
|
1094
|
+
.worker-modal-content {
|
|
1095
|
+
background-color: #3d4043;
|
|
1096
|
+
color: var(--card-background-color);
|
|
1097
|
+
margin: auto; padding: 20px; border: 1px solid var(--border-color, #888);
|
|
1098
|
+
width: 80%; max-width: 700px; border-radius: 8px;
|
|
1099
|
+
position: relative; box-shadow: 0 5px 15px rgba(0,0,0,0.5);
|
|
1100
|
+
}
|
|
1101
|
+
.worker-modal-close {
|
|
1102
|
+
position: absolute; top: 10px; right: 20px;
|
|
1103
|
+
font-size: 28px; font-weight: bold; cursor: pointer;
|
|
1104
|
+
line-height: 1;
|
|
1105
|
+
}
|
|
1106
|
+
.worker-modal-close:hover, .worker-modal-close:focus {
|
|
1107
|
+
color: var(--text-color, #000);
|
|
1108
|
+
}
|
|
1109
|
+
#worker-modal-body-${chartId} ul {
|
|
1110
|
+
list-style-type: none; padding-left: 0; margin-top: 15px; max-height: 45vh; overflow-y: auto;
|
|
1111
|
+
}
|
|
1112
|
+
#worker-modal-body-${chartId} li {
|
|
1113
|
+
padding: 8px 5px; border-bottom: 1px solid var(--border-color, #eee);
|
|
1114
|
+
font-size: 0.9em;
|
|
1115
|
+
}
|
|
1116
|
+
#worker-modal-body-${chartId} li:last-child {
|
|
1117
|
+
border-bottom: none;
|
|
1118
|
+
}
|
|
1119
|
+
#worker-modal-body-${chartId} li > span {
|
|
1120
|
+
display: inline-block;
|
|
1121
|
+
width: 70px;
|
|
1122
|
+
font-weight: bold;
|
|
1123
|
+
text-align: right;
|
|
1124
|
+
margin-right: 10px;
|
|
1125
|
+
}
|
|
1126
|
+
</style>
|
|
1127
|
+
|
|
1128
|
+
<div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}" style="min-height: 350px;">
|
|
1129
|
+
<div class="no-data">Loading Worker Distribution Chart...</div>
|
|
1130
|
+
</div>
|
|
1131
|
+
|
|
1132
|
+
<div id="worker-modal-${chartId}" class="worker-modal-overlay">
|
|
1133
|
+
<div class="worker-modal-content">
|
|
1134
|
+
<span class="worker-modal-close">×</span>
|
|
1135
|
+
<h3 id="worker-modal-title-${chartId}" style="text-align: center; margin-top: 0; margin-bottom: 25px; font-size: 1.25em; font-weight: 600; color: #fff"></h3>
|
|
1136
|
+
<div id="worker-modal-body-${chartId}"></div>
|
|
1137
|
+
</div>
|
|
1138
|
+
</div>
|
|
1139
|
+
|
|
1140
|
+
<script>
|
|
1141
|
+
// Namespace for modal functions to avoid global scope pollution
|
|
1142
|
+
window.${modalJsNamespace} = {};
|
|
1143
|
+
|
|
1144
|
+
window.${renderFunctionName} = function() {
|
|
1145
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
1146
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found."); return; }
|
|
1147
|
+
|
|
1148
|
+
// --- Modal Setup ---
|
|
1149
|
+
const modal = document.getElementById('worker-modal-${chartId}');
|
|
1150
|
+
const modalTitle = document.getElementById('worker-modal-title-${chartId}');
|
|
1151
|
+
const modalBody = document.getElementById('worker-modal-body-${chartId}');
|
|
1152
|
+
const closeModalBtn = modal.querySelector('.worker-modal-close');
|
|
1153
|
+
|
|
1154
|
+
window.${modalJsNamespace}.open = function(worker) {
|
|
1155
|
+
if (!worker) return;
|
|
1156
|
+
modalTitle.textContent = 'Test Details for ' + worker.name;
|
|
1157
|
+
|
|
1158
|
+
let testListHtml = '<ul>';
|
|
1159
|
+
if (worker.tests && worker.tests.length > 0) {
|
|
1160
|
+
worker.tests.forEach(test => {
|
|
1161
|
+
let color = 'inherit';
|
|
1162
|
+
if (test.status === 'passed') color = 'var(--success-color)';
|
|
1163
|
+
else if (test.status === 'failed') color = 'var(--danger-color)';
|
|
1164
|
+
else if (test.status === 'skipped') color = 'var(--warning-color)';
|
|
1165
|
+
|
|
1166
|
+
const escapedName = test.name.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1167
|
+
testListHtml += \`<li style="color: \${color};"><span style="color: \${color}">[\${test.status.toUpperCase()}]</span> \${escapedName}</li>\`;
|
|
1168
|
+
});
|
|
1169
|
+
} else {
|
|
1170
|
+
testListHtml += '<li>No detailed test data available for this worker.</li>';
|
|
1171
|
+
}
|
|
1172
|
+
testListHtml += '</ul>';
|
|
1173
|
+
|
|
1174
|
+
modalBody.innerHTML = testListHtml;
|
|
1175
|
+
modal.style.display = 'flex';
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
const closeModal = function() {
|
|
1179
|
+
modal.style.display = 'none';
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
closeModalBtn.onclick = closeModal;
|
|
1183
|
+
modal.onclick = function(event) {
|
|
1184
|
+
// Close if clicked on the dark overlay background
|
|
1185
|
+
if (event.target == modal) {
|
|
1186
|
+
closeModal();
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
// --- Highcharts Setup ---
|
|
1192
|
+
if (typeof Highcharts !== 'undefined') {
|
|
1193
|
+
try {
|
|
1194
|
+
chartContainer.innerHTML = '';
|
|
1195
|
+
const fullData = ${fullDataString};
|
|
1196
|
+
|
|
1197
|
+
const chartOptions = {
|
|
1198
|
+
chart: { type: 'bar', height: 350, backgroundColor: 'transparent' },
|
|
1199
|
+
title: { text: null },
|
|
1200
|
+
xAxis: {
|
|
1201
|
+
categories: ${categoriesString},
|
|
1202
|
+
title: { text: 'Worker ID' },
|
|
1203
|
+
labels: { style: { color: 'var(--text-color-secondary)' }}
|
|
1204
|
+
},
|
|
1205
|
+
yAxis: {
|
|
1206
|
+
min: 0,
|
|
1207
|
+
title: { text: 'Number of Tests' },
|
|
1208
|
+
labels: { style: { color: 'var(--text-color-secondary)' }},
|
|
1209
|
+
stackLabels: { enabled: true, style: { fontWeight: 'bold', color: 'var(--text-color)' } }
|
|
1210
|
+
},
|
|
1211
|
+
legend: { reversed: true, itemStyle: { fontSize: "12px", color: 'var(--text-color)' } },
|
|
1212
|
+
plotOptions: {
|
|
1213
|
+
series: {
|
|
1214
|
+
stacking: 'normal',
|
|
1215
|
+
cursor: 'pointer',
|
|
1216
|
+
point: {
|
|
1217
|
+
events: {
|
|
1218
|
+
click: function () {
|
|
1219
|
+
// 'this.x' is the index of the category
|
|
1220
|
+
const workerData = fullData[this.x];
|
|
1221
|
+
window.${modalJsNamespace}.open(workerData);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
},
|
|
1227
|
+
tooltip: {
|
|
1228
|
+
shared: true,
|
|
1229
|
+
headerFormat: '<b>{point.key}</b> (Click for details)<br/>',
|
|
1230
|
+
pointFormat: '<span style="color:{series.color}">●</span> {series.name}: <b>{point.y}</b><br/>',
|
|
1231
|
+
footerFormat: 'Total: <b>{point.total}</b>'
|
|
1232
|
+
},
|
|
1233
|
+
series: ${seriesString},
|
|
1234
|
+
credits: { enabled: false }
|
|
1235
|
+
};
|
|
1236
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
console.error("Error rendering chart ${chartId}:", e);
|
|
1239
|
+
chartContainer.innerHTML = '<div class="no-data">Error rendering worker distribution chart.</div>';
|
|
1240
|
+
}
|
|
1241
|
+
} else {
|
|
1242
|
+
chartContainer.innerHTML = '<div class="no-data">Charting library not available for worker distribution.</div>';
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
</script>
|
|
1246
|
+
`;
|
|
1247
|
+
}
|
|
1248
|
+
const infoTooltip = `
|
|
1249
|
+
<span class="info-tooltip" style="display: inline-block; margin-left: 8px;">
|
|
1250
|
+
<span class="info-icon"
|
|
1251
|
+
style="cursor: pointer; font-size: 1.25rem;"
|
|
1252
|
+
onclick="window.workerInfoPrompt()">ℹ️</span>
|
|
1253
|
+
</span>
|
|
1254
|
+
<script>
|
|
1255
|
+
window.workerInfoPrompt = function() {
|
|
1256
|
+
const message = 'Why is worker -1 special?\\n\\n' +
|
|
1257
|
+
'Playwright assigns skipped tests to worker -1 because:\\n' +
|
|
1258
|
+
'1. They don\\'t require browser execution\\n' +
|
|
1259
|
+
'2. This keeps real workers focused on actual tests\\n' +
|
|
1260
|
+
'3. Maintains clean reporting\\n\\n' +
|
|
1261
|
+
'This is an intentional optimization by Playwright.';
|
|
1262
|
+
alert(message);
|
|
1263
|
+
}
|
|
1264
|
+
</script>
|
|
1265
|
+
`;
|
|
1022
1266
|
function generateTestHistoryContent(trendData) {
|
|
1023
1267
|
if (
|
|
1024
1268
|
!trendData ||
|
|
@@ -1086,19 +1330,19 @@ function generateTestHistoryContent(trendData) {
|
|
|
1086
1330
|
|
|
1087
1331
|
<div class="test-history-grid">
|
|
1088
1332
|
${testHistory
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1333
|
+
.map((test) => {
|
|
1334
|
+
const latestRun =
|
|
1335
|
+
test.history.length > 0
|
|
1336
|
+
? test.history[test.history.length - 1]
|
|
1337
|
+
: { status: "unknown" };
|
|
1338
|
+
return `
|
|
1095
1339
|
<div class="test-history-card" data-test-name="${sanitizeHTML(
|
|
1096
|
-
|
|
1097
|
-
|
|
1340
|
+
test.testTitle.toLowerCase()
|
|
1341
|
+
)}" data-latest-status="${latestRun.status}">
|
|
1098
1342
|
<div class="test-history-header">
|
|
1099
1343
|
<p title="${sanitizeHTML(test.testTitle)}">${capitalize(
|
|
1100
|
-
|
|
1101
|
-
|
|
1344
|
+
sanitizeHTML(test.testTitle)
|
|
1345
|
+
)}</p>
|
|
1102
1346
|
<span class="status-badge ${getStatusClass(latestRun.status)}">
|
|
1103
1347
|
${String(latestRun.status).toUpperCase()}
|
|
1104
1348
|
</span>
|
|
@@ -1113,27 +1357,27 @@ function generateTestHistoryContent(trendData) {
|
|
|
1113
1357
|
<thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Date</th></tr></thead>
|
|
1114
1358
|
<tbody>
|
|
1115
1359
|
${test.history
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1360
|
+
.slice()
|
|
1361
|
+
.reverse()
|
|
1362
|
+
.map(
|
|
1363
|
+
(run) => `
|
|
1120
1364
|
<tr>
|
|
1121
1365
|
<td>${run.runId}</td>
|
|
1122
1366
|
<td><span class="status-badge-small ${getStatusClass(
|
|
1123
|
-
|
|
1124
|
-
|
|
1367
|
+
run.status
|
|
1368
|
+
)}">${String(run.status).toUpperCase()}</span></td>
|
|
1125
1369
|
<td>${formatDuration(run.duration)}</td>
|
|
1126
1370
|
<td>${formatDate(run.timestamp)}</td>
|
|
1127
1371
|
</tr>`
|
|
1128
|
-
|
|
1129
|
-
|
|
1372
|
+
)
|
|
1373
|
+
.join("")}
|
|
1130
1374
|
</tbody>
|
|
1131
1375
|
</table>
|
|
1132
1376
|
</div>
|
|
1133
1377
|
</details>
|
|
1134
1378
|
</div>`;
|
|
1135
|
-
|
|
1136
|
-
|
|
1379
|
+
})
|
|
1380
|
+
.join("")}
|
|
1137
1381
|
</div>
|
|
1138
1382
|
</div>
|
|
1139
1383
|
`;
|
|
@@ -1218,12 +1462,11 @@ function generateSuitesWidget(suitesData) {
|
|
|
1218
1462
|
<div class="suites-widget">
|
|
1219
1463
|
<div class="suites-header">
|
|
1220
1464
|
<h2>Test Suites</h2>
|
|
1221
|
-
<span class="summary-badge">${
|
|
1222
|
-
suitesData.length
|
|
1465
|
+
<span class="summary-badge">${suitesData.length
|
|
1223
1466
|
} suites • ${suitesData.reduce(
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1467
|
+
(sum, suite) => sum + suite.count,
|
|
1468
|
+
0
|
|
1469
|
+
)} tests</span>
|
|
1227
1470
|
</div>
|
|
1228
1471
|
<div class="suites-grid">
|
|
1229
1472
|
${suitesData
|
|
@@ -1236,28 +1479,24 @@ function generateSuitesWidget(suitesData) {
|
|
|
1236
1479
|
)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
|
|
1237
1480
|
</div>
|
|
1238
1481
|
<div>🖥️ <span class="browser-tag">${sanitizeHTML(
|
|
1239
|
-
|
|
1240
|
-
|
|
1482
|
+
suite.browser
|
|
1483
|
+
)}</span></div>
|
|
1241
1484
|
<div class="suite-card-body">
|
|
1242
|
-
<span class="test-count">${suite.count} test${
|
|
1243
|
-
|
|
1244
|
-
}</span>
|
|
1485
|
+
<span class="test-count">${suite.count} test${suite.count !== 1 ? "s" : ""
|
|
1486
|
+
}</span>
|
|
1245
1487
|
<div class="suite-stats">
|
|
1246
|
-
${
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
${
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
? `<span class="stat-skipped" title="Skipped"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-exclamation-triangle-fill" viewBox="0 0 16 16"><path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/></svg> ${suite.skipped}</span>`
|
|
1259
|
-
: ""
|
|
1260
|
-
}
|
|
1488
|
+
${suite.passed > 0
|
|
1489
|
+
? `<span class="stat-passed" title="Passed"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/></svg> ${suite.passed}</span>`
|
|
1490
|
+
: ""
|
|
1491
|
+
}
|
|
1492
|
+
${suite.failed > 0
|
|
1493
|
+
? `<span class="stat-failed" title="Failed"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/></svg> ${suite.failed}</span>`
|
|
1494
|
+
: ""
|
|
1495
|
+
}
|
|
1496
|
+
${suite.skipped > 0
|
|
1497
|
+
? `<span class="stat-skipped" title="Skipped"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-exclamation-triangle-fill" viewBox="0 0 16 16"><path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/></svg> ${suite.skipped}</span>`
|
|
1498
|
+
: ""
|
|
1499
|
+
}
|
|
1261
1500
|
</div>
|
|
1262
1501
|
</div>
|
|
1263
1502
|
</div>`
|
|
@@ -1267,13 +1506,103 @@ function generateSuitesWidget(suitesData) {
|
|
|
1267
1506
|
</div>`;
|
|
1268
1507
|
}
|
|
1269
1508
|
function getAttachmentIcon(contentType) {
|
|
1270
|
-
if (contentType
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
if (
|
|
1509
|
+
if (!contentType) return "📎"; // Handle undefined/null
|
|
1510
|
+
|
|
1511
|
+
const normalizedType = contentType.toLowerCase();
|
|
1512
|
+
|
|
1513
|
+
if (normalizedType.includes("pdf")) return "📄";
|
|
1514
|
+
if (normalizedType.includes("json")) return "{ }";
|
|
1515
|
+
if (/html/.test(normalizedType)) return "🌐"; // Fixed: regex for any HTML type
|
|
1516
|
+
if (normalizedType.includes("xml")) return "<>";
|
|
1517
|
+
if (normalizedType.includes("csv")) return "📊";
|
|
1518
|
+
if (normalizedType.startsWith("text/")) return "📝";
|
|
1275
1519
|
return "📎";
|
|
1276
1520
|
}
|
|
1521
|
+
function generateAIFailureAnalyzerTab(results) {
|
|
1522
|
+
const failedTests = (results || []).filter(test => test.status === 'failed');
|
|
1523
|
+
|
|
1524
|
+
if (failedTests.length === 0) {
|
|
1525
|
+
return `
|
|
1526
|
+
<h2 class="tab-main-title">AI Failure Analysis</h2>
|
|
1527
|
+
<div class="no-data">Congratulations! No failed tests in this run.</div>
|
|
1528
|
+
`;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// btoa is not available in Node.js environment, so we define a simple polyfill for it.
|
|
1532
|
+
const btoa = (str) => Buffer.from(str).toString('base64');
|
|
1533
|
+
|
|
1534
|
+
return `
|
|
1535
|
+
<h2 class="tab-main-title">AI Failure Analysis</h2>
|
|
1536
|
+
<div class="ai-analyzer-stats">
|
|
1537
|
+
<div class="stat-item">
|
|
1538
|
+
<span class="stat-number">${failedTests.length}</span>
|
|
1539
|
+
<span class="stat-label">Failed Tests</span>
|
|
1540
|
+
</div>
|
|
1541
|
+
<div class="stat-item">
|
|
1542
|
+
<span class="stat-number">${new Set(failedTests.map(t => t.browser)).size}</span>
|
|
1543
|
+
<span class="stat-label">Browsers</span>
|
|
1544
|
+
</div>
|
|
1545
|
+
<div class="stat-item">
|
|
1546
|
+
<span class="stat-number">${(Math.round(failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) / 1000))}s</span>
|
|
1547
|
+
<span class="stat-label">Total Duration</span>
|
|
1548
|
+
</div>
|
|
1549
|
+
</div>
|
|
1550
|
+
<p class="ai-analyzer-description">
|
|
1551
|
+
Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for specific failed test.
|
|
1552
|
+
</p>
|
|
1553
|
+
|
|
1554
|
+
<div class="compact-failure-list">
|
|
1555
|
+
${failedTests.map(test => {
|
|
1556
|
+
const testTitle = test.name.split(" > ").pop() || "Unnamed Test";
|
|
1557
|
+
const testJson = btoa(JSON.stringify(test)); // Base64 encode the test object
|
|
1558
|
+
const truncatedError = (test.errorMessage || "No error message").slice(0, 150) +
|
|
1559
|
+
(test.errorMessage && test.errorMessage.length > 150 ? "..." : "");
|
|
1560
|
+
|
|
1561
|
+
return `
|
|
1562
|
+
<div class="compact-failure-item">
|
|
1563
|
+
<div class="failure-header">
|
|
1564
|
+
<div class="failure-main-info">
|
|
1565
|
+
<h3 class="failure-title" title="${sanitizeHTML(test.name)}">${sanitizeHTML(testTitle)}</h3>
|
|
1566
|
+
<div class="failure-meta">
|
|
1567
|
+
<span class="browser-indicator">${sanitizeHTML(test.browser || 'unknown')}</span>
|
|
1568
|
+
<span class="duration-indicator">${formatDuration(test.duration)}</span>
|
|
1569
|
+
</div>
|
|
1570
|
+
</div>
|
|
1571
|
+
<button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
|
|
1572
|
+
<span class="ai-text">AI Fix</span>
|
|
1573
|
+
</button>
|
|
1574
|
+
</div>
|
|
1575
|
+
<div class="failure-error-preview">
|
|
1576
|
+
<div class="error-snippet">${formatPlaywrightError(truncatedError)}</div>
|
|
1577
|
+
<button class="expand-error-btn" onclick="toggleErrorDetails(this)">
|
|
1578
|
+
<span class="expand-text">Show Full Error</span>
|
|
1579
|
+
<span class="expand-icon">▼</span>
|
|
1580
|
+
</button>
|
|
1581
|
+
</div>
|
|
1582
|
+
<div class="full-error-details" style="display: none;">
|
|
1583
|
+
<div class="full-error-content">
|
|
1584
|
+
${formatPlaywrightError(test.errorMessage || "No detailed error message available")}
|
|
1585
|
+
</div>
|
|
1586
|
+
</div>
|
|
1587
|
+
</div>
|
|
1588
|
+
`
|
|
1589
|
+
}).join('')}
|
|
1590
|
+
</div>
|
|
1591
|
+
|
|
1592
|
+
<!-- AI Fix Modal -->
|
|
1593
|
+
<div id="ai-fix-modal" class="ai-modal-overlay" onclick="closeAiModal()">
|
|
1594
|
+
<div class="ai-modal-content" onclick="event.stopPropagation()">
|
|
1595
|
+
<div class="ai-modal-header">
|
|
1596
|
+
<h3 id="ai-fix-modal-title">AI Analysis</h3>
|
|
1597
|
+
<span class="ai-modal-close" onclick="closeAiModal()">×</span>
|
|
1598
|
+
</div>
|
|
1599
|
+
<div class="ai-modal-body" id="ai-fix-modal-content">
|
|
1600
|
+
<!-- Content will be injected by JavaScript -->
|
|
1601
|
+
</div>
|
|
1602
|
+
</div>
|
|
1603
|
+
</div>
|
|
1604
|
+
`;
|
|
1605
|
+
}
|
|
1277
1606
|
function generateHTML(reportData, trendData = null) {
|
|
1278
1607
|
const { run, results } = reportData;
|
|
1279
1608
|
const suitesData = getSuitesData(reportData.results || []);
|
|
@@ -1285,6 +1614,13 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1285
1614
|
duration: 0,
|
|
1286
1615
|
timestamp: new Date().toISOString(),
|
|
1287
1616
|
};
|
|
1617
|
+
|
|
1618
|
+
const fixPath = (p) => {
|
|
1619
|
+
if (!p) return "";
|
|
1620
|
+
// This regex handles both forward slashes and backslashes
|
|
1621
|
+
return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`), '');
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1288
1624
|
const totalTestsOr1 = runSummary.totalTests || 1;
|
|
1289
1625
|
const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
|
|
1290
1626
|
const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
|
|
@@ -1327,23 +1663,20 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1327
1663
|
)}</span>
|
|
1328
1664
|
</div>
|
|
1329
1665
|
<div class="step-details" style="display: none;">
|
|
1330
|
-
${
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
)}</div>`
|
|
1666
|
+
${step.codeLocation
|
|
1667
|
+
? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
|
|
1668
|
+
step.codeLocation
|
|
1669
|
+
)}</div>`
|
|
1335
1670
|
: ""
|
|
1336
|
-
|
|
1337
|
-
${
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
: ""
|
|
1346
|
-
}
|
|
1671
|
+
}
|
|
1672
|
+
${step.errorMessage
|
|
1673
|
+
? `<div class="test-error-summary">
|
|
1674
|
+
${step.stackTrace
|
|
1675
|
+
? `<div class="stack-trace">${formatPlaywrightError(
|
|
1676
|
+
step.stackTrace
|
|
1677
|
+
)}</div>`
|
|
1678
|
+
: ""
|
|
1679
|
+
}
|
|
1347
1680
|
<button
|
|
1348
1681
|
class="copy-error-btn"
|
|
1349
1682
|
onclick="copyErrorToClipboard(this)"
|
|
@@ -1365,15 +1698,14 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1365
1698
|
</button>
|
|
1366
1699
|
</div>`
|
|
1367
1700
|
: ""
|
|
1368
|
-
|
|
1369
|
-
${
|
|
1370
|
-
hasNestedSteps
|
|
1701
|
+
}
|
|
1702
|
+
${hasNestedSteps
|
|
1371
1703
|
? `<div class="nested-steps">${generateStepsHTML(
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1704
|
+
step.steps,
|
|
1705
|
+
depth + 1
|
|
1706
|
+
)}</div>`
|
|
1375
1707
|
: ""
|
|
1376
|
-
|
|
1708
|
+
}
|
|
1377
1709
|
</div>
|
|
1378
1710
|
</div>`;
|
|
1379
1711
|
})
|
|
@@ -1381,29 +1713,27 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1381
1713
|
};
|
|
1382
1714
|
|
|
1383
1715
|
return `
|
|
1384
|
-
<div class="test-case" data-status="${
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
.toLowerCase()}">
|
|
1716
|
+
<div class="test-case" data-status="${test.status
|
|
1717
|
+
}" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
|
|
1718
|
+
.join(",")
|
|
1719
|
+
.toLowerCase()}">
|
|
1389
1720
|
<div class="test-case-header" role="button" aria-expanded="false">
|
|
1390
1721
|
<div class="test-case-summary">
|
|
1391
1722
|
<span class="status-badge ${getStatusClass(test.status)}">${String(
|
|
1392
|
-
|
|
1393
|
-
|
|
1723
|
+
test.status
|
|
1724
|
+
).toUpperCase()}</span>
|
|
1394
1725
|
<span class="test-case-title" title="${sanitizeHTML(
|
|
1395
1726
|
test.name
|
|
1396
1727
|
)}">${sanitizeHTML(testTitle)}</span>
|
|
1397
1728
|
<span class="test-case-browser">(${sanitizeHTML(browser)})</span>
|
|
1398
1729
|
</div>
|
|
1399
1730
|
<div class="test-case-meta">
|
|
1400
|
-
${
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
}
|
|
1731
|
+
${test.tags && test.tags.length > 0
|
|
1732
|
+
? test.tags
|
|
1733
|
+
.map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
|
|
1734
|
+
.join(" ")
|
|
1735
|
+
: ""
|
|
1736
|
+
}
|
|
1407
1737
|
<span class="test-duration">${formatDuration(test.duration)}</span>
|
|
1408
1738
|
</div>
|
|
1409
1739
|
</div>
|
|
@@ -1412,13 +1742,12 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1412
1742
|
<p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
|
|
1413
1743
|
test.workerId
|
|
1414
1744
|
)} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
${
|
|
1418
|
-
test
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
)}
|
|
1745
|
+
test.totalWorkers
|
|
1746
|
+
)}]</p>
|
|
1747
|
+
${test.errorMessage
|
|
1748
|
+
? `<div class="test-error-summary">${formatPlaywrightError(
|
|
1749
|
+
test.errorMessage
|
|
1750
|
+
)}
|
|
1422
1751
|
<button
|
|
1423
1752
|
class="copy-error-btn"
|
|
1424
1753
|
onclick="copyErrorToClipboard(this)"
|
|
@@ -1439,90 +1768,100 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1439
1768
|
Copy Error Prompt
|
|
1440
1769
|
</button>
|
|
1441
1770
|
</div>`
|
|
1442
|
-
|
|
1771
|
+
: ""
|
|
1772
|
+
}
|
|
1773
|
+
${test.snippet
|
|
1774
|
+
? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
|
|
1775
|
+
test.snippet
|
|
1776
|
+
)}</code></pre></div>`
|
|
1777
|
+
: ""
|
|
1443
1778
|
}
|
|
1444
1779
|
<h4>Steps</h4>
|
|
1445
1780
|
<div class="steps-list">${generateStepsHTML(test.steps)}</div>
|
|
1446
|
-
${
|
|
1447
|
-
test.stdout
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1781
|
+
${(() => {
|
|
1782
|
+
if (!test.stdout || test.stdout.length === 0) return "";
|
|
1783
|
+
// Create a unique ID for the <pre> element to target it for copying
|
|
1784
|
+
const logId = `stdout-log-${test.id || index}`;
|
|
1785
|
+
return `<div class="console-output-section">
|
|
1786
|
+
<h4>Console Output (stdout)
|
|
1787
|
+
<button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy Console</button>
|
|
1788
|
+
</h4>
|
|
1789
|
+
<div class="log-wrapper">
|
|
1790
|
+
<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(
|
|
1791
|
+
test.stdout.map((line) => sanitizeHTML(line)).join("\n")
|
|
1792
|
+
)}</pre>
|
|
1793
|
+
</div>
|
|
1794
|
+
</div>`;
|
|
1795
|
+
})()}
|
|
1796
|
+
${test.stderr && test.stderr.length > 0
|
|
1797
|
+
? `<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(
|
|
1798
|
+
test.stderr.map((line) => sanitizeHTML(line)).join("\n")
|
|
1799
|
+
)}</pre></div>`
|
|
1800
|
+
: ""
|
|
1459
1801
|
}
|
|
1460
|
-
${
|
|
1461
|
-
|
|
1462
|
-
? `
|
|
1802
|
+
${test.screenshots && test.screenshots.length > 0
|
|
1803
|
+
? `
|
|
1463
1804
|
<div class="attachments-section">
|
|
1464
1805
|
<h4>Screenshots</h4>
|
|
1465
1806
|
<div class="attachments-grid">
|
|
1466
1807
|
${test.screenshots
|
|
1467
|
-
|
|
1468
|
-
|
|
1808
|
+
.map(
|
|
1809
|
+
(screenshot, index) => `
|
|
1469
1810
|
<div class="attachment-item">
|
|
1470
|
-
<img src="${screenshot}" alt="Screenshot ${index + 1}">
|
|
1811
|
+
<img src="${fixPath(screenshot)}" alt="Screenshot ${index + 1}">
|
|
1471
1812
|
<div class="attachment-info">
|
|
1472
1813
|
<div class="trace-actions">
|
|
1473
|
-
<a href="${screenshot}" target="_blank" class="view-full">View Full Image</a>
|
|
1474
|
-
<a href="${screenshot}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
|
|
1814
|
+
<a href="${fixPath(screenshot)}" target="_blank" class="view-full">View Full Image</a>
|
|
1815
|
+
<a href="${fixPath(screenshot)}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
|
|
1475
1816
|
</div>
|
|
1476
1817
|
</div>
|
|
1477
1818
|
</div>
|
|
1478
1819
|
`
|
|
1479
|
-
|
|
1480
|
-
|
|
1820
|
+
)
|
|
1821
|
+
.join("")}
|
|
1481
1822
|
</div>
|
|
1482
1823
|
</div>
|
|
1483
1824
|
`
|
|
1484
|
-
|
|
1825
|
+
: ""
|
|
1485
1826
|
}
|
|
1486
|
-
${
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
<video controls width="100%" height="auto" title="Video ${
|
|
1504
|
-
|
|
1505
|
-
}">
|
|
1827
|
+
${test.videoPath && test.videoPath.length > 0
|
|
1828
|
+
? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
|
|
1829
|
+
.map((videoUrl, index) => {
|
|
1830
|
+
const fixedVideoUrl = fixPath(videoUrl);
|
|
1831
|
+
const fileExtension = String(fixedVideoUrl)
|
|
1832
|
+
.split(".")
|
|
1833
|
+
.pop()
|
|
1834
|
+
.toLowerCase();
|
|
1835
|
+
const mimeType =
|
|
1836
|
+
{
|
|
1837
|
+
mp4: "video/mp4",
|
|
1838
|
+
webm: "video/webm",
|
|
1839
|
+
ogg: "video/ogg",
|
|
1840
|
+
mov: "video/quicktime",
|
|
1841
|
+
avi: "video/x-msvideo",
|
|
1842
|
+
}[fileExtension] || "video/mp4";
|
|
1843
|
+
return `<div class="attachment-item video-item">
|
|
1844
|
+
<video controls width="100%" height="auto" title="Video ${index + 1
|
|
1845
|
+
}">
|
|
1506
1846
|
<source src="${sanitizeHTML(
|
|
1507
|
-
|
|
1508
|
-
|
|
1847
|
+
fixedVideoUrl
|
|
1848
|
+
)}" type="${mimeType}">
|
|
1509
1849
|
Your browser does not support the video tag.
|
|
1510
1850
|
</video>
|
|
1511
1851
|
<div class="attachment-info">
|
|
1512
1852
|
<div class="trace-actions">
|
|
1513
1853
|
<a href="${sanitizeHTML(
|
|
1514
|
-
|
|
1515
|
-
|
|
1854
|
+
fixedVideoUrl
|
|
1855
|
+
)}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
|
|
1516
1856
|
</div>
|
|
1517
1857
|
</div>
|
|
1518
1858
|
</div>`;
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1859
|
+
})
|
|
1860
|
+
.join("")}</div></div>`
|
|
1861
|
+
: ""
|
|
1522
1862
|
}
|
|
1523
|
-
${
|
|
1524
|
-
|
|
1525
|
-
? `
|
|
1863
|
+
${test.tracePath
|
|
1864
|
+
? `
|
|
1526
1865
|
<div class="attachments-section">
|
|
1527
1866
|
<h4>Trace Files</h4>
|
|
1528
1867
|
<div class="attachments-grid">
|
|
@@ -1530,69 +1869,70 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1530
1869
|
<div class="trace-preview">
|
|
1531
1870
|
<span class="trace-icon">📄</span>
|
|
1532
1871
|
<span class="trace-name">${sanitizeHTML(
|
|
1533
|
-
|
|
1534
|
-
|
|
1872
|
+
path.basename(test.tracePath)
|
|
1873
|
+
)}</span>
|
|
1535
1874
|
</div>
|
|
1536
1875
|
<div class="attachment-info">
|
|
1537
1876
|
<div class="trace-actions">
|
|
1538
1877
|
<a href="${sanitizeHTML(
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1878
|
+
fixPath(test.tracePath)
|
|
1879
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
1880
|
+
path.basename(test.tracePath)
|
|
1881
|
+
)}" class="download-trace">Download Trace</a>
|
|
1543
1882
|
</div>
|
|
1544
1883
|
</div>
|
|
1545
1884
|
</div>
|
|
1546
1885
|
</div>
|
|
1547
1886
|
</div>
|
|
1548
1887
|
`
|
|
1549
|
-
|
|
1888
|
+
: ""
|
|
1550
1889
|
}
|
|
1551
|
-
${
|
|
1552
|
-
|
|
1553
|
-
? `
|
|
1890
|
+
${test.attachments && test.attachments.length > 0
|
|
1891
|
+
? `
|
|
1554
1892
|
<div class="attachments-section">
|
|
1555
1893
|
<h4>Other Attachments</h4>
|
|
1556
1894
|
<div class="attachments-grid">
|
|
1557
1895
|
${test.attachments
|
|
1558
|
-
|
|
1559
|
-
|
|
1896
|
+
.map(
|
|
1897
|
+
(attachment) => `
|
|
1560
1898
|
<div class="attachment-item generic-attachment">
|
|
1561
1899
|
<div class="attachment-icon">${getAttachmentIcon(
|
|
1562
|
-
|
|
1563
|
-
|
|
1900
|
+
attachment.contentType
|
|
1901
|
+
)}</div>
|
|
1564
1902
|
<div class="attachment-caption">
|
|
1565
1903
|
<span class="attachment-name" title="${sanitizeHTML(
|
|
1566
|
-
|
|
1567
|
-
|
|
1904
|
+
attachment.name
|
|
1905
|
+
)}">${sanitizeHTML(attachment.name)}</span>
|
|
1568
1906
|
<span class="attachment-type">${sanitizeHTML(
|
|
1569
|
-
|
|
1570
|
-
|
|
1907
|
+
attachment.contentType
|
|
1908
|
+
)}</span>
|
|
1571
1909
|
</div>
|
|
1572
1910
|
<div class="attachment-info">
|
|
1573
1911
|
<div class="trace-actions">
|
|
1912
|
+
<a href="${sanitizeHTML(
|
|
1913
|
+
fixPath(attachment.path)
|
|
1914
|
+
)}" target="_blank" class="view-full">View</a>
|
|
1574
1915
|
<a href="${sanitizeHTML(
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1916
|
+
fixPath(attachment.path)
|
|
1917
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
1918
|
+
attachment.name
|
|
1919
|
+
)}" class="download-trace">Download</a>
|
|
1579
1920
|
</div>
|
|
1580
1921
|
</div>
|
|
1581
1922
|
</div>
|
|
1582
1923
|
`
|
|
1583
|
-
|
|
1584
|
-
|
|
1924
|
+
)
|
|
1925
|
+
.join("")}
|
|
1585
1926
|
</div>
|
|
1586
1927
|
</div>
|
|
1587
1928
|
`
|
|
1588
|
-
|
|
1929
|
+
: ""
|
|
1589
1930
|
}
|
|
1590
|
-
${
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
: ""
|
|
1931
|
+
${test.codeSnippet
|
|
1932
|
+
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
|
|
1933
|
+
sanitizeHTML(test.codeSnippet)
|
|
1934
|
+
)}</code></pre></div>`
|
|
1935
|
+
: ""
|
|
1596
1936
|
}
|
|
1597
1937
|
</div>
|
|
1598
1938
|
</div>`;
|
|
@@ -1605,8 +1945,8 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1605
1945
|
<head>
|
|
1606
1946
|
<meta charset="UTF-8">
|
|
1607
1947
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1608
|
-
<link rel="icon" type="image/png" href="https://i.postimg.cc/
|
|
1609
|
-
<link rel="apple-touch-icon" href="https://i.postimg.cc/
|
|
1948
|
+
<link rel="icon" type="image/png" href="https://i.postimg.cc/v817w4sg/logo.png">
|
|
1949
|
+
<link rel="apple-touch-icon" href="https://i.postimg.cc/v817w4sg/logo.png">
|
|
1610
1950
|
<script src="https://code.highcharts.com/highcharts.js" defer></script>
|
|
1611
1951
|
<title>Playwright Pulse Report</title>
|
|
1612
1952
|
<style>
|
|
@@ -1630,7 +1970,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1630
1970
|
.header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; padding-bottom: 25px; border-bottom: 1px solid var(--border-color); margin-bottom: 25px; }
|
|
1631
1971
|
.header-title { display: flex; align-items: center; gap: 15px; }
|
|
1632
1972
|
.header h1 { margin: 0; font-size: 1.85em; font-weight: 600; color: var(--primary-color); }
|
|
1633
|
-
#report-logo { height: 40px; width:
|
|
1973
|
+
#report-logo { height: 40px; width: 55px; }
|
|
1634
1974
|
.run-info { font-size: 0.9em; text-align: right; color: var(--text-color-secondary); line-height:1.5;}
|
|
1635
1975
|
.run-info strong { color: var(--text-color); }
|
|
1636
1976
|
.tabs { display: flex; border-bottom: 2px solid var(--border-color); margin-bottom: 30px; overflow-x: auto; }
|
|
@@ -1709,8 +2049,8 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1709
2049
|
.step-duration { color: var(--dark-gray-color); font-size: 0.9em; }
|
|
1710
2050
|
.step-details { display: none; padding: 14px; margin-top: 8px; background: #fdfdfd; border-radius: 6px; font-size: 0.95em; border: 1px solid var(--light-gray-color); }
|
|
1711
2051
|
.step-info { margin-bottom: 8px; }
|
|
1712
|
-
.
|
|
1713
|
-
.
|
|
2052
|
+
.test-error-summary { color: var(--danger-color); margin-top: 12px; padding: 14px; background: rgba(244,67,54,0.05); border-radius: 4px; font-size: 0.95em; border-left: 3px solid var(--danger-color); }
|
|
2053
|
+
.test-error-summary pre.stack-trace { margin-top: 10px; padding: 12px; background-color: rgba(0,0,0,0.03); border-radius: 4px; font-size:0.9em; max-height: 280px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; }
|
|
1714
2054
|
.step-hook { background-color: rgba(33,150,243,0.04); border-left: 3px solid var(--info-color) !important; }
|
|
1715
2055
|
.step-hook .step-title { font-style: italic; color: var(--info-color)}
|
|
1716
2056
|
.nested-steps { margin-top: 12px; }
|
|
@@ -1734,7 +2074,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1734
2074
|
.attachment-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1735
2075
|
.attachment-type { font-size: 0.8rem; color: var(--text-color-secondary); }
|
|
1736
2076
|
.trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
|
|
1737
|
-
.test-history-container h2.tab-main-title { font-size: 1.6em; margin-bottom: 18px; color: var(--primary-color); border-bottom: 1px solid var(--border-color); padding-bottom: 12px;}
|
|
2077
|
+
.test-history-container h2.tab-main-title, .ai-analyzer-container h2.tab-main-title { font-size: 1.6em; margin-bottom: 18px; color: var(--primary-color); border-bottom: 1px solid var(--border-color); padding-bottom: 12px;}
|
|
1738
2078
|
.test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
|
|
1739
2079
|
.test-history-card { background: var(--card-background-color); border: 1px solid var(--border-color); border-radius: var(--border-radius); padding: 22px; box-shadow: var(--box-shadow-light); display: flex; flex-direction: column; }
|
|
1740
2080
|
.test-history-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 14px; border-bottom: 1px solid var(--light-gray-color); }
|
|
@@ -1754,8 +2094,24 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1754
2094
|
.status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
|
|
1755
2095
|
.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); }
|
|
1756
2096
|
.no-data-chart {font-size: 0.95em; padding: 18px;}
|
|
1757
|
-
|
|
1758
|
-
|
|
2097
|
+
.ai-failure-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 22px; }
|
|
2098
|
+
.ai-failure-card { background: var(--card-background-color); border: 1px solid var(--border-color); border-left: 5px solid var(--danger-color); border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); display: flex; flex-direction: column; }
|
|
2099
|
+
.ai-failure-card-header { padding: 15px 20px; border-bottom: 1px solid var(--light-gray-color); display: flex; align-items: center; justify-content: space-between; gap: 15px; }
|
|
2100
|
+
.ai-failure-card-header h3 { margin: 0; font-size: 1.1em; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2101
|
+
.ai-failure-card-body { padding: 20px; }
|
|
2102
|
+
.ai-fix-btn { background-color: var(--primary-color); color: white; border: none; padding: 10px 18px; font-size: 1em; font-weight: 600; border-radius: 6px; cursor: pointer; transition: background-color 0.2s ease, transform 0.2s ease; display: inline-flex; align-items: center; gap: 8px; }
|
|
2103
|
+
.ai-fix-btn:hover { background-color: var(--accent-color); transform: translateY(-2px); }
|
|
2104
|
+
.ai-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.65); display: none; align-items: center; justify-content: center; z-index: 1050; animation: fadeIn 0.3s; }
|
|
2105
|
+
.ai-modal-content { background-color: var(--card-background-color); color: var(--text-color); border-radius: var(--border-radius); width: 90%; max-width: 800px; max-height: 90vh; box-shadow: 0 10px 30px rgba(0,0,0,0.2); display: flex; flex-direction: column; overflow: hidden; }
|
|
2106
|
+
.ai-modal-header { padding: 18px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
|
|
2107
|
+
.ai-modal-header h3 { margin: 0; font-size: 1.25em; }
|
|
2108
|
+
.ai-modal-close { font-size: 2rem; font-weight: 300; cursor: pointer; color: var(--dark-gray-color); line-height: 1; transition: color 0.2s; }
|
|
2109
|
+
.ai-modal-close:hover { color: var(--danger-color); }
|
|
2110
|
+
.ai-modal-body { padding: 25px; overflow-y: auto; }
|
|
2111
|
+
.ai-modal-body h4 { margin-top: 18px; margin-bottom: 10px; font-size: 1.1em; color: var(--primary-color); }
|
|
2112
|
+
.ai-modal-body p { margin-bottom: 15px; }
|
|
2113
|
+
.ai-loader { margin: 40px auto; border: 5px solid #f3f3f3; border-top: 5px solid var(--primary-color); border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite; }
|
|
2114
|
+
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
1759
2115
|
.trace-preview { padding: 1rem; text-align: center; background: #f5f5f5; border-bottom: 1px solid #e1e1e1; }
|
|
1760
2116
|
.trace-icon { font-size: 2rem; display: block; margin-bottom: 0.5rem; }
|
|
1761
2117
|
.trace-name { word-break: break-word; font-size: 0.9rem; }
|
|
@@ -1767,22 +2123,230 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1767
2123
|
.download-trace:hover { background: #cbd5e0; }
|
|
1768
2124
|
.filters button.clear-filters-btn { background-color: var(--medium-gray-color); color: var(--text-color); }
|
|
1769
2125
|
.filters button.clear-filters-btn:hover { background-color: var(--dark-gray-color); color: #fff; }
|
|
2126
|
+
.copy-btn {color: var(--primary-color); background: #fefefe; border-radius: 8px; cursor: pointer; border-color: var(--primary-color); font-size: 1em; margin-left: 93%; font-weight: 600;}
|
|
2127
|
+
/* Compact AI Failure Analyzer Styles */
|
|
2128
|
+
.ai-analyzer-stats {
|
|
2129
|
+
display: flex;
|
|
2130
|
+
gap: 20px;
|
|
2131
|
+
margin-bottom: 25px;
|
|
2132
|
+
padding: 20px;
|
|
2133
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
2134
|
+
border-radius: var(--border-radius);
|
|
2135
|
+
justify-content: center;
|
|
2136
|
+
}
|
|
2137
|
+
.stat-item {
|
|
2138
|
+
text-align: center;
|
|
2139
|
+
color: white;
|
|
2140
|
+
}
|
|
2141
|
+
.stat-number {
|
|
2142
|
+
display: block;
|
|
2143
|
+
font-size: 2em;
|
|
2144
|
+
font-weight: 700;
|
|
2145
|
+
line-height: 1;
|
|
2146
|
+
}
|
|
2147
|
+
.stat-label {
|
|
2148
|
+
font-size: 0.9em;
|
|
2149
|
+
opacity: 0.9;
|
|
2150
|
+
font-weight: 500;
|
|
2151
|
+
}
|
|
2152
|
+
.ai-analyzer-description {
|
|
2153
|
+
margin-bottom: 25px;
|
|
2154
|
+
font-size: 1em;
|
|
2155
|
+
color: var(--text-color-secondary);
|
|
2156
|
+
text-align: center;
|
|
2157
|
+
max-width: 600px;
|
|
2158
|
+
margin-left: auto;
|
|
2159
|
+
margin-right: auto;
|
|
2160
|
+
}
|
|
2161
|
+
.compact-failure-list {
|
|
2162
|
+
display: flex;
|
|
2163
|
+
flex-direction: column;
|
|
2164
|
+
gap: 15px;
|
|
2165
|
+
}
|
|
2166
|
+
.compact-failure-item {
|
|
2167
|
+
background: var(--card-background-color);
|
|
2168
|
+
border: 1px solid var(--border-color);
|
|
2169
|
+
border-left: 4px solid var(--danger-color);
|
|
2170
|
+
border-radius: var(--border-radius);
|
|
2171
|
+
box-shadow: var(--box-shadow-light);
|
|
2172
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
2173
|
+
}
|
|
2174
|
+
.compact-failure-item:hover {
|
|
2175
|
+
transform: translateY(-2px);
|
|
2176
|
+
box-shadow: var(--box-shadow);
|
|
2177
|
+
}
|
|
2178
|
+
.failure-header {
|
|
2179
|
+
display: flex;
|
|
2180
|
+
justify-content: space-between;
|
|
2181
|
+
align-items: center;
|
|
2182
|
+
padding: 18px 20px;
|
|
2183
|
+
gap: 15px;
|
|
2184
|
+
}
|
|
2185
|
+
.failure-main-info {
|
|
2186
|
+
flex: 1;
|
|
2187
|
+
min-width: 0;
|
|
2188
|
+
}
|
|
2189
|
+
.failure-title {
|
|
2190
|
+
margin: 0 0 8px 0;
|
|
2191
|
+
font-size: 1.1em;
|
|
2192
|
+
font-weight: 600;
|
|
2193
|
+
color: var(--text-color);
|
|
2194
|
+
white-space: nowrap;
|
|
2195
|
+
overflow: hidden;
|
|
2196
|
+
text-overflow: ellipsis;
|
|
2197
|
+
}
|
|
2198
|
+
.failure-meta {
|
|
2199
|
+
display: flex;
|
|
2200
|
+
gap: 12px;
|
|
2201
|
+
align-items: center;
|
|
2202
|
+
}
|
|
2203
|
+
.browser-indicator, .duration-indicator {
|
|
2204
|
+
font-size: 0.85em;
|
|
2205
|
+
padding: 3px 8px;
|
|
2206
|
+
border-radius: 12px;
|
|
2207
|
+
font-weight: 500;
|
|
2208
|
+
}
|
|
2209
|
+
.browser-indicator {
|
|
2210
|
+
background: var(--info-color);
|
|
2211
|
+
color: white;
|
|
2212
|
+
}
|
|
2213
|
+
.duration-indicator {
|
|
2214
|
+
background: var(--medium-gray-color);
|
|
2215
|
+
color: var(--text-color);
|
|
2216
|
+
}
|
|
2217
|
+
.compact-ai-btn {
|
|
2218
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
2219
|
+
color: white;
|
|
2220
|
+
border: none;
|
|
2221
|
+
padding: 12px 18px;
|
|
2222
|
+
border-radius: 6px;
|
|
2223
|
+
cursor: pointer;
|
|
2224
|
+
font-weight: 600;
|
|
2225
|
+
display: flex;
|
|
2226
|
+
align-items: center;
|
|
2227
|
+
gap: 8px;
|
|
2228
|
+
transition: all 0.3s ease;
|
|
2229
|
+
white-space: nowrap;
|
|
2230
|
+
}
|
|
2231
|
+
.compact-ai-btn:hover {
|
|
2232
|
+
transform: translateY(-2px);
|
|
2233
|
+
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
|
2234
|
+
}
|
|
2235
|
+
.ai-icon {
|
|
2236
|
+
font-size: 1.2em;
|
|
2237
|
+
}
|
|
2238
|
+
.ai-text {
|
|
2239
|
+
font-size: 0.95em;
|
|
2240
|
+
}
|
|
2241
|
+
.failure-error-preview {
|
|
2242
|
+
padding: 0 20px 18px 20px;
|
|
2243
|
+
border-top: 1px solid var(--light-gray-color);
|
|
2244
|
+
}
|
|
2245
|
+
.error-snippet {
|
|
2246
|
+
background: rgba(244, 67, 54, 0.05);
|
|
2247
|
+
border: 1px solid rgba(244, 67, 54, 0.2);
|
|
2248
|
+
border-radius: 6px;
|
|
2249
|
+
padding: 12px;
|
|
2250
|
+
margin-bottom: 12px;
|
|
2251
|
+
font-family: monospace;
|
|
2252
|
+
font-size: 0.9em;
|
|
2253
|
+
color: var(--danger-color);
|
|
2254
|
+
line-height: 1.4;
|
|
2255
|
+
}
|
|
2256
|
+
.expand-error-btn {
|
|
2257
|
+
background: none;
|
|
2258
|
+
border: 1px solid var(--border-color);
|
|
2259
|
+
color: var(--text-color-secondary);
|
|
2260
|
+
padding: 6px 12px;
|
|
2261
|
+
border-radius: 4px;
|
|
2262
|
+
cursor: pointer;
|
|
2263
|
+
font-size: 0.85em;
|
|
2264
|
+
display: flex;
|
|
2265
|
+
align-items: center;
|
|
2266
|
+
gap: 6px;
|
|
2267
|
+
transition: all 0.2s ease;
|
|
2268
|
+
}
|
|
2269
|
+
.expand-error-btn:hover {
|
|
2270
|
+
background: var(--light-gray-color);
|
|
2271
|
+
border-color: var(--medium-gray-color);
|
|
2272
|
+
}
|
|
2273
|
+
.expand-icon {
|
|
2274
|
+
transition: transform 0.2s ease;
|
|
2275
|
+
font-size: 0.8em;
|
|
2276
|
+
}
|
|
2277
|
+
.expand-error-btn.expanded .expand-icon {
|
|
2278
|
+
transform: rotate(180deg);
|
|
2279
|
+
}
|
|
2280
|
+
.full-error-details {
|
|
2281
|
+
padding: 0 20px 20px 20px;
|
|
2282
|
+
border-top: 1px solid var(--light-gray-color);
|
|
2283
|
+
margin-top: 0;
|
|
2284
|
+
}
|
|
2285
|
+
.full-error-content {
|
|
2286
|
+
background: rgba(244, 67, 54, 0.05);
|
|
2287
|
+
border: 1px solid rgba(244, 67, 54, 0.2);
|
|
2288
|
+
border-radius: 6px;
|
|
2289
|
+
padding: 15px;
|
|
2290
|
+
font-family: monospace;
|
|
2291
|
+
font-size: 0.9em;
|
|
2292
|
+
color: var(--danger-color);
|
|
2293
|
+
line-height: 1.4;
|
|
2294
|
+
max-height: 300px;
|
|
2295
|
+
overflow-y: auto;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
/* Responsive adjustments for compact design */
|
|
2299
|
+
@media (max-width: 768px) {
|
|
2300
|
+
.ai-analyzer-stats {
|
|
2301
|
+
flex-direction: column;
|
|
2302
|
+
gap: 15px;
|
|
2303
|
+
text-align: center;
|
|
2304
|
+
}
|
|
2305
|
+
.failure-header {
|
|
2306
|
+
flex-direction: column;
|
|
2307
|
+
align-items: stretch;
|
|
2308
|
+
gap: 15px;
|
|
2309
|
+
}
|
|
2310
|
+
.failure-main-info {
|
|
2311
|
+
text-align: center;
|
|
2312
|
+
}
|
|
2313
|
+
.failure-meta {
|
|
2314
|
+
justify-content: center;
|
|
2315
|
+
}
|
|
2316
|
+
.compact-ai-btn {
|
|
2317
|
+
justify-content: center;
|
|
2318
|
+
padding: 12px 20px;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
@media (max-width: 480px) {
|
|
2322
|
+
.stat-item .stat-number {
|
|
2323
|
+
font-size: 1.5em;
|
|
2324
|
+
}
|
|
2325
|
+
.failure-header {
|
|
2326
|
+
padding: 15px;
|
|
2327
|
+
}
|
|
2328
|
+
.failure-error-preview, .full-error-details {
|
|
2329
|
+
padding-left: 15px;
|
|
2330
|
+
padding-right: 15px;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
|
|
1770
2334
|
@media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
|
|
1771
2335
|
@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; } }
|
|
1772
|
-
@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;} }
|
|
1773
|
-
@media (max-width: 480px) { body {font-size: 14px;} .container {padding: 15px;} .header h1 {font-size: 1.4em;} #report-logo { height: 35px; width:
|
|
2336
|
+
@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; } }
|
|
2337
|
+
@media (max-width: 480px) { body {font-size: 14px;} .container {padding: 15px;} .header h1 {font-size: 1.4em;} #report-logo { height: 35px; width: 50px; } .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;} }
|
|
1774
2338
|
</style>
|
|
1775
2339
|
</head>
|
|
1776
2340
|
<body>
|
|
1777
2341
|
<div class="container">
|
|
1778
2342
|
<header class="header">
|
|
1779
2343
|
<div class="header-title">
|
|
1780
|
-
<img id="report-logo" src="
|
|
2344
|
+
<img id="report-logo" src="https://i.postimg.cc/v817w4sg/logo.png" alt="Report Logo">
|
|
1781
2345
|
<h1>Playwright Pulse Report</h1>
|
|
1782
2346
|
</div>
|
|
1783
2347
|
<div class="run-info"><strong>Run Date:</strong> ${formatDate(
|
|
1784
|
-
|
|
1785
|
-
|
|
2348
|
+
runSummary.timestamp
|
|
2349
|
+
)}<br><strong>Total Duration:</strong> ${formatDuration(
|
|
1786
2350
|
runSummary.duration
|
|
1787
2351
|
)}</div>
|
|
1788
2352
|
</header>
|
|
@@ -1790,44 +2354,39 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1790
2354
|
<button class="tab-button active" data-tab="dashboard">Dashboard</button>
|
|
1791
2355
|
<button class="tab-button" data-tab="test-runs">Test Run Summary</button>
|
|
1792
2356
|
<button class="tab-button" data-tab="test-history">Test History</button>
|
|
1793
|
-
<button class="tab-button" data-tab="
|
|
2357
|
+
<button class="tab-button" data-tab="ai-failure-analyzer">AI Failure Analyzer</button>
|
|
1794
2358
|
</div>
|
|
1795
2359
|
<div id="dashboard" class="tab-content active">
|
|
1796
2360
|
<div class="dashboard-grid">
|
|
1797
|
-
<div class="summary-card"><h3>Total Tests</h3><div class="value">${
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
<div class="summary-card status-
|
|
1804
|
-
|
|
1805
|
-
}</div><div class="trend-percentage">${failPercentage}%</div></div>
|
|
1806
|
-
<div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
|
|
1807
|
-
runSummary.skipped || 0
|
|
1808
|
-
}</div><div class="trend-percentage">${skipPercentage}%</div></div>
|
|
2361
|
+
<div class="summary-card"><h3>Total Tests</h3><div class="value">${runSummary.totalTests
|
|
2362
|
+
}</div></div>
|
|
2363
|
+
<div class="summary-card status-passed"><h3>Passed</h3><div class="value">${runSummary.passed
|
|
2364
|
+
}</div><div class="trend-percentage">${passPercentage}%</div></div>
|
|
2365
|
+
<div class="summary-card status-failed"><h3>Failed</h3><div class="value">${runSummary.failed
|
|
2366
|
+
}</div><div class="trend-percentage">${failPercentage}%</div></div>
|
|
2367
|
+
<div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${runSummary.skipped || 0
|
|
2368
|
+
}</div><div class="trend-percentage">${skipPercentage}%</div></div>
|
|
1809
2369
|
<div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
|
|
1810
2370
|
<div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
|
|
1811
|
-
|
|
1812
|
-
|
|
2371
|
+
runSummary.duration
|
|
2372
|
+
)}</div></div>
|
|
1813
2373
|
</div>
|
|
1814
2374
|
<div class="dashboard-bottom-row">
|
|
1815
2375
|
<div style="display: grid; gap: 20px">
|
|
1816
2376
|
${generatePieChart(
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
${
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
}
|
|
2377
|
+
[
|
|
2378
|
+
{ label: "Passed", value: runSummary.passed },
|
|
2379
|
+
{ label: "Failed", value: runSummary.failed },
|
|
2380
|
+
{ label: "Skipped", value: runSummary.skipped || 0 },
|
|
2381
|
+
],
|
|
2382
|
+
400,
|
|
2383
|
+
390
|
|
2384
|
+
)}
|
|
2385
|
+
${runSummary.environment &&
|
|
2386
|
+
Object.keys(runSummary.environment).length > 0
|
|
2387
|
+
? generateEnvironmentDashboard(runSummary.environment)
|
|
2388
|
+
: '<div class="no-data">Environment data not available.</div>'
|
|
2389
|
+
}
|
|
1831
2390
|
</div>
|
|
1832
2391
|
${generateSuitesWidget(suitesData)}
|
|
1833
2392
|
</div>
|
|
@@ -1837,17 +2396,17 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1837
2396
|
<input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
|
|
1838
2397
|
<select id="filter-status"><option value="">All Statuses</option><option value="passed">Passed</option><option value="failed">Failed</option><option value="skipped">Skipped</option></select>
|
|
1839
2398
|
<select id="filter-browser"><option value="">All Browsers</option>${Array.from(
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2399
|
+
new Set(
|
|
2400
|
+
(results || []).map((test) => test.browser || "unknown")
|
|
2401
|
+
)
|
|
2402
|
+
)
|
|
2403
|
+
.map(
|
|
2404
|
+
(browser) =>
|
|
2405
|
+
`<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
|
|
2406
|
+
browser
|
|
2407
|
+
)}</option>`
|
|
2408
|
+
)
|
|
2409
|
+
.join("")}</select>
|
|
1851
2410
|
<button id="expand-all-tests">Expand All</button> <button id="collapse-all-tests">Collapse All</button> <button id="clear-run-summary-filters" class="clear-filters-btn">Clear Filters</button>
|
|
1852
2411
|
</div>
|
|
1853
2412
|
<div class="test-cases-list">${generateTestCasesHTML()}</div>
|
|
@@ -1856,35 +2415,37 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1856
2415
|
<h2 class="tab-main-title">Execution Trends</h2>
|
|
1857
2416
|
<div class="trend-charts-row">
|
|
1858
2417
|
<div class="trend-chart"><h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
|
|
1859
|
-
${
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
}
|
|
2418
|
+
${trendData && trendData.overall && trendData.overall.length > 0
|
|
2419
|
+
? generateTestTrendsChart(trendData)
|
|
2420
|
+
: '<div class="no-data">Overall trend data not available for test counts.</div>'
|
|
2421
|
+
}
|
|
1864
2422
|
</div>
|
|
1865
2423
|
<div class="trend-chart"><h3 class="chart-title-header">Execution Duration Trends</h3>
|
|
1866
|
-
${
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
}
|
|
2424
|
+
${trendData && trendData.overall && trendData.overall.length > 0
|
|
2425
|
+
? generateDurationTrendChart(trendData)
|
|
2426
|
+
: '<div class="no-data">Overall trend data not available for durations.</div>'
|
|
2427
|
+
}
|
|
1871
2428
|
</div>
|
|
1872
2429
|
</div>
|
|
2430
|
+
<h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
|
|
2431
|
+
<div class="trend-charts-row">
|
|
2432
|
+
<div class="trend-chart">
|
|
2433
|
+
${generateWorkerDistributionChart(results)}
|
|
2434
|
+
</div>
|
|
2435
|
+
</div>
|
|
1873
2436
|
<h2 class="tab-main-title">Individual Test History</h2>
|
|
1874
|
-
${
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
}
|
|
2437
|
+
${trendData &&
|
|
2438
|
+
trendData.testRuns &&
|
|
2439
|
+
Object.keys(trendData.testRuns).length > 0
|
|
2440
|
+
? generateTestHistoryContent(trendData)
|
|
2441
|
+
: '<div class="no-data">Individual test history data not available.</div>'
|
|
2442
|
+
}
|
|
1881
2443
|
</div>
|
|
1882
|
-
<div id="
|
|
1883
|
-
|
|
2444
|
+
<div id="ai-failure-analyzer" class="tab-content">
|
|
2445
|
+
${generateAIFailureAnalyzerTab(results)}
|
|
1884
2446
|
</div>
|
|
1885
2447
|
<footer style="padding: 0.5rem; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); text-align: center; font-family: 'Segoe UI', system-ui, sans-serif;">
|
|
1886
2448
|
<div style="display: inline-flex; align-items: center; gap: 0.5rem; color: #333; font-size: 0.9rem; font-weight: 600; letter-spacing: 0.5px;">
|
|
1887
|
-
<img width="48" height="48" src="https://img.icons8.com/emoji/48/index-pointing-at-the-viewer-light-skin-tone-emoji.png" alt="index-pointing-at-the-viewer-light-skin-tone-emoji"/>
|
|
1888
2449
|
<span>Created by</span>
|
|
1889
2450
|
<a href="https://github.com/Arghajit47" target="_blank" rel="noopener noreferrer" style="color: #7737BF; font-weight: 700; font-style: italic; text-decoration: none; transition: all 0.2s ease;" onmouseover="this.style.color='#BF5C37'" onmouseout="this.style.color='#7737BF'">Arghajit Singha</a>
|
|
1890
2451
|
</div>
|
|
@@ -1899,6 +2460,149 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1899
2460
|
return (ms / 1000).toFixed(1) + "s";
|
|
1900
2461
|
}
|
|
1901
2462
|
}
|
|
2463
|
+
function copyLogContent(elementId, button) {
|
|
2464
|
+
const logElement = document.getElementById(elementId);
|
|
2465
|
+
if (!logElement) {
|
|
2466
|
+
console.error('Could not find log element with ID:', elementId);
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
navigator.clipboard.writeText(logElement.innerText).then(() => {
|
|
2470
|
+
button.textContent = 'Copied!';
|
|
2471
|
+
setTimeout(() => { button.textContent = 'Copy'; }, 2000);
|
|
2472
|
+
}).catch(err => {
|
|
2473
|
+
console.error('Failed to copy log content:', err);
|
|
2474
|
+
button.textContent = 'Failed';
|
|
2475
|
+
setTimeout(() => { button.textContent = 'Copy'; }, 2000);
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// --- AI Failure Analyzer Functions ---
|
|
2480
|
+
function getAIFix(button) {
|
|
2481
|
+
const modal = document.getElementById('ai-fix-modal');
|
|
2482
|
+
const modalContent = document.getElementById('ai-fix-modal-content');
|
|
2483
|
+
const modalTitle = document.getElementById('ai-fix-modal-title');
|
|
2484
|
+
|
|
2485
|
+
modal.style.display = 'flex';
|
|
2486
|
+
modalTitle.textContent = 'Analyzing...';
|
|
2487
|
+
modalContent.innerHTML = '<div class="ai-loader"></div>';
|
|
2488
|
+
|
|
2489
|
+
try {
|
|
2490
|
+
const testJson = button.dataset.testJson;
|
|
2491
|
+
const test = JSON.parse(atob(testJson));
|
|
2492
|
+
|
|
2493
|
+
const testName = test.name || 'Unknown Test';
|
|
2494
|
+
const failureLogsAndErrors = [
|
|
2495
|
+
'Error Message:',
|
|
2496
|
+
test.errorMessage || 'Not available.',
|
|
2497
|
+
'\\n\\n--- stdout ---',
|
|
2498
|
+
(test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
|
|
2499
|
+
'\\n\\n--- stderr ---',
|
|
2500
|
+
(test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
|
|
2501
|
+
].join('\\n');
|
|
2502
|
+
const codeSnippet = test.snippet || '';
|
|
2503
|
+
|
|
2504
|
+
const shortTestName = testName.split(' > ').pop();
|
|
2505
|
+
modalTitle.textContent = \`Analysis for: \${shortTestName}\`;
|
|
2506
|
+
|
|
2507
|
+
const apiUrl = 'https://ai-test-analyser.netlify.app/api/analyze';
|
|
2508
|
+
fetch(apiUrl, {
|
|
2509
|
+
method: 'POST',
|
|
2510
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2511
|
+
body: JSON.stringify({
|
|
2512
|
+
testName: testName,
|
|
2513
|
+
failureLogsAndErrors: failureLogsAndErrors,
|
|
2514
|
+
codeSnippet: codeSnippet,
|
|
2515
|
+
}),
|
|
2516
|
+
})
|
|
2517
|
+
.then(response => {
|
|
2518
|
+
if (!response.ok) {
|
|
2519
|
+
return response.text().then(text => {
|
|
2520
|
+
throw new Error(\`API request failed with status \${response.status}: \${text || response.statusText}\`);
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
return response.text();
|
|
2524
|
+
})
|
|
2525
|
+
.then(text => {
|
|
2526
|
+
if (!text) {
|
|
2527
|
+
throw new Error("The AI analyzer returned an empty response. This might happen during high load or if the request was blocked. Please try again in a moment.");
|
|
2528
|
+
}
|
|
2529
|
+
try {
|
|
2530
|
+
return JSON.parse(text);
|
|
2531
|
+
} catch (e) {
|
|
2532
|
+
console.error("Failed to parse JSON:", text);
|
|
2533
|
+
throw new Error(\`The AI analyzer returned an invalid response. \${e.message}\`);
|
|
2534
|
+
}
|
|
2535
|
+
})
|
|
2536
|
+
.then(data => {
|
|
2537
|
+
// Helper function to prevent XSS by escaping HTML characters
|
|
2538
|
+
const escapeHtml = (unsafe) => {
|
|
2539
|
+
if (typeof unsafe !== 'string') return '';
|
|
2540
|
+
return unsafe
|
|
2541
|
+
.replace(/&/g, "&")
|
|
2542
|
+
.replace(/</g, "<")
|
|
2543
|
+
.replace(/>/g, ">")
|
|
2544
|
+
.replace(/"/g, """)
|
|
2545
|
+
.replace(/'/g, "'");
|
|
2546
|
+
};
|
|
2547
|
+
|
|
2548
|
+
// Build the "Analysis" part from the 'rootCause' field
|
|
2549
|
+
const analysisHtml = \`<h4>Analysis</h4><p>\${escapeHtml(data.rootCause) || 'No analysis provided.'}</p>\`;
|
|
2550
|
+
|
|
2551
|
+
// Build the "Suggestions" part by iterating through the 'suggestedFixes' array
|
|
2552
|
+
let suggestionsHtml = '<h4>Suggestions</h4>';
|
|
2553
|
+
if (data.suggestedFixes && data.suggestedFixes.length > 0) {
|
|
2554
|
+
suggestionsHtml += '<div class="suggestions-list" style="margin-top: 15px;">';
|
|
2555
|
+
data.suggestedFixes.forEach(fix => {
|
|
2556
|
+
suggestionsHtml += \`
|
|
2557
|
+
<div class="suggestion-item" style="margin-bottom: 22px; border-left: 3px solid var(--accent-color-alt); padding-left: 15px;">
|
|
2558
|
+
<p style="margin: 0 0 8px 0; font-weight: 500;">\${escapeHtml(fix.description)}</p>
|
|
2559
|
+
\${fix.codeSnippet ? \`<div class="code-section"><pre><code>\${escapeHtml(fix.codeSnippet)}</code></pre></div>\` : ''}
|
|
2560
|
+
</div>
|
|
2561
|
+
\`;
|
|
2562
|
+
});
|
|
2563
|
+
suggestionsHtml += '</div>';
|
|
2564
|
+
} else {
|
|
2565
|
+
// Fallback if there are no suggestions
|
|
2566
|
+
suggestionsHtml += \`<div class="code-section"><pre><code>No suggestion provided.</code></pre></div>\`;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
// Combine both parts and set the modal content
|
|
2570
|
+
modalContent.innerHTML = analysisHtml + suggestionsHtml;
|
|
2571
|
+
})
|
|
2572
|
+
.catch(err => {
|
|
2573
|
+
console.error('AI Fix Error:', err);
|
|
2574
|
+
modalContent.innerHTML = \`<div class="test-error-summary"><strong>Error:</strong> Failed to get AI analysis. Please check the console for details. <br><br> \${err.message}</div>\`;
|
|
2575
|
+
});
|
|
2576
|
+
|
|
2577
|
+
} catch (e) {
|
|
2578
|
+
console.error('Error processing test data for AI Fix:', e);
|
|
2579
|
+
modalTitle.textContent = 'Error';
|
|
2580
|
+
modalContent.innerHTML = \`<div class="test-error-summary">Could not process test data. Is it formatted correctly?</div>\`;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
|
|
2585
|
+
function closeAiModal() {
|
|
2586
|
+
const modal = document.getElementById('ai-fix-modal');
|
|
2587
|
+
if(modal) modal.style.display = 'none';
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
function toggleErrorDetails(button) {
|
|
2591
|
+
const errorDetails = button.closest('.compact-failure-item').querySelector('.full-error-details');
|
|
2592
|
+
const expandText = button.querySelector('.expand-text');
|
|
2593
|
+
const expandIcon = button.querySelector('.expand-icon');
|
|
2594
|
+
|
|
2595
|
+
if (errorDetails.style.display === 'none' || !errorDetails.style.display) {
|
|
2596
|
+
errorDetails.style.display = 'block';
|
|
2597
|
+
expandText.textContent = 'Hide Full Error';
|
|
2598
|
+
button.classList.add('expanded');
|
|
2599
|
+
} else {
|
|
2600
|
+
errorDetails.style.display = 'none';
|
|
2601
|
+
expandText.textContent = 'Show Full Error';
|
|
2602
|
+
button.classList.remove('expanded');
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
|
|
1902
2606
|
function initializeReportInteractivity() {
|
|
1903
2607
|
const tabButtons = document.querySelectorAll('.tab-button');
|
|
1904
2608
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
@@ -1911,9 +2615,9 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1911
2615
|
const activeContent = document.getElementById(tabId);
|
|
1912
2616
|
if (activeContent) {
|
|
1913
2617
|
activeContent.classList.add('active');
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2618
|
+
if ('IntersectionObserver' in window) {
|
|
2619
|
+
// Handled by observer
|
|
2620
|
+
}
|
|
1917
2621
|
}
|
|
1918
2622
|
});
|
|
1919
2623
|
});
|
|
@@ -1999,19 +2703,13 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1999
2703
|
if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
|
|
2000
2704
|
if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
|
|
2001
2705
|
// --- Intersection Observer for Lazy Loading ---
|
|
2002
|
-
const lazyLoadElements = document.querySelectorAll('.lazy-load-chart
|
|
2706
|
+
const lazyLoadElements = document.querySelectorAll('.lazy-load-chart');
|
|
2003
2707
|
if ('IntersectionObserver' in window) {
|
|
2004
2708
|
let lazyObserver = new IntersectionObserver((entries, observer) => {
|
|
2005
2709
|
entries.forEach(entry => {
|
|
2006
2710
|
if (entry.isIntersecting) {
|
|
2007
2711
|
const element = entry.target;
|
|
2008
|
-
if (element.classList.contains('lazy-load-
|
|
2009
|
-
if (element.dataset.src) {
|
|
2010
|
-
element.src = element.dataset.src;
|
|
2011
|
-
element.removeAttribute('data-src'); // Optional: remove data-src after loading
|
|
2012
|
-
console.log('Lazy loaded iframe:', element.title || 'Untitled Iframe');
|
|
2013
|
-
}
|
|
2014
|
-
} else if (element.classList.contains('lazy-load-chart')) {
|
|
2712
|
+
if (element.classList.contains('lazy-load-chart')) {
|
|
2015
2713
|
const renderFunctionName = element.dataset.renderFunctionName;
|
|
2016
2714
|
if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
|
|
2017
2715
|
try {
|
|
@@ -2038,12 +2736,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2038
2736
|
} else { // Fallback for browsers without IntersectionObserver
|
|
2039
2737
|
console.warn("IntersectionObserver not supported. Loading all items immediately.");
|
|
2040
2738
|
lazyLoadElements.forEach(element => {
|
|
2041
|
-
if (element.classList.contains('lazy-load-
|
|
2042
|
-
if (element.dataset.src) {
|
|
2043
|
-
element.src = element.dataset.src;
|
|
2044
|
-
element.removeAttribute('data-src');
|
|
2045
|
-
}
|
|
2046
|
-
} else if (element.classList.contains('lazy-load-chart')) {
|
|
2739
|
+
if (element.classList.contains('lazy-load-chart')) {
|
|
2047
2740
|
const renderFunctionName = element.dataset.renderFunctionName;
|
|
2048
2741
|
if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
|
|
2049
2742
|
try {
|
|
@@ -2061,9 +2754,9 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2061
2754
|
|
|
2062
2755
|
function copyErrorToClipboard(button) {
|
|
2063
2756
|
// 1. Find the main error container, which should always be present.
|
|
2064
|
-
const errorContainer = button.closest('.
|
|
2757
|
+
const errorContainer = button.closest('.test-error-summary');
|
|
2065
2758
|
if (!errorContainer) {
|
|
2066
|
-
console.error("Could not find '.
|
|
2759
|
+
console.error("Could not find '.test-error-summary' container. The report's HTML structure might have changed.");
|
|
2067
2760
|
return;
|
|
2068
2761
|
}
|
|
2069
2762
|
|
|
@@ -2338,4 +3031,4 @@ main().catch((err) => {
|
|
|
2338
3031
|
);
|
|
2339
3032
|
console.error(err.stack);
|
|
2340
3033
|
process.exit(1);
|
|
2341
|
-
});
|
|
3034
|
+
});
|