@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
  // Use dynamic import for chalk as it's ESM only
11
12
  let chalk;
@@ -294,6 +295,12 @@ function generateTestTrendsChart(trendData) {
294
295
  color: "var(--warning-color)",
295
296
  marker: { symbol: "circle" },
296
297
  },
298
+ {
299
+ name: "Flaky",
300
+ data: runs.map((r) => r.flaky || 0),
301
+ color: "#00ccd3",
302
+ marker: { symbol: "circle" },
303
+ },
297
304
  ];
298
305
  const runsForTooltip = runs.map((r) => ({
299
306
  runId: r.runId,
@@ -480,6 +487,9 @@ function generateTestHistoryChart(history) {
480
487
  case "skipped":
481
488
  color = "var(--warning-color)";
482
489
  break;
490
+ case "flaky":
491
+ color = "var(--neutral-500)";
492
+ break;
483
493
  default:
484
494
  color = "var(--dark-gray-color)";
485
495
  }
@@ -589,6 +599,9 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
589
599
  case "Failed":
590
600
  color = "var(--danger-color)";
591
601
  break;
602
+ case "Flaky":
603
+ color = "#00ccd3";
604
+ break;
592
605
  case "Skipped":
593
606
  color = "var(--warning-color)";
594
607
  break;
@@ -688,21 +701,175 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
688
701
  </div>
689
702
  `;
690
703
  }
691
- function generateEnvironmentDashboard(environment) {
692
- // Format memory for display
693
- const formattedMemory = environment.memory.replace(/(\d+\.\d{2})GB/, "$1 GB");
694
-
695
- // Generate a unique ID for the dashboard
696
- const dashboardId = `envDashboard-${Date.now()}-${Math.random()
697
- .toString(36)
698
- .substring(2, 7)}`;
704
+ function generateEnvironmentSection(environmentData) {
705
+ if (!environmentData) {
706
+ return '<div class="no-data">Environment data not available.</div>';
707
+ }
708
+
709
+ if (Array.isArray(environmentData)) {
710
+ return `
711
+ <div class="sharded-env-section">
712
+ <div class="sharded-env-header">
713
+ <div class="sharded-env-title-row">
714
+ <div>
715
+ <div class="sharded-env-title">
716
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
717
+ <rect width="20" height="8" x="2" y="2" rx="2" ry="2"></rect>
718
+ <rect width="20" height="8" x="2" y="14" rx="2" ry="2"></rect>
719
+ <line x1="6" x2="6.01" y1="6" y2="6"></line>
720
+ <line x1="6" x2="6.01" y1="18" y2="18"></line>
721
+ </svg>
722
+ System Information
723
+ </div>
724
+ <div class="sharded-env-subtitle">Test execution environment details - ${environmentData.length} shard${environmentData.length > 1 ? "s" : ""}</div>
725
+ </div>
726
+ <div class="env-icon-badge">
727
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
728
+ <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>
729
+ </svg>
730
+ </div>
731
+ </div>
732
+ </div>
733
+ <div class="sharded-environments-container">
734
+ <div class="sharded-environments-wrapper">
735
+ ${environmentData
736
+ .map(
737
+ (env, index) => `
738
+ <div class="env-card-wrapper">
739
+ <div class="env-card-badge">Shard ${index + 1}</div>
740
+ ${generateEnvironmentDashboard(env, true)}
741
+ </div>
742
+ `,
743
+ )
744
+ .join("")}
745
+ </div>
746
+ </div>
747
+ </div>
748
+ <style>
749
+ .sharded-env-section {
750
+ border: 1px solid #e2e8f0;
751
+ border-radius: 12px;
752
+ background: #fafbfc;
753
+ overflow: hidden;
754
+ }
755
+ .sharded-env-header {
756
+ position: sticky;
757
+ top: 0;
758
+ z-index: 20;
759
+ background: linear-gradient(to bottom right, #ffffff 0%, #fafafa 100%);
760
+ border-bottom: 1px solid #e2e8f0;
761
+ padding: 24px 24px 16px;
762
+ }
763
+ .sharded-env-title-row {
764
+ display: flex;
765
+ justify-content: space-between;
766
+ align-items: center;
767
+ }
768
+ .sharded-env-title {
769
+ display: flex;
770
+ align-items: center;
771
+ font-size: 18px;
772
+ font-weight: 600;
773
+ color: #0f172a;
774
+ }
775
+ .sharded-env-title svg {
776
+ width: 18px;
777
+ height: 18px;
778
+ margin-right: 8px;
779
+ stroke: currentColor;
780
+ fill: none;
781
+ }
782
+ .sharded-env-subtitle {
783
+ font-size: 13px;
784
+ color: #64748b;
785
+ margin-top: 4px;
786
+ }
787
+ .sharded-environments-container {
788
+ max-height: 520px;
789
+ overflow-y: auto;
790
+ overflow-x: hidden;
791
+ padding: 16px;
792
+ }
793
+ .sharded-environments-container::-webkit-scrollbar {
794
+ width: 8px;
795
+ }
796
+ .sharded-environments-container::-webkit-scrollbar-track {
797
+ background: #f1f1f1;
798
+ border-radius: 4px;
799
+ }
800
+ .sharded-environments-container::-webkit-scrollbar-thumb {
801
+ background: #cbd5e0;
802
+ border-radius: 4px;
803
+ }
804
+ .sharded-environments-container::-webkit-scrollbar-thumb:hover {
805
+ background: #a0aec0;
806
+ }
807
+ .sharded-environments-wrapper {
808
+ display: grid;
809
+ grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));
810
+ gap: 24px;
811
+ }
812
+ @media (max-width: 768px) {
813
+ .sharded-environments-wrapper {
814
+ grid-template-columns: 1fr;
815
+ }
816
+ }
817
+ .env-card-wrapper {
818
+ position: relative;
819
+ }
820
+ .env-card-badge {
821
+ position: absolute;
822
+ top: -10px;
823
+ right: 16px;
824
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
825
+ color: white;
826
+ padding: 6px 14px;
827
+ border-radius: 20px;
828
+ font-size: 0.75em;
829
+ font-weight: 700;
830
+ text-transform: uppercase;
831
+ letter-spacing: 0.5px;
832
+ z-index: 10;
833
+ box-shadow: 0 4px 6px -1px rgba(99, 102, 241, 0.3);
834
+ }
835
+ </style>
836
+ `;
837
+ }
838
+
839
+ return generateEnvironmentDashboard(environmentData);
840
+ }
699
841
 
700
- // Logic for Run Context
842
+ function generateEnvironmentDashboard(environment, hideHeader = false) {
843
+ const cpuModel = environment.cpu && environment.cpu.model ? environment.cpu.model : "N/A";
844
+ const cpuCores = environment.cpu && environment.cpu.cores ? environment.cpu.cores : "N/A";
845
+ const cpuInfo = `model: ${cpuModel}, cores: ${cpuCores}`;
846
+ const osInfo = environment.os || "N/A";
847
+ const nodeInfo = environment.node || "N/A";
848
+ const v8Info = environment.v8 || "N/A";
849
+ const cwdInfo = environment.cwd || "N/A";
850
+ const formattedMemory = environment.memory || "N/A";
701
851
  const runContext = process.env.CI ? "CI" : "Local Test";
702
852
 
703
853
  return `
704
- <div class="environment-dashboard-wrapper" id="${dashboardId}">
854
+ <div class="env-modern-card${hideHeader ? " no-header" : ""}">
705
855
  <style>
856
+ .env-modern-card {
857
+ background: linear-gradient(to bottom right, #ffffff 0%, #fafafa 100%);
858
+ border: 0;
859
+ border-radius: 12px;
860
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
861
+ margin-top: 24px;
862
+ transition: all 0.3s ease;
863
+ font-family: var(--font-family);
864
+ overflow: hidden;
865
+ }
866
+ .env-modern-card:hover {
867
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
868
+ }
869
+ .env-modern-card {
870
+ margin-bottom: 0;
871
+ }
872
+
706
873
  .environment-dashboard-wrapper *,
707
874
  .environment-dashboard-wrapper *::before,
708
875
  .environment-dashboard-wrapper *::after {
@@ -726,279 +893,269 @@ function generateEnvironmentDashboard(environment) {
726
893
  transform: translateZ(0);
727
894
  }
728
895
 
729
- @media (max-width: 768px) {
730
- .environment-dashboard-wrapper {
731
- grid-template-columns: 1fr;
732
- padding: 32px 24px;
733
- }
896
+ .env-card-header {
897
+ display: flex;
898
+ flex-direction: column;
899
+ padding: 24px 24px 12px;
734
900
  }
735
- @media (max-width: 480px) {
736
- .environment-dashboard-wrapper {
737
- padding: 24px;
738
- }
901
+ .env-modern-card.no-header .env-card-header {
902
+ display: none;
739
903
  }
740
-
741
- .env-dashboard-header {
742
- grid-column: 1 / -1;
743
- margin-bottom: 24px;
904
+ .env-modern-card.no-header {
905
+ margin-top: 0;
744
906
  }
745
-
746
- .env-dashboard-title {
747
- font-size: 2em;
748
- font-weight: 900;
749
- color: #0f172a;
750
- letter-spacing: -0.02em;
751
- margin: 0 0 8px 0;
907
+ .env-modern-card.no-header .env-card-content {
908
+ padding-top: 24px;
752
909
  }
753
-
754
- .env-dashboard-subtitle {
755
- font-size: 1.05em;
756
- color: #64748b;
757
- margin: 0;
758
- font-weight: 400;
910
+ .env-card-title-row {
911
+ display: flex;
912
+ justify-content: space-between;
913
+ align-items: center;
759
914
  }
760
-
761
- .env-card {
762
- background: white;
763
- border: none;
764
- border-left: 4px solid #e2e8f0;
765
- padding: 28px;
915
+ .env-card-title {
766
916
  display: flex;
767
- flex-direction: column;
768
- gap: 20px;
769
- transition: all 0.12s ease;
770
- transform: translateZ(0);
917
+ align-items: center;
918
+ font-size: 16px;
919
+ font-weight: 600;
920
+ color: #0f172a;
921
+ transition: color 0.3s;
771
922
  }
772
-
773
- .env-card:hover {
774
- border-left-color: var(--primary-color);
775
- background: #fafbfc;
923
+ .env-modern-card:hover .env-card-title {
924
+ color: #6366f1;
776
925
  }
777
-
778
- .env-card-header {
779
- font-weight: 700;
780
- font-size: 1.05em;
781
- color: #0f172a;
926
+ .env-card-title svg {
927
+ width: 16px;
928
+ height: 16px;
929
+ margin-right: 8px;
930
+ stroke: currentColor;
931
+ fill: none;
932
+ }
933
+ .env-card-subtitle {
934
+ font-size: 12px;
935
+ color: #64748b;
936
+ margin-top: 4px;
937
+ }
938
+ .env-icon-badge {
939
+ width: 36px;
940
+ height: 36px;
941
+ border-radius: 50%;
942
+ background: linear-gradient(to bottom right, rgba(99, 102, 241, 0.1), rgba(99, 102, 241, 0.05));
782
943
  display: flex;
783
944
  align-items: center;
784
- gap: 10px;
785
- text-transform: uppercase;
786
- letter-spacing: 0.5px;
945
+ justify-content: center;
787
946
  }
788
-
789
- .env-card-header svg {
790
- width: 18px;
791
- height: 18px;
792
- fill: #6366f1;
947
+ .env-icon-badge svg {
948
+ width: 16px;
949
+ height: 16px;
950
+ stroke: #6366f1;
951
+ fill: none;
793
952
  }
794
-
795
953
  .env-card-content {
796
- display: flex;
797
- flex-direction: column;
798
- gap: 16px;
954
+ padding: 0 24px 24px;
799
955
  }
800
-
801
- .env-detail-row {
802
- display: flex;
803
- justify-content: space-between;
804
- align-items: flex-start;
805
- gap: 16px;
806
- font-size: 1em;
807
- padding: 8px 0;
808
- }
809
-
810
- .env-detail-label {
811
- color: #64748b;
812
- font-weight: 600;
813
- font-size: 0.9em;
814
- text-transform: uppercase;
815
- letter-spacing: 0.3px;
816
- flex-shrink: 0;
956
+ .env-items-grid {
957
+ display: grid;
958
+ grid-template-columns: repeat(2, 1fr);
959
+ gap: 10px;
817
960
  }
818
-
819
- .env-detail-value {
820
- color: #0f172a;
821
- font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
822
- font-size: 0.95em;
823
- text-align: right;
824
- word-break: break-word;
825
- margin-left: auto;
961
+ @media (min-width: 768px) {
962
+ .env-items-grid {
963
+ grid-template-columns: repeat(4, 1fr);
964
+ }
826
965
  }
827
-
828
- .env-chip {
829
- display: inline-flex;
830
- align-items: center;
831
- padding: 6px 14px;
832
- border-radius: 6px;
833
- font-size: 0.85em;
834
- font-weight: 700;
835
- text-transform: uppercase;
836
- letter-spacing: 0.5px;
966
+ .env-item {
967
+ display: flex;
968
+ align-items: flex-start;
969
+ gap: 8px;
970
+ padding: 8px;
971
+ border-radius: 8px;
972
+ transition: background-color 0.2s;
973
+ min-height: 48px;
837
974
  }
838
-
839
- .env-chip-primary {
840
- background-color: #ede9fe;
841
- color: #6366f1;
975
+ .env-item:hover {
976
+ background-color: rgba(100, 116, 139, 0.05);
842
977
  }
843
-
844
- .env-chip-success {
845
- background-color: #d1fae5;
846
- color: #10b981;
978
+ .env-item-icon {
979
+ flex-shrink: 0;
847
980
  }
848
-
849
- .env-chip-warning {
850
- background-color: #fef3c7;
851
- color: #f59e0b;
981
+ .env-item-icon svg {
982
+ width: 16px;
983
+ height: 16px;
984
+ stroke: #6366f1;
985
+ fill: none;
852
986
  }
853
-
854
- .env-cpu-cores {
855
- display: flex;
856
- align-items: center;
857
- gap: 6px;
987
+ .env-item-content {
988
+ flex-grow: 1;
989
+ min-width: 0;
858
990
  }
859
-
860
- .env-core-indicator {
861
- width: 12px;
862
- height: 12px;
863
- border-radius: 50%;
864
- background-color: var(--success-color);
865
- border: 1px solid rgba(0,0,0,0.1);
991
+ .env-item-label {
992
+ font-size: 12px;
993
+ font-weight: 500;
994
+ color: #64748b;
995
+ white-space: nowrap;
996
+ overflow: hidden;
997
+ text-overflow: ellipsis;
866
998
  }
867
-
868
- .env-core-indicator.inactive {
869
- background-color: var(--border-light-color);
870
- opacity: 0.7;
871
- border-color: var(--border-color);
999
+ .env-item-value {
1000
+ font-size: 12px;
1001
+ font-weight: 600;
1002
+ color: #0f172a;
1003
+ word-wrap: break-word;
1004
+ overflow-wrap: break-word;
1005
+ line-height: 1.4;
872
1006
  }
873
1007
  </style>
874
1008
 
875
- <div class="env-dashboard-header">
876
- <div>
877
- <h3 class="env-dashboard-title">System Environment</h3>
878
- <p class="env-dashboard-subtitle">Snapshot of the execution environment</p>
879
- </div>
880
- <span class="env-chip env-chip-primary">${environment.host}</span>
881
- </div>
882
-
883
- <div class="env-card">
884
- <div class="env-card-header">
885
- <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>
886
- Hardware
887
- </div>
888
- <div class="env-card-content">
889
- <div class="env-detail-row">
890
- <span class="env-detail-label">CPU Model</span>
891
- <span class="env-detail-value">${environment.cpu.model}</span>
892
- </div>
893
- <div class="env-detail-row">
894
- <span class="env-detail-label">CPU Cores</span>
895
- <span class="env-detail-value">
896
- <div class="env-cpu-cores">
897
- <span>${environment.cpu.cores || "N/A"} core${environment.cpu.cores !== 1 ? "s" : ""}</span>
898
- </div>
899
- </span>
1009
+ <div class="env-card-header">
1010
+ <div class="env-card-title-row">
1011
+ <div>
1012
+ <div class="env-card-title">
1013
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1014
+ <rect width="20" height="8" x="2" y="2" rx="2" ry="2"></rect>
1015
+ <rect width="20" height="8" x="2" y="14" rx="2" ry="2"></rect>
1016
+ <line x1="6" x2="6.01" y1="6" y2="6"></line>
1017
+ <line x1="6" x2="6.01" y1="18" y2="18"></line>
1018
+ </svg>
1019
+ System Information
1020
+ </div>
1021
+ <div class="env-card-subtitle">Test execution environment details</div>
900
1022
  </div>
901
- <div class="env-detail-row">
902
- <span class="env-detail-label">Memory</span>
903
- <span class="env-detail-value">${formattedMemory}</span>
1023
+ <div class="env-icon-badge">
1024
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1025
+ <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>
1026
+ </svg>
904
1027
  </div>
905
1028
  </div>
906
1029
  </div>
907
1030
 
908
- <div class="env-card">
909
- <div class="env-card-header">
910
- <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>
911
- Operating System
912
- </div>
913
- <div class="env-card-content">
914
- <div class="env-detail-row">
915
- <span class="env-detail-label">OS Type</span>
916
- <span class="env-detail-value">${
917
- environment.os.split(" ")[0] === "darwin"
918
- ? "darwin (macOS)"
919
- : environment.os.split(" ")[0] || "Unknown"
920
- }</span>
921
- </div>
922
- <div class="env-detail-row">
923
- <span class="env-detail-label">OS Version</span>
924
- <span class="env-detail-value">${
925
- environment.os.split(" ")[1] || "N/A"
926
- }</span>
927
- </div>
928
- <div class="env-detail-row">
929
- <span class="env-detail-label">Hostname</span>
930
- <span class="env-detail-value" title="${environment.host}">${
931
- environment.host
932
- }</span>
1031
+ <div class="env-card-content">
1032
+ <div class="env-items-grid">
1033
+ <div class="env-item">
1034
+ <div class="env-item-icon">
1035
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1036
+ <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>
1037
+ </svg>
1038
+ </div>
1039
+ <div class="env-item-content">
1040
+ <p class="env-item-label">Host</p>
1041
+ <div class="env-item-value" title="${environment.host}">${environment.host}</div>
1042
+ </div>
933
1043
  </div>
934
- </div>
935
- </div>
936
-
937
- <div class="env-card">
938
- <div class="env-card-header">
939
- <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>
940
- Node.js Runtime
941
- </div>
942
- <div class="env-card-content">
943
- <div class="env-detail-row">
944
- <span class="env-detail-label">Node Version</span>
945
- <span class="env-detail-value">${environment.node}</span>
1044
+
1045
+ <div class="env-item">
1046
+ <div class="env-item-icon">
1047
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1048
+ <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>
1049
+ </svg>
1050
+ </div>
1051
+ <div class="env-item-content">
1052
+ <p class="env-item-label">Os</p>
1053
+ <div class="env-item-value" title="${environment.os}">${environment.os}</div>
1054
+ </div>
946
1055
  </div>
947
- <div class="env-detail-row">
948
- <span class="env-detail-label">V8 Engine</span>
949
- <span class="env-detail-value">${environment.v8}</span>
1056
+
1057
+ <div class="env-item">
1058
+ <div class="env-item-icon">
1059
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1060
+ <rect width="16" height="16" x="4" y="4" rx="2"></rect>
1061
+ <rect width="6" height="6" x="9" y="9" rx="1"></rect>
1062
+ <path d="M15 2v2"></path>
1063
+ <path d="M15 20v2"></path>
1064
+ <path d="M2 15h2"></path>
1065
+ <path d="M2 9h2"></path>
1066
+ <path d="M20 15h2"></path>
1067
+ <path d="M20 9h2"></path>
1068
+ <path d="M9 2v2"></path>
1069
+ <path d="M9 20v2"></path>
1070
+ </svg>
1071
+ </div>
1072
+ <div class="env-item-content">
1073
+ <p class="env-item-label">Cpu</p>
1074
+ <div class="env-item-value" title='${JSON.stringify(environment.cpu)}'>${cpuInfo}</div>
1075
+ </div>
950
1076
  </div>
951
- <div class="env-detail-row">
952
- <span class="env-detail-label">Working Dir</span>
953
- <span class="env-detail-value" title="${environment.cwd}">${
954
- environment.cwd.length > 25
955
- ? "..." + environment.cwd.slice(-22)
956
- : environment.cwd
957
- }</span>
1077
+
1078
+ <div class="env-item">
1079
+ <div class="env-item-icon">
1080
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1081
+ <path d="M6 19v-3"></path>
1082
+ <path d="M10 19v-3"></path>
1083
+ <path d="M14 19v-3"></path>
1084
+ <path d="M18 19v-3"></path>
1085
+ <path d="M8 11V9"></path>
1086
+ <path d="M16 11V9"></path>
1087
+ <path d="M12 11V9"></path>
1088
+ <path d="M2 15h20"></path>
1089
+ <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>
1090
+ </svg>
1091
+ </div>
1092
+ <div class="env-item-content">
1093
+ <p class="env-item-label">Memory</p>
1094
+ <div class="env-item-value" title="${environment.memory}">${environment.memory}</div>
1095
+ </div>
958
1096
  </div>
959
- </div>
960
- </div>
961
-
962
- <div class="env-card">
963
- <div class="env-card-header">
964
- <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>
965
- System Summary
966
- </div>
967
- <div class="env-card-content">
968
- <div class="env-detail-row">
969
- <span class="env-detail-label">Platform Arch</span>
970
- <span class="env-detail-value">
971
- <span class="env-chip ${
972
- environment.os.includes("darwin") &&
973
- environment.cpu.model.toLowerCase().includes("apple")
974
- ? "env-chip-success"
975
- : "env-chip-warning"
976
- }">
977
- ${
978
- environment.os.includes("darwin") &&
979
- environment.cpu.model.toLowerCase().includes("apple")
980
- ? "Apple Silicon"
981
- : environment.cpu.model.toLowerCase().includes("arm") ||
982
- environment.cpu.model.toLowerCase().includes("aarch64")
983
- ? "ARM-based"
984
- : "x86/Other"
985
- }
986
- </span>
987
- </span>
1097
+
1098
+ <div class="env-item">
1099
+ <div class="env-item-icon">
1100
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1101
+ <path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"></path>
1102
+ <path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"></path>
1103
+ <path d="M12 2v2"></path>
1104
+ <path d="M12 22v-2"></path>
1105
+ <path d="m17 20.66-1-1.73"></path>
1106
+ <path d="M11 10.27 7 3.34"></path>
1107
+ <path d="m20.66 17-1.73-1"></path>
1108
+ <path d="m3.34 7 1.73 1"></path>
1109
+ <path d="M14 12h8"></path>
1110
+ <path d="M2 12h2"></path>
1111
+ <path d="m20.66 7-1.73 1"></path>
1112
+ <path d="m3.34 17 1.73-1"></path>
1113
+ <path d="m17 3.34-1 1.73"></path>
1114
+ <path d="m11 13.73-4 6.93"></path>
1115
+ </svg>
1116
+ </div>
1117
+ <div class="env-item-content">
1118
+ <p class="env-item-label">Node</p>
1119
+ <div class="env-item-value" title="${environment.node}">${environment.node}</div>
1120
+ </div>
988
1121
  </div>
989
- <div class="env-detail-row">
990
- <span class="env-detail-label">Memory per Core</span>
991
- <span class="env-detail-value">${
992
- environment.cpu.cores > 0
993
- ? (
994
- parseFloat(environment.memory) / environment.cpu.cores
995
- ).toFixed(2) + " GB"
996
- : "N/A"
997
- }</span>
1122
+
1123
+ <div class="env-item">
1124
+ <div class="env-item-icon">
1125
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1126
+ <path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"></path>
1127
+ <path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"></path>
1128
+ <path d="M12 2v2"></path>
1129
+ <path d="M12 22v-2"></path>
1130
+ <path d="m17 20.66-1-1.73"></path>
1131
+ <path d="M11 10.27 7 3.34"></path>
1132
+ <path d="m20.66 17-1.73-1"></path>
1133
+ <path d="m3.34 7 1.73 1"></path>
1134
+ <path d="M14 12h8"></path>
1135
+ <path d="M2 12h2"></path>
1136
+ <path d="m20.66 7-1.73 1"></path>
1137
+ <path d="m3.34 17 1.73-1"></path>
1138
+ <path d="m17 3.34-1 1.73"></path>
1139
+ <path d="m11 13.73-4 6.93"></path>
1140
+ </svg>
1141
+ </div>
1142
+ <div class="env-item-content">
1143
+ <p class="env-item-label">V8</p>
1144
+ <div class="env-item-value" title="${environment.v8}">${environment.v8}</div>
1145
+ </div>
998
1146
  </div>
999
- <div class="env-detail-row">
1000
- <span class="env-detail-label">Run Context</span>
1001
- <span class="env-detail-value">${runContext}</span>
1147
+
1148
+ <div class="env-item">
1149
+ <div class="env-item-icon">
1150
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1151
+ <path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
1152
+ <polyline points="9 22 9 12 15 12 15 22"></polyline>
1153
+ </svg>
1154
+ </div>
1155
+ <div class="env-item-content">
1156
+ <p class="env-item-label">Working Dir</p>
1157
+ <div class="env-item-value" title="${cwdInfo}">${cwdInfo.length > 30 ? "..." + cwdInfo.slice(-27) : cwdInfo}</div>
1158
+ </div>
1002
1159
  </div>
1003
1160
  </div>
1004
1161
  </div>
@@ -1021,11 +1178,11 @@ function generateWorkerDistributionChart(results) {
1021
1178
  const workerId =
1022
1179
  typeof test.workerId !== "undefined" ? test.workerId : "N/A";
1023
1180
  if (!acc[workerId]) {
1024
- acc[workerId] = { passed: 0, failed: 0, skipped: 0, tests: [] };
1181
+ acc[workerId] = { passed: 0, failed: 0, skipped: 0, flaky: 0, tests: [] };
1025
1182
  }
1026
1183
 
1027
1184
  const status = String(test.status).toLowerCase();
1028
- if (status === "passed" || status === "failed" || status === "skipped") {
1185
+ if (status === "passed" || status === "failed" || status === "skipped" || status === "flaky") {
1029
1186
  acc[workerId][status]++;
1030
1187
  }
1031
1188
 
@@ -1070,12 +1227,14 @@ function generateWorkerDistributionChart(results) {
1070
1227
  const passedData = workerIds.map((id) => workerData[id].passed);
1071
1228
  const failedData = workerIds.map((id) => workerData[id].failed);
1072
1229
  const skippedData = workerIds.map((id) => workerData[id].skipped);
1230
+ const flakyData = workerIds.map((id) => workerData[id].flaky);
1073
1231
 
1074
1232
  const categoriesString = JSON.stringify(categories);
1075
1233
  const fullDataString = JSON.stringify(fullWorkerData);
1076
1234
  const seriesString = JSON.stringify([
1077
1235
  { name: "Passed", data: passedData, color: "var(--success-color)" },
1078
1236
  { name: "Failed", data: failedData, color: "var(--danger-color)" },
1237
+ { name: "Flaky", data: flakyData, color: "#00ccd3" },
1079
1238
  { name: "Skipped", data: skippedData, color: "var(--warning-color)" },
1080
1239
  ]);
1081
1240
 
@@ -1168,6 +1327,7 @@ function generateWorkerDistributionChart(results) {
1168
1327
  if (test.status === 'passed') color = 'var(--success-color)';
1169
1328
  else if (test.status === 'failed') color = 'var(--danger-color)';
1170
1329
  else if (test.status === 'skipped') color = 'var(--warning-color)';
1330
+ else if (test.status === 'flaky') color = '#00ccd3';
1171
1331
 
1172
1332
  const escapedName = test.name.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
1173
1333
  testListHtml += \`<li style="color: \${color};"><span style="color: \${color}">[\${test.status.toUpperCase()}]</span> \${escapedName}</li>\`;
@@ -1333,6 +1493,7 @@ function generateTestHistoryContent(trendData) {
1333
1493
  <option value="">All Statuses</option>
1334
1494
  <option value="passed">Passed</option>
1335
1495
  <option value="failed">Failed</option>
1496
+ <option value="flaky">Flaky</option>
1336
1497
  <option value="skipped">Skipped</option>
1337
1498
  </select>
1338
1499
  <button id="clear-history-filters" class="clear-filters-btn">Clear Filters</button>
@@ -1400,6 +1561,8 @@ function getStatusClass(status) {
1400
1561
  return "status-failed";
1401
1562
  case "skipped":
1402
1563
  return "status-skipped";
1564
+ case "flaky":
1565
+ return "status-flaky";
1403
1566
  default:
1404
1567
  return "status-unknown";
1405
1568
  }
@@ -1412,6 +1575,8 @@ function getStatusIcon(status) {
1412
1575
  return "❌";
1413
1576
  case "skipped":
1414
1577
  return "⏭️";
1578
+ case "flaky":
1579
+ return "⚠️";
1415
1580
  default:
1416
1581
  return "❓";
1417
1582
  }
@@ -1447,6 +1612,7 @@ function getSuitesData(results) {
1447
1612
  browser: browser,
1448
1613
  passed: 0,
1449
1614
  failed: 0,
1615
+ flaky: 0,
1450
1616
  skipped: 0,
1451
1617
  count: 0,
1452
1618
  statusOverall: "passed",
@@ -1454,12 +1620,15 @@ function getSuitesData(results) {
1454
1620
  }
1455
1621
  const suite = suitesMap.get(key);
1456
1622
  suite.count++;
1457
- const currentStatus = String(test.status).toLowerCase();
1623
+ let currentStatus = String(test.status).toLowerCase();
1624
+ if (test.outcome === 'flaky') currentStatus = 'flaky';
1458
1625
  if (currentStatus && suite[currentStatus] !== undefined) {
1459
1626
  suite[currentStatus]++;
1460
1627
  }
1461
1628
  if (currentStatus === "failed") suite.statusOverall = "failed";
1462
- else if (currentStatus === "skipped" && suite.statusOverall !== "failed")
1629
+ else if (currentStatus === "flaky" && suite.statusOverall !== "failed")
1630
+ suite.statusOverall = "flaky";
1631
+ else if (currentStatus === "skipped" && suite.statusOverall !== "failed" && suite.statusOverall !== "flaky")
1463
1632
  suite.statusOverall = "skipped";
1464
1633
  });
1465
1634
  return Array.from(suitesMap.values());
@@ -1470,10 +1639,10 @@ function generateSuitesWidget(suitesData) {
1470
1639
  return `<div class="suites-widget" style="height: 450px;"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
1471
1640
  }
1472
1641
 
1473
- // Added inline styles for height consistency with Pie Chart (approx 450px) and scrolling
1642
+ // Uses CSS classes for responsiveness instead of inline styles
1474
1643
  return `
1475
- <div class="suites-widget" style="height: 450px; display: flex; flex-direction: column;">
1476
- <div class="suites-header" style="flex-shrink: 0;">
1644
+ <div class="suites-widget fixed-height-widget">
1645
+ <div class="suites-header">
1477
1646
  <h2>Test Suites</h2>
1478
1647
  <span class="summary-badge">${
1479
1648
  suitesData.length
@@ -1483,40 +1652,40 @@ function generateSuitesWidget(suitesData) {
1483
1652
  )} tests</span>
1484
1653
  </div>
1485
1654
 
1486
- <div class="suites-grid-container" style="flex-grow: 1; overflow-y: auto; padding-right: 5px;">
1655
+ <div class="suites-grid-container">
1487
1656
  <div class="suites-grid">
1488
1657
  ${suitesData
1489
1658
  .map(
1490
1659
  (suite) => `
1491
1660
  <div class="suite-card status-${suite.statusOverall}">
1492
1661
  <div class="suite-card-header">
1493
- <h3 class="suite-name" title="${sanitizeHTML(
1494
- suite.name,
1495
- )} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
1496
- </div>
1497
- <div style="margin-bottom: 12px;"><span class="browser-tag" title="🌐 ${sanitizeHTML(suite.browser)}">🌐 ${sanitizeHTML(
1498
- suite.browser,
1499
- )}</span></div>
1500
- <div class="suite-card-body">
1501
- <span class="test-count">${suite.count} test${
1502
- suite.count !== 1 ? "s" : ""
1503
- }</span>
1662
+ <h3 class="suite-name" title="${sanitizeHTML(suite.name)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
1663
+ <div class="status-indicator-dot status-${suite.statusOverall}" title="${suite.statusOverall.charAt(0).toUpperCase() + suite.statusOverall.slice(1)}"></div>
1664
+ </div>
1665
+
1666
+ <div class="browser-tag" title="🌐Browser: ${sanitizeHTML(suite.browser)}">
1667
+ <span style="font-size: 1.1em;">🌐</span> ${sanitizeHTML(suite.browser)}
1668
+ </div>
1669
+
1670
+ <div class="suite-card-body">
1671
+ <span class="test-count-label">${suite.count} Test${suite.count !== 1 ? "s" : ""}</span>
1504
1672
  <div class="suite-stats">
1505
- ${
1506
- suite.passed > 0
1507
- ? `<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>`
1508
- : ""
1509
- }
1510
- ${
1511
- suite.failed > 0
1512
- ? `<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>`
1513
- : ""
1514
- }
1515
- ${
1516
- suite.skipped > 0
1517
- ? `<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>`
1518
- : ""
1519
- }
1673
+ <span class="stat-pill passed" title="Passed">
1674
+ <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>
1675
+ ${suite.passed}
1676
+ </span>
1677
+ <span class="stat-pill failed" title="Failed">
1678
+ <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>
1679
+ ${suite.failed}
1680
+ </span>
1681
+ <span class="stat-pill flaky" title="Flaky">
1682
+ <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>
1683
+ ${suite.flaky || 0}
1684
+ </span>
1685
+ <span class="stat-pill skipped" title="Skipped">
1686
+ <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>
1687
+ ${suite.skipped}
1688
+ </span>
1520
1689
  </div>
1521
1690
  </div>
1522
1691
  </div>`,
@@ -1917,6 +2086,7 @@ function generateSeverityDistributionChart(results) {
1917
2086
  const data = {
1918
2087
  passed: [0, 0, 0, 0, 0],
1919
2088
  failed: [0, 0, 0, 0, 0],
2089
+ flaky: [0, 0, 0, 0, 0],
1920
2090
  skipped: [0, 0, 0, 0, 0],
1921
2091
  };
1922
2092
 
@@ -1935,6 +2105,8 @@ function generateSeverityDistributionChart(results) {
1935
2105
  status === "interrupted"
1936
2106
  ) {
1937
2107
  data.failed[index]++;
2108
+ } else if (status === "flaky") {
2109
+ data.flaky[index]++;
1938
2110
  } else {
1939
2111
  data.skipped[index]++;
1940
2112
  }
@@ -1948,6 +2120,7 @@ function generateSeverityDistributionChart(results) {
1948
2120
  const seriesData = [
1949
2121
  { name: "Passed", data: data.passed, color: "var(--success-color)" },
1950
2122
  { name: "Failed", data: data.failed, color: "var(--danger-color)" },
2123
+ { name: "Flaky", data: data.flaky, color: "#00ccd3" },
1951
2124
  { name: "Skipped", data: data.skipped, color: "var(--warning-color)" },
1952
2125
  ];
1953
2126
 
@@ -2061,32 +2234,85 @@ function generateHTML(reportData, trendData = null) {
2061
2234
  return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`), "");
2062
2235
  };
2063
2236
 
2064
- const totalTestsOr1 = runSummary.totalTests || 1;
2065
- const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
2066
- const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
2067
- const skipPercentage = Math.round(
2068
- ((runSummary.skipped || 0) / totalTestsOr1) * 100,
2069
- );
2237
+
2070
2238
  const avgTestDuration =
2071
2239
  runSummary.totalTests > 0
2072
2240
  ? formatDuration(runSummary.duration / runSummary.totalTests)
2073
2241
  : "0.0s";
2074
2242
 
2243
+ const flakyCount = (results || []).filter(r => r.outcome === 'flaky').length;
2244
+
2075
2245
  // Calculate retry statistics
2246
+ let retriedTestsCount = 0;
2076
2247
  const totalRetried = (results || []).reduce((acc, test) => {
2077
- if (test.retries && test.retries > 0) {
2078
- return acc + 1;
2248
+ if (test.retryHistory && test.retryHistory.length > 0) {
2249
+ // Filter out any "passed" or "skipped" entries in the history
2250
+ // We only count attempts that actually failed or timed out, triggering a retry.
2251
+ const unsuccessfulRetries = test.retryHistory.filter(attempt =>
2252
+ attempt.status === 'failed' || attempt.status === 'timedout' || attempt.status === 'flaky'
2253
+ );
2254
+ if (unsuccessfulRetries.length > 0) {
2255
+ retriedTestsCount++;
2256
+ }
2257
+ return acc + unsuccessfulRetries.length;
2079
2258
  }
2080
2259
  return acc;
2081
2260
  }, 0);
2082
2261
 
2262
+ // --- RECALCULATE KPI METRICS BASED ON FINAL_STATUS ---
2263
+ let calculatedPassed = 0;
2264
+ let calculatedFailed = 0;
2265
+ let calculatedSkipped = 0;
2266
+ let calculatedFlaky = 0;
2267
+ let calculatedTotal = 0;
2268
+
2269
+ (results || []).forEach(test => {
2270
+ calculatedTotal++;
2271
+ // New Logic: If outcome is 'flaky', it overrides everything.
2272
+ let statusToUse = test.status;
2273
+ if (test.outcome === 'flaky') {
2274
+ statusToUse = 'flaky';
2275
+ } else if (test.status === 'flaky') {
2276
+ // Just in case outcome wasn't set but status was (unlikely with new reporter)
2277
+ statusToUse = 'flaky';
2278
+ } else if (test.retryHistory && test.retryHistory.length > 0 && test.final_status) {
2279
+ statusToUse = test.final_status;
2280
+ }
2281
+
2282
+ // Update test status in place for charts
2283
+ test.status = statusToUse;
2284
+
2285
+ const s = String(statusToUse).toLowerCase();
2286
+ if (s === 'passed') calculatedPassed++;
2287
+ else if (s === 'skipped') calculatedSkipped++;
2288
+ else if (s === 'flaky') calculatedFlaky++;
2289
+ else calculatedFailed++; // failed, timedout, interrupted
2290
+ });
2291
+
2292
+ // Override runSummary counts with our calculated ones if results exist
2293
+ if (results && results.length > 0) {
2294
+ runSummary.passed = calculatedPassed;
2295
+ runSummary.failed = calculatedFailed;
2296
+ runSummary.skipped = calculatedSkipped;
2297
+ runSummary.flaky = calculatedFlaky;
2298
+ runSummary.totalTests = calculatedTotal;
2299
+ }
2300
+
2301
+ const totalTestsOr1 = runSummary.totalTests || 1;
2302
+ const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
2303
+ const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
2304
+ const skipPercentage = Math.round(
2305
+ ((runSummary.skipped || 0) / totalTestsOr1) * 100,
2306
+ );
2307
+ const flakyPercentage = Math.round(((runSummary.flaky || 0) / totalTestsOr1) * 100);
2308
+
2309
+
2083
2310
  // Calculate browser distribution
2084
2311
  const browserStats = (results || []).reduce((acc, test) => {
2085
2312
  let browserName = "unknown";
2086
2313
  if (test.browser) {
2087
- // Extract browser name from strings like "Chrome v143 on Windows 10"
2088
- const match = test.browser.match(/^(\w+)/);
2089
- browserName = match ? match[1] : test.browser;
2314
+ // Use full browser name
2315
+ browserName = test.browser;
2090
2316
  }
2091
2317
  acc[browserName] = (acc[browserName] || 0) + 1;
2092
2318
  return acc;
@@ -2112,6 +2338,10 @@ function generateHTML(reportData, trendData = null) {
2112
2338
  // --- Simplified Severity Badge ---
2113
2339
  const severity = test.severity || "Medium";
2114
2340
  const severityBadge = `<span class="severity-badge" data-severity="${severity.toLowerCase()}">${severity}</span>`;
2341
+
2342
+ // --- Retry Count Badge (only show if retries occurred) ---
2343
+ const retryCount = (test.retryHistory && test.retryHistory.length > 0) ? test.retryHistory.length : 0;
2344
+ const retryBadge = (test.retryHistory && test.retryHistory.length > 0) ? `<span class="retry-badge">Retry Count: ${retryCount}</span>` : '';
2115
2345
  const generateStepsHTML = (steps, depth = 0) => {
2116
2346
  if (!steps || steps.length === 0)
2117
2347
  return "<div class='no-steps'>No steps recorded for this test.</div>";
@@ -2119,17 +2349,20 @@ function generateHTML(reportData, trendData = null) {
2119
2349
  .map((step) => {
2120
2350
  const hasNestedSteps = step.steps && step.steps.length > 0;
2121
2351
  const isHook = step.hookType;
2352
+ const isFailedStep = step.isFailedStep === true;
2122
2353
  const stepClass = isHook
2123
2354
  ? `step-hook step-hook-${step.hookType}`
2124
2355
  : "";
2356
+ const failedStepClass = isFailedStep ? " failed-step-highlight" : "";
2125
2357
  const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
2358
+ const failedStepIndicator = isFailedStep ? ` <span class="failed-step-marker">⚠️ Failed at this step</span>` : "";
2126
2359
  return `
2127
- <div class="step-item" style="--depth: ${depth};">
2360
+ <div class="step-item${failedStepClass}" style="--depth: ${depth};">
2128
2361
  <div class="step-header ${stepClass}" role="button" aria-expanded="false">
2129
2362
  <span class="step-icon">${getStatusIcon(step.status)}</span>
2130
2363
  <span class="step-title">${sanitizeHTML(
2131
2364
  step.title,
2132
- )}${hookIndicator}</span>
2365
+ )}${hookIndicator}${failedStepIndicator}</span>
2133
2366
  <span class="step-duration">${formatDuration(
2134
2367
  step.duration,
2135
2368
  )}</span>
@@ -2142,6 +2375,13 @@ function generateHTML(reportData, trendData = null) {
2142
2375
  )}</div>`
2143
2376
  : ""
2144
2377
  }
2378
+ ${
2379
+ step.codeSnippet
2380
+ ? `<div class="code-snippet-section"><pre class="code-snippet">${sanitizeHTML(
2381
+ step.codeSnippet,
2382
+ )}</pre></div>`
2383
+ : ""
2384
+ }
2145
2385
  ${
2146
2386
  step.errorMessage
2147
2387
  ? `<div class="test-error-summary">
@@ -2157,74 +2397,69 @@ function generateHTML(reportData, trendData = null) {
2157
2397
  onclick="copyErrorToClipboard(this)"
2158
2398
  style="
2159
2399
  margin-top: 8px;
2160
- padding: 4px 8px;
2400
+ padding: 6px 12px;
2161
2401
  background: #f0f0f0;
2162
2402
  border: 2px solid #ccc;
2163
2403
  border-radius: 4px;
2164
2404
  cursor: pointer;
2165
2405
  font-size: 12px;
2166
- border-color: #8B0000;
2167
- color: #8B0000;
2168
- "
2169
- onmouseover="this.style.background='#e0e0e0'"
2170
- onmouseout="this.style.background='#f0f0f0'"
2171
- >
2172
- Copy Error Prompt
2173
- </button>
2174
- </div>`
2175
- : ""
2176
- }
2177
- ${
2178
- hasNestedSteps
2179
- ? `<div class="nested-steps">${generateStepsHTML(
2180
- step.steps,
2181
- depth + 1,
2182
- )}</div>`
2183
- : ""
2184
- }
2185
- </div>
2186
- </div>`;
2187
- })
2188
- .join("");
2189
- };
2190
-
2191
- return `
2192
- <div class="test-case" data-status="${
2193
- test.status
2194
- }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
2195
- .join(",")
2196
- .toLowerCase()}">
2197
- <div class="test-case-header" role="button" aria-expanded="false">
2198
- <div class="test-case-summary">
2199
- <span class="test-case-title" title="${sanitizeHTML(
2200
- test.name,
2201
- )}">${sanitizeHTML(testTitle)}</span>
2202
- <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
2203
- </div>
2204
- <div class="test-case-meta">
2205
- ${severityBadge}
2206
- ${
2207
- test.tags && test.tags.length > 0
2208
- ? test.tags
2209
- .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
2210
- .join(" ")
2211
- : ""
2212
- }
2213
- </div>
2214
- <div class="test-case-status-duration">
2215
- <span class="status-badge ${getStatusClass(test.status)}">${String(
2216
- test.status,
2217
- ).toUpperCase()}</span>
2218
- <span class="test-duration">${formatDuration(test.duration)}</span>
2219
- </div>
2220
- </div>
2221
- <div class="test-case-content" style="display: none;">
2222
- <p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
2406
+ border-color: #8B0000;
2407
+ color: #8B0000;
2408
+ align-self: flex-end;
2409
+ width: auto;
2410
+ "
2411
+ onmouseover="this.style.background='#e0e0e0'"
2412
+ onmouseout="this.style.background='#f0f0f0'"
2413
+ >
2414
+ Copy Error Prompt
2415
+ </button>
2416
+ </div>`
2417
+ : ""
2418
+ }
2419
+ ${
2420
+ hasNestedSteps
2421
+ ? `<div class="nested-steps">${generateStepsHTML(
2422
+ step.steps,
2423
+ depth + 1,
2424
+ )}</div>`
2425
+ : ""
2426
+ }
2427
+ </div>
2428
+ </div>`;
2429
+ })
2430
+ .join("");
2431
+ };
2432
+
2433
+ // Helper for Tab Badges
2434
+ const getSmallStatusBadge = (status) => {
2435
+ const s = String(status).toLowerCase();
2436
+ let colorVar = 'var(--text-tertiary)';
2437
+ if(s === 'passed') colorVar = 'var(--success-color)';
2438
+ else if(s === 'failed') colorVar = 'var(--danger-color)';
2439
+ else if(s === 'skipped') colorVar = 'var(--warning-color)';
2440
+ else if(s === 'flaky') colorVar = '#00ccd3';
2441
+
2442
+ return `<span style="
2443
+ display: inline-block;
2444
+ width: 8px;
2445
+ height: 8px;
2446
+ border-radius: 50%;
2447
+ background-color: ${colorVar};
2448
+ margin-left: 6px;
2449
+ vertical-align: middle;
2450
+ " title="${s}"></span>`;
2451
+ };
2452
+
2453
+ // Function to generate test content HTML (used for base run and retry tabs)
2454
+ const getTestContentHTML = (testData, runSuffix) => {
2455
+ const logId = `stdout-log-${test.id || index}-${runSuffix}`;
2456
+ return `
2457
+ <p><strong>Full Path:</strong> ${sanitizeHTML(testData.name)}</p>
2223
2458
  ${
2224
- test.annotations && test.annotations.length > 0
2459
+ testData.annotations && testData.annotations.length > 0
2225
2460
  ? `<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;">
2226
2461
  <h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
2227
- ${test.annotations
2462
+ ${testData.annotations
2228
2463
  .map((annotation, idx) => {
2229
2464
  const isIssueOrBug =
2230
2465
  annotation.type === "issue" ||
@@ -2232,7 +2467,7 @@ function generateHTML(reportData, trendData = null) {
2232
2467
  const descriptionText = annotation.description || "";
2233
2468
  const typeLabel = sanitizeHTML(annotation.type);
2234
2469
  const descriptionHtml =
2235
- isIssueOrBug && descriptionText.match(/^[A-Z]+-\d+$/)
2470
+ isIssueOrBug && descriptionText.match(/^[A-Z]+-\\d+$/)
2236
2471
  ? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
2237
2472
  descriptionText,
2238
2473
  )}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
@@ -2247,7 +2482,7 @@ function generateHTML(reportData, trendData = null) {
2247
2482
  }</div>`
2248
2483
  : "";
2249
2484
  return `<div style="margin-bottom: ${
2250
- idx < test.annotations.length - 1 ? "10px" : "0"
2485
+ idx < testData.annotations.length - 1 ? "10px" : "0"
2251
2486
  };">
2252
2487
  <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>
2253
2488
  ${
@@ -2263,21 +2498,21 @@ function generateHTML(reportData, trendData = null) {
2263
2498
  : ""
2264
2499
  }
2265
2500
  <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
2266
- test.workerId,
2501
+ testData.workerId,
2267
2502
  )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
2268
- test.totalWorkers,
2503
+ testData.totalWorkers,
2269
2504
  )}]</p>
2270
2505
  ${
2271
- test.errorMessage
2272
- ? `<div class="test-error-summary">${formatPlaywrightError(
2273
- test.errorMessage,
2274
- )}
2506
+ testData.errorMessage
2507
+ ? `<div class="test-error-summary"><div class="stack-trace">${formatPlaywrightError(
2508
+ testData.errorMessage,
2509
+ )}</div>
2275
2510
  <button
2276
2511
  class="copy-error-btn"
2277
2512
  onclick="copyErrorToClipboard(this)"
2278
2513
  style="
2279
2514
  margin-top: 8px;
2280
- padding: 4px 8px;
2515
+ padding: 6px 12px;
2281
2516
  background: #f0f0f0;
2282
2517
  border: 2px solid #ccc;
2283
2518
  border-radius: 4px;
@@ -2285,6 +2520,8 @@ function generateHTML(reportData, trendData = null) {
2285
2520
  font-size: 12px;
2286
2521
  border-color: #8B0000;
2287
2522
  color: #8B0000;
2523
+ align-self: flex-end;
2524
+ width: auto;
2288
2525
  "
2289
2526
  onmouseover="this.style.background='#e0e0e0'"
2290
2527
  onmouseout="this.style.background='#f0f0f0'"
@@ -2295,50 +2532,48 @@ function generateHTML(reportData, trendData = null) {
2295
2532
  : ""
2296
2533
  }
2297
2534
  ${
2298
- test.snippet
2535
+ testData.snippet
2299
2536
  ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
2300
- test.snippet,
2537
+ testData.snippet,
2301
2538
  )}</code></pre></div>`
2302
2539
  : ""
2303
2540
  }
2304
2541
  <h4>Steps</h4>
2305
- <div class="steps-list">${generateStepsHTML(test.steps)}</div>
2542
+ <div class="steps-list">${generateStepsHTML(testData.steps)}</div>
2306
2543
  ${(() => {
2307
- if (!test.stdout || test.stdout.length === 0) return "";
2308
- // Create a unique ID for the <pre> element to target it for copying
2309
- const logId = `stdout-log-${test.id || index}`;
2544
+ if (!testData.stdout || testData.stdout.length === 0) return "";
2310
2545
  return `<div class="console-output-section">
2311
2546
  <h4>Console Output (stdout)
2312
- <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy</button>
2547
+ <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy</ button>
2313
2548
  </h4>
2314
2549
  <div class="log-wrapper">
2315
2550
  <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(
2316
- test.stdout
2551
+ testData.stdout
2317
2552
  .map((line) => sanitizeHTML(line))
2318
- .join("\n"),
2553
+ .join("\\n"),
2319
2554
  )}</pre>
2320
2555
  </div>
2321
2556
  </div>`;
2322
2557
  })()}
2323
2558
  ${
2324
- test.stderr && test.stderr.length > 0
2559
+ testData.stderr && testData.stderr.length > 0
2325
2560
  ? `<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(
2326
- test.stderr.map((line) => sanitizeHTML(line)).join("\n"),
2561
+ testData.stderr.map((line) => sanitizeHTML(line)).join("\\n"),
2327
2562
  )}</pre></div>`
2328
2563
  : ""
2329
2564
  }
2330
2565
  ${
2331
- test.screenshots && test.screenshots.length > 0
2566
+ testData.screenshots && testData.screenshots.length > 0
2332
2567
  ? `
2333
2568
  <div class="attachments-section">
2334
2569
  <h4>Screenshots</h4>
2335
2570
  <div class="attachments-grid">
2336
- ${test.screenshots
2571
+ ${testData.screenshots
2337
2572
  .map(
2338
- (screenshot, index) => `
2573
+ (screenshot, screenshotIndex) => `
2339
2574
  <div class="attachment-item">
2340
2575
  <img src="${fixPath(screenshot)}" alt="Screenshot ${
2341
- index + 1
2576
+ screenshotIndex + 1
2342
2577
  }">
2343
2578
  <div class="attachment-info">
2344
2579
  <div class="trace-actions">
@@ -2347,7 +2582,7 @@ function generateHTML(reportData, trendData = null) {
2347
2582
  )}" target="_blank" class="view-full">View Full Image</a>
2348
2583
  <a href="${fixPath(
2349
2584
  screenshot,
2350
- )}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
2585
+ )}" target="_blank" download="screenshot-${Date.now()}-${screenshotIndex}.png">Download</a>
2351
2586
  </div>
2352
2587
  </div>
2353
2588
  </div>
@@ -2360,9 +2595,9 @@ function generateHTML(reportData, trendData = null) {
2360
2595
  : ""
2361
2596
  }
2362
2597
  ${
2363
- test.videoPath && test.videoPath.length > 0
2364
- ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
2365
- .map((videoUrl, index) => {
2598
+ testData.videoPath && testData.videoPath.length > 0
2599
+ ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${testData.videoPath
2600
+ .map((videoUrl, videoIndex) => {
2366
2601
  const fixedVideoUrl = fixPath(videoUrl);
2367
2602
  const fileExtension = String(fixedVideoUrl)
2368
2603
  .split(".")
@@ -2378,7 +2613,7 @@ function generateHTML(reportData, trendData = null) {
2378
2613
  }[fileExtension] || "video/mp4";
2379
2614
  return `<div class="attachment-item video-item">
2380
2615
  <video controls width="100%" height="auto" title="Video ${
2381
- index + 1
2616
+ videoIndex + 1
2382
2617
  }">
2383
2618
  <source src="${sanitizeHTML(
2384
2619
  fixedVideoUrl,
@@ -2389,7 +2624,7 @@ function generateHTML(reportData, trendData = null) {
2389
2624
  <div class="trace-actions">
2390
2625
  <a href="${sanitizeHTML(
2391
2626
  fixedVideoUrl,
2392
- )}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
2627
+ )}" target="_blank" download="video-${Date.now()}-${videoIndex}.${fileExtension}">Download</a>
2393
2628
  </div>
2394
2629
  </div>
2395
2630
  </div>`;
@@ -2398,7 +2633,7 @@ function generateHTML(reportData, trendData = null) {
2398
2633
  : ""
2399
2634
  }
2400
2635
  ${
2401
- test.tracePath
2636
+ testData.tracePath
2402
2637
  ? `
2403
2638
  <div class="attachments-section">
2404
2639
  <h4>Trace Files</h4>
@@ -2407,15 +2642,15 @@ function generateHTML(reportData, trendData = null) {
2407
2642
  <div class="trace-preview">
2408
2643
  <span class="trace-icon">📄</span>
2409
2644
  <span class="trace-name">${sanitizeHTML(
2410
- path.basename(test.tracePath),
2645
+ path.basename(testData.tracePath),
2411
2646
  )}</span>
2412
2647
  </div>
2413
2648
  <div class="attachment-info">
2414
2649
  <div class="trace-actions">
2415
2650
  <a href="${sanitizeHTML(
2416
- fixPath(test.tracePath),
2651
+ fixPath(testData.tracePath),
2417
2652
  )}" target="_blank" download="${sanitizeHTML(
2418
- path.basename(test.tracePath),
2653
+ path.basename(testData.tracePath),
2419
2654
  )}" class="download-trace">Download Trace</a>
2420
2655
  </div>
2421
2656
  </div>
@@ -2426,12 +2661,12 @@ function generateHTML(reportData, trendData = null) {
2426
2661
  : ""
2427
2662
  }
2428
2663
  ${
2429
- test.attachments && test.attachments.length > 0
2664
+ testData.attachments && testData.attachments.length > 0
2430
2665
  ? `
2431
2666
  <div class="attachments-section">
2432
2667
  <h4>Other Attachments</h4>
2433
2668
  <div class="attachments-grid">
2434
- ${test.attachments
2669
+ ${testData.attachments
2435
2670
  .map(
2436
2671
  (attachment) => `
2437
2672
  <div class="attachment-item generic-attachment">
@@ -2467,13 +2702,76 @@ function generateHTML(reportData, trendData = null) {
2467
2702
  `
2468
2703
  : ""
2469
2704
  }
2470
- ${
2471
- test.codeSnippet
2472
- ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
2473
- sanitizeHTML(test.codeSnippet),
2474
- )}</code></pre></div>`
2475
- : ""
2476
- }
2705
+
2706
+ `;
2707
+ };
2708
+
2709
+ // Determine header status: use final_status if retried, else normal status
2710
+ const headerStatus = (test.retryHistory && test.retryHistory.length > 0 && test.final_status)
2711
+ ? test.final_status
2712
+ : test.status;
2713
+
2714
+ const outcomeBadge = (test.outcome && test.outcome !== 'flaky')
2715
+ ? `<span class="outcome-badge ${test.outcome}">${test.outcome}</span>`
2716
+ : '';
2717
+
2718
+ return `
2719
+ <div class="test-case" data-status="${
2720
+ headerStatus
2721
+ }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
2722
+ .join(",")
2723
+ .toLowerCase()}">
2724
+ <div class="test-case-header" role="button" aria-expanded="false">
2725
+ <div class="test-case-summary">
2726
+ <span class="test-case-title" title="${sanitizeHTML(
2727
+ test.name,
2728
+ )}">${sanitizeHTML(testTitle)}</span>
2729
+ <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
2730
+ </div>
2731
+ <div class="test-case-meta">
2732
+ ${severityBadge}
2733
+ ${retryBadge}
2734
+ ${outcomeBadge}
2735
+ ${
2736
+ test.tags && test.tags.length > 0
2737
+ ? test.tags
2738
+ .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
2739
+ .join(" ")
2740
+ : ""
2741
+ }
2742
+ </div>
2743
+ <div class="test-case-status-duration">
2744
+ <span class="status-badge ${getStatusClass(headerStatus)}">${String(
2745
+ headerStatus,
2746
+ ).toUpperCase()}</span>
2747
+ <span class="test-duration">${formatDuration(test.duration)}</span>
2748
+ </div>
2749
+ </div>
2750
+ <div class="test-case-content" style="display: none;">
2751
+ ${test.retryHistory && test.retryHistory.length > 0 ? `
2752
+ <div class="retry-tabs-container">
2753
+ <div class="retry-tabs-header">
2754
+ <button class="retry-tab active" onclick="switchRetryTab(event, 'base-run-${test.id}')">
2755
+ Base Run ${getSmallStatusBadge(test.final_status || test.status)}
2756
+ </button>
2757
+ ${test.retryHistory.map((retry, idx) => `
2758
+ <button class="retry-tab" onclick="switchRetryTab(event, 'retry-${idx + 1}-${test.id}')">
2759
+ Retry ${idx + 1} ${getSmallStatusBadge(retry.final_status || retry.status)}
2760
+ </button>
2761
+ `).join('')}
2762
+ </div>
2763
+
2764
+ <div id="base-run-${test.id}" class="retry-tab-content active">
2765
+ ${getTestContentHTML(test, 'base')}
2766
+ </div>
2767
+
2768
+ ${test.retryHistory.map((retry, idx) => `
2769
+ <div id="retry-${idx + 1}-${test.id}" class="retry-tab-content" style="display: none;">
2770
+ ${getTestContentHTML(retry, `retry-${idx + 1}`)}
2771
+ </div>
2772
+ `).join('')}
2773
+ </div>
2774
+ ` : getTestContentHTML(test, 'single')}
2477
2775
  </div>
2478
2776
  </div>`;
2479
2777
  })
@@ -2506,7 +2804,8 @@ function generateHTML(reportData, trendData = null) {
2506
2804
  --success-color: #10b981; --success-dark: #059669; --success-light: #34d399;
2507
2805
  --danger-color: #ef4444; --danger-dark: #dc2626; --danger-light: #f87171;
2508
2806
  --warning-color: #f59e0b; --warning-dark: #d97706; --warning-light: #fbbf24;
2509
- --info-color: #3b82f6;
2807
+ --info-color: #3b82f6;
2808
+ --flaky-color: #00ccd3;
2510
2809
  --neutral-50: #fafafa; --neutral-100: #f5f5f5; --neutral-200: #e5e5e5; --neutral-300: #d4d4d4;
2511
2810
  --neutral-400: #a3a3a3; --neutral-500: #737373; --neutral-600: #525252; --neutral-700: #404040;
2512
2811
  --neutral-800: #262626; --neutral-900: #171717;
@@ -2524,7 +2823,9 @@ function generateHTML(reportData, trendData = null) {
2524
2823
  --glow-primary: 0 0 20px rgba(99, 102, 241, 0.4), 0 0 40px rgba(99, 102, 241, 0.2);
2525
2824
  --glow-success: 0 0 20px rgba(16, 185, 129, 0.4), 0 0 40px rgba(16, 185, 129, 0.2);
2526
2825
  --glow-danger: 0 0 20px rgba(239, 68, 68, 0.4), 0 0 40px rgba(239, 68, 68, 0.2);
2527
- }
2826
+ --bg-card: #ffffff; --bg-card-hover: #f8fafc;
2827
+ --gradient-card: linear-gradient(145deg, #ffffff 0%, #f9fafb 100%);
2828
+ --border-medium: #cbd5e1;
2528
2829
  * { margin: 0; padding: 0; box-sizing: border-box; }
2529
2830
  ::selection { background: var(--primary-color); color: white; }
2530
2831
  ::-webkit-scrollbar { width: 0; height: 0; display: none; }
@@ -2596,11 +2897,11 @@ function generateHTML(reportData, trendData = null) {
2596
2897
  display: flex;
2597
2898
  gap: 16px;
2598
2899
  align-items: stretch;
2599
- background: #ffffff;
2900
+ background: transparent;
2600
2901
  border-radius: 12px;
2601
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
2602
- border: 1px solid #e2e8f0;
2603
- overflow: hidden;
2902
+ padding: 0;
2903
+ box-shadow: var(--shadow-md); /* Inherited from base static style */
2904
+ overflow: hidden; /* Inherited */
2604
2905
  }
2605
2906
  .run-info-item {
2606
2907
  display: flex;
@@ -2609,54 +2910,61 @@ function generateHTML(reportData, trendData = null) {
2609
2910
  padding: 16px 28px;
2610
2911
  position: relative;
2611
2912
  flex: 1;
2612
- min-width: 0;
2613
- max-width: 100%;
2614
- overflow-wrap: break-word;
2615
- word-break: break-word;
2616
- }
2617
- .run-info-item:not(:last-child)::after {
2618
- content: '';
2619
- position: absolute;
2620
- right: 0;
2621
- top: 20%;
2622
- bottom: 20%;
2623
- width: 1px;
2624
- background: linear-gradient(to bottom, transparent, #e2e8f0 20%, #e2e8f0 80%, transparent);
2913
+ min-width: fit-content;
2625
2914
  }
2915
+
2626
2916
  .run-info-item:first-child {
2627
- background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
2917
+ background: linear-gradient(135deg, rgba(251, 191, 36, 0.2) 0%, rgba(245, 158, 11, 0.15) 50%, rgba(217, 119, 6, 0.1) 100%);
2918
+ border: 1px solid rgba(251, 191, 36, 0.3);
2919
+ border-radius: var(--radius-md);
2920
+ box-shadow: 0 4px 16px rgba(251, 191, 36, 0.2), inset 0 1px 0 rgba(251, 191, 36, 0.25), 0 0 40px rgba(251, 191, 36, 0.08);
2921
+ }
2922
+ .run-info-item:first-child:hover {
2923
+ background: linear-gradient(135deg, rgba(251, 191, 36, 0.28) 0%, rgba(245, 158, 11, 0.22) 50%, rgba(217, 119, 6, 0.15) 100%);
2924
+ border-color: rgba(251, 191, 36, 0.4);
2925
+ box-shadow: 0 8px 24px rgba(251, 191, 36, 0.3), inset 0 1px 0 rgba(251, 191, 36, 0.35), 0 0 50px rgba(251, 191, 36, 0.15);
2628
2926
  }
2629
2927
  .run-info-item:last-child {
2630
- background: linear-gradient(135deg, #ddd6fe 0%, #c4b5fd 100%);
2928
+ background: linear-gradient(135deg, rgba(139, 92, 246, 0.18) 0%, rgba(124, 58, 237, 0.12) 50%, rgba(109, 40, 217, 0.08) 100%);
2929
+ border: 1px solid rgba(139, 92, 246, 0.3);
2930
+ border-radius: var(--radius-md);
2931
+ box-shadow: 0 4px 16px rgba(139, 92, 246, 0.2), inset 0 1px 0 rgba(139, 92, 246, 0.25), 0 0 40px rgba(139, 92, 246, 0.08);
2932
+ }
2933
+ .run-info-item:last-child:hover {
2934
+ background: linear-gradient(135deg, rgba(139, 92, 246, 0.25) 0%, rgba(124, 58, 237, 0.18) 50%, rgba(109, 40, 217, 0.12) 100%);
2935
+ border-color: rgba(139, 92, 246, 0.4);
2936
+ box-shadow: 0 8px 24px rgba(139, 92, 246, 0.3), inset 0 1px 0 rgba(139, 92, 246, 0.35), 0 0 50px rgba(139, 92, 246, 0.15);
2631
2937
  }
2632
2938
  .run-info strong {
2633
2939
  display: flex;
2634
2940
  align-items: center;
2635
- gap: 6px;
2941
+ gap: 8px;
2636
2942
  font-size: 0.7em;
2637
2943
  text-transform: uppercase;
2638
- letter-spacing: 1px;
2639
- color: #64748b;
2944
+ letter-spacing: 1.2px;
2945
+ color: #9ca3af;
2640
2946
  margin: 0;
2641
2947
  font-weight: 700;
2642
2948
  }
2643
2949
  .run-info strong::before {
2644
2950
  content: '';
2645
- width: 8px;
2646
- height: 8px;
2951
+ width: 10px;
2952
+ height: 10px;
2647
2953
  border-radius: 50%;
2648
2954
  background: currentColor;
2649
- opacity: 0.6;
2955
+ opacity: 0.7;
2956
+ box-shadow: 0 0 8px currentColor;
2650
2957
  }
2651
2958
  .run-info-item:first-child strong {
2652
- color: #92400e;
2959
+ color: var(--warning-light);
2653
2960
  }
2654
2961
  .run-info-item:last-child strong {
2655
- color: #5b21b6;
2962
+ color: var(--secondary-light);
2656
2963
  }
2657
2964
  .run-info span {
2965
+ font-size: 1.5em;
2658
2966
  font-weight: 800;
2659
- color: #0f172a;
2967
+ color: #0f172a; /* Adjusted for light theme consistency, static uses #f9fafb */
2660
2968
  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
2661
2969
  letter-spacing: -0.02em;
2662
2970
  line-height: 1.2;
@@ -2720,12 +3028,17 @@ function generateHTML(reportData, trendData = null) {
2720
3028
  }
2721
3029
  }
2722
3030
 
3031
+
3032
+ .stat-pill.flaky { color: #4b5563; }
3033
+
2723
3034
  .dashboard-grid {
2724
3035
  display: grid;
2725
3036
  grid-template-columns: repeat(4, 1fr);
2726
3037
  gap: 0;
2727
3038
  margin: 0 0 40px 0;
2728
3039
  }
3040
+ .stats-pill.failed { color: var(--danger-dark); }
3041
+ .stats-pill.flaky { color: #4b5563; }
2729
3042
  .browser-breakdown {
2730
3043
  display: flex;
2731
3044
  flex-direction: column;
@@ -2766,9 +3079,17 @@ function generateHTML(reportData, trendData = null) {
2766
3079
  color: #0f172a;
2767
3080
  text-transform: capitalize;
2768
3081
  font-size: 1.05em;
3082
+ white-space: nowrap;
3083
+ overflow: hidden;
3084
+ text-overflow: ellipsis;
3085
+ flex: 1;
3086
+ min-width: 0;
3087
+ margin-right: 8px;
2769
3088
  }
2770
3089
  .browser-stats {
2771
3090
  color: #64748b;
3091
+ white-space: nowrap;
3092
+ flex-shrink: 0;
2772
3093
  font-weight: 700;
2773
3094
  font-size: 0.95em;
2774
3095
  }
@@ -2812,9 +3133,11 @@ function generateHTML(reportData, trendData = null) {
2812
3133
  align-items: flex-start;
2813
3134
  }
2814
3135
  .run-info {
3136
+ flex-direction: column;
3137
+ gap: 0;
2815
3138
  width: 100%;
2816
- justify-content: flex-start;
2817
- gap: 24px;
3139
+ border-radius: 14px;
3140
+ overflow: hidden;
2818
3141
  }
2819
3142
  .dashboard-grid {
2820
3143
  grid-template-columns: repeat(2, 1fr);
@@ -2823,11 +3146,23 @@ function generateHTML(reportData, trendData = null) {
2823
3146
  .summary-card:nth-child(n+7) { border-bottom: none; }
2824
3147
  .filters {
2825
3148
  padding: 24px;
2826
- flex-direction: column;
3149
+ flex-wrap: wrap;
3150
+ gap: 12px;
3151
+ }
3152
+ .filters input {
3153
+ flex: 1 1 auto;
3154
+ min-width: 0;
3155
+ width: auto;
3156
+ }
3157
+ .filters select {
3158
+ flex: 0 0 auto;
3159
+ min-width: 0;
3160
+ width: auto;
3161
+ }
3162
+ .filters button {
3163
+ width: auto;
3164
+ flex: 0 0 auto;
2827
3165
  }
2828
- .filters input { min-width: 100%; }
2829
- .filters select { min-width: 100%; }
2830
- .filters button { width: 100%; }
2831
3166
  .copy-btn {
2832
3167
  font-size: 0.75em;
2833
3168
  padding: 8px 16px;
@@ -3026,16 +3361,13 @@ function generateHTML(reportData, trendData = null) {
3026
3361
  display: none;
3027
3362
  }
3028
3363
  .run-info-item:not(:last-child) {
3029
- border-bottom: 1px solid var(--light-gray-color);
3364
+ border-bottom: 1px solid var(--border-medium);
3030
3365
  }
3031
- .run-info strong {
3032
- font-size: 0.65em;
3366
+ .run-info strong {
3367
+ font-size: 0.65em;
3033
3368
  }
3034
- .run-info span {
3035
- font-size: 1.1em;
3036
- white-space: normal;
3037
- word-break: break-word;
3038
- overflow-wrap: break-word;
3369
+ .run-info span {
3370
+ font-size: 1.1em;
3039
3371
  }
3040
3372
  .tabs {
3041
3373
  flex-wrap: wrap;
@@ -3151,12 +3483,19 @@ function generateHTML(reportData, trendData = null) {
3151
3483
  box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2);
3152
3484
  }
3153
3485
  .summary-card.status-failed .value { color: #ef4444; }
3486
+ .summary-card.status-flaky::before { background: #00ccd3; }
3154
3487
  .summary-card.status-skipped { background: rgba(245, 158, 11, 0.02); }
3155
3488
  .summary-card.status-skipped:hover {
3156
3489
  background: rgba(245, 158, 11, 0.15);
3157
3490
  box-shadow: 0 4px 12px rgba(245, 158, 11, 0.2);
3158
3491
  }
3159
3492
  .summary-card.status-skipped .value { color: #f59e0b; }
3493
+ .summary-card.flaky-status { background: rgba(0, 204, 211, 0.05); }
3494
+ .summary-card.flaky-status:hover {
3495
+ background: rgba(0, 204, 211, 0.15);
3496
+ box-shadow: 0 4px 12px rgba(0, 204, 211, 0.2);
3497
+ }
3498
+ .summary-card.flaky-status .value { color: #00ccd3; }
3160
3499
  .summary-card:not([class*='status-']) .value { color: #0f172a; }
3161
3500
  .dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: start; }
3162
3501
  .dashboard-column {
@@ -3197,58 +3536,167 @@ function generateHTML(reportData, trendData = null) {
3197
3536
  .status-badge-small-tooltip.status-failed { background-color: var(--danger-color); }
3198
3537
  .status-badge-small-tooltip.status-skipped { background-color: var(--warning-color); }
3199
3538
  .status-badge-small-tooltip.status-unknown { background-color: var(--dark-gray-color); }
3200
- .suites-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
3539
+ .suites-header {
3540
+ flex-shrink: 0;
3541
+ display: flex;
3542
+ justify-content: space-between;
3543
+ align-items: center;
3544
+ margin-bottom: 20px;
3545
+ }
3201
3546
  .summary-badge { background-color: var(--light-gray-color); color: var(--text-color-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
3202
3547
  .suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
3203
- .suite-card {
3204
- border: none;
3205
- border-left: 4px solid #e2e8f0;
3206
- padding: 24px;
3207
- background: white;
3208
- transition: all 0.15s ease;
3548
+ .suites-widget {
3549
+ display: flex;
3550
+ flex-direction: column;
3209
3551
  }
3210
- .suite-card:hover {
3211
- background: #fafbfc;
3212
- border-left-color: #6366f1;
3213
- }
3214
- .suite-card.status-passed { border-left-color: #10b981; }
3215
- .suite-card.status-passed:hover { background: rgba(16, 185, 129, 0.02); }
3216
- .suite-card.status-failed { border-left-color: #ef4444; }
3217
- .suite-card.status-failed:hover { background: rgba(239, 68, 68, 0.02); }
3218
- .suite-card.status-skipped { border-left-color: #f59e0b; }
3219
- .suite-card.status-skipped:hover { background: rgba(245, 158, 11, 0.02); }
3220
- .suite-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
3221
- .suite-name { font-weight: 600; font-size: 1.05em; color: var(--text-color); margin-right: 10px; word-break: break-word;}
3552
+ .fixed-height-widget {
3553
+ height: 450px;
3554
+ }
3555
+ .suites-grid-container {
3556
+ flex-grow: 1;
3557
+ overflow-y: auto;
3558
+ padding-right: 5px;
3559
+ }
3560
+
3561
+ @media (max-width: 768px) {
3562
+ .fixed-height-widget {
3563
+ height: auto;
3564
+ max-height: 600px;
3565
+ }
3566
+ }
3567
+ .suite-card {
3568
+ background: #ffffff;
3569
+ border: 1px solid var(--border-light);
3570
+ border-radius: 16px;
3571
+ padding: 24px;
3572
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
3573
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3574
+ display: flex;
3575
+ flex-direction: column;
3576
+ height: 100%;
3577
+ position: relative;
3578
+ overflow: hidden;
3579
+ }
3580
+ .suite-card:hover {
3581
+ transform: translateY(-4px);
3582
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
3583
+ border-color: var(--primary-light);
3584
+ }
3585
+ .suite-card::before {
3586
+ content: '';
3587
+ position: absolute;
3588
+ top: 0;
3589
+ left: 0;
3590
+ width: 100%;
3591
+ height: 4px;
3592
+ background: var(--neutral-200);
3593
+ opacity: 0.8;
3594
+ transition: background 0.3s ease;
3595
+ }
3596
+ .suite-card.status-passed::before { background: var(--success-color); }
3597
+ .suite-card.status-failed::before { background: var(--danger-color); }
3598
+ .suite-card.status-flaky::before { background: #00ccd3; }
3599
+ .suite-card.status-skipped::before { background: var(--warning-color); }
3600
+
3601
+ /* Outcome Badge */
3602
+ .outcome-badge {
3603
+ background-color: var(--secondary-color);
3604
+ color: #fff;
3605
+ padding: 2px 8px;
3606
+ border-radius: 4px;
3607
+ font-size: 0.75em;
3608
+ font-weight: 700;
3609
+ text-transform: uppercase;
3610
+ margin-right: 8px;
3611
+ letter-spacing: 0.5px;
3612
+ }
3613
+ .outcome-badge.flaky {
3614
+ background-color: #00ccd3;
3615
+ color: #fff;
3616
+ }
3617
+
3618
+ .suite-card-header {
3619
+ display: flex;
3620
+ justify-content: space-between;
3621
+ align-items: flex-start;
3622
+ margin-bottom: 16px;
3623
+ }
3624
+ .suite-name {
3625
+ font-size: 1.15em;
3626
+ font-weight: 700;
3627
+ color: var(--text-primary);
3628
+ line-height: 1.4;
3629
+ display: -webkit-box;
3630
+ -webkit-line-clamp: 2;
3631
+ -webkit-box-orient: vertical;
3632
+ overflow: hidden;
3633
+ margin-right: 12px;
3634
+ }
3635
+ .status-indicator-dot {
3636
+ width: 10px;
3637
+ height: 10px;
3638
+ border-radius: 50%;
3639
+ flex-shrink: 0;
3640
+ margin-top: 6px;
3641
+ }
3642
+ .status-indicator-dot.status-passed { background-color: var(--success-color); box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.15); }
3643
+ .status-indicator-dot.status-failed { background-color: var(--danger-color); box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.15); }
3644
+ .status-indicator-dot.status-flaky { background-color: #00ccd3; box-shadow: 0 0 0 4px rgba(0, 204, 211, 0.15); }
3645
+ .status-indicator-dot.status-skipped { background-color: rgba(245, 158, 11, 0.1); color: var(--warning-dark); border: 1px solid rgba(245, 158, 11, 0.2); }
3646
+ .status-flaky { background-color: rgba(0, 204, 211, 0.1); color: #00ccd3; border: 1px solid #00ccd3; }
3647
+
3222
3648
  .browser-tag {
3649
+ font-size: 0.8em;
3650
+ font-weight: 600;
3651
+ background: var(--bg-secondary);
3652
+ color: var(--text-secondary);
3653
+ padding: 4px 10px;
3654
+ border-radius: 20px;
3655
+ border: 1px solid var(--border-light);
3656
+ display: inline-flex;
3657
+ align-items: center;
3658
+ gap: 6px;
3659
+ margin-bottom: 20px;
3660
+ align-self: flex-start;
3661
+ box-shadow: none;
3662
+ text-shadow: none;
3663
+ }
3664
+
3665
+ .suite-card-body {
3666
+ margin-top: auto;
3667
+ }
3668
+
3669
+ .test-count-label {
3223
3670
  font-size: 0.85em;
3224
3671
  font-weight: 600;
3225
- background: linear-gradient(135deg, rgba(96, 165, 250, 0.2) 0%, rgba(59, 130, 246, 0.15) 100%);
3226
- padding: 6px 12px;
3227
- border-radius: var(--radius-sm);
3228
- border: 1px solid rgba(96, 165, 250, 0.3);
3229
- display: inline-block;
3230
- box-shadow: 0 2px 8px rgba(96, 165, 250, 0.15), inset 0 1px 0 rgba(96, 165, 250, 0.2);
3231
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
3232
- letter-spacing: 0.3px;
3233
- max-width: 200px;
3234
- overflow: hidden;
3235
- text-overflow: ellipsis;
3236
- white-space: nowrap;
3237
- vertical-align: middle;
3238
- cursor: help;
3239
- transition: all 0.2s ease;
3672
+ color: var(--text-tertiary);
3673
+ text-transform: uppercase;
3674
+ letter-spacing: 0.05em;
3675
+ margin-bottom: 8px;
3676
+ display: block;
3677
+ }
3678
+
3679
+ .suite-stats {
3680
+ display: flex;
3681
+ gap: 8px;
3682
+ background: var(--bg-secondary);
3683
+ padding: 10px 14px;
3684
+ border-radius: 10px;
3685
+ justify-content: space-between;
3240
3686
  }
3241
- .browser-tag:hover {
3242
- background: linear-gradient(135deg, rgba(96, 165, 250, 0.3) 0%, rgba(59, 130, 246, 0.25) 100%);
3243
- border-color: rgba(96, 165, 250, 0.5);
3244
- }
3245
- .suite-card-body .test-count { font-size: 0.95em; color: var(--text-color-secondary); display: block; margin-bottom: 10px; }
3246
- .suite-stats { display: flex; gap: 14px; font-size: 0.95em; align-items: center; }
3247
- .suite-stats span { display: flex; align-items: center; gap: 6px; }
3248
- .suite-stats svg { vertical-align: middle; font-size: 1.15em; }
3249
- .suite-stats .stat-passed { color: #10b981; }
3250
- .suite-stats .stat-failed { color: #ef4444; }
3251
- .suite-stats .stat-skipped { color: #f59e0b; }
3687
+
3688
+ .stat-pill {
3689
+ display: flex;
3690
+ align-items: center;
3691
+ gap: 6px;
3692
+ font-size: 0.9em;
3693
+ font-weight: 600;
3694
+ }
3695
+ .stat-pill svg { width: 14px; height: 14px; }
3696
+ .stat-pill.passed { color: var(--success-dark); }
3697
+ .stat-pill.failed { color: var(--danger-dark); }
3698
+ .stat-pill.flaky { color: #00ccd3; }
3699
+ .stat-pill.skipped { color: var(--warning-dark); }
3252
3700
  .filters {
3253
3701
  display: flex;
3254
3702
  flex-wrap: wrap;
@@ -3280,6 +3728,7 @@ function generateHTML(reportData, trendData = null) {
3280
3728
  min-width: 180px;
3281
3729
  background: white;
3282
3730
  cursor: pointer;
3731
+ width: 100%;
3283
3732
  }
3284
3733
  .filters select:focus {
3285
3734
  outline: none;
@@ -3411,7 +3860,7 @@ function generateHTML(reportData, trendData = null) {
3411
3860
  border-radius: 0;
3412
3861
  font-size: 0.7em;
3413
3862
  font-weight: 800;
3414
- color: white;
3863
+ color: black;
3415
3864
  text-transform: uppercase;
3416
3865
  min-width: 100px;
3417
3866
  text-align: center;
@@ -3473,6 +3922,65 @@ function generateHTML(reportData, trendData = null) {
3473
3922
  border-color: rgba(148, 163, 184, 0.25);
3474
3923
  }
3475
3924
 
3925
+ /* --- RETRY COUNT BADGE --- */
3926
+ .retry-badge {
3927
+ display: inline-flex;
3928
+ align-items: center;
3929
+ padding: 5px 12px;
3930
+ border-radius: 12px;
3931
+ font-size: 0.75rem;
3932
+ font-weight: 600;
3933
+ background: rgba(147, 51, 234, 0.15);
3934
+ color: #a855f7;
3935
+ border: 1px solid rgba(147, 51, 234, 0.3);
3936
+ margin-left: 8px;
3937
+ }
3938
+
3939
+ /* --- RETRY TABS --- */
3940
+ .retry-tabs-container {
3941
+ margin-top: 16px;
3942
+ }
3943
+
3944
+ .retry-tabs-header {
3945
+ display: flex;
3946
+ gap: 8px;
3947
+ border-bottom: 2px solid var(--border-medium);
3948
+ margin-bottom: 20px;
3949
+ flex-wrap: wrap;
3950
+ }
3951
+
3952
+ .retry-tab {
3953
+ padding: 10px 20px;
3954
+ background: transparent;
3955
+ border: none;
3956
+ border-bottom: 3px solid transparent;
3957
+ cursor: pointer;
3958
+ font-size: 0.95rem;
3959
+ font-weight: 600;
3960
+ color: var(--text-color-secondary);
3961
+ transition: all 0.2s ease;
3962
+ }
3963
+
3964
+ .retry-tab:hover {
3965
+ color: var(--primary-color);
3966
+ background: rgba(147, 51, 234, 0.05);
3967
+ }
3968
+
3969
+ .retry-tab.active {
3970
+ color: #a855f7;
3971
+ border-bottom-color: #a855f7;
3972
+ background: rgba(147, 51, 234, 0.1);
3973
+ }
3974
+
3975
+ .retry-tab-content {
3976
+ animation: fadeIn 0.3s ease-in;
3977
+ }
3978
+
3979
+ @keyframes fadeIn {
3980
+ from { opacity: 0; }
3981
+ to { opacity: 1; }
3982
+ }
3983
+
3476
3984
  .tag {
3477
3985
  display: inline-flex;
3478
3986
  align-items: center;
@@ -3503,7 +4011,16 @@ function generateHTML(reportData, trendData = null) {
3503
4011
  }
3504
4012
  .test-case-content h4 { margin-top: 22px; margin-bottom: 14px; font-size: 1.15em; color: var(--primary-color); }
3505
4013
  .test-case-content p { margin-bottom: 10px; font-size: 1em; }
3506
- .test-error-summary { margin-bottom: 20px; padding: 14px; background-color: rgba(244,67,54,0.05); border: 1px solid rgba(244,67,54,0.2); border-left: 4px solid var(--danger-color); border-radius: 4px; }
4014
+ .test-error-summary {
4015
+ margin-bottom: 20px;
4016
+ padding: 14px;
4017
+ background-color: rgba(244,67,54,0.05);
4018
+ border: 1px solid rgba(244,67,54,0.2);
4019
+ border-left: 4px solid var(--danger-color);
4020
+ border-radius: 4px;
4021
+ display: flex;
4022
+ flex-direction: column;
4023
+ }
3507
4024
  .test-error-summary h4 { color: var(--danger-color); margin-top:0;}
3508
4025
  .test-error-summary pre { white-space: pre-wrap; word-break: break-all; color: var(--danger-color); font-size: 0.95em;}
3509
4026
  .steps-list { margin: 18px 0; }
@@ -3515,10 +4032,15 @@ function generateHTML(reportData, trendData = null) {
3515
4032
  .step-duration { color: var(--dark-gray-color); font-size: 0.9em; }
3516
4033
  .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); }
3517
4034
  .step-info { margin-bottom: 8px; }
4035
+ .code-snippet-section { margin: 12px 0; }
4036
+ .code-snippet { background-color: #f8f9fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 12px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 0.9em; line-height: 1.5; overflow-x: auto; color: #24292e; margin: 0; white-space: pre; }
3518
4037
  .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); }
3519
4038
  .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; }
3520
4039
  .step-hook { background-color: rgba(33,150,243,0.04); border-left: 3px solid var(--info-color) !important; }
3521
4040
  .step-hook .step-title { font-style: italic; color: var(--info-color)}
4041
+ .failed-step-highlight { border-left: 4px solid var(--danger-color) !important; background-color: rgba(244,67,54,0.03); }
4042
+ .failed-step-highlight .step-header { background-color: rgba(244,67,54,0.05); border-color: rgba(244,67,54,0.3); }
4043
+ .failed-step-marker { display: inline-block; margin-left: 10px; padding: 2px 8px; background-color: var(--danger-color); color: white; border-radius: 4px; font-size: 0.85em; font-weight: 600; }
3522
4044
  .nested-steps { margin-top: 12px; }
3523
4045
  .attachments-section { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--light-gray-color); }
3524
4046
  .attachments-section h4 { margin-top: 0; margin-bottom: 20px; font-size: 1.1em; color: var(--text-color); }
@@ -3651,6 +4173,7 @@ function generateHTML(reportData, trendData = null) {
3651
4173
  color: var(--text-color);
3652
4174
  pointer-events: auto;
3653
4175
  cursor: pointer;
4176
+ width: 100%;
3654
4177
  }
3655
4178
  .filters button.clear-filters-btn:active,
3656
4179
  .filters button.clear-filters-btn:focus {
@@ -4046,31 +4569,33 @@ function generateHTML(reportData, trendData = null) {
4046
4569
  <div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
4047
4570
  runSummary.skipped || 0
4048
4571
  }</div><div class="trend-percentage">${skipPercentage}%</div></div>
4049
- <div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
4050
- <div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
4051
- runSummary.duration,
4052
- )}</div></div>
4053
- <div class="summary-card">
4054
- <h3>🔄 Retry Count</h3>
4055
- <div class="value">${totalRetried}</div>
4056
- </div>
4572
+ <div class="summary-card flaky-status"><h3>Flaky</h3><div class="value">${runSummary.flaky || 0}</div>
4573
+ <div class="trend-percentage">${flakyPercentage}%</div></div>
4574
+ <div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
4575
+ runSummary.duration,
4576
+ )}</div><div class="trend-percentage">Avg. Test Duration ${avgTestDuration}</div></div>
4577
+ <div class="summary-card">
4578
+ <h3>Total Retry Count</h3>
4579
+ <div class="value">${totalRetried}</div>
4580
+ <div class="trend-percentage">Test Retried ${retriedTestsCount}</div>
4581
+ </div>
4057
4582
  <div class="summary-card">
4058
4583
  <h3>🌐 Browser Distribution <span style="font-size: 0.7em; color: var(--text-color-secondary); font-weight: 400;">(${browserBreakdown.length} total)</span></h3>
4059
4584
  <div class="browser-breakdown" style="max-height: 200px; overflow-y: auto; padding-right: 4px;">
4060
4585
  ${browserBreakdown
4061
- .slice(0, 5)
4586
+ .slice(0, 3)
4062
4587
  .map(
4063
4588
  (b) =>
4064
4589
  `<div class="browser-item">
4065
- <span class="browser-name">${sanitizeHTML(b.browser)}</span>
4590
+ <span class="browser-name" title="${sanitizeHTML(b.browser)}">${sanitizeHTML(b.browser)}</span>
4066
4591
  <span class="browser-stats">${b.percentage}% (${b.count})</span>
4067
4592
  </div>`,
4068
4593
  )
4069
4594
  .join("")}
4070
4595
  ${
4071
- browserBreakdown.length > 5
4596
+ browserBreakdown.length > 3
4072
4597
  ? `<div class="browser-item" style="opacity: 0.6; font-style: italic; justify-content: center; border-top: 1px solid #e2e8f0; margin-top: 8px; padding-top: 8px;">
4073
- <span>+${browserBreakdown.length - 5} more browsers</span>
4598
+ <span>+${browserBreakdown.length - 3} more browsers</span>
4074
4599
  </div>`
4075
4600
  : ""
4076
4601
  }
@@ -4083,17 +4608,13 @@ function generateHTML(reportData, trendData = null) {
4083
4608
  [
4084
4609
  { label: "Passed", value: runSummary.passed },
4085
4610
  { label: "Failed", value: runSummary.failed },
4611
+ { label: "Flaky", value: runSummary.flaky || 0 },
4086
4612
  { label: "Skipped", value: runSummary.skipped || 0 },
4087
4613
  ],
4088
4614
  400,
4089
4615
  390,
4090
4616
  )}
4091
- ${
4092
- runSummary.environment &&
4093
- Object.keys(runSummary.environment).length > 0
4094
- ? generateEnvironmentDashboard(runSummary.environment)
4095
- : '<div class="no-data">Environment data not available.</div>'
4096
- }
4617
+ ${generateEnvironmentSection(runSummary.environment)}
4097
4618
  </div>
4098
4619
 
4099
4620
  <div class="dashboard-column">
@@ -4105,7 +4626,7 @@ function generateHTML(reportData, trendData = null) {
4105
4626
  <div id="test-runs" class="tab-content">
4106
4627
  <div class="filters">
4107
4628
  <input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
4108
- <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>
4629
+ <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>
4109
4630
  <select id="filter-browser"><option value="">All Browsers</option>${Array.from(
4110
4631
  new Set(
4111
4632
  (results || []).map((test) => test.browser || "unknown"),
@@ -4202,6 +4723,33 @@ function generateHTML(reportData, trendData = null) {
4202
4723
  });
4203
4724
  }
4204
4725
 
4726
+ // --- Retry Tab Switching Function ---
4727
+ function switchRetryTab(event, tabId) {
4728
+ const tabButton = event.currentTarget;
4729
+ const tabsContainer = tabButton.closest('.retry-tabs-container');
4730
+
4731
+ // Hide all tab contents in this container
4732
+ const allTabContents = tabsContainer.querySelectorAll('.retry-tab-content');
4733
+ allTabContents.forEach(content => {
4734
+ content.style.display = 'none';
4735
+ content.classList.remove('active');
4736
+ });
4737
+
4738
+ // Remove active class from all tabs
4739
+ const allTabs = tabsContainer.querySelectorAll('.retry-tab');
4740
+ allTabs.forEach(tab => tab.classList.remove('active'));
4741
+
4742
+ // Show selected tab content
4743
+ const selectedContent = document.getElementById(tabId);
4744
+ if (selectedContent) {
4745
+ selectedContent.style.display = 'block';
4746
+ selectedContent.classList.add('active');
4747
+ }
4748
+
4749
+ // Add active class to clicked tab
4750
+ tabButton.classList.add('active');
4751
+ }
4752
+
4205
4753
  // --- AI Failure Analyzer Functions ---
4206
4754
  function getAIFix(button) {
4207
4755
  const failureItem = button.closest('.compact-failure-item');
@@ -4669,6 +5217,8 @@ async function runScript(scriptPath, args = []) {
4669
5217
  });
4670
5218
  }
4671
5219
  async function main() {
5220
+ await animate();
5221
+
4672
5222
  const __filename = fileURLToPath(import.meta.url);
4673
5223
  const __dirname = path.dirname(__filename);
4674
5224
 
@@ -4834,6 +5384,7 @@ async function main() {
4834
5384
  passed: histRunReport.run.passed,
4835
5385
  failed: histRunReport.run.failed,
4836
5386
  skipped: histRunReport.run.skipped || 0,
5387
+ flaky: histRunReport.run.flaky || (histRunReport.results ? histRunReport.results.filter(r => r.status === 'flaky' || r.outcome === 'flaky').length : 0),
4837
5388
  });
4838
5389
 
4839
5390
  if (histRunReport.results && Array.isArray(histRunReport.results)) {
@@ -4842,7 +5393,7 @@ async function main() {
4842
5393
  (test) => ({
4843
5394
  testName: test.name,
4844
5395
  duration: test.duration,
4845
- status: test.status,
5396
+ status: test.final_status || test.status,
4846
5397
  timestamp: new Date(test.startTime),
4847
5398
  }),
4848
5399
  );
@@ -4860,7 +5411,7 @@ async function main() {
4860
5411
  await fs.writeFile(reportHtmlPath, htmlContent, "utf-8");
4861
5412
  console.log(
4862
5413
  chalk.green.bold(
4863
- `🎉 Pulse report generated successfully at: ${reportHtmlPath}`,
5414
+ `Pulse report generated successfully at: ${reportHtmlPath}`,
4864
5415
  ),
4865
5416
  );
4866
5417
  console.log(chalk.gray(`(You can open this file in your browser)`));