@arghajit/playwright-pulse-report 0.3.3 → 0.3.4

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.
@@ -6,6 +6,7 @@ import path from "path";
6
6
  import { fork } from "child_process";
7
7
  import { fileURLToPath } from "url";
8
8
  import { getOutputDir } from "./config-reader.mjs";
9
+ import { animate } from "./terminal-logo.mjs";
9
10
 
10
11
  /**
11
12
  * Dynamically imports the 'chalk' library for terminal string styling.
@@ -338,6 +339,12 @@ function generateTestTrendsChart(trendData) {
338
339
  color: "var(--warning-color)",
339
340
  marker: { symbol: "circle" },
340
341
  },
342
+ {
343
+ name: "Flaky",
344
+ data: runs.map((r) => r.flaky || 0),
345
+ color: "#00ccd3",
346
+ marker: { symbol: "circle" },
347
+ },
341
348
  ];
342
349
  const runsForTooltip = runs.map((r) => ({
343
350
  runId: r.runId,
@@ -541,6 +548,9 @@ function generateTestHistoryChart(history) {
541
548
  case "skipped":
542
549
  color = "var(--warning-color)";
543
550
  break;
551
+ case "flaky":
552
+ color = "var(--neutral-500)";
553
+ break;
544
554
  default:
545
555
  color = "var(--dark-gray-color)";
546
556
  }
@@ -657,6 +667,9 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
657
667
  case "Failed":
658
668
  color = "var(--danger-color)";
659
669
  break;
670
+ case "Flaky":
671
+ color = "#00ccd3";
672
+ break;
660
673
  case "Skipped":
661
674
  color = "var(--warning-color)";
662
675
  break;
@@ -764,62 +777,264 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
764
777
  * @param {number} [dashboardHeight=600] The height of the dashboard.
765
778
  * @returns {string} The HTML string for the environment dashboard.
766
779
  */
767
- function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
768
- // Format memory for display
769
- const formattedMemory = environment.memory.replace(/(\d+\.\d{2})GB/, "$1 GB");
780
+ function generateEnvironmentSection(environmentData) {
781
+ if (!environmentData) {
782
+ return '<div class="no-data">Environment data not available.</div>';
783
+ }
770
784
 
771
- // Generate a unique ID for the dashboard
772
- const dashboardId = `envDashboard-${Date.now()}-${Math.random()
773
- .toString(36)
774
- .substring(2, 7)}`;
785
+ if (Array.isArray(environmentData)) {
786
+ return `
787
+ <div class="sharded-env-section">
788
+ <div class="sharded-env-header">
789
+ <div class="sharded-env-title-row">
790
+ <div>
791
+ <div class="sharded-env-title">
792
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
793
+ <rect width="20" height="8" x="2" y="2" rx="2" ry="2"></rect>
794
+ <rect width="20" height="8" x="2" y="14" rx="2" ry="2"></rect>
795
+ <line x1="6" x2="6.01" y1="6" y2="6"></line>
796
+ <line x1="6" x2="6.01" y1="18" y2="18"></line>
797
+ </svg>
798
+ System Information
799
+ </div>
800
+ <div class="sharded-env-subtitle">Test execution environment details - ${environmentData.length} shard${environmentData.length > 1 ? "s" : ""}</div>
801
+ </div>
802
+ <div class="env-icon-badge">
803
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
804
+ <path d="M20 16V7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9m16 0H4m16 0 1.28 2.55a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45L4 16"></path>
805
+ </svg>
806
+ </div>
807
+ </div>
808
+ </div>
809
+ <div class="sharded-environments-container">
810
+ <div class="sharded-environments-wrapper">
811
+ ${environmentData
812
+ .map(
813
+ (env, index) => `
814
+ <div class="env-card-wrapper">
815
+ <div class="env-card-badge">Shard ${index + 1}</div>
816
+ ${generateEnvironmentDashboard(env, true)}
817
+ </div>
818
+ `,
819
+ )
820
+ .join("")}
821
+ </div>
822
+ </div>
823
+ </div>
824
+ <style>
825
+ .sharded-env-section {
826
+ border: 1px solid var(--border-light);
827
+ border-radius: 12px;
828
+ background: var(--bg-secondary);
829
+ overflow: hidden;
830
+ }
831
+ .sharded-env-header {
832
+ position: sticky;
833
+ top: 0;
834
+ z-index: 20;
835
+ background: linear-gradient(to bottom right, var(--bg-primary) 0%, var(--bg-secondary) 100%);
836
+ border-bottom: 1px solid var(--border-light);
837
+ padding: 24px 24px 16px;
838
+ }
839
+ .sharded-env-title-row {
840
+ display: flex;
841
+ justify-content: space-between;
842
+ align-items: center;
843
+ }
844
+ .sharded-env-title {
845
+ display: flex;
846
+ align-items: center;
847
+ font-size: 18px;
848
+ font-weight: 600;
849
+ color: var(--text-primary);
850
+ }
851
+ .sharded-env-title svg {
852
+ width: 18px;
853
+ height: 18px;
854
+ margin-right: 8px;
855
+ stroke: currentColor;
856
+ fill: none;
857
+ }
858
+ .sharded-env-subtitle {
859
+ font-size: 13px;
860
+ color: var(--text-secondary);
861
+ margin-top: 4px;
862
+ }
863
+ .sharded-environments-container {
864
+ max-height: 520px;
865
+ overflow-y: auto;
866
+ overflow-x: hidden;
867
+ padding: 16px;
868
+ }
869
+ .sharded-environments-container::-webkit-scrollbar {
870
+ width: 8px;
871
+ }
872
+ .sharded-environments-container::-webkit-scrollbar-track {
873
+ background: var(--bg-tertiary);
874
+ border-radius: 4px;
875
+ }
876
+ .sharded-environments-container::-webkit-scrollbar-thumb {
877
+ background: var(--border-medium);
878
+ border-radius: 4px;
879
+ }
880
+ .sharded-environments-container::-webkit-scrollbar-thumb:hover {
881
+ background: var(--border-color);
882
+ }
883
+ .sharded-environments-wrapper {
884
+ display: grid;
885
+ grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));
886
+ gap: 24px;
887
+ }
888
+ @media (max-width: 768px) {
889
+ .sharded-environments-wrapper {
890
+ grid-template-columns: 1fr;
891
+ }
892
+ }
893
+ .env-card-wrapper {
894
+ position: relative;
895
+ }
896
+ .env-card-badge {
897
+ position: absolute;
898
+ top: -10px;
899
+ right: 16px;
900
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
901
+ color: white;
902
+ padding: 6px 14px;
903
+ border-radius: 20px;
904
+ font-size: 0.75em;
905
+ font-weight: 700;
906
+ text-transform: uppercase;
907
+ letter-spacing: 0.5px;
908
+ z-index: 10;
909
+ box-shadow: 0 4px 6px -1px rgba(99, 102, 241, 0.3);
910
+ }
911
+ </style>
912
+ `;
913
+ }
775
914
 
776
- const cardHeight = Math.floor(dashboardHeight * 0.44);
777
- const cardContentPadding = 16; // px
915
+ return generateEnvironmentDashboard(environmentData);
916
+ }
778
917
 
779
- // Logic for Run Context
780
- const runContext = process.env.CI ? "CI" : "Local Test";
918
+ function generateEnvironmentDashboard(environment, hideHeader = false) {
919
+ const cpuModel = environment.cpu && environment.cpu.model ? environment.cpu.model : "N/A";
920
+ const cpuCores = environment.cpu && environment.cpu.cores ? environment.cpu.cores : "N/A";
921
+ const cpuInfo = `model: ${cpuModel}, cores: ${cpuCores}`;
922
+ const cwdInfo = environment.cwd || "N/A";
781
923
 
782
924
  return `
783
- <div class="environment-dashboard-wrapper" id="${dashboardId}">
925
+ <div class="env-modern-card${hideHeader ? " no-header" : ""}">
784
926
  <style>
785
- .environment-dashboard-wrapper *,
786
- .environment-dashboard-wrapper *::before,
787
- .environment-dashboard-wrapper *::after {
788
- box-sizing: border-box;
789
- }
790
-
791
- .environment-dashboard-wrapper {
792
- --primary-color: var(--primary-dark, #6366f1);
793
- --success-color: var(--success-dark, #10b981);
794
- --warning-color: var(--warning-dark, #f59e0b);
795
-
796
- background-color: var(--bg-tertiary);
797
- padding: 48px;
798
- border-bottom: 1px solid var(--border-light);
927
+ .env-modern-card {
928
+ background: linear-gradient(to bottom right, var(--bg-primary) 0%, var(--bg-secondary) 100%);
929
+ border: 0;
930
+ border-radius: 12px;
931
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
932
+ margin-top: 24px;
933
+ transition: all 0.3s ease;
799
934
  font-family: var(--font-family);
935
+ overflow: hidden;
936
+ }
937
+ .env-modern-card:hover {
938
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
939
+ }
940
+ .env-modern-card {
941
+ margin-bottom: 0;
942
+ }
943
+ .env-card-header {
944
+ display: flex;
945
+ flex-direction: column;
946
+ padding: 24px 24px 12px;
947
+ }
948
+ .env-modern-card.no-header .env-card-header {
949
+ display: none;
950
+ }
951
+ .env-modern-card.no-header {
952
+ margin-top: 0;
953
+ }
954
+ .env-modern-card.no-header .env-card-content {
955
+ padding-top: 24px;
956
+ }
957
+ .env-card-title-row {
958
+ display: flex;
959
+ justify-content: space-between;
960
+ align-items: center;
961
+ }
962
+ .env-card-title {
963
+ display: flex;
964
+ align-items: center;
965
+ font-size: 16px;
966
+ font-weight: 600;
800
967
  color: var(--text-primary);
968
+ transition: color 0.3s;
969
+ }
970
+ .env-modern-card:hover .env-card-title {
971
+ color: var(--primary-dark, #6366f1);
972
+ }
973
+ .env-card-title svg {
974
+ width: 16px;
975
+ height: 16px;
976
+ margin-right: 8px;
977
+ stroke: currentColor;
978
+ fill: none;
979
+ }
980
+ .env-card-subtitle {
981
+ font-size: 12px;
982
+ color: var(--text-secondary);
983
+ margin-top: 4px;
984
+ }
985
+ .env-card-content {
986
+ padding: 0 24px 24px;
987
+ }
988
+ .env-items-grid {
801
989
  display: grid;
802
990
  grid-template-columns: repeat(2, 1fr);
803
- gap: 32px;
804
- font-size: 15px;
805
- transform: translateZ(0);
991
+ gap: 10px;
806
992
  }
807
-
808
- @media (max-width: 768px) {
809
- .environment-dashboard-wrapper {
810
- grid-template-columns: 1fr;
811
- padding: 32px 24px;
812
- }
993
+ @media (min-width: 768px) {
994
+ .env-items-grid {
995
+ grid-template-columns: repeat(4, 1fr);
996
+ }
813
997
  }
814
- @media (max-width: 480px) {
815
- .environment-dashboard-wrapper {
816
- padding: 24px;
817
- }
998
+ .env-item {
999
+ display: flex;
1000
+ align-items: flex-start;
1001
+ gap: 8px;
1002
+ padding: 8px;
1003
+ border-radius: 8px;
1004
+ transition: background-color 0.2s;
1005
+ min-height: 48px;
818
1006
  }
819
-
820
- .env-dashboard-header {
821
- grid-column: 1 / -1;
822
- margin-bottom: 24px;
1007
+ .env-item:hover {
1008
+ background-color: var(--bg-hover);
1009
+ }
1010
+ .env-item-icon {
1011
+ flex-shrink: 0;
1012
+ }
1013
+ .env-item-icon svg {
1014
+ width: 16px;
1015
+ height: 16px;
1016
+ stroke: var(--primary-dark, #6366f1);
1017
+ fill: none;
1018
+ }
1019
+ .env-item-content {
1020
+ flex-grow: 1;
1021
+ min-width: 0;
1022
+ }
1023
+ .env-item-label {
1024
+ font-size: 12px;
1025
+ font-weight: 500;
1026
+ color: var(--text-secondary);
1027
+ white-space: nowrap;
1028
+ overflow: hidden;
1029
+ text-overflow: ellipsis;
1030
+ }
1031
+ .env-item-value {
1032
+ font-size: 12px;
1033
+ font-weight: 600;
1034
+ color: var(--text-primary);
1035
+ word-wrap: break-word;
1036
+ overflow-wrap: break-word;
1037
+ line-height: 1.4;
823
1038
  }
824
1039
 
825
1040
  .env-dashboard-title {
@@ -951,133 +1166,151 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
951
1166
  }
952
1167
  </style>
953
1168
 
954
- <div class="env-dashboard-header">
955
- <div>
956
- <h3 class="env-dashboard-title">System Environment</h3>
957
- <p class="env-dashboard-subtitle">Snapshot of the execution environment</p>
958
- </div>
959
- <span class="env-chip env-chip-primary">${environment.host}</span>
960
- </div>
961
-
962
- <div class="env-card">
963
- <div class="env-card-header">
964
- <svg viewBox="0 0 24 24"><path d="M4 6h16V4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8h-2v10H4V6zm18-2h-4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2H6a2 2 0 0 0-2 2v2h20V6a2 2 0 0 0-2-2zM8 12h8v2H8v-2zm0 4h8v2H8v-2z"/></svg>
965
- Hardware
966
- </div>
967
- <div class="env-card-content">
968
- <div class="env-detail-row">
969
- <span class="env-detail-label">CPU Model</span>
970
- <span class="env-detail-value">${environment.cpu.model}</span>
971
- </div>
972
- <div class="env-detail-row">
973
- <span class="env-detail-label">CPU Cores</span>
974
- <span class="env-detail-value">
975
- <div class="env-cpu-cores">
976
- <span>${environment.cpu.cores || "N/A"} core${environment.cpu.cores !== 1 ? "s" : ""}</span>
977
- </div>
978
- </span>
979
- </div>
980
- <div class="env-detail-row">
981
- <span class="env-detail-label">Memory</span>
982
- <span class="env-detail-value">${formattedMemory}</span>
1169
+ <div class="env-card-header">
1170
+ <div class="env-card-title-row">
1171
+ <div>
1172
+ <div class="env-card-title">
1173
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1174
+ <rect width="20" height="8" x="2" y="2" rx="2" ry="2"></rect>
1175
+ <rect width="20" height="8" x="2" y="14" rx="2" ry="2"></rect>
1176
+ <line x1="6" x2="6.01" y1="6" y2="6"></line>
1177
+ <line x1="6" x2="6.01" y1="18" y2="18"></line>
1178
+ </svg>
1179
+ System Information
1180
+ </div>
1181
+ <div class="env-card-subtitle">Test execution environment details</div>
983
1182
  </div>
984
1183
  </div>
985
1184
  </div>
986
1185
 
987
- <div class="env-card">
988
- <div class="env-card-header">
989
- <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-0.01 18c-2.76 0-5.26-1.12-7.07-2.93A7.973 7.973 0 0 1 4 12c0-2.21.9-4.21 2.36-5.64A7.994 7.994 0 0 1 11.99 4c4.41 0 8 3.59 8 8 0 2.76-1.12 5.26-2.93 7.07A7.973 7.973 0 0 1 11.99 20zM12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"/></svg>
990
- Operating System
991
- </div>
992
- <div class="env-card-content">
993
- <div class="env-detail-row">
994
- <span class="env-detail-label">OS Type</span>
995
- <span class="env-detail-value">${
996
- environment.os.split(" ")[0] === "darwin"
997
- ? "darwin (macOS)"
998
- : environment.os.split(" ")[0] || "Unknown"
999
- }</span>
1000
- </div>
1001
- <div class="env-detail-row">
1002
- <span class="env-detail-label">OS Version</span>
1003
- <span class="env-detail-value">${
1004
- environment.os.split(" ")[1] || "N/A"
1005
- }</span>
1006
- </div>
1007
- <div class="env-detail-row">
1008
- <span class="env-detail-label">Hostname</span>
1009
- <span class="env-detail-value" title="${environment.host}">${
1010
- environment.host
1011
- }</span>
1186
+ <div class="env-card-content">
1187
+ <div class="env-items-grid">
1188
+ <div class="env-item">
1189
+ <div class="env-item-icon">
1190
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1191
+ <path d="M20 16V7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9m16 0H4m16 0 1.28 2.55a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45L4 16"></path>
1192
+ </svg>
1193
+ </div>
1194
+ <div class="env-item-content">
1195
+ <p class="env-item-label">Host</p>
1196
+ <div class="env-item-value" title="${environment.host}">${environment.host}</div>
1197
+ </div>
1012
1198
  </div>
1013
- </div>
1014
- </div>
1015
-
1016
- <div class="env-card">
1017
- <div class="env-card-header">
1018
- <svg viewBox="0 0 24 24"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
1019
- Node.js Runtime
1020
- </div>
1021
- <div class="env-card-content">
1022
- <div class="env-detail-row">
1023
- <span class="env-detail-label">Node Version</span>
1024
- <span class="env-detail-value">${environment.node}</span>
1199
+
1200
+ <div class="env-item">
1201
+ <div class="env-item-icon">
1202
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1203
+ <path d="M20 16V7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9m16 0H4m16 0 1.28 2.55a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45L4 16"></path>
1204
+ </svg>
1205
+ </div>
1206
+ <div class="env-item-content">
1207
+ <p class="env-item-label">Os</p>
1208
+ <div class="env-item-value" title="${environment.os}">${environment.os}</div>
1209
+ </div>
1025
1210
  </div>
1026
- <div class="env-detail-row">
1027
- <span class="env-detail-label">V8 Engine</span>
1028
- <span class="env-detail-value">${environment.v8}</span>
1211
+
1212
+ <div class="env-item">
1213
+ <div class="env-item-icon">
1214
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1215
+ <rect width="16" height="16" x="4" y="4" rx="2"></rect>
1216
+ <rect width="6" height="6" x="9" y="9" rx="1"></rect>
1217
+ <path d="M15 2v2"></path>
1218
+ <path d="M15 20v2"></path>
1219
+ <path d="M2 15h2"></path>
1220
+ <path d="M2 9h2"></path>
1221
+ <path d="M20 15h2"></path>
1222
+ <path d="M20 9h2"></path>
1223
+ <path d="M9 2v2"></path>
1224
+ <path d="M9 20v2"></path>
1225
+ </svg>
1226
+ </div>
1227
+ <div class="env-item-content">
1228
+ <p class="env-item-label">Cpu</p>
1229
+ <div class="env-item-value" title='${JSON.stringify(environment.cpu)}'>${cpuInfo}</div>
1230
+ </div>
1029
1231
  </div>
1030
- <div class="env-detail-row">
1031
- <span class="env-detail-label">Working Dir</span>
1032
- <span class="env-detail-value" title="${environment.cwd}">${
1033
- environment.cwd.length > 25
1034
- ? "..." + environment.cwd.slice(-22)
1035
- : environment.cwd
1036
- }</span>
1232
+
1233
+ <div class="env-item">
1234
+ <div class="env-item-icon">
1235
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1236
+ <path d="M6 19v-3"></path>
1237
+ <path d="M10 19v-3"></path>
1238
+ <path d="M14 19v-3"></path>
1239
+ <path d="M18 19v-3"></path>
1240
+ <path d="M8 11V9"></path>
1241
+ <path d="M16 11V9"></path>
1242
+ <path d="M12 11V9"></path>
1243
+ <path d="M2 15h20"></path>
1244
+ <path d="M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1.1a2 2 0 0 0 0 3.837V17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5.1a2 2 0 0 0 0-3.837Z"></path>
1245
+ </svg>
1246
+ </div>
1247
+ <div class="env-item-content">
1248
+ <p class="env-item-label">Memory</p>
1249
+ <div class="env-item-value" title="${environment.memory}">${environment.memory}</div>
1250
+ </div>
1037
1251
  </div>
1038
- </div>
1039
- </div>
1040
-
1041
- <div class="env-card">
1042
- <div class="env-card-header">
1043
- <svg viewBox="0 0 24 24"><path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4s1.79-4 4-4h.71C7.37 8.69 9.48 7 12 7c2.76 0 5 2.24 5 5v1h2c1.66 0 3 1.34 3 3s-1.34 3-3 3z"/></svg>
1044
- System Summary
1045
- </div>
1046
- <div class="env-card-content">
1047
- <div class="env-detail-row">
1048
- <span class="env-detail-label">Platform Arch</span>
1049
- <span class="env-detail-value">
1050
- <span class="env-chip ${
1051
- environment.os.includes("darwin") &&
1052
- environment.cpu.model.toLowerCase().includes("apple")
1053
- ? "env-chip-success"
1054
- : "env-chip-warning"
1055
- }">
1056
- ${
1057
- environment.os.includes("darwin") &&
1058
- environment.cpu.model.toLowerCase().includes("apple")
1059
- ? "Apple Silicon"
1060
- : environment.cpu.model.toLowerCase().includes("arm") ||
1061
- environment.cpu.model.toLowerCase().includes("aarch64")
1062
- ? "ARM-based"
1063
- : "x86/Other"
1064
- }
1065
- </span>
1066
- </span>
1252
+
1253
+ <div class="env-item">
1254
+ <div class="env-item-icon">
1255
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1256
+ <path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"></path>
1257
+ <path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"></path>
1258
+ <path d="M12 2v2"></path>
1259
+ <path d="M12 22v-2"></path>
1260
+ <path d="m17 20.66-1-1.73"></path>
1261
+ <path d="M11 10.27 7 3.34"></path>
1262
+ <path d="m20.66 17-1.73-1"></path>
1263
+ <path d="m3.34 7 1.73 1"></path>
1264
+ <path d="M14 12h8"></path>
1265
+ <path d="M2 12h2"></path>
1266
+ <path d="m20.66 7-1.73 1"></path>
1267
+ <path d="m3.34 17 1.73-1"></path>
1268
+ <path d="m17 3.34-1 1.73"></path>
1269
+ <path d="m11 13.73-4 6.93"></path>
1270
+ </svg>
1271
+ </div>
1272
+ <div class="env-item-content">
1273
+ <p class="env-item-label">Node</p>
1274
+ <div class="env-item-value" title="${environment.node}">${environment.node}</div>
1275
+ </div>
1067
1276
  </div>
1068
- <div class="env-detail-row">
1069
- <span class="env-detail-label">Memory per Core</span>
1070
- <span class="env-detail-value">${
1071
- environment.cpu.cores > 0
1072
- ? (
1073
- parseFloat(environment.memory) / environment.cpu.cores
1074
- ).toFixed(2) + " GB"
1075
- : "N/A"
1076
- }</span>
1277
+
1278
+ <div class="env-item">
1279
+ <div class="env-item-icon">
1280
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1281
+ <path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"></path>
1282
+ <path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"></path>
1283
+ <path d="M12 2v2"></path>
1284
+ <path d="M12 22v-2"></path>
1285
+ <path d="m17 20.66-1-1.73"></path>
1286
+ <path d="M11 10.27 7 3.34"></path>
1287
+ <path d="m20.66 17-1.73-1"></path>
1288
+ <path d="m3.34 7 1.73 1"></path>
1289
+ <path d="M14 12h8"></path>
1290
+ <path d="M2 12h2"></path>
1291
+ <path d="m20.66 7-1.73 1"></path>
1292
+ <path d="m3.34 17 1.73-1"></path>
1293
+ <path d="m17 3.34-1 1.73"></path>
1294
+ <path d="m11 13.73-4 6.93"></path>
1295
+ </svg>
1296
+ </div>
1297
+ <div class="env-item-content">
1298
+ <p class="env-item-label">V8</p>
1299
+ <div class="env-item-value" title="${environment.v8}">${environment.v8}</div>
1300
+ </div>
1077
1301
  </div>
1078
- <div class="env-detail-row">
1079
- <span class="env-detail-label">Run Context</span>
1080
- <span class="env-detail-value">${runContext}</span>
1302
+
1303
+ <div class="env-item">
1304
+ <div class="env-item-icon">
1305
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1306
+ <path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
1307
+ <polyline points="9 22 9 12 15 12 15 22"></polyline>
1308
+ </svg>
1309
+ </div>
1310
+ <div class="env-item-content">
1311
+ <p class="env-item-label">Working Dir</p>
1312
+ <div class="env-item-value" title="${cwdInfo}">${cwdInfo.length > 30 ? "..." + cwdInfo.slice(-27) : cwdInfo}</div>
1313
+ </div>
1081
1314
  </div>
1082
1315
  </div>
1083
1316
  </div>
@@ -1105,11 +1338,11 @@ function generateWorkerDistributionChart(results) {
1105
1338
  const workerId =
1106
1339
  typeof test.workerId !== "undefined" ? test.workerId : "N/A";
1107
1340
  if (!acc[workerId]) {
1108
- acc[workerId] = { passed: 0, failed: 0, skipped: 0, tests: [] };
1341
+ acc[workerId] = { passed: 0, failed: 0, skipped: 0, flaky: 0, tests: [] };
1109
1342
  }
1110
1343
 
1111
1344
  const status = String(test.status).toLowerCase();
1112
- if (status === "passed" || status === "failed" || status === "skipped") {
1345
+ if (status === "passed" || status === "failed" || status === "skipped" || status === "flaky") {
1113
1346
  acc[workerId][status]++;
1114
1347
  }
1115
1348
 
@@ -1154,6 +1387,7 @@ function generateWorkerDistributionChart(results) {
1154
1387
  const passedData = workerIds.map((id) => workerData[id].passed);
1155
1388
  const failedData = workerIds.map((id) => workerData[id].failed);
1156
1389
  const skippedData = workerIds.map((id) => workerData[id].skipped);
1390
+ const flakyData = workerIds.map((id) => workerData[id].flaky);
1157
1391
 
1158
1392
  const categoriesString = JSON.stringify(categories);
1159
1393
  const fullDataString = JSON.stringify(fullWorkerData);
@@ -1161,6 +1395,7 @@ function generateWorkerDistributionChart(results) {
1161
1395
  { name: "Passed", data: passedData, color: "var(--success-color)" },
1162
1396
  { name: "Failed", data: failedData, color: "var(--danger-color)" },
1163
1397
  { name: "Skipped", data: skippedData, color: "var(--warning-color)" },
1398
+ { name: "Flaky", data: flakyData, color: "#00ccd3" },
1164
1399
  ]);
1165
1400
 
1166
1401
  // The HTML now includes the chart container, the modal, and styles for the modal
@@ -1427,6 +1662,7 @@ function generateTestHistoryContent(trendData) {
1427
1662
  <option value="">All Statuses</option>
1428
1663
  <option value="passed">Passed</option>
1429
1664
  <option value="failed">Failed</option>
1665
+ <option value="flaky">Flaky</option>
1430
1666
  <option value="skipped">Skipped</option>
1431
1667
  </select>
1432
1668
  <button id="clear-history-filters" class="clear-filters-btn">Clear Filters</button>
@@ -1499,6 +1735,8 @@ function getStatusClass(status) {
1499
1735
  return "status-failed";
1500
1736
  case "skipped":
1501
1737
  return "status-skipped";
1738
+ case "flaky":
1739
+ return "status-flaky";
1502
1740
  default:
1503
1741
  return "status-unknown";
1504
1742
  }
@@ -1516,6 +1754,8 @@ function getStatusIcon(status) {
1516
1754
  return "❌";
1517
1755
  case "skipped":
1518
1756
  return "⏭️";
1757
+ case "flaky":
1758
+ return "⚠️";
1519
1759
  default:
1520
1760
  return "❓";
1521
1761
  }
@@ -1556,6 +1796,7 @@ function getSuitesData(results) {
1556
1796
  browser: browser,
1557
1797
  passed: 0,
1558
1798
  failed: 0,
1799
+ flaky: 0,
1559
1800
  skipped: 0,
1560
1801
  count: 0,
1561
1802
  statusOverall: "passed",
@@ -1563,12 +1804,15 @@ function getSuitesData(results) {
1563
1804
  }
1564
1805
  const suite = suitesMap.get(key);
1565
1806
  suite.count++;
1566
- const currentStatus = String(test.status).toLowerCase();
1807
+ let currentStatus = String(test.status).toLowerCase();
1808
+ if (test.outcome === 'flaky') currentStatus = 'flaky';
1567
1809
  if (currentStatus && suite[currentStatus] !== undefined) {
1568
1810
  suite[currentStatus]++;
1569
1811
  }
1570
1812
  if (currentStatus === "failed") suite.statusOverall = "failed";
1571
- else if (currentStatus === "skipped" && suite.statusOverall !== "failed")
1813
+ else if (currentStatus === "flaky" && suite.statusOverall !== "failed")
1814
+ suite.statusOverall = "flaky";
1815
+ else if (currentStatus === "skipped" && suite.statusOverall !== "failed" && suite.statusOverall !== "flaky")
1572
1816
  suite.statusOverall = "skipped";
1573
1817
  });
1574
1818
  return Array.from(suitesMap.values());
@@ -1614,8 +1858,8 @@ function generateSuitesWidget(suitesData) {
1614
1858
 
1615
1859
  // Added inline styles for height consistency with Pie Chart (approx 450px) and scrolling
1616
1860
  return `
1617
- <div class="suites-widget" style="height: 450px; display: flex; flex-direction: column;">
1618
- <div class="suites-header" style="flex-shrink: 0;">
1861
+ <div class="suites-widget fixed-height-widget">
1862
+ <div class="suites-header">
1619
1863
  <h2>Test Suites</h2>
1620
1864
  <span class="summary-badge">${
1621
1865
  suitesData.length
@@ -1625,40 +1869,40 @@ function generateSuitesWidget(suitesData) {
1625
1869
  )} tests</span>
1626
1870
  </div>
1627
1871
 
1628
- <div class="suites-grid-container" style="flex-grow: 1; overflow-y: auto; padding-right: 5px;">
1872
+ <div class="suites-grid-container">
1629
1873
  <div class="suites-grid">
1630
1874
  ${suitesData
1631
1875
  .map(
1632
1876
  (suite) => `
1633
1877
  <div class="suite-card status-${suite.statusOverall}">
1634
1878
  <div class="suite-card-header">
1635
- <h3 class="suite-name" title="${sanitizeHTML(
1636
- suite.name,
1637
- )} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
1879
+ <h3 class="suite-name" title="${sanitizeHTML(suite.name)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
1880
+ <div class="status-indicator-dot status-${suite.statusOverall}" title="${suite.statusOverall.charAt(0).toUpperCase() + suite.statusOverall.slice(1)}"></div>
1638
1881
  </div>
1639
- <div style="margin-bottom: 12px;"><span class="browser-tag" title="🌐 ${sanitizeHTML(suite.browser)}">🌐 ${sanitizeHTML(
1640
- suite.browser,
1641
- )}</span></div>
1882
+
1883
+ <div class="browser-tag" title="🌐Browser: ${sanitizeHTML(suite.browser)}">
1884
+ <span style="font-size: 1.1em;">🌐</span> ${sanitizeHTML(suite.browser)}
1885
+ </div>
1886
+
1642
1887
  <div class="suite-card-body">
1643
- <span class="test-count">${suite.count} test${
1644
- suite.count !== 1 ? "s" : ""
1645
- }</span>
1888
+ <span class="test-count-label">${suite.count} Test${suite.count !== 1 ? "s" : ""}</span>
1646
1889
  <div class="suite-stats">
1647
- ${
1648
- suite.passed > 0
1649
- ? `<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>`
1650
- : ""
1651
- }
1652
- ${
1653
- suite.failed > 0
1654
- ? `<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>`
1655
- : ""
1656
- }
1657
- ${
1658
- suite.skipped > 0
1659
- ? `<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>`
1660
- : ""
1661
- }
1890
+ <span class="stat-pill passed" title="Passed">
1891
+ <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" 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>
1892
+ ${suite.passed}
1893
+ </span>
1894
+ <span class="stat-pill failed" title="Failed">
1895
+ <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" 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>
1896
+ ${suite.failed}
1897
+ </span>
1898
+ <span class="stat-pill flaky" title="Flaky">
1899
+ <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" 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>
1900
+ ${suite.flaky || 0}
1901
+ </span>
1902
+ <span class="stat-pill skipped" title="Skipped">
1903
+ <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" 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>
1904
+ ${suite.skipped}
1905
+ </span>
1662
1906
  </div>
1663
1907
  </div>
1664
1908
  </div>`,
@@ -2037,6 +2281,7 @@ function generateSeverityDistributionChart(results) {
2037
2281
  const data = {
2038
2282
  passed: [0, 0, 0, 0, 0],
2039
2283
  failed: [0, 0, 0, 0, 0],
2284
+ flaky: [0, 0, 0, 0, 0],
2040
2285
  skipped: [0, 0, 0, 0, 0],
2041
2286
  };
2042
2287
 
@@ -2055,6 +2300,8 @@ function generateSeverityDistributionChart(results) {
2055
2300
  status === "interrupted"
2056
2301
  ) {
2057
2302
  data.failed[index]++;
2303
+ } else if (status === "flaky") {
2304
+ data.flaky[index]++;
2058
2305
  } else {
2059
2306
  data.skipped[index]++;
2060
2307
  }
@@ -2068,6 +2315,7 @@ function generateSeverityDistributionChart(results) {
2068
2315
  const seriesData = [
2069
2316
  { name: "Passed", data: data.passed, color: "var(--success-color)" },
2070
2317
  { name: "Failed", data: data.failed, color: "var(--danger-color)" },
2318
+ { name: "Flaky", data: data.flaky, color: "#00ccd3" },
2071
2319
  { name: "Skipped", data: data.skipped, color: "var(--warning-color)" },
2072
2320
  ];
2073
2321
 
@@ -2211,32 +2459,85 @@ function generateHTML(reportData, trendData = null) {
2211
2459
  duration: 0,
2212
2460
  timestamp: new Date().toISOString(),
2213
2461
  };
2214
- const totalTestsOr1 = runSummary.totalTests || 1;
2215
- const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
2216
- const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
2217
- const skipPercentage = Math.round(
2218
- ((runSummary.skipped || 0) / totalTestsOr1) * 100,
2219
- );
2462
+
2220
2463
  const avgTestDuration =
2221
2464
  runSummary.totalTests > 0
2222
2465
  ? formatDuration(runSummary.duration / runSummary.totalTests)
2223
2466
  : "0.0s";
2224
2467
 
2468
+ const flakyCount = (results || []).filter(r => r.outcome === 'flaky').length;
2469
+
2225
2470
  // Calculate retry statistics
2471
+ let retriedTestsCount = 0;
2226
2472
  const totalRetried = (results || []).reduce((acc, test) => {
2227
- if (test.retries && test.retries > 0) {
2228
- return acc + 1;
2473
+ if (test.retryHistory && test.retryHistory.length > 0) {
2474
+ // Filter out any "passed" or "skipped" entries in the history
2475
+ // We only count attempts that actually failed or timed out, triggering a retry.
2476
+ const unsuccessfulRetries = test.retryHistory.filter(attempt =>
2477
+ attempt.status === 'failed' || attempt.status === 'timedout' || attempt.status === 'flaky'
2478
+ );
2479
+ if (unsuccessfulRetries.length > 0) {
2480
+ retriedTestsCount++;
2481
+ }
2482
+ return acc + unsuccessfulRetries.length;
2229
2483
  }
2230
2484
  return acc;
2231
2485
  }, 0);
2232
2486
 
2487
+ // --- RECALCULATE KPI METRICS BASED ON FINAL_STATUS ---
2488
+ let calculatedPassed = 0;
2489
+ let calculatedFailed = 0;
2490
+ let calculatedSkipped = 0;
2491
+ let calculatedFlaky = 0;
2492
+ let calculatedTotal = 0;
2493
+
2494
+ (results || []).forEach(test => {
2495
+ calculatedTotal++;
2496
+ // New Logic: If outcome is 'flaky', it overrides everything.
2497
+ let statusToUse = test.status;
2498
+ if (test.outcome === 'flaky') {
2499
+ statusToUse = 'flaky';
2500
+ } else if (test.status === 'flaky') {
2501
+ // Just in case outcome wasn't set but status was (unlikely with new reporter)
2502
+ statusToUse = 'flaky';
2503
+ } else if (test.retryHistory && test.retryHistory.length > 0 && test.final_status) {
2504
+ statusToUse = test.final_status;
2505
+ }
2506
+
2507
+ // Update test status in place for charts
2508
+ test.status = statusToUse;
2509
+
2510
+ const s = String(statusToUse).toLowerCase();
2511
+ if (s === 'passed') calculatedPassed++;
2512
+ else if (s === 'skipped') calculatedSkipped++;
2513
+ else if (s === 'flaky') calculatedFlaky++;
2514
+ else calculatedFailed++; // failed, timedout, interrupted
2515
+ });
2516
+
2517
+ // Override runSummary counts with our calculated ones if results exist
2518
+ if (results && results.length > 0) {
2519
+ runSummary.passed = calculatedPassed;
2520
+ runSummary.failed = calculatedFailed;
2521
+ runSummary.skipped = calculatedSkipped;
2522
+ runSummary.flaky = calculatedFlaky;
2523
+ runSummary.totalTests = calculatedTotal;
2524
+ }
2525
+
2526
+ const totalTestsOr1 = runSummary.totalTests || 1;
2527
+ const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
2528
+ const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
2529
+ const skipPercentage = Math.round(
2530
+ ((runSummary.skipped || 0) / totalTestsOr1) * 100,
2531
+ );
2532
+ const flakyPercentage = Math.round(((runSummary.flaky || 0) / totalTestsOr1) * 100);
2533
+
2534
+
2233
2535
  // Calculate browser distribution
2234
2536
  const browserStats = (results || []).reduce((acc, test) => {
2235
2537
  let browserName = "unknown";
2236
2538
  if (test.browser) {
2237
- // Extract browser name from strings like "Chrome v143 on Windows 10"
2238
- const match = test.browser.match(/^(\w+)/);
2239
- browserName = match ? match[1] : test.browser;
2539
+ // Use full browser name
2540
+ browserName = test.browser;
2240
2541
  }
2241
2542
  acc[browserName] = (acc[browserName] || 0) + 1;
2242
2543
  return acc;
@@ -2277,6 +2578,10 @@ function generateHTML(reportData, trendData = null) {
2277
2578
  const severity = test.severity || "Medium";
2278
2579
  const severityBadge = `<span class="severity-badge" data-severity="${severity.toLowerCase()}">${severity}</span>`;
2279
2580
 
2581
+ // --- Retry Count Badge (only show if retries occurred) ---
2582
+ const retryCount = (test.retryHistory && test.retryHistory.length > 0) ? test.retryHistory.length : 0;
2583
+ const retryBadge = (test.retryHistory && test.retryHistory.length > 0) ? `<span class="retry-badge">Retry Count: ${retryCount}</span>` : '';
2584
+
2280
2585
  // --- Step Generation ---
2281
2586
  const generateStepsHTML = (steps, depth = 0) => {
2282
2587
  if (!steps || steps.length === 0)
@@ -2285,17 +2590,20 @@ function generateHTML(reportData, trendData = null) {
2285
2590
  .map((step) => {
2286
2591
  const hasNestedSteps = step.steps && step.steps.length > 0;
2287
2592
  const isHook = step.hookType;
2593
+ const isFailedStep = step.isFailedStep === true;
2288
2594
  const stepClass = isHook
2289
2595
  ? `step-hook step-hook-${step.hookType}`
2290
2596
  : "";
2597
+ const failedStepClass = isFailedStep ? " failed-step-highlight" : "";
2291
2598
  const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
2599
+ const failedStepIndicator = isFailedStep ? ` <span class="failed-step-marker">⚠️ Failed at this step</span>` : "";
2292
2600
  return `
2293
- <div class="step-item" style="--depth: ${depth};">
2601
+ <div class="step-item${failedStepClass}" style="--depth: ${depth};">
2294
2602
  <div class="step-header ${stepClass}" role="button" aria-expanded="false">
2295
2603
  <span class="step-icon">${getStatusIcon(step.status)}</span>
2296
2604
  <span class="step-title">${sanitizeHTML(
2297
2605
  step.title,
2298
- )}${hookIndicator}</span>
2606
+ )}${hookIndicator}${failedStepIndicator}</span>
2299
2607
  <span class="step-duration">${formatDuration(
2300
2608
  step.duration,
2301
2609
  )}</span>
@@ -2308,6 +2616,13 @@ function generateHTML(reportData, trendData = null) {
2308
2616
  )}</div>`
2309
2617
  : ""
2310
2618
  }
2619
+ ${
2620
+ step.codeSnippet
2621
+ ? `<div class="code-snippet-section"><pre class="code-snippet">${sanitizeHTML(
2622
+ step.codeSnippet,
2623
+ )}</pre></div>`
2624
+ : ""
2625
+ }
2311
2626
  ${
2312
2627
  step.errorMessage
2313
2628
  ? `<div class="test-error-summary">
@@ -2318,7 +2633,7 @@ function generateHTML(reportData, trendData = null) {
2318
2633
  )}</div>`
2319
2634
  : ""
2320
2635
  }
2321
- <button class="copy-error-btn" onclick="copyErrorToClipboard(this)" style="margin-top: 8px; padding: 6px 12px; background: rgba(248, 113, 113, 0.15); border: 2px solid var(--danger-color); border-radius: 6px; cursor: pointer; font-size: 12px; color: var(--danger-color); font-weight: 600; transition: all 0.2s;" onmouseover="this.style.background='rgba(248, 113, 113, 0.25)'" onmouseout="this.style.background='rgba(248, 113, 113, 0.15)'">Copy Error Prompt</button>
2636
+ <button class="copy-error-btn" onclick="copyErrorToClipboard(this)" style="margin-top: 8px; padding: 6px 12px; background: rgba(248, 113, 113, 0.15); border: 2px solid var(--danger-color); border-radius: 6px; cursor: pointer; font-size: 12px; color: var(--danger-color); font-weight: 600; transition: all 0.2s; align-self: flex-end; width: auto;" onmouseover="this.style.background='rgba(248, 113, 113, 0.25)'" onmouseout="this.style.background='rgba(248, 113, 113, 0.15)'">Copy Error Prompt</button>
2322
2637
  </div>`
2323
2638
  : ""
2324
2639
  }
@@ -2336,43 +2651,36 @@ function generateHTML(reportData, trendData = null) {
2336
2651
  .join("");
2337
2652
  };
2338
2653
 
2339
- return `
2340
- <div class="test-case" data-status="${
2341
- test.status
2342
- }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
2343
- .join(",")
2344
- .toLowerCase()}">
2345
- <div class="test-case-header" role="button" aria-expanded="false">
2346
- <div class="test-case-summary">
2347
- <span class="test-case-title" title="${sanitizeHTML(
2348
- test.name,
2349
- )}">${sanitizeHTML(testTitle)}</span>
2350
- <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
2351
- </div>
2352
- <div class="test-case-meta">
2353
- ${severityBadge}
2354
- ${
2355
- test.tags && test.tags.length > 0
2356
- ? test.tags
2357
- .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
2358
- .join(" ")
2359
- : ""
2360
- }
2361
- </div>
2362
- <div class="test-case-status-duration">
2363
- <span class="status-badge ${getStatusClass(test.status)}">${String(
2364
- test.status,
2365
- ).toUpperCase()}</span>
2366
- <span class="test-duration">${formatDuration(test.duration)}</span>
2367
- </div>
2368
- </div>
2369
- <div class="test-case-content" style="display: none;">
2370
- <p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
2654
+ // Helper for Tab Badges
2655
+ const getSmallStatusBadge = (status) => {
2656
+ const s = String(status).toLowerCase();
2657
+ let colorVar = 'var(--text-tertiary)';
2658
+ if(s === 'passed') colorVar = 'var(--success-color)';
2659
+ else if(s === 'failed') colorVar = 'var(--danger-color)';
2660
+ else if(s === 'skipped') colorVar = 'var(--warning-color)';
2661
+ else if(s === 'flaky') colorVar = '#00ccd3';
2662
+
2663
+ return `<span style="
2664
+ display: inline-block;
2665
+ width: 8px;
2666
+ height: 8px;
2667
+ border-radius: 50%;
2668
+ background-color: ${colorVar};
2669
+ margin-left: 6px;
2670
+ vertical-align: middle;
2671
+ " title="${s}"></span>`;
2672
+ };
2673
+
2674
+ // Function to generate test content HTML (used for base run and retry tabs)
2675
+ const getTestContentHTML = (testData, runSuffix) => {
2676
+ const logId = `stdout-log-${test.id || testIndex}-${runSuffix}`;
2677
+ return `
2678
+ <p><strong>Full Path:</strong> ${sanitizeHTML(testData.name)}</p>
2371
2679
  ${
2372
- test.annotations && test.annotations.length > 0
2373
- ? `<div class="annotations-section">
2374
- <h4 style="margin-top: 0; margin-bottom: 10px; font-size: 1.1em;">📌 Annotations</h4>
2375
- ${test.annotations
2680
+ testData.annotations && testData.annotations.length > 0
2681
+ ? `<div class="annotations-section" style="margin: 12px 0; padding: 12px; background-color: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.3); border-left: 4px solid #8b5cf6; border-radius: 4px;">
2682
+ <h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
2683
+ ${testData.annotations
2376
2684
  .map((annotation, idx) => {
2377
2685
  const isIssueOrBug =
2378
2686
  annotation.type === "issue" ||
@@ -2380,176 +2688,312 @@ function generateHTML(reportData, trendData = null) {
2380
2688
  const descriptionText = annotation.description || "";
2381
2689
  const typeLabel = sanitizeHTML(annotation.type);
2382
2690
  const descriptionHtml =
2383
- isIssueOrBug && descriptionText.match(/^[A-Z]+-\d+$/)
2691
+ isIssueOrBug && descriptionText.match(/^[A-Z]+-\\d+$/)
2384
2692
  ? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
2385
2693
  descriptionText,
2386
- )}" style="color: var(--info-color); text-decoration: underline; cursor: pointer; font-weight: 600;">${sanitizeHTML(
2694
+ )}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
2387
2695
  descriptionText,
2388
2696
  )}</a>`
2389
2697
  : sanitizeHTML(descriptionText);
2390
2698
  const locationText = annotation.location
2391
- ? `<div style="font-size: 0.85em; color: var(--text-tertiary); margin-top: 4px;">Location: ${sanitizeHTML(
2699
+ ? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
2392
2700
  annotation.location.file,
2393
2701
  )}:${annotation.location.line}:${
2394
2702
  annotation.location.column
2395
2703
  }</div>`
2396
2704
  : "";
2397
2705
  return `<div style="margin-bottom: ${
2398
- idx < test.annotations.length - 1 ? "10px" : "0"
2399
- };"><strong>Type:</strong> <span style="background-color: rgba(139, 92, 246, 0.2); padding: 2px 8px; border-radius: 4px; font-size: 0.9em;">${typeLabel}</span>${
2706
+ idx < testData.annotations.length - 1 ? "10px" : "0"
2707
+ };">
2708
+ <strong style="color: #8b5cf6;">Type:</strong> <span style="background-color: rgba(139, 92, 246, 0.2); padding: 2px 8px; border-radius: 4px; font-size: 0.9em;">${typeLabel}</span>
2709
+ ${
2400
2710
  descriptionText
2401
- ? `<br><strong>Description:</strong> ${descriptionHtml}`
2711
+ ? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
2402
2712
  : ""
2403
- }${locationText}</div>`;
2713
+ }
2714
+ ${locationText}
2715
+ </div>`;
2404
2716
  })
2405
2717
  .join("")}
2406
2718
  </div>`
2407
2719
  : ""
2408
2720
  }
2409
2721
  <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
2410
- test.workerId,
2722
+ testData.workerId,
2411
2723
  )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
2412
- test.totalWorkers,
2724
+ testData.totalWorkers,
2413
2725
  )}]</p>
2414
2726
  ${
2415
- test.errorMessage
2416
- ? `<div class="test-error-summary">${formatPlaywrightError(
2417
- test.errorMessage,
2418
- )}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)" style="margin-top: 8px; padding: 4px 8px; background: #f0f0f0; border: 2px solid #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; border-color: #8B0000; color: #8B0000;" onmouseover="this.style.background='#e0e0e0'" onmouseout="this.style.background='#f0f0f0'">Copy Error Prompt</button></div>`
2727
+ testData.errorMessage
2728
+ ? `<div class="test-error-summary"><div class="stack-trace">${formatPlaywrightError(
2729
+ testData.errorMessage,
2730
+ )}</div>
2731
+ <button
2732
+ class="copy-error-btn"
2733
+ onclick="copyErrorToClipboard(this)"
2734
+ style="
2735
+ margin-top: 8px;
2736
+ padding: 6px 12px;
2737
+ background: rgba(248, 113, 113, 0.15);
2738
+ border: 2px solid var(--danger-color);
2739
+ border-radius: 6px;
2740
+ cursor: pointer;
2741
+ font-size: 12px;
2742
+ color: var(--danger-color);
2743
+ font-weight: 600;
2744
+ transition: 0.2s;
2745
+ align-self: flex-end;
2746
+ width: auto;
2747
+ "
2748
+ onmouseover="this.style.background='#e0e0e0'"
2749
+ onmouseout="this.style.background='#f0f0f0'"
2750
+ >
2751
+ Copy Error Prompt
2752
+ </button>
2753
+ </div>`
2419
2754
  : ""
2420
2755
  }
2421
2756
  ${
2422
- test.snippet
2757
+ testData.snippet
2423
2758
  ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
2424
- test.snippet,
2759
+ testData.snippet,
2425
2760
  )}</code></pre></div>`
2426
2761
  : ""
2427
2762
  }
2428
2763
  <h4>Steps</h4>
2429
- <div class="steps-list">${generateStepsHTML(test.steps)}</div>
2430
-
2431
- ${(() => {
2432
- if (!test.stdout || test.stdout.length === 0) return "";
2433
- // FIXED: Now using 'testIndex' which is guaranteed to be defined
2434
- const logId = `stdout-log-${test.id || testIndex}`;
2435
- return `<div class="console-output-section"><h4>Console Output (stdout) <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy</button></h4><div class="log-wrapper"><pre id="${logId}" class="console-log stdout-log" style="background-color: var(--bg-tertiary); color: #f3e8c3; padding: 1.25em; border-radius: 0.85em; line-height: 1.2; border: 1px solid var(--border-light);">${formatPlaywrightError(
2436
- test.stdout.map((line) => sanitizeHTML(line)).join("\n"),
2437
- )}</pre></div></div>`;
2438
- })()}
2439
-
2440
- ${(() => {
2441
- if (!test.stderr || test.stderr.length === 0) return "";
2442
- // FIXED: Using 'testIndex'
2443
- const logId = `stderr-log-${test.id || testIndex}`;
2444
- return `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log" style="background-color: var(--bg-tertiary); color: #f87171; padding: 1.25em; border-radius: 0.85em; line-height: 1.2; border: 1px solid var(--border-light);">${formatPlaywrightError(
2445
- test.stderr.map((line) => sanitizeHTML(line)).join("\n"),
2446
- )}</pre></div>`;
2447
- })()}
2448
-
2449
- ${(() => {
2450
- if (!test.screenshots || test.screenshots.length === 0) return "";
2451
- const screenshotsHTML = test.screenshots
2452
- .map((screenshotPath, sIndex) => {
2453
- try {
2454
- const imagePath = path.resolve(
2455
- DEFAULT_OUTPUT_DIR,
2456
- screenshotPath,
2457
- );
2458
- if (!fsExistsSync(imagePath))
2459
- return `<div class="attachment-item error">Screenshot not found</div>`;
2460
- const base64ImageData =
2461
- readFileSync(imagePath).toString("base64");
2462
- // LAZY LOAD: Using helper with unique ID
2463
- return createLazyMedia(
2464
- base64ImageData,
2465
- "image/png",
2466
- "image",
2467
- sIndex + 1,
2468
- `screenshot-${testIndex}-${sIndex}.png`,
2469
- );
2470
- } catch (e) {
2471
- return `<div class="attachment-item error">Error loading screenshot</div>`;
2472
- }
2473
- })
2474
- .join("");
2475
- return `<div class="attachments-section"><h4>Screenshots</h4><div class="attachments-grid">${screenshotsHTML}</div></div>`;
2476
- })()}
2477
-
2764
+ <div class="steps-list">${generateStepsHTML(testData.steps)}</div>
2478
2765
  ${(() => {
2479
- if (!test.videoPath || test.videoPath.length === 0) return "";
2480
- const videosHTML = test.videoPath
2481
- .map((videoPath, vIndex) => {
2482
- try {
2483
- const videoFilePath = path.resolve(
2484
- DEFAULT_OUTPUT_DIR,
2485
- videoPath,
2486
- );
2487
- if (!fsExistsSync(videoFilePath))
2488
- return `<div class="attachment-item error">Video not found</div>`;
2489
- const videoBase64 =
2490
- readFileSync(videoFilePath).toString("base64");
2491
- const ext = path.extname(videoPath).slice(1).toLowerCase();
2492
- const mime =
2493
- { mp4: "video/mp4", webm: "video/webm" }[ext] ||
2494
- "video/mp4";
2495
- // LAZY LOAD: Using helper with unique ID
2496
- return createLazyMedia(
2497
- videoBase64,
2498
- mime,
2499
- "video",
2500
- vIndex + 1,
2501
- `video-${testIndex}-${vIndex}.${ext}`,
2502
- );
2503
- } catch (e) {
2504
- return `<div class="attachment-item error">Error loading video</div>`;
2505
- }
2506
- })
2507
- .join("");
2508
- return `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${videosHTML}</div></div>`;
2766
+ if (!testData.stdout || testData.stdout.length === 0) return "";
2767
+ return `<div class="console-output-section">
2768
+ <h4>Console Output (stdout)
2769
+ <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy</ button>
2770
+ </h4>
2771
+ <div class="log-wrapper">
2772
+ <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(
2773
+ testData.stdout
2774
+ .map((line) => sanitizeHTML(line))
2775
+ .join("\\n"),
2776
+ )}</pre>
2777
+ </div>
2778
+ </div>`;
2509
2779
  })()}
2510
-
2511
2780
  ${
2512
- test.tracePath
2513
- ? `<div class="attachments-section"><h4>Trace Files</h4><div class="attachments-grid"><div class="attachment-item trace-item"><div class="trace-preview"><span class="trace-icon">📄</span><span class="trace-name">${sanitizeHTML(
2514
- path.basename(test.tracePath),
2515
- )}</span></div><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
2516
- test.tracePath,
2517
- )}" target="_blank" download="${sanitizeHTML(
2518
- path.basename(test.tracePath),
2519
- )}" class="download-trace">Download Trace</a></div></div></div></div></div>`
2781
+ testData.stderr && testData.stderr.length > 0
2782
+ ? `<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(
2783
+ testData.stderr.map((line) => sanitizeHTML(line)).join("\\n"),
2784
+ )}</pre></div>`
2520
2785
  : ""
2521
2786
  }
2522
2787
  ${
2523
- test.attachments && test.attachments.length > 0
2524
- ? `<div class="attachments-section"><h4>Other Attachments</h4><div class="attachments-grid">${test.attachments
2788
+ testData.screenshots && testData.screenshots.length > 0
2789
+ ? `
2790
+ <div class="attachments-section">
2791
+ <h4>Screenshots</h4>
2792
+ <div class="attachments-grid">
2793
+ ${testData.screenshots
2525
2794
  .map(
2526
- (attachment) =>
2527
- `<div class="attachment-item generic-attachment"><div class="attachment-icon">${getAttachmentIcon(
2528
- attachment.contentType,
2529
- )}</div><div class="attachment-caption"><span class="attachment-name" title="${sanitizeHTML(
2530
- attachment.name,
2531
- )}">${sanitizeHTML(
2532
- attachment.name,
2533
- )}</span><span class="attachment-type">${sanitizeHTML(
2534
- attachment.contentType,
2535
- )}</span></div><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
2536
- attachment.path,
2537
- )}" target="_blank" class="view-full">View</a><a href="${sanitizeHTML(
2538
- attachment.path,
2539
- )}" target="_blank" download="${sanitizeHTML(
2540
- attachment.name,
2541
- )}" class="download-trace">Download</a></div></div></div>`,
2795
+ (screenshot, screenshotIndex) => `
2796
+ <div class="attachment-item">
2797
+ <img src="${sanitizeHTML(screenshot)}" alt="Screenshot ${
2798
+ screenshotIndex + 1
2799
+ }">
2800
+ <div class="attachment-info">
2801
+ <div class="trace-actions">
2802
+ <a href="${sanitizeHTML(
2803
+ screenshot,
2804
+ )}" target="_blank" class="view-full">View Full Image</a>
2805
+ <a href="${sanitizeHTML(
2806
+ screenshot,
2807
+ )}" target="_blank" download="screenshot-${Date.now()}-${screenshotIndex}.png">Download</a>
2808
+ </div>
2809
+ </div>
2810
+ </div>
2811
+ `,
2542
2812
  )
2813
+ .join("")}
2814
+ </div>
2815
+ </div>
2816
+ `
2817
+ : ""
2818
+ }
2819
+ ${
2820
+ testData.videoPath && testData.videoPath.length > 0
2821
+ ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${testData.videoPath
2822
+ .map((videoUrl, videoIndex) => {
2823
+ const fixedVideoUrl = sanitizeHTML(videoUrl);
2824
+ const fileExtension = String(fixedVideoUrl)
2825
+ .split(".")
2826
+ .pop()
2827
+ .toLowerCase();
2828
+ const mimeType =
2829
+ {
2830
+ mp4: "video/mp4",
2831
+ webm: "video/webm",
2832
+ ogg: "video/ogg",
2833
+ mov: "video/quicktime",
2834
+ avi: "video/x-msvideo",
2835
+ }[fileExtension] || "video/mp4";
2836
+ return `<div class="attachment-item video-item">
2837
+ <video controls width="100%" height="auto" title="Video ${
2838
+ videoIndex + 1
2839
+ }">
2840
+ <source src="${sanitizeHTML(
2841
+ fixedVideoUrl,
2842
+ )}" type="${mimeType}">
2843
+ Your browser does not support the video tag.
2844
+ </video>
2845
+ <div class="attachment-info">
2846
+ <div class="trace-actions">
2847
+ <a href="${sanitizeHTML(
2848
+ fixedVideoUrl,
2849
+ )}" target="_blank" download="video-${Date.now()}-${videoIndex}.${fileExtension}">Download</a>
2850
+ </div>
2851
+ </div>
2852
+ </div>`;
2853
+ })
2543
2854
  .join("")}</div></div>`
2544
2855
  : ""
2545
2856
  }
2546
2857
  ${
2547
- test.codeSnippet
2548
- ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
2549
- sanitizeHTML(test.codeSnippet),
2550
- )}</code></pre></div>`
2858
+ testData.tracePath
2859
+ ? `
2860
+ <div class="attachments-section">
2861
+ <h4>Trace Files</h4>
2862
+ <div class="attachments-grid">
2863
+ <div class="attachment-item trace-item">
2864
+ <div class="trace-preview">
2865
+ <span class="trace-icon">📄</span>
2866
+ <span class="trace-name">${sanitizeHTML(
2867
+ path.basename(testData.tracePath),
2868
+ )}</span>
2869
+ </div>
2870
+ <div class="attachment-info">
2871
+ <div class="trace-actions">
2872
+ <a href="${sanitizeHTML(
2873
+ sanitizeHTML(testData.tracePath),
2874
+ )}" target="_blank" download="${sanitizeHTML(
2875
+ path.basename(testData.tracePath),
2876
+ )}" class="download-trace">Download Trace</a>
2877
+ </div>
2878
+ </div>
2879
+ </div>
2880
+ </div>
2881
+ </div>
2882
+ `
2883
+ : ""
2884
+ }
2885
+ ${
2886
+ testData.attachments && testData.attachments.length > 0
2887
+ ? `
2888
+ <div class="attachments-section">
2889
+ <h4>Other Attachments</h4>
2890
+ <div class="attachments-grid">
2891
+ ${testData.attachments
2892
+ .map(
2893
+ (attachment) => `
2894
+ <div class="attachment-item generic-attachment">
2895
+ <div class="attachment-icon">${getAttachmentIcon(
2896
+ attachment.contentType,
2897
+ )}</div>
2898
+ <div class="attachment-caption">
2899
+ <span class="attachment-name" title="${sanitizeHTML(
2900
+ attachment.name,
2901
+ )}">${sanitizeHTML(attachment.name)}</span>
2902
+ <span class="attachment-type">${sanitizeHTML(
2903
+ attachment.contentType,
2904
+ )}</span>
2905
+ </div>
2906
+ <div class="attachment-info">
2907
+ <div class="trace-actions">
2908
+ <a href="${sanitizeHTML(
2909
+ sanitizeHTML(attachment.path),
2910
+ )}" target="_blank" class="view-full">View</a>
2911
+ <a href="${sanitizeHTML(
2912
+ sanitizeHTML(attachment.path),
2913
+ )}" target="_blank" download="${sanitizeHTML(
2914
+ attachment.name,
2915
+ )}" class="download-trace">Download</a>
2916
+ </div>
2917
+ </div>
2918
+ </div>
2919
+ `,
2920
+ )
2921
+ .join("")}
2922
+ </div>
2923
+ </div>
2924
+ `
2551
2925
  : ""
2552
2926
  }
2927
+ `;
2928
+ };
2929
+
2930
+
2931
+ // Determine header status: use final_status if retried, else normal status
2932
+ const headerStatus = (test.retryHistory && test.retryHistory.length > 0 && test.final_status)
2933
+ ? test.final_status
2934
+ : test.status;
2935
+
2936
+ const outcomeBadge = (test.outcome && test.outcome !== 'flaky')
2937
+ ? `<span class="outcome-badge ${test.outcome}">${test.outcome}</span>`
2938
+ : '';
2939
+
2940
+ return `
2941
+ <div class="test-case" data-status="${
2942
+ headerStatus
2943
+ }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
2944
+ .join(",")
2945
+ .toLowerCase()}">
2946
+ <div class="test-case-header" role="button" aria-expanded="false">
2947
+ <div class="test-case-summary">
2948
+ <span class="test-case-title" title="${sanitizeHTML(
2949
+ test.name,
2950
+ )}">${sanitizeHTML(testTitle)}</span>
2951
+ <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
2952
+ </div>
2953
+ <div class="test-case-meta">
2954
+ ${severityBadge}
2955
+ ${retryBadge}
2956
+ ${outcomeBadge}
2957
+ ${
2958
+ test.tags && test.tags.length > 0
2959
+ ? test.tags
2960
+ .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
2961
+ .join(" ")
2962
+ : ""
2963
+ }
2964
+ </div>
2965
+ <div class="test-case-status-duration">
2966
+ <span class="status-badge ${getStatusClass(headerStatus)}">${String(
2967
+ headerStatus,
2968
+ ).toUpperCase()}</span>
2969
+ <span class="test-duration">${formatDuration(test.duration)}</span>
2970
+ </div>
2971
+ </div>
2972
+ <div class="test-case-content" style="display: none;">
2973
+ ${test.retryHistory && test.retryHistory.length > 0 ? `
2974
+ <div class="retry-tabs-container">
2975
+ <div class="retry-tabs-header">
2976
+ <button class="retry-tab active" onclick="switchRetryTab(event, 'base-run-${test.id}')">
2977
+ Base Run ${getSmallStatusBadge(test.final_status || test.status)}
2978
+ </button>
2979
+ ${test.retryHistory.map((retry, idx) => `
2980
+ <button class="retry-tab" onclick="switchRetryTab(event, 'retry-${idx + 1}-${test.id}')">
2981
+ Retry ${idx + 1} ${getSmallStatusBadge(retry.final_status || retry.status)}
2982
+ </button>
2983
+ `).join('')}
2984
+ </div>
2985
+
2986
+ <div id="base-run-${test.id}" class="retry-tab-content active">
2987
+ ${getTestContentHTML(test, 'base')}
2988
+ </div>
2989
+
2990
+ ${test.retryHistory.map((retry, idx) => `
2991
+ <div id="retry-${idx + 1}-${test.id}" class="retry-tab-content" style="display: none;">
2992
+ ${getTestContentHTML(retry, `retry-${idx + 1}`)}
2993
+ </div>
2994
+ `).join('')}
2995
+ </div>
2996
+ ` : getTestContentHTML(test, 'single')}
2553
2997
  </div>
2554
2998
  </div>`;
2555
2999
  })
@@ -2583,7 +3027,8 @@ function generateHTML(reportData, trendData = null) {
2583
3027
  --success-color: #34d399; --success-dark: #10b981; --success-light: #6ee7b7;
2584
3028
  --danger-color: #f87171; --danger-dark: #ef4444; --danger-light: #fca5a5;
2585
3029
  --warning-color: #fbbf24; --warning-dark: #f59e0b; --warning-light: #fcd34d;
2586
- --info-color: #9ca3af;
3030
+ --info-color: #9ca3af;
3031
+ --flaky-color: #00ccd3;
2587
3032
  --text-primary: #f9fafb; --text-secondary: #e5e7eb; --text-tertiary: #d1d5db;
2588
3033
  --bg-primary: #000000; --bg-secondary: #0a0a0a; --bg-tertiary: #050505;
2589
3034
  --bg-card: #0d0d0d; --bg-card-hover: #121212;
@@ -2591,6 +3036,8 @@ function generateHTML(reportData, trendData = null) {
2591
3036
  --light-gray-color: #262626; --medium-gray-color: #333333; --dark-gray-color: #a3a3a3;
2592
3037
  --text-color: #f9fafb; --text-color-secondary: #e5e7eb; --border-color: #262626;
2593
3038
  --card-background-color: #0d0d0d;
3039
+ --neutral-100: #171717; --neutral-200: #262626; --neutral-300: #404040;
3040
+ --bg-hover: #171717;
2594
3041
  --font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
2595
3042
  --radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-xl: 20px; --radius-2xl: 24px;
2596
3043
  --shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.8);
@@ -2682,7 +3129,6 @@ function generateHTML(reportData, trendData = null) {
2682
3129
  background: var(--gradient-card);
2683
3130
  border-radius: 14px;
2684
3131
  box-shadow: var(--shadow-md);
2685
- border: 1px solid var(--border-light);
2686
3132
  overflow: hidden;
2687
3133
  }
2688
3134
  .run-info-item {
@@ -2856,8 +3302,17 @@ function generateHTML(reportData, trendData = null) {
2856
3302
  color: #f9fafb;
2857
3303
  text-transform: capitalize;
2858
3304
  font-size: 1.05em;
3305
+ white-space: nowrap;
3306
+ overflow: hidden;
3307
+ text-overflow: ellipsis;
3308
+ flex: 1;
3309
+ min-width: 0;
3310
+ margin-right: 8px;
2859
3311
  }
2860
3312
  .browser-stats {
3313
+ color: #9ca3af;
3314
+ white-space: nowrap;
3315
+ flex-shrink: 0;
2861
3316
  color: #9ca3af;
2862
3317
  font-weight: 700;
2863
3318
  font-size: 0.95em;
@@ -2893,57 +3348,7 @@ function generateHTML(reportData, trendData = null) {
2893
3348
  padding: 10px !important;
2894
3349
  border-radius: 6px !important;
2895
3350
  }
2896
- .env-dashboard {
2897
- background: var(--gradient-card);
2898
- border: 1px solid var(--border-light);
2899
- border-radius: 12px;
2900
- padding: 24px;
2901
- margin-top: 20px;
2902
- box-shadow: var(--shadow-md);
2903
- }
2904
- .env-dashboard-title {
2905
- font-size: 1.2em;
2906
- font-weight: 700;
2907
- color: #f9fafb;
2908
- margin-bottom: 8px;
2909
- }
2910
- .env-dashboard-subtitle {
2911
- font-size: 0.9em;
2912
- color: #9ca3af;
2913
- margin-bottom: 16px;
2914
- }
2915
- .env-grid {
2916
- display: grid;
2917
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
2918
- gap: 12px;
2919
- }
2920
- .env-card {
2921
- background: var(--bg-secondary);
2922
- border: 1px solid var(--border-light);
2923
- border-radius: 8px;
2924
- padding: 14px;
2925
- transition: all 0.3s ease;
2926
- }
2927
- .env-card:hover {
2928
- transform: translateY(-3px);
2929
- border-color: var(--primary-color);
2930
- box-shadow: var(--shadow-lg), var(--glow-primary);
2931
- background: var(--bg-card);
2932
- }
2933
- .env-card-header {
2934
- font-size: 0.85em;
2935
- color: #9ca3af;
2936
- margin-bottom: 6px;
2937
- text-transform: uppercase;
2938
- letter-spacing: 0.5px;
2939
- font-weight: 600;
2940
- }
2941
- .env-card-value {
2942
- font-size: 1.1em;
2943
- color: #f9fafb;
2944
- font-weight: 700;
2945
- word-break: break-word;
2946
- }
3351
+
2947
3352
  .suites-widget {
2948
3353
  background: var(--bg-card);
2949
3354
  border: 1px solid var(--border-light);
@@ -2961,17 +3366,9 @@ function generateHTML(reportData, trendData = null) {
2961
3366
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
2962
3367
  gap: 20px;
2963
3368
  }
3369
+ /* Updated Suite Cards in Main Block */
2964
3370
  .suite-card {
2965
- background: var(--bg-secondary);
2966
- border: 1px solid var(--border-light);
2967
- border-radius: 8px;
2968
- padding: 20px;
2969
- transition: all 0.2s ease;
2970
- }
2971
- .suite-card:hover {
2972
- transform: translateY(-2px);
2973
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
2974
- border-color: var(--primary-color);
3371
+ /* See line ~3455 for main definition */
2975
3372
  }
2976
3373
  .suite-name {
2977
3374
  font-size: 1.1em;
@@ -3084,6 +3481,16 @@ function generateHTML(reportData, trendData = null) {
3084
3481
  .summary-card.status-skipped .value {
3085
3482
  color: #f59e0b;
3086
3483
  }
3484
+ .summary-card.flaky-status {
3485
+ background: rgba(0, 204, 211, 0.05);
3486
+ }
3487
+ .summary-card.flaky-status:hover {
3488
+ background: rgba(0, 204, 211, 0.15);
3489
+ box-shadow: 0 4px 12px rgba(0, 204, 211, 0.2);
3490
+ }
3491
+ .summary-card.flaky-status .value {
3492
+ color: #00ccd3;
3493
+ }
3087
3494
  .summary-card:not([class*='status-']) .value {
3088
3495
  color: #f9fafb;
3089
3496
  }
@@ -3146,70 +3553,171 @@ function generateHTML(reportData, trendData = null) {
3146
3553
  .status-badge-small-tooltip.status-unknown {
3147
3554
  background-color: #9ca3af;
3148
3555
  }
3149
- .suites-header {
3150
- display: flex;
3151
- justify-content: space-between;
3152
- align-items: center;
3153
- margin-bottom: 20px;
3154
- }
3155
- .summary-badge {
3156
- background-color: var(--border-light);
3157
- color: var(--text-secondary);
3158
- padding: 7px 14px;
3159
- border-radius: 16px;
3160
- font-size: 0.9em;
3556
+ .suites-header {
3557
+ flex-shrink: 0;
3558
+ display: flex;
3559
+ justify-content: space-between;
3560
+ align-items: center;
3561
+ margin-bottom: 20px;
3161
3562
  }
3162
- .suites-grid {
3163
- display: grid;
3164
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
3165
- gap: 20px;
3563
+ .summary-badge { background-color: var(--border-light); color: var(--text-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
3564
+ .suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
3565
+ .suites-widget {
3566
+ display: flex;
3567
+ flex-direction: column;
3166
3568
  }
3167
- .suite-card {
3168
- border: none;
3169
- border-left: 4px solid var(--border-light);
3170
- padding: 24px;
3171
- background-color: var(--bg-card);
3172
- transition: all 0.15s ease;
3569
+ .fixed-height-widget {
3570
+ height: 450px;
3173
3571
  }
3174
- .suite-card:hover {
3175
- background: rgba(255, 255, 255, 0.03);
3176
- border-left-color: var(--border-medium);
3572
+ .suites-grid-container {
3573
+ flex-grow: 1;
3574
+ overflow-y: auto;
3575
+ padding-right: 5px;
3177
3576
  }
3178
- .suite-card.status-passed {
3179
- border-left-color: #10b981;
3577
+
3578
+ @media (max-width: 768px) {
3579
+ .fixed-height-widget {
3580
+ height: auto;
3581
+ max-height: 600px;
3582
+ }
3180
3583
  }
3181
- .suite-card.status-passed:hover {
3182
- background: rgba(16, 185, 129, 0.05);
3584
+ .suite-card {
3585
+ background: var(--bg-card); /* Changed from #ffffff */
3586
+ border: 1px solid var(--border-medium); /* Changed from border-light for better contrast */
3587
+ border-radius: 16px;
3588
+ padding: 24px;
3589
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); /* Darker shadow */
3590
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3591
+ display: flex;
3592
+ flex-direction: column;
3593
+ height: 100%;
3594
+ position: relative;
3595
+ overflow: hidden;
3183
3596
  }
3184
- .suite-card.status-failed {
3185
- border-left-color: #ef4444;
3597
+ .suite-card:hover {
3598
+ transform: translateY(-4px);
3599
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4);
3600
+ background: var(--bg-card-hover);
3601
+ border-color: var(--primary-dark);
3186
3602
  }
3187
- .suite-card.status-failed:hover {
3188
- background: rgba(239, 68, 68, 0.05);
3603
+ .suite-card::before {
3604
+ content: '';
3605
+ position: absolute;
3606
+ top: 0;
3607
+ left: 0;
3608
+ width: 100%;
3609
+ height: 4px;
3610
+ background: var(--neutral-200);
3611
+ opacity: 0.8;
3612
+ transition: background 0.3s ease;
3613
+ }
3614
+ .suite-card.status-passed::before { background: var(--success-color); }
3615
+ .suite-card.status-failed::before { background: var(--danger-color); }
3616
+ .suite-card.status-flaky::before { background: #00ccd3; }
3617
+ .suite-card.status-skipped::before { background: var(--warning-color); }
3618
+
3619
+ .suite-card.status-skipped::before { background: var(--warning-color); }
3620
+
3621
+ /* Outcome Badge */
3622
+ .outcome-badge {
3623
+ background-color: var(--secondary-color);
3624
+ color: #000;
3625
+ padding: 2px 8px;
3626
+ border-radius: 4px;
3627
+ font-size: 0.75em;
3628
+ font-weight: 700;
3629
+ text-transform: uppercase;
3630
+ margin-right: 8px;
3631
+ letter-spacing: 0.5px;
3189
3632
  }
3190
- .suite-card.status-skipped {
3191
- border-left-color: #f59e0b;
3633
+ .outcome-badge.flaky {
3634
+ background-color: #00ccd3;
3635
+ color: #fff;
3192
3636
  }
3193
- .suite-card.status-skipped:hover {
3194
- background: rgba(245, 158, 11, 0.05);
3637
+
3638
+ .suite-card-header {
3639
+ display: flex;
3640
+ justify-content: space-between;
3641
+ align-items: flex-start;
3642
+ margin-bottom: 16px;
3195
3643
  }
3196
- .suite-card-header {
3197
- display: flex;
3198
- justify-content: space-between;
3199
- align-items: flex-start;
3200
- margin-bottom: 12px;
3644
+ .suite-name {
3645
+ font-size: 1.15em;
3646
+ font-weight: 700;
3647
+ color: var(--text-primary);
3648
+ line-height: 1.4;
3649
+ display: -webkit-box;
3650
+ -webkit-line-clamp: 2;
3651
+ -webkit-box-orient: vertical;
3652
+ overflow: hidden;
3653
+ margin-right: 12px;
3201
3654
  }
3202
- .suite-name {
3203
- font-weight: 600;
3204
- font-size: 1.05em;
3205
- color: #f9fafb;
3206
- margin-right: 10px;
3207
- word-break: break-word;
3655
+ .status-indicator-dot {
3656
+ width: 10px;
3657
+ height: 10px;
3658
+ border-radius: 50%;
3659
+ flex-shrink: 0;
3660
+ margin-top: 6px;
3208
3661
  }
3662
+ .status-indicator-dot.status-passed { background-color: var(--success-color); box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.15); }
3663
+ .status-indicator-dot.status-failed { background-color: var(--danger-color); box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.15); }
3664
+ .status-indicator-dot.status-skipped { background-color: var(--warning-color); box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.15); }
3665
+
3209
3666
  .browser-tag {
3667
+ font-size: 0.8em;
3668
+ font-weight: 600;
3669
+ background: var(--bg-secondary);
3670
+ color: var(--text-secondary);
3671
+ padding: 4px 10px;
3672
+ border-radius: 20px;
3673
+ border: 1px solid var(--border-light);
3674
+ display: inline-flex;
3675
+ align-items: center;
3676
+ gap: 6px;
3677
+ margin-bottom: 20px;
3678
+ align-self: flex-start;
3679
+ box-shadow: none;
3680
+ text-shadow: none;
3681
+ }
3682
+ .browser-tag:hover {
3683
+ /* Remove hover effect from previous */
3684
+ }
3685
+
3686
+ .suite-card-body {
3687
+ margin-top: auto;
3688
+ }
3689
+
3690
+ .test-count-label {
3210
3691
  font-size: 0.85em;
3211
3692
  font-weight: 600;
3212
- background: linear-gradient(135deg, rgba(96, 165, 250, 0.2) 0%, rgba(59, 130, 246, 0.15) 100%);
3693
+ color: var(--text-tertiary);
3694
+ text-transform: uppercase;
3695
+ letter-spacing: 0.05em;
3696
+ margin-bottom: 8px;
3697
+ display: block;
3698
+ }
3699
+
3700
+ .suite-stats {
3701
+ display: flex;
3702
+ gap: 8px;
3703
+ background: var(--bg-secondary);
3704
+ padding: 10px 14px;
3705
+ border-radius: 10px;
3706
+ justify-content: space-between;
3707
+ }
3708
+
3709
+ .stat-pill {
3710
+ display: flex;
3711
+ align-items: center;
3712
+ gap: 6px;
3713
+ font-size: 0.9em;
3714
+ font-weight: 600;
3715
+ }
3716
+ .stat-pill svg { width: 14px; height: 14px; }
3717
+ .stat-pill.passed { color: var(--success-dark); }
3718
+ .stat-pill.failed { color: var(--danger-dark); }
3719
+ .stat-pill.flaky { color: #00ccd3; }
3720
+ .stat-pill.skipped { color: var(--warning-dark); }
3213
3721
  color: #93c5fd;
3214
3722
  padding: 6px 12px;
3215
3723
  border-radius: var(--radius-sm);
@@ -3403,6 +3911,10 @@ function generateHTML(reportData, trendData = null) {
3403
3911
  .status-badge.status-skipped {
3404
3912
  background: var(--warning-color);
3405
3913
  }
3914
+ .status-badge.status-flaky {
3915
+ background-color: #00ccd3;
3916
+ color: #fff;
3917
+ }
3406
3918
  .status-badge.status-unknown {
3407
3919
  background: var(--dark-gray-color);
3408
3920
  }
@@ -3458,6 +3970,65 @@ function generateHTML(reportData, trendData = null) {
3458
3970
  border-color: rgba(148, 163, 184, 0.25);
3459
3971
  }
3460
3972
 
3973
+ /* --- RETRY COUNT BADGE --- */
3974
+ .retry-badge {
3975
+ display: inline-flex;
3976
+ align-items: center;
3977
+ padding: 5px 12px;
3978
+ border-radius: 12px;
3979
+ font-size: 0.75rem;
3980
+ font-weight: 600;
3981
+ background: rgba(147, 51, 234, 0.15);
3982
+ color: #a855f7;
3983
+ border: 1px solid rgba(147, 51, 234, 0.3);
3984
+ margin-left: 8px;
3985
+ }
3986
+
3987
+ /* --- RETRY TABS --- */
3988
+ .retry-tabs-container {
3989
+ margin-top: 16px;
3990
+ }
3991
+
3992
+ .retry-tabs-header {
3993
+ display: flex;
3994
+ gap: 8px;
3995
+ border-bottom: 2px solid var(--border-medium);
3996
+ margin-bottom: 20px;
3997
+ flex-wrap: wrap;
3998
+ }
3999
+
4000
+ .retry-tab {
4001
+ padding: 10px 20px;
4002
+ background: transparent;
4003
+ border: none;
4004
+ border-bottom: 3px solid transparent;
4005
+ cursor: pointer;
4006
+ font-size: 0.95rem;
4007
+ font-weight: 600;
4008
+ color: var(--text-color-secondary);
4009
+ transition: all 0.2s ease;
4010
+ }
4011
+
4012
+ .retry-tab:hover {
4013
+ color: var(--primary-color);
4014
+ background: rgba(147, 51, 234, 0.05);
4015
+ }
4016
+
4017
+ .retry-tab.active {
4018
+ color: #a855f7;
4019
+ border-bottom-color: #a855f7;
4020
+ background: rgba(147, 51, 234, 0.1);
4021
+ }
4022
+
4023
+ .retry-tab-content {
4024
+ animation: fadeIn 0.3s ease-in;
4025
+ }
4026
+
4027
+ @keyframes fadeIn {
4028
+ from { opacity: 0; }
4029
+ to { opacity: 1; }
4030
+ }
4031
+
3461
4032
  .tag {
3462
4033
  display: inline-flex;
3463
4034
  align-items: center;
@@ -3507,7 +4078,9 @@ function generateHTML(reportData, trendData = null) {
3507
4078
  background-color: rgba(248,113,113,0.08);
3508
4079
  border: 1px solid rgba(248,113,113,0.25);
3509
4080
  border-left: 4px solid var(--danger-color);
3510
- border-radius: 4px;
4081
+ border-radius: 4px;
4082
+ display: flex;
4083
+ flex-direction: column;
3511
4084
  }
3512
4085
  .test-error-summary h4 {
3513
4086
  color: #ef4444;
@@ -3758,6 +4331,40 @@ function generateHTML(reportData, trendData = null) {
3758
4331
  font-size: 0.9em;
3759
4332
  border: 1px solid var(--border-light);
3760
4333
  }
4334
+ .failed-step-highlight {
4335
+ border-left: 4px solid var(--danger-color) !important;
4336
+ background-color: rgba(244,67,54,0.03);
4337
+ }
4338
+ .failed-step-highlight .step-header {
4339
+ background-color: rgba(244,67,54,0.05);
4340
+ border-color: rgba(244,67,54,0.3);
4341
+ }
4342
+ .failed-step-marker {
4343
+ display: inline-block;
4344
+ margin-left: 10px;
4345
+ padding: 2px 8px;
4346
+ background-color: var(--danger-color);
4347
+ color: white;
4348
+ border-radius: 4px;
4349
+ font-size: 0.85em;
4350
+ font-weight: 600;
4351
+ }
4352
+ .code-snippet-section {
4353
+ margin: 12px 0;
4354
+ }
4355
+ .code-snippet {
4356
+ background-color: #f8f9fa;
4357
+ border: 1px solid #e1e4e8;
4358
+ border-radius: 6px;
4359
+ padding: 12px;
4360
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
4361
+ font-size: 0.9em;
4362
+ line-height: 1.5;
4363
+ overflow-x: auto;
4364
+ color: #24292e;
4365
+ margin: 0;
4366
+ white-space: pre;
4367
+ }
3761
4368
  .nested-steps {
3762
4369
  margin-top: 12px;
3763
4370
  }
@@ -3941,6 +4548,10 @@ function generateHTML(reportData, trendData = null) {
3941
4548
  .status-badge-small.status-skipped {
3942
4549
  background-color: #f59e0b;
3943
4550
  }
4551
+ .status-badge-small.status-flaky {
4552
+ background-color: #00ccd3;
4553
+ color: #fff;
4554
+ }
3944
4555
  .status-badge-small.status-unknown {
3945
4556
  background-color: var(--dark-gray-color);
3946
4557
  }
@@ -5166,8 +5777,17 @@ function generateHTML(reportData, trendData = null) {
5166
5777
  color: #f9fafb;
5167
5778
  text-transform: capitalize;
5168
5779
  font-size: 1.05em;
5780
+ white-space: nowrap;
5781
+ overflow: hidden;
5782
+ text-overflow: ellipsis;
5783
+ flex: 1;
5784
+ min-width: 0;
5785
+ margin-right: 8px;
5169
5786
  }
5170
5787
  .browser-stats {
5788
+ color: #9ca3af;
5789
+ white-space: nowrap;
5790
+ flex-shrink: 0;
5171
5791
  color: #9ca3af;
5172
5792
  font-weight: 700;
5173
5793
  font-size: 0.95em;
@@ -5632,31 +6252,33 @@ function generateHTML(reportData, trendData = null) {
5632
6252
  <div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
5633
6253
  runSummary.skipped || 0
5634
6254
  }</div><div class="trend-percentage">${skipPercentage}%</div></div>
5635
- <div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
6255
+ <div class="summary-card flaky-status"><h3>Flaky</h3><div class="value">${runSummary.flaky || 0}</div>
6256
+ <div class="trend-percentage">${flakyPercentage}%</div></div>
5636
6257
  <div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
5637
6258
  runSummary.duration,
5638
- )}</div></div>
6259
+ )}</div><div class="trend-percentage">Avg. Test Duration ${avgTestDuration}</div></div>
5639
6260
  <div class="summary-card">
5640
- <h3>🔄 Retry Count</h3>
6261
+ <h3>Total Retry Count</h3>
5641
6262
  <div class="value">${totalRetried}</div>
6263
+ <div class="trend-percentage">Test Retried ${retriedTestsCount}</div>
5642
6264
  </div>
5643
6265
  <div class="summary-card">
5644
6266
  <h3>🌐 Browser Distribution <span style="font-size: 0.7em; color: var(--text-color-secondary); font-weight: 400;">(${browserBreakdown.length} total)</span></h3>
5645
6267
  <div class="browser-breakdown" style="max-height: 200px; overflow-y: auto; padding-right: 4px;">
5646
6268
  ${browserBreakdown
5647
- .slice(0, 5)
6269
+ .slice(0, 3)
5648
6270
  .map(
5649
6271
  (b) =>
5650
6272
  `<div class="browser-item">
5651
- <span class="browser-name">${sanitizeHTML(b.browser)}</span>
6273
+ <span class="browser-name" title="${sanitizeHTML(b.browser)}">${sanitizeHTML(b.browser)}</span>
5652
6274
  <span class="browser-stats">${b.percentage}% (${b.count})</span>
5653
6275
  </div>`,
5654
6276
  )
5655
6277
  .join("")}
5656
6278
  ${
5657
- browserBreakdown.length > 5
6279
+ browserBreakdown.length > 3
5658
6280
  ? `<div class="browser-item" style="opacity: 0.6; font-style: italic; justify-content: center; border-top: 1px solid var(--border-light); margin-top: 8px; padding-top: 8px;">
5659
- <span>+${browserBreakdown.length - 5} more browsers</span>
6281
+ <span>+${browserBreakdown.length - 3} more browsers</span>
5660
6282
  </div>`
5661
6283
  : ""
5662
6284
  }
@@ -5669,17 +6291,13 @@ function generateHTML(reportData, trendData = null) {
5669
6291
  [
5670
6292
  { label: "Passed", value: runSummary.passed },
5671
6293
  { label: "Failed", value: runSummary.failed },
6294
+ { label: "Flaky", value: runSummary.flaky || 0 },
5672
6295
  { label: "Skipped", value: runSummary.skipped || 0 },
5673
6296
  ],
5674
6297
  400,
5675
6298
  390,
5676
6299
  )}
5677
- ${
5678
- runSummary.environment &&
5679
- Object.keys(runSummary.environment).length > 0
5680
- ? generateEnvironmentDashboard(runSummary.environment)
5681
- : '<div class="no-data">Environment data not available.</div>'
5682
- }
6300
+ ${generateEnvironmentSection(runSummary.environment)}
5683
6301
  </div>
5684
6302
  <div style="display: flex; flex-direction: column; gap: 28px;">
5685
6303
  ${generateSuitesWidget(suitesData)}
@@ -5690,7 +6308,7 @@ function generateHTML(reportData, trendData = null) {
5690
6308
  <div id="test-runs" class="tab-content">
5691
6309
  <div class="filters" style="border-color: black; border-style: groove;">
5692
6310
  <input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
5693
- <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>
6311
+ <select id="filter-status"><option value="">All Statuses</option><option value="passed">Passed</option><option value="failed">Failed</option><option value="flaky">Flaky</option><option value="skipped">Skipped</option></select>
5694
6312
  <select id="filter-browser"><option value="">All Browsers</option>${Array.from(
5695
6313
  new Set(
5696
6314
  (results || []).map((test) => test.browser || "unknown"),
@@ -5781,6 +6399,29 @@ function generateHTML(reportData, trendData = null) {
5781
6399
  return (ms / 1000).toFixed(1) + "s";
5782
6400
  }
5783
6401
  }
6402
+ function switchRetryTab(event, tabId) {
6403
+ // Find container
6404
+ const container = event.target.closest('.retry-tabs-container');
6405
+
6406
+ // Update tab buttons
6407
+ const buttons = container.querySelectorAll('.retry-tab');
6408
+ buttons.forEach(btn => btn.classList.remove('active'));
6409
+ event.target.classList.add('active');
6410
+
6411
+ // Update content
6412
+ const contents = container.querySelectorAll('.retry-tab-content');
6413
+ contents.forEach(content => {
6414
+ content.style.display = 'none';
6415
+ content.classList.remove('active');
6416
+ });
6417
+
6418
+ const activeContent = container.querySelector('#' + tabId);
6419
+ if (activeContent) {
6420
+ activeContent.style.display = 'block';
6421
+ activeContent.classList.add('active');
6422
+ }
6423
+ }
6424
+
5784
6425
  function copyLogContent(elementId, button) {
5785
6426
  const logElement = document.getElementById(elementId);
5786
6427
  if (!logElement) {
@@ -6437,6 +7078,8 @@ async function runScript(scriptPath, args = []) {
6437
7078
  * prepares the data, and then generates and writes the final HTML report file.
6438
7079
  */
6439
7080
  async function main() {
7081
+ await animate();
7082
+
6440
7083
  const __filename = fileURLToPath(import.meta.url);
6441
7084
  const __dirname = path.dirname(__filename);
6442
7085
 
@@ -6605,6 +7248,7 @@ async function main() {
6605
7248
  passed: histRunReport.run.passed,
6606
7249
  failed: histRunReport.run.failed,
6607
7250
  skipped: histRunReport.run.skipped || 0,
7251
+ flaky: histRunReport.run.flaky || (histRunReport.results ? histRunReport.results.filter(r => r.status === 'flaky' || r.outcome === 'flaky').length : 0),
6608
7252
  });
6609
7253
 
6610
7254
  if (histRunReport.results && Array.isArray(histRunReport.results)) {
@@ -6613,7 +7257,7 @@ async function main() {
6613
7257
  (test) => ({
6614
7258
  testName: test.name,
6615
7259
  duration: test.duration,
6616
- status: test.status,
7260
+ status: test.final_status || test.status,
6617
7261
  timestamp: new Date(test.startTime),
6618
7262
  }),
6619
7263
  );
@@ -6631,7 +7275,7 @@ async function main() {
6631
7275
  await fs.writeFile(reportHtmlPath, htmlContent, "utf-8");
6632
7276
  console.log(
6633
7277
  chalk.green.bold(
6634
- `🎉 Pulse report generated successfully at: ${reportHtmlPath}`,
7278
+ `Pulse report generated successfully at: ${reportHtmlPath}`,
6635
7279
  ),
6636
7280
  );
6637
7281
  console.log(chalk.gray(`(You can open this file in your browser)`));