@arghajit/playwright-pulse-report 0.2.4 → 0.2.6

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