@arghajit/dummy 0.1.0-beta-26 → 0.1.0-beta-28
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.
|
@@ -158,22 +158,18 @@ function formatPlaywrightError(error) {
|
|
|
158
158
|
return convertPlaywrightErrorToHTML(commandOutput);
|
|
159
159
|
}
|
|
160
160
|
function convertPlaywrightErrorToHTML(str) {
|
|
161
|
-
|
|
162
|
-
str
|
|
163
|
-
// Convert leading spaces to and tabs to
|
|
161
|
+
if (!str) return "";
|
|
162
|
+
return str
|
|
164
163
|
.replace(/^(\s+)/gm, (match) =>
|
|
165
|
-
match.replace(/ /g, "
|
|
164
|
+
match.replace(/ /g, " ").replace(/\t/g, " ")
|
|
166
165
|
)
|
|
167
|
-
// Color and style replacements
|
|
168
166
|
.replace(/<red>/g, '<span style="color: red;">')
|
|
169
167
|
.replace(/<green>/g, '<span style="color: green;">')
|
|
170
168
|
.replace(/<dim>/g, '<span style="opacity: 0.6;">')
|
|
171
|
-
.replace(/<intensity>/g, '<span style="font-weight: bold;">')
|
|
169
|
+
.replace(/<intensity>/g, '<span style="font-weight: bold;">')
|
|
172
170
|
.replace(/<\/color>/g, "</span>")
|
|
173
171
|
.replace(/<\/intensity>/g, "</span>")
|
|
174
|
-
|
|
175
|
-
.replace(/\n/g, "<br>")
|
|
176
|
-
);
|
|
172
|
+
.replace(/\n/g, "<br>");
|
|
177
173
|
}
|
|
178
174
|
function formatDuration(ms, options = {}) {
|
|
179
175
|
const {
|
|
@@ -214,19 +210,12 @@ function formatDuration(ms, options = {}) {
|
|
|
214
210
|
|
|
215
211
|
const totalRawSeconds = numMs / MS_PER_SECOND;
|
|
216
212
|
|
|
217
|
-
// Decision: Are we going to display hours or minutes?
|
|
218
|
-
// This happens if the duration is inherently >= 1 minute OR
|
|
219
|
-
// if it's < 1 minute but ceiling the seconds makes it >= 1 minute.
|
|
220
213
|
if (
|
|
221
214
|
totalRawSeconds < SECONDS_PER_MINUTE &&
|
|
222
215
|
Math.ceil(totalRawSeconds) < SECONDS_PER_MINUTE
|
|
223
216
|
) {
|
|
224
|
-
// Strictly seconds-only display, use precision.
|
|
225
217
|
return `${totalRawSeconds.toFixed(validPrecision)}s`;
|
|
226
218
|
} else {
|
|
227
|
-
// Display will include minutes and/or hours, or seconds round up to a minute.
|
|
228
|
-
// Seconds part should be an integer (ceiling).
|
|
229
|
-
// Round the total milliseconds UP to the nearest full second.
|
|
230
219
|
const totalMsRoundedUpToSecond =
|
|
231
220
|
Math.ceil(numMs / MS_PER_SECOND) * MS_PER_SECOND;
|
|
232
221
|
|
|
@@ -238,21 +227,15 @@ function formatDuration(ms, options = {}) {
|
|
|
238
227
|
const m = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_MINUTE));
|
|
239
228
|
remainingMs %= MS_PER_SECOND * SECONDS_PER_MINUTE;
|
|
240
229
|
|
|
241
|
-
const s = Math.floor(remainingMs / MS_PER_SECOND);
|
|
230
|
+
const s = Math.floor(remainingMs / MS_PER_SECOND);
|
|
242
231
|
|
|
243
232
|
const parts = [];
|
|
244
233
|
if (h > 0) {
|
|
245
234
|
parts.push(`${h}h`);
|
|
246
235
|
}
|
|
247
|
-
|
|
248
|
-
// Show minutes if:
|
|
249
|
-
// - hours are present (e.g., "1h 0m 5s")
|
|
250
|
-
// - OR minutes themselves are > 0 (e.g., "5m 10s")
|
|
251
|
-
// - OR the original duration was >= 1 minute (ensures "1m 0s" for 60000ms)
|
|
252
236
|
if (h > 0 || m > 0 || numMs >= MS_PER_SECOND * SECONDS_PER_MINUTE) {
|
|
253
237
|
parts.push(`${m}m`);
|
|
254
238
|
}
|
|
255
|
-
|
|
256
239
|
parts.push(`${s}s`);
|
|
257
240
|
|
|
258
241
|
return parts.join(" ");
|
|
@@ -1023,339 +1006,79 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
1023
1006
|
</div>
|
|
1024
1007
|
`;
|
|
1025
1008
|
}
|
|
1026
|
-
function
|
|
1027
|
-
if (
|
|
1028
|
-
|
|
1029
|
-
!trendData.testRuns ||
|
|
1030
|
-
Object.keys(trendData.testRuns).length === 0
|
|
1031
|
-
) {
|
|
1032
|
-
return '<div class="no-data">No historical test data available.</div>';
|
|
1009
|
+
function generateWorkerDistributionChart(results) {
|
|
1010
|
+
if (!results || results.length === 0) {
|
|
1011
|
+
return '<div class="no-data">No test results data available to display worker distribution.</div>';
|
|
1033
1012
|
}
|
|
1034
1013
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1014
|
+
// 1. Sort results by startTime to ensure chronological order
|
|
1015
|
+
const sortedResults = [...results].sort((a, b) => {
|
|
1016
|
+
const timeA = a.startTime ? new Date(a.startTime).getTime() : 0;
|
|
1017
|
+
const timeB = b.startTime ? new Date(b.startTime).getTime() : 0;
|
|
1018
|
+
return timeA - timeB;
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
const workerData = sortedResults.reduce((acc, test) => {
|
|
1022
|
+
const workerId =
|
|
1023
|
+
typeof test.workerId !== "undefined" ? test.workerId : "N/A";
|
|
1024
|
+
if (!acc[workerId]) {
|
|
1025
|
+
acc[workerId] = { passed: 0, failed: 0, skipped: 0, tests: [] };
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const status = String(test.status).toLowerCase();
|
|
1029
|
+
if (status === "passed" || status === "failed" || status === "skipped") {
|
|
1030
|
+
acc[workerId][status]++;
|
|
1045
1031
|
}
|
|
1032
|
+
|
|
1033
|
+
const testTitleParts = test.name.split(" > ");
|
|
1034
|
+
const testTitle =
|
|
1035
|
+
testTitleParts[testTitleParts.length - 1] || "Unnamed Test";
|
|
1036
|
+
// Store both name and status for each test
|
|
1037
|
+
acc[workerId].tests.push({ name: testTitle, status: status });
|
|
1038
|
+
|
|
1039
|
+
return acc;
|
|
1040
|
+
}, {});
|
|
1041
|
+
|
|
1042
|
+
const workerIds = Object.keys(workerData).sort((a, b) => {
|
|
1043
|
+
if (a === "N/A") return 1;
|
|
1044
|
+
if (b === "N/A") return -1;
|
|
1045
|
+
return parseInt(a, 10) - parseInt(b, 10);
|
|
1046
1046
|
});
|
|
1047
1047
|
|
|
1048
|
-
if (
|
|
1049
|
-
return '<div class="no-data">
|
|
1048
|
+
if (workerIds.length === 0) {
|
|
1049
|
+
return '<div class="no-data">Could not determine worker distribution from test data.</div>';
|
|
1050
1050
|
}
|
|
1051
1051
|
|
|
1052
|
-
const
|
|
1053
|
-
.
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
(t) => t && t.testName === fullTestName
|
|
1061
|
-
);
|
|
1062
|
-
if (testRunForThisOverallRun) {
|
|
1063
|
-
history.push({
|
|
1064
|
-
runId: overallRun.runId || index + 1,
|
|
1065
|
-
status: testRunForThisOverallRun.status || "unknown",
|
|
1066
|
-
duration: testRunForThisOverallRun.duration || 0,
|
|
1067
|
-
timestamp:
|
|
1068
|
-
testRunForThisOverallRun.timestamp ||
|
|
1069
|
-
overallRun.timestamp ||
|
|
1070
|
-
new Date(),
|
|
1071
|
-
});
|
|
1072
|
-
}
|
|
1073
|
-
});
|
|
1074
|
-
return { fullTestName, testTitle, history };
|
|
1075
|
-
})
|
|
1076
|
-
.filter((item) => item.history.length > 0);
|
|
1052
|
+
const chartId = `workerDistChart-${Date.now()}-${Math.random()
|
|
1053
|
+
.toString(36)
|
|
1054
|
+
.substring(2, 7)}`;
|
|
1055
|
+
const renderFunctionName = `renderWorkerDistChart_${chartId.replace(
|
|
1056
|
+
/-/g,
|
|
1057
|
+
"_"
|
|
1058
|
+
)}`;
|
|
1059
|
+
const modalJsNamespace = `modal_funcs_${chartId.replace(/-/g, "_")}`;
|
|
1077
1060
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
<div class="filters" style="border-color: black; border-style: groove;">
|
|
1081
|
-
<input type="text" id="history-filter-name" placeholder="Search by test title..." style="border-color: black; border-style: outset;">
|
|
1082
|
-
<select id="history-filter-status">
|
|
1083
|
-
<option value="">All Statuses</option>
|
|
1084
|
-
<option value="passed">Passed</option>
|
|
1085
|
-
<option value="failed">Failed</option>
|
|
1086
|
-
<option value="skipped">Skipped</option>
|
|
1087
|
-
</select>
|
|
1088
|
-
<button id="clear-history-filters" class="clear-filters-btn">Clear Filters</button>
|
|
1089
|
-
</div>
|
|
1090
|
-
|
|
1091
|
-
<div class="test-history-grid">
|
|
1092
|
-
${testHistory
|
|
1093
|
-
.map((test) => {
|
|
1094
|
-
const latestRun =
|
|
1095
|
-
test.history.length > 0
|
|
1096
|
-
? test.history[test.history.length - 1]
|
|
1097
|
-
: { status: "unknown" };
|
|
1098
|
-
return `
|
|
1099
|
-
<div class="test-history-card" data-test-name="${sanitizeHTML(
|
|
1100
|
-
test.testTitle.toLowerCase()
|
|
1101
|
-
)}" data-latest-status="${latestRun.status}">
|
|
1102
|
-
<div class="test-history-header">
|
|
1103
|
-
<p title="${sanitizeHTML(test.testTitle)}">${capitalize(
|
|
1104
|
-
sanitizeHTML(test.testTitle)
|
|
1105
|
-
)}</p>
|
|
1106
|
-
<span class="status-badge ${getStatusClass(latestRun.status)}">
|
|
1107
|
-
${String(latestRun.status).toUpperCase()}
|
|
1108
|
-
</span>
|
|
1109
|
-
</div>
|
|
1110
|
-
<div class="test-history-trend">
|
|
1111
|
-
${generateTestHistoryChart(test.history)}
|
|
1112
|
-
</div>
|
|
1113
|
-
<details class="test-history-details-collapsible">
|
|
1114
|
-
<summary>Show Run Details (${test.history.length})</summary>
|
|
1115
|
-
<div class="test-history-details">
|
|
1116
|
-
<table>
|
|
1117
|
-
<thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Date</th></tr></thead>
|
|
1118
|
-
<tbody>
|
|
1119
|
-
${test.history
|
|
1120
|
-
.slice()
|
|
1121
|
-
.reverse()
|
|
1122
|
-
.map(
|
|
1123
|
-
(run) => `
|
|
1124
|
-
<tr>
|
|
1125
|
-
<td>${run.runId}</td>
|
|
1126
|
-
<td><span class="status-badge-small ${getStatusClass(
|
|
1127
|
-
run.status
|
|
1128
|
-
)}">${String(run.status).toUpperCase()}</span></td>
|
|
1129
|
-
<td>${formatDuration(run.duration)}</td>
|
|
1130
|
-
<td>${formatDate(run.timestamp)}</td>
|
|
1131
|
-
</tr>`
|
|
1132
|
-
)
|
|
1133
|
-
.join("")}
|
|
1134
|
-
</tbody>
|
|
1135
|
-
</table>
|
|
1136
|
-
</div>
|
|
1137
|
-
</details>
|
|
1138
|
-
</div>`;
|
|
1139
|
-
})
|
|
1140
|
-
.join("")}
|
|
1141
|
-
</div>
|
|
1142
|
-
</div>
|
|
1143
|
-
`;
|
|
1144
|
-
}
|
|
1145
|
-
function getStatusClass(status) {
|
|
1146
|
-
switch (String(status).toLowerCase()) {
|
|
1147
|
-
case "passed":
|
|
1148
|
-
return "status-passed";
|
|
1149
|
-
case "failed":
|
|
1150
|
-
return "status-failed";
|
|
1151
|
-
case "skipped":
|
|
1152
|
-
return "status-skipped";
|
|
1153
|
-
default:
|
|
1154
|
-
return "status-unknown";
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
function getStatusIcon(status) {
|
|
1158
|
-
switch (String(status).toLowerCase()) {
|
|
1159
|
-
case "passed":
|
|
1160
|
-
return "✅";
|
|
1161
|
-
case "failed":
|
|
1162
|
-
return "❌";
|
|
1163
|
-
case "skipped":
|
|
1164
|
-
return "⏭️";
|
|
1165
|
-
default:
|
|
1166
|
-
return "❓";
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
function getSuitesData(results) {
|
|
1170
|
-
const suitesMap = new Map();
|
|
1171
|
-
if (!results || results.length === 0) return [];
|
|
1061
|
+
// The categories now just need the name for the axis labels
|
|
1062
|
+
const categories = workerIds.map((id) => `Worker ${id}`);
|
|
1172
1063
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
} else if (suiteParts.length > 1) {
|
|
1180
|
-
suiteNameCandidate = suiteParts[0]
|
|
1181
|
-
.split(path.sep)
|
|
1182
|
-
.pop()
|
|
1183
|
-
.replace(/\.(spec|test)\.(ts|js|mjs|cjs)$/, "");
|
|
1184
|
-
} else {
|
|
1185
|
-
suiteNameCandidate = test.name
|
|
1186
|
-
.split(path.sep)
|
|
1187
|
-
.pop()
|
|
1188
|
-
.replace(/\.(spec|test)\.(ts|js|mjs|cjs)$/, "");
|
|
1189
|
-
}
|
|
1190
|
-
const suiteName = suiteNameCandidate;
|
|
1191
|
-
const key = `${suiteName}|${browser}`;
|
|
1064
|
+
// We pass the full data separately to the script
|
|
1065
|
+
const fullWorkerData = workerIds.map((id) => ({
|
|
1066
|
+
id: id,
|
|
1067
|
+
name: `Worker ${id}`,
|
|
1068
|
+
tests: workerData[id].tests,
|
|
1069
|
+
}));
|
|
1192
1070
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
name: suiteName,
|
|
1197
|
-
browser: browser,
|
|
1198
|
-
passed: 0,
|
|
1199
|
-
failed: 0,
|
|
1200
|
-
skipped: 0,
|
|
1201
|
-
count: 0,
|
|
1202
|
-
statusOverall: "passed",
|
|
1203
|
-
});
|
|
1204
|
-
}
|
|
1205
|
-
const suite = suitesMap.get(key);
|
|
1206
|
-
suite.count++;
|
|
1207
|
-
const currentStatus = String(test.status).toLowerCase();
|
|
1208
|
-
if (currentStatus && suite[currentStatus] !== undefined) {
|
|
1209
|
-
suite[currentStatus]++;
|
|
1210
|
-
}
|
|
1211
|
-
if (currentStatus === "failed") suite.statusOverall = "failed";
|
|
1212
|
-
else if (currentStatus === "skipped" && suite.statusOverall !== "failed")
|
|
1213
|
-
suite.statusOverall = "skipped";
|
|
1214
|
-
});
|
|
1215
|
-
return Array.from(suitesMap.values());
|
|
1216
|
-
}
|
|
1217
|
-
function getAttachmentIcon(contentType) {
|
|
1218
|
-
if (!contentType) return "📎"; // Handle undefined/null
|
|
1071
|
+
const passedData = workerIds.map((id) => workerData[id].passed);
|
|
1072
|
+
const failedData = workerIds.map((id) => workerData[id].failed);
|
|
1073
|
+
const skippedData = workerIds.map((id) => workerData[id].skipped);
|
|
1219
1074
|
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
if (normalizedType.startsWith("text/")) return "📝";
|
|
1228
|
-
return "📎";
|
|
1229
|
-
}
|
|
1230
|
-
function generateSuitesWidget(suitesData) {
|
|
1231
|
-
if (!suitesData || suitesData.length === 0) {
|
|
1232
|
-
return `<div class="suites-widget"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
|
|
1233
|
-
}
|
|
1234
|
-
return `
|
|
1235
|
-
<div class="suites-widget">
|
|
1236
|
-
<div class="suites-header">
|
|
1237
|
-
<h2>Test Suites</h2>
|
|
1238
|
-
<span class="summary-badge">${
|
|
1239
|
-
suitesData.length
|
|
1240
|
-
} suites • ${suitesData.reduce(
|
|
1241
|
-
(sum, suite) => sum + suite.count,
|
|
1242
|
-
0
|
|
1243
|
-
)} tests</span>
|
|
1244
|
-
</div>
|
|
1245
|
-
<div class="suites-grid">
|
|
1246
|
-
${suitesData
|
|
1247
|
-
.map(
|
|
1248
|
-
(suite) => `
|
|
1249
|
-
<div class="suite-card status-${suite.statusOverall}">
|
|
1250
|
-
<div class="suite-card-header">
|
|
1251
|
-
<h3 class="suite-name" title="${sanitizeHTML(
|
|
1252
|
-
suite.name
|
|
1253
|
-
)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
|
|
1254
|
-
</div>
|
|
1255
|
-
<div>🖥️ <span class="browser-tag">${sanitizeHTML(
|
|
1256
|
-
suite.browser
|
|
1257
|
-
)}</span></div>
|
|
1258
|
-
<div class="suite-card-body">
|
|
1259
|
-
<span class="test-count">${suite.count} test${
|
|
1260
|
-
suite.count !== 1 ? "s" : ""
|
|
1261
|
-
}</span>
|
|
1262
|
-
<div class="suite-stats">
|
|
1263
|
-
${
|
|
1264
|
-
suite.passed > 0
|
|
1265
|
-
? `<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>`
|
|
1266
|
-
: ""
|
|
1267
|
-
}
|
|
1268
|
-
${
|
|
1269
|
-
suite.failed > 0
|
|
1270
|
-
? `<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>`
|
|
1271
|
-
: ""
|
|
1272
|
-
}
|
|
1273
|
-
${
|
|
1274
|
-
suite.skipped > 0
|
|
1275
|
-
? `<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>`
|
|
1276
|
-
: ""
|
|
1277
|
-
}
|
|
1278
|
-
</div>
|
|
1279
|
-
</div>
|
|
1280
|
-
</div>`
|
|
1281
|
-
)
|
|
1282
|
-
.join("")}
|
|
1283
|
-
</div>
|
|
1284
|
-
</div>`;
|
|
1285
|
-
}
|
|
1286
|
-
function generateWorkerDistributionChart(results) {
|
|
1287
|
-
if (!results || results.length === 0) {
|
|
1288
|
-
return '<div class="no-data">No test results data available to display worker distribution.</div>';
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
// 1. Sort results by startTime to ensure chronological order
|
|
1292
|
-
const sortedResults = [...results].sort((a, b) => {
|
|
1293
|
-
const timeA = a.startTime ? new Date(a.startTime).getTime() : 0;
|
|
1294
|
-
const timeB = b.startTime ? new Date(b.startTime).getTime() : 0;
|
|
1295
|
-
return timeA - timeB;
|
|
1296
|
-
});
|
|
1297
|
-
|
|
1298
|
-
const workerData = sortedResults.reduce((acc, test) => {
|
|
1299
|
-
const workerId =
|
|
1300
|
-
typeof test.workerId !== "undefined" ? test.workerId : "N/A";
|
|
1301
|
-
if (!acc[workerId]) {
|
|
1302
|
-
acc[workerId] = { passed: 0, failed: 0, skipped: 0, tests: [] };
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
const status = String(test.status).toLowerCase();
|
|
1306
|
-
if (status === "passed" || status === "failed" || status === "skipped") {
|
|
1307
|
-
acc[workerId][status]++;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
const testTitleParts = test.name.split(" > ");
|
|
1311
|
-
const testTitle =
|
|
1312
|
-
testTitleParts[testTitleParts.length - 1] || "Unnamed Test";
|
|
1313
|
-
// Store both name and status for each test
|
|
1314
|
-
acc[workerId].tests.push({ name: testTitle, status: status });
|
|
1315
|
-
|
|
1316
|
-
return acc;
|
|
1317
|
-
}, {});
|
|
1318
|
-
|
|
1319
|
-
const workerIds = Object.keys(workerData).sort((a, b) => {
|
|
1320
|
-
if (a === "N/A") return 1;
|
|
1321
|
-
if (b === "N/A") return -1;
|
|
1322
|
-
return parseInt(a, 10) - parseInt(b, 10);
|
|
1323
|
-
});
|
|
1324
|
-
|
|
1325
|
-
if (workerIds.length === 0) {
|
|
1326
|
-
return '<div class="no-data">Could not determine worker distribution from test data.</div>';
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
const chartId = `workerDistChart-${Date.now()}-${Math.random()
|
|
1330
|
-
.toString(36)
|
|
1331
|
-
.substring(2, 7)}`;
|
|
1332
|
-
const renderFunctionName = `renderWorkerDistChart_${chartId.replace(
|
|
1333
|
-
/-/g,
|
|
1334
|
-
"_"
|
|
1335
|
-
)}`;
|
|
1336
|
-
const modalJsNamespace = `modal_funcs_${chartId.replace(/-/g, "_")}`;
|
|
1337
|
-
|
|
1338
|
-
// The categories now just need the name for the axis labels
|
|
1339
|
-
const categories = workerIds.map((id) => `Worker ${id}`);
|
|
1340
|
-
|
|
1341
|
-
// We pass the full data separately to the script
|
|
1342
|
-
const fullWorkerData = workerIds.map((id) => ({
|
|
1343
|
-
id: id,
|
|
1344
|
-
name: `Worker ${id}`,
|
|
1345
|
-
tests: workerData[id].tests,
|
|
1346
|
-
}));
|
|
1347
|
-
|
|
1348
|
-
const passedData = workerIds.map((id) => workerData[id].passed);
|
|
1349
|
-
const failedData = workerIds.map((id) => workerData[id].failed);
|
|
1350
|
-
const skippedData = workerIds.map((id) => workerData[id].skipped);
|
|
1351
|
-
|
|
1352
|
-
const categoriesString = JSON.stringify(categories);
|
|
1353
|
-
const fullDataString = JSON.stringify(fullWorkerData);
|
|
1354
|
-
const seriesString = JSON.stringify([
|
|
1355
|
-
{ name: "Passed", data: passedData, color: "var(--success-color)" },
|
|
1356
|
-
{ name: "Failed", data: failedData, color: "var(--danger-color)" },
|
|
1357
|
-
{ name: "Skipped", data: skippedData, color: "var(--warning-color)" },
|
|
1358
|
-
]);
|
|
1075
|
+
const categoriesString = JSON.stringify(categories);
|
|
1076
|
+
const fullDataString = JSON.stringify(fullWorkerData);
|
|
1077
|
+
const seriesString = JSON.stringify([
|
|
1078
|
+
{ name: "Passed", data: passedData, color: "var(--success-color)" },
|
|
1079
|
+
{ name: "Failed", data: failedData, color: "var(--danger-color)" },
|
|
1080
|
+
{ name: "Skipped", data: skippedData, color: "var(--warning-color)" },
|
|
1081
|
+
]);
|
|
1359
1082
|
|
|
1360
1083
|
// The HTML now includes the chart container, the modal, and styles for the modal
|
|
1361
1084
|
return `
|
|
@@ -1537,6 +1260,351 @@ const infoTooltip = `
|
|
|
1537
1260
|
}
|
|
1538
1261
|
</script>
|
|
1539
1262
|
`;
|
|
1263
|
+
function generateTestHistoryContent(trendData) {
|
|
1264
|
+
if (
|
|
1265
|
+
!trendData ||
|
|
1266
|
+
!trendData.testRuns ||
|
|
1267
|
+
Object.keys(trendData.testRuns).length === 0
|
|
1268
|
+
) {
|
|
1269
|
+
return '<div class="no-data">No historical test data available.</div>';
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const allTestNamesAndPaths = new Map();
|
|
1273
|
+
Object.values(trendData.testRuns).forEach((run) => {
|
|
1274
|
+
if (Array.isArray(run)) {
|
|
1275
|
+
run.forEach((test) => {
|
|
1276
|
+
if (test && test.testName && !allTestNamesAndPaths.has(test.testName)) {
|
|
1277
|
+
const parts = test.testName.split(" > ");
|
|
1278
|
+
const title = parts[parts.length - 1];
|
|
1279
|
+
allTestNamesAndPaths.set(test.testName, title);
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
if (allTestNamesAndPaths.size === 0) {
|
|
1286
|
+
return '<div class="no-data">No historical test data found after processing.</div>';
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const testHistory = Array.from(allTestNamesAndPaths.entries())
|
|
1290
|
+
.map(([fullTestName, testTitle]) => {
|
|
1291
|
+
const history = [];
|
|
1292
|
+
(trendData.overall || []).forEach((overallRun, index) => {
|
|
1293
|
+
const runKey = overallRun.runId
|
|
1294
|
+
? `test run ${overallRun.runId}`
|
|
1295
|
+
: `test run ${index + 1}`;
|
|
1296
|
+
const testRunForThisOverallRun = trendData.testRuns[runKey]?.find(
|
|
1297
|
+
(t) => t && t.testName === fullTestName
|
|
1298
|
+
);
|
|
1299
|
+
if (testRunForThisOverallRun) {
|
|
1300
|
+
history.push({
|
|
1301
|
+
runId: overallRun.runId || index + 1,
|
|
1302
|
+
status: testRunForThisOverallRun.status || "unknown",
|
|
1303
|
+
duration: testRunForThisOverallRun.duration || 0,
|
|
1304
|
+
timestamp:
|
|
1305
|
+
testRunForThisOverallRun.timestamp ||
|
|
1306
|
+
overallRun.timestamp ||
|
|
1307
|
+
new Date(),
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
return { fullTestName, testTitle, history };
|
|
1312
|
+
})
|
|
1313
|
+
.filter((item) => item.history.length > 0);
|
|
1314
|
+
|
|
1315
|
+
return `
|
|
1316
|
+
<div class="test-history-container">
|
|
1317
|
+
<div class="filters" style="border-color: black; border-style: groove;">
|
|
1318
|
+
<input type="text" id="history-filter-name" placeholder="Search by test title..." style="border-color: black; border-style: outset;">
|
|
1319
|
+
<select id="history-filter-status">
|
|
1320
|
+
<option value="">All Statuses</option>
|
|
1321
|
+
<option value="passed">Passed</option>
|
|
1322
|
+
<option value="failed">Failed</option>
|
|
1323
|
+
<option value="skipped">Skipped</option>
|
|
1324
|
+
</select>
|
|
1325
|
+
<button id="clear-history-filters" class="clear-filters-btn">Clear Filters</button>
|
|
1326
|
+
</div>
|
|
1327
|
+
|
|
1328
|
+
<div class="test-history-grid">
|
|
1329
|
+
${testHistory
|
|
1330
|
+
.map((test) => {
|
|
1331
|
+
const latestRun =
|
|
1332
|
+
test.history.length > 0
|
|
1333
|
+
? test.history[test.history.length - 1]
|
|
1334
|
+
: { status: "unknown" };
|
|
1335
|
+
return `
|
|
1336
|
+
<div class="test-history-card" data-test-name="${sanitizeHTML(
|
|
1337
|
+
test.testTitle.toLowerCase()
|
|
1338
|
+
)}" data-latest-status="${latestRun.status}">
|
|
1339
|
+
<div class="test-history-header">
|
|
1340
|
+
<p title="${sanitizeHTML(test.testTitle)}">${capitalize(
|
|
1341
|
+
sanitizeHTML(test.testTitle)
|
|
1342
|
+
)}</p>
|
|
1343
|
+
<span class="status-badge ${getStatusClass(latestRun.status)}">
|
|
1344
|
+
${String(latestRun.status).toUpperCase()}
|
|
1345
|
+
</span>
|
|
1346
|
+
</div>
|
|
1347
|
+
<div class="test-history-trend">
|
|
1348
|
+
${generateTestHistoryChart(test.history)}
|
|
1349
|
+
</div>
|
|
1350
|
+
<details class="test-history-details-collapsible">
|
|
1351
|
+
<summary>Show Run Details (${test.history.length})</summary>
|
|
1352
|
+
<div class="test-history-details">
|
|
1353
|
+
<table>
|
|
1354
|
+
<thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Date</th></tr></thead>
|
|
1355
|
+
<tbody>
|
|
1356
|
+
${test.history
|
|
1357
|
+
.slice()
|
|
1358
|
+
.reverse()
|
|
1359
|
+
.map(
|
|
1360
|
+
(run) => `
|
|
1361
|
+
<tr>
|
|
1362
|
+
<td>${run.runId}</td>
|
|
1363
|
+
<td><span class="status-badge-small ${getStatusClass(
|
|
1364
|
+
run.status
|
|
1365
|
+
)}">${String(run.status).toUpperCase()}</span></td>
|
|
1366
|
+
<td>${formatDuration(run.duration)}</td>
|
|
1367
|
+
<td>${formatDate(run.timestamp)}</td>
|
|
1368
|
+
</tr>`
|
|
1369
|
+
)
|
|
1370
|
+
.join("")}
|
|
1371
|
+
</tbody>
|
|
1372
|
+
</table>
|
|
1373
|
+
</div>
|
|
1374
|
+
</details>
|
|
1375
|
+
</div>`;
|
|
1376
|
+
})
|
|
1377
|
+
.join("")}
|
|
1378
|
+
</div>
|
|
1379
|
+
</div>
|
|
1380
|
+
`;
|
|
1381
|
+
}
|
|
1382
|
+
function getStatusClass(status) {
|
|
1383
|
+
switch (String(status).toLowerCase()) {
|
|
1384
|
+
case "passed":
|
|
1385
|
+
return "status-passed";
|
|
1386
|
+
case "failed":
|
|
1387
|
+
return "status-failed";
|
|
1388
|
+
case "skipped":
|
|
1389
|
+
return "status-skipped";
|
|
1390
|
+
default:
|
|
1391
|
+
return "status-unknown";
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
function getStatusIcon(status) {
|
|
1395
|
+
switch (String(status).toLowerCase()) {
|
|
1396
|
+
case "passed":
|
|
1397
|
+
return "✅";
|
|
1398
|
+
case "failed":
|
|
1399
|
+
return "❌";
|
|
1400
|
+
case "skipped":
|
|
1401
|
+
return "⏭️";
|
|
1402
|
+
default:
|
|
1403
|
+
return "❓";
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
function getSuitesData(results) {
|
|
1407
|
+
const suitesMap = new Map();
|
|
1408
|
+
if (!results || results.length === 0) return [];
|
|
1409
|
+
|
|
1410
|
+
results.forEach((test) => {
|
|
1411
|
+
const browser = test.browser || "unknown";
|
|
1412
|
+
const suiteParts = test.name.split(" > ");
|
|
1413
|
+
let suiteNameCandidate = "Default Suite";
|
|
1414
|
+
if (suiteParts.length > 2) {
|
|
1415
|
+
suiteNameCandidate = suiteParts[1];
|
|
1416
|
+
} else if (suiteParts.length > 1) {
|
|
1417
|
+
suiteNameCandidate = suiteParts[0]
|
|
1418
|
+
.split(path.sep)
|
|
1419
|
+
.pop()
|
|
1420
|
+
.replace(/\.(spec|test)\.(ts|js|mjs|cjs)$/, "");
|
|
1421
|
+
} else {
|
|
1422
|
+
suiteNameCandidate = test.name
|
|
1423
|
+
.split(path.sep)
|
|
1424
|
+
.pop()
|
|
1425
|
+
.replace(/\.(spec|test)\.(ts|js|mjs|cjs)$/, "");
|
|
1426
|
+
}
|
|
1427
|
+
const suiteName = suiteNameCandidate;
|
|
1428
|
+
const key = `${suiteName}|${browser}`;
|
|
1429
|
+
|
|
1430
|
+
if (!suitesMap.has(key)) {
|
|
1431
|
+
suitesMap.set(key, {
|
|
1432
|
+
id: test.id || key,
|
|
1433
|
+
name: suiteName,
|
|
1434
|
+
browser: browser,
|
|
1435
|
+
passed: 0,
|
|
1436
|
+
failed: 0,
|
|
1437
|
+
skipped: 0,
|
|
1438
|
+
count: 0,
|
|
1439
|
+
statusOverall: "passed",
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
const suite = suitesMap.get(key);
|
|
1443
|
+
suite.count++;
|
|
1444
|
+
const currentStatus = String(test.status).toLowerCase();
|
|
1445
|
+
if (currentStatus && suite[currentStatus] !== undefined) {
|
|
1446
|
+
suite[currentStatus]++;
|
|
1447
|
+
}
|
|
1448
|
+
if (currentStatus === "failed") suite.statusOverall = "failed";
|
|
1449
|
+
else if (currentStatus === "skipped" && suite.statusOverall !== "failed")
|
|
1450
|
+
suite.statusOverall = "skipped";
|
|
1451
|
+
});
|
|
1452
|
+
return Array.from(suitesMap.values());
|
|
1453
|
+
}
|
|
1454
|
+
function getAttachmentIcon(contentType) {
|
|
1455
|
+
if (!contentType) return "📎"; // Handle undefined/null
|
|
1456
|
+
|
|
1457
|
+
const normalizedType = contentType.toLowerCase();
|
|
1458
|
+
|
|
1459
|
+
if (normalizedType.includes("pdf")) return "📄";
|
|
1460
|
+
if (normalizedType.includes("json")) return "{ }";
|
|
1461
|
+
if (/html/.test(normalizedType)) return "🌐"; // Fixed: regex for any HTML type
|
|
1462
|
+
if (normalizedType.includes("xml")) return "<>";
|
|
1463
|
+
if (normalizedType.includes("csv")) return "📊";
|
|
1464
|
+
if (normalizedType.startsWith("text/")) return "📝";
|
|
1465
|
+
return "📎";
|
|
1466
|
+
}
|
|
1467
|
+
function generateSuitesWidget(suitesData) {
|
|
1468
|
+
if (!suitesData || suitesData.length === 0) {
|
|
1469
|
+
return `<div class="suites-widget"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
|
|
1470
|
+
}
|
|
1471
|
+
return `
|
|
1472
|
+
<div class="suites-widget">
|
|
1473
|
+
<div class="suites-header">
|
|
1474
|
+
<h2>Test Suites</h2>
|
|
1475
|
+
<span class="summary-badge">${
|
|
1476
|
+
suitesData.length
|
|
1477
|
+
} suites • ${suitesData.reduce(
|
|
1478
|
+
(sum, suite) => sum + suite.count,
|
|
1479
|
+
0
|
|
1480
|
+
)} tests</span>
|
|
1481
|
+
</div>
|
|
1482
|
+
<div class="suites-grid">
|
|
1483
|
+
${suitesData
|
|
1484
|
+
.map(
|
|
1485
|
+
(suite) => `
|
|
1486
|
+
<div class="suite-card status-${suite.statusOverall}">
|
|
1487
|
+
<div class="suite-card-header">
|
|
1488
|
+
<h3 class="suite-name" title="${sanitizeHTML(
|
|
1489
|
+
suite.name
|
|
1490
|
+
)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
|
|
1491
|
+
</div>
|
|
1492
|
+
<div>🖥️ <span class="browser-tag">${sanitizeHTML(
|
|
1493
|
+
suite.browser
|
|
1494
|
+
)}</span></div>
|
|
1495
|
+
<div class="suite-card-body">
|
|
1496
|
+
<span class="test-count">${suite.count} test${
|
|
1497
|
+
suite.count !== 1 ? "s" : ""
|
|
1498
|
+
}</span>
|
|
1499
|
+
<div class="suite-stats">
|
|
1500
|
+
${
|
|
1501
|
+
suite.passed > 0
|
|
1502
|
+
? `<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>`
|
|
1503
|
+
: ""
|
|
1504
|
+
}
|
|
1505
|
+
${
|
|
1506
|
+
suite.failed > 0
|
|
1507
|
+
? `<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>`
|
|
1508
|
+
: ""
|
|
1509
|
+
}
|
|
1510
|
+
${
|
|
1511
|
+
suite.skipped > 0
|
|
1512
|
+
? `<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>`
|
|
1513
|
+
: ""
|
|
1514
|
+
}
|
|
1515
|
+
</div>
|
|
1516
|
+
</div>
|
|
1517
|
+
</div>`
|
|
1518
|
+
)
|
|
1519
|
+
.join("")}
|
|
1520
|
+
</div>
|
|
1521
|
+
</div>`;
|
|
1522
|
+
}
|
|
1523
|
+
function generateAIFailureAnalyzerTab(results) {
|
|
1524
|
+
const failedTests = (results || []).filter(test => test.status === 'failed');
|
|
1525
|
+
|
|
1526
|
+
if (failedTests.length === 0) {
|
|
1527
|
+
return `
|
|
1528
|
+
<h2 class="tab-main-title">AI Failure Analysis</h2>
|
|
1529
|
+
<div class="no-data">Congratulations! No failed tests in this run.</div>
|
|
1530
|
+
`;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// btoa is not available in Node.js environment, so we define a simple polyfill for it.
|
|
1534
|
+
const btoa = (str) => Buffer.from(str).toString('base64');
|
|
1535
|
+
|
|
1536
|
+
return `
|
|
1537
|
+
<h2 class="tab-main-title">AI Failure Analysis</h2>
|
|
1538
|
+
<div class="ai-analyzer-stats">
|
|
1539
|
+
<div class="stat-item">
|
|
1540
|
+
<span class="stat-number">${failedTests.length}</span>
|
|
1541
|
+
<span class="stat-label">Failed Tests</span>
|
|
1542
|
+
</div>
|
|
1543
|
+
<div class="stat-item">
|
|
1544
|
+
<span class="stat-number">${new Set(failedTests.map(t => t.browser)).size}</span>
|
|
1545
|
+
<span class="stat-label">Browsers</span>
|
|
1546
|
+
</div>
|
|
1547
|
+
<div class="stat-item">
|
|
1548
|
+
<span class="stat-number">${(Math.round(failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) / 1000))}s</span>
|
|
1549
|
+
<span class="stat-label">Total Duration</span>
|
|
1550
|
+
</div>
|
|
1551
|
+
</div>
|
|
1552
|
+
<p class="ai-analyzer-description">
|
|
1553
|
+
Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for specific failed test.
|
|
1554
|
+
</p>
|
|
1555
|
+
|
|
1556
|
+
<div class="compact-failure-list">
|
|
1557
|
+
${failedTests.map(test => {
|
|
1558
|
+
const testTitle = test.name.split(" > ").pop() || "Unnamed Test";
|
|
1559
|
+
const testJson = btoa(JSON.stringify(test)); // Base64 encode the test object
|
|
1560
|
+
const truncatedError = (test.errorMessage || "No error message").slice(0, 150) +
|
|
1561
|
+
(test.errorMessage && test.errorMessage.length > 150 ? "..." : "");
|
|
1562
|
+
|
|
1563
|
+
return `
|
|
1564
|
+
<div class="compact-failure-item">
|
|
1565
|
+
<div class="failure-header">
|
|
1566
|
+
<div class="failure-main-info">
|
|
1567
|
+
<h3 class="failure-title" title="${sanitizeHTML(test.name)}">${sanitizeHTML(testTitle)}</h3>
|
|
1568
|
+
<div class="failure-meta">
|
|
1569
|
+
<span class="browser-indicator">${sanitizeHTML(test.browser || 'unknown')}</span>
|
|
1570
|
+
<span class="duration-indicator">${formatDuration(test.duration)}</span>
|
|
1571
|
+
</div>
|
|
1572
|
+
</div>
|
|
1573
|
+
<button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
|
|
1574
|
+
<span class="ai-text">AI Fix</span>
|
|
1575
|
+
</button>
|
|
1576
|
+
</div>
|
|
1577
|
+
<div class="failure-error-preview">
|
|
1578
|
+
<div class="error-snippet">${formatPlaywrightError(truncatedError)}</div>
|
|
1579
|
+
<button class="expand-error-btn" onclick="toggleErrorDetails(this)">
|
|
1580
|
+
<span class="expand-text">Show Full Error</span>
|
|
1581
|
+
<span class="expand-icon">▼</span>
|
|
1582
|
+
</button>
|
|
1583
|
+
</div>
|
|
1584
|
+
<div class="full-error-details" style="display: none;">
|
|
1585
|
+
<div class="full-error-content">
|
|
1586
|
+
${formatPlaywrightError(test.errorMessage || "No detailed error message available")}
|
|
1587
|
+
</div>
|
|
1588
|
+
</div>
|
|
1589
|
+
</div>
|
|
1590
|
+
`
|
|
1591
|
+
}).join('')}
|
|
1592
|
+
</div>
|
|
1593
|
+
|
|
1594
|
+
<!-- AI Fix Modal -->
|
|
1595
|
+
<div id="ai-fix-modal" class="ai-modal-overlay" onclick="closeAiModal()">
|
|
1596
|
+
<div class="ai-modal-content" onclick="event.stopPropagation()">
|
|
1597
|
+
<div class="ai-modal-header">
|
|
1598
|
+
<h3 id="ai-fix-modal-title">AI Analysis</h3>
|
|
1599
|
+
<span class="ai-modal-close" onclick="closeAiModal()">×</span>
|
|
1600
|
+
</div>
|
|
1601
|
+
<div class="ai-modal-body" id="ai-fix-modal-content">
|
|
1602
|
+
<!-- Content will be injected by JavaScript -->
|
|
1603
|
+
</div>
|
|
1604
|
+
</div>
|
|
1605
|
+
</div>
|
|
1606
|
+
`;
|
|
1607
|
+
}
|
|
1540
1608
|
function generateHTML(reportData, trendData = null) {
|
|
1541
1609
|
const { run, results } = reportData;
|
|
1542
1610
|
const suitesData = getSuitesData(reportData.results || []);
|
|
@@ -1563,7 +1631,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1563
1631
|
if (!results || results.length === 0)
|
|
1564
1632
|
return '<div class="no-tests">No test results found in this run.</div>';
|
|
1565
1633
|
return results
|
|
1566
|
-
.map((test) => {
|
|
1634
|
+
.map((test, testIndex) => {
|
|
1567
1635
|
const browser = test.browser || "unknown";
|
|
1568
1636
|
const testFileParts = test.name.split(" > ");
|
|
1569
1637
|
const testTitle =
|
|
@@ -1862,7 +1930,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1862
1930
|
<link rel="icon" type="image/png" href="https://i.postimg.cc/v817w4sg/logo.png">
|
|
1863
1931
|
<link rel="apple-touch-icon" href="https://i.postimg.cc/v817w4sg/logo.png">
|
|
1864
1932
|
<script src="https://code.highcharts.com/highcharts.js" defer></script>
|
|
1865
|
-
<title>Playwright Pulse Report</title>
|
|
1933
|
+
<title>Playwright Pulse Report (Static Report)</title>
|
|
1866
1934
|
<style>
|
|
1867
1935
|
:root {
|
|
1868
1936
|
--primary-color: #3f51b5; --secondary-color: #ff4081; --accent-color: #673ab7; --accent-color-alt: #FF9800;
|
|
@@ -1874,8 +1942,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1874
1942
|
}
|
|
1875
1943
|
.trend-chart-container, .test-history-trend div[id^="testHistoryChart-"] { min-height: 100px; }
|
|
1876
1944
|
.lazy-load-chart .no-data, .lazy-load-chart .no-data-chart { display: flex; align-items: center; justify-content: center; height: 100%; font-style: italic; color: var(--dark-gray-color); }
|
|
1877
|
-
|
|
1878
|
-
/* General Highcharts styling */
|
|
1879
1945
|
.highcharts-background { fill: transparent; }
|
|
1880
1946
|
.highcharts-title, .highcharts-subtitle { font-family: var(--font-family); }
|
|
1881
1947
|
.highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
|
|
@@ -2003,8 +2069,24 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2003
2069
|
.status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
|
|
2004
2070
|
.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); }
|
|
2005
2071
|
.no-data-chart {font-size: 0.95em; padding: 18px;}
|
|
2006
|
-
|
|
2007
|
-
|
|
2072
|
+
.ai-failure-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 22px; }
|
|
2073
|
+
.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; }
|
|
2074
|
+
.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; }
|
|
2075
|
+
.ai-failure-card-header h3 { margin: 0; font-size: 1.1em; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2076
|
+
.ai-failure-card-body { padding: 20px; }
|
|
2077
|
+
.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; }
|
|
2078
|
+
.ai-fix-btn:hover { background-color: var(--accent-color); transform: translateY(-2px); }
|
|
2079
|
+
.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; }
|
|
2080
|
+
.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; }
|
|
2081
|
+
.ai-modal-header { padding: 18px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
|
|
2082
|
+
.ai-modal-header h3 { margin: 0; font-size: 1.25em; }
|
|
2083
|
+
.ai-modal-close { font-size: 2rem; font-weight: 300; cursor: pointer; color: var(--dark-gray-color); line-height: 1; transition: color 0.2s; }
|
|
2084
|
+
.ai-modal-close:hover { color: var(--danger-color); }
|
|
2085
|
+
.ai-modal-body { padding: 25px; overflow-y: auto; }
|
|
2086
|
+
.ai-modal-body h4 { margin-top: 18px; margin-bottom: 10px; font-size: 1.1em; color: var(--primary-color); }
|
|
2087
|
+
.ai-modal-body p { margin-bottom: 15px; }
|
|
2088
|
+
.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; }
|
|
2089
|
+
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
2008
2090
|
.trace-preview { padding: 1rem; text-align: center; background: #f5f5f5; border-bottom: 1px solid #e1e1e1; }
|
|
2009
2091
|
.trace-icon { font-size: 2rem; display: block; margin-bottom: 0.5rem; }
|
|
2010
2092
|
.trace-name { word-break: break-word; font-size: 0.9rem; }
|
|
@@ -2017,10 +2099,36 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2017
2099
|
.filters button.clear-filters-btn { background-color: var(--medium-gray-color); color: var(--text-color); }
|
|
2018
2100
|
.filters button.clear-filters-btn:hover { background-color: var(--dark-gray-color); color: #fff; }
|
|
2019
2101
|
.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;}
|
|
2102
|
+
.ai-analyzer-stats { display: flex; gap: 20px; margin-bottom: 25px; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: var(--border-radius); justify-content: center; }
|
|
2103
|
+
.stat-item { text-align: center; color: white; }
|
|
2104
|
+
.stat-number { display: block; font-size: 2em; font-weight: 700; line-height: 1;}
|
|
2105
|
+
.stat-label { font-size: 0.9em; opacity: 0.9; font-weight: 500;}
|
|
2106
|
+
.ai-analyzer-description { margin-bottom: 25px; font-size: 1em; color: var(--text-color-secondary); text-align: center; max-width: 600px; margin-left: auto; margin-right: auto;}
|
|
2107
|
+
.compact-failure-list { display: flex; flex-direction: column; gap: 15px; }
|
|
2108
|
+
.compact-failure-item { background: var(--card-background-color); border: 1px solid var(--border-color); border-left: 4px solid var(--danger-color); border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); transition: transform 0.2s ease, box-shadow 0.2s ease;}
|
|
2109
|
+
.compact-failure-item:hover { transform: translateY(-2px); box-shadow: var(--box-shadow); }
|
|
2110
|
+
.failure-header { display: flex; justify-content: space-between; align-items: center; padding: 18px 20px; gap: 15px;}
|
|
2111
|
+
.failure-main-info { flex: 1; min-width: 0; }
|
|
2112
|
+
.failure-title { margin: 0 0 8px 0; font-size: 1.1em; font-weight: 600; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
|
|
2113
|
+
.failure-meta { display: flex; gap: 12px; align-items: center;}
|
|
2114
|
+
.browser-indicator, .duration-indicator { font-size: 0.85em; padding: 3px 8px; border-radius: 12px; font-weight: 500;}
|
|
2115
|
+
.browser-indicator { background: var(--info-color); color: white; }
|
|
2116
|
+
.duration-indicator { background: var(--medium-gray-color); color: var(--text-color); }
|
|
2117
|
+
.compact-ai-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 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;}
|
|
2118
|
+
.compact-ai-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); }
|
|
2119
|
+
.ai-text { font-size: 0.95em; }
|
|
2120
|
+
.failure-error-preview { padding: 0 20px 18px 20px; border-top: 1px solid var(--light-gray-color);}
|
|
2121
|
+
.error-snippet { background: rgba(244, 67, 54, 0.05); border: 1px solid rgba(244, 67, 54, 0.2); border-radius: 6px; padding: 12px; margin-bottom: 12px; font-family: monospace; font-size: 0.9em; color: var(--danger-color); line-height: 1.4;}
|
|
2122
|
+
.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;}
|
|
2123
|
+
.expand-error-btn:hover { background: var(--light-gray-color); border-color: var(--medium-gray-color); }
|
|
2124
|
+
.expand-icon { transition: transform 0.2s ease; font-size: 0.8em;}
|
|
2125
|
+
.expand-error-btn.expanded .expand-icon { transform: rotate(180deg); }
|
|
2126
|
+
.full-error-details { padding: 0 20px 20px 20px; border-top: 1px solid var(--light-gray-color); margin-top: 0;}
|
|
2127
|
+
.full-error-content { background: rgba(244, 67, 54, 0.05); border: 1px solid rgba(244, 67, 54, 0.2); 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;}
|
|
2020
2128
|
@media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
|
|
2021
2129
|
@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; } }
|
|
2022
|
-
@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;} }
|
|
2023
|
-
@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;} }
|
|
2130
|
+
@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; } }
|
|
2131
|
+
@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; } }
|
|
2024
2132
|
.trace-actions a { text-decoration: none; color: var(--primary-color); font-weight: 500; font-size: 0.9em; }
|
|
2025
2133
|
.generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
|
|
2026
2134
|
.attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
|
|
@@ -2046,7 +2154,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2046
2154
|
<button class="tab-button active" data-tab="dashboard">Dashboard</button>
|
|
2047
2155
|
<button class="tab-button" data-tab="test-runs">Test Run Summary</button>
|
|
2048
2156
|
<button class="tab-button" data-tab="test-history">Test History</button>
|
|
2049
|
-
<button class="tab-button" data-tab="
|
|
2157
|
+
<button class="tab-button" data-tab="ai-failure-analyzer">AI Failure Analyzer</button>
|
|
2050
2158
|
</div>
|
|
2051
2159
|
<div id="dashboard" class="tab-content active">
|
|
2052
2160
|
<div class="dashboard-grid">
|
|
@@ -2141,8 +2249,8 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2141
2249
|
: '<div class="no-data">Individual test history data not available.</div>'
|
|
2142
2250
|
}
|
|
2143
2251
|
</div>
|
|
2144
|
-
<div id="
|
|
2145
|
-
|
|
2252
|
+
<div id="ai-failure-analyzer" class="tab-content">
|
|
2253
|
+
${generateAIFailureAnalyzerTab(results)}
|
|
2146
2254
|
</div>
|
|
2147
2255
|
<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;">
|
|
2148
2256
|
<div style="display: inline-flex; align-items: center; gap: 0.5rem; color: #333; font-size: 0.9rem; font-weight: 600; letter-spacing: 0.5px;">
|
|
@@ -2174,7 +2282,127 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2174
2282
|
button.textContent = 'Failed';
|
|
2175
2283
|
setTimeout(() => { button.textContent = 'Copy'; }, 2000);
|
|
2176
2284
|
});
|
|
2177
|
-
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
function getAIFix(button) {
|
|
2288
|
+
const modal = document.getElementById('ai-fix-modal');
|
|
2289
|
+
const modalContent = document.getElementById('ai-fix-modal-content');
|
|
2290
|
+
const modalTitle = document.getElementById('ai-fix-modal-title');
|
|
2291
|
+
|
|
2292
|
+
modal.style.display = 'flex';
|
|
2293
|
+
modalTitle.textContent = 'Analyzing...';
|
|
2294
|
+
modalContent.innerHTML = '<div class="ai-loader"></div>';
|
|
2295
|
+
|
|
2296
|
+
try {
|
|
2297
|
+
const testJson = button.dataset.testJson;
|
|
2298
|
+
const test = JSON.parse(atob(testJson));
|
|
2299
|
+
|
|
2300
|
+
const testName = test.name || 'Unknown Test';
|
|
2301
|
+
const failureLogsAndErrors = [
|
|
2302
|
+
'Error Message:',
|
|
2303
|
+
test.errorMessage || 'Not available.',
|
|
2304
|
+
'\\n\\n--- stdout ---',
|
|
2305
|
+
(test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
|
|
2306
|
+
'\\n\\n--- stderr ---',
|
|
2307
|
+
(test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
|
|
2308
|
+
].join('\\n');
|
|
2309
|
+
const codeSnippet = test.snippet || '';
|
|
2310
|
+
|
|
2311
|
+
const shortTestName = testName.split(' > ').pop();
|
|
2312
|
+
modalTitle.textContent = \`Analysis for: \${shortTestName}\`;
|
|
2313
|
+
|
|
2314
|
+
const apiUrl = 'https://ai-test-analyser.netlify.app/api/analyze';
|
|
2315
|
+
fetch(apiUrl, {
|
|
2316
|
+
method: 'POST',
|
|
2317
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2318
|
+
body: JSON.stringify({
|
|
2319
|
+
testName: testName,
|
|
2320
|
+
failureLogsAndErrors: failureLogsAndErrors,
|
|
2321
|
+
codeSnippet: codeSnippet,
|
|
2322
|
+
}),
|
|
2323
|
+
})
|
|
2324
|
+
.then(response => {
|
|
2325
|
+
if (!response.ok) {
|
|
2326
|
+
return response.text().then(text => {
|
|
2327
|
+
throw new Error(\`API request failed with status \${response.status}: \${text || response.statusText}\`);
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
return response.text();
|
|
2331
|
+
})
|
|
2332
|
+
.then(text => {
|
|
2333
|
+
if (!text) {
|
|
2334
|
+
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.");
|
|
2335
|
+
}
|
|
2336
|
+
try {
|
|
2337
|
+
return JSON.parse(text);
|
|
2338
|
+
} catch (e) {
|
|
2339
|
+
console.error("Failed to parse JSON:", text);
|
|
2340
|
+
throw new Error(\`The AI analyzer returned an invalid response. \${e.message}\`);
|
|
2341
|
+
}
|
|
2342
|
+
})
|
|
2343
|
+
.then(data => {
|
|
2344
|
+
const escapeHtml = (unsafe) => {
|
|
2345
|
+
if (typeof unsafe !== 'string') return '';
|
|
2346
|
+
return unsafe
|
|
2347
|
+
.replace(/&/g, "&")
|
|
2348
|
+
.replace(/</g, "<")
|
|
2349
|
+
.replace(/>/g, ">")
|
|
2350
|
+
.replace(/"/g, """)
|
|
2351
|
+
.replace(/'/g, "'");
|
|
2352
|
+
};
|
|
2353
|
+
|
|
2354
|
+
const analysisHtml = \`<h4>Analysis</h4><p>\${escapeHtml(data.rootCause) || 'No analysis provided.'}</p>\`;
|
|
2355
|
+
|
|
2356
|
+
let suggestionsHtml = '<h4>Suggestions</h4>';
|
|
2357
|
+
if (data.suggestedFixes && data.suggestedFixes.length > 0) {
|
|
2358
|
+
suggestionsHtml += '<div class="suggestions-list" style="margin-top: 15px;">';
|
|
2359
|
+
data.suggestedFixes.forEach(fix => {
|
|
2360
|
+
suggestionsHtml += \`
|
|
2361
|
+
<div class="suggestion-item" style="margin-bottom: 22px; border-left: 3px solid var(--accent-color-alt); padding-left: 15px;">
|
|
2362
|
+
<p style="margin: 0 0 8px 0; font-weight: 500;">\${escapeHtml(fix.description)}</p>
|
|
2363
|
+
\${fix.codeSnippet ? \`<div class="code-section"><pre><code>\${escapeHtml(fix.codeSnippet)}</code></pre></div>\` : ''}
|
|
2364
|
+
</div>
|
|
2365
|
+
\`;
|
|
2366
|
+
});
|
|
2367
|
+
suggestionsHtml += '</div>';
|
|
2368
|
+
} else {
|
|
2369
|
+
suggestionsHtml += \`<div class="code-section"><pre><code>No suggestion provided.</code></pre></div>\`;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
modalContent.innerHTML = analysisHtml + suggestionsHtml;
|
|
2373
|
+
})
|
|
2374
|
+
.catch(err => {
|
|
2375
|
+
console.error('AI Fix Error:', err);
|
|
2376
|
+
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>\`;
|
|
2377
|
+
});
|
|
2378
|
+
|
|
2379
|
+
} catch (e) {
|
|
2380
|
+
console.error('Error processing test data for AI Fix:', e);
|
|
2381
|
+
modalTitle.textContent = 'Error';
|
|
2382
|
+
modalContent.innerHTML = \`<div class="test-error-summary">Could not process test data. Is it formatted correctly?</div>\`;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
function closeAiModal() {
|
|
2387
|
+
const modal = document.getElementById('ai-fix-modal');
|
|
2388
|
+
if(modal) modal.style.display = 'none';
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
function toggleErrorDetails(button) {
|
|
2392
|
+
const errorDetails = button.closest('.compact-failure-item').querySelector('.full-error-details');
|
|
2393
|
+
const expandText = button.querySelector('.expand-text');
|
|
2394
|
+
|
|
2395
|
+
if (errorDetails.style.display === 'none' || !errorDetails.style.display) {
|
|
2396
|
+
errorDetails.style.display = 'block';
|
|
2397
|
+
expandText.textContent = 'Hide Full Error';
|
|
2398
|
+
button.classList.add('expanded');
|
|
2399
|
+
} else {
|
|
2400
|
+
errorDetails.style.display = 'none';
|
|
2401
|
+
expandText.textContent = 'Show Full Error';
|
|
2402
|
+
button.classList.remove('expanded');
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2178
2406
|
function initializeReportInteractivity() {
|
|
2179
2407
|
const tabButtons = document.querySelectorAll('.tab-button');
|
|
2180
2408
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
@@ -2187,9 +2415,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2187
2415
|
const activeContent = document.getElementById(tabId);
|
|
2188
2416
|
if (activeContent) {
|
|
2189
2417
|
activeContent.classList.add('active');
|
|
2190
|
-
// Check if IntersectionObserver is already handling elements in this tab
|
|
2191
|
-
// For simplicity, we assume if an element is observed, it will be handled when it becomes visible.
|
|
2192
|
-
// If IntersectionObserver is not supported, already-visible elements would have been loaded by fallback.
|
|
2193
2418
|
}
|
|
2194
2419
|
});
|
|
2195
2420
|
});
|
|
@@ -2327,65 +2552,36 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2327
2552
|
document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
|
|
2328
2553
|
|
|
2329
2554
|
function copyErrorToClipboard(button) {
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2555
|
+
const errorContainer = button.closest('.test-error-summary');
|
|
2556
|
+
if (!errorContainer) {
|
|
2557
|
+
console.error("Could not find '.test-error-summary' container.");
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
let errorText;
|
|
2561
|
+
const stackTraceElement = errorContainer.querySelector('.stack-trace');
|
|
2562
|
+
if (stackTraceElement) {
|
|
2563
|
+
errorText = stackTraceElement.textContent;
|
|
2564
|
+
} else {
|
|
2565
|
+
const clonedContainer = errorContainer.cloneNode(true);
|
|
2566
|
+
const buttonInClone = clonedContainer.querySelector('button');
|
|
2567
|
+
if (buttonInClone) buttonInClone.remove();
|
|
2568
|
+
errorText = clonedContainer.textContent;
|
|
2569
|
+
}
|
|
2341
2570
|
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2571
|
+
if (!errorText) {
|
|
2572
|
+
button.textContent = 'Nothing to copy';
|
|
2573
|
+
setTimeout(() => { button.textContent = 'Copy Error'; }, 2000);
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
navigator.clipboard.writeText(errorText.trim()).then(() => {
|
|
2577
|
+
const originalText = button.textContent;
|
|
2578
|
+
button.textContent = 'Copied!';
|
|
2579
|
+
setTimeout(() => { button.textContent = originalText; }, 2000);
|
|
2580
|
+
}).catch(err => {
|
|
2581
|
+
console.error('Failed to copy: ', err);
|
|
2582
|
+
button.textContent = 'Failed';
|
|
2583
|
+
});
|
|
2354
2584
|
}
|
|
2355
|
-
|
|
2356
|
-
// Use the text content of the cleaned container as the fallback.
|
|
2357
|
-
errorText = clonedContainer.textContent;
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
// 4. Proceed with the clipboard logic, ensuring text is not null and is trimmed.
|
|
2361
|
-
if (!errorText) {
|
|
2362
|
-
console.error('Could not extract error text.');
|
|
2363
|
-
button.textContent = 'Nothing to copy';
|
|
2364
|
-
setTimeout(() => { button.textContent = 'Copy Error'; }, 2000);
|
|
2365
|
-
return;
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
const textarea = document.createElement('textarea');
|
|
2369
|
-
textarea.value = errorText.trim(); // Trim whitespace for a cleaner copy.
|
|
2370
|
-
textarea.style.position = 'fixed'; // Prevent screen scroll
|
|
2371
|
-
textarea.style.top = '-9999px';
|
|
2372
|
-
document.body.appendChild(textarea);
|
|
2373
|
-
textarea.select();
|
|
2374
|
-
|
|
2375
|
-
try {
|
|
2376
|
-
const successful = document.execCommand('copy');
|
|
2377
|
-
const originalText = button.textContent;
|
|
2378
|
-
button.textContent = successful ? 'Copied!' : 'Failed';
|
|
2379
|
-
setTimeout(() => {
|
|
2380
|
-
button.textContent = originalText;
|
|
2381
|
-
}, 2000);
|
|
2382
|
-
} catch (err) {
|
|
2383
|
-
console.error('Failed to copy: ', err);
|
|
2384
|
-
button.textContent = 'Failed';
|
|
2385
|
-
}
|
|
2386
|
-
|
|
2387
|
-
document.body.removeChild(textarea);
|
|
2388
|
-
}
|
|
2389
2585
|
</script>
|
|
2390
2586
|
</body>
|
|
2391
2587
|
</html>
|
|
@@ -2605,4 +2801,4 @@ main().catch((err) => {
|
|
|
2605
2801
|
);
|
|
2606
2802
|
console.error(err.stack);
|
|
2607
2803
|
process.exit(1);
|
|
2608
|
-
});
|
|
2804
|
+
});
|