@arghajit/playwright-pulse-report 0.3.2 → 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;
@@ -98,12 +99,12 @@ export function ansiToHtml(text) {
98
99
  if (codes[code]) {
99
100
  if (code === "39") {
100
101
  currentStylesArray = currentStylesArray.filter(
101
- (s) => !s.startsWith("color:")
102
+ (s) => !s.startsWith("color:"),
102
103
  );
103
104
  currentStylesArray.push("color:inherit");
104
105
  } else if (code === "49") {
105
106
  currentStylesArray = currentStylesArray.filter(
106
- (s) => !s.startsWith("background-color:")
107
+ (s) => !s.startsWith("background-color:"),
107
108
  );
108
109
  currentStylesArray.push("background-color:inherit");
109
110
  } else {
@@ -114,10 +115,10 @@ export function ansiToHtml(text) {
114
115
  const type = parts[0] === "38" ? "color" : "background-color";
115
116
  if (parts.length === 5) {
116
117
  currentStylesArray = currentStylesArray.filter(
117
- (s) => !s.startsWith(type + ":")
118
+ (s) => !s.startsWith(type + ":"),
118
119
  );
119
120
  currentStylesArray.push(
120
- `${type}:rgb(${parts[2]},${parts[3]},${parts[4]})`
121
+ `${type}:rgb(${parts[2]},${parts[3]},${parts[4]})`,
121
122
  );
122
123
  }
123
124
  }
@@ -175,7 +176,7 @@ function convertPlaywrightErrorToHTML(str) {
175
176
  if (!str) return "";
176
177
  return str
177
178
  .replace(/^(\s+)/gm, (match) =>
178
- match.replace(/ /g, " ").replace(/\t/g, " ")
179
+ match.replace(/ /g, " ").replace(/\t/g, " "),
179
180
  )
180
181
  .replace(/<red>/g, '<span style="color: red;">')
181
182
  .replace(/<green>/g, '<span style="color: green;">')
@@ -265,7 +266,7 @@ function generateTestTrendsChart(trendData) {
265
266
  .substring(2, 7)}`;
266
267
  const renderFunctionName = `renderTestTrendsChart_${chartId.replace(
267
268
  /-/g,
268
- "_"
269
+ "_",
269
270
  )}`;
270
271
  const runs = trendData.overall;
271
272
 
@@ -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,
@@ -360,7 +367,7 @@ function generateDurationTrendChart(trendData) {
360
367
  .substring(2, 7)}`;
361
368
  const renderFunctionName = `renderDurationTrendChart_${chartId.replace(
362
369
  /-/g,
363
- "_"
370
+ "_",
364
371
  )}`;
365
372
  const runs = trendData.overall;
366
373
 
@@ -455,7 +462,7 @@ function generateTestHistoryChart(history) {
455
462
  if (!history || history.length === 0)
456
463
  return '<div class="no-data-chart">No data for chart</div>';
457
464
  const validHistory = history.filter(
458
- (h) => h && typeof h.duration === "number" && h.duration >= 0
465
+ (h) => h && typeof h.duration === "number" && h.duration >= 0,
459
466
  );
460
467
  if (validHistory.length === 0)
461
468
  return '<div class="no-data-chart">No valid data for chart</div>';
@@ -465,7 +472,7 @@ function generateTestHistoryChart(history) {
465
472
  .substring(2, 7)}`;
466
473
  const renderFunctionName = `renderTestHistoryChart_${chartId.replace(
467
474
  /-/g,
468
- "_"
475
+ "_",
469
476
  )}`;
470
477
 
471
478
  const seriesDataPoints = validHistory.map((run) => {
@@ -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
  }
@@ -499,12 +509,12 @@ function generateTestHistoryChart(history) {
499
509
  const accentColorRGB = "103, 58, 183"; // Assuming var(--accent-color) is Deep Purple #673ab7
500
510
 
501
511
  const categoriesString = JSON.stringify(
502
- validHistory.map((_, i) => `R${i + 1}`)
512
+ validHistory.map((_, i) => `R${i + 1}`),
503
513
  );
504
514
  const seriesDataPointsString = JSON.stringify(seriesDataPoints);
505
515
 
506
516
  return `
507
- <div id="${chartId}" style="width: 320px; height: 100px;" class="lazy-load-chart" data-render-function-name="${renderFunctionName}">
517
+ <div id="${chartId}" style="width: 100%; max-width: 320px; height: 100px;" class="lazy-load-chart" data-render-function-name="${renderFunctionName}">
508
518
  <div class="no-data-chart">Loading History...</div>
509
519
  </div>
510
520
  <script>
@@ -568,7 +578,7 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
568
578
  }
569
579
  const passedEntry = data.find((d) => d.label === "Passed");
570
580
  const passedPercentage = Math.round(
571
- ((passedEntry ? passedEntry.value : 0) / total) * 100
581
+ ((passedEntry ? passedEntry.value : 0) / total) * 100,
572
582
  );
573
583
 
574
584
  const chartId = `pieChart-${Date.now()}-${Math.random()
@@ -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;
@@ -614,13 +627,11 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
614
627
  {
615
628
  chart: {
616
629
  type: 'pie',
617
- width: ${chartWidth},
618
- height: ${
619
- chartHeight - 40
620
- }, // Adjusted height to make space for legend if chartHeight is for the whole wrapper
630
+ width: null,
631
+ height: ${chartHeight - 40},
621
632
  backgroundColor: 'transparent',
622
633
  plotShadow: false,
623
- spacingBottom: 40 // Ensure space for legend
634
+ spacingBottom: 40
624
635
  },
625
636
  title: {
626
637
  text: '${passedPercentage}%',
@@ -670,8 +681,8 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
670
681
  <div class="pie-chart-wrapper" style="align-items: center; max-height: 450px">
671
682
  <div style="display: flex; align-items: start; width: 100%;"><h3>Test Distribution</h3></div>
672
683
  <div id="${chartId}" style="width: ${chartWidth}px; height: ${
673
- chartHeight - 40
674
- }px;"></div>
684
+ chartHeight - 40
685
+ }px;"></div>
675
686
  <script>
676
687
  document.addEventListener('DOMContentLoaded', function() {
677
688
  if (typeof Highcharts !== 'undefined') {
@@ -690,24 +701,175 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
690
701
  </div>
691
702
  `;
692
703
  }
693
- function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
694
- // Format memory for display
695
- const formattedMemory = environment.memory.replace(/(\d+\.\d{2})GB/, "$1 GB");
696
-
697
- // Generate a unique ID for the dashboard
698
- const dashboardId = `envDashboard-${Date.now()}-${Math.random()
699
- .toString(36)
700
- .substring(2, 7)}`;
701
-
702
- const cardHeight = Math.floor(dashboardHeight * 0.44);
703
- const cardContentPadding = 16; // px
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
+ }
704
841
 
705
- // 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";
706
851
  const runContext = process.env.CI ? "CI" : "Local Test";
707
852
 
708
853
  return `
709
- <div class="environment-dashboard-wrapper" id="${dashboardId}">
854
+ <div class="env-modern-card${hideHeader ? " no-header" : ""}">
710
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
+
711
873
  .environment-dashboard-wrapper *,
712
874
  .environment-dashboard-wrapper *::before,
713
875
  .environment-dashboard-wrapper *::after {
@@ -715,326 +877,285 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
715
877
  }
716
878
 
717
879
  .environment-dashboard-wrapper {
718
- --primary-color: #007bff;
719
- --primary-light-color: #e6f2ff;
720
- --secondary-color: #6c757d;
721
- --success-color: #28a745;
722
- --success-light-color: #eaf6ec;
723
- --warning-color: #ffc107;
724
- --warning-light-color: #fff9e6;
725
- --danger-color: #dc3545;
880
+ --primary-color: #6366f1;
881
+ --success-color: #10b981;
882
+ --warning-color: #f59e0b;
726
883
 
727
- --background-color: #ffffff;
728
- --card-background-color: #ffffff;
729
- --text-color: #212529;
730
- --text-color-secondary: #6c757d;
731
- --border-color: #dee2e6;
732
- --border-light-color: #f1f3f5;
733
- --icon-color: #495057;
734
- --chip-background: #e9ecef;
735
- --chip-text: #495057;
736
- --shadow-color: rgba(0, 0, 0, 0.075);
737
-
738
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
739
- background-color: var(--background-color);
740
- border-radius: 12px;
741
- box-shadow: 0 6px 12px var(--shadow-color);
742
- padding: 24px;
743
- color: var(--text-color);
884
+ background-color: white;
885
+ padding: 48px;
886
+ border-bottom: 1px solid #e2e8f0;
887
+ font-family: var(--font-family);
888
+ color: #0f172a;
744
889
  display: grid;
745
- grid-template-columns: 1fr 1fr;
746
- grid-template-rows: auto 1fr;
747
- gap: 20px;
748
- font-size: 14px;
890
+ grid-template-columns: repeat(2, 1fr);
891
+ gap: 32px;
892
+ font-size: 15px;
893
+ transform: translateZ(0);
749
894
  }
750
895
 
751
- /* Mobile Responsiveness */
752
- @media (max-width: 768px) {
753
- .environment-dashboard-wrapper {
754
- grid-template-columns: 1fr; /* Stack columns on mobile */
755
- grid-template-rows: auto;
756
- padding: 16px;
757
- height: auto !important; /* Allow height to grow */
758
- }
759
- .env-card {
760
- height: auto !important; /* Allow cards to grow based on content */
761
- min-height: 200px;
762
- }
896
+ .env-card-header {
897
+ display: flex;
898
+ flex-direction: column;
899
+ padding: 24px 24px 12px;
763
900
  }
764
-
765
- .env-dashboard-header {
766
- grid-column: 1 / -1;
901
+ .env-modern-card.no-header .env-card-header {
902
+ display: none;
903
+ }
904
+ .env-modern-card.no-header {
905
+ margin-top: 0;
906
+ }
907
+ .env-modern-card.no-header .env-card-content {
908
+ padding-top: 24px;
909
+ }
910
+ .env-card-title-row {
767
911
  display: flex;
768
912
  justify-content: space-between;
769
913
  align-items: center;
770
- border-bottom: 1px solid var(--border-color);
771
- padding-bottom: 16px;
772
- margin-bottom: 8px;
773
- flex-wrap: wrap; /* Allow wrapping header items */
774
- gap: 10px;
775
914
  }
776
-
777
- .env-dashboard-title {
778
- font-size: 1.5rem;
915
+ .env-card-title {
916
+ display: flex;
917
+ align-items: center;
918
+ font-size: 16px;
779
919
  font-weight: 600;
780
- color: var(--text-color);
781
- margin: 0;
920
+ color: #0f172a;
921
+ transition: color 0.3s;
782
922
  }
783
-
784
- .env-dashboard-subtitle {
785
- font-size: 0.875rem;
786
- color: var(--text-color-secondary);
787
- margin-top: 4px;
923
+ .env-modern-card:hover .env-card-title {
924
+ color: #6366f1;
788
925
  }
789
-
790
- .env-card {
791
- background-color: var(--card-background-color);
792
- border-radius: 8px;
793
- padding: ${cardContentPadding}px;
794
- box-shadow: 0 3px 6px var(--shadow-color);
795
- height: ${cardHeight}px;
796
- display: flex;
797
- flex-direction: column;
798
- overflow: hidden;
926
+ .env-card-title svg {
927
+ width: 16px;
928
+ height: 16px;
929
+ margin-right: 8px;
930
+ stroke: currentColor;
931
+ fill: none;
799
932
  }
800
-
801
- .env-card-header {
802
- font-weight: 600;
803
- font-size: 1rem;
804
- margin-bottom: 12px;
805
- color: var(--text-color);
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));
806
943
  display: flex;
807
944
  align-items: center;
808
- padding-bottom: 8px;
809
- border-bottom: 1px solid var(--border-light-color);
945
+ justify-content: center;
810
946
  }
811
-
812
- .env-card-header svg {
813
- margin-right: 10px;
814
- width: 18px;
815
- height: 18px;
816
- fill: var(--icon-color);
947
+ .env-icon-badge svg {
948
+ width: 16px;
949
+ height: 16px;
950
+ stroke: #6366f1;
951
+ fill: none;
817
952
  }
818
-
819
953
  .env-card-content {
820
- flex-grow: 1;
821
- overflow-y: auto;
822
- padding-right: 5px;
954
+ padding: 0 24px 24px;
823
955
  }
824
-
825
- .env-detail-row {
826
- display: flex;
827
- justify-content: space-between;
828
- align-items: center;
829
- padding: 10px 0;
830
- border-bottom: 1px solid var(--border-light-color);
831
- font-size: 0.875rem;
832
- flex-wrap: wrap; /* Allow details to wrap on very small screens */
833
- gap: 8px;
834
- }
835
-
836
- .env-detail-row:last-child {
837
- border-bottom: none;
838
- }
839
-
840
- .env-detail-label {
841
- color: var(--text-color-secondary);
842
- font-weight: 500;
843
- margin-right: 10px;
956
+ .env-items-grid {
957
+ display: grid;
958
+ grid-template-columns: repeat(2, 1fr);
959
+ gap: 10px;
844
960
  }
845
-
846
- .env-detail-value {
847
- color: var(--text-color);
848
- font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
849
- text-align: right;
850
- word-break: break-all;
851
- margin-left: auto; /* Push to right */
961
+ @media (min-width: 768px) {
962
+ .env-items-grid {
963
+ grid-template-columns: repeat(4, 1fr);
964
+ }
852
965
  }
853
-
854
- .env-chip {
855
- display: inline-block;
856
- padding: 4px 10px;
857
- border-radius: 16px;
858
- font-size: 0.75rem;
859
- font-weight: 500;
860
- line-height: 1.2;
861
- background-color: var(--chip-background);
862
- color: var(--chip-text);
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;
863
974
  }
864
-
865
- .env-chip-primary {
866
- background-color: var(--primary-light-color);
867
- color: var(--primary-color);
975
+ .env-item:hover {
976
+ background-color: rgba(100, 116, 139, 0.05);
868
977
  }
869
-
870
- .env-chip-success {
871
- background-color: var(--success-light-color);
872
- color: var(--success-color);
978
+ .env-item-icon {
979
+ flex-shrink: 0;
873
980
  }
874
-
875
- .env-chip-warning {
876
- background-color: var(--warning-light-color);
877
- color: var(--warning-color);
981
+ .env-item-icon svg {
982
+ width: 16px;
983
+ height: 16px;
984
+ stroke: #6366f1;
985
+ fill: none;
878
986
  }
879
-
880
- .env-cpu-cores {
881
- display: flex;
882
- align-items: center;
883
- gap: 6px;
987
+ .env-item-content {
988
+ flex-grow: 1;
989
+ min-width: 0;
884
990
  }
885
-
886
- .env-core-indicator {
887
- width: 12px;
888
- height: 12px;
889
- border-radius: 50%;
890
- background-color: var(--success-color);
891
- 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;
892
998
  }
893
-
894
- .env-core-indicator.inactive {
895
- background-color: var(--border-light-color);
896
- opacity: 0.7;
897
- 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;
898
1006
  }
899
1007
  </style>
900
1008
 
901
- <div class="env-dashboard-header">
902
- <div>
903
- <h3 class="env-dashboard-title">System Environment</h3>
904
- <p class="env-dashboard-subtitle">Snapshot of the execution environment</p>
905
- </div>
906
- <span class="env-chip env-chip-primary">${environment.host}</span>
907
- </div>
908
-
909
- <div class="env-card">
910
- <div class="env-card-header">
911
- <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>
912
- Hardware
913
- </div>
914
- <div class="env-card-content">
915
- <div class="env-detail-row">
916
- <span class="env-detail-label">CPU Model</span>
917
- <span class="env-detail-value">${environment.cpu.model}</span>
918
- </div>
919
- <div class="env-detail-row">
920
- <span class="env-detail-label">CPU Cores</span>
921
- <span class="env-detail-value">
922
- <div class="env-cpu-cores">
923
- ${Array.from(
924
- { length: Math.max(0, environment.cpu.cores || 0) },
925
- (_, i) =>
926
- `<div class="env-core-indicator ${
927
- i >=
928
- (environment.cpu.cores >= 8 ? 8 : environment.cpu.cores)
929
- ? "inactive"
930
- : ""
931
- }" title="Core ${i + 1}"></div>`
932
- ).join("")}
933
- <span>${environment.cpu.cores || "N/A"} cores</span>
934
- </div>
935
- </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>
936
1022
  </div>
937
- <div class="env-detail-row">
938
- <span class="env-detail-label">Memory</span>
939
- <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>
940
1027
  </div>
941
1028
  </div>
942
1029
  </div>
943
1030
 
944
- <div class="env-card">
945
- <div class="env-card-header">
946
- <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>
947
- Operating System
948
- </div>
949
- <div class="env-card-content">
950
- <div class="env-detail-row">
951
- <span class="env-detail-label">OS Type</span>
952
- <span class="env-detail-value">${
953
- environment.os.split(" ")[0] === "darwin"
954
- ? "darwin (macOS)"
955
- : environment.os.split(" ")[0] || "Unknown"
956
- }</span>
957
- </div>
958
- <div class="env-detail-row">
959
- <span class="env-detail-label">OS Version</span>
960
- <span class="env-detail-value">${
961
- environment.os.split(" ")[1] || "N/A"
962
- }</span>
963
- </div>
964
- <div class="env-detail-row">
965
- <span class="env-detail-label">Hostname</span>
966
- <span class="env-detail-value" title="${environment.host}">${
967
- environment.host
968
- }</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>
969
1043
  </div>
970
- </div>
971
- </div>
972
-
973
- <div class="env-card">
974
- <div class="env-card-header">
975
- <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>
976
- Node.js Runtime
977
- </div>
978
- <div class="env-card-content">
979
- <div class="env-detail-row">
980
- <span class="env-detail-label">Node Version</span>
981
- <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>
982
1055
  </div>
983
- <div class="env-detail-row">
984
- <span class="env-detail-label">V8 Engine</span>
985
- <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>
986
1076
  </div>
987
- <div class="env-detail-row">
988
- <span class="env-detail-label">Working Dir</span>
989
- <span class="env-detail-value" title="${environment.cwd}">${
990
- environment.cwd.length > 25
991
- ? "..." + environment.cwd.slice(-22)
992
- : environment.cwd
993
- }</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>
994
1096
  </div>
995
- </div>
996
- </div>
997
-
998
- <div class="env-card">
999
- <div class="env-card-header">
1000
- <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>
1001
- System Summary
1002
- </div>
1003
- <div class="env-card-content">
1004
- <div class="env-detail-row">
1005
- <span class="env-detail-label">Platform Arch</span>
1006
- <span class="env-detail-value">
1007
- <span class="env-chip ${
1008
- environment.os.includes("darwin") &&
1009
- environment.cpu.model.toLowerCase().includes("apple")
1010
- ? "env-chip-success"
1011
- : "env-chip-warning"
1012
- }">
1013
- ${
1014
- environment.os.includes("darwin") &&
1015
- environment.cpu.model.toLowerCase().includes("apple")
1016
- ? "Apple Silicon"
1017
- : environment.cpu.model.toLowerCase().includes("arm") ||
1018
- environment.cpu.model.toLowerCase().includes("aarch64")
1019
- ? "ARM-based"
1020
- : "x86/Other"
1021
- }
1022
- </span>
1023
- </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>
1024
1121
  </div>
1025
- <div class="env-detail-row">
1026
- <span class="env-detail-label">Memory per Core</span>
1027
- <span class="env-detail-value">${
1028
- environment.cpu.cores > 0
1029
- ? (
1030
- parseFloat(environment.memory) / environment.cpu.cores
1031
- ).toFixed(2) + " GB"
1032
- : "N/A"
1033
- }</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>
1034
1146
  </div>
1035
- <div class="env-detail-row">
1036
- <span class="env-detail-label">Run Context</span>
1037
- <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>
1038
1159
  </div>
1039
1160
  </div>
1040
1161
  </div>
@@ -1057,11 +1178,11 @@ function generateWorkerDistributionChart(results) {
1057
1178
  const workerId =
1058
1179
  typeof test.workerId !== "undefined" ? test.workerId : "N/A";
1059
1180
  if (!acc[workerId]) {
1060
- acc[workerId] = { passed: 0, failed: 0, skipped: 0, tests: [] };
1181
+ acc[workerId] = { passed: 0, failed: 0, skipped: 0, flaky: 0, tests: [] };
1061
1182
  }
1062
1183
 
1063
1184
  const status = String(test.status).toLowerCase();
1064
- if (status === "passed" || status === "failed" || status === "skipped") {
1185
+ if (status === "passed" || status === "failed" || status === "skipped" || status === "flaky") {
1065
1186
  acc[workerId][status]++;
1066
1187
  }
1067
1188
 
@@ -1089,7 +1210,7 @@ function generateWorkerDistributionChart(results) {
1089
1210
  .substring(2, 7)}`;
1090
1211
  const renderFunctionName = `renderWorkerDistChart_${chartId.replace(
1091
1212
  /-/g,
1092
- "_"
1213
+ "_",
1093
1214
  )}`;
1094
1215
  const modalJsNamespace = `modal_funcs_${chartId.replace(/-/g, "_")}`;
1095
1216
 
@@ -1106,12 +1227,14 @@ function generateWorkerDistributionChart(results) {
1106
1227
  const passedData = workerIds.map((id) => workerData[id].passed);
1107
1228
  const failedData = workerIds.map((id) => workerData[id].failed);
1108
1229
  const skippedData = workerIds.map((id) => workerData[id].skipped);
1230
+ const flakyData = workerIds.map((id) => workerData[id].flaky);
1109
1231
 
1110
1232
  const categoriesString = JSON.stringify(categories);
1111
1233
  const fullDataString = JSON.stringify(fullWorkerData);
1112
1234
  const seriesString = JSON.stringify([
1113
1235
  { name: "Passed", data: passedData, color: "var(--success-color)" },
1114
1236
  { name: "Failed", data: failedData, color: "var(--danger-color)" },
1237
+ { name: "Flaky", data: flakyData, color: "#00ccd3" },
1115
1238
  { name: "Skipped", data: skippedData, color: "var(--warning-color)" },
1116
1239
  ]);
1117
1240
 
@@ -1131,12 +1254,22 @@ function generateWorkerDistributionChart(results) {
1131
1254
  position: relative; box-shadow: 0 5px 15px rgba(0,0,0,0.5);
1132
1255
  }
1133
1256
  .worker-modal-close {
1134
- position: absolute; top: 10px; right: 20px;
1135
- font-size: 28px; font-weight: bold; cursor: pointer;
1257
+ position: absolute;
1258
+ top: 15px;
1259
+ right: 25px;
1260
+ font-size: 32px;
1261
+ font-weight: bold;
1262
+ cursor: pointer;
1136
1263
  line-height: 1;
1264
+ z-index: 10;
1265
+ color: #fff;
1266
+ transition: color 0.2s ease;
1267
+ user-select: none;
1268
+ -webkit-user-select: none;
1137
1269
  }
1138
1270
  .worker-modal-close:hover, .worker-modal-close:focus {
1139
- color: var(--text-color, #000);
1271
+ color: #ef4444;
1272
+ transform: scale(1.1);
1140
1273
  }
1141
1274
  #worker-modal-body-${chartId} ul {
1142
1275
  list-style-type: none; padding-left: 0; margin-top: 15px; max-height: 45vh; overflow-y: auto;
@@ -1163,7 +1296,7 @@ function generateWorkerDistributionChart(results) {
1163
1296
 
1164
1297
  <div id="worker-modal-${chartId}" class="worker-modal-overlay">
1165
1298
  <div class="worker-modal-content">
1166
- <span class="worker-modal-close">×</span>
1299
+ <span class="worker-modal-close" onclick="window.${modalJsNamespace}.close?.()">×</span>
1167
1300
  <h3 id="worker-modal-title-${chartId}" style="text-align: center; margin-top: 0; margin-bottom: 25px; font-size: 1.25em; font-weight: 600; color: #fff"></h3>
1168
1301
  <div id="worker-modal-body-${chartId}"></div>
1169
1302
  </div>
@@ -1194,6 +1327,7 @@ function generateWorkerDistributionChart(results) {
1194
1327
  if (test.status === 'passed') color = 'var(--success-color)';
1195
1328
  else if (test.status === 'failed') color = 'var(--danger-color)';
1196
1329
  else if (test.status === 'skipped') color = 'var(--warning-color)';
1330
+ else if (test.status === 'flaky') color = '#00ccd3';
1197
1331
 
1198
1332
  const escapedName = test.name.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
1199
1333
  testListHtml += \`<li style="color: \${color};"><span style="color: \${color}">[\${test.status.toUpperCase()}]</span> \${escapedName}</li>\`;
@@ -1210,10 +1344,14 @@ function generateWorkerDistributionChart(results) {
1210
1344
  const closeModal = function() {
1211
1345
  modal.style.display = 'none';
1212
1346
  };
1347
+
1348
+ window.${modalJsNamespace}.close = closeModal;
1213
1349
 
1214
- closeModalBtn.onclick = closeModal;
1350
+ if (closeModalBtn) {
1351
+ closeModalBtn.onclick = closeModal;
1352
+ }
1353
+
1215
1354
  modal.onclick = function(event) {
1216
- // Close if clicked on the dark overlay background
1217
1355
  if (event.target == modal) {
1218
1356
  closeModal();
1219
1357
  }
@@ -1329,7 +1467,7 @@ function generateTestHistoryContent(trendData) {
1329
1467
  ? `test run ${overallRun.runId}`
1330
1468
  : `test run ${index + 1}`;
1331
1469
  const testRunForThisOverallRun = trendData.testRuns[runKey]?.find(
1332
- (t) => t && t.testName === fullTestName
1470
+ (t) => t && t.testName === fullTestName,
1333
1471
  );
1334
1472
  if (testRunForThisOverallRun) {
1335
1473
  history.push({
@@ -1355,6 +1493,7 @@ function generateTestHistoryContent(trendData) {
1355
1493
  <option value="">All Statuses</option>
1356
1494
  <option value="passed">Passed</option>
1357
1495
  <option value="failed">Failed</option>
1496
+ <option value="flaky">Flaky</option>
1358
1497
  <option value="skipped">Skipped</option>
1359
1498
  </select>
1360
1499
  <button id="clear-history-filters" class="clear-filters-btn">Clear Filters</button>
@@ -1369,12 +1508,12 @@ function generateTestHistoryContent(trendData) {
1369
1508
  : { status: "unknown" };
1370
1509
  return `
1371
1510
  <div class="test-history-card" data-test-name="${sanitizeHTML(
1372
- test.testTitle.toLowerCase()
1511
+ test.testTitle.toLowerCase(),
1373
1512
  )}" data-latest-status="${latestRun.status}">
1374
1513
  <div class="test-history-header">
1375
1514
  <p title="${sanitizeHTML(test.testTitle)}">${capitalize(
1376
- sanitizeHTML(test.testTitle)
1377
- )}</p>
1515
+ sanitizeHTML(test.testTitle),
1516
+ )}</p>
1378
1517
  <span class="status-badge ${getStatusClass(latestRun.status)}">
1379
1518
  ${String(latestRun.status).toUpperCase()}
1380
1519
  </span>
@@ -1396,11 +1535,11 @@ function generateTestHistoryContent(trendData) {
1396
1535
  <tr>
1397
1536
  <td>${run.runId}</td>
1398
1537
  <td><span class="status-badge-small ${getStatusClass(
1399
- run.status
1538
+ run.status,
1400
1539
  )}">${String(run.status).toUpperCase()}</span></td>
1401
1540
  <td>${formatDuration(run.duration)}</td>
1402
1541
  <td>${formatDate(run.timestamp)}</td>
1403
- </tr>`
1542
+ </tr>`,
1404
1543
  )
1405
1544
  .join("")}
1406
1545
  </tbody>
@@ -1422,6 +1561,8 @@ function getStatusClass(status) {
1422
1561
  return "status-failed";
1423
1562
  case "skipped":
1424
1563
  return "status-skipped";
1564
+ case "flaky":
1565
+ return "status-flaky";
1425
1566
  default:
1426
1567
  return "status-unknown";
1427
1568
  }
@@ -1434,6 +1575,8 @@ function getStatusIcon(status) {
1434
1575
  return "❌";
1435
1576
  case "skipped":
1436
1577
  return "⏭️";
1578
+ case "flaky":
1579
+ return "⚠️";
1437
1580
  default:
1438
1581
  return "❓";
1439
1582
  }
@@ -1469,6 +1612,7 @@ function getSuitesData(results) {
1469
1612
  browser: browser,
1470
1613
  passed: 0,
1471
1614
  failed: 0,
1615
+ flaky: 0,
1472
1616
  skipped: 0,
1473
1617
  count: 0,
1474
1618
  statusOverall: "passed",
@@ -1476,12 +1620,15 @@ function getSuitesData(results) {
1476
1620
  }
1477
1621
  const suite = suitesMap.get(key);
1478
1622
  suite.count++;
1479
- const currentStatus = String(test.status).toLowerCase();
1623
+ let currentStatus = String(test.status).toLowerCase();
1624
+ if (test.outcome === 'flaky') currentStatus = 'flaky';
1480
1625
  if (currentStatus && suite[currentStatus] !== undefined) {
1481
1626
  suite[currentStatus]++;
1482
1627
  }
1483
1628
  if (currentStatus === "failed") suite.statusOverall = "failed";
1484
- 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")
1485
1632
  suite.statusOverall = "skipped";
1486
1633
  });
1487
1634
  return Array.from(suitesMap.values());
@@ -1492,58 +1639,56 @@ function generateSuitesWidget(suitesData) {
1492
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>`;
1493
1640
  }
1494
1641
 
1495
- // Added inline styles for height consistency with Pie Chart (approx 450px) and scrolling
1642
+ // Uses CSS classes for responsiveness instead of inline styles
1496
1643
  return `
1497
- <div class="suites-widget" style="height: 450px; display: flex; flex-direction: column;">
1498
- <div class="suites-header" style="flex-shrink: 0;">
1644
+ <div class="suites-widget fixed-height-widget">
1645
+ <div class="suites-header">
1499
1646
  <h2>Test Suites</h2>
1500
1647
  <span class="summary-badge">${
1501
1648
  suitesData.length
1502
1649
  } suites • ${suitesData.reduce(
1503
- (sum, suite) => sum + suite.count,
1504
- 0
1505
- )} tests</span>
1650
+ (sum, suite) => sum + suite.count,
1651
+ 0,
1652
+ )} tests</span>
1506
1653
  </div>
1507
1654
 
1508
- <div class="suites-grid-container" style="flex-grow: 1; overflow-y: auto; padding-right: 5px;">
1655
+ <div class="suites-grid-container">
1509
1656
  <div class="suites-grid">
1510
1657
  ${suitesData
1511
1658
  .map(
1512
1659
  (suite) => `
1513
1660
  <div class="suite-card status-${suite.statusOverall}">
1514
1661
  <div class="suite-card-header">
1515
- <h3 class="suite-name" title="${sanitizeHTML(
1516
- suite.name
1517
- )} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(
1518
- suite.name
1519
- )}</h3>
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)}
1520
1668
  </div>
1521
- <div>🖥️ <span class="browser-tag">${sanitizeHTML(
1522
- suite.browser
1523
- )}</span></div>
1669
+
1524
1670
  <div class="suite-card-body">
1525
- <span class="test-count">${suite.count} test${
1526
- suite.count !== 1 ? "s" : ""
1527
- }</span>
1671
+ <span class="test-count-label">${suite.count} Test${suite.count !== 1 ? "s" : ""}</span>
1528
1672
  <div class="suite-stats">
1529
- ${
1530
- suite.passed > 0
1531
- ? `<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>`
1532
- : ""
1533
- }
1534
- ${
1535
- suite.failed > 0
1536
- ? `<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>`
1537
- : ""
1538
- }
1539
- ${
1540
- suite.skipped > 0
1541
- ? `<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>`
1542
- : ""
1543
- }
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>
1544
1689
  </div>
1545
1690
  </div>
1546
- </div>`
1691
+ </div>`,
1547
1692
  )
1548
1693
  .join("")}
1549
1694
  </div>
@@ -1565,12 +1710,11 @@ function getAttachmentIcon(contentType) {
1565
1710
  }
1566
1711
  function generateAIFailureAnalyzerTab(results) {
1567
1712
  const failedTests = (results || []).filter(
1568
- (test) => test.status === "failed"
1713
+ (test) => test.status === "failed",
1569
1714
  );
1570
1715
 
1571
1716
  if (failedTests.length === 0) {
1572
1717
  return `
1573
- <h2 class="tab-main-title">AI Failure Analysis</h2>
1574
1718
  <div class="no-data">Congratulations! No failed tests in this run.</div>
1575
1719
  `;
1576
1720
  }
@@ -1579,7 +1723,6 @@ function generateAIFailureAnalyzerTab(results) {
1579
1723
  const btoa = (str) => Buffer.from(str).toString("base64");
1580
1724
 
1581
1725
  return `
1582
- <h2 class="tab-main-title">AI Failure Analysis</h2>
1583
1726
  <div class="ai-analyzer-stats">
1584
1727
  <div class="stat-item">
1585
1728
  <span class="stat-number">${failedTests.length}</span>
@@ -1594,7 +1737,7 @@ function generateAIFailureAnalyzerTab(results) {
1594
1737
  <div class="stat-item">
1595
1738
  <span class="stat-number">${Math.round(
1596
1739
  failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) /
1597
- 1000
1740
+ 1000,
1598
1741
  )}s</span>
1599
1742
  <span class="stat-label">Total Duration</span>
1600
1743
  </div>
@@ -1617,14 +1760,14 @@ function generateAIFailureAnalyzerTab(results) {
1617
1760
  <div class="failure-header">
1618
1761
  <div class="failure-main-info">
1619
1762
  <h3 class="failure-title" title="${sanitizeHTML(
1620
- test.name
1763
+ test.name,
1621
1764
  )}">${sanitizeHTML(testTitle)}</h3>
1622
1765
  <div class="failure-meta">
1623
1766
  <span class="browser-indicator">${sanitizeHTML(
1624
- test.browser || "unknown"
1767
+ test.browser || "unknown",
1625
1768
  )}</span>
1626
1769
  <span class="duration-indicator">${formatDuration(
1627
- test.duration
1770
+ test.duration,
1628
1771
  )}</span>
1629
1772
  </div>
1630
1773
  </div>
@@ -1639,7 +1782,7 @@ function generateAIFailureAnalyzerTab(results) {
1639
1782
  </div>
1640
1783
  <div class="failure-error-preview">
1641
1784
  <div class="error-snippet">${formatPlaywrightError(
1642
- truncatedError
1785
+ truncatedError,
1643
1786
  )}</div>
1644
1787
  <button class="expand-error-btn" onclick="toggleErrorDetails(this)">
1645
1788
  <span class="expand-text">Show Full Error</span>
@@ -1649,10 +1792,16 @@ function generateAIFailureAnalyzerTab(results) {
1649
1792
  <div class="full-error-details" style="display: none;">
1650
1793
  <div class="full-error-content">
1651
1794
  ${formatPlaywrightError(
1652
- test.errorMessage || "No detailed error message available"
1795
+ test.errorMessage ||
1796
+ "No detailed error message available",
1653
1797
  )}
1654
1798
  </div>
1655
1799
  </div>
1800
+ <div class="ai-suggestion-container" style="display: none;">
1801
+ <div class="ai-suggestion-content">
1802
+ <!-- AI suggestion will be injected here -->
1803
+ </div>
1804
+ </div>
1656
1805
  </div>
1657
1806
  `;
1658
1807
  })
@@ -1895,7 +2044,25 @@ function generateDescribeDurationChart(results) {
1895
2044
  series: [{
1896
2045
  name: 'Duration',
1897
2046
  data: ${dataStr},
1898
- color: 'var(--accent-color-alt)',
2047
+ colorByPoint: true,
2048
+ colors: [
2049
+ '#9333ea',
2050
+ '#6366f1',
2051
+ '#0ea5e9',
2052
+ '#10b981',
2053
+ '#84cc16',
2054
+ '#eab308',
2055
+ '#f97316',
2056
+ '#ef4444',
2057
+ '#ec4899',
2058
+ '#8b5cf6',
2059
+ '#06b6d4',
2060
+ '#14b8a6',
2061
+ '#a3e635',
2062
+ '#fbbf24',
2063
+ '#fb923c',
2064
+ '#f87171'
2065
+ ],
1899
2066
  }],
1900
2067
  credits: { enabled: false }
1901
2068
  });
@@ -1919,6 +2086,7 @@ function generateSeverityDistributionChart(results) {
1919
2086
  const data = {
1920
2087
  passed: [0, 0, 0, 0, 0],
1921
2088
  failed: [0, 0, 0, 0, 0],
2089
+ flaky: [0, 0, 0, 0, 0],
1922
2090
  skipped: [0, 0, 0, 0, 0],
1923
2091
  };
1924
2092
 
@@ -1937,6 +2105,8 @@ function generateSeverityDistributionChart(results) {
1937
2105
  status === "interrupted"
1938
2106
  ) {
1939
2107
  data.failed[index]++;
2108
+ } else if (status === "flaky") {
2109
+ data.flaky[index]++;
1940
2110
  } else {
1941
2111
  data.skipped[index]++;
1942
2112
  }
@@ -1950,6 +2120,7 @@ function generateSeverityDistributionChart(results) {
1950
2120
  const seriesData = [
1951
2121
  { name: "Passed", data: data.passed, color: "var(--success-color)" },
1952
2122
  { name: "Failed", data: data.failed, color: "var(--danger-color)" },
2123
+ { name: "Flaky", data: data.flaky, color: "#00ccd3" },
1953
2124
  { name: "Skipped", data: data.skipped, color: "var(--warning-color)" },
1954
2125
  ];
1955
2126
 
@@ -2063,16 +2234,98 @@ function generateHTML(reportData, trendData = null) {
2063
2234
  return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`), "");
2064
2235
  };
2065
2236
 
2237
+
2238
+ const avgTestDuration =
2239
+ runSummary.totalTests > 0
2240
+ ? formatDuration(runSummary.duration / runSummary.totalTests)
2241
+ : "0.0s";
2242
+
2243
+ const flakyCount = (results || []).filter(r => r.outcome === 'flaky').length;
2244
+
2245
+ // Calculate retry statistics
2246
+ let retriedTestsCount = 0;
2247
+ const totalRetried = (results || []).reduce((acc, test) => {
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;
2258
+ }
2259
+ return acc;
2260
+ }, 0);
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
+
2066
2301
  const totalTestsOr1 = runSummary.totalTests || 1;
2067
2302
  const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
2068
2303
  const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
2069
2304
  const skipPercentage = Math.round(
2070
- ((runSummary.skipped || 0) / totalTestsOr1) * 100
2305
+ ((runSummary.skipped || 0) / totalTestsOr1) * 100,
2071
2306
  );
2072
- const avgTestDuration =
2073
- runSummary.totalTests > 0
2074
- ? formatDuration(runSummary.duration / runSummary.totalTests)
2075
- : "0.0s";
2307
+ const flakyPercentage = Math.round(((runSummary.flaky || 0) / totalTestsOr1) * 100);
2308
+
2309
+
2310
+ // Calculate browser distribution
2311
+ const browserStats = (results || []).reduce((acc, test) => {
2312
+ let browserName = "unknown";
2313
+ if (test.browser) {
2314
+ // Use full browser name
2315
+ browserName = test.browser;
2316
+ }
2317
+ acc[browserName] = (acc[browserName] || 0) + 1;
2318
+ return acc;
2319
+ }, {});
2320
+
2321
+ const totalTests = runSummary.totalTests || 1;
2322
+ const browserBreakdown = Object.entries(browserStats)
2323
+ .map(([browser, count]) => ({
2324
+ browser,
2325
+ count,
2326
+ percentage: Math.round((count / totalTests) * 100),
2327
+ }))
2328
+ .sort((a, b) => b.count - a.count);
2076
2329
  function generateTestCasesHTML() {
2077
2330
  if (!results || results.length === 0)
2078
2331
  return '<div class="no-tests">No test results found in this run.</div>';
@@ -2082,28 +2335,13 @@ function generateHTML(reportData, trendData = null) {
2082
2335
  const testFileParts = test.name.split(" > ");
2083
2336
  const testTitle =
2084
2337
  testFileParts[testFileParts.length - 1] || "Unnamed Test";
2085
- // --- NEW: Severity Logic ---
2338
+ // --- Simplified Severity Badge ---
2086
2339
  const severity = test.severity || "Medium";
2087
- const getSeverityColor = (level) => {
2088
- switch (level) {
2089
- case "Minor":
2090
- return "#006064";
2091
- case "Low":
2092
- return "#FFA07A";
2093
- case "Medium":
2094
- return "#577A11";
2095
- case "High":
2096
- return "#B71C1C";
2097
- case "Critical":
2098
- return "#64158A";
2099
- default:
2100
- return "#577A11";
2101
- }
2102
- };
2103
- const severityColor = getSeverityColor(severity);
2104
- // We reuse 'status-badge' class for size/font consistency, but override background color
2105
- const severityBadge = `<span class="status-badge" style="background-color: ${severityColor}; margin-right: 8px;">${severity}</span>`;
2106
- // ---------------------------
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>` : '';
2107
2345
  const generateStepsHTML = (steps, depth = 0) => {
2108
2346
  if (!steps || steps.length === 0)
2109
2347
  return "<div class='no-steps'>No steps recorded for this test.</div>";
@@ -2111,36 +2349,46 @@ function generateHTML(reportData, trendData = null) {
2111
2349
  .map((step) => {
2112
2350
  const hasNestedSteps = step.steps && step.steps.length > 0;
2113
2351
  const isHook = step.hookType;
2352
+ const isFailedStep = step.isFailedStep === true;
2114
2353
  const stepClass = isHook
2115
2354
  ? `step-hook step-hook-${step.hookType}`
2116
2355
  : "";
2356
+ const failedStepClass = isFailedStep ? " failed-step-highlight" : "";
2117
2357
  const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
2358
+ const failedStepIndicator = isFailedStep ? ` <span class="failed-step-marker">⚠️ Failed at this step</span>` : "";
2118
2359
  return `
2119
- <div class="step-item" style="--depth: ${depth};">
2360
+ <div class="step-item${failedStepClass}" style="--depth: ${depth};">
2120
2361
  <div class="step-header ${stepClass}" role="button" aria-expanded="false">
2121
2362
  <span class="step-icon">${getStatusIcon(step.status)}</span>
2122
2363
  <span class="step-title">${sanitizeHTML(
2123
- step.title
2124
- )}${hookIndicator}</span>
2364
+ step.title,
2365
+ )}${hookIndicator}${failedStepIndicator}</span>
2125
2366
  <span class="step-duration">${formatDuration(
2126
- step.duration
2367
+ step.duration,
2127
2368
  )}</span>
2128
2369
  </div>
2129
2370
  <div class="step-details" style="display: none;">
2130
2371
  ${
2131
2372
  step.codeLocation
2132
2373
  ? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
2133
- step.codeLocation
2374
+ step.codeLocation,
2134
2375
  )}</div>`
2135
2376
  : ""
2136
2377
  }
2378
+ ${
2379
+ step.codeSnippet
2380
+ ? `<div class="code-snippet-section"><pre class="code-snippet">${sanitizeHTML(
2381
+ step.codeSnippet,
2382
+ )}</pre></div>`
2383
+ : ""
2384
+ }
2137
2385
  ${
2138
2386
  step.errorMessage
2139
2387
  ? `<div class="test-error-summary">
2140
2388
  ${
2141
2389
  step.stackTrace
2142
2390
  ? `<div class="stack-trace">${formatPlaywrightError(
2143
- step.stackTrace
2391
+ step.stackTrace,
2144
2392
  )}</div>`
2145
2393
  : ""
2146
2394
  }
@@ -2149,7 +2397,7 @@ function generateHTML(reportData, trendData = null) {
2149
2397
  onclick="copyErrorToClipboard(this)"
2150
2398
  style="
2151
2399
  margin-top: 8px;
2152
- padding: 4px 8px;
2400
+ padding: 6px 12px;
2153
2401
  background: #f0f0f0;
2154
2402
  border: 2px solid #ccc;
2155
2403
  border-radius: 4px;
@@ -2157,6 +2405,8 @@ function generateHTML(reportData, trendData = null) {
2157
2405
  font-size: 12px;
2158
2406
  border-color: #8B0000;
2159
2407
  color: #8B0000;
2408
+ align-self: flex-end;
2409
+ width: auto;
2160
2410
  "
2161
2411
  onmouseover="this.style.background='#e0e0e0'"
2162
2412
  onmouseout="this.style.background='#f0f0f0'"
@@ -2170,7 +2420,7 @@ function generateHTML(reportData, trendData = null) {
2170
2420
  hasNestedSteps
2171
2421
  ? `<div class="nested-steps">${generateStepsHTML(
2172
2422
  step.steps,
2173
- depth + 1
2423
+ depth + 1,
2174
2424
  )}</div>`
2175
2425
  : ""
2176
2426
  }
@@ -2180,41 +2430,36 @@ function generateHTML(reportData, trendData = null) {
2180
2430
  .join("");
2181
2431
  };
2182
2432
 
2183
- return `
2184
- <div class="test-case" data-status="${
2185
- test.status
2186
- }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
2187
- .join(",")
2188
- .toLowerCase()}">
2189
- <div class="test-case-header" role="button" aria-expanded="false">
2190
- <div class="test-case-summary">
2191
- <span class="status-badge ${getStatusClass(test.status)}">${String(
2192
- test.status
2193
- ).toUpperCase()}</span>
2194
- <span class="test-case-title" title="${sanitizeHTML(
2195
- test.name
2196
- )}">${sanitizeHTML(testTitle)}</span>
2197
- <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
2198
- </div>
2199
- <div class="test-case-meta">
2200
- ${severityBadge}
2201
- ${
2202
- test.tags && test.tags.length > 0
2203
- ? test.tags
2204
- .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
2205
- .join(" ")
2206
- : ""
2207
- }
2208
- <span class="test-duration">${formatDuration(test.duration)}</span>
2209
- </div>
2210
- </div>
2211
- <div class="test-case-content" style="display: none;">
2212
- <p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
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>
2213
2458
  ${
2214
- test.annotations && test.annotations.length > 0
2459
+ testData.annotations && testData.annotations.length > 0
2215
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;">
2216
2461
  <h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
2217
- ${test.annotations
2462
+ ${testData.annotations
2218
2463
  .map((annotation, idx) => {
2219
2464
  const isIssueOrBug =
2220
2465
  annotation.type === "issue" ||
@@ -2222,22 +2467,22 @@ function generateHTML(reportData, trendData = null) {
2222
2467
  const descriptionText = annotation.description || "";
2223
2468
  const typeLabel = sanitizeHTML(annotation.type);
2224
2469
  const descriptionHtml =
2225
- isIssueOrBug && descriptionText.match(/^[A-Z]+-\d+$/)
2470
+ isIssueOrBug && descriptionText.match(/^[A-Z]+-\\d+$/)
2226
2471
  ? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
2227
- descriptionText
2472
+ descriptionText,
2228
2473
  )}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
2229
- descriptionText
2474
+ descriptionText,
2230
2475
  )}</a>`
2231
2476
  : sanitizeHTML(descriptionText);
2232
2477
  const locationText = annotation.location
2233
2478
  ? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
2234
- annotation.location.file
2479
+ annotation.location.file,
2235
2480
  )}:${annotation.location.line}:${
2236
2481
  annotation.location.column
2237
2482
  }</div>`
2238
2483
  : "";
2239
2484
  return `<div style="margin-bottom: ${
2240
- idx < test.annotations.length - 1 ? "10px" : "0"
2485
+ idx < testData.annotations.length - 1 ? "10px" : "0"
2241
2486
  };">
2242
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>
2243
2488
  ${
@@ -2253,21 +2498,21 @@ function generateHTML(reportData, trendData = null) {
2253
2498
  : ""
2254
2499
  }
2255
2500
  <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
2256
- test.workerId
2501
+ testData.workerId,
2257
2502
  )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
2258
- test.totalWorkers
2259
- )}]</p>
2503
+ testData.totalWorkers,
2504
+ )}]</p>
2260
2505
  ${
2261
- test.errorMessage
2262
- ? `<div class="test-error-summary">${formatPlaywrightError(
2263
- test.errorMessage
2264
- )}
2506
+ testData.errorMessage
2507
+ ? `<div class="test-error-summary"><div class="stack-trace">${formatPlaywrightError(
2508
+ testData.errorMessage,
2509
+ )}</div>
2265
2510
  <button
2266
2511
  class="copy-error-btn"
2267
2512
  onclick="copyErrorToClipboard(this)"
2268
2513
  style="
2269
2514
  margin-top: 8px;
2270
- padding: 4px 8px;
2515
+ padding: 6px 12px;
2271
2516
  background: #f0f0f0;
2272
2517
  border: 2px solid #ccc;
2273
2518
  border-radius: 4px;
@@ -2275,6 +2520,8 @@ function generateHTML(reportData, trendData = null) {
2275
2520
  font-size: 12px;
2276
2521
  border-color: #8B0000;
2277
2522
  color: #8B0000;
2523
+ align-self: flex-end;
2524
+ width: auto;
2278
2525
  "
2279
2526
  onmouseover="this.style.background='#e0e0e0'"
2280
2527
  onmouseout="this.style.background='#f0f0f0'"
@@ -2285,61 +2532,61 @@ function generateHTML(reportData, trendData = null) {
2285
2532
  : ""
2286
2533
  }
2287
2534
  ${
2288
- test.snippet
2535
+ testData.snippet
2289
2536
  ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
2290
- test.snippet
2537
+ testData.snippet,
2291
2538
  )}</code></pre></div>`
2292
2539
  : ""
2293
2540
  }
2294
2541
  <h4>Steps</h4>
2295
- <div class="steps-list">${generateStepsHTML(test.steps)}</div>
2542
+ <div class="steps-list">${generateStepsHTML(testData.steps)}</div>
2296
2543
  ${(() => {
2297
- if (!test.stdout || test.stdout.length === 0) return "";
2298
- // Create a unique ID for the <pre> element to target it for copying
2299
- const logId = `stdout-log-${test.id || index}`;
2544
+ if (!testData.stdout || testData.stdout.length === 0) return "";
2300
2545
  return `<div class="console-output-section">
2301
2546
  <h4>Console Output (stdout)
2302
- <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy Console</button>
2547
+ <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy</ button>
2303
2548
  </h4>
2304
2549
  <div class="log-wrapper">
2305
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(
2306
- test.stdout.map((line) => sanitizeHTML(line)).join("\n")
2307
- )}</pre>
2551
+ testData.stdout
2552
+ .map((line) => sanitizeHTML(line))
2553
+ .join("\\n"),
2554
+ )}</pre>
2308
2555
  </div>
2309
2556
  </div>`;
2310
2557
  })()}
2311
2558
  ${
2312
- test.stderr && test.stderr.length > 0
2559
+ testData.stderr && testData.stderr.length > 0
2313
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(
2314
- test.stderr.map((line) => sanitizeHTML(line)).join("\n")
2561
+ testData.stderr.map((line) => sanitizeHTML(line)).join("\\n"),
2315
2562
  )}</pre></div>`
2316
2563
  : ""
2317
2564
  }
2318
2565
  ${
2319
- test.screenshots && test.screenshots.length > 0
2566
+ testData.screenshots && testData.screenshots.length > 0
2320
2567
  ? `
2321
2568
  <div class="attachments-section">
2322
2569
  <h4>Screenshots</h4>
2323
2570
  <div class="attachments-grid">
2324
- ${test.screenshots
2571
+ ${testData.screenshots
2325
2572
  .map(
2326
- (screenshot, index) => `
2573
+ (screenshot, screenshotIndex) => `
2327
2574
  <div class="attachment-item">
2328
2575
  <img src="${fixPath(screenshot)}" alt="Screenshot ${
2329
- index + 1
2576
+ screenshotIndex + 1
2330
2577
  }">
2331
2578
  <div class="attachment-info">
2332
2579
  <div class="trace-actions">
2333
2580
  <a href="${fixPath(
2334
- screenshot
2581
+ screenshot,
2335
2582
  )}" target="_blank" class="view-full">View Full Image</a>
2336
2583
  <a href="${fixPath(
2337
- screenshot
2338
- )}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
2584
+ screenshot,
2585
+ )}" target="_blank" download="screenshot-${Date.now()}-${screenshotIndex}.png">Download</a>
2339
2586
  </div>
2340
2587
  </div>
2341
2588
  </div>
2342
- `
2589
+ `,
2343
2590
  )
2344
2591
  .join("")}
2345
2592
  </div>
@@ -2348,9 +2595,9 @@ function generateHTML(reportData, trendData = null) {
2348
2595
  : ""
2349
2596
  }
2350
2597
  ${
2351
- test.videoPath && test.videoPath.length > 0
2352
- ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
2353
- .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) => {
2354
2601
  const fixedVideoUrl = fixPath(videoUrl);
2355
2602
  const fileExtension = String(fixedVideoUrl)
2356
2603
  .split(".")
@@ -2366,18 +2613,18 @@ function generateHTML(reportData, trendData = null) {
2366
2613
  }[fileExtension] || "video/mp4";
2367
2614
  return `<div class="attachment-item video-item">
2368
2615
  <video controls width="100%" height="auto" title="Video ${
2369
- index + 1
2616
+ videoIndex + 1
2370
2617
  }">
2371
2618
  <source src="${sanitizeHTML(
2372
- fixedVideoUrl
2619
+ fixedVideoUrl,
2373
2620
  )}" type="${mimeType}">
2374
2621
  Your browser does not support the video tag.
2375
2622
  </video>
2376
2623
  <div class="attachment-info">
2377
2624
  <div class="trace-actions">
2378
2625
  <a href="${sanitizeHTML(
2379
- fixedVideoUrl
2380
- )}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
2626
+ fixedVideoUrl,
2627
+ )}" target="_blank" download="video-${Date.now()}-${videoIndex}.${fileExtension}">Download</a>
2381
2628
  </div>
2382
2629
  </div>
2383
2630
  </div>`;
@@ -2386,7 +2633,7 @@ function generateHTML(reportData, trendData = null) {
2386
2633
  : ""
2387
2634
  }
2388
2635
  ${
2389
- test.tracePath
2636
+ testData.tracePath
2390
2637
  ? `
2391
2638
  <div class="attachments-section">
2392
2639
  <h4>Trace Files</h4>
@@ -2395,16 +2642,16 @@ function generateHTML(reportData, trendData = null) {
2395
2642
  <div class="trace-preview">
2396
2643
  <span class="trace-icon">📄</span>
2397
2644
  <span class="trace-name">${sanitizeHTML(
2398
- path.basename(test.tracePath)
2645
+ path.basename(testData.tracePath),
2399
2646
  )}</span>
2400
2647
  </div>
2401
2648
  <div class="attachment-info">
2402
2649
  <div class="trace-actions">
2403
2650
  <a href="${sanitizeHTML(
2404
- fixPath(test.tracePath)
2651
+ fixPath(testData.tracePath),
2405
2652
  )}" target="_blank" download="${sanitizeHTML(
2406
- path.basename(test.tracePath)
2407
- )}" class="download-trace">Download Trace</a>
2653
+ path.basename(testData.tracePath),
2654
+ )}" class="download-trace">Download Trace</a>
2408
2655
  </div>
2409
2656
  </div>
2410
2657
  </div>
@@ -2414,40 +2661,40 @@ function generateHTML(reportData, trendData = null) {
2414
2661
  : ""
2415
2662
  }
2416
2663
  ${
2417
- test.attachments && test.attachments.length > 0
2664
+ testData.attachments && testData.attachments.length > 0
2418
2665
  ? `
2419
2666
  <div class="attachments-section">
2420
2667
  <h4>Other Attachments</h4>
2421
2668
  <div class="attachments-grid">
2422
- ${test.attachments
2669
+ ${testData.attachments
2423
2670
  .map(
2424
2671
  (attachment) => `
2425
2672
  <div class="attachment-item generic-attachment">
2426
2673
  <div class="attachment-icon">${getAttachmentIcon(
2427
- attachment.contentType
2674
+ attachment.contentType,
2428
2675
  )}</div>
2429
2676
  <div class="attachment-caption">
2430
2677
  <span class="attachment-name" title="${sanitizeHTML(
2431
- attachment.name
2678
+ attachment.name,
2432
2679
  )}">${sanitizeHTML(attachment.name)}</span>
2433
2680
  <span class="attachment-type">${sanitizeHTML(
2434
- attachment.contentType
2681
+ attachment.contentType,
2435
2682
  )}</span>
2436
2683
  </div>
2437
2684
  <div class="attachment-info">
2438
2685
  <div class="trace-actions">
2439
2686
  <a href="${sanitizeHTML(
2440
- fixPath(attachment.path)
2687
+ fixPath(attachment.path),
2441
2688
  )}" target="_blank" class="view-full">View</a>
2442
2689
  <a href="${sanitizeHTML(
2443
- fixPath(attachment.path)
2690
+ fixPath(attachment.path),
2444
2691
  )}" target="_blank" download="${sanitizeHTML(
2445
- attachment.name
2446
- )}" class="download-trace">Download</a>
2692
+ attachment.name,
2693
+ )}" class="download-trace">Download</a>
2447
2694
  </div>
2448
2695
  </div>
2449
2696
  </div>
2450
- `
2697
+ `,
2451
2698
  )
2452
2699
  .join("")}
2453
2700
  </div>
@@ -2455,13 +2702,76 @@ function generateHTML(reportData, trendData = null) {
2455
2702
  `
2456
2703
  : ""
2457
2704
  }
2458
- ${
2459
- test.codeSnippet
2460
- ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
2461
- sanitizeHTML(test.codeSnippet)
2462
- )}</code></pre></div>`
2463
- : ""
2464
- }
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')}
2465
2775
  </div>
2466
2776
  </div>`;
2467
2777
  })
@@ -2475,17 +2785,54 @@ function generateHTML(reportData, trendData = null) {
2475
2785
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2476
2786
  <link rel="icon" type="image/png" href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png">
2477
2787
  <link rel="apple-touch-icon" href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png">
2788
+ <!-- Preconnect to external domains -->
2789
+ <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
2790
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
2791
+ <link rel="preconnect" href="https://code.highcharts.com">
2792
+
2793
+ <!-- Preload critical font -->
2794
+ <link rel="preload" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
2795
+ <noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700&display=swap"></noscript>
2796
+
2478
2797
  <script src="https://code.highcharts.com/highcharts.js" defer></script>
2479
2798
  <title>Pulse Report</title>
2480
2799
  <style>
2481
2800
  :root {
2482
- --primary-color: #3f51b5; --secondary-color: #ff4081; --accent-color: #673ab7; --accent-color-alt: #FF9800;
2483
- --success-color: #4CAF50; --danger-color: #F44336; --warning-color: #FFC107; --info-color: #2196F3;
2484
- --light-gray-color: #f5f5f5; --medium-gray-color: #e0e0e0; --dark-gray-color: #757575;
2485
- --text-color: #333; --text-color-secondary: #555; --border-color: #ddd; --background-color: #f8f9fa;
2486
- --card-background-color: #fff; --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
2487
- --border-radius: 8px; --box-shadow: 0 5px 15px rgba(0,0,0,0.08); --box-shadow-light: 0 3px 8px rgba(0,0,0,0.05); --box-shadow-inset: inset 0 1px 3px rgba(0,0,0,0.07);
2488
- }
2801
+ --primary-color: #6366f1; --primary-dark: #4f46e5; --primary-light: #818cf8;
2802
+ --secondary-color: #8b5cf6; --secondary-dark: #7c3aed; --secondary-light: #a78bfa;
2803
+ --accent-color: #ec4899; --accent-alt: #06b6d4;
2804
+ --success-color: #10b981; --success-dark: #059669; --success-light: #34d399;
2805
+ --danger-color: #ef4444; --danger-dark: #dc2626; --danger-light: #f87171;
2806
+ --warning-color: #f59e0b; --warning-dark: #d97706; --warning-light: #fbbf24;
2807
+ --info-color: #3b82f6;
2808
+ --flaky-color: #00ccd3;
2809
+ --neutral-50: #fafafa; --neutral-100: #f5f5f5; --neutral-200: #e5e5e5; --neutral-300: #d4d4d4;
2810
+ --neutral-400: #a3a3a3; --neutral-500: #737373; --neutral-600: #525252; --neutral-700: #404040;
2811
+ --neutral-800: #262626; --neutral-900: #171717;
2812
+ --text-primary: #0f172a; --text-secondary: #475569; --text-tertiary: #94a3b8;
2813
+ --bg-primary: #ffffff; --bg-secondary: #f8fafc; --bg-tertiary: #f1f5f9;
2814
+ --border-light: #e2e8f0; --border-medium: #cbd5e1; --border-dark: #94a3b8;
2815
+ --font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
2816
+ --radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-xl: 20px; --radius-2xl: 24px;
2817
+ --shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
2818
+ --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
2819
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
2820
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
2821
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
2822
+ --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
2823
+ --glow-primary: 0 0 20px rgba(99, 102, 241, 0.4), 0 0 40px rgba(99, 102, 241, 0.2);
2824
+ --glow-success: 0 0 20px rgba(16, 185, 129, 0.4), 0 0 40px rgba(16, 185, 129, 0.2);
2825
+ --glow-danger: 0 0 20px rgba(239, 68, 68, 0.4), 0 0 40px rgba(239, 68, 68, 0.2);
2826
+ --bg-card: #ffffff; --bg-card-hover: #f8fafc;
2827
+ --gradient-card: linear-gradient(145deg, #ffffff 0%, #f9fafb 100%);
2828
+ --border-medium: #cbd5e1;
2829
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2830
+ ::selection { background: var(--primary-color); color: white; }
2831
+ ::-webkit-scrollbar { width: 0; height: 0; display: none; }
2832
+ ::-webkit-scrollbar-track { display: none; }
2833
+ ::-webkit-scrollbar-thumb { display: none; }
2834
+ ::-webkit-scrollbar-thumb:hover { display: none; }
2835
+ * { scrollbar-width: none; -ms-overflow-style: none; }
2489
2836
  .trend-chart-container, .test-history-trend div[id^="testHistoryChart-"] { min-height: 100px; }
2490
2837
  .lazy-load-chart .no-data, .lazy-load-chart .no-data-chart { display: flex; align-items: center; justify-content: center; height: 100%; font-style: italic; color: var(--dark-gray-color); }
2491
2838
  .highcharts-background { fill: transparent; }
@@ -2493,79 +2840,1187 @@ function generateHTML(reportData, trendData = null) {
2493
2840
  .highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
2494
2841
  .highcharts-axis-title { fill: var(--text-color) !important; }
2495
2842
  .highcharts-tooltip > span { background-color: rgba(10,10,10,0.92) !important; border-color: rgba(10,10,10,0.92) !important; color: #f5f5f5 !important; padding: 10px !important; border-radius: 6px !important; }
2496
- body { font-family: var(--font-family); margin: 0; background-color: var(--background-color); color: var(--text-color); line-height: 1.65; font-size: 16px; }
2497
- .container { padding: 30px; border-radius: var(--border-radius); box-shadow: var(--box-shadow); background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec); }
2498
- .header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; padding-bottom: 25px; border-bottom: 1px solid var(--border-color); margin-bottom: 25px; }
2499
- .header-title { display: flex; align-items: center; gap: 15px; }
2500
- .header h1 { margin: 0; font-size: 1.85em; font-weight: 600; color: var(--primary-color); }
2501
- #report-logo { height: 40px; width: 55px; }
2502
- .run-info { font-size: 0.9em; text-align: right; color: var(--text-color-secondary); line-height:1.5;}
2503
- .run-info strong { color: var(--text-color); }
2504
- .tabs { display: flex; border-bottom: 2px solid var(--border-color); margin-bottom: 30px; overflow-x: auto; }
2505
- .tab-button { padding: 15px 25px; background: none; border: none; border-bottom: 3px solid transparent; cursor: pointer; font-size: 1.1em; font-weight: 600; color: black; transition: color 0.2s ease, border-color 0.2s ease; white-space: nowrap; }
2506
- .tab-button:hover { color: var(--accent-color); }
2507
- .tab-button.active { color: var(--primary-color); border-bottom-color: var(--primary-color); }
2508
- .tab-content { display: none; animation: fadeIn 0.4s ease-out; }
2509
- .tab-content.active { display: block; }
2843
+ html {
2844
+ overflow-x: hidden;
2845
+ }
2846
+ body {
2847
+ font-family: var(--font-family);
2848
+ margin: 0;
2849
+ background: #fafbfc;
2850
+ color: var(--text-primary);
2851
+ line-height: 1.6;
2852
+ font-size: 15px;
2853
+ min-height: 100vh;
2854
+ overflow-x: hidden;
2855
+ -webkit-font-smoothing: antialiased;
2856
+ -moz-osx-font-smoothing: grayscale;
2857
+ text-rendering: optimizeLegibility;
2858
+ }
2859
+ * {
2860
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
2861
+ will-change: transform, opacity;
2862
+ }
2863
+ *:not(input):not(select):not(textarea):not(button) {
2864
+ transition-duration: 0.15s;
2865
+ }
2866
+ .container {
2867
+ padding: 0;
2868
+ margin: 0;
2869
+ max-width: 100%;
2870
+ overflow-x: hidden;
2871
+ }
2872
+ .header {
2873
+ display: flex;
2874
+ justify-content: space-between;
2875
+ align-items: center;
2876
+ padding: 32px 40px 28px 40px;
2877
+ border-bottom: 1px solid #e2e8f0;
2878
+ background: rgba(255, 255, 255, 0.95);
2879
+ }
2880
+ .header-title {
2881
+ display: flex;
2882
+ align-items: center;
2883
+ gap: 20px;
2884
+ }
2885
+ .header h1 {
2886
+ margin: 0;
2887
+ font-size: 2.5em;
2888
+ font-weight: 900;
2889
+ color: #0f172a;
2890
+ line-height: 1;
2891
+ letter-spacing: -0.03em;
2892
+ }
2893
+ #report-logo {
2894
+ height: 60px;
2895
+ }
2896
+ .run-info {
2897
+ display: flex;
2898
+ gap: 16px;
2899
+ align-items: stretch;
2900
+ background: transparent;
2901
+ border-radius: 12px;
2902
+ padding: 0;
2903
+ box-shadow: var(--shadow-md); /* Inherited from base static style */
2904
+ overflow: hidden; /* Inherited */
2905
+ }
2906
+ .run-info-item {
2907
+ display: flex;
2908
+ flex-direction: column;
2909
+ gap: 8px;
2910
+ padding: 16px 28px;
2911
+ position: relative;
2912
+ flex: 1;
2913
+ min-width: fit-content;
2914
+ }
2915
+
2916
+ .run-info-item:first-child {
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);
2926
+ }
2927
+ .run-info-item:last-child {
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);
2937
+ }
2938
+ .run-info strong {
2939
+ display: flex;
2940
+ align-items: center;
2941
+ gap: 8px;
2942
+ font-size: 0.7em;
2943
+ text-transform: uppercase;
2944
+ letter-spacing: 1.2px;
2945
+ color: #9ca3af;
2946
+ margin: 0;
2947
+ font-weight: 700;
2948
+ }
2949
+ .run-info strong::before {
2950
+ content: '';
2951
+ width: 10px;
2952
+ height: 10px;
2953
+ border-radius: 50%;
2954
+ background: currentColor;
2955
+ opacity: 0.7;
2956
+ box-shadow: 0 0 8px currentColor;
2957
+ }
2958
+ .run-info-item:first-child strong {
2959
+ color: var(--warning-light);
2960
+ }
2961
+ .run-info-item:last-child strong {
2962
+ color: var(--secondary-light);
2963
+ }
2964
+ .run-info span {
2965
+ font-size: 1.5em;
2966
+ font-weight: 800;
2967
+ color: #0f172a; /* Adjusted for light theme consistency, static uses #f9fafb */
2968
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
2969
+ letter-spacing: -0.02em;
2970
+ line-height: 1.2;
2971
+ white-space: nowrap;
2972
+ }
2973
+ .tabs {
2974
+ display: flex;
2975
+ background: #0f172a;
2976
+ padding: 0;
2977
+ margin: 0;
2978
+ position: sticky;
2979
+ top: 0;
2980
+ z-index: 100;
2981
+ overflow-x: auto;
2982
+ -webkit-overflow-scrolling: touch;
2983
+ max-width: 100vw;
2984
+ width: 100%;
2985
+ }
2986
+ .tab-button {
2987
+ flex: 1 1 auto;
2988
+ padding: 24px 20px;
2989
+ background: transparent;
2990
+ border: none;
2991
+ cursor: pointer;
2992
+ font-size: 0.85em;
2993
+ font-weight: 700;
2994
+ color: #64748b;
2995
+ transition: all 0.2s ease;
2996
+ white-space: nowrap;
2997
+ text-transform: uppercase;
2998
+ letter-spacing: 1.2px;
2999
+ border-right: 1px solid #1e293b;
3000
+ min-width: 0;
3001
+ }
3002
+ .tab-button:last-child { border-right: none; }
3003
+ .tab-button:hover {
3004
+ background: #1e293b;
3005
+ color: #ffffff;
3006
+ }
3007
+ .tab-button.active {
3008
+ background: #6366f1;
3009
+ color: #ffffff;
3010
+ }
3011
+ .tab-content {
3012
+ display: none;
3013
+ animation: fadeIn 0.4s ease-out;
3014
+ overflow-x: hidden;
3015
+ max-width: 100%;
3016
+ }
3017
+ .tab-content.active {
3018
+ display: block;
3019
+ }
2510
3020
  @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
2511
- .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 22px; margin-bottom: 35px; }
2512
- .summary-card { background-color: var(--card-background-color); border: 1px solid var(--border-color); border-radius: var(--border-radius); padding: 22px; text-align: center; box-shadow: var(--box-shadow-light); transition: transform 0.2s ease, box-shadow 0.2s ease; }
2513
- .summary-card:hover { transform: translateY(-5px); box-shadow: var(--box-shadow); }
2514
- .summary-card h3 { margin: 0 0 10px; font-size: 1.05em; font-weight: 500; color: var(--text-color-secondary); }
2515
- .summary-card .value { font-size: 2.4em; font-weight: 600; margin-bottom: 8px; }
2516
- .summary-card .trend-percentage { font-size: 1em; color: var(--dark-gray-color); }
2517
- .status-passed .value, .stat-passed svg { color: var(--success-color); }
2518
- .status-failed .value, .stat-failed svg { color: var(--danger-color); }
2519
- .status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
3021
+
3022
+ @media (max-width: 1200px) {
3023
+ .trend-charts-row {
3024
+ grid-template-columns: 1fr;
3025
+ }
3026
+ .dashboard-bottom-row {
3027
+ grid-template-columns: 1fr;
3028
+ }
3029
+ }
3030
+
3031
+
3032
+ .stat-pill.flaky { color: #4b5563; }
3033
+
3034
+ .dashboard-grid {
3035
+ display: grid;
3036
+ grid-template-columns: repeat(4, 1fr);
3037
+ gap: 0;
3038
+ margin: 0 0 40px 0;
3039
+ }
3040
+ .stats-pill.failed { color: var(--danger-dark); }
3041
+ .stats-pill.flaky { color: #4b5563; }
3042
+ .browser-breakdown {
3043
+ display: flex;
3044
+ flex-direction: column;
3045
+ gap: 4px;
3046
+ margin-top: 6px;
3047
+ max-height: 150px;
3048
+ overflow-y: auto;
3049
+ padding-right: 4px;
3050
+ scrollbar-width: thin;
3051
+ scrollbar-color: #cbd5e1 #f1f5f9;
3052
+ }
3053
+ .browser-breakdown::-webkit-scrollbar {
3054
+ width: 6px;
3055
+ display: block;
3056
+ }
3057
+ .browser-breakdown::-webkit-scrollbar-track {
3058
+ background: #f1f5f9;
3059
+ border-radius: 3px;
3060
+ display: block;
3061
+ }
3062
+ .browser-breakdown::-webkit-scrollbar-thumb {
3063
+ background: #cbd5e1;
3064
+ border-radius: 3px;
3065
+ display: block;
3066
+ }
3067
+ .browser-breakdown::-webkit-scrollbar-thumb:hover {
3068
+ background: #94a3b8;
3069
+ display: block;
3070
+ }
3071
+ .browser-item {
3072
+ display: flex;
3073
+ justify-content: space-between;
3074
+ align-items: center;
3075
+ font-size: 0.95em;
3076
+ }
3077
+ .browser-name {
3078
+ font-weight: 700;
3079
+ color: #0f172a;
3080
+ text-transform: capitalize;
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;
3088
+ }
3089
+ .browser-stats {
3090
+ color: #64748b;
3091
+ white-space: nowrap;
3092
+ flex-shrink: 0;
3093
+ font-weight: 700;
3094
+ font-size: 0.95em;
3095
+ }
3096
+ .summary-card {
3097
+ padding: 36px 32px;
3098
+ text-align: left;
3099
+ background: rgba(255, 255, 255, 0.95);
3100
+ border: 1px solid #e2e8f0;
3101
+ transition: background 0.2s ease;
3102
+ border-right: 1px solid #e2e8f0;
3103
+ border-bottom: 1px solid #e2e8f0;
3104
+ }
3105
+ .summary-card:nth-child(4n) { border-right: none; }
3106
+ .summary-card h3 {
3107
+ margin: 0 0 12px;
3108
+ font-size: 0.7em;
3109
+ font-weight: 700;
3110
+ color: #94a3b8;
3111
+ text-transform: uppercase;
3112
+ letter-spacing: 1.2px;
3113
+ }
3114
+ .summary-card .value {
3115
+ font-size: 2.8em;
3116
+ font-weight: 900;
3117
+ margin: 0;
3118
+ line-height: 1;
3119
+ letter-spacing: -0.03em;
3120
+ }
3121
+ .summary-card .trend-percentage {
3122
+ font-size: 0.9em;
3123
+ color: #64748b;
3124
+ margin-top: 8px;
3125
+ font-weight: 600;
3126
+ }
3127
+
3128
+ @media (max-width: 1024px) {
3129
+ .header {
3130
+ padding: 32px 24px;
3131
+ flex-direction: column;
3132
+ gap: 24px;
3133
+ align-items: flex-start;
3134
+ }
3135
+ .run-info {
3136
+ flex-direction: column;
3137
+ gap: 0;
3138
+ width: 100%;
3139
+ border-radius: 14px;
3140
+ overflow: hidden;
3141
+ }
3142
+ .dashboard-grid {
3143
+ grid-template-columns: repeat(2, 1fr);
3144
+ }
3145
+ .summary-card:nth-child(2n) { border-right: none; }
3146
+ .summary-card:nth-child(n+7) { border-bottom: none; }
3147
+ .filters {
3148
+ padding: 24px;
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;
3165
+ }
3166
+ .copy-btn {
3167
+ font-size: 0.75em;
3168
+ padding: 8px 16px;
3169
+ margin-left: 0;
3170
+ }
3171
+ .console-output-section h4 {
3172
+ flex-direction: column;
3173
+ align-items: flex-start;
3174
+ gap: 8px;
3175
+ }
3176
+ .log-wrapper {
3177
+ max-height: 300px;
3178
+ }
3179
+ .tabs {
3180
+ overflow-x: auto;
3181
+ }
3182
+ .tab-button {
3183
+ padding: 20px 24px;
3184
+ font-size: 0.75em;
3185
+ white-space: nowrap;
3186
+ }
3187
+ .tag {
3188
+ font-size: 0.65em;
3189
+ padding: 4px 10px;
3190
+ margin-right: 4px;
3191
+ margin-bottom: 4px;
3192
+ letter-spacing: 0.3px;
3193
+ }
3194
+ .test-case-header {
3195
+ grid-template-columns: 1fr;
3196
+ grid-template-rows: auto auto auto;
3197
+ gap: 12px;
3198
+ padding: 16px 20px;
3199
+ }
3200
+ .test-case-summary {
3201
+ grid-column: 1;
3202
+ grid-row: 1;
3203
+ flex-direction: column;
3204
+ align-items: flex-start;
3205
+ gap: 8px;
3206
+ width: 100%;
3207
+ max-width: 100%;
3208
+ overflow: hidden;
3209
+ }
3210
+ .test-case-title {
3211
+ width: 100%;
3212
+ max-width: 100%;
3213
+ }
3214
+ .test-case-browser {
3215
+ width: 100%;
3216
+ max-width: 100%;
3217
+ white-space: normal;
3218
+ }
3219
+ .test-case-meta {
3220
+ grid-column: 1;
3221
+ grid-row: 2;
3222
+ width: 100%;
3223
+ gap: 6px;
3224
+ }
3225
+ .test-case-status-duration {
3226
+ grid-column: 1;
3227
+ grid-row: 3;
3228
+ align-items: flex-start;
3229
+ }
3230
+ .test-case {
3231
+ margin: 0 0 12px 0;
3232
+ border-radius: 8px;
3233
+ }
3234
+ .test-case-content {
3235
+ padding: 20px;
3236
+ }
3237
+ .pie-chart-wrapper, .suites-widget, .trend-chart {
3238
+ padding: 32px 24px;
3239
+ }
3240
+ .test-history-grid {
3241
+ grid-template-columns: 1fr;
3242
+ }
3243
+ .ai-failure-cards-grid {
3244
+ grid-template-columns: 1fr;
3245
+ }
3246
+ }
3247
+
3248
+ @media (max-width: 768px) {
3249
+ .header h1 { font-size: 1.8em; }
3250
+ #report-logo { height: 48px; }
3251
+ .tabs {
3252
+ flex-wrap: nowrap;
3253
+ gap: 0;
3254
+ overflow-x: auto;
3255
+ }
3256
+ .tab-button {
3257
+ padding: 16px 20px;
3258
+ font-size: 0.7em;
3259
+ flex: 1 1 auto;
3260
+ min-width: 100px;
3261
+ }
3262
+ .dashboard-grid {
3263
+ grid-template-columns: 1fr;
3264
+ }
3265
+ .summary-card {
3266
+ padding: 32px 24px !important;
3267
+ border-right: none !important;
3268
+ }
3269
+ .summary-card .value { font-size: 2.5em !important; }
3270
+ .dashboard-bottom-row {
3271
+ grid-template-columns: 1fr;
3272
+ gap: 0;
3273
+ }
3274
+ .dashboard-column {
3275
+ gap: 0;
3276
+ }
3277
+ .pie-chart-wrapper, .suites-widget, .trend-chart {
3278
+ padding: 28px 20px;
3279
+ }
3280
+ .pie-chart-wrapper h3, .suites-header h2, .trend-chart h3, .chart-title-header {
3281
+ font-size: 1.2em;
3282
+ margin-bottom: 20px;
3283
+ }
3284
+ .pie-chart-wrapper div[id^="pieChart-"] {
3285
+ width: 100% !important;
3286
+ max-width: 100% !important;
3287
+ min-height: 280px;
3288
+ overflow: visible !important;
3289
+ }
3290
+ .pie-chart-wrapper {
3291
+ overflow: visible !important;
3292
+ }
3293
+ .trend-chart-container {
3294
+ min-height: 280px;
3295
+ }
3296
+ .suites-grid {
3297
+ grid-template-columns: 1fr;
3298
+ }
3299
+ .test-case-summary {
3300
+ flex-direction: column;
3301
+ align-items: flex-start;
3302
+ gap: 8px;
3303
+ }
3304
+ .test-case-title {
3305
+ width: 100%;
3306
+ }
3307
+ .test-case-browser {
3308
+ width: 100%;
3309
+ }
3310
+ .test-case-meta {
3311
+ flex-wrap: wrap;
3312
+ gap: 6px;
3313
+ width: 100%;
3314
+ }
3315
+ .test-history-trend-section {
3316
+ padding: 0px 20px !important;
3317
+ }
3318
+ .ai-failure-cards-grid {
3319
+ grid-template-columns: 1fr;
3320
+ }
3321
+ .ai-analyzer-stats {
3322
+ flex-direction: column;
3323
+ gap: 15px;
3324
+ text-align: center;
3325
+ }
3326
+ .failure-header {
3327
+ flex-direction: column;
3328
+ align-items: stretch;
3329
+ gap: 15px;
3330
+ }
3331
+ .failure-main-info {
3332
+ text-align: center;
3333
+ }
3334
+ .failure-meta {
3335
+ justify-content: center;
3336
+ }
3337
+ .ai-buttons-group {
3338
+ flex-direction: column;
3339
+ width: 100%;
3340
+ }
3341
+ .compact-ai-btn, .copy-prompt-btn {
3342
+ justify-content: center;
3343
+ padding: 12px 20px;
3344
+ width: 100%;
3345
+ }
3346
+ }
3347
+
3348
+ @media (max-width: 480px) {
3349
+ .header { padding: 24px 16px; }
3350
+ .header h1 { font-size: 1.5em; }
3351
+ #report-logo { height: 42px; }
3352
+ .run-info {
3353
+ flex-direction: column;
3354
+ gap: 12px;
3355
+ width: 100%;
3356
+ }
3357
+ .run-info-item {
3358
+ padding: 14px 20px;
3359
+ }
3360
+ .run-info-item:not(:last-child)::after {
3361
+ display: none;
3362
+ }
3363
+ .run-info-item:not(:last-child) {
3364
+ border-bottom: 1px solid var(--border-medium);
3365
+ }
3366
+ .run-info strong {
3367
+ font-size: 0.65em;
3368
+ }
3369
+ .run-info span {
3370
+ font-size: 1.1em;
3371
+ }
3372
+ .tabs {
3373
+ flex-wrap: wrap;
3374
+ gap: 4px;
3375
+ padding: 8px;
3376
+ }
3377
+ .tab-button {
3378
+ padding: 14px 10px;
3379
+ font-size: 0.6em;
3380
+ letter-spacing: 0.3px;
3381
+ flex: 1 1 calc(50% - 4px);
3382
+ min-width: 0;
3383
+ text-align: center;
3384
+ }
3385
+ .dashboard-grid { gap: 0; }
3386
+ .summary-card { padding: 28px 16px !important; }
3387
+ .summary-card h3 { font-size: 0.65em; }
3388
+ .summary-card .value { font-size: 2em !important; }
3389
+ .dashboard-bottom-row { gap: 0; }
3390
+ .dashboard-column {
3391
+ gap: 0;
3392
+ }
3393
+ .pie-chart-wrapper, .suites-widget, .trend-chart {
3394
+ padding: 20px 16px;
3395
+ }
3396
+ .pie-chart-wrapper h3, .suites-header h2, .trend-chart h3, .chart-title-header {
3397
+ font-size: 1em;
3398
+ margin-bottom: 16px;
3399
+ font-weight: 800;
3400
+ }
3401
+ .env-dashboard-title {
3402
+ font-size: 1em;
3403
+ margin-bottom: 6px;
3404
+ }
3405
+ .env-dashboard-subtitle {
3406
+ font-size: 0.85em;
3407
+ }
3408
+ .env-card-header {
3409
+ font-size: 0.85em;
3410
+ }
3411
+ .pie-chart-wrapper div[id^="pieChart-"] {
3412
+ width: 100% !important;
3413
+ max-width: 100% !important;
3414
+ min-height: 250px;
3415
+ overflow: visible !important;
3416
+ }
3417
+ .pie-chart-wrapper {
3418
+ overflow: visible !important;
3419
+ padding: 20px 12px;
3420
+ }
3421
+ .trend-chart-container {
3422
+ min-height: 250px;
3423
+ }
3424
+ .suites-grid {
3425
+ grid-template-columns: 1fr;
3426
+ gap: 16px;
3427
+ }
3428
+ .suite-card {
3429
+ padding: 16px;
3430
+ }
3431
+ .filters {
3432
+ padding: 16px;
3433
+ gap: 8px;
3434
+ }
3435
+ .test-history-trend-section {
3436
+ padding: 0px 16px !important;
3437
+ }
3438
+ .test-case {
3439
+ margin: 0 0 10px 0;
3440
+ border-radius: 6px;
3441
+ }
3442
+ .test-case-header {
3443
+ padding: 14px 16px;
3444
+ }
3445
+ .test-case-content {
3446
+ padding: 16px;
3447
+ }
3448
+ .stat-item .stat-number {
3449
+ font-size: 1.5em;
3450
+ }
3451
+ .failure-header {
3452
+ padding: 15px;
3453
+ }
3454
+ .failure-error-preview, .full-error-details {
3455
+ padding-left: 15px;
3456
+ padding-right: 15px;
3457
+ }
3458
+ .header h1 {
3459
+ word-break: break-word;
3460
+ overflow-wrap: break-word;
3461
+ }
3462
+ h2, h3, h4 {
3463
+ word-break: break-word;
3464
+ overflow-wrap: break-word;
3465
+ }
3466
+ .environment-dashboard-wrapper {
3467
+ padding: 24px 16px;
3468
+ gap: 24px;
3469
+ }
3470
+ .env-card {
3471
+ padding: 20px;
3472
+ }
3473
+ }
3474
+ .summary-card.status-passed { background: rgba(16, 185, 129, 0.02); }
3475
+ .summary-card.status-passed:hover {
3476
+ background: rgba(16, 185, 129, 0.15);
3477
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
3478
+ }
3479
+ .summary-card.status-passed .value { color: #10b981; }
3480
+ .summary-card.status-failed { background: rgba(239, 68, 68, 0.02); }
3481
+ .summary-card.status-failed:hover {
3482
+ background: rgba(239, 68, 68, 0.15);
3483
+ box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2);
3484
+ }
3485
+ .summary-card.status-failed .value { color: #ef4444; }
3486
+ .summary-card.status-flaky::before { background: #00ccd3; }
3487
+ .summary-card.status-skipped { background: rgba(245, 158, 11, 0.02); }
3488
+ .summary-card.status-skipped:hover {
3489
+ background: rgba(245, 158, 11, 0.15);
3490
+ box-shadow: 0 4px 12px rgba(245, 158, 11, 0.2);
3491
+ }
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; }
3499
+ .summary-card:not([class*='status-']) .value { color: #0f172a; }
2520
3500
  .dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: start; }
2521
- .pie-chart-wrapper, .suites-widget, .trend-chart { background-color: var(--card-background-color); padding: 28px; border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); display: flex; flex-direction: column; }
2522
- .pie-chart-wrapper h3, .suites-header h2, .trend-chart h3 { text-align: center; margin-top: 0; margin-bottom: 25px; font-size: 1.25em; font-weight: 600; color: var(--text-color); }
2523
- .trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { flex-grow: 1; min-height: 250px; }
3501
+ .dashboard-column {
3502
+ display: flex;
3503
+ flex-direction: column;
3504
+ gap: 28px;
3505
+ }
3506
+ .pie-chart-wrapper, .suites-widget, .trend-chart {
3507
+ background: rgba(255, 255, 255, 0.95);
3508
+ padding: 48px;
3509
+ border: 1px solid #e2e8f0;
3510
+ border-radius: 16px;
3511
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
3512
+ display: flex;
3513
+ flex-direction: column;
3514
+ overflow: visible;
3515
+ margin-bottom: 24px;
3516
+ }
3517
+ .pie-chart-wrapper {
3518
+ position: relative;
3519
+ }
3520
+ .pie-chart-wrapper h3, .suites-header h2, .trend-chart h3, .chart-title-header {
3521
+ text-align: left;
3522
+ margin: 0 0 40px 0;
3523
+ font-size: 1.8em;
3524
+ font-weight: 900;
3525
+ color: #0f172a;
3526
+ letter-spacing: -0.02em;
3527
+ }
3528
+ .trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] {
3529
+ flex-grow: 1;
3530
+ min-height: 250px;
3531
+ width: 100%;
3532
+ overflow: visible;
3533
+ }
2524
3534
  .status-badge-small-tooltip { padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; }
2525
3535
  .status-badge-small-tooltip.status-passed { background-color: var(--success-color); }
2526
3536
  .status-badge-small-tooltip.status-failed { background-color: var(--danger-color); }
2527
3537
  .status-badge-small-tooltip.status-skipped { background-color: var(--warning-color); }
2528
3538
  .status-badge-small-tooltip.status-unknown { background-color: var(--dark-gray-color); }
2529
- .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
+ }
2530
3546
  .summary-badge { background-color: var(--light-gray-color); color: var(--text-color-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
2531
3547
  .suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
2532
- .suite-card { border: 1px solid var(--border-color); border-left-width: 5px; border-radius: calc(var(--border-radius) / 1.5); padding: 20px; background-color: var(--card-background-color); transition: box-shadow 0.2s ease, border-left-color 0.2s ease; }
2533
- .suite-card:hover { box-shadow: var(--box-shadow); }
2534
- .suite-card.status-passed { border-left-color: var(--success-color); }
2535
- .suite-card.status-failed { border-left-color: var(--danger-color); }
2536
- .suite-card.status-skipped { border-left-color: var(--warning-color); }
2537
- .suite-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
2538
- .suite-name { font-weight: 600; font-size: 1.05em; color: var(--text-color); margin-right: 10px; word-break: break-word;}
2539
- .browser-tag { font-size: 0.8em; background-color: var(--medium-gray-color); color: var(--text-color-secondary); padding: 3px 8px; border-radius: 4px; white-space: nowrap;}
2540
- .suite-card-body .test-count { font-size: 0.95em; color: var(--text-color-secondary); display: block; margin-bottom: 10px; }
2541
- .suite-stats { display: flex; gap: 14px; font-size: 0.95em; align-items: center; }
2542
- .suite-stats span { display: flex; align-items: center; gap: 6px; }
2543
- .suite-stats svg { vertical-align: middle; font-size: 1.15em; }
2544
- .filters { display: flex; flex-wrap: wrap; gap: 18px; margin-bottom: 28px; padding: 20px; background-color: var(--light-gray-color); border-radius: var(--border-radius); box-shadow: var(--box-shadow-inset); border-color: black; border-style: groove; }
2545
- .filters input, .filters select, .filters button { padding: 11px 15px; border: 1px solid var(--border-color); border-radius: 6px; font-size: 1em; }
2546
- .filters input { flex-grow: 1; min-width: 240px;}
2547
- .filters select {min-width: 180px;}
2548
- .filters button { background-color: var(--primary-color); color: white; cursor: pointer; transition: background-color 0.2s ease, box-shadow 0.2s ease; border: none; }
2549
- .filters button:hover { background-color: var(--accent-color); box-shadow: 0 2px 5px rgba(0,0,0,0.15);}
2550
- .test-case { margin-bottom: 15px; border: 1px solid var(--border-color); border-radius: var(--border-radius); background-color: var(--card-background-color); box-shadow: var(--box-shadow-light); overflow: hidden; }
2551
- .test-case-header { padding: 10px 15px; background-color: #fff; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid transparent; transition: background-color 0.2s ease; }
2552
- .test-case-header:hover { background-color: #f4f6f8; }
2553
- .test-case-header[aria-expanded="true"] { border-bottom-color: var(--border-color); background-color: #f9fafb; }
2554
- .test-case-summary { display: flex; align-items: center; gap: 14px; flex-grow: 1; flex-wrap: wrap;}
2555
- .test-case-title { font-weight: 600; color: var(--text-color); font-size: 1em; }
2556
- .test-case-browser { font-size: 0.9em; color: var(--text-color-secondary); }
2557
- .test-case-meta { display: flex; align-items: center; gap: 12px; font-size: 0.9em; color: var(--text-color-secondary); flex-shrink: 0; }
2558
- .test-duration { background-color: var(--light-gray-color); padding: 4px 10px; border-radius: 12px; font-size: 0.9em;}
2559
- .status-badge { padding: 5px; border-radius: 6px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase; min-width: 70px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
2560
- .status-badge.status-passed { background-color: var(--success-color); }
2561
- .status-badge.status-failed { background-color: var(--danger-color); }
2562
- .status-badge.status-skipped { background-color: var(--warning-color); }
2563
- .status-badge.status-unknown { background-color: var(--dark-gray-color); }
2564
- .tag { display: inline-block; background: linear-gradient( #fff, #333, #000); color: #fff; padding: 3px 10px; border-radius: 12px; font-size: 0.85em; margin-right: 6px; font-weight: 400; }
2565
- .test-case-content { display: none; padding: 20px; border-top: 1px solid var(--border-color); background-color: #fcfdff; }
3548
+ .suites-widget {
3549
+ display: flex;
3550
+ flex-direction: column;
3551
+ }
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
+
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 {
3670
+ font-size: 0.85em;
3671
+ font-weight: 600;
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;
3686
+ }
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); }
3700
+ .filters {
3701
+ display: flex;
3702
+ flex-wrap: wrap;
3703
+ gap: 12px;
3704
+ margin: 0;
3705
+ padding: 24px 32px;
3706
+ background: #f8fafc;
3707
+ border-bottom: 1px solid #e2e8f0;
3708
+ }
3709
+ .filters input, .filters select, .filters button {
3710
+ padding: 14px 18px;
3711
+ border: 2px solid #e2e8f0;
3712
+ font-size: 0.9em;
3713
+ font-family: var(--font-family);
3714
+ font-weight: 600;
3715
+ transition: all 0.15s ease;
3716
+ }
3717
+ .filters input {
3718
+ flex: 1 1 300px;
3719
+ min-width: 0;
3720
+ background: white;
3721
+ }
3722
+ .filters input:focus {
3723
+ outline: none;
3724
+ border-color: #6366f1;
3725
+ }
3726
+ .filters select {
3727
+ flex: 0 0 auto;
3728
+ min-width: 180px;
3729
+ background: white;
3730
+ cursor: pointer;
3731
+ width: 100%;
3732
+ }
3733
+ .filters select:focus {
3734
+ outline: none;
3735
+ border-color: #6366f1;
3736
+ }
3737
+ .filters button {
3738
+ background: #0f172a;
3739
+ color: white;
3740
+ cursor: pointer;
3741
+ border: none;
3742
+ font-weight: 700;
3743
+ padding: 14px 32px;
3744
+ text-transform: uppercase;
3745
+ letter-spacing: 0.5px;
3746
+ font-size: 0.8em;
3747
+ flex: 0 0 auto;
3748
+ }
3749
+ .filters button:hover {
3750
+ background: #1e293b;
3751
+ color: white;
3752
+ }
3753
+ .test-case {
3754
+ margin: 0 0 16px 0;
3755
+ padding: 0;
3756
+ border: 1px solid #e2e8f0;
3757
+ border-radius: 12px;
3758
+ background: rgba(255, 255, 255, 0.95);
3759
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
3760
+ transition: transform 0.2s ease;
3761
+ overflow: hidden;
3762
+ }
3763
+ .test-case:hover {
3764
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
3765
+ transform: translateY(-2px);
3766
+ border-color: #cbd5e1;
3767
+ }
3768
+ .test-case:last-child {
3769
+ margin-bottom: 0;
3770
+ }
3771
+ .test-case-header {
3772
+ padding: 20px 24px;
3773
+ background: linear-gradient(to right, #ffffff 0%, #f8fafc 100%);
3774
+ cursor: pointer;
3775
+ display: grid;
3776
+ grid-template-columns: 1fr auto;
3777
+ grid-template-rows: auto auto;
3778
+ gap: 12px 20px;
3779
+ transition: all 0.2s ease;
3780
+ border-bottom: 2px solid #f1f5f9;
3781
+ position: relative;
3782
+ }
3783
+ .test-case-header::before {
3784
+ content: '';
3785
+ position: absolute;
3786
+ left: 0;
3787
+ top: 0;
3788
+ bottom: 0;
3789
+ width: 4px;
3790
+ background: transparent;
3791
+ transition: background 0.2s ease;
3792
+ }
3793
+ .test-case-header:hover::before {
3794
+ background: linear-gradient(to bottom, #6366f1 0%, #8b5cf6 100%);
3795
+ }
3796
+ .test-case-header[aria-expanded="true"] {
3797
+ background: linear-gradient(to right, #f8fafc 0%, #f1f5f9 100%);
3798
+ border-bottom-color: #e2e8f0;
3799
+ }
3800
+ .test-case-header[aria-expanded="true"]::before {
3801
+ background: linear-gradient(to bottom, #6366f1 0%, #8b5cf6 100%);
3802
+ }
3803
+ .test-case-summary {
3804
+ display: flex;
3805
+ align-items: center;
3806
+ gap: 14px;
3807
+ flex-wrap: wrap;
3808
+ min-width: 0;
3809
+ grid-column: 1;
3810
+ grid-row: 1;
3811
+ }
3812
+ .test-case-title {
3813
+ font-weight: 600;
3814
+ color: var(--text-color);
3815
+ font-size: 1em;
3816
+ word-break: break-word;
3817
+ overflow-wrap: break-word;
3818
+ flex: 1 1 auto;
3819
+ min-width: 0;
3820
+ }
3821
+ .test-case-browser {
3822
+ font-size: 0.9em;
3823
+ color: var(--text-color-secondary);
3824
+ word-break: break-word;
3825
+ overflow-wrap: break-word;
3826
+ max-width: 100%;
3827
+ }
3828
+ .test-case-meta {
3829
+ display: flex;
3830
+ align-items: center;
3831
+ gap: 8px;
3832
+ font-size: 0.9em;
3833
+ color: var(--text-color-secondary);
3834
+ flex-wrap: wrap;
3835
+ min-width: 0;
3836
+ grid-column: 1;
3837
+ grid-row: 2;
3838
+ }
3839
+ .test-case-status-duration {
3840
+ display: flex;
3841
+ flex-direction: column;
3842
+ align-items: flex-end;
3843
+ gap: 8px;
3844
+ grid-column: 2;
3845
+ grid-row: 1 / 3;
3846
+ align-self: center;
3847
+ }
3848
+ .test-duration {
3849
+ background-color: var(--light-gray-color);
3850
+ padding: 6px 12px;
3851
+ border-radius: 8px;
3852
+ font-size: 0.9em;
3853
+ white-space: nowrap;
3854
+ flex-shrink: 0;
3855
+ font-weight: 700;
3856
+ color: #0f172a;
3857
+ }
3858
+ .status-badge {
3859
+ padding: 8px 20px;
3860
+ border-radius: 0;
3861
+ font-size: 0.7em;
3862
+ font-weight: 800;
3863
+ color: black;
3864
+ text-transform: uppercase;
3865
+ min-width: 100px;
3866
+ text-align: center;
3867
+ letter-spacing: 1px;
3868
+ }
3869
+ .status-badge.status-passed { background: #10b981; }
3870
+ .status-badge.status-failed { background: #ef4444; }
3871
+ .status-badge.status-skipped { background: #f59e0b; }
3872
+ .status-badge.status-unknown { background: #64748b; }
3873
+
3874
+ /* --- NEON GLASS SEVERITY BADGES --- */
3875
+ .severity-badge {
3876
+ display: inline-flex;
3877
+ align-items: center;
3878
+ gap: 6px;
3879
+ padding: 5px 12px;
3880
+ border-radius: 99px;
3881
+ font-size: 0.75em;
3882
+ font-weight: 700;
3883
+ text-transform: uppercase;
3884
+ letter-spacing: 0.05em;
3885
+ border: 1px solid;
3886
+ backdrop-filter: blur(4px);
3887
+ -webkit-backdrop-filter: blur(4px);
3888
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
3889
+ }
3890
+ .severity-badge::before {
3891
+ content: '';
3892
+ width: 6px;
3893
+ height: 6px;
3894
+ border-radius: 50%;
3895
+ background-color: currentColor;
3896
+ box-shadow: 0 0 6px currentColor;
3897
+ }
3898
+ /* Auto-map colors based on data-severity attribute */
3899
+ .severity-badge[data-severity="critical"] {
3900
+ color: #ff4d4d;
3901
+ background-color: rgba(255, 77, 77, 0.1);
3902
+ border-color: rgba(255, 77, 77, 0.25);
3903
+ }
3904
+ .severity-badge[data-severity="high"] {
3905
+ color: #fb923c;
3906
+ background-color: rgba(251, 146, 60, 0.1);
3907
+ border-color: rgba(251, 146, 60, 0.25);
3908
+ }
3909
+ .severity-badge[data-severity="medium"] {
3910
+ color: #facc15;
3911
+ background-color: rgba(250, 204, 21, 0.1);
3912
+ border-color: rgba(250, 204, 21, 0.25);
3913
+ }
3914
+ .severity-badge[data-severity="low"] {
3915
+ color: #4ade80;
3916
+ background-color: rgba(74, 222, 128, 0.1);
3917
+ border-color: rgba(74, 222, 128, 0.25);
3918
+ }
3919
+ .severity-badge[data-severity="minor"] {
3920
+ color: #94a3b8;
3921
+ background-color: rgba(148, 163, 184, 0.1);
3922
+ border-color: rgba(148, 163, 184, 0.25);
3923
+ }
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
+
3984
+ .tag {
3985
+ display: inline-flex;
3986
+ align-items: center;
3987
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
3988
+ color: #ffffff;
3989
+ padding: 6px 14px;
3990
+ border-radius: 6px;
3991
+ font-size: 0.8em;
3992
+ margin-right: 8px;
3993
+ margin-bottom: 4px;
3994
+ font-weight: 600;
3995
+ text-transform: uppercase;
3996
+ letter-spacing: 0.5px;
3997
+ box-shadow: 0 2px 6px rgba(99, 102, 241, 0.25);
3998
+ transition: all 0.2s ease;
3999
+ flex-shrink: 0;
4000
+ white-space: nowrap;
4001
+ }
4002
+ .tag:hover {
4003
+ box-shadow: 0 4px 10px rgba(99, 102, 241, 0.35);
4004
+ transform: translateY(-1px);
4005
+ }
4006
+ .test-case-content {
4007
+ display: none;
4008
+ padding: 24px;
4009
+ background: linear-gradient(to bottom, #ffffff 0%, #f9fafb 100%);
4010
+ border-top: 1px solid #e2e8f0;
4011
+ }
2566
4012
  .test-case-content h4 { margin-top: 22px; margin-bottom: 14px; font-size: 1.15em; color: var(--primary-color); }
2567
4013
  .test-case-content p { margin-bottom: 10px; font-size: 1em; }
2568
- .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
+ }
2569
4024
  .test-error-summary h4 { color: var(--danger-color); margin-top:0;}
2570
4025
  .test-error-summary pre { white-space: pre-wrap; word-break: break-all; color: var(--danger-color); font-size: 0.95em;}
2571
4026
  .steps-list { margin: 18px 0; }
@@ -2577,10 +4032,15 @@ function generateHTML(reportData, trendData = null) {
2577
4032
  .step-duration { color: var(--dark-gray-color); font-size: 0.9em; }
2578
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); }
2579
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; }
2580
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); }
2581
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; }
2582
4039
  .step-hook { background-color: rgba(33,150,243,0.04); border-left: 3px solid var(--info-color) !important; }
2583
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; }
2584
4044
  .nested-steps { margin-top: 12px; }
2585
4045
  .attachments-section { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--light-gray-color); }
2586
4046
  .attachments-section h4 { margin-top: 0; margin-bottom: 20px; font-size: 1.1em; color: var(--text-color); }
@@ -2609,10 +4069,24 @@ function generateHTML(reportData, trendData = null) {
2609
4069
  .test-history-header h3 { margin: 0; font-size: 1.15em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* This was h3, changed to p for consistency with user file */
2610
4070
  .test-history-header p { font-weight: 500 } /* Added this */
2611
4071
  .test-history-trend { margin-bottom: 20px; min-height: 110px; }
4072
+ .test-history-trend-section {
4073
+ padding: 0px 48px !important;
4074
+ }
4075
+ .test-history-trend-section .chart-title-header {
4076
+ margin: 0 0 20px 0 !important;
4077
+ }
2612
4078
  .test-history-trend div[id^="testHistoryChart-"] { display: block; margin: 0 auto; max-width:100%; height: 100px; width: 320px; }
2613
4079
  .test-history-details-collapsible summary { cursor: pointer; font-size: 1em; color: var(--primary-color); margin-bottom: 10px; font-weight:500; }
2614
4080
  .test-history-details-collapsible summary:hover {text-decoration: underline;}
2615
- .test-history-details table { width: 100%; border-collapse: collapse; font-size: 0.95em; }
4081
+ .test-history-details {
4082
+ overflow-x: auto;
4083
+ max-width: 100%;
4084
+ }
4085
+ .test-history-details table {
4086
+ width: 100%;
4087
+ border-collapse: collapse;
4088
+ font-size: 0.95em;
4089
+ }
2616
4090
  .test-history-details th, .test-history-details td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--light-gray-color); }
2617
4091
  .test-history-details th { background-color: var(--light-gray-color); font-weight: 600; }
2618
4092
  .status-badge-small { padding: 3px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase; display: inline-block; }
@@ -2630,13 +4104,57 @@ function generateHTML(reportData, trendData = null) {
2630
4104
  .ai-failure-card-body { padding: 20px; }
2631
4105
  .ai-fix-btn { background-color: var(--primary-color); color: white; border: none; padding: 10px 18px; font-size: 1em; font-weight: 600; border-radius: 6px; cursor: pointer; transition: background-color 0.2s ease, transform 0.2s ease; display: inline-flex; align-items: center; gap: 8px; }
2632
4106
  .ai-fix-btn:hover { background-color: var(--accent-color); transform: translateY(-2px); }
2633
- .ai-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.65); display: none; align-items: center; justify-content: center; z-index: 1050; animation: fadeIn 0.3s; }
2634
- .ai-modal-content { background-color: var(--card-background-color); color: var(--text-color); border-radius: var(--border-radius); width: 90%; max-width: 800px; max-height: 90vh; box-shadow: 0 10px 30px rgba(0,0,0,0.2); display: flex; flex-direction: column; overflow: hidden; }
2635
- .ai-modal-header { padding: 18px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
2636
- .ai-modal-header h3 { margin: 0; font-size: 1.25em; }
2637
- .ai-modal-close { font-size: 2rem; font-weight: 300; cursor: pointer; color: var(--dark-gray-color); line-height: 1; transition: color 0.2s; }
2638
- .ai-modal-close:hover { color: var(--danger-color); }
2639
- .ai-modal-body { padding: 25px; overflow-y: auto; }
4107
+ .ai-modal-overlay {
4108
+ position: fixed;
4109
+ top: 0;
4110
+ left: 0;
4111
+ width: 100%;
4112
+ height: 100%;
4113
+ background-color: rgba(0,0,0,0.8);
4114
+ display: none;
4115
+ align-items: center;
4116
+ justify-content: center;
4117
+ z-index: 1050;
4118
+ animation: fadeIn 0.3s;
4119
+ }
4120
+ .ai-modal-content {
4121
+ background-color: var(--card-background-color);
4122
+ color: var(--text-color);
4123
+ border-radius: var(--border-radius);
4124
+ width: 90%;
4125
+ max-width: 800px;
4126
+ max-height: 90vh;
4127
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5);
4128
+ display: flex;
4129
+ flex-direction: column;
4130
+ overflow: hidden;
4131
+ }
4132
+ .ai-modal-header {
4133
+ padding: 18px 25px;
4134
+ border-bottom: 1px solid var(--border-color);
4135
+ display: flex;
4136
+ justify-content: space-between;
4137
+ align-items: center;
4138
+ }
4139
+ .ai-modal-header h3 {
4140
+ margin: 0;
4141
+ font-size: 1.25em;
4142
+ }
4143
+ .ai-modal-close {
4144
+ font-size: 2rem;
4145
+ font-weight: 300;
4146
+ cursor: pointer;
4147
+ color: var(--dark-gray-color);
4148
+ line-height: 1;
4149
+ transition: color 0.2s;
4150
+ }
4151
+ .ai-modal-close:hover {
4152
+ color: var(--danger-color);
4153
+ }
4154
+ .ai-modal-body {
4155
+ padding: 25px;
4156
+ overflow-y: auto;
4157
+ }
2640
4158
  .ai-modal-body h4 { margin-top: 18px; margin-bottom: 10px; font-size: 1.1em; color: var(--primary-color); }
2641
4159
  .ai-modal-body p { margin-bottom: 15px; }
2642
4160
  .ai-loader { margin: 40px auto; border: 5px solid #f3f3f3; border-top: 5px solid var(--primary-color); border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite; }
@@ -2650,9 +4168,69 @@ function generateHTML(reportData, trendData = null) {
2650
4168
  .view-trace:hover { background: #2c5282; }
2651
4169
  .download-trace { background: #e2e8f0; color: #2d3748; }
2652
4170
  .download-trace:hover { background: #cbd5e0; }
2653
- .filters button.clear-filters-btn { background-color: var(--medium-gray-color); color: var(--text-color); }
2654
- .filters button.clear-filters-btn:hover { background-color: var(--dark-gray-color); color: #fff; }
2655
- .copy-btn {color: var(--primary-color); background: #fefefe; border-radius: 8px; cursor: pointer; border-color: var(--primary-color); font-size: 1em; margin-left: 93%; font-weight: 600;}
4171
+ .filters button.clear-filters-btn {
4172
+ background-color: var(--medium-gray-color);
4173
+ color: var(--text-color);
4174
+ pointer-events: auto;
4175
+ cursor: pointer;
4176
+ width: 100%;
4177
+ }
4178
+ .filters button.clear-filters-btn:active,
4179
+ .filters button.clear-filters-btn:focus {
4180
+ background-color: var(--medium-gray-color);
4181
+ color: var(--text-color);
4182
+ transform: none;
4183
+ box-shadow: none;
4184
+ outline: none;
4185
+ }
4186
+ .copy-btn {
4187
+ color: #ffffff;
4188
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
4189
+ border: none;
4190
+ border-radius: 8px;
4191
+ cursor: pointer;
4192
+ font-size: 0.85em;
4193
+ font-weight: 600;
4194
+ padding: 10px 20px;
4195
+ transition: all 0.2s ease;
4196
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.2);
4197
+ text-transform: uppercase;
4198
+ letter-spacing: 0.5px;
4199
+ margin-left: auto;
4200
+ display: inline-flex;
4201
+ align-items: center;
4202
+ gap: 6px;
4203
+ }
4204
+ .copy-btn:hover {
4205
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
4206
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
4207
+ transform: translateY(-1px);
4208
+ }
4209
+ .copy-btn:active {
4210
+ transform: translateY(0);
4211
+ box-shadow: 0 2px 6px rgba(99, 102, 241, 0.2);
4212
+ }
4213
+ .log-wrapper {
4214
+ max-width: 100%;
4215
+ overflow-x: auto;
4216
+ overflow-y: auto;
4217
+ max-height: 400px;
4218
+ border-radius: 8px;
4219
+ background: #2d2d2d;
4220
+ }
4221
+ .log-wrapper pre {
4222
+ margin: 0;
4223
+ white-space: pre;
4224
+ word-wrap: normal;
4225
+ overflow-wrap: normal;
4226
+ }
4227
+ .console-output-section h4 {
4228
+ display: flex;
4229
+ align-items: center;
4230
+ justify-content: space-between;
4231
+ gap: 16px;
4232
+ margin-bottom: 12px;
4233
+ }
2656
4234
  /* Compact AI Failure Analyzer Styles */
2657
4235
  .ai-analyzer-stats {
2658
4236
  display: flex;
@@ -2852,6 +4430,62 @@ function generateHTML(reportData, trendData = null) {
2852
4430
  max-height: 300px;
2853
4431
  overflow-y: auto;
2854
4432
  }
4433
+ .ai-suggestion-container {
4434
+ margin-top: 15px;
4435
+ border-top: 2px solid #e2e8f0;
4436
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
4437
+ animation: slideDown 0.3s ease-out;
4438
+ }
4439
+ @keyframes slideDown {
4440
+ from {
4441
+ opacity: 0;
4442
+ max-height: 0;
4443
+ transform: translateY(-10px);
4444
+ }
4445
+ to {
4446
+ opacity: 1;
4447
+ max-height: 1000px;
4448
+ transform: translateY(0);
4449
+ }
4450
+ }
4451
+ .ai-suggestion-content {
4452
+ padding: 20px;
4453
+ }
4454
+ .ai-suggestion-header {
4455
+ display: flex;
4456
+ align-items: center;
4457
+ gap: 10px;
4458
+ margin-bottom: 15px;
4459
+ padding-bottom: 10px;
4460
+ border-bottom: 2px solid #6366f1;
4461
+ }
4462
+ .ai-suggestion-header h4 {
4463
+ margin: 0;
4464
+ color: #6366f1;
4465
+ font-size: 1.1em;
4466
+ font-weight: 700;
4467
+ }
4468
+ .ai-suggestion-body {
4469
+ color: #0f172a;
4470
+ line-height: 1.6;
4471
+ }
4472
+ .ai-suggestion-body h4 {
4473
+ color: #6366f1;
4474
+ margin-top: 15px;
4475
+ margin-bottom: 8px;
4476
+ font-size: 1em;
4477
+ }
4478
+ .ai-suggestion-body p {
4479
+ margin-bottom: 10px;
4480
+ }
4481
+ .ai-suggestion-body pre {
4482
+ background: #1e293b;
4483
+ color: #e2e8f0;
4484
+ padding: 12px;
4485
+ border-radius: 6px;
4486
+ overflow-x: auto;
4487
+ font-size: 0.9em;
4488
+ }
2855
4489
 
2856
4490
  /* Responsive adjustments for compact design */
2857
4491
  @media (max-width: 768px) {
@@ -2894,10 +4528,7 @@ function generateHTML(reportData, trendData = null) {
2894
4528
  }
2895
4529
  }
2896
4530
 
2897
- @media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
2898
- @media (max-width: 992px) { .dashboard-bottom-row { grid-template-columns: 1fr; } .pie-chart-wrapper div[id^="pieChart-"] { max-width: 350px; margin: 0 auto; } .filters input { min-width: 180px; } .filters select { min-width: 150px; } }
2899
- @media (max-width: 768px) { body { font-size: 15px; } .container { margin: 10px; padding: 20px; } .header { flex-direction: column; align-items: flex-start; gap: 15px; } .header h1 { font-size: 1.6em; } .run-info { text-align: left; font-size:0.9em; } .tabs { margin-bottom: 25px;} .tab-button { padding: 12px 20px; font-size: 1.05em;} .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;} .summary-card .value {font-size: 2em;} .summary-card h3 {font-size: 0.95em;} .filters { flex-direction: column; padding: 18px; gap: 12px;} .filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;} .test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; } .test-case-summary {gap: 10px;} .test-case-title {font-size: 1.05em;} .test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;} .attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;} .test-history-grid {grid-template-columns: 1fr;} .pie-chart-wrapper {min-height: auto;} .ai-failure-cards-grid { grid-template-columns: 1fr; } }
2900
- @media (max-width: 480px) { body {font-size: 14px;} .container {padding: 15px;} .header h1 {font-size: 1.4em;} #report-logo { height: 35px; width: 50px; } .tab-button {padding: 10px 15px; font-size: 1em;} .summary-card .value {font-size: 1.8em;} .attachments-grid {grid-template-columns: 1fr;} .step-item {padding-left: calc(var(--depth, 0) * 18px);} .test-case-content, .step-details {padding: 15px;} .trend-charts-row {gap: 20px;} .trend-chart {padding: 20px;} }
4531
+
2901
4532
  </style>
2902
4533
  </head>
2903
4534
  <body>
@@ -2907,11 +4538,16 @@ function generateHTML(reportData, trendData = null) {
2907
4538
  <img id="report-logo" src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png" alt="Report Logo">
2908
4539
  <h1>Pulse Report</h1>
2909
4540
  </div>
2910
- <div class="run-info"><strong>Run Date:</strong> ${formatDate(
2911
- runSummary.timestamp
2912
- )}<br><strong>Total Duration:</strong> ${formatDuration(
2913
- runSummary.duration
2914
- )}</div>
4541
+ <div class="run-info">
4542
+ <div class="run-info-item">
4543
+ <strong>Run Date</strong>
4544
+ <span>${formatDate(runSummary.timestamp)}</span>
4545
+ </div>
4546
+ <div class="run-info-item">
4547
+ <strong>Total Duration</strong>
4548
+ <span>${formatDuration(runSummary.duration)}</span>
4549
+ </div>
4550
+ </div>
2915
4551
  </header>
2916
4552
  <div class="tabs">
2917
4553
  <button class="tab-button active" data-tab="dashboard">Dashboard</button>
@@ -2933,31 +4569,55 @@ function generateHTML(reportData, trendData = null) {
2933
4569
  <div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
2934
4570
  runSummary.skipped || 0
2935
4571
  }</div><div class="trend-percentage">${skipPercentage}%</div></div>
2936
- <div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
2937
- <div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
2938
- runSummary.duration
2939
- )}</div></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>
4582
+ <div class="summary-card">
4583
+ <h3>🌐 Browser Distribution <span style="font-size: 0.7em; color: var(--text-color-secondary); font-weight: 400;">(${browserBreakdown.length} total)</span></h3>
4584
+ <div class="browser-breakdown" style="max-height: 200px; overflow-y: auto; padding-right: 4px;">
4585
+ ${browserBreakdown
4586
+ .slice(0, 3)
4587
+ .map(
4588
+ (b) =>
4589
+ `<div class="browser-item">
4590
+ <span class="browser-name" title="${sanitizeHTML(b.browser)}">${sanitizeHTML(b.browser)}</span>
4591
+ <span class="browser-stats">${b.percentage}% (${b.count})</span>
4592
+ </div>`,
4593
+ )
4594
+ .join("")}
4595
+ ${
4596
+ browserBreakdown.length > 3
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;">
4598
+ <span>+${browserBreakdown.length - 3} more browsers</span>
4599
+ </div>`
4600
+ : ""
4601
+ }
4602
+ </div>
4603
+ </div>
2940
4604
  </div>
2941
4605
  <div class="dashboard-bottom-row">
2942
- <div style="display: flex; flex-direction: column; gap: 28px;">
4606
+ <div class="dashboard-column">
2943
4607
  ${generatePieChart(
2944
4608
  [
2945
4609
  { label: "Passed", value: runSummary.passed },
2946
4610
  { label: "Failed", value: runSummary.failed },
4611
+ { label: "Flaky", value: runSummary.flaky || 0 },
2947
4612
  { label: "Skipped", value: runSummary.skipped || 0 },
2948
4613
  ],
2949
4614
  400,
2950
- 390
4615
+ 390,
2951
4616
  )}
2952
- ${
2953
- runSummary.environment &&
2954
- Object.keys(runSummary.environment).length > 0
2955
- ? generateEnvironmentDashboard(runSummary.environment)
2956
- : '<div class="no-data">Environment data not available.</div>'
2957
- }
4617
+ ${generateEnvironmentSection(runSummary.environment)}
2958
4618
  </div>
2959
4619
 
2960
- <div style="display: flex; flex-direction: column; gap: 28px;">
4620
+ <div class="dashboard-column">
2961
4621
  ${generateSuitesWidget(suitesData)}
2962
4622
  ${generateSeverityDistributionChart(results)}
2963
4623
  </div>
@@ -2966,25 +4626,24 @@ function generateHTML(reportData, trendData = null) {
2966
4626
  <div id="test-runs" class="tab-content">
2967
4627
  <div class="filters">
2968
4628
  <input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
2969
- <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>
2970
4630
  <select id="filter-browser"><option value="">All Browsers</option>${Array.from(
2971
4631
  new Set(
2972
- (results || []).map((test) => test.browser || "unknown")
2973
- )
4632
+ (results || []).map((test) => test.browser || "unknown"),
4633
+ ),
2974
4634
  )
2975
4635
  .map(
2976
4636
  (browser) =>
2977
4637
  `<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
2978
- browser
2979
- )}</option>`
4638
+ browser,
4639
+ )}</option>`,
2980
4640
  )
2981
4641
  .join("")}</select>
2982
- <button id="expand-all-tests">Expand All</button> <button id="collapse-all-tests">Collapse All</button> <button id="clear-run-summary-filters" class="clear-filters-btn">Clear Filters</button>
4642
+ <button id="clear-run-summary-filters" class="clear-filters-btn">Clear Filters</button>
2983
4643
  </div>
2984
4644
  <div class="test-cases-list">${generateTestCasesHTML()}</div>
2985
4645
  </div>
2986
4646
  <div id="test-history" class="tab-content">
2987
- <h2 class="tab-main-title">Execution Trends</h2>
2988
4647
  <div class="trend-charts-row">
2989
4648
  <div class="trend-chart"><h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
2990
4649
  ${
@@ -3011,13 +4670,15 @@ function generateHTML(reportData, trendData = null) {
3011
4670
  ${generateDescribeDurationChart(results)}
3012
4671
  </div>
3013
4672
  </div>
3014
- <h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
3015
4673
  <div class="trend-charts-row">
3016
4674
  <div class="trend-chart">
4675
+ <h3 class="chart-title-header">Test Distribution by Worker ${infoTooltip}</h3>
3017
4676
  ${generateWorkerDistributionChart(results)}
3018
4677
  </div>
3019
4678
  </div>
3020
- <h2 class="tab-main-title">Individual Test History</h2>
4679
+ <div class="trend-chart test-history-trend-section" style="border-bottom: none;">
4680
+ <h3 class="chart-title-header">Individual Test History</h3>
4681
+ </div>
3021
4682
  ${
3022
4683
  trendData &&
3023
4684
  trendData.testRuns &&
@@ -3032,7 +4693,7 @@ function generateHTML(reportData, trendData = null) {
3032
4693
  <footer style="padding: 0.5rem; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); text-align: center; font-family: 'Segoe UI', system-ui, sans-serif;">
3033
4694
  <div style="display: inline-flex; align-items: center; gap: 0.5rem; color: #333; font-size: 0.9rem; font-weight: 600; letter-spacing: 0.5px;">
3034
4695
  <span>Created by</span>
3035
- <a href="https://github.com/Arghajit47" target="_blank" rel="noopener noreferrer" style="color: #7737BF; font-weight: 700; font-style: italic; text-decoration: none; transition: all 0.2s ease;" onmouseover="this.style.color='#BF5C37'" onmouseout="this.style.color='#7737BF'">Arghajit Singha</a>
4696
+ <a href="https://www.npmjs.com/package/@arghajit/playwright-pulse-report" target="_blank" rel="noopener noreferrer" style="color: #7737BF; font-weight: 700; font-style: italic; text-decoration: none; transition: all 0.2s ease;" onmouseover="this.style.color='#BF5C37'" onmouseout="this.style.color='#7737BF'">Pulse Report</a>
3036
4697
  </div>
3037
4698
  <div style="margin-top: 0.5rem; font-size: 0.75rem; color: #666;">Crafted with precision</div>
3038
4699
  </footer>
@@ -3051,25 +4712,62 @@ function generateHTML(reportData, trendData = null) {
3051
4712
  console.error('Could not find log element with ID:', elementId);
3052
4713
  return;
3053
4714
  }
4715
+ const originalText = button.textContent;
3054
4716
  navigator.clipboard.writeText(logElement.innerText).then(() => {
3055
4717
  button.textContent = 'Copied!';
3056
- setTimeout(() => { button.textContent = 'Copy'; }, 2000);
4718
+ setTimeout(() => { button.textContent = originalText; }, 2000);
3057
4719
  }).catch(err => {
3058
4720
  console.error('Failed to copy log content:', err);
3059
4721
  button.textContent = 'Failed';
3060
- setTimeout(() => { button.textContent = 'Copy'; }, 2000);
4722
+ setTimeout(() => { button.textContent = originalText; }, 2000);
3061
4723
  });
3062
4724
  }
3063
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
+
3064
4753
  // --- AI Failure Analyzer Functions ---
3065
4754
  function getAIFix(button) {
3066
- const modal = document.getElementById('ai-fix-modal');
3067
- const modalContent = document.getElementById('ai-fix-modal-content');
3068
- const modalTitle = document.getElementById('ai-fix-modal-title');
4755
+ const failureItem = button.closest('.compact-failure-item');
4756
+ const aiContainer = failureItem.querySelector('.ai-suggestion-container');
4757
+ const aiContent = failureItem.querySelector('.ai-suggestion-content');
4758
+
4759
+ // Toggle if already visible
4760
+ if (aiContainer.style.display === 'block') {
4761
+ aiContainer.style.display = 'none';
4762
+ button.querySelector('.ai-text').textContent = 'AI Fix';
4763
+ return;
4764
+ }
3069
4765
 
3070
- modal.style.display = 'flex';
3071
- modalTitle.textContent = 'Analyzing...';
3072
- modalContent.innerHTML = '<div class="ai-loader"></div>';
4766
+ // Show loading state
4767
+ aiContainer.style.display = 'block';
4768
+ aiContent.innerHTML = '<div class="ai-loader" style="margin: 40px auto;"></div>';
4769
+ button.querySelector('.ai-text').textContent = 'Loading...';
4770
+ button.disabled = true;
3073
4771
 
3074
4772
  try {
3075
4773
  const testJson = button.dataset.testJson;
@@ -3087,7 +4785,6 @@ function getAIFix(button) {
3087
4785
  const codeSnippet = test.snippet || '';
3088
4786
 
3089
4787
  const shortTestName = testName.split(' > ').pop();
3090
- modalTitle.textContent = \`Analysis for: \${shortTestName}\`;
3091
4788
 
3092
4789
  const apiUrl = 'https://ai-test-analyser.netlify.app/api/analyze';
3093
4790
  fetch(apiUrl, {
@@ -3151,18 +4848,31 @@ function getAIFix(button) {
3151
4848
  suggestionsHtml += \`<div class="code-section"><pre><code>No suggestion provided.</code></pre></div>\`;
3152
4849
  }
3153
4850
 
3154
- // Combine both parts and set the modal content
3155
- modalContent.innerHTML = analysisHtml + suggestionsHtml;
4851
+ // Combine both parts and display inline
4852
+ button.querySelector('.ai-text').textContent = 'Hide AI Fix';
4853
+ button.disabled = false;
4854
+ aiContent.innerHTML = \`
4855
+ <div class="ai-suggestion-header">
4856
+ <h4>🤖 AI Analysis Result</h4>
4857
+ </div>
4858
+ <div class="ai-suggestion-body">
4859
+ \${analysisHtml}
4860
+ \${suggestionsHtml}
4861
+ </div>
4862
+ \`;
3156
4863
  })
3157
4864
  .catch(err => {
3158
4865
  console.error('AI Fix Error:', err);
3159
- modalContent.innerHTML = \`<div class="test-error-summary"><strong>Error:</strong> Failed to get AI analysis. Please check the console for details. <br><br> \${err.message}</div>\`;
4866
+ button.disabled = false;
4867
+ button.querySelector('.ai-text').textContent = 'AI Fix';
4868
+ aiContent.innerHTML = \`<div class="test-error-summary"><strong>Error:</strong> Failed to get AI analysis. Please check the console for details. <br><br> \${err.message}</div>\`;
3160
4869
  });
3161
4870
 
3162
4871
  } catch (e) {
3163
4872
  console.error('Error processing test data for AI Fix:', e);
3164
- modalTitle.textContent = 'Error';
3165
- modalContent.innerHTML = \`<div class="test-error-summary">Could not process test data. Is it formatted correctly?</div>\`;
4873
+ button.disabled = false;
4874
+ button.querySelector('.ai-text').textContent = 'AI Fix';
4875
+ aiContent.innerHTML = \`<div class="test-error-summary">Could not process test data. Is it formatted correctly?</div>\`;
3166
4876
  }
3167
4877
  }
3168
4878
 
@@ -3245,6 +4955,7 @@ Code Snippet:
3245
4955
  function closeAiModal() {
3246
4956
  const modal = document.getElementById('ai-fix-modal');
3247
4957
  if(modal) modal.style.display = 'none';
4958
+ document.body.style.setProperty('overflow', '', 'important');
3248
4959
  }
3249
4960
 
3250
4961
  function toggleErrorDetails(button) {
@@ -3352,16 +5063,7 @@ Code Snippet:
3352
5063
  document.querySelectorAll('#test-runs .step-header').forEach(header => {
3353
5064
  header.addEventListener('click', () => toggleElementDetails(header, '.step-details'));
3354
5065
  });
3355
- const expandAllBtn = document.getElementById('expand-all-tests');
3356
- const collapseAllBtn = document.getElementById('collapse-all-tests');
3357
- function setAllTestRunDetailsVisibility(displayMode, ariaState) {
3358
- document.querySelectorAll('#test-runs .test-case-content').forEach(el => el.style.display = displayMode);
3359
- document.querySelectorAll('#test-runs .step-details').forEach(el => el.style.display = displayMode);
3360
- document.querySelectorAll('#test-runs .test-case-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
3361
- document.querySelectorAll('#test-runs .step-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
3362
- }
3363
- if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
3364
- if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
5066
+
3365
5067
  // --- Annotation Link Handler ---
3366
5068
  document.querySelectorAll('a.annotation-link').forEach(link => {
3367
5069
  link.addEventListener('click', (e) => {
@@ -3515,6 +5217,8 @@ async function runScript(scriptPath, args = []) {
3515
5217
  });
3516
5218
  }
3517
5219
  async function main() {
5220
+ await animate();
5221
+
3518
5222
  const __filename = fileURLToPath(import.meta.url);
3519
5223
  const __dirname = path.dirname(__filename);
3520
5224
 
@@ -3530,7 +5234,7 @@ async function main() {
3530
5234
  // Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
3531
5235
  const archiveRunScriptPath = path.resolve(
3532
5236
  __dirname,
3533
- "generate-trend.mjs" // Keeping the filename as per your request
5237
+ "generate-trend.mjs", // Keeping the filename as per your request
3534
5238
  );
3535
5239
 
3536
5240
  const outputDir = await getOutputDir(customOutputDir);
@@ -3547,7 +5251,7 @@ async function main() {
3547
5251
  console.log(chalk.gray(` (from CLI argument)`));
3548
5252
  } else {
3549
5253
  console.log(
3550
- chalk.gray(` (auto-detected from playwright.config or using default)`)
5254
+ chalk.gray(` (auto-detected from playwright.config or using default)`),
3551
5255
  );
3552
5256
  }
3553
5257
 
@@ -3556,14 +5260,14 @@ async function main() {
3556
5260
  const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
3557
5261
  await runScript(archiveRunScriptPath, archiveArgs);
3558
5262
  console.log(
3559
- chalk.green("Current run data archiving to history completed.")
5263
+ chalk.green("Current run data archiving to history completed."),
3560
5264
  );
3561
5265
  } catch (error) {
3562
5266
  console.error(
3563
5267
  chalk.red(
3564
- "Failed to archive current run data. Report might use stale or incomplete historical trends."
5268
+ "Failed to archive current run data. Report might use stale or incomplete historical trends.",
3565
5269
  ),
3566
- error
5270
+ error,
3567
5271
  );
3568
5272
  }
3569
5273
 
@@ -3578,22 +5282,22 @@ async function main() {
3578
5282
  !currentRunReportData.results
3579
5283
  ) {
3580
5284
  throw new Error(
3581
- "Invalid report JSON structure. 'results' field is missing or invalid."
5285
+ "Invalid report JSON structure. 'results' field is missing or invalid.",
3582
5286
  );
3583
5287
  }
3584
5288
  if (!Array.isArray(currentRunReportData.results)) {
3585
5289
  currentRunReportData.results = [];
3586
5290
  console.warn(
3587
5291
  chalk.yellow(
3588
- "Warning: 'results' field in current run JSON was not an array. Treated as empty."
3589
- )
5292
+ "Warning: 'results' field in current run JSON was not an array. Treated as empty.",
5293
+ ),
3590
5294
  );
3591
5295
  }
3592
5296
  } catch (error) {
3593
5297
  console.error(
3594
5298
  chalk.red(
3595
- `Critical Error: Could not read or parse main report JSON at ${reportJsonPath}: ${error.message}`
3596
- )
5299
+ `Critical Error: Could not read or parse main report JSON at ${reportJsonPath}: ${error.message}`,
5300
+ ),
3597
5301
  );
3598
5302
  process.exit(1);
3599
5303
  }
@@ -3606,7 +5310,8 @@ async function main() {
3606
5310
 
3607
5311
  const jsonHistoryFiles = allHistoryFiles
3608
5312
  .filter(
3609
- (file) => file.startsWith(HISTORY_FILE_PREFIX) && file.endsWith(".json")
5313
+ (file) =>
5314
+ file.startsWith(HISTORY_FILE_PREFIX) && file.endsWith(".json"),
3610
5315
  )
3611
5316
  .map((file) => {
3612
5317
  const timestampPart = file
@@ -3623,7 +5328,7 @@ async function main() {
3623
5328
 
3624
5329
  const filesToLoadForTrend = jsonHistoryFiles.slice(
3625
5330
  0,
3626
- MAX_HISTORY_FILES_TO_LOAD_FOR_REPORT
5331
+ MAX_HISTORY_FILES_TO_LOAD_FOR_REPORT,
3627
5332
  );
3628
5333
 
3629
5334
  for (const fileMeta of filesToLoadForTrend) {
@@ -3634,29 +5339,29 @@ async function main() {
3634
5339
  } catch (fileReadError) {
3635
5340
  console.warn(
3636
5341
  chalk.yellow(
3637
- `Could not read/parse history file ${fileMeta.name}: ${fileReadError.message}`
3638
- )
5342
+ `Could not read/parse history file ${fileMeta.name}: ${fileReadError.message}`,
5343
+ ),
3639
5344
  );
3640
5345
  }
3641
5346
  }
3642
5347
  historicalRuns.reverse(); // Oldest first for charts
3643
5348
  console.log(
3644
5349
  chalk.green(
3645
- `Loaded ${historicalRuns.length} historical run(s) for trend analysis.`
3646
- )
5350
+ `Loaded ${historicalRuns.length} historical run(s) for trend analysis.`,
5351
+ ),
3647
5352
  );
3648
5353
  } catch (error) {
3649
5354
  if (error.code === "ENOENT") {
3650
5355
  console.warn(
3651
5356
  chalk.yellow(
3652
- `History directory '${historyDir}' not found. No historical trends will be displayed.`
3653
- )
5357
+ `History directory '${historyDir}' not found. No historical trends will be displayed.`,
5358
+ ),
3654
5359
  );
3655
5360
  } else {
3656
5361
  console.warn(
3657
5362
  chalk.yellow(
3658
- `Error loading historical data from '${historyDir}': ${error.message}`
3659
- )
5363
+ `Error loading historical data from '${historyDir}': ${error.message}`,
5364
+ ),
3660
5365
  );
3661
5366
  }
3662
5367
  }
@@ -3679,6 +5384,7 @@ async function main() {
3679
5384
  passed: histRunReport.run.passed,
3680
5385
  failed: histRunReport.run.failed,
3681
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),
3682
5388
  });
3683
5389
 
3684
5390
  if (histRunReport.results && Array.isArray(histRunReport.results)) {
@@ -3687,15 +5393,15 @@ async function main() {
3687
5393
  (test) => ({
3688
5394
  testName: test.name,
3689
5395
  duration: test.duration,
3690
- status: test.status,
5396
+ status: test.final_status || test.status,
3691
5397
  timestamp: new Date(test.startTime),
3692
- })
5398
+ }),
3693
5399
  );
3694
5400
  }
3695
5401
  }
3696
5402
  });
3697
5403
  trendData.overall.sort(
3698
- (a, b) => a.timestamp.getTime() - b.timestamp.getTime()
5404
+ (a, b) => a.timestamp.getTime() - b.timestamp.getTime(),
3699
5405
  );
3700
5406
  }
3701
5407
 
@@ -3705,8 +5411,8 @@ async function main() {
3705
5411
  await fs.writeFile(reportHtmlPath, htmlContent, "utf-8");
3706
5412
  console.log(
3707
5413
  chalk.green.bold(
3708
- `🎉 Pulse report generated successfully at: ${reportHtmlPath}`
3709
- )
5414
+ `Pulse report generated successfully at: ${reportHtmlPath}`,
5415
+ ),
3710
5416
  );
3711
5417
  console.log(chalk.gray(`(You can open this file in your browser)`));
3712
5418
  } catch (error) {
@@ -3717,7 +5423,7 @@ async function main() {
3717
5423
  }
3718
5424
  main().catch((err) => {
3719
5425
  console.error(
3720
- chalk.red.bold(`Unhandled error during script execution: ${err.message}`)
5426
+ chalk.red.bold(`Unhandled error during script execution: ${err.message}`),
3721
5427
  );
3722
5428
  console.error(err.stack);
3723
5429
  process.exit(1);