@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
- return (
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, " ").replace(/\t/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;">') // Changed to apply bold
169
+ .replace(/<intensity>/g, '<span style="font-weight: bold;">')
172
170
  .replace(/<\/color>/g, "</span>")
173
171
  .replace(/<\/intensity>/g, "</span>")
174
- // Convert newlines to <br> after processing other replacements
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); // This will be an integer
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 generateTestHistoryContent(trendData) {
1027
- if (
1028
- !trendData ||
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
- const allTestNamesAndPaths = new Map();
1036
- Object.values(trendData.testRuns).forEach((run) => {
1037
- if (Array.isArray(run)) {
1038
- run.forEach((test) => {
1039
- if (test && test.testName && !allTestNamesAndPaths.has(test.testName)) {
1040
- const parts = test.testName.split(" > ");
1041
- const title = parts[parts.length - 1];
1042
- allTestNamesAndPaths.set(test.testName, title);
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 (allTestNamesAndPaths.size === 0) {
1049
- return '<div class="no-data">No historical test data found after processing.</div>';
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 testHistory = Array.from(allTestNamesAndPaths.entries())
1053
- .map(([fullTestName, testTitle]) => {
1054
- const history = [];
1055
- (trendData.overall || []).forEach((overallRun, index) => {
1056
- const runKey = overallRun.runId
1057
- ? `test run ${overallRun.runId}`
1058
- : `test run ${index + 1}`;
1059
- const testRunForThisOverallRun = trendData.testRuns[runKey]?.find(
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
- return `
1079
- <div class="test-history-container">
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
- results.forEach((test) => {
1174
- const browser = test.browser || "unknown";
1175
- const suiteParts = test.name.split(" > ");
1176
- let suiteNameCandidate = "Default Suite";
1177
- if (suiteParts.length > 2) {
1178
- suiteNameCandidate = suiteParts[1];
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
- if (!suitesMap.has(key)) {
1194
- suitesMap.set(key, {
1195
- id: test.id || key,
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 normalizedType = contentType.toLowerCase();
1221
-
1222
- if (normalizedType.includes("pdf")) return "📄";
1223
- if (normalizedType.includes("json")) return "{ }";
1224
- if (/html/.test(normalizedType)) return "🌐"; // Fixed: regex for any HTML type
1225
- if (normalizedType.includes("xml")) return "<>";
1226
- if (normalizedType.includes("csv")) return "📊";
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
- #test-ai iframe { border: 1px solid var(--border-color); width: 100%; height: 85vh; border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); }
2007
- #test-ai p {margin-bottom: 18px; font-size: 1em; color: var(--text-color-secondary);}
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="test-ai">AI Analysis</button>
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="test-ai" class="tab-content">
2145
- <iframe data-src="https://ai-test-analyser.netlify.app/" width="100%" height="100%" frameborder="0" allowfullscreen class="lazy-load-iframe" title="AI Test Analyser" style="border: none; height: 100vh;"></iframe>
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, "&amp;")
2348
+ .replace(/</g, "&lt;")
2349
+ .replace(/>/g, "&gt;")
2350
+ .replace(/"/g, "&quot;")
2351
+ .replace(/'/g, "&#039;");
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
- // 1. Find the main error container, which should always be present.
2331
- const errorContainer = button.closest('.test-error-summary');
2332
- if (!errorContainer) {
2333
- console.error("Could not find '.test-error-summary' container. The report's HTML structure might have changed.");
2334
- return;
2335
- }
2336
-
2337
- let errorText;
2338
-
2339
- // 2. First, try to find the preferred .stack-trace element (the "happy path").
2340
- const stackTraceElement = errorContainer.querySelector('.stack-trace');
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
- if (stackTraceElement) {
2343
- // If it exists, use its text content. This handles standard assertion errors.
2344
- errorText = stackTraceElement.textContent;
2345
- } else {
2346
- // 3. FALLBACK: If .stack-trace doesn't exist, this is likely an unstructured error.
2347
- // We clone the container to avoid manipulating the live DOM or copying the button's own text.
2348
- const clonedContainer = errorContainer.cloneNode(true);
2349
-
2350
- // Remove the button from our clone before extracting the text.
2351
- const buttonInClone = clonedContainer.querySelector('button');
2352
- if (buttonInClone) {
2353
- buttonInClone.remove();
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
+ });