@arghajit/dummy 0.1.0-beta-28 → 0.1.0-beta-29

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,7 +6,11 @@ import path from "path";
6
6
  import { fork } from "child_process";
7
7
  import { fileURLToPath } from "url";
8
8
 
9
- // Use dynamic import for chalk as it's ESM only
9
+ /**
10
+ * Dynamically imports the 'chalk' library for terminal string styling.
11
+ * This is necessary because chalk is an ESM-only module.
12
+ * If the import fails, a fallback object with plain console log functions is used.
13
+ */
10
14
  let chalk;
11
15
  try {
12
16
  chalk = (await import("chalk")).default;
@@ -22,12 +26,30 @@ try {
22
26
  };
23
27
  }
24
28
 
25
- // Default configuration
29
+ /**
30
+ * @constant {string} DEFAULT_OUTPUT_DIR
31
+ * The default directory where the report will be generated.
32
+ */
26
33
  const DEFAULT_OUTPUT_DIR = "pulse-report";
34
+
35
+ /**
36
+ * @constant {string} DEFAULT_JSON_FILE
37
+ * The default name for the JSON file containing the test data.
38
+ */
27
39
  const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
40
+
41
+ /**
42
+ * @constant {string} DEFAULT_HTML_FILE
43
+ * The default name for the generated HTML report file.
44
+ */
28
45
  const DEFAULT_HTML_FILE = "playwright-pulse-static-report.html";
29
46
 
30
47
  // Helper functions
48
+ /**
49
+ * Converts a string with ANSI escape codes to an HTML string with inline styles.
50
+ * @param {string} text The text with ANSI codes.
51
+ * @returns {string} The converted HTML string.
52
+ */
31
53
  export function ansiToHtml(text) {
32
54
  if (!text) {
33
55
  return "";
@@ -141,6 +163,11 @@ export function ansiToHtml(text) {
141
163
  }
142
164
  return html;
143
165
  }
166
+ /**
167
+ * Sanitizes an HTML string by replacing special characters with their corresponding HTML entities.
168
+ * @param {string} str The HTML string to sanitize.
169
+ * @returns {string} The sanitized HTML string.
170
+ */
144
171
  function sanitizeHTML(str) {
145
172
  if (str === null || str === undefined) return "";
146
173
  return String(str).replace(
@@ -149,28 +176,52 @@ function sanitizeHTML(str) {
149
176
  ({ "&": "&", "<": "<", ">": ">", '"': '"', "'": "'" }[match] || match)
150
177
  );
151
178
  }
179
+ /**
180
+ * Capitalizes the first letter of a string and converts the rest to lowercase.
181
+ * @param {string} str The string to capitalize.
182
+ * @returns {string} The capitalized string.
183
+ */
152
184
  function capitalize(str) {
153
185
  if (!str) return "";
154
186
  return str[0].toUpperCase() + str.slice(1).toLowerCase();
155
187
  }
188
+ /**
189
+ * Formats a Playwright error object or message into an HTML string.
190
+ * @param {Error|string} error The error object or message string.
191
+ * @returns {string} The formatted HTML error string.
192
+ */
156
193
  function formatPlaywrightError(error) {
157
194
  const commandOutput = ansiToHtml(error || error.message);
158
195
  return convertPlaywrightErrorToHTML(commandOutput);
159
196
  }
197
+ /**
198
+ * Converts a string containing Playwright-style error formatting to HTML.
199
+ * @param {string} str The error string.
200
+ * @returns {string} The HTML-formatted error string.
201
+ */
160
202
  function convertPlaywrightErrorToHTML(str) {
161
- if (!str) return "";
162
- return str
163
- .replace(/^(\s+)/gm, (match) =>
164
- match.replace(/ /g, " ").replace(/\t/g, " ")
165
- )
166
- .replace(/<red>/g, '<span style="color: red;">')
167
- .replace(/<green>/g, '<span style="color: green;">')
168
- .replace(/<dim>/g, '<span style="opacity: 0.6;">')
169
- .replace(/<intensity>/g, '<span style="font-weight: bold;">')
170
- .replace(/<\/color>/g, "</span>")
171
- .replace(/<\/intensity>/g, "</span>")
172
- .replace(/\n/g, "<br>");
203
+ if (!str) return "";
204
+ return str
205
+ .replace(/^(\s+)/gm, (match) =>
206
+ match.replace(/ /g, " ").replace(/\t/g, " ")
207
+ )
208
+ .replace(/<red>/g, '<span style="color: red;">')
209
+ .replace(/<green>/g, '<span style="color: green;">')
210
+ .replace(/<dim>/g, '<span style="opacity: 0.6;">')
211
+ .replace(/<intensity>/g, '<span style="font-weight: bold;">')
212
+ .replace(/<\/color>/g, "</span>")
213
+ .replace(/<\/intensity>/g, "</span>")
214
+ .replace(/\n/g, "<br>");
173
215
  }
216
+ /**
217
+ * Formats a duration in milliseconds into a human-readable string (e.g., '1h 2m 3s', '4.5s').
218
+ * @param {number} ms The duration in milliseconds.
219
+ * @param {object} [options={}] Formatting options.
220
+ * @param {number} [options.precision=1] The number of decimal places for seconds.
221
+ * @param {string} [options.invalidInputReturn="N/A"] The string to return for invalid input.
222
+ * @param {string|null} [options.defaultForNullUndefinedNegative=null] The value for null, undefined, or negative inputs.
223
+ * @returns {string} The formatted duration string.
224
+ */
174
225
  function formatDuration(ms, options = {}) {
175
226
  const {
176
227
  precision = 1,
@@ -241,6 +292,12 @@ function formatDuration(ms, options = {}) {
241
292
  return parts.join(" ");
242
293
  }
243
294
  }
295
+ /**
296
+ * Generates HTML and JavaScript for a Highcharts line chart to display test result trends over multiple runs.
297
+ * @param {object} trendData The trend data.
298
+ * @param {Array<object>} trendData.overall An array of run objects with test statistics.
299
+ * @returns {string} The HTML string for the test trends chart.
300
+ */
244
301
  function generateTestTrendsChart(trendData) {
245
302
  if (!trendData || !trendData.overall || trendData.overall.length === 0) {
246
303
  return '<div class="no-data">No overall trend data available for test counts.</div>';
@@ -336,6 +393,12 @@ function generateTestTrendsChart(trendData) {
336
393
  </script>
337
394
  `;
338
395
  }
396
+ /**
397
+ * Generates HTML and JavaScript for a Highcharts area chart to display test duration trends.
398
+ * @param {object} trendData Data for duration trends.
399
+ * @param {Array<object>} trendData.overall Array of objects, each representing a test run with a duration.
400
+ * @returns {string} The HTML string for the duration trend chart.
401
+ */
339
402
  function generateDurationTrendChart(trendData) {
340
403
  if (!trendData || !trendData.overall || trendData.overall.length === 0) {
341
404
  return '<div class="no-data">No overall trend data available for durations.</div>';
@@ -420,6 +483,11 @@ function generateDurationTrendChart(trendData) {
420
483
  </script>
421
484
  `;
422
485
  }
486
+ /**
487
+ * Formats a date string or Date object into a more readable format (e.g., "MM/DD/YY HH:MM").
488
+ * @param {string|Date} dateStrOrDate The date string or Date object to format.
489
+ * @returns {string} The formatted date string, or "N/A" for invalid dates.
490
+ */
423
491
  function formatDate(dateStrOrDate) {
424
492
  if (!dateStrOrDate) return "N/A";
425
493
  try {
@@ -438,6 +506,12 @@ function formatDate(dateStrOrDate) {
438
506
  return "Invalid Date Format";
439
507
  }
440
508
  }
509
+ /**
510
+ * Generates a small area chart showing the duration history of a single test across multiple runs.
511
+ * The status of each run is indicated by the color of the marker.
512
+ * @param {Array<object>} history An array of run objects, each with status and duration.
513
+ * @returns {string} The HTML string for the test history chart.
514
+ */
441
515
  function generateTestHistoryChart(history) {
442
516
  if (!history || history.length === 0)
443
517
  return '<div class="no-data-chart">No data for chart</div>';
@@ -548,6 +622,13 @@ function generateTestHistoryChart(history) {
548
622
  </script>
549
623
  `;
550
624
  }
625
+ /**
626
+ * Generates a Highcharts pie chart to visualize the distribution of test statuses.
627
+ * @param {Array<object>} data The data for the pie chart, with each object having a 'label' and 'value'.
628
+ * @param {number} [chartWidth=300] The width of the chart.
629
+ * @param {number} [chartHeight=300] The height of the chart.
630
+ * @returns {string} The HTML string for the pie chart.
631
+ */
551
632
  function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
552
633
  const total = data.reduce((sum, d) => sum + d.value, 0);
553
634
  if (total === 0) {
@@ -677,6 +758,12 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
677
758
  </div>
678
759
  `;
679
760
  }
761
+ /**
762
+ * Generates an HTML dashboard to display environment details.
763
+ * @param {object} environment The environment information.
764
+ * @param {number} [dashboardHeight=600] The height of the dashboard.
765
+ * @returns {string} The HTML string for the environment dashboard.
766
+ */
680
767
  function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
681
768
  // Format memory for display
682
769
  const formattedMemory = environment.memory.replace(/(\d+\.\d{2})GB/, "$1 GB");
@@ -692,176 +779,176 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
692
779
  return `
693
780
  <div class="environment-dashboard-wrapper" id="${dashboardId}">
694
781
  <style>
695
- .environment-dashboard-wrapper *,
696
- .environment-dashboard-wrapper *::before,
697
- .environment-dashboard-wrapper *::after {
698
- box-sizing: border-box;
699
- }
782
+ .environment-dashboard-wrapper *,
783
+ .environment-dashboard-wrapper *::before,
784
+ .environment-dashboard-wrapper *::after {
785
+ box-sizing: border-box;
786
+ }
700
787
 
701
- .environment-dashboard-wrapper {
702
- --primary-color: #007bff;
703
- --primary-light-color: #e6f2ff;
704
- --secondary-color: #6c757d;
705
- --success-color: #28a745;
706
- --success-light-color: #eaf6ec;
707
- --warning-color: #ffc107;
708
- --warning-light-color: #fff9e6;
709
- --danger-color: #dc3545;
710
-
711
- --background-color: #ffffff;
712
- --card-background-color: #ffffff;
713
- --text-color: #212529;
714
- --text-color-secondary: #6c757d;
715
- --border-color: #dee2e6;
716
- --border-light-color: #f1f3f5;
717
- --icon-color: #495057;
718
- --chip-background: #e9ecef;
719
- --chip-text: #495057;
720
- --shadow-color: rgba(0, 0, 0, 0.075);
721
-
722
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
723
- background-color: var(--background-color);
724
- border-radius: 12px;
725
- box-shadow: 0 6px 12px var(--shadow-color);
726
- padding: 24px;
727
- color: var(--text-color);
728
- display: grid;
729
- grid-template-columns: 1fr 1fr;
730
- grid-template-rows: auto 1fr;
731
- gap: 20px;
732
- font-size: 14px;
733
- }
734
-
735
- .env-dashboard-header {
736
- grid-column: 1 / -1;
737
- display: flex;
738
- justify-content: space-between;
739
- align-items: center;
740
- border-bottom: 1px solid var(--border-color);
741
- padding-bottom: 16px;
742
- margin-bottom: 8px;
743
- }
744
-
745
- .env-dashboard-title {
746
- font-size: 1.5rem;
747
- font-weight: 600;
748
- color: var(--text-color);
749
- margin: 0;
750
- }
751
-
752
- .env-dashboard-subtitle {
753
- font-size: 0.875rem;
754
- color: var(--text-color-secondary);
755
- margin-top: 4px;
756
- }
757
-
758
- .env-card {
759
- background-color: var(--card-background-color);
760
- border-radius: 8px;
761
- padding: ${cardContentPadding}px;
762
- box-shadow: 0 3px 6px var(--shadow-color);
763
- height: ${cardHeight}px;
764
- display: flex;
765
- flex-direction: column;
766
- overflow: hidden;
767
- }
768
-
769
- .env-card-header {
770
- font-weight: 600;
771
- font-size: 1rem;
772
- margin-bottom: 12px;
773
- color: var(--text-color);
774
- display: flex;
775
- align-items: center;
776
- padding-bottom: 8px;
777
- border-bottom: 1px solid var(--border-light-color);
778
- }
779
-
780
- .env-card-header svg {
781
- margin-right: 10px;
782
- width: 18px;
783
- height: 18px;
784
- fill: var(--icon-color);
785
- }
788
+ .environment-dashboard-wrapper {
789
+ --primary-color: #4a9eff;
790
+ --primary-light-color: #1a2332;
791
+ --secondary-color: #9ca3af;
792
+ --success-color: #34d399;
793
+ --success-light-color: #1a2e23;
794
+ --warning-color: #fbbf24;
795
+ --warning-light-color: #2d2a1a;
796
+ --danger-color: #f87171;
797
+
798
+ --background-color: #1f2937;
799
+ --card-background-color: #374151;
800
+ --text-color: #f9fafb;
801
+ --text-color-secondary: #d1d5db;
802
+ --border-color: #4b5563;
803
+ --border-light-color: #374151;
804
+ --icon-color: #d1d5db;
805
+ --chip-background: #4b5563;
806
+ --chip-text: #f9fafb;
807
+ --shadow-color: rgba(0, 0, 0, 0.3);
808
+
809
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
810
+ background-color: var(--background-color);
811
+ border-radius: 12px;
812
+ box-shadow: 0 6px 12px var(--shadow-color);
813
+ padding: 24px;
814
+ color: var(--text-color);
815
+ display: grid;
816
+ grid-template-columns: 1fr 1fr;
817
+ grid-template-rows: auto 1fr;
818
+ gap: 20px;
819
+ font-size: 14px;
820
+ }
786
821
 
787
- .env-card-content {
788
- flex-grow: 1;
789
- overflow-y: auto;
790
- padding-right: 5px;
791
- }
792
-
793
- .env-detail-row {
794
- display: flex;
795
- justify-content: space-between;
796
- align-items: center;
797
- padding: 10px 0;
798
- border-bottom: 1px solid var(--border-light-color);
799
- font-size: 0.875rem;
800
- }
801
-
802
- .env-detail-row:last-child {
803
- border-bottom: none;
804
- }
805
-
806
- .env-detail-label {
807
- color: var(--text-color-secondary);
808
- font-weight: 500;
809
- margin-right: 10px;
810
- }
811
-
812
- .env-detail-value {
813
- color: var(--text-color);
814
- font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
815
- text-align: right;
816
- word-break: break-all;
817
- }
818
-
819
- .env-chip {
820
- display: inline-block;
821
- padding: 4px 10px;
822
- border-radius: 16px;
823
- font-size: 0.75rem;
824
- font-weight: 500;
825
- line-height: 1.2;
826
- background-color: var(--chip-background);
827
- color: var(--chip-text);
828
- }
829
-
830
- .env-chip-primary {
831
- background-color: var(--primary-light-color);
832
- color: var(--primary-color);
833
- }
834
-
835
- .env-chip-success {
836
- background-color: var(--success-light-color);
837
- color: var(--success-color);
838
- }
839
-
840
- .env-chip-warning {
841
- background-color: var(--warning-light-color);
842
- color: var(--warning-color);
843
- }
844
-
845
- .env-cpu-cores {
846
- display: flex;
847
- align-items: center;
848
- gap: 6px;
849
- }
850
-
851
- .env-core-indicator {
852
- width: 12px;
853
- height: 12px;
854
- border-radius: 50%;
855
- background-color: var(--success-color);
856
- border: 1px solid rgba(0,0,0,0.1);
857
- }
858
-
859
- .env-core-indicator.inactive {
860
- background-color: var(--border-light-color);
861
- opacity: 0.7;
862
- border-color: var(--border-color);
863
- }
864
- </style>
822
+ .env-dashboard-header {
823
+ grid-column: 1 / -1;
824
+ display: flex;
825
+ justify-content: space-between;
826
+ align-items: center;
827
+ border-bottom: 1px solid var(--border-color);
828
+ padding-bottom: 16px;
829
+ margin-bottom: 8px;
830
+ }
831
+
832
+ .env-dashboard-title {
833
+ font-size: 1.5rem;
834
+ font-weight: 600;
835
+ color: var(--text-color);
836
+ margin: 0;
837
+ }
838
+
839
+ .env-dashboard-subtitle {
840
+ font-size: 0.875rem;
841
+ color: var(--text-color-secondary);
842
+ margin-top: 4px;
843
+ }
844
+
845
+ .env-card {
846
+ background-color: var(--card-background-color);
847
+ border-radius: 8px;
848
+ padding: ${cardContentPadding}px;
849
+ box-shadow: 0 3px 6px var(--shadow-color);
850
+ height: ${cardHeight}px;
851
+ display: flex;
852
+ flex-direction: column;
853
+ overflow: hidden;
854
+ }
855
+
856
+ .env-card-header {
857
+ font-weight: 600;
858
+ font-size: 1rem;
859
+ margin-bottom: 12px;
860
+ color: var(--text-color);
861
+ display: flex;
862
+ align-items: center;
863
+ padding-bottom: 8px;
864
+ border-bottom: 1px solid var(--border-light-color);
865
+ }
866
+
867
+ .env-card-header svg {
868
+ margin-right: 10px;
869
+ width: 18px;
870
+ height: 18px;
871
+ fill: var(--icon-color);
872
+ }
873
+
874
+ .env-card-content {
875
+ flex-grow: 1;
876
+ overflow-y: auto;
877
+ padding-right: 5px;
878
+ }
879
+
880
+ .env-detail-row {
881
+ display: flex;
882
+ justify-content: space-between;
883
+ align-items: center;
884
+ padding: 10px 0;
885
+ border-bottom: 1px solid var(--border-light-color);
886
+ font-size: 0.875rem;
887
+ }
888
+
889
+ .env-detail-row:last-child {
890
+ border-bottom: none;
891
+ }
892
+
893
+ .env-detail-label {
894
+ color: var(--text-color-secondary);
895
+ font-weight: 500;
896
+ margin-right: 10px;
897
+ }
898
+
899
+ .env-detail-value {
900
+ color: var(--text-color);
901
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
902
+ text-align: right;
903
+ word-break: break-all;
904
+ }
905
+
906
+ .env-chip {
907
+ display: inline-block;
908
+ padding: 4px 10px;
909
+ border-radius: 16px;
910
+ font-size: 0.75rem;
911
+ font-weight: 500;
912
+ line-height: 1.2;
913
+ background-color: var(--chip-background);
914
+ color: var(--chip-text);
915
+ }
916
+
917
+ .env-chip-primary {
918
+ background-color: var(--primary-light-color);
919
+ color: var(--primary-color);
920
+ }
921
+
922
+ .env-chip-success {
923
+ background-color: var(--success-light-color);
924
+ color: var(--success-color);
925
+ }
926
+
927
+ .env-chip-warning {
928
+ background-color: var(--warning-light-color);
929
+ color: var(--warning-color);
930
+ }
931
+
932
+ .env-cpu-cores {
933
+ display: flex;
934
+ align-items: center;
935
+ gap: 6px;
936
+ }
937
+
938
+ .env-core-indicator {
939
+ width: 12px;
940
+ height: 12px;
941
+ border-radius: 50%;
942
+ background-color: var(--success-color);
943
+ border: 1px solid rgba(255,255,255,0.2);
944
+ }
945
+
946
+ .env-core-indicator.inactive {
947
+ background-color: var(--border-light-color);
948
+ opacity: 0.7;
949
+ border-color: var(--border-color);
950
+ }
951
+ </style>
865
952
 
866
953
  <div class="env-dashboard-header">
867
954
  <div>
@@ -1006,6 +1093,11 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
1006
1093
  </div>
1007
1094
  `;
1008
1095
  }
1096
+ /**
1097
+ * Generates a Highcharts bar chart to visualize the distribution of test results across different workers.
1098
+ * @param {Array<object>} results The test results data.
1099
+ * @returns {string} The HTML string for the worker distribution chart and its associated modal.
1100
+ */
1009
1101
  function generateWorkerDistributionChart(results) {
1010
1102
  if (!results || results.length === 0) {
1011
1103
  return '<div class="no-data">No test results data available to display worker distribution.</div>';
@@ -1082,45 +1174,46 @@ function generateWorkerDistributionChart(results) {
1082
1174
 
1083
1175
  // The HTML now includes the chart container, the modal, and styles for the modal
1084
1176
  return `
1085
- <style>
1086
- .worker-modal-overlay {
1087
- position: fixed; z-index: 1050; left: 0; top: 0; width: 100%; height: 100%;
1088
- overflow: auto; background-color: rgba(0,0,0,0.6);
1089
- display: none; align-items: center; justify-content: center;
1090
- }
1091
- .worker-modal-content {
1092
- background-color: #3d4043;
1093
- color: var(--card-background-color);
1094
- margin: auto; padding: 20px; border: 1px solid var(--border-color, #888);
1095
- width: 80%; max-width: 700px; border-radius: 8px;
1096
- position: relative; box-shadow: 0 5px 15px rgba(0,0,0,0.5);
1097
- }
1098
- .worker-modal-close {
1099
- position: absolute; top: 10px; right: 20px;
1100
- font-size: 28px; font-weight: bold; cursor: pointer;
1101
- line-height: 1;
1102
- }
1103
- .worker-modal-close:hover, .worker-modal-close:focus {
1104
- color: var(--text-color, #000);
1105
- }
1106
- #worker-modal-body-${chartId} ul {
1107
- list-style-type: none; padding-left: 0; margin-top: 15px; max-height: 45vh; overflow-y: auto;
1108
- }
1109
- #worker-modal-body-${chartId} li {
1110
- padding: 8px 5px; border-bottom: 1px solid var(--border-color, #eee);
1111
- font-size: 0.9em;
1112
- }
1113
- #worker-modal-body-${chartId} li:last-child {
1114
- border-bottom: none;
1115
- }
1116
- #worker-modal-body-${chartId} li > span {
1117
- display: inline-block;
1118
- width: 70px;
1119
- font-weight: bold;
1120
- text-align: right;
1121
- margin-right: 10px;
1122
- }
1123
- </style>
1177
+ <style>
1178
+ .worker-modal-overlay {
1179
+ position: fixed; z-index: 10000; left: 0; top: 0; width: 100%; height: 100%;
1180
+ overflow: auto; background-color: rgba(0,0,0,0.8);
1181
+ display: none; align-items: center; justify-content: center;
1182
+ }
1183
+ .worker-modal-content {
1184
+ background-color: #1f2937;
1185
+ color: #f9fafb;
1186
+ margin: auto; padding: 20px; border: 1px solid #4b5563;
1187
+ width: 80%; max-width: 700px; border-radius: 8px;
1188
+ position: relative; box-shadow: 0 5px 15px rgba(0,0,0,0.7);
1189
+ }
1190
+ .worker-modal-close {
1191
+ position: absolute; top: 10px; right: 20px;
1192
+ font-size: 28px; font-weight: bold; cursor: pointer;
1193
+ line-height: 1; color: #d1d5db;
1194
+ }
1195
+ .worker-modal-close:hover, .worker-modal-close:focus {
1196
+ color: #f9fafb;
1197
+ }
1198
+ #worker-modal-body-${chartId} ul {
1199
+ list-style-type: none; padding-left: 0; margin-top: 15px; max-height: 45vh; overflow-y: auto;
1200
+ }
1201
+ #worker-modal-body-${chartId} li {
1202
+ padding: 8px 5px; border-bottom: 1px solid #4b5563;
1203
+ font-size: 0.9em; color: #f9fafb;
1204
+ }
1205
+ #worker-modal-body-${chartId} li:last-child {
1206
+ border-bottom: none;
1207
+ }
1208
+ #worker-modal-body-${chartId} li > span {
1209
+ display: inline-block;
1210
+ width: 70px;
1211
+ font-weight: bold;
1212
+ text-align: right;
1213
+ margin-right: 10px;
1214
+ color: #d1d5db;
1215
+ }
1216
+ </style>
1124
1217
 
1125
1218
  <div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}" style="min-height: 350px;">
1126
1219
  <div class="no-data">Loading Worker Distribution Chart...</div>
@@ -1147,42 +1240,63 @@ function generateWorkerDistributionChart(results) {
1147
1240
  const modalTitle = document.getElementById('worker-modal-title-${chartId}');
1148
1241
  const modalBody = document.getElementById('worker-modal-body-${chartId}');
1149
1242
  const closeModalBtn = modal.querySelector('.worker-modal-close');
1243
+ if (modal && modal.parentElement !== document.body) {
1244
+ document.body.appendChild(modal);
1245
+ }
1246
+
1247
+ // Lightweight HTML escaper for client-side use
1248
+ function __escHtml(s){return String(s==null?'':s).replace(/[&<>\"]/g,function(ch){return ch==='&'?'&amp;':ch==='<'?'&lt;':ch==='>'?'&gt;':'&quot;';});}
1150
1249
 
1151
1250
  window.${modalJsNamespace}.open = function(worker) {
1152
1251
  if (!worker) return;
1153
- modalTitle.textContent = 'Test Details for ' + worker.name;
1154
-
1155
- let testListHtml = '<ul>';
1156
- if (worker.tests && worker.tests.length > 0) {
1157
- worker.tests.forEach(test => {
1158
- let color = 'inherit';
1159
- if (test.status === 'passed') color = 'var(--success-color)';
1160
- else if (test.status === 'failed') color = 'var(--danger-color)';
1161
- else if (test.status === 'skipped') color = 'var(--warning-color)';
1162
-
1163
- const escapedName = test.name.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
1164
- testListHtml += \`<li style="color: \${color};"><span style="color: \${color}">[\${test.status.toUpperCase()}]</span> \${escapedName}</li>\`;
1165
- });
1166
- } else {
1167
- testListHtml += '<li>No detailed test data available for this worker.</li>';
1168
- }
1169
- testListHtml += '</ul>';
1252
+ try {
1253
+ modalTitle.textContent = 'Test Details for ' + worker.name;
1254
+
1255
+ let testListHtml = '<ul>';
1256
+ if (worker.tests && worker.tests.length > 0) {
1257
+ worker.tests.forEach(test => {
1258
+ let color = 'inherit';
1259
+ if (test.status === 'passed') color = 'var(--success-color)';
1260
+ else if (test.status === 'failed') color = 'var(--danger-color)';
1261
+ else if (test.status === 'skipped') color = 'var(--warning-color)';
1262
+
1263
+ const safeName = __escHtml(test.name);
1264
+ testListHtml += '<li style="color: ' + color + ';"><span style="color: ' + color + '">[' + String(test.status).toUpperCase() + ']</span> ' + safeName + '</li>';
1265
+ });
1266
+ } else {
1267
+ testListHtml += '<li>No detailed test data available for this worker.</li>';
1268
+ }
1269
+ testListHtml += '</ul>';
1170
1270
 
1171
- modalBody.innerHTML = testListHtml;
1172
- modal.style.display = 'flex';
1271
+ modalBody.innerHTML = testListHtml;
1272
+ if (typeof openModal === 'function') openModal(); else modal.style.display = 'flex';
1273
+ } catch (err) {
1274
+ console.error('Failed to open worker modal:', err);
1275
+ }
1173
1276
  };
1174
1277
 
1175
1278
  const closeModal = function() {
1176
1279
  modal.style.display = 'none';
1280
+ try { document.body.style.overflow = ''; } catch (_) {}
1177
1281
  };
1178
1282
 
1179
- closeModalBtn.onclick = closeModal;
1180
- modal.onclick = function(event) {
1181
- // Close if clicked on the dark overlay background
1182
- if (event.target == modal) {
1283
+ const openModal = function() {
1284
+ modal.style.display = 'flex';
1285
+ try { document.body.style.overflow = 'hidden'; } catch (_) {}
1286
+ };
1287
+
1288
+ if (closeModalBtn) closeModalBtn.onclick = closeModal;
1289
+ modal.addEventListener('click', function(event) {
1290
+ if (event.target === modal) {
1183
1291
  closeModal();
1184
1292
  }
1185
- };
1293
+ });
1294
+
1295
+ document.addEventListener('keydown', function escHandler(e) {
1296
+ if (modal.style.display === 'flex' && (e.key === 'Escape' || e.key === 'Esc')) {
1297
+ closeModal();
1298
+ }
1299
+ });
1186
1300
 
1187
1301
 
1188
1302
  // --- Highcharts Setup ---
@@ -1242,6 +1356,10 @@ function generateWorkerDistributionChart(results) {
1242
1356
  </script>
1243
1357
  `;
1244
1358
  }
1359
+ /**
1360
+ * A tooltip providing information about why worker -1 is special in Playwright.
1361
+ * @type {string}
1362
+ */
1245
1363
  const infoTooltip = `
1246
1364
  <span class="info-tooltip" style="display: inline-block; margin-left: 8px;">
1247
1365
  <span class="info-icon"
@@ -1260,6 +1378,11 @@ const infoTooltip = `
1260
1378
  }
1261
1379
  </script>
1262
1380
  `;
1381
+ /**
1382
+ * Generates the HTML content for the test history section.
1383
+ * @param {object} trendData - The historical trend data.
1384
+ * @returns {string} The HTML string for the test history content.
1385
+ */
1263
1386
  function generateTestHistoryContent(trendData) {
1264
1387
  if (
1265
1388
  !trendData ||
@@ -1379,6 +1502,11 @@ function generateTestHistoryContent(trendData) {
1379
1502
  </div>
1380
1503
  `;
1381
1504
  }
1505
+ /**
1506
+ * Gets the CSS class for a given test status.
1507
+ * @param {string} status - The test status.
1508
+ * @returns {string} The CSS class for the status.
1509
+ */
1382
1510
  function getStatusClass(status) {
1383
1511
  switch (String(status).toLowerCase()) {
1384
1512
  case "passed":
@@ -1391,6 +1519,11 @@ function getStatusClass(status) {
1391
1519
  return "status-unknown";
1392
1520
  }
1393
1521
  }
1522
+ /**
1523
+ * Gets the icon for a given test status.
1524
+ * @param {string} status - The test status.
1525
+ * @returns {string} The icon for the status.
1526
+ */
1394
1527
  function getStatusIcon(status) {
1395
1528
  switch (String(status).toLowerCase()) {
1396
1529
  case "passed":
@@ -1403,6 +1536,11 @@ function getStatusIcon(status) {
1403
1536
  return "❓";
1404
1537
  }
1405
1538
  }
1539
+ /**
1540
+ * Processes test results to extract suite data.
1541
+ * @param {Array<object>} results - The test results.
1542
+ * @returns {Array<object>} An array of suite data objects.
1543
+ */
1406
1544
  function getSuitesData(results) {
1407
1545
  const suitesMap = new Map();
1408
1546
  if (!results || results.length === 0) return [];
@@ -1451,6 +1589,16 @@ function getSuitesData(results) {
1451
1589
  });
1452
1590
  return Array.from(suitesMap.values());
1453
1591
  }
1592
+ /**
1593
+ * Returns an icon for a given content type.
1594
+ * @param {string} contentType - The content type of the file.
1595
+ * @returns {string} The icon for the content type.
1596
+ */
1597
+ /**
1598
+ * Returns an icon for a given content type.
1599
+ * @param {string} contentType - The content type of the file.
1600
+ * @returns {string} The icon for the content type.
1601
+ */
1454
1602
  function getAttachmentIcon(contentType) {
1455
1603
  if (!contentType) return "📎"; // Handle undefined/null
1456
1604
 
@@ -1464,6 +1612,16 @@ function getAttachmentIcon(contentType) {
1464
1612
  if (normalizedType.startsWith("text/")) return "📝";
1465
1613
  return "📎";
1466
1614
  }
1615
+ /**
1616
+ * Generates the HTML for the suites widget.
1617
+ * @param {Array} suitesData - The data for the suites.
1618
+ * @returns {string} The HTML for the suites widget.
1619
+ */
1620
+ /**
1621
+ * Generates the HTML for the suites widget.
1622
+ * @param {Array} suitesData - The data for the suites.
1623
+ * @returns {string} The HTML for the suites widget.
1624
+ */
1467
1625
  function generateSuitesWidget(suitesData) {
1468
1626
  if (!suitesData || suitesData.length === 0) {
1469
1627
  return `<div class="suites-widget"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
@@ -1520,8 +1678,15 @@ function generateSuitesWidget(suitesData) {
1520
1678
  </div>
1521
1679
  </div>`;
1522
1680
  }
1681
+ /**
1682
+ * Generates the HTML for the AI failure analyzer tab.
1683
+ * @param {Array} results - The results of the test run.
1684
+ * @returns {string} The HTML for the AI failure analyzer tab.
1685
+ */
1523
1686
  function generateAIFailureAnalyzerTab(results) {
1524
- const failedTests = (results || []).filter(test => test.status === 'failed');
1687
+ const failedTests = (results || []).filter(
1688
+ (test) => test.status === "failed"
1689
+ );
1525
1690
 
1526
1691
  if (failedTests.length === 0) {
1527
1692
  return `
@@ -1531,7 +1696,7 @@ function generateAIFailureAnalyzerTab(results) {
1531
1696
  }
1532
1697
 
1533
1698
  // btoa is not available in Node.js environment, so we define a simple polyfill for it.
1534
- const btoa = (str) => Buffer.from(str).toString('base64');
1699
+ const btoa = (str) => Buffer.from(str).toString("base64");
1535
1700
 
1536
1701
  return `
1537
1702
  <h2 class="tab-main-title">AI Failure Analysis</h2>
@@ -1541,11 +1706,16 @@ function generateAIFailureAnalyzerTab(results) {
1541
1706
  <span class="stat-label">Failed Tests</span>
1542
1707
  </div>
1543
1708
  <div class="stat-item">
1544
- <span class="stat-number">${new Set(failedTests.map(t => t.browser)).size}</span>
1709
+ <span class="stat-number">${
1710
+ new Set(failedTests.map((t) => t.browser)).size
1711
+ }</span>
1545
1712
  <span class="stat-label">Browsers</span>
1546
1713
  </div>
1547
1714
  <div class="stat-item">
1548
- <span class="stat-number">${(Math.round(failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) / 1000))}s</span>
1715
+ <span class="stat-number">${Math.round(
1716
+ failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) /
1717
+ 1000
1718
+ )}s</span>
1549
1719
  <span class="stat-label">Total Duration</span>
1550
1720
  </div>
1551
1721
  </div>
@@ -1554,20 +1724,28 @@ function generateAIFailureAnalyzerTab(results) {
1554
1724
  </p>
1555
1725
 
1556
1726
  <div class="compact-failure-list">
1557
- ${failedTests.map(test => {
1558
- const testTitle = test.name.split(" > ").pop() || "Unnamed Test";
1559
- const testJson = btoa(JSON.stringify(test)); // Base64 encode the test object
1560
- const truncatedError = (test.errorMessage || "No error message").slice(0, 150) +
1561
- (test.errorMessage && test.errorMessage.length > 150 ? "..." : "");
1562
-
1563
- return `
1727
+ ${failedTests
1728
+ .map((test) => {
1729
+ const testTitle = test.name.split(" > ").pop() || "Unnamed Test";
1730
+ const testJson = btoa(JSON.stringify(test)); // Base64 encode the test object
1731
+ const truncatedError =
1732
+ (test.errorMessage || "No error message").slice(0, 150) +
1733
+ (test.errorMessage && test.errorMessage.length > 150 ? "..." : "");
1734
+
1735
+ return `
1564
1736
  <div class="compact-failure-item">
1565
1737
  <div class="failure-header">
1566
1738
  <div class="failure-main-info">
1567
- <h3 class="failure-title" title="${sanitizeHTML(test.name)}">${sanitizeHTML(testTitle)}</h3>
1739
+ <h3 class="failure-title" title="${sanitizeHTML(
1740
+ test.name
1741
+ )}">${sanitizeHTML(testTitle)}</h3>
1568
1742
  <div class="failure-meta">
1569
- <span class="browser-indicator">${sanitizeHTML(test.browser || 'unknown')}</span>
1570
- <span class="duration-indicator">${formatDuration(test.duration)}</span>
1743
+ <span class="browser-indicator">${sanitizeHTML(
1744
+ test.browser || "unknown"
1745
+ )}</span>
1746
+ <span class="duration-indicator">${formatDuration(
1747
+ test.duration
1748
+ )}</span>
1571
1749
  </div>
1572
1750
  </div>
1573
1751
  <button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
@@ -1575,7 +1753,9 @@ function generateAIFailureAnalyzerTab(results) {
1575
1753
  </button>
1576
1754
  </div>
1577
1755
  <div class="failure-error-preview">
1578
- <div class="error-snippet">${formatPlaywrightError(truncatedError)}</div>
1756
+ <div class="error-snippet">${formatPlaywrightError(
1757
+ truncatedError
1758
+ )}</div>
1579
1759
  <button class="expand-error-btn" onclick="toggleErrorDetails(this)">
1580
1760
  <span class="expand-text">Show Full Error</span>
1581
1761
  <span class="expand-icon">▼</span>
@@ -1583,12 +1763,15 @@ function generateAIFailureAnalyzerTab(results) {
1583
1763
  </div>
1584
1764
  <div class="full-error-details" style="display: none;">
1585
1765
  <div class="full-error-content">
1586
- ${formatPlaywrightError(test.errorMessage || "No detailed error message available")}
1766
+ ${formatPlaywrightError(
1767
+ test.errorMessage || "No detailed error message available"
1768
+ )}
1587
1769
  </div>
1588
1770
  </div>
1589
1771
  </div>
1590
- `
1591
- }).join('')}
1772
+ `;
1773
+ })
1774
+ .join("")}
1592
1775
  </div>
1593
1776
 
1594
1777
  <!-- AI Fix Modal -->
@@ -1605,6 +1788,12 @@ function generateAIFailureAnalyzerTab(results) {
1605
1788
  </div>
1606
1789
  `;
1607
1790
  }
1791
+ /**
1792
+ * Generates the HTML report.
1793
+ * @param {object} reportData - The data for the report.
1794
+ * @param {object} trendData - The data for the trend chart.
1795
+ * @returns {string} The HTML report.
1796
+ */
1608
1797
  function generateHTML(reportData, trendData = null) {
1609
1798
  const { run, results } = reportData;
1610
1799
  const suitesData = getSuitesData(reportData.results || []);
@@ -1627,11 +1816,16 @@ function generateHTML(reportData, trendData = null) {
1627
1816
  ? formatDuration(runSummary.duration / runSummary.totalTests)
1628
1817
  : "0.0s";
1629
1818
 
1630
- function generateTestCasesHTML() {
1819
+ /**
1820
+ * Generates the HTML for the test cases.
1821
+ * @returns {string} The HTML for the test cases.
1822
+ */
1823
+ function generateTestCasesHTML(subset = results, baseIndex = 0) {
1631
1824
  if (!results || results.length === 0)
1632
1825
  return '<div class="no-tests">No test results found in this run.</div>';
1633
- return results
1634
- .map((test, testIndex) => {
1826
+ return subset
1827
+ .map((test, i) => {
1828
+ const testIndex = baseIndex + i;
1635
1829
  const browser = test.browser || "unknown";
1636
1830
  const testFileParts = test.name.split(" > ");
1637
1831
  const testTitle =
@@ -1669,6 +1863,42 @@ function generateHTML(reportData, trendData = null) {
1669
1863
  : ""
1670
1864
  }<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
1671
1865
  : ""
1866
+ }${
1867
+ (() => {
1868
+ if (!step.attachments || step.attachments.length === 0) return "";
1869
+ return `<div class="attachments-section"><h4>Step Attachments</h4><div class="attachments-grid">${step.attachments
1870
+ .map((attachment) => {
1871
+ try {
1872
+ const attachmentPath = path.resolve(
1873
+ DEFAULT_OUTPUT_DIR,
1874
+ attachment.path
1875
+ );
1876
+ if (!fsExistsSync(attachmentPath)) {
1877
+ return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
1878
+ attachment.name
1879
+ )}</div>`;
1880
+ }
1881
+ const attachmentBase64 = readFileSync(attachmentPath).toString("base64");
1882
+ const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
1883
+ return `<div class="attachment-item generic-attachment">
1884
+ <div class="attachment-icon">${getAttachmentIcon(attachment.contentType)}</div>
1885
+ <div class="attachment-caption">
1886
+ <span class="attachment-name" title="${sanitizeHTML(attachment.name)}">${sanitizeHTML(attachment.name)}</span>
1887
+ <span class="attachment-type">${sanitizeHTML(attachment.contentType)}</span>
1888
+ </div>
1889
+ <div class="attachment-info">
1890
+ <div class="trace-actions">
1891
+ <a href="#" data-href="${attachmentDataUri}" class="view-full lazy-load-attachment" target="_blank">View</a>
1892
+ <a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(attachment.name)}">Download</a>
1893
+ </div>
1894
+ </div>
1895
+ </div>`;
1896
+ } catch (e) {
1897
+ return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(attachment.name)}</div>`;
1898
+ }
1899
+ })
1900
+ .join("")}</div></div>`;
1901
+ })()
1672
1902
  }${
1673
1903
  hasNestedSteps
1674
1904
  ? `<div class="nested-steps">${generateStepsHTML(
@@ -1686,7 +1916,7 @@ function generateHTML(reportData, trendData = null) {
1686
1916
  test.tags || []
1687
1917
  )
1688
1918
  .join(",")
1689
- .toLowerCase()}">
1919
+ .toLowerCase()}" data-test-id="${sanitizeHTML(String(test.id || testIndex))}">
1690
1920
  <div class="test-case-header" role="button" aria-expanded="false"><div class="test-case-summary"><span class="status-badge ${getStatusClass(
1691
1921
  test.status
1692
1922
  )}">${String(
@@ -1722,6 +1952,13 @@ function generateHTML(reportData, trendData = null) {
1722
1952
  )}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
1723
1953
  : ""
1724
1954
  }
1955
+ ${
1956
+ test.snippet
1957
+ ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
1958
+ test.snippet
1959
+ )}</code></pre></div>`
1960
+ : ""
1961
+ }
1725
1962
  <h4>Steps</h4><div class="steps-list">${generateStepsHTML(
1726
1963
  test.steps
1727
1964
  )}</div>
@@ -1745,9 +1982,12 @@ function generateHTML(reportData, trendData = null) {
1745
1982
  })()}
1746
1983
  ${
1747
1984
  test.stderr && test.stderr.length > 0
1748
- ? `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log">${test.stderr
1749
- .map((line) => sanitizeHTML(line))
1750
- .join("\\n")}</pre></div>`
1985
+ ? (() => {
1986
+ const logId = `stderr-log-${test.id || testIndex}`;
1987
+ return `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre id="${logId}" class="console-log stderr-log">${test.stderr
1988
+ .map((line) => sanitizeHTML(line))
1989
+ .join("\\n")}</pre></div>`;
1990
+ })()
1751
1991
  : ""
1752
1992
  }
1753
1993
 
@@ -1772,7 +2012,7 @@ function generateHTML(reportData, trendData = null) {
1772
2012
  readFileSync(imagePath).toString("base64");
1773
2013
  return `<div class="attachment-item"><img src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" data-src="data:image/png;base64,${base64ImageData}" alt="Screenshot ${
1774
2014
  index + 1
1775
- }" class="lazy-load-image"><div class="attachment-info"><div class="trace-actions"><a href="data:image/png;base64,${base64ImageData}" target="_blank" download="screenshot-${index}.png">Download</a></div></div></div>`;
2015
+ }" class="lazy-load-image"><div class="attachment-info"><div class="trace-actions"><a href="#" data-href="data:image/png;base64,${base64ImageData}" class="lazy-load-attachment" target="_blank" download="screenshot-${index}.png">Download</a></div></div></div>`;
1776
2016
  } catch (e) {
1777
2017
  return `<div class="attachment-item error">Failed to load screenshot: ${sanitizeHTML(
1778
2018
  screenshotPath
@@ -1813,7 +2053,7 @@ function generateHTML(reportData, trendData = null) {
1813
2053
  avi: "video/x-msvideo",
1814
2054
  }[fileExtension] || "video/mp4";
1815
2055
  const videoDataUri = `data:${mimeType};base64,${videoBase64}`;
1816
- return `<div class="attachment-item video-item"><video controls preload="none" class="lazy-load-video"><source data-src="${videoDataUri}" type="${mimeType}"></video><div class="attachment-info"><div class="trace-actions"><a href="${videoDataUri}" target="_blank" download="video-${index}.${fileExtension}">Download</a></div></div></div>`;
2056
+ return `<div class="attachment-item video-item"><video controls preload="none" class="lazy-load-video"><source data-src="${videoDataUri}" type="${mimeType}"></video><div class="attachment-info"><div class="trace-actions"><a href="#" data-href="${videoDataUri}" class="lazy-load-attachment" target="_blank" download="video-${index}.${fileExtension}">Download</a></div></div></div>`;
1817
2057
  } catch (e) {
1818
2058
  return `<div class="attachment-item error">Failed to load video: ${sanitizeHTML(
1819
2059
  videoPath
@@ -1889,8 +2129,8 @@ function generateHTML(reportData, trendData = null) {
1889
2129
  </div>
1890
2130
  <div class="attachment-info">
1891
2131
  <div class="trace-actions">
1892
- <a href="${attachmentDataUri}" target="_blank" class="view-full">View</a>
1893
- <a href="${attachmentDataUri}" download="${sanitizeHTML(
2132
+ <a href="#" data-href="${attachmentDataUri}" class="view-full lazy-load-attachment" target="_blank">View</a>
2133
+ <a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
1894
2134
  attachment.name
1895
2135
  )}">Download</a>
1896
2136
  </div>
@@ -1931,211 +2171,239 @@ function generateHTML(reportData, trendData = null) {
1931
2171
  <link rel="apple-touch-icon" href="https://i.postimg.cc/v817w4sg/logo.png">
1932
2172
  <script src="https://code.highcharts.com/highcharts.js" defer></script>
1933
2173
  <title>Playwright Pulse Report (Static Report)</title>
1934
- <style>
1935
- :root {
1936
- --primary-color: #3f51b5; --secondary-color: #ff4081; --accent-color: #673ab7; --accent-color-alt: #FF9800;
1937
- --success-color: #4CAF50; --danger-color: #F44336; --warning-color: #FFC107; --info-color: #2196F3;
1938
- --light-gray-color: #f5f5f5; --medium-gray-color: #e0e0e0; --dark-gray-color: #757575;
1939
- --text-color: #333; --text-color-secondary: #555; --border-color: #ddd; --background-color: #f8f9fa;
1940
- --card-background-color: #fff; --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
1941
- --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);
1942
- }
1943
- .trend-chart-container, .test-history-trend div[id^="testHistoryChart-"] { min-height: 100px; }
1944
- .lazy-load-chart .no-data, .lazy-load-chart .no-data-chart { display: flex; align-items: center; justify-content: center; height: 100%; font-style: italic; color: var(--dark-gray-color); }
1945
- .highcharts-background { fill: transparent; }
1946
- .highcharts-title, .highcharts-subtitle { font-family: var(--font-family); }
1947
- .highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
1948
- .highcharts-axis-title { fill: var(--text-color) !important; }
1949
- .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; }
1950
- body { font-family: var(--font-family); margin: 0; background-color: var(--background-color); color: var(--text-color); line-height: 1.65; font-size: 16px; }
1951
- .container { padding: 30px; border-radius: var(--border-radius); box-shadow: var(--box-shadow); background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec); }
1952
- .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; }
1953
- .header-title { display: flex; align-items: center; gap: 15px; }
1954
- .header h1 { margin: 0; font-size: 1.85em; font-weight: 600; color: var(--primary-color); }
1955
- #report-logo { height: 40px; width: 55px; }
1956
- .run-info { font-size: 0.9em; text-align: right; color: var(--text-color-secondary); line-height:1.5;}
1957
- .run-info strong { color: var(--text-color); }
1958
- .tabs { display: flex; border-bottom: 2px solid var(--border-color); margin-bottom: 30px; overflow-x: auto; }
1959
- .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; }
1960
- .tab-button:hover { color: var(--accent-color); }
1961
- .tab-button.active { color: var(--primary-color); border-bottom-color: var(--primary-color); }
1962
- .tab-content { display: none; animation: fadeIn 0.4s ease-out; }
1963
- .tab-content.active { display: block; }
1964
- @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
1965
- .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 22px; margin-bottom: 35px; }
1966
- .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; }
1967
- .summary-card:hover { transform: translateY(-5px); box-shadow: var(--box-shadow); }
1968
- .summary-card h3 { margin: 0 0 10px; font-size: 1.05em; font-weight: 500; color: var(--text-color-secondary); }
1969
- .summary-card .value { font-size: 2.4em; font-weight: 600; margin-bottom: 8px; }
1970
- .summary-card .trend-percentage { font-size: 1em; color: var(--dark-gray-color); }
1971
- .status-passed .value, .stat-passed svg { color: var(--success-color); }
1972
- .status-failed .value, .stat-failed svg { color: var(--danger-color); }
1973
- .status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
1974
- .dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: stretch; }
1975
- .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; }
1976
- .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); }
1977
- .trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { flex-grow: 1; min-height: 250px; }
1978
- .status-badge-small-tooltip { padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; }
1979
- .status-badge-small-tooltip.status-passed { background-color: var(--success-color); }
1980
- .status-badge-small-tooltip.status-failed { background-color: var(--danger-color); }
1981
- .status-badge-small-tooltip.status-skipped { background-color: var(--warning-color); }
1982
- .status-badge-small-tooltip.status-unknown { background-color: var(--dark-gray-color); }
1983
- .suites-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
1984
- .summary-badge { background-color: var(--light-gray-color); color: var(--text-color-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
1985
- .suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
1986
- .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; }
1987
- .suite-card:hover { box-shadow: var(--box-shadow); }
1988
- .suite-card.status-passed { border-left-color: var(--success-color); }
1989
- .suite-card.status-failed { border-left-color: var(--danger-color); }
1990
- .suite-card.status-skipped { border-left-color: var(--warning-color); }
1991
- .suite-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
1992
- .suite-name { font-weight: 600; font-size: 1.05em; color: var(--text-color); margin-right: 10px; word-break: break-word;}
1993
- .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;}
1994
- .suite-card-body .test-count { font-size: 0.95em; color: var(--text-color-secondary); display: block; margin-bottom: 10px; }
1995
- .suite-stats { display: flex; gap: 14px; font-size: 0.95em; align-items: center; }
1996
- .suite-stats span { display: flex; align-items: center; gap: 6px; }
1997
- .suite-stats svg { vertical-align: middle; font-size: 1.15em; }
1998
- .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; }
1999
- .filters input, .filters select, .filters button { padding: 11px 15px; border: 1px solid var(--border-color); border-radius: 6px; font-size: 1em; }
2000
- .filters input { flex-grow: 1; min-width: 240px;}
2001
- .filters select {min-width: 180px;}
2002
- .filters button { background-color: var(--primary-color); color: white; cursor: pointer; transition: background-color 0.2s ease, box-shadow 0.2s ease; border: none; }
2003
- .filters button:hover { background-color: var(--accent-color); box-shadow: 0 2px 5px rgba(0,0,0,0.15);}
2004
- .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; }
2005
- .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; }
2006
- .test-case-header:hover { background-color: #f4f6f8; }
2007
- .test-case-header[aria-expanded="true"] { border-bottom-color: var(--border-color); background-color: #f9fafb; }
2008
- .test-case-summary { display: flex; align-items: center; gap: 14px; flex-grow: 1; flex-wrap: wrap;}
2009
- .test-case-title { font-weight: 600; color: var(--text-color); font-size: 1em; }
2010
- .test-case-browser { font-size: 0.9em; color: var(--text-color-secondary); }
2011
- .test-case-meta { display: flex; align-items: center; gap: 12px; font-size: 0.9em; color: var(--text-color-secondary); flex-shrink: 0; }
2012
- .test-duration { background-color: var(--light-gray-color); padding: 4px 10px; border-radius: 12px; font-size: 0.9em;}
2013
- .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); }
2014
- .status-badge.status-passed { background-color: var(--success-color); }
2015
- .status-badge.status-failed { background-color: var(--danger-color); }
2016
- .status-badge.status-skipped { background-color: var(--warning-color); }
2017
- .status-badge.status-unknown { background-color: var(--dark-gray-color); }
2018
- .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; }
2019
- .test-case-content { display: none; padding: 20px; border-top: 1px solid var(--border-color); background-color: #fcfdff; }
2020
- .test-case-content h4 { margin-top: 22px; margin-bottom: 14px; font-size: 1.15em; color: var(--primary-color); }
2021
- .test-case-content p { margin-bottom: 10px; font-size: 1em; }
2022
- .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; }
2023
- .test-error-summary h4 { color: var(--danger-color); margin-top:0;}
2024
- .test-error-summary pre { white-space: pre-wrap; word-break: break-all; color: var(--danger-color); font-size: 0.95em;}
2025
- .steps-list { margin: 18px 0; }
2026
- .step-item { margin-bottom: 8px; padding-left: calc(var(--depth, 0) * 28px); }
2027
- .step-header { display: flex; align-items: center; cursor: pointer; padding: 10px 14px; border-radius: 6px; background-color: #fff; border: 1px solid var(--light-gray-color); transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; }
2028
- .step-header:hover { background-color: #f0f2f5; border-color: var(--medium-gray-color); box-shadow: var(--box-shadow-inset); }
2029
- .step-icon { margin-right: 12px; width: 20px; text-align: center; font-size: 1.1em; }
2030
- .step-title { flex: 1; font-size: 1em; }
2031
- .step-duration { color: var(--dark-gray-color); font-size: 0.9em; }
2032
- .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); }
2033
- .step-info { margin-bottom: 8px; }
2034
- .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); }
2035
- .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; }
2036
- .step-hook { background-color: rgba(33,150,243,0.04); border-left: 3px solid var(--info-color) !important; }
2037
- .step-hook .step-title { font-style: italic; color: var(--info-color)}
2038
- .nested-steps { margin-top: 12px; }
2039
- .attachments-section { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--light-gray-color); }
2040
- .attachments-section h4 { margin-top: 0; margin-bottom: 20px; font-size: 1.1em; color: var(--text-color); }
2041
- .attachments-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 22px; }
2042
- .attachment-item { border: 1px solid var(--border-color); border-radius: var(--border-radius); background-color: #fff; box-shadow: var(--box-shadow-light); overflow: hidden; display: flex; flex-direction: column; transition: transform 0.2s ease-out, box-shadow 0.2s ease-out; }
2043
- .attachment-item:hover { transform: translateY(-4px); box-shadow: var(--box-shadow); }
2044
- .attachment-item img, .attachment-item video { width: 100%; height: 180px; object-fit: cover; display: block; background-color: #eee; border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease; }
2045
- .attachment-info { padding: 12px; margin-top: auto; background-color: #fafafa;}
2046
- .attachment-item a:hover img { opacity: 0.85; }
2047
- .attachment-caption { padding: 12px 15px; font-size: 0.9em; text-align: center; color: var(--text-color-secondary); word-break: break-word; background-color: var(--light-gray-color); }
2048
- .video-item a, .trace-item a { display: block; margin-bottom: 8px; color: var(--primary-color); text-decoration: none; font-weight: 500; }
2049
- .video-item a:hover, .trace-item a:hover { text-decoration: underline; }
2050
- .code-section pre { background-color: #2d2d2d; color: #f0f0f0; padding: 20px; border-radius: 6px; overflow-x: auto; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 0.95em; line-height:1.6;}
2051
- .trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
2052
- .test-history-container h2.tab-main-title { font-size: 1.6em; margin-bottom: 18px; color: var(--primary-color); border-bottom: 1px solid var(--border-color); padding-bottom: 12px;}
2053
- .test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
2054
- .test-history-card { background: var(--card-background-color); border: 1px solid var(--border-color); border-radius: var(--border-radius); padding: 22px; box-shadow: var(--box-shadow-light); display: flex; flex-direction: column; }
2055
- .test-history-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 14px; border-bottom: 1px solid var(--light-gray-color); }
2056
- .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 */
2057
- .test-history-header p { font-weight: 500 } /* Added this */
2058
- .test-history-trend { margin-bottom: 20px; min-height: 110px; }
2059
- .test-history-trend div[id^="testHistoryChart-"] { display: block; margin: 0 auto; max-width:100%; height: 100px; width: 320px; }
2060
- .test-history-details-collapsible summary { cursor: pointer; font-size: 1em; color: var(--primary-color); margin-bottom: 10px; font-weight:500; }
2061
- .test-history-details-collapsible summary:hover {text-decoration: underline;}
2062
- .test-history-details table { width: 100%; border-collapse: collapse; font-size: 0.95em; }
2063
- .test-history-details th, .test-history-details td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--light-gray-color); }
2064
- .test-history-details th { background-color: var(--light-gray-color); font-weight: 600; }
2065
- .status-badge-small { padding: 3px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase; display: inline-block; }
2066
- .status-badge-small.status-passed { background-color: var(--success-color); }
2067
- .status-badge-small.status-failed { background-color: var(--danger-color); }
2068
- .status-badge-small.status-skipped { background-color: var(--warning-color); }
2069
- .status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
2070
- .no-data, .no-tests, .no-steps, .no-data-chart { padding: 28px; text-align: center; color: var(--dark-gray-color); font-style: italic; font-size:1.1em; background-color: var(--light-gray-color); border-radius: var(--border-radius); margin: 18px 0; border: 1px dashed var(--medium-gray-color); }
2071
- .no-data-chart {font-size: 0.95em; padding: 18px;}
2072
- .ai-failure-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 22px; }
2073
- .ai-failure-card { background: var(--card-background-color); border: 1px solid var(--border-color); border-left: 5px solid var(--danger-color); border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); display: flex; flex-direction: column; }
2074
- .ai-failure-card-header { padding: 15px 20px; border-bottom: 1px solid var(--light-gray-color); display: flex; align-items: center; justify-content: space-between; gap: 15px; }
2075
- .ai-failure-card-header h3 { margin: 0; font-size: 1.1em; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2076
- .ai-failure-card-body { padding: 20px; }
2077
- .ai-fix-btn { background-color: var(--primary-color); color: white; border: none; padding: 10px 18px; font-size: 1em; font-weight: 600; border-radius: 6px; cursor: pointer; transition: background-color 0.2s ease, transform 0.2s ease; display: inline-flex; align-items: center; gap: 8px; }
2078
- .ai-fix-btn:hover { background-color: var(--accent-color); transform: translateY(-2px); }
2079
- .ai-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.65); display: none; align-items: center; justify-content: center; z-index: 1050; animation: fadeIn 0.3s; }
2080
- .ai-modal-content { background-color: var(--card-background-color); color: var(--text-color); border-radius: var(--border-radius); width: 90%; max-width: 800px; max-height: 90vh; box-shadow: 0 10px 30px rgba(0,0,0,0.2); display: flex; flex-direction: column; overflow: hidden; }
2081
- .ai-modal-header { padding: 18px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
2082
- .ai-modal-header h3 { margin: 0; font-size: 1.25em; }
2083
- .ai-modal-close { font-size: 2rem; font-weight: 300; cursor: pointer; color: var(--dark-gray-color); line-height: 1; transition: color 0.2s; }
2084
- .ai-modal-close:hover { color: var(--danger-color); }
2085
- .ai-modal-body { padding: 25px; overflow-y: auto; }
2086
- .ai-modal-body h4 { margin-top: 18px; margin-bottom: 10px; font-size: 1.1em; color: var(--primary-color); }
2087
- .ai-modal-body p { margin-bottom: 15px; }
2088
- .ai-loader { margin: 40px auto; border: 5px solid #f3f3f3; border-top: 5px solid var(--primary-color); border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite; }
2089
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
2090
- .trace-preview { padding: 1rem; text-align: center; background: #f5f5f5; border-bottom: 1px solid #e1e1e1; }
2091
- .trace-icon { font-size: 2rem; display: block; margin-bottom: 0.5rem; }
2092
- .trace-name { word-break: break-word; font-size: 0.9rem; }
2093
- .trace-actions { display: flex; gap: 0.5rem; }
2094
- .trace-actions a { flex: 1; text-align: center; padding: 0.25rem 0.5rem; font-size: 0.85rem; border-radius: 4px; text-decoration: none; background: cornflowerblue; color: aliceblue; }
2095
- .view-trace { background: #3182ce; color: white; }
2096
- .view-trace:hover { background: #2c5282; }
2097
- .download-trace { background: #e2e8f0; color: #2d3748; }
2098
- .download-trace:hover { background: #cbd5e0; }
2099
- .filters button.clear-filters-btn { background-color: var(--medium-gray-color); color: var(--text-color); }
2100
- .filters button.clear-filters-btn:hover { background-color: var(--dark-gray-color); color: #fff; }
2101
- .copy-btn {color: var(--primary-color); background: #fefefe; border-radius: 8px; cursor: pointer; border-color: var(--primary-color); font-size: 1em; margin-left: 93%; font-weight: 600;}
2102
- .ai-analyzer-stats { display: flex; gap: 20px; margin-bottom: 25px; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: var(--border-radius); justify-content: center; }
2103
- .stat-item { text-align: center; color: white; }
2104
- .stat-number { display: block; font-size: 2em; font-weight: 700; line-height: 1;}
2105
- .stat-label { font-size: 0.9em; opacity: 0.9; font-weight: 500;}
2106
- .ai-analyzer-description { margin-bottom: 25px; font-size: 1em; color: var(--text-color-secondary); text-align: center; max-width: 600px; margin-left: auto; margin-right: auto;}
2107
- .compact-failure-list { display: flex; flex-direction: column; gap: 15px; }
2108
- .compact-failure-item { background: var(--card-background-color); border: 1px solid var(--border-color); border-left: 4px solid var(--danger-color); border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); transition: transform 0.2s ease, box-shadow 0.2s ease;}
2109
- .compact-failure-item:hover { transform: translateY(-2px); box-shadow: var(--box-shadow); }
2110
- .failure-header { display: flex; justify-content: space-between; align-items: center; padding: 18px 20px; gap: 15px;}
2111
- .failure-main-info { flex: 1; min-width: 0; }
2112
- .failure-title { margin: 0 0 8px 0; font-size: 1.1em; font-weight: 600; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
2113
- .failure-meta { display: flex; gap: 12px; align-items: center;}
2114
- .browser-indicator, .duration-indicator { font-size: 0.85em; padding: 3px 8px; border-radius: 12px; font-weight: 500;}
2115
- .browser-indicator { background: var(--info-color); color: white; }
2116
- .duration-indicator { background: var(--medium-gray-color); color: var(--text-color); }
2117
- .compact-ai-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 12px 18px; border-radius: 6px; cursor: pointer; font-weight: 600; display: flex; align-items: center; gap: 8px; transition: all 0.3s ease; white-space: nowrap;}
2118
- .compact-ai-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); }
2119
- .ai-text { font-size: 0.95em; }
2120
- .failure-error-preview { padding: 0 20px 18px 20px; border-top: 1px solid var(--light-gray-color);}
2121
- .error-snippet { background: rgba(244, 67, 54, 0.05); border: 1px solid rgba(244, 67, 54, 0.2); border-radius: 6px; padding: 12px; margin-bottom: 12px; font-family: monospace; font-size: 0.9em; color: var(--danger-color); line-height: 1.4;}
2122
- .expand-error-btn { background: none; border: 1px solid var(--border-color); color: var(--text-color-secondary); padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 0.85em; display: flex; align-items: center; gap: 6px; transition: all 0.2s ease;}
2123
- .expand-error-btn:hover { background: var(--light-gray-color); border-color: var(--medium-gray-color); }
2124
- .expand-icon { transition: transform 0.2s ease; font-size: 0.8em;}
2125
- .expand-error-btn.expanded .expand-icon { transform: rotate(180deg); }
2126
- .full-error-details { padding: 0 20px 20px 20px; border-top: 1px solid var(--light-gray-color); margin-top: 0;}
2127
- .full-error-content { background: rgba(244, 67, 54, 0.05); border: 1px solid rgba(244, 67, 54, 0.2); border-radius: 6px; padding: 15px; font-family: monospace; font-size: 0.9em; color: var(--danger-color); line-height: 1.4; max-height: 300px; overflow-y: auto;}
2128
- @media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
2129
- @media (max-width: 992px) { .dashboard-bottom-row { grid-template-columns: 1fr; } .pie-chart-wrapper div[id^="pieChart-"] { max-width: 350px; margin: 0 auto; } .filters input { min-width: 180px; } .filters select { min-width: 150px; } }
2130
- @media (max-width: 768px) { body { font-size: 15px; } .container { margin: 10px; padding: 20px; } .header { flex-direction: column; align-items: flex-start; gap: 15px; } .header h1 { font-size: 1.6em; } .run-info { text-align: left; font-size:0.9em; } .tabs { margin-bottom: 25px;} .tab-button { padding: 12px 20px; font-size: 1.05em;} .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;} .summary-card .value {font-size: 2em;} .summary-card h3 {font-size: 0.95em;} .filters { flex-direction: column; padding: 18px; gap: 12px;} .filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;} .test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; } .test-case-summary {gap: 10px;} .test-case-title {font-size: 1.05em;} .test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;} .attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;} .test-history-grid {grid-template-columns: 1fr;} .pie-chart-wrapper {min-height: auto;} .ai-failure-cards-grid { grid-template-columns: 1fr; } .ai-analyzer-stats { flex-direction: column; gap: 15px; text-align: center; } .failure-header { flex-direction: column; align-items: stretch; gap: 15px; } .failure-main-info { text-align: center; } .failure-meta { justify-content: center; } .compact-ai-btn { justify-content: center; padding: 12px 20px; } }
2131
- @media (max-width: 480px) { body {font-size: 14px;} .container {padding: 15px;} .header h1 {font-size: 1.4em;} #report-logo { height: 35px; width: 45px; } .tab-button {padding: 10px 15px; font-size: 1em;} .summary-card .value {font-size: 1.8em;} .attachments-grid {grid-template-columns: 1fr;} .step-item {padding-left: calc(var(--depth, 0) * 18px);} .test-case-content, .step-details {padding: 15px;} .trend-charts-row {gap: 20px;} .trend-chart {padding: 20px;} .stat-item .stat-number { font-size: 1.5em; } .failure-header { padding: 15px; } .failure-error-preview, .full-error-details { padding-left: 15px; padding-right: 15px; } }
2132
- .trace-actions a { text-decoration: none; color: var(--primary-color); font-weight: 500; font-size: 0.9em; }
2133
- .generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
2134
- .attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
2135
- .attachment-caption { display: flex; flex-direction: column; align-items: center; justify-content: center; flex-grow: 1; }
2136
- .attachment-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
2137
- .attachment-type { font-size: 0.8rem; color: var(--text-color-secondary); }
2138
- </style>
2174
+
2175
+ <style>
2176
+ :root {
2177
+ --primary-color: #60a5fa; --secondary-color: #f472b6; --accent-color: #a78bfa; --accent-color-alt: #fb923c;
2178
+ --success-color: #34d399; --danger-color: #f87171; --warning-color: #fbbf24; --info-color: #60a5fa;
2179
+ --light-gray-color: #374151; --medium-gray-color: #4b5563; --dark-gray-color: #9ca3af;
2180
+ --text-color: #f9fafb; --text-color-secondary: #d1d5db; --border-color: #4b5563; --background-color: #111827;
2181
+ --card-background-color: #1f2937; --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
2182
+ --border-radius: 8px; --box-shadow: 0 5px 15px rgba(0,0,0,0.3); --box-shadow-light: 0 3px 8px rgba(0,0,0,0.2); --box-shadow-inset: inset 0 1px 3px rgba(0,0,0,0.3);
2183
+ }
2184
+ .trend-chart-container, .test-history-trend div[id^="testHistoryChart-"] { min-height: 100px; }
2185
+ .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); }
2186
+ .highcharts-background { fill: transparent; }
2187
+ .highcharts-title, .highcharts-subtitle { font-family: var(--font-family); }
2188
+ .highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
2189
+ .highcharts-axis-title { fill: var(--text-color) !important; }
2190
+ .highcharts-tooltip > span { background-color: rgba(31,41,55,0.95) !important; border-color: rgba(31,41,55,0.95) !important; color: #f9fafb !important; padding: 10px !important; border-radius: 6px !important; }
2191
+ body { font-family: var(--font-family); margin: 0; background-color: var(--background-color); color: var(--text-color); line-height: 1.65; font-size: 16px; }
2192
+ .container { padding: 30px; border-radius: var(--border-radius); box-shadow: var(--box-shadow); background: repeating-linear-gradient(#1f2937, #374151, #1f2937); }
2193
+ .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; }
2194
+ .header-title { display: flex; align-items: center; gap: 15px; }
2195
+ .header h1 { margin: 0; font-size: 1.85em; font-weight: 600; color: var(--primary-color); }
2196
+ #report-logo { height: 40px; width: 55px; }
2197
+ .run-info { font-size: 0.9em; text-align: right; color: var(--text-color-secondary); line-height:1.5;}
2198
+ .run-info strong { color: var(--text-color); }
2199
+ .tabs { display: flex; border-bottom: 2px solid var(--border-color); margin-bottom: 30px; overflow-x: auto; }
2200
+ .tab-button { padding: 15px 25px; background: none; border: none; border-bottom: 3px solid transparent; cursor: pointer; font-size: 1.1em; font-weight: 600; color: var(--text-color); transition: color 0.2s ease, border-color 0.2s ease; white-space: nowrap; }
2201
+ .tab-button:hover { color: var(--accent-color); }
2202
+ .tab-button.active { color: var(--primary-color); border-bottom-color: var(--primary-color); }
2203
+ .tab-content { display: none; animation: fadeIn 0.4s ease-out; }
2204
+ .tab-content.active { display: block; }
2205
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
2206
+ .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 22px; margin-bottom: 35px; }
2207
+ .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; }
2208
+ .summary-card:hover { transform: translateY(-5px); box-shadow: var(--box-shadow); }
2209
+ .summary-card h3 { margin: 0 0 10px; font-size: 1.05em; font-weight: 500; color: var(--text-color-secondary); }
2210
+ .summary-card .value { font-size: 2.4em; font-weight: 600; margin-bottom: 8px; }
2211
+ .summary-card .trend-percentage { font-size: 1em; color: var(--dark-gray-color); }
2212
+ .status-passed .value, .stat-passed svg { color: var(--success-color); }
2213
+ .status-failed .value, .stat-failed svg { color: var(--danger-color); }
2214
+ .status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
2215
+ .dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: stretch; }
2216
+ .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; }
2217
+ .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); }
2218
+ .trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { flex-grow: 1; min-height: 250px; }
2219
+ .status-badge-small-tooltip { padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; }
2220
+ .status-badge-small-tooltip.status-passed { background-color: var(--success-color); }
2221
+ .status-badge-small-tooltip.status-failed { background-color: var(--danger-color); }
2222
+ .status-badge-small-tooltip.status-skipped { background-color: var(--warning-color); }
2223
+ .status-badge-small-tooltip.status-unknown { background-color: var(--dark-gray-color); }
2224
+ .suites-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
2225
+ .summary-badge { background-color: var(--light-gray-color); color: var(--text-color-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
2226
+ .suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
2227
+ .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; }
2228
+ .suite-card:hover { box-shadow: var(--box-shadow); }
2229
+ .suite-card.status-passed { border-left-color: var(--success-color); }
2230
+ .suite-card.status-failed { border-left-color: var(--danger-color); }
2231
+ .suite-card.status-skipped { border-left-color: var(--warning-color); }
2232
+ .suite-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
2233
+ .suite-name { font-weight: 600; font-size: 1.05em; color: var(--text-color); margin-right: 10px; word-break: break-word;}
2234
+ .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;}
2235
+ .suite-card-body .test-count { font-size: 0.95em; color: var(--text-color-secondary); display: block; margin-bottom: 10px; }
2236
+ .suite-stats { display: flex; gap: 14px; font-size: 0.95em; align-items: center; }
2237
+ .suite-stats span { display: flex; align-items: center; gap: 6px; }
2238
+ .suite-stats svg { vertical-align: middle; font-size: 1.15em; }
2239
+ .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: 1px solid var(--border-color); }
2240
+ .filters input, .filters select, .filters button { padding: 11px 15px; border: 1px solid var(--border-color); border-radius: 6px; font-size: 1em; background-color: var(--card-background-color); color: var(--text-color); }
2241
+ .filters input { flex-grow: 1; min-width: 240px;}
2242
+ .filters select {min-width: 180px;}
2243
+ .filters button { background-color: var(--primary-color); color: white; cursor: pointer; transition: background-color 0.2s ease, box-shadow 0.2s ease; border: none; }
2244
+ .filters button:hover { background-color: var(--accent-color); box-shadow: 0 2px 5px rgba(0,0,0,0.3);}
2245
+ .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; }
2246
+ .test-case-header { padding: 10px 15px; background-color: var(--card-background-color); cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid transparent; transition: background-color 0.2s ease; }
2247
+ .test-case-header:hover { background-color: var(--light-gray-color); }
2248
+ .test-case-header[aria-expanded="true"] { border-bottom-color: var(--border-color); background-color: var(--light-gray-color); }
2249
+ .test-case-summary { display: flex; align-items: center; gap: 14px; flex-grow: 1; flex-wrap: wrap;}
2250
+ .test-case-title { font-weight: 600; color: var(--text-color); font-size: 1em; }
2251
+ .test-case-browser { font-size: 0.9em; color: var(--text-color-secondary); }
2252
+ .test-case-meta { display: flex; align-items: center; gap: 12px; font-size: 0.9em; color: var(--text-color-secondary); flex-shrink: 0; }
2253
+ .test-duration { background-color: var(--light-gray-color); padding: 4px 10px; border-radius: 12px; font-size: 0.9em;}
2254
+ .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.3); }
2255
+ .status-badge.status-passed { background-color: var(--success-color); }
2256
+ .status-badge.status-failed { background-color: var(--danger-color); }
2257
+ .status-badge.status-skipped { background-color: var(--warning-color); }
2258
+ .status-badge.status-unknown { background-color: var(--dark-gray-color); }
2259
+ .tag { display: inline-block; background: linear-gradient(#4b5563, #1f2937, #111827); color: #f9fafb; padding: 3px 10px; border-radius: 12px; font-size: 0.85em; margin-right: 6px; font-weight: 400; }
2260
+ .test-case-content { display: none; padding: 20px; border-top: 1px solid var(--border-color); background-color: var(--light-gray-color); }
2261
+ .test-case-content h4 { margin-top: 22px; margin-bottom: 14px; font-size: 1.15em; color: var(--primary-color); }
2262
+ .test-case-content p { margin-bottom: 10px; font-size: 1em; }
2263
+ .test-error-summary { margin-bottom: 20px; padding: 14px; background-color: rgba(248,113,113,0.1); border: 1px solid rgba(248,113,113,0.3); border-left: 4px solid var(--danger-color); border-radius: 4px; }
2264
+ .test-error-summary h4 { color: var(--danger-color); margin-top:0;}
2265
+ .test-error-summary pre { white-space: pre-wrap; word-break: break-all; color: var(--danger-color); font-size: 0.95em;}
2266
+ .steps-list { margin: 18px 0; }
2267
+ @supports (content-visibility: auto) {
2268
+ .tab-content,
2269
+ #test-runs .test-case,
2270
+ .attachments-section,
2271
+ .test-history-card,
2272
+ .trend-chart,
2273
+ .suite-card {
2274
+ content-visibility: auto;
2275
+ contain-intrinsic-size: 1px 600px;
2276
+ }
2277
+ }
2278
+ .test-case,
2279
+ .test-history-card,
2280
+ .suite-card,
2281
+ .attachments-section {
2282
+ contain: content;
2283
+ }
2284
+ .attachments-grid .attachment-item img.lazy-load-image {
2285
+ width: 100%;
2286
+ aspect-ratio: 4 / 3;
2287
+ object-fit: cover;
2288
+ }
2289
+ .attachments-grid .attachment-item.video-item {
2290
+ aspect-ratio: 16 / 9;
2291
+ }
2292
+
2293
+ .step-item { margin-bottom: 8px; padding-left: calc(var(--depth, 0) * 28px); }
2294
+ .step-header { display: flex; align-items: center; cursor: pointer; padding: 10px 14px; border-radius: 6px; background-color: var(--card-background-color); border: 1px solid var(--light-gray-color); transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; }
2295
+ .step-header:hover { background-color: var(--light-gray-color); border-color: var(--medium-gray-color); box-shadow: var(--box-shadow-inset); }
2296
+ .step-icon { margin-right: 12px; width: 20px; text-align: center; font-size: 1.1em; }
2297
+ .step-title { flex: 1; font-size: 1em; }
2298
+ .step-duration { color: var(--dark-gray-color); font-size: 0.9em; }
2299
+ .step-details { display: none; padding: 14px; margin-top: 8px; background: var(--light-gray-color); border-radius: 6px; font-size: 0.95em; border: 1px solid var(--light-gray-color); }
2300
+ .step-info { margin-bottom: 8px; }
2301
+ .test-error-summary { color: var(--danger-color); margin-top: 12px; padding: 14px; background: rgba(248,113,113,0.1); border-radius: 4px; font-size: 0.95em; border-left: 3px solid var(--danger-color); }
2302
+ .test-error-summary pre.stack-trace { margin-top: 10px; padding: 12px; background-color: rgba(0,0,0,0.2); border-radius: 4px; font-size:0.9em; max-height: 280px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; }
2303
+ .step-hook { background-color: rgba(96,165,250,0.1); border-left: 3px solid var(--info-color) !important; }
2304
+ .step-hook .step-title { font-style: italic; color: var(--info-color)}
2305
+ .nested-steps { margin-top: 12px; }
2306
+ .attachments-section { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--light-gray-color); }
2307
+ .attachments-section h4 { margin-top: 0; margin-bottom: 20px; font-size: 1.1em; color: var(--text-color); }
2308
+ .attachments-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 22px; }
2309
+ .attachment-item { border: 1px solid var(--border-color); border-radius: var(--border-radius); background-color: var(--card-background-color); box-shadow: var(--box-shadow-light); display: flex; flex-direction: column; transition: transform 0.2s ease-out, box-shadow 0.2s ease-out; }
2310
+ .attachment-item:hover { transform: translateY(-4px); box-shadow: var(--box-shadow); }
2311
+ .attachment-item img, .attachment-item video { width: 100%; height: 180px; object-fit: cover; display: block; background-color: var(--medium-gray-color); border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease; }
2312
+ .attachment-info { padding: 12px; margin-top: auto; background-color: var(--light-gray-color);}
2313
+ .attachment-item a:hover img { opacity: 0.85; }
2314
+ .attachment-caption { padding: 12px 15px; font-size: 0.9em; text-align: center; color: var(--text-color-secondary); word-break: break-word; background-color: var(--light-gray-color); }
2315
+ .video-item a, .trace-item a { display: block; margin-bottom: 8px; color: var(--primary-color); text-decoration: none; font-weight: 500; }
2316
+ .video-item a:hover, .trace-item a:hover { text-decoration: underline; }
2317
+ .code-section pre { background-color: #111827; color: #f9fafb; padding: 20px; border-radius: 6px; overflow-x: auto; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 0.95em; line-height:1.6;}
2318
+ .trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
2319
+ .test-history-container h2.tab-main-title { font-size: 1.6em; margin-bottom: 18px; color: var(--primary-color); border-bottom: 1px solid var(--border-color); padding-bottom: 12px;}
2320
+ .test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
2321
+ .test-history-card { background: var(--card-background-color); border: 1px solid var(--border-color); border-radius: var(--border-radius); padding: 22px; box-shadow: var(--box-shadow-light); display: flex; flex-direction: column; }
2322
+ .test-history-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 14px; border-bottom: 1px solid var(--light-gray-color); }
2323
+ .test-history-header h3 { margin: 0; font-size: 1.15em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2324
+ .test-history-header p { font-weight: 500 }
2325
+ .test-history-trend { margin-bottom: 20px; min-height: 110px; }
2326
+ .test-history-trend div[id^="testHistoryChart-"] { display: block; margin: 0 auto; max-width:100%; height: 100px; width: 320px; }
2327
+ .test-history-details-collapsible summary { cursor: pointer; font-size: 1em; color: var(--primary-color); margin-bottom: 10px; font-weight:500; }
2328
+ .test-history-details-collapsible summary:hover {text-decoration: underline;}
2329
+ .test-history-details table { width: 100%; border-collapse: collapse; font-size: 0.95em; }
2330
+ .test-history-details th, .test-history-details td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--light-gray-color); }
2331
+ .test-history-details th { background-color: var(--light-gray-color); font-weight: 600; }
2332
+ .status-badge-small { padding: 3px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase; display: inline-block; }
2333
+ .status-badge-small.status-passed { background-color: var(--success-color); }
2334
+ .status-badge-small.status-failed { background-color: var(--danger-color); }
2335
+ .status-badge-small.status-skipped { background-color: var(--warning-color); }
2336
+ .status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
2337
+ .no-data, .no-tests, .no-steps, .no-data-chart { padding: 28px; text-align: center; color: var(--dark-gray-color); font-style: italic; font-size:1.1em; background-color: var(--light-gray-color); border-radius: var(--border-radius); margin: 18px 0; border: 1px dashed var(--medium-gray-color); }
2338
+ .no-data-chart {font-size: 0.95em; padding: 18px;}
2339
+ .ai-failure-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 22px; }
2340
+ .ai-failure-card { background: var(--card-background-color); border: 1px solid var(--border-color); border-left: 5px solid var(--danger-color); border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); display: flex; flex-direction: column; }
2341
+ .ai-failure-card-header { padding: 15px 20px; border-bottom: 1px solid var(--light-gray-color); display: flex; align-items: center; justify-content: space-between; gap: 15px; }
2342
+ .ai-failure-card-header h3 { margin: 0; font-size: 1.1em; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2343
+ .ai-failure-card-body { padding: 20px; }
2344
+ .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; }
2345
+ .ai-fix-btn:hover { background-color: var(--accent-color); transform: translateY(-2px); }
2346
+ .ai-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.8); display: none; align-items: center; justify-content: center; z-index: 1050; animation: fadeIn 0.3s; }
2347
+ .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.5); display: flex; flex-direction: column; overflow: hidden; }
2348
+ .ai-modal-header { padding: 18px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
2349
+ .ai-modal-header h3 { margin: 0; font-size: 1.25em; }
2350
+ .ai-modal-close { font-size: 2rem; font-weight: 300; cursor: pointer; color: var(--dark-gray-color); line-height: 1; transition: color 0.2s; }
2351
+ .ai-modal-close:hover { color: var(--danger-color); }
2352
+ .ai-modal-body { padding: 25px; overflow-y: auto; }
2353
+ .ai-modal-body h4 { margin-top: 18px; margin-bottom: 10px; font-size: 1.1em; color: var(--primary-color); }
2354
+ .ai-modal-body p { margin-bottom: 15px; }
2355
+ .ai-loader { margin: 40px auto; border: 5px solid var(--medium-gray-color); border-top: 5px solid var(--primary-color); border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite; }
2356
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
2357
+ .trace-preview { padding: 1rem; text-align: center; background: var(--light-gray-color); border-bottom: 1px solid var(--border-color); }
2358
+ .trace-icon { font-size: 2rem; display: block; margin-bottom: 0.5rem; }
2359
+ .trace-name { word-break: break-word; font-size: 0.9rem; }
2360
+ .trace-actions { display: flex; gap: 0.5rem; }
2361
+ .trace-actions a { flex: 1; text-align: center; padding: 0.25rem 0.5rem; font-size: 0.85rem; border-radius: 4px; text-decoration: none; background: var(--primary-color); color: white; }
2362
+ .view-trace { background: var(--primary-color); color: white; }
2363
+ .view-trace:hover { background: var(--accent-color); }
2364
+ .download-trace { background: var(--medium-gray-color); color: var(--text-color); }
2365
+ .download-trace:hover { background: var(--dark-gray-color); }
2366
+ .filters button.clear-filters-btn { background-color: var(--medium-gray-color); color: var(--text-color); }
2367
+ .filters button.clear-filters-btn:hover { background-color: var(--dark-gray-color); color: #fff; }
2368
+ .copy-btn {color: var(--primary-color); background: var(--card-background-color); border-radius: 8px; cursor: pointer; border-color: var(--primary-color); font-size: 1em; margin-left: 93%; font-weight: 600;}
2369
+ .ai-analyzer-stats { display: flex; gap: 20px; margin-bottom: 25px; padding: 20px; background: linear-gradient(135deg, #374151 0%, #1f2937 100%); border-radius: var(--border-radius); justify-content: center; }
2370
+ .stat-item { text-align: center; color: white; }
2371
+ .stat-number { display: block; font-size: 2em; font-weight: 700; line-height: 1;}
2372
+ .stat-label { font-size: 0.9em; opacity: 0.9; font-weight: 500;}
2373
+ .ai-analyzer-description { margin-bottom: 25px; font-size: 1em; color: var(--text-color-secondary); text-align: center; max-width: 600px; margin-left: auto; margin-right: auto;}
2374
+ .compact-failure-list { display: flex; flex-direction: column; gap: 15px; }
2375
+ .compact-failure-item { background: var(--card-background-color); border: 1px solid var(--border-color); border-left: 4px solid var(--danger-color); border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); transition: transform 0.2s ease, box-shadow 0.2s ease;}
2376
+ .compact-failure-item:hover { transform: translateY(-2px); box-shadow: var(--box-shadow); }
2377
+ .failure-header { display: flex; justify-content: space-between; align-items: center; padding: 18px 20px; gap: 15px;}
2378
+ .failure-main-info { flex: 1; min-width: 0; }
2379
+ .failure-title { margin: 0 0 8px 0; font-size: 1.1em; font-weight: 600; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
2380
+ .failure-meta { display: flex; gap: 12px; align-items: center;}
2381
+ .browser-indicator, .duration-indicator { font-size: 0.85em; padding: 3px 8px; border-radius: 12px; font-weight: 500;}
2382
+ .browser-indicator { background: var(--info-color); color: white; }
2383
+ #load-more-tests { font-size: 16px; padding: 4px; background-color: var(--light-gray-color); border-radius: 4px; color: var(--text-color); }
2384
+ .duration-indicator { background: var(--medium-gray-color); color: var(--text-color); }
2385
+ .compact-ai-btn { background: linear-gradient(135deg, #374151 0%, #1f2937 100%); color: white; border: none; padding: 12px 18px; border-radius: 6px; cursor: pointer; font-weight: 600; display: flex; align-items: center; gap: 8px; transition: all 0.3s ease; white-space: nowrap;}
2386
+ .compact-ai-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(55, 65, 81, 0.4); }
2387
+ .ai-text { font-size: 0.95em; }
2388
+ .failure-error-preview { padding: 0 20px 18px 20px; border-top: 1px solid var(--light-gray-color);}
2389
+ .error-snippet { background: rgba(248, 113, 113, 0.1); border: 1px solid rgba(248, 113, 113, 0.3); border-radius: 6px; padding: 12px; margin-bottom: 12px; font-family: monospace; font-size: 0.9em; color: var(--danger-color); line-height: 1.4;}
2390
+ .expand-error-btn { background: none; border: 1px solid var(--border-color); color: var(--text-color-secondary); padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 0.85em; display: flex; align-items: center; gap: 6px; transition: all 0.2s ease;}
2391
+ .expand-error-btn:hover { background: var(--light-gray-color); border-color: var(--medium-gray-color); }
2392
+ .expand-icon { transition: transform 0.2s ease; font-size: 0.8em;}
2393
+ .expand-error-btn.expanded .expand-icon { transform: rotate(180deg); }
2394
+ .full-error-details { padding: 0 20px 20px 20px; border-top: 1px solid var(--light-gray-color); margin-top: 0;}
2395
+ .full-error-content { background: rgba(248, 113, 113, 0.1); border: 1px solid rgba(248, 113, 113, 0.3); border-radius: 6px; padding: 15px; font-family: monospace; font-size: 0.9em; color: var(--danger-color); line-height: 1.4; max-height: 300px; overflow-y: auto;}
2396
+ @media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
2397
+ @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; } }
2398
+ @media (max-width: 768px) { body { font-size: 15px; } .container { margin: 10px; padding: 20px; } .header { flex-direction: column; align-items: flex-start; gap: 15px; } .header h1 { font-size: 1.6em; } .run-info { text-align: left; font-size:0.9em; } .tabs { margin-bottom: 25px;} .tab-button { padding: 12px 20px; font-size: 1.05em;} .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;} .summary-card .value {font-size: 2em;} .summary-card h3 {font-size: 0.95em;} .filters { flex-direction: column; padding: 18px; gap: 12px;} .filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;} .test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; } .test-case-summary {gap: 10px;} .test-case-title {font-size: 1.05em;} .test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;} .attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;} .test-history-grid {grid-template-columns: 1fr;} .pie-chart-wrapper {min-height: auto;} .ai-failure-cards-grid { grid-template-columns: 1fr; } .ai-analyzer-stats { flex-direction: column; gap: 15px; text-align: center; } .failure-header { flex-direction: column; align-items: stretch; gap: 15px; } .failure-main-info { text-align: center; } .failure-meta { justify-content: center; } .compact-ai-btn { justify-content: center; padding: 12px 20px; } }
2399
+ @media (max-width: 480px) { body {font-size: 14px;} .container {padding: 15px;} .header h1 {font-size: 1.4em;} #report-logo { height: 35px; width: 45px; } .tab-button {padding: 10px 15px; font-size: 1em;} .summary-card .value {font-size: 1.8em;} .attachments-grid {grid-template-columns: 1fr;} .step-item {padding-left: calc(var(--depth, 0) * 18px);} .test-case-content, .step-details {padding: 15px;} .trend-charts-row {gap: 20px;} .trend-chart {padding: 20px;} .stat-item .stat-number { font-size: 1.5em; } .failure-header { padding: 15px; } .failure-error-preview, .full-error-details { padding-left: 15px; padding-right: 15px; } }
2400
+ .trace-actions a { text-decoration: none; color: var(--primary-color); font-weight: 500; font-size: 0.9em; }
2401
+ .generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
2402
+ .attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
2403
+ .attachment-caption { display: flex; flex-direction: column; align-items: center; justify-content: center; flex-grow: 1; }
2404
+ .attachment-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
2405
+ .attachment-type { font-size: 0.8rem; color: var(--text-color-secondary); }
2406
+ </style>
2139
2407
  </head>
2140
2408
  <body>
2141
2409
  <div class="container">
@@ -2214,7 +2482,8 @@ function generateHTML(reportData, trendData = null) {
2214
2482
  .join("")}</select>
2215
2483
  <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>
2216
2484
  </div>
2217
- <div class="test-cases-list">${generateTestCasesHTML()}</div>
2485
+ <div class="test-cases-list">${generateTestCasesHTML(results.slice(0, 50), 0)}</div>
2486
+ ${results.length > 50 ? `<div class="load-more-wrapper"><button id="load-more-tests">Load more</button></div><script type="application/json" id="remaining-tests-b64">${Buffer.from(generateTestCasesHTML(results.slice(50), 50), 'utf8').toString('base64')}</script>` : ``}
2218
2487
  </div>
2219
2488
  <div id="test-history" class="tab-content">
2220
2489
  <h2 class="tab-main-title">Execution Trends</h2>
@@ -2420,10 +2689,46 @@ function generateHTML(reportData, trendData = null) {
2420
2689
  });
2421
2690
  // --- Test Run Summary Filters ---
2422
2691
  const nameFilter = document.getElementById('filter-name');
2692
+ function ensureAllTestsAppended() {
2693
+ const node = document.getElementById('remaining-tests-b64');
2694
+ const loadMoreBtn = document.getElementById('load-more-tests');
2695
+ if (!node) return;
2696
+ const b64 = (node.textContent || '').trim();
2697
+ function b64ToUtf8(b64Str) {
2698
+ try { return decodeURIComponent(escape(window.atob(b64Str))); }
2699
+ catch (e) { return window.atob(b64Str); }
2700
+ }
2701
+ const html = b64ToUtf8(b64);
2702
+ const container = document.querySelector('#test-runs .test-cases-list');
2703
+ if (container) container.insertAdjacentHTML('beforeend', html);
2704
+ if (loadMoreBtn) loadMoreBtn.remove();
2705
+ node.remove();
2706
+ }
2707
+ const loadMoreBtn = document.getElementById('load-more-tests');
2708
+ if (loadMoreBtn) {
2709
+ loadMoreBtn.addEventListener('click', () => {
2710
+ const node = document.getElementById('remaining-tests-b64');
2711
+ if (!node) return;
2712
+ const b64 = (node.textContent || '').trim();
2713
+ function b64ToUtf8(b64Str) {
2714
+ try { return decodeURIComponent(escape(window.atob(b64Str))); }
2715
+ catch (e) { return window.atob(b64Str); }
2716
+ }
2717
+ const html = b64ToUtf8(b64);
2718
+ const container = document.querySelector('#test-runs .test-cases-list');
2719
+ if (container) container.insertAdjacentHTML('beforeend', html);
2720
+ loadMoreBtn.remove();
2721
+ node.remove();
2722
+ });
2723
+ }
2724
+
2725
+
2726
+
2423
2727
  const statusFilter = document.getElementById('filter-status');
2424
2728
  const browserFilter = document.getElementById('filter-browser');
2425
2729
  const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters');
2426
2730
  function filterTestCases() {
2731
+ ensureAllTestsAppended();
2427
2732
  const nameValue = nameFilter ? nameFilter.value.toLowerCase() : "";
2428
2733
  const statusValue = statusFilter ? statusFilter.value : "";
2429
2734
  const browserValue = browserFilter ? browserFilter.value : "";
@@ -2442,7 +2747,10 @@ function generateHTML(reportData, trendData = null) {
2442
2747
  if(statusFilter) statusFilter.addEventListener('change', filterTestCases);
2443
2748
  if(browserFilter) browserFilter.addEventListener('change', filterTestCases);
2444
2749
  if(clearRunSummaryFiltersBtn) clearRunSummaryFiltersBtn.addEventListener('click', () => {
2445
- if(nameFilter) nameFilter.value = ''; if(statusFilter) statusFilter.value = ''; if(browserFilter) browserFilter.value = '';
2750
+ ensureAllTestsAppended();
2751
+ if(nameFilter) nameFilter.value = '';
2752
+ if(statusFilter) statusFilter.value = '';
2753
+ if(browserFilter) browserFilter.value = '';
2446
2754
  filterTestCases();
2447
2755
  });
2448
2756
  // --- Test History Filters ---
@@ -2483,12 +2791,6 @@ function generateHTML(reportData, trendData = null) {
2483
2791
  headerElement.setAttribute('aria-expanded', String(!isExpanded));
2484
2792
  }
2485
2793
  }
2486
- document.querySelectorAll('#test-runs .test-case-header').forEach(header => {
2487
- header.addEventListener('click', () => toggleElementDetails(header));
2488
- });
2489
- document.querySelectorAll('#test-runs .step-header').forEach(header => {
2490
- header.addEventListener('click', () => toggleElementDetails(header, '.step-details'));
2491
- });
2492
2794
  const expandAllBtn = document.getElementById('expand-all-tests');
2493
2795
  const collapseAllBtn = document.getElementById('collapse-all-tests');
2494
2796
  function setAllTestRunDetailsVisibility(displayMode, ariaState) {
@@ -2499,31 +2801,89 @@ function generateHTML(reportData, trendData = null) {
2499
2801
  }
2500
2802
  if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
2501
2803
  if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
2502
- // --- Intersection Observer for Lazy Loading ---
2503
- const lazyLoadElements = document.querySelectorAll('.lazy-load-chart, .lazy-load-iframe, .lazy-load-image, .lazy-load-video, .lazy-load-attachment');
2804
+ document.addEventListener('click', (e) => {
2805
+ const inHighcharts = e.target && e.target.closest && e.target.closest('.highcharts-container');
2806
+ if (inHighcharts) {
2807
+ return;
2808
+ }
2809
+ const header = e.target.closest('#test-runs .test-case-header');
2810
+ if (header) {
2811
+ let contentElement = header.parentElement.querySelector('.test-case-content');
2812
+ if (contentElement) {
2813
+ const isExpanded = contentElement.style.display === 'block';
2814
+ contentElement.style.display = isExpanded ? 'none' : 'block';
2815
+ header.setAttribute('aria-expanded', String(!isExpanded));
2816
+ }
2817
+ return;
2818
+ }
2819
+ const stepHeader = e.target.closest('#test-runs .step-header');
2820
+ if (stepHeader) {
2821
+ let details = stepHeader.nextElementSibling;
2822
+ if (details && details.matches('.step-details')) {
2823
+ const isExpanded = details.style.display === 'block';
2824
+ details.style.display = isExpanded ? 'none' : 'block';
2825
+ stepHeader.setAttribute('aria-expanded', String(!isExpanded));
2826
+ }
2827
+ return;
2828
+ }
2829
+ const img = e.target.closest('img.lazy-load-image');
2830
+ if (img && img.dataset && img.dataset.src) {
2831
+ if (e.preventDefault) e.preventDefault();
2832
+ img.src = img.dataset.src;
2833
+ img.removeAttribute('data-src');
2834
+ const parentLink = img.closest('a.lazy-load-attachment');
2835
+ if (parentLink && parentLink.dataset && parentLink.dataset.href) {
2836
+ parentLink.href = parentLink.dataset.href;
2837
+ parentLink.removeAttribute('data-href');
2838
+ }
2839
+ return;
2840
+ }
2841
+ const video = e.target.closest('video.lazy-load-video');
2842
+ if (video) {
2843
+ if (e.preventDefault) e.preventDefault();
2844
+ const s = video.querySelector('source');
2845
+ if (s && s.dataset && s.dataset.src && !s.src) {
2846
+ s.src = s.dataset.src;
2847
+ s.removeAttribute('data-src');
2848
+ video.load();
2849
+ } else if (video.dataset && video.dataset.src && !video.src) {
2850
+ video.src = video.dataset.src;
2851
+ video.removeAttribute('data-src');
2852
+ video.load();
2853
+ }
2854
+ return;
2855
+ }
2856
+ const a = e.target.closest('a.lazy-load-attachment');
2857
+ if (a && a.dataset && a.dataset.href) {
2858
+ e.preventDefault();
2859
+ a.href = a.dataset.href;
2860
+ a.removeAttribute('data-href');
2861
+ a.click();
2862
+ return;
2863
+ }
2864
+ });
2865
+ document.addEventListener('play', (e) => {
2866
+ const video = e.target && e.target.closest ? e.target.closest('video.lazy-load-video') : null;
2867
+ if (video) {
2868
+ const s = video.querySelector('source');
2869
+ if (s && s.dataset && s.dataset.src && !s.src) {
2870
+ s.src = s.dataset.src;
2871
+ s.removeAttribute('data-src');
2872
+ video.load();
2873
+ } else if (video.dataset && video.dataset.src && !video.src) {
2874
+ video.src = video.dataset.src;
2875
+ video.removeAttribute('data-src');
2876
+ video.load();
2877
+ }
2878
+ }
2879
+ }, true);
2880
+ const lazyLoadElements = document.querySelectorAll('.lazy-load-chart, .lazy-load-iframe');
2504
2881
  if ('IntersectionObserver' in window) {
2505
2882
  let lazyObserver = new IntersectionObserver((entries, observer) => {
2506
2883
  entries.forEach(entry => {
2507
2884
  if (entry.isIntersecting) {
2508
2885
  const element = entry.target;
2509
- if (element.classList.contains('lazy-load-image')) {
2510
- if (element.dataset.src) {
2511
- element.src = element.dataset.src;
2512
- element.removeAttribute('data-src');
2513
- }
2514
- } else if (element.classList.contains('lazy-load-video')) {
2515
- const source = element.querySelector('source');
2516
- if (source && source.dataset.src) {
2517
- source.src = source.dataset.src;
2518
- source.removeAttribute('data-src');
2519
- element.load();
2520
- }
2521
- } else if (element.classList.contains('lazy-load-attachment')) {
2522
- if (element.dataset.href) {
2523
- element.href = element.dataset.href;
2524
- element.removeAttribute('data-href');
2525
- }
2526
- } else if (element.classList.contains('lazy-load-iframe')) {
2886
+ if (element.classList.contains('lazy-load-iframe')) {
2527
2887
  if (element.dataset.src) {
2528
2888
  element.src = element.dataset.src;
2529
2889
  element.removeAttribute('data-src');
@@ -2539,13 +2899,10 @@ function generateHTML(reportData, trendData = null) {
2539
2899
  });
2540
2900
  }, { rootMargin: "0px 0px 200px 0px" });
2541
2901
  lazyLoadElements.forEach(el => lazyObserver.observe(el));
2542
- } else { // Fallback for browsers without IntersectionObserver
2902
+ } else {
2543
2903
  lazyLoadElements.forEach(element => {
2544
- if (element.classList.contains('lazy-load-image') && element.dataset.src) element.src = element.dataset.src;
2545
- else if (element.classList.contains('lazy-load-video')) { const source = element.querySelector('source'); if (source && source.dataset.src) { source.src = source.dataset.src; element.load(); } }
2546
- else if (element.classList.contains('lazy-load-attachment') && element.dataset.href) element.href = element.dataset.href;
2547
- else if (element.classList.contains('lazy-load-iframe') && element.dataset.src) element.src = element.dataset.src;
2548
- else if (element.classList.contains('lazy-load-chart')) { const renderFn = element.dataset.renderFunctionName; if(renderFn && window[renderFn]) window[renderFn](); }
2904
+ if (element.classList.contains('lazy-load-iframe') && element.dataset.src) element.src = element.dataset.src;
2905
+ else if (element.classList.contains('lazy-load-chart')) { const renderFn = element.dataset.renderFunctionName; if (renderFn && window[renderFn]) window[renderFn](); }
2549
2906
  });
2550
2907
  }
2551
2908
  }
@@ -2611,6 +2968,11 @@ async function runScript(scriptPath) {
2611
2968
  });
2612
2969
  });
2613
2970
  }
2971
+ /**
2972
+ * The main function that orchestrates the generation of the static HTML report.
2973
+ * It reads the latest test run data, loads historical data for trend analysis,
2974
+ * prepares the data, and then generates and writes the final HTML report file.
2975
+ */
2614
2976
  async function main() {
2615
2977
  const __filename = fileURLToPath(import.meta.url);
2616
2978
  const __dirname = path.dirname(__filename);