@arghajit/dummy 0.1.0-beta-27 → 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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/generate-report.mjs +704 -309
- package/scripts/generate-static-report.mjs +1380 -847
|
@@ -6,7 +6,11 @@ import path from "path";
|
|
|
6
6
|
import { fork } from "child_process";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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,32 +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
|
-
return
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
.replace(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.replace(/<\/intensity>/g, "</span>")
|
|
174
|
-
// Convert newlines to <br> after processing other replacements
|
|
175
|
-
.replace(/\n/g, "<br>")
|
|
176
|
-
);
|
|
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>");
|
|
177
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
|
+
*/
|
|
178
225
|
function formatDuration(ms, options = {}) {
|
|
179
226
|
const {
|
|
180
227
|
precision = 1,
|
|
@@ -214,19 +261,12 @@ function formatDuration(ms, options = {}) {
|
|
|
214
261
|
|
|
215
262
|
const totalRawSeconds = numMs / MS_PER_SECOND;
|
|
216
263
|
|
|
217
|
-
// Decision: Are we going to display hours or minutes?
|
|
218
|
-
// This happens if the duration is inherently >= 1 minute OR
|
|
219
|
-
// if it's < 1 minute but ceiling the seconds makes it >= 1 minute.
|
|
220
264
|
if (
|
|
221
265
|
totalRawSeconds < SECONDS_PER_MINUTE &&
|
|
222
266
|
Math.ceil(totalRawSeconds) < SECONDS_PER_MINUTE
|
|
223
267
|
) {
|
|
224
|
-
// Strictly seconds-only display, use precision.
|
|
225
268
|
return `${totalRawSeconds.toFixed(validPrecision)}s`;
|
|
226
269
|
} else {
|
|
227
|
-
// Display will include minutes and/or hours, or seconds round up to a minute.
|
|
228
|
-
// Seconds part should be an integer (ceiling).
|
|
229
|
-
// Round the total milliseconds UP to the nearest full second.
|
|
230
270
|
const totalMsRoundedUpToSecond =
|
|
231
271
|
Math.ceil(numMs / MS_PER_SECOND) * MS_PER_SECOND;
|
|
232
272
|
|
|
@@ -238,26 +278,26 @@ function formatDuration(ms, options = {}) {
|
|
|
238
278
|
const m = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_MINUTE));
|
|
239
279
|
remainingMs %= MS_PER_SECOND * SECONDS_PER_MINUTE;
|
|
240
280
|
|
|
241
|
-
const s = Math.floor(remainingMs / MS_PER_SECOND);
|
|
281
|
+
const s = Math.floor(remainingMs / MS_PER_SECOND);
|
|
242
282
|
|
|
243
283
|
const parts = [];
|
|
244
284
|
if (h > 0) {
|
|
245
285
|
parts.push(`${h}h`);
|
|
246
286
|
}
|
|
247
|
-
|
|
248
|
-
// Show minutes if:
|
|
249
|
-
// - hours are present (e.g., "1h 0m 5s")
|
|
250
|
-
// - OR minutes themselves are > 0 (e.g., "5m 10s")
|
|
251
|
-
// - OR the original duration was >= 1 minute (ensures "1m 0s" for 60000ms)
|
|
252
287
|
if (h > 0 || m > 0 || numMs >= MS_PER_SECOND * SECONDS_PER_MINUTE) {
|
|
253
288
|
parts.push(`${m}m`);
|
|
254
289
|
}
|
|
255
|
-
|
|
256
290
|
parts.push(`${s}s`);
|
|
257
291
|
|
|
258
292
|
return parts.join(" ");
|
|
259
293
|
}
|
|
260
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
|
+
*/
|
|
261
301
|
function generateTestTrendsChart(trendData) {
|
|
262
302
|
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
263
303
|
return '<div class="no-data">No overall trend data available for test counts.</div>';
|
|
@@ -353,6 +393,12 @@ function generateTestTrendsChart(trendData) {
|
|
|
353
393
|
</script>
|
|
354
394
|
`;
|
|
355
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
|
+
*/
|
|
356
402
|
function generateDurationTrendChart(trendData) {
|
|
357
403
|
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
358
404
|
return '<div class="no-data">No overall trend data available for durations.</div>';
|
|
@@ -437,6 +483,11 @@ function generateDurationTrendChart(trendData) {
|
|
|
437
483
|
</script>
|
|
438
484
|
`;
|
|
439
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
|
+
*/
|
|
440
491
|
function formatDate(dateStrOrDate) {
|
|
441
492
|
if (!dateStrOrDate) return "N/A";
|
|
442
493
|
try {
|
|
@@ -455,6 +506,12 @@ function formatDate(dateStrOrDate) {
|
|
|
455
506
|
return "Invalid Date Format";
|
|
456
507
|
}
|
|
457
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
|
+
*/
|
|
458
515
|
function generateTestHistoryChart(history) {
|
|
459
516
|
if (!history || history.length === 0)
|
|
460
517
|
return '<div class="no-data-chart">No data for chart</div>';
|
|
@@ -565,6 +622,13 @@ function generateTestHistoryChart(history) {
|
|
|
565
622
|
</script>
|
|
566
623
|
`;
|
|
567
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
|
+
*/
|
|
568
632
|
function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
569
633
|
const total = data.reduce((sum, d) => sum + d.value, 0);
|
|
570
634
|
if (total === 0) {
|
|
@@ -694,6 +758,12 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
694
758
|
</div>
|
|
695
759
|
`;
|
|
696
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
|
+
*/
|
|
697
767
|
function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
698
768
|
// Format memory for display
|
|
699
769
|
const formattedMemory = environment.memory.replace(/(\d+\.\d{2})GB/, "$1 GB");
|
|
@@ -709,176 +779,176 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
709
779
|
return `
|
|
710
780
|
<div class="environment-dashboard-wrapper" id="${dashboardId}">
|
|
711
781
|
<style>
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
782
|
+
.environment-dashboard-wrapper *,
|
|
783
|
+
.environment-dashboard-wrapper *::before,
|
|
784
|
+
.environment-dashboard-wrapper *::after {
|
|
785
|
+
box-sizing: border-box;
|
|
786
|
+
}
|
|
717
787
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
.env-dashboard-header {
|
|
753
|
-
grid-column: 1 / -1;
|
|
754
|
-
display: flex;
|
|
755
|
-
justify-content: space-between;
|
|
756
|
-
align-items: center;
|
|
757
|
-
border-bottom: 1px solid var(--border-color);
|
|
758
|
-
padding-bottom: 16px;
|
|
759
|
-
margin-bottom: 8px;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
.env-dashboard-title {
|
|
763
|
-
font-size: 1.5rem;
|
|
764
|
-
font-weight: 600;
|
|
765
|
-
color: var(--text-color);
|
|
766
|
-
margin: 0;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
.env-dashboard-subtitle {
|
|
770
|
-
font-size: 0.875rem;
|
|
771
|
-
color: var(--text-color-secondary);
|
|
772
|
-
margin-top: 4px;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
.env-card {
|
|
776
|
-
background-color: var(--card-background-color);
|
|
777
|
-
border-radius: 8px;
|
|
778
|
-
padding: ${cardContentPadding}px;
|
|
779
|
-
box-shadow: 0 3px 6px var(--shadow-color);
|
|
780
|
-
height: ${cardHeight}px;
|
|
781
|
-
display: flex;
|
|
782
|
-
flex-direction: column;
|
|
783
|
-
overflow: hidden;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
.env-card-header {
|
|
787
|
-
font-weight: 600;
|
|
788
|
-
font-size: 1rem;
|
|
789
|
-
margin-bottom: 12px;
|
|
790
|
-
color: var(--text-color);
|
|
791
|
-
display: flex;
|
|
792
|
-
align-items: center;
|
|
793
|
-
padding-bottom: 8px;
|
|
794
|
-
border-bottom: 1px solid var(--border-light-color);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
.env-card-header svg {
|
|
798
|
-
margin-right: 10px;
|
|
799
|
-
width: 18px;
|
|
800
|
-
height: 18px;
|
|
801
|
-
fill: var(--icon-color);
|
|
802
|
-
}
|
|
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
|
+
}
|
|
803
821
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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>
|
|
882
952
|
|
|
883
953
|
<div class="env-dashboard-header">
|
|
884
954
|
<div>
|
|
@@ -1023,125 +1093,420 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
1023
1093
|
</div>
|
|
1024
1094
|
`;
|
|
1025
1095
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
+
*/
|
|
1101
|
+
function generateWorkerDistributionChart(results) {
|
|
1102
|
+
if (!results || results.length === 0) {
|
|
1103
|
+
return '<div class="no-data">No test results data available to display worker distribution.</div>';
|
|
1033
1104
|
}
|
|
1034
1105
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1106
|
+
// 1. Sort results by startTime to ensure chronological order
|
|
1107
|
+
const sortedResults = [...results].sort((a, b) => {
|
|
1108
|
+
const timeA = a.startTime ? new Date(a.startTime).getTime() : 0;
|
|
1109
|
+
const timeB = b.startTime ? new Date(b.startTime).getTime() : 0;
|
|
1110
|
+
return timeA - timeB;
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const workerData = sortedResults.reduce((acc, test) => {
|
|
1114
|
+
const workerId =
|
|
1115
|
+
typeof test.workerId !== "undefined" ? test.workerId : "N/A";
|
|
1116
|
+
if (!acc[workerId]) {
|
|
1117
|
+
acc[workerId] = { passed: 0, failed: 0, skipped: 0, tests: [] };
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const status = String(test.status).toLowerCase();
|
|
1121
|
+
if (status === "passed" || status === "failed" || status === "skipped") {
|
|
1122
|
+
acc[workerId][status]++;
|
|
1045
1123
|
}
|
|
1124
|
+
|
|
1125
|
+
const testTitleParts = test.name.split(" > ");
|
|
1126
|
+
const testTitle =
|
|
1127
|
+
testTitleParts[testTitleParts.length - 1] || "Unnamed Test";
|
|
1128
|
+
// Store both name and status for each test
|
|
1129
|
+
acc[workerId].tests.push({ name: testTitle, status: status });
|
|
1130
|
+
|
|
1131
|
+
return acc;
|
|
1132
|
+
}, {});
|
|
1133
|
+
|
|
1134
|
+
const workerIds = Object.keys(workerData).sort((a, b) => {
|
|
1135
|
+
if (a === "N/A") return 1;
|
|
1136
|
+
if (b === "N/A") return -1;
|
|
1137
|
+
return parseInt(a, 10) - parseInt(b, 10);
|
|
1046
1138
|
});
|
|
1047
1139
|
|
|
1048
|
-
if (
|
|
1049
|
-
return '<div class="no-data">
|
|
1140
|
+
if (workerIds.length === 0) {
|
|
1141
|
+
return '<div class="no-data">Could not determine worker distribution from test data.</div>';
|
|
1050
1142
|
}
|
|
1051
1143
|
|
|
1052
|
-
const
|
|
1053
|
-
.
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1144
|
+
const chartId = `workerDistChart-${Date.now()}-${Math.random()
|
|
1145
|
+
.toString(36)
|
|
1146
|
+
.substring(2, 7)}`;
|
|
1147
|
+
const renderFunctionName = `renderWorkerDistChart_${chartId.replace(
|
|
1148
|
+
/-/g,
|
|
1149
|
+
"_"
|
|
1150
|
+
)}`;
|
|
1151
|
+
const modalJsNamespace = `modal_funcs_${chartId.replace(/-/g, "_")}`;
|
|
1152
|
+
|
|
1153
|
+
// The categories now just need the name for the axis labels
|
|
1154
|
+
const categories = workerIds.map((id) => `Worker ${id}`);
|
|
1155
|
+
|
|
1156
|
+
// We pass the full data separately to the script
|
|
1157
|
+
const fullWorkerData = workerIds.map((id) => ({
|
|
1158
|
+
id: id,
|
|
1159
|
+
name: `Worker ${id}`,
|
|
1160
|
+
tests: workerData[id].tests,
|
|
1161
|
+
}));
|
|
1162
|
+
|
|
1163
|
+
const passedData = workerIds.map((id) => workerData[id].passed);
|
|
1164
|
+
const failedData = workerIds.map((id) => workerData[id].failed);
|
|
1165
|
+
const skippedData = workerIds.map((id) => workerData[id].skipped);
|
|
1166
|
+
|
|
1167
|
+
const categoriesString = JSON.stringify(categories);
|
|
1168
|
+
const fullDataString = JSON.stringify(fullWorkerData);
|
|
1169
|
+
const seriesString = JSON.stringify([
|
|
1170
|
+
{ name: "Passed", data: passedData, color: "var(--success-color)" },
|
|
1171
|
+
{ name: "Failed", data: failedData, color: "var(--danger-color)" },
|
|
1172
|
+
{ name: "Skipped", data: skippedData, color: "var(--warning-color)" },
|
|
1173
|
+
]);
|
|
1077
1174
|
|
|
1175
|
+
// The HTML now includes the chart container, the modal, and styles for the modal
|
|
1078
1176
|
return `
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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>
|
|
1217
|
+
|
|
1218
|
+
<div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}" style="min-height: 350px;">
|
|
1219
|
+
<div class="no-data">Loading Worker Distribution Chart...</div>
|
|
1220
|
+
</div>
|
|
1221
|
+
|
|
1222
|
+
<div id="worker-modal-${chartId}" class="worker-modal-overlay">
|
|
1223
|
+
<div class="worker-modal-content">
|
|
1224
|
+
<span class="worker-modal-close">×</span>
|
|
1225
|
+
<h3 id="worker-modal-title-${chartId}" style="text-align: center; margin-top: 0; margin-bottom: 25px; font-size: 1.25em; font-weight: 600; color: #fff"></h3>
|
|
1226
|
+
<div id="worker-modal-body-${chartId}"></div>
|
|
1227
|
+
</div>
|
|
1228
|
+
</div>
|
|
1229
|
+
|
|
1230
|
+
<script>
|
|
1231
|
+
// Namespace for modal functions to avoid global scope pollution
|
|
1232
|
+
window.${modalJsNamespace} = {};
|
|
1233
|
+
|
|
1234
|
+
window.${renderFunctionName} = function() {
|
|
1235
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
1236
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found."); return; }
|
|
1237
|
+
|
|
1238
|
+
// --- Modal Setup ---
|
|
1239
|
+
const modal = document.getElementById('worker-modal-${chartId}');
|
|
1240
|
+
const modalTitle = document.getElementById('worker-modal-title-${chartId}');
|
|
1241
|
+
const modalBody = document.getElementById('worker-modal-body-${chartId}');
|
|
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==='&'?'&':ch==='<'?'<':ch==='>'?'>':'"';});}
|
|
1249
|
+
|
|
1250
|
+
window.${modalJsNamespace}.open = function(worker) {
|
|
1251
|
+
if (!worker) return;
|
|
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>';
|
|
1270
|
+
|
|
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
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
const closeModal = function() {
|
|
1279
|
+
modal.style.display = 'none';
|
|
1280
|
+
try { document.body.style.overflow = ''; } catch (_) {}
|
|
1281
|
+
};
|
|
1282
|
+
|
|
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) {
|
|
1291
|
+
closeModal();
|
|
1292
|
+
}
|
|
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
|
+
});
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
// --- Highcharts Setup ---
|
|
1303
|
+
if (typeof Highcharts !== 'undefined') {
|
|
1304
|
+
try {
|
|
1305
|
+
chartContainer.innerHTML = '';
|
|
1306
|
+
const fullData = ${fullDataString};
|
|
1307
|
+
|
|
1308
|
+
const chartOptions = {
|
|
1309
|
+
chart: { type: 'bar', height: 350, backgroundColor: 'transparent' },
|
|
1310
|
+
title: { text: null },
|
|
1311
|
+
xAxis: {
|
|
1312
|
+
categories: ${categoriesString},
|
|
1313
|
+
title: { text: 'Worker ID' },
|
|
1314
|
+
labels: { style: { color: 'var(--text-color-secondary)' }}
|
|
1315
|
+
},
|
|
1316
|
+
yAxis: {
|
|
1317
|
+
min: 0,
|
|
1318
|
+
title: { text: 'Number of Tests' },
|
|
1319
|
+
labels: { style: { color: 'var(--text-color-secondary)' }},
|
|
1320
|
+
stackLabels: { enabled: true, style: { fontWeight: 'bold', color: 'var(--text-color)' } }
|
|
1321
|
+
},
|
|
1322
|
+
legend: { reversed: true, itemStyle: { fontSize: "12px", color: 'var(--text-color)' } },
|
|
1323
|
+
plotOptions: {
|
|
1324
|
+
series: {
|
|
1325
|
+
stacking: 'normal',
|
|
1326
|
+
cursor: 'pointer',
|
|
1327
|
+
point: {
|
|
1328
|
+
events: {
|
|
1329
|
+
click: function () {
|
|
1330
|
+
// 'this.x' is the index of the category
|
|
1331
|
+
const workerData = fullData[this.x];
|
|
1332
|
+
window.${modalJsNamespace}.open(workerData);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
},
|
|
1338
|
+
tooltip: {
|
|
1339
|
+
shared: true,
|
|
1340
|
+
headerFormat: '<b>{point.key}</b> (Click for details)<br/>',
|
|
1341
|
+
pointFormat: '<span style="color:{series.color}">●</span> {series.name}: <b>{point.y}</b><br/>',
|
|
1342
|
+
footerFormat: 'Total: <b>{point.total}</b>'
|
|
1343
|
+
},
|
|
1344
|
+
series: ${seriesString},
|
|
1345
|
+
credits: { enabled: false }
|
|
1346
|
+
};
|
|
1347
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
1348
|
+
} catch (e) {
|
|
1349
|
+
console.error("Error rendering chart ${chartId}:", e);
|
|
1350
|
+
chartContainer.innerHTML = '<div class="no-data">Error rendering worker distribution chart.</div>';
|
|
1351
|
+
}
|
|
1352
|
+
} else {
|
|
1353
|
+
chartContainer.innerHTML = '<div class="no-data">Charting library not available for worker distribution.</div>';
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
</script>
|
|
1143
1357
|
`;
|
|
1144
1358
|
}
|
|
1359
|
+
/**
|
|
1360
|
+
* A tooltip providing information about why worker -1 is special in Playwright.
|
|
1361
|
+
* @type {string}
|
|
1362
|
+
*/
|
|
1363
|
+
const infoTooltip = `
|
|
1364
|
+
<span class="info-tooltip" style="display: inline-block; margin-left: 8px;">
|
|
1365
|
+
<span class="info-icon"
|
|
1366
|
+
style="cursor: pointer; font-size: 1.25rem;"
|
|
1367
|
+
onclick="window.workerInfoPrompt()">ℹ️</span>
|
|
1368
|
+
</span>
|
|
1369
|
+
<script>
|
|
1370
|
+
window.workerInfoPrompt = function() {
|
|
1371
|
+
const message = 'Why is worker -1 special?\\n\\n' +
|
|
1372
|
+
'Playwright assigns skipped tests to worker -1 because:\\n' +
|
|
1373
|
+
'1. They don\\'t require browser execution\\n' +
|
|
1374
|
+
'2. This keeps real workers focused on actual tests\\n' +
|
|
1375
|
+
'3. Maintains clean reporting\\n\\n' +
|
|
1376
|
+
'This is an intentional optimization by Playwright.';
|
|
1377
|
+
alert(message);
|
|
1378
|
+
}
|
|
1379
|
+
</script>
|
|
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
|
+
*/
|
|
1386
|
+
function generateTestHistoryContent(trendData) {
|
|
1387
|
+
if (
|
|
1388
|
+
!trendData ||
|
|
1389
|
+
!trendData.testRuns ||
|
|
1390
|
+
Object.keys(trendData.testRuns).length === 0
|
|
1391
|
+
) {
|
|
1392
|
+
return '<div class="no-data">No historical test data available.</div>';
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const allTestNamesAndPaths = new Map();
|
|
1396
|
+
Object.values(trendData.testRuns).forEach((run) => {
|
|
1397
|
+
if (Array.isArray(run)) {
|
|
1398
|
+
run.forEach((test) => {
|
|
1399
|
+
if (test && test.testName && !allTestNamesAndPaths.has(test.testName)) {
|
|
1400
|
+
const parts = test.testName.split(" > ");
|
|
1401
|
+
const title = parts[parts.length - 1];
|
|
1402
|
+
allTestNamesAndPaths.set(test.testName, title);
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
if (allTestNamesAndPaths.size === 0) {
|
|
1409
|
+
return '<div class="no-data">No historical test data found after processing.</div>';
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
const testHistory = Array.from(allTestNamesAndPaths.entries())
|
|
1413
|
+
.map(([fullTestName, testTitle]) => {
|
|
1414
|
+
const history = [];
|
|
1415
|
+
(trendData.overall || []).forEach((overallRun, index) => {
|
|
1416
|
+
const runKey = overallRun.runId
|
|
1417
|
+
? `test run ${overallRun.runId}`
|
|
1418
|
+
: `test run ${index + 1}`;
|
|
1419
|
+
const testRunForThisOverallRun = trendData.testRuns[runKey]?.find(
|
|
1420
|
+
(t) => t && t.testName === fullTestName
|
|
1421
|
+
);
|
|
1422
|
+
if (testRunForThisOverallRun) {
|
|
1423
|
+
history.push({
|
|
1424
|
+
runId: overallRun.runId || index + 1,
|
|
1425
|
+
status: testRunForThisOverallRun.status || "unknown",
|
|
1426
|
+
duration: testRunForThisOverallRun.duration || 0,
|
|
1427
|
+
timestamp:
|
|
1428
|
+
testRunForThisOverallRun.timestamp ||
|
|
1429
|
+
overallRun.timestamp ||
|
|
1430
|
+
new Date(),
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
return { fullTestName, testTitle, history };
|
|
1435
|
+
})
|
|
1436
|
+
.filter((item) => item.history.length > 0);
|
|
1437
|
+
|
|
1438
|
+
return `
|
|
1439
|
+
<div class="test-history-container">
|
|
1440
|
+
<div class="filters" style="border-color: black; border-style: groove;">
|
|
1441
|
+
<input type="text" id="history-filter-name" placeholder="Search by test title..." style="border-color: black; border-style: outset;">
|
|
1442
|
+
<select id="history-filter-status">
|
|
1443
|
+
<option value="">All Statuses</option>
|
|
1444
|
+
<option value="passed">Passed</option>
|
|
1445
|
+
<option value="failed">Failed</option>
|
|
1446
|
+
<option value="skipped">Skipped</option>
|
|
1447
|
+
</select>
|
|
1448
|
+
<button id="clear-history-filters" class="clear-filters-btn">Clear Filters</button>
|
|
1449
|
+
</div>
|
|
1450
|
+
|
|
1451
|
+
<div class="test-history-grid">
|
|
1452
|
+
${testHistory
|
|
1453
|
+
.map((test) => {
|
|
1454
|
+
const latestRun =
|
|
1455
|
+
test.history.length > 0
|
|
1456
|
+
? test.history[test.history.length - 1]
|
|
1457
|
+
: { status: "unknown" };
|
|
1458
|
+
return `
|
|
1459
|
+
<div class="test-history-card" data-test-name="${sanitizeHTML(
|
|
1460
|
+
test.testTitle.toLowerCase()
|
|
1461
|
+
)}" data-latest-status="${latestRun.status}">
|
|
1462
|
+
<div class="test-history-header">
|
|
1463
|
+
<p title="${sanitizeHTML(test.testTitle)}">${capitalize(
|
|
1464
|
+
sanitizeHTML(test.testTitle)
|
|
1465
|
+
)}</p>
|
|
1466
|
+
<span class="status-badge ${getStatusClass(latestRun.status)}">
|
|
1467
|
+
${String(latestRun.status).toUpperCase()}
|
|
1468
|
+
</span>
|
|
1469
|
+
</div>
|
|
1470
|
+
<div class="test-history-trend">
|
|
1471
|
+
${generateTestHistoryChart(test.history)}
|
|
1472
|
+
</div>
|
|
1473
|
+
<details class="test-history-details-collapsible">
|
|
1474
|
+
<summary>Show Run Details (${test.history.length})</summary>
|
|
1475
|
+
<div class="test-history-details">
|
|
1476
|
+
<table>
|
|
1477
|
+
<thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Date</th></tr></thead>
|
|
1478
|
+
<tbody>
|
|
1479
|
+
${test.history
|
|
1480
|
+
.slice()
|
|
1481
|
+
.reverse()
|
|
1482
|
+
.map(
|
|
1483
|
+
(run) => `
|
|
1484
|
+
<tr>
|
|
1485
|
+
<td>${run.runId}</td>
|
|
1486
|
+
<td><span class="status-badge-small ${getStatusClass(
|
|
1487
|
+
run.status
|
|
1488
|
+
)}">${String(run.status).toUpperCase()}</span></td>
|
|
1489
|
+
<td>${formatDuration(run.duration)}</td>
|
|
1490
|
+
<td>${formatDate(run.timestamp)}</td>
|
|
1491
|
+
</tr>`
|
|
1492
|
+
)
|
|
1493
|
+
.join("")}
|
|
1494
|
+
</tbody>
|
|
1495
|
+
</table>
|
|
1496
|
+
</div>
|
|
1497
|
+
</details>
|
|
1498
|
+
</div>`;
|
|
1499
|
+
})
|
|
1500
|
+
.join("")}
|
|
1501
|
+
</div>
|
|
1502
|
+
</div>
|
|
1503
|
+
`;
|
|
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
|
+
*/
|
|
1145
1510
|
function getStatusClass(status) {
|
|
1146
1511
|
switch (String(status).toLowerCase()) {
|
|
1147
1512
|
case "passed":
|
|
@@ -1154,6 +1519,11 @@ function getStatusClass(status) {
|
|
|
1154
1519
|
return "status-unknown";
|
|
1155
1520
|
}
|
|
1156
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
|
+
*/
|
|
1157
1527
|
function getStatusIcon(status) {
|
|
1158
1528
|
switch (String(status).toLowerCase()) {
|
|
1159
1529
|
case "passed":
|
|
@@ -1166,6 +1536,11 @@ function getStatusIcon(status) {
|
|
|
1166
1536
|
return "❓";
|
|
1167
1537
|
}
|
|
1168
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
|
+
*/
|
|
1169
1544
|
function getSuitesData(results) {
|
|
1170
1545
|
const suitesMap = new Map();
|
|
1171
1546
|
if (!results || results.length === 0) return [];
|
|
@@ -1214,6 +1589,16 @@ function getSuitesData(results) {
|
|
|
1214
1589
|
});
|
|
1215
1590
|
return Array.from(suitesMap.values());
|
|
1216
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
|
+
*/
|
|
1217
1602
|
function getAttachmentIcon(contentType) {
|
|
1218
1603
|
if (!contentType) return "📎"; // Handle undefined/null
|
|
1219
1604
|
|
|
@@ -1227,6 +1612,16 @@ function getAttachmentIcon(contentType) {
|
|
|
1227
1612
|
if (normalizedType.startsWith("text/")) return "📝";
|
|
1228
1613
|
return "📎";
|
|
1229
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
|
+
*/
|
|
1230
1625
|
function generateSuitesWidget(suitesData) {
|
|
1231
1626
|
if (!suitesData || suitesData.length === 0) {
|
|
1232
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>`;
|
|
@@ -1282,261 +1677,123 @@ function generateSuitesWidget(suitesData) {
|
|
|
1282
1677
|
.join("")}
|
|
1283
1678
|
</div>
|
|
1284
1679
|
</div>`;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
return timeA - timeB;
|
|
1296
|
-
});
|
|
1297
|
-
|
|
1298
|
-
const workerData = sortedResults.reduce((acc, test) => {
|
|
1299
|
-
const workerId =
|
|
1300
|
-
typeof test.workerId !== "undefined" ? test.workerId : "N/A";
|
|
1301
|
-
if (!acc[workerId]) {
|
|
1302
|
-
acc[workerId] = { passed: 0, failed: 0, skipped: 0, tests: [] };
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
const status = String(test.status).toLowerCase();
|
|
1306
|
-
if (status === "passed" || status === "failed" || status === "skipped") {
|
|
1307
|
-
acc[workerId][status]++;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
const testTitleParts = test.name.split(" > ");
|
|
1311
|
-
const testTitle =
|
|
1312
|
-
testTitleParts[testTitleParts.length - 1] || "Unnamed Test";
|
|
1313
|
-
// Store both name and status for each test
|
|
1314
|
-
acc[workerId].tests.push({ name: testTitle, status: status });
|
|
1315
|
-
|
|
1316
|
-
return acc;
|
|
1317
|
-
}, {});
|
|
1318
|
-
|
|
1319
|
-
const workerIds = Object.keys(workerData).sort((a, b) => {
|
|
1320
|
-
if (a === "N/A") return 1;
|
|
1321
|
-
if (b === "N/A") return -1;
|
|
1322
|
-
return parseInt(a, 10) - parseInt(b, 10);
|
|
1323
|
-
});
|
|
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
|
+
*/
|
|
1686
|
+
function generateAIFailureAnalyzerTab(results) {
|
|
1687
|
+
const failedTests = (results || []).filter(
|
|
1688
|
+
(test) => test.status === "failed"
|
|
1689
|
+
);
|
|
1324
1690
|
|
|
1325
|
-
if (
|
|
1326
|
-
return
|
|
1691
|
+
if (failedTests.length === 0) {
|
|
1692
|
+
return `
|
|
1693
|
+
<h2 class="tab-main-title">AI Failure Analysis</h2>
|
|
1694
|
+
<div class="no-data">Congratulations! No failed tests in this run.</div>
|
|
1695
|
+
`;
|
|
1327
1696
|
}
|
|
1328
1697
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
.substring(2, 7)}`;
|
|
1332
|
-
const renderFunctionName = `renderWorkerDistChart_${chartId.replace(
|
|
1333
|
-
/-/g,
|
|
1334
|
-
"_"
|
|
1335
|
-
)}`;
|
|
1336
|
-
const modalJsNamespace = `modal_funcs_${chartId.replace(/-/g, "_")}`;
|
|
1337
|
-
|
|
1338
|
-
// The categories now just need the name for the axis labels
|
|
1339
|
-
const categories = workerIds.map((id) => `Worker ${id}`);
|
|
1340
|
-
|
|
1341
|
-
// We pass the full data separately to the script
|
|
1342
|
-
const fullWorkerData = workerIds.map((id) => ({
|
|
1343
|
-
id: id,
|
|
1344
|
-
name: `Worker ${id}`,
|
|
1345
|
-
tests: workerData[id].tests,
|
|
1346
|
-
}));
|
|
1347
|
-
|
|
1348
|
-
const passedData = workerIds.map((id) => workerData[id].passed);
|
|
1349
|
-
const failedData = workerIds.map((id) => workerData[id].failed);
|
|
1350
|
-
const skippedData = workerIds.map((id) => workerData[id].skipped);
|
|
1351
|
-
|
|
1352
|
-
const categoriesString = JSON.stringify(categories);
|
|
1353
|
-
const fullDataString = JSON.stringify(fullWorkerData);
|
|
1354
|
-
const seriesString = JSON.stringify([
|
|
1355
|
-
{ name: "Passed", data: passedData, color: "var(--success-color)" },
|
|
1356
|
-
{ name: "Failed", data: failedData, color: "var(--danger-color)" },
|
|
1357
|
-
{ name: "Skipped", data: skippedData, color: "var(--warning-color)" },
|
|
1358
|
-
]);
|
|
1698
|
+
// btoa is not available in Node.js environment, so we define a simple polyfill for it.
|
|
1699
|
+
const btoa = (str) => Buffer.from(str).toString("base64");
|
|
1359
1700
|
|
|
1360
|
-
// The HTML now includes the chart container, the modal, and styles for the modal
|
|
1361
1701
|
return `
|
|
1362
|
-
<
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1702
|
+
<h2 class="tab-main-title">AI Failure Analysis</h2>
|
|
1703
|
+
<div class="ai-analyzer-stats">
|
|
1704
|
+
<div class="stat-item">
|
|
1705
|
+
<span class="stat-number">${failedTests.length}</span>
|
|
1706
|
+
<span class="stat-label">Failed Tests</span>
|
|
1707
|
+
</div>
|
|
1708
|
+
<div class="stat-item">
|
|
1709
|
+
<span class="stat-number">${
|
|
1710
|
+
new Set(failedTests.map((t) => t.browser)).size
|
|
1711
|
+
}</span>
|
|
1712
|
+
<span class="stat-label">Browsers</span>
|
|
1713
|
+
</div>
|
|
1714
|
+
<div class="stat-item">
|
|
1715
|
+
<span class="stat-number">${Math.round(
|
|
1716
|
+
failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) /
|
|
1717
|
+
1000
|
|
1718
|
+
)}s</span>
|
|
1719
|
+
<span class="stat-label">Total Duration</span>
|
|
1720
|
+
</div>
|
|
1721
|
+
</div>
|
|
1722
|
+
<p class="ai-analyzer-description">
|
|
1723
|
+
Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for specific failed test.
|
|
1724
|
+
</p>
|
|
1725
|
+
|
|
1726
|
+
<div class="compact-failure-list">
|
|
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 `
|
|
1736
|
+
<div class="compact-failure-item">
|
|
1737
|
+
<div class="failure-header">
|
|
1738
|
+
<div class="failure-main-info">
|
|
1739
|
+
<h3 class="failure-title" title="${sanitizeHTML(
|
|
1740
|
+
test.name
|
|
1741
|
+
)}">${sanitizeHTML(testTitle)}</h3>
|
|
1742
|
+
<div class="failure-meta">
|
|
1743
|
+
<span class="browser-indicator">${sanitizeHTML(
|
|
1744
|
+
test.browser || "unknown"
|
|
1745
|
+
)}</span>
|
|
1746
|
+
<span class="duration-indicator">${formatDuration(
|
|
1747
|
+
test.duration
|
|
1748
|
+
)}</span>
|
|
1749
|
+
</div>
|
|
1750
|
+
</div>
|
|
1751
|
+
<button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
|
|
1752
|
+
<span class="ai-text">AI Fix</span>
|
|
1753
|
+
</button>
|
|
1754
|
+
</div>
|
|
1755
|
+
<div class="failure-error-preview">
|
|
1756
|
+
<div class="error-snippet">${formatPlaywrightError(
|
|
1757
|
+
truncatedError
|
|
1758
|
+
)}</div>
|
|
1759
|
+
<button class="expand-error-btn" onclick="toggleErrorDetails(this)">
|
|
1760
|
+
<span class="expand-text">Show Full Error</span>
|
|
1761
|
+
<span class="expand-icon">▼</span>
|
|
1762
|
+
</button>
|
|
1763
|
+
</div>
|
|
1764
|
+
<div class="full-error-details" style="display: none;">
|
|
1765
|
+
<div class="full-error-content">
|
|
1766
|
+
${formatPlaywrightError(
|
|
1767
|
+
test.errorMessage || "No detailed error message available"
|
|
1768
|
+
)}
|
|
1769
|
+
</div>
|
|
1770
|
+
</div>
|
|
1771
|
+
</div>
|
|
1772
|
+
`;
|
|
1773
|
+
})
|
|
1774
|
+
.join("")}
|
|
1404
1775
|
</div>
|
|
1405
1776
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
<
|
|
1410
|
-
|
|
1777
|
+
<!-- AI Fix Modal -->
|
|
1778
|
+
<div id="ai-fix-modal" class="ai-modal-overlay" onclick="closeAiModal()">
|
|
1779
|
+
<div class="ai-modal-content" onclick="event.stopPropagation()">
|
|
1780
|
+
<div class="ai-modal-header">
|
|
1781
|
+
<h3 id="ai-fix-modal-title">AI Analysis</h3>
|
|
1782
|
+
<span class="ai-modal-close" onclick="closeAiModal()">×</span>
|
|
1783
|
+
</div>
|
|
1784
|
+
<div class="ai-modal-body" id="ai-fix-modal-content">
|
|
1785
|
+
<!-- Content will be injected by JavaScript -->
|
|
1786
|
+
</div>
|
|
1411
1787
|
</div>
|
|
1412
1788
|
</div>
|
|
1413
|
-
|
|
1414
|
-
<script>
|
|
1415
|
-
// Namespace for modal functions to avoid global scope pollution
|
|
1416
|
-
window.${modalJsNamespace} = {};
|
|
1417
|
-
|
|
1418
|
-
window.${renderFunctionName} = function() {
|
|
1419
|
-
const chartContainer = document.getElementById('${chartId}');
|
|
1420
|
-
if (!chartContainer) { console.error("Chart container ${chartId} not found."); return; }
|
|
1421
|
-
|
|
1422
|
-
// --- Modal Setup ---
|
|
1423
|
-
const modal = document.getElementById('worker-modal-${chartId}');
|
|
1424
|
-
const modalTitle = document.getElementById('worker-modal-title-${chartId}');
|
|
1425
|
-
const modalBody = document.getElementById('worker-modal-body-${chartId}');
|
|
1426
|
-
const closeModalBtn = modal.querySelector('.worker-modal-close');
|
|
1427
|
-
|
|
1428
|
-
window.${modalJsNamespace}.open = function(worker) {
|
|
1429
|
-
if (!worker) return;
|
|
1430
|
-
modalTitle.textContent = 'Test Details for ' + worker.name;
|
|
1431
|
-
|
|
1432
|
-
let testListHtml = '<ul>';
|
|
1433
|
-
if (worker.tests && worker.tests.length > 0) {
|
|
1434
|
-
worker.tests.forEach(test => {
|
|
1435
|
-
let color = 'inherit';
|
|
1436
|
-
if (test.status === 'passed') color = 'var(--success-color)';
|
|
1437
|
-
else if (test.status === 'failed') color = 'var(--danger-color)';
|
|
1438
|
-
else if (test.status === 'skipped') color = 'var(--warning-color)';
|
|
1439
|
-
|
|
1440
|
-
const escapedName = test.name.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1441
|
-
testListHtml += \`<li style="color: \${color};"><span style="color: \${color}">[\${test.status.toUpperCase()}]</span> \${escapedName}</li>\`;
|
|
1442
|
-
});
|
|
1443
|
-
} else {
|
|
1444
|
-
testListHtml += '<li>No detailed test data available for this worker.</li>';
|
|
1445
|
-
}
|
|
1446
|
-
testListHtml += '</ul>';
|
|
1447
|
-
|
|
1448
|
-
modalBody.innerHTML = testListHtml;
|
|
1449
|
-
modal.style.display = 'flex';
|
|
1450
|
-
};
|
|
1451
|
-
|
|
1452
|
-
const closeModal = function() {
|
|
1453
|
-
modal.style.display = 'none';
|
|
1454
|
-
};
|
|
1455
|
-
|
|
1456
|
-
closeModalBtn.onclick = closeModal;
|
|
1457
|
-
modal.onclick = function(event) {
|
|
1458
|
-
// Close if clicked on the dark overlay background
|
|
1459
|
-
if (event.target == modal) {
|
|
1460
|
-
closeModal();
|
|
1461
|
-
}
|
|
1462
|
-
};
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
// --- Highcharts Setup ---
|
|
1466
|
-
if (typeof Highcharts !== 'undefined') {
|
|
1467
|
-
try {
|
|
1468
|
-
chartContainer.innerHTML = '';
|
|
1469
|
-
const fullData = ${fullDataString};
|
|
1470
|
-
|
|
1471
|
-
const chartOptions = {
|
|
1472
|
-
chart: { type: 'bar', height: 350, backgroundColor: 'transparent' },
|
|
1473
|
-
title: { text: null },
|
|
1474
|
-
xAxis: {
|
|
1475
|
-
categories: ${categoriesString},
|
|
1476
|
-
title: { text: 'Worker ID' },
|
|
1477
|
-
labels: { style: { color: 'var(--text-color-secondary)' }}
|
|
1478
|
-
},
|
|
1479
|
-
yAxis: {
|
|
1480
|
-
min: 0,
|
|
1481
|
-
title: { text: 'Number of Tests' },
|
|
1482
|
-
labels: { style: { color: 'var(--text-color-secondary)' }},
|
|
1483
|
-
stackLabels: { enabled: true, style: { fontWeight: 'bold', color: 'var(--text-color)' } }
|
|
1484
|
-
},
|
|
1485
|
-
legend: { reversed: true, itemStyle: { fontSize: "12px", color: 'var(--text-color)' } },
|
|
1486
|
-
plotOptions: {
|
|
1487
|
-
series: {
|
|
1488
|
-
stacking: 'normal',
|
|
1489
|
-
cursor: 'pointer',
|
|
1490
|
-
point: {
|
|
1491
|
-
events: {
|
|
1492
|
-
click: function () {
|
|
1493
|
-
// 'this.x' is the index of the category
|
|
1494
|
-
const workerData = fullData[this.x];
|
|
1495
|
-
window.${modalJsNamespace}.open(workerData);
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
},
|
|
1501
|
-
tooltip: {
|
|
1502
|
-
shared: true,
|
|
1503
|
-
headerFormat: '<b>{point.key}</b> (Click for details)<br/>',
|
|
1504
|
-
pointFormat: '<span style="color:{series.color}">●</span> {series.name}: <b>{point.y}</b><br/>',
|
|
1505
|
-
footerFormat: 'Total: <b>{point.total}</b>'
|
|
1506
|
-
},
|
|
1507
|
-
series: ${seriesString},
|
|
1508
|
-
credits: { enabled: false }
|
|
1509
|
-
};
|
|
1510
|
-
Highcharts.chart('${chartId}', chartOptions);
|
|
1511
|
-
} catch (e) {
|
|
1512
|
-
console.error("Error rendering chart ${chartId}:", e);
|
|
1513
|
-
chartContainer.innerHTML = '<div class="no-data">Error rendering worker distribution chart.</div>';
|
|
1514
|
-
}
|
|
1515
|
-
} else {
|
|
1516
|
-
chartContainer.innerHTML = '<div class="no-data">Charting library not available for worker distribution.</div>';
|
|
1517
|
-
}
|
|
1518
|
-
};
|
|
1519
|
-
</script>
|
|
1520
1789
|
`;
|
|
1521
1790
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
<script>
|
|
1529
|
-
window.workerInfoPrompt = function() {
|
|
1530
|
-
const message = 'Why is worker -1 special?\\n\\n' +
|
|
1531
|
-
'Playwright assigns skipped tests to worker -1 because:\\n' +
|
|
1532
|
-
'1. They don\\'t require browser execution\\n' +
|
|
1533
|
-
'2. This keeps real workers focused on actual tests\\n' +
|
|
1534
|
-
'3. Maintains clean reporting\\n\\n' +
|
|
1535
|
-
'This is an intentional optimization by Playwright.';
|
|
1536
|
-
alert(message);
|
|
1537
|
-
}
|
|
1538
|
-
</script>
|
|
1539
|
-
`;
|
|
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
|
+
*/
|
|
1540
1797
|
function generateHTML(reportData, trendData = null) {
|
|
1541
1798
|
const { run, results } = reportData;
|
|
1542
1799
|
const suitesData = getSuitesData(reportData.results || []);
|
|
@@ -1559,11 +1816,16 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1559
1816
|
? formatDuration(runSummary.duration / runSummary.totalTests)
|
|
1560
1817
|
: "0.0s";
|
|
1561
1818
|
|
|
1562
|
-
|
|
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) {
|
|
1563
1824
|
if (!results || results.length === 0)
|
|
1564
1825
|
return '<div class="no-tests">No test results found in this run.</div>';
|
|
1565
|
-
return
|
|
1566
|
-
.map((test) => {
|
|
1826
|
+
return subset
|
|
1827
|
+
.map((test, i) => {
|
|
1828
|
+
const testIndex = baseIndex + i;
|
|
1567
1829
|
const browser = test.browser || "unknown";
|
|
1568
1830
|
const testFileParts = test.name.split(" > ");
|
|
1569
1831
|
const testTitle =
|
|
@@ -1601,6 +1863,42 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1601
1863
|
: ""
|
|
1602
1864
|
}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
|
|
1603
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
|
+
})()
|
|
1604
1902
|
}${
|
|
1605
1903
|
hasNestedSteps
|
|
1606
1904
|
? `<div class="nested-steps">${generateStepsHTML(
|
|
@@ -1618,7 +1916,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1618
1916
|
test.tags || []
|
|
1619
1917
|
)
|
|
1620
1918
|
.join(",")
|
|
1621
|
-
.toLowerCase()}">
|
|
1919
|
+
.toLowerCase()}" data-test-id="${sanitizeHTML(String(test.id || testIndex))}">
|
|
1622
1920
|
<div class="test-case-header" role="button" aria-expanded="false"><div class="test-case-summary"><span class="status-badge ${getStatusClass(
|
|
1623
1921
|
test.status
|
|
1624
1922
|
)}">${String(
|
|
@@ -1651,25 +1949,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1651
1949
|
test.errorMessage
|
|
1652
1950
|
? `<div class="test-error-summary">${formatPlaywrightError(
|
|
1653
1951
|
test.errorMessage
|
|
1654
|
-
)}<button
|
|
1655
|
-
class="copy-error-btn"
|
|
1656
|
-
onclick="copyErrorToClipboard(this)"
|
|
1657
|
-
style="
|
|
1658
|
-
margin-top: 8px;
|
|
1659
|
-
padding: 4px 8px;
|
|
1660
|
-
background: #f0f0f0;
|
|
1661
|
-
border: 2px solid #ccc;
|
|
1662
|
-
border-radius: 4px;
|
|
1663
|
-
cursor: pointer;
|
|
1664
|
-
font-size: 12px;
|
|
1665
|
-
border-color: #8B0000;
|
|
1666
|
-
color: #8B0000;
|
|
1667
|
-
"
|
|
1668
|
-
onmouseover="this.style.background='#e0e0e0'"
|
|
1669
|
-
onmouseout="this.style.background='#f0f0f0'"
|
|
1670
|
-
>
|
|
1671
|
-
Copy Error Prompt
|
|
1672
|
-
</button></div>`
|
|
1952
|
+
)}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
|
|
1673
1953
|
: ""
|
|
1674
1954
|
}
|
|
1675
1955
|
${
|
|
@@ -1702,9 +1982,12 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1702
1982
|
})()}
|
|
1703
1983
|
${
|
|
1704
1984
|
test.stderr && test.stderr.length > 0
|
|
1705
|
-
?
|
|
1706
|
-
.
|
|
1707
|
-
|
|
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
|
+
})()
|
|
1708
1991
|
: ""
|
|
1709
1992
|
}
|
|
1710
1993
|
|
|
@@ -1729,7 +2012,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1729
2012
|
readFileSync(imagePath).toString("base64");
|
|
1730
2013
|
return `<div class="attachment-item"><img src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" data-src="data:image/png;base64,${base64ImageData}" alt="Screenshot ${
|
|
1731
2014
|
index + 1
|
|
1732
|
-
}" 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>`;
|
|
1733
2016
|
} catch (e) {
|
|
1734
2017
|
return `<div class="attachment-item error">Failed to load screenshot: ${sanitizeHTML(
|
|
1735
2018
|
screenshotPath
|
|
@@ -1770,7 +2053,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1770
2053
|
avi: "video/x-msvideo",
|
|
1771
2054
|
}[fileExtension] || "video/mp4";
|
|
1772
2055
|
const videoDataUri = `data:${mimeType};base64,${videoBase64}`;
|
|
1773
|
-
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>`;
|
|
1774
2057
|
} catch (e) {
|
|
1775
2058
|
return `<div class="attachment-item error">Failed to load video: ${sanitizeHTML(
|
|
1776
2059
|
videoPath
|
|
@@ -1846,8 +2129,8 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1846
2129
|
</div>
|
|
1847
2130
|
<div class="attachment-info">
|
|
1848
2131
|
<div class="trace-actions">
|
|
1849
|
-
<a href="${attachmentDataUri}"
|
|
1850
|
-
<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(
|
|
1851
2134
|
attachment.name
|
|
1852
2135
|
)}">Download</a>
|
|
1853
2136
|
</div>
|
|
@@ -1887,172 +2170,240 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1887
2170
|
<link rel="icon" type="image/png" href="https://i.postimg.cc/v817w4sg/logo.png">
|
|
1888
2171
|
<link rel="apple-touch-icon" href="https://i.postimg.cc/v817w4sg/logo.png">
|
|
1889
2172
|
<script src="https://code.highcharts.com/highcharts.js" defer></script>
|
|
1890
|
-
<title>Playwright Pulse Report</title>
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2173
|
+
<title>Playwright Pulse Report (Static Report)</title>
|
|
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>
|
|
2056
2407
|
</head>
|
|
2057
2408
|
<body>
|
|
2058
2409
|
<div class="container">
|
|
@@ -2071,7 +2422,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2071
2422
|
<button class="tab-button active" data-tab="dashboard">Dashboard</button>
|
|
2072
2423
|
<button class="tab-button" data-tab="test-runs">Test Run Summary</button>
|
|
2073
2424
|
<button class="tab-button" data-tab="test-history">Test History</button>
|
|
2074
|
-
<button class="tab-button" data-tab="
|
|
2425
|
+
<button class="tab-button" data-tab="ai-failure-analyzer">AI Failure Analyzer</button>
|
|
2075
2426
|
</div>
|
|
2076
2427
|
<div id="dashboard" class="tab-content active">
|
|
2077
2428
|
<div class="dashboard-grid">
|
|
@@ -2131,7 +2482,8 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2131
2482
|
.join("")}</select>
|
|
2132
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>
|
|
2133
2484
|
</div>
|
|
2134
|
-
<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>` : ``}
|
|
2135
2487
|
</div>
|
|
2136
2488
|
<div id="test-history" class="tab-content">
|
|
2137
2489
|
<h2 class="tab-main-title">Execution Trends</h2>
|
|
@@ -2166,8 +2518,8 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2166
2518
|
: '<div class="no-data">Individual test history data not available.</div>'
|
|
2167
2519
|
}
|
|
2168
2520
|
</div>
|
|
2169
|
-
<div id="
|
|
2170
|
-
|
|
2521
|
+
<div id="ai-failure-analyzer" class="tab-content">
|
|
2522
|
+
${generateAIFailureAnalyzerTab(results)}
|
|
2171
2523
|
</div>
|
|
2172
2524
|
<footer style="padding: 0.5rem; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); text-align: center; font-family: 'Segoe UI', system-ui, sans-serif;">
|
|
2173
2525
|
<div style="display: inline-flex; align-items: center; gap: 0.5rem; color: #333; font-size: 0.9rem; font-weight: 600; letter-spacing: 0.5px;">
|
|
@@ -2199,7 +2551,127 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2199
2551
|
button.textContent = 'Failed';
|
|
2200
2552
|
setTimeout(() => { button.textContent = 'Copy'; }, 2000);
|
|
2201
2553
|
});
|
|
2202
|
-
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
function getAIFix(button) {
|
|
2557
|
+
const modal = document.getElementById('ai-fix-modal');
|
|
2558
|
+
const modalContent = document.getElementById('ai-fix-modal-content');
|
|
2559
|
+
const modalTitle = document.getElementById('ai-fix-modal-title');
|
|
2560
|
+
|
|
2561
|
+
modal.style.display = 'flex';
|
|
2562
|
+
modalTitle.textContent = 'Analyzing...';
|
|
2563
|
+
modalContent.innerHTML = '<div class="ai-loader"></div>';
|
|
2564
|
+
|
|
2565
|
+
try {
|
|
2566
|
+
const testJson = button.dataset.testJson;
|
|
2567
|
+
const test = JSON.parse(atob(testJson));
|
|
2568
|
+
|
|
2569
|
+
const testName = test.name || 'Unknown Test';
|
|
2570
|
+
const failureLogsAndErrors = [
|
|
2571
|
+
'Error Message:',
|
|
2572
|
+
test.errorMessage || 'Not available.',
|
|
2573
|
+
'\\n\\n--- stdout ---',
|
|
2574
|
+
(test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
|
|
2575
|
+
'\\n\\n--- stderr ---',
|
|
2576
|
+
(test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
|
|
2577
|
+
].join('\\n');
|
|
2578
|
+
const codeSnippet = test.snippet || '';
|
|
2579
|
+
|
|
2580
|
+
const shortTestName = testName.split(' > ').pop();
|
|
2581
|
+
modalTitle.textContent = \`Analysis for: \${shortTestName}\`;
|
|
2582
|
+
|
|
2583
|
+
const apiUrl = 'https://ai-test-analyser.netlify.app/api/analyze';
|
|
2584
|
+
fetch(apiUrl, {
|
|
2585
|
+
method: 'POST',
|
|
2586
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2587
|
+
body: JSON.stringify({
|
|
2588
|
+
testName: testName,
|
|
2589
|
+
failureLogsAndErrors: failureLogsAndErrors,
|
|
2590
|
+
codeSnippet: codeSnippet,
|
|
2591
|
+
}),
|
|
2592
|
+
})
|
|
2593
|
+
.then(response => {
|
|
2594
|
+
if (!response.ok) {
|
|
2595
|
+
return response.text().then(text => {
|
|
2596
|
+
throw new Error(\`API request failed with status \${response.status}: \${text || response.statusText}\`);
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
return response.text();
|
|
2600
|
+
})
|
|
2601
|
+
.then(text => {
|
|
2602
|
+
if (!text) {
|
|
2603
|
+
throw new Error("The AI analyzer returned an empty response. This might happen during high load or if the request was blocked. Please try again in a moment.");
|
|
2604
|
+
}
|
|
2605
|
+
try {
|
|
2606
|
+
return JSON.parse(text);
|
|
2607
|
+
} catch (e) {
|
|
2608
|
+
console.error("Failed to parse JSON:", text);
|
|
2609
|
+
throw new Error(\`The AI analyzer returned an invalid response. \${e.message}\`);
|
|
2610
|
+
}
|
|
2611
|
+
})
|
|
2612
|
+
.then(data => {
|
|
2613
|
+
const escapeHtml = (unsafe) => {
|
|
2614
|
+
if (typeof unsafe !== 'string') return '';
|
|
2615
|
+
return unsafe
|
|
2616
|
+
.replace(/&/g, "&")
|
|
2617
|
+
.replace(/</g, "<")
|
|
2618
|
+
.replace(/>/g, ">")
|
|
2619
|
+
.replace(/"/g, """)
|
|
2620
|
+
.replace(/'/g, "'");
|
|
2621
|
+
};
|
|
2622
|
+
|
|
2623
|
+
const analysisHtml = \`<h4>Analysis</h4><p>\${escapeHtml(data.rootCause) || 'No analysis provided.'}</p>\`;
|
|
2624
|
+
|
|
2625
|
+
let suggestionsHtml = '<h4>Suggestions</h4>';
|
|
2626
|
+
if (data.suggestedFixes && data.suggestedFixes.length > 0) {
|
|
2627
|
+
suggestionsHtml += '<div class="suggestions-list" style="margin-top: 15px;">';
|
|
2628
|
+
data.suggestedFixes.forEach(fix => {
|
|
2629
|
+
suggestionsHtml += \`
|
|
2630
|
+
<div class="suggestion-item" style="margin-bottom: 22px; border-left: 3px solid var(--accent-color-alt); padding-left: 15px;">
|
|
2631
|
+
<p style="margin: 0 0 8px 0; font-weight: 500;">\${escapeHtml(fix.description)}</p>
|
|
2632
|
+
\${fix.codeSnippet ? \`<div class="code-section"><pre><code>\${escapeHtml(fix.codeSnippet)}</code></pre></div>\` : ''}
|
|
2633
|
+
</div>
|
|
2634
|
+
\`;
|
|
2635
|
+
});
|
|
2636
|
+
suggestionsHtml += '</div>';
|
|
2637
|
+
} else {
|
|
2638
|
+
suggestionsHtml += \`<div class="code-section"><pre><code>No suggestion provided.</code></pre></div>\`;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
modalContent.innerHTML = analysisHtml + suggestionsHtml;
|
|
2642
|
+
})
|
|
2643
|
+
.catch(err => {
|
|
2644
|
+
console.error('AI Fix Error:', err);
|
|
2645
|
+
modalContent.innerHTML = \`<div class="test-error-summary"><strong>Error:</strong> Failed to get AI analysis. Please check the console for details. <br><br> \${err.message}</div>\`;
|
|
2646
|
+
});
|
|
2647
|
+
|
|
2648
|
+
} catch (e) {
|
|
2649
|
+
console.error('Error processing test data for AI Fix:', e);
|
|
2650
|
+
modalTitle.textContent = 'Error';
|
|
2651
|
+
modalContent.innerHTML = \`<div class="test-error-summary">Could not process test data. Is it formatted correctly?</div>\`;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
function closeAiModal() {
|
|
2656
|
+
const modal = document.getElementById('ai-fix-modal');
|
|
2657
|
+
if(modal) modal.style.display = 'none';
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
function toggleErrorDetails(button) {
|
|
2661
|
+
const errorDetails = button.closest('.compact-failure-item').querySelector('.full-error-details');
|
|
2662
|
+
const expandText = button.querySelector('.expand-text');
|
|
2663
|
+
|
|
2664
|
+
if (errorDetails.style.display === 'none' || !errorDetails.style.display) {
|
|
2665
|
+
errorDetails.style.display = 'block';
|
|
2666
|
+
expandText.textContent = 'Hide Full Error';
|
|
2667
|
+
button.classList.add('expanded');
|
|
2668
|
+
} else {
|
|
2669
|
+
errorDetails.style.display = 'none';
|
|
2670
|
+
expandText.textContent = 'Show Full Error';
|
|
2671
|
+
button.classList.remove('expanded');
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2203
2675
|
function initializeReportInteractivity() {
|
|
2204
2676
|
const tabButtons = document.querySelectorAll('.tab-button');
|
|
2205
2677
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
@@ -2212,18 +2684,51 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2212
2684
|
const activeContent = document.getElementById(tabId);
|
|
2213
2685
|
if (activeContent) {
|
|
2214
2686
|
activeContent.classList.add('active');
|
|
2215
|
-
// Check if IntersectionObserver is already handling elements in this tab
|
|
2216
|
-
// For simplicity, we assume if an element is observed, it will be handled when it becomes visible.
|
|
2217
|
-
// If IntersectionObserver is not supported, already-visible elements would have been loaded by fallback.
|
|
2218
2687
|
}
|
|
2219
2688
|
});
|
|
2220
2689
|
});
|
|
2221
2690
|
// --- Test Run Summary Filters ---
|
|
2222
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
|
+
|
|
2223
2727
|
const statusFilter = document.getElementById('filter-status');
|
|
2224
2728
|
const browserFilter = document.getElementById('filter-browser');
|
|
2225
2729
|
const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters');
|
|
2226
2730
|
function filterTestCases() {
|
|
2731
|
+
ensureAllTestsAppended();
|
|
2227
2732
|
const nameValue = nameFilter ? nameFilter.value.toLowerCase() : "";
|
|
2228
2733
|
const statusValue = statusFilter ? statusFilter.value : "";
|
|
2229
2734
|
const browserValue = browserFilter ? browserFilter.value : "";
|
|
@@ -2242,7 +2747,10 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2242
2747
|
if(statusFilter) statusFilter.addEventListener('change', filterTestCases);
|
|
2243
2748
|
if(browserFilter) browserFilter.addEventListener('change', filterTestCases);
|
|
2244
2749
|
if(clearRunSummaryFiltersBtn) clearRunSummaryFiltersBtn.addEventListener('click', () => {
|
|
2245
|
-
|
|
2750
|
+
ensureAllTestsAppended();
|
|
2751
|
+
if(nameFilter) nameFilter.value = '';
|
|
2752
|
+
if(statusFilter) statusFilter.value = '';
|
|
2753
|
+
if(browserFilter) browserFilter.value = '';
|
|
2246
2754
|
filterTestCases();
|
|
2247
2755
|
});
|
|
2248
2756
|
// --- Test History Filters ---
|
|
@@ -2283,12 +2791,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2283
2791
|
headerElement.setAttribute('aria-expanded', String(!isExpanded));
|
|
2284
2792
|
}
|
|
2285
2793
|
}
|
|
2286
|
-
document.querySelectorAll('#test-runs .test-case-header').forEach(header => {
|
|
2287
|
-
header.addEventListener('click', () => toggleElementDetails(header));
|
|
2288
|
-
});
|
|
2289
|
-
document.querySelectorAll('#test-runs .step-header').forEach(header => {
|
|
2290
|
-
header.addEventListener('click', () => toggleElementDetails(header, '.step-details'));
|
|
2291
|
-
});
|
|
2292
2794
|
const expandAllBtn = document.getElementById('expand-all-tests');
|
|
2293
2795
|
const collapseAllBtn = document.getElementById('collapse-all-tests');
|
|
2294
2796
|
function setAllTestRunDetailsVisibility(displayMode, ariaState) {
|
|
@@ -2299,31 +2801,89 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2299
2801
|
}
|
|
2300
2802
|
if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
|
|
2301
2803
|
if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
|
|
2302
|
-
|
|
2303
|
-
|
|
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');
|
|
2304
2881
|
if ('IntersectionObserver' in window) {
|
|
2305
2882
|
let lazyObserver = new IntersectionObserver((entries, observer) => {
|
|
2306
2883
|
entries.forEach(entry => {
|
|
2307
2884
|
if (entry.isIntersecting) {
|
|
2308
2885
|
const element = entry.target;
|
|
2309
|
-
if (element.classList.contains('lazy-load-
|
|
2310
|
-
if (element.dataset.src) {
|
|
2311
|
-
element.src = element.dataset.src;
|
|
2312
|
-
element.removeAttribute('data-src');
|
|
2313
|
-
}
|
|
2314
|
-
} else if (element.classList.contains('lazy-load-video')) {
|
|
2315
|
-
const source = element.querySelector('source');
|
|
2316
|
-
if (source && source.dataset.src) {
|
|
2317
|
-
source.src = source.dataset.src;
|
|
2318
|
-
source.removeAttribute('data-src');
|
|
2319
|
-
element.load();
|
|
2320
|
-
}
|
|
2321
|
-
} else if (element.classList.contains('lazy-load-attachment')) {
|
|
2322
|
-
if (element.dataset.href) {
|
|
2323
|
-
element.href = element.dataset.href;
|
|
2324
|
-
element.removeAttribute('data-href');
|
|
2325
|
-
}
|
|
2326
|
-
} else if (element.classList.contains('lazy-load-iframe')) {
|
|
2886
|
+
if (element.classList.contains('lazy-load-iframe')) {
|
|
2327
2887
|
if (element.dataset.src) {
|
|
2328
2888
|
element.src = element.dataset.src;
|
|
2329
2889
|
element.removeAttribute('data-src');
|
|
@@ -2339,78 +2899,46 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2339
2899
|
});
|
|
2340
2900
|
}, { rootMargin: "0px 0px 200px 0px" });
|
|
2341
2901
|
lazyLoadElements.forEach(el => lazyObserver.observe(el));
|
|
2342
|
-
} else {
|
|
2902
|
+
} else {
|
|
2343
2903
|
lazyLoadElements.forEach(element => {
|
|
2344
|
-
if (element.classList.contains('lazy-load-
|
|
2345
|
-
else if (element.classList.contains('lazy-load-
|
|
2346
|
-
else if (element.classList.contains('lazy-load-attachment') && element.dataset.href) element.href = element.dataset.href;
|
|
2347
|
-
else if (element.classList.contains('lazy-load-iframe') && element.dataset.src) element.src = element.dataset.src;
|
|
2348
|
-
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](); }
|
|
2349
2906
|
});
|
|
2350
2907
|
}
|
|
2351
2908
|
}
|
|
2352
2909
|
document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
|
|
2353
2910
|
|
|
2354
2911
|
function copyErrorToClipboard(button) {
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2912
|
+
const errorContainer = button.closest('.test-error-summary');
|
|
2913
|
+
if (!errorContainer) {
|
|
2914
|
+
console.error("Could not find '.test-error-summary' container.");
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
let errorText;
|
|
2918
|
+
const stackTraceElement = errorContainer.querySelector('.stack-trace');
|
|
2919
|
+
if (stackTraceElement) {
|
|
2920
|
+
errorText = stackTraceElement.textContent;
|
|
2921
|
+
} else {
|
|
2922
|
+
const clonedContainer = errorContainer.cloneNode(true);
|
|
2923
|
+
const buttonInClone = clonedContainer.querySelector('button');
|
|
2924
|
+
if (buttonInClone) buttonInClone.remove();
|
|
2925
|
+
errorText = clonedContainer.textContent;
|
|
2926
|
+
}
|
|
2366
2927
|
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2928
|
+
if (!errorText) {
|
|
2929
|
+
button.textContent = 'Nothing to copy';
|
|
2930
|
+
setTimeout(() => { button.textContent = 'Copy Error'; }, 2000);
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
navigator.clipboard.writeText(errorText.trim()).then(() => {
|
|
2934
|
+
const originalText = button.textContent;
|
|
2935
|
+
button.textContent = 'Copied!';
|
|
2936
|
+
setTimeout(() => { button.textContent = originalText; }, 2000);
|
|
2937
|
+
}).catch(err => {
|
|
2938
|
+
console.error('Failed to copy: ', err);
|
|
2939
|
+
button.textContent = 'Failed';
|
|
2940
|
+
});
|
|
2379
2941
|
}
|
|
2380
|
-
|
|
2381
|
-
// Use the text content of the cleaned container as the fallback.
|
|
2382
|
-
errorText = clonedContainer.textContent;
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
// 4. Proceed with the clipboard logic, ensuring text is not null and is trimmed.
|
|
2386
|
-
if (!errorText) {
|
|
2387
|
-
console.error('Could not extract error text.');
|
|
2388
|
-
button.textContent = 'Nothing to copy';
|
|
2389
|
-
setTimeout(() => { button.textContent = 'Copy Error'; }, 2000);
|
|
2390
|
-
return;
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
const textarea = document.createElement('textarea');
|
|
2394
|
-
textarea.value = errorText.trim(); // Trim whitespace for a cleaner copy.
|
|
2395
|
-
textarea.style.position = 'fixed'; // Prevent screen scroll
|
|
2396
|
-
textarea.style.top = '-9999px';
|
|
2397
|
-
document.body.appendChild(textarea);
|
|
2398
|
-
textarea.select();
|
|
2399
|
-
|
|
2400
|
-
try {
|
|
2401
|
-
const successful = document.execCommand('copy');
|
|
2402
|
-
const originalText = button.textContent;
|
|
2403
|
-
button.textContent = successful ? 'Copied!' : 'Failed';
|
|
2404
|
-
setTimeout(() => {
|
|
2405
|
-
button.textContent = originalText;
|
|
2406
|
-
}, 2000);
|
|
2407
|
-
} catch (err) {
|
|
2408
|
-
console.error('Failed to copy: ', err);
|
|
2409
|
-
button.textContent = 'Failed';
|
|
2410
|
-
}
|
|
2411
|
-
|
|
2412
|
-
document.body.removeChild(textarea);
|
|
2413
|
-
}
|
|
2414
2942
|
</script>
|
|
2415
2943
|
</body>
|
|
2416
2944
|
</html>
|
|
@@ -2440,6 +2968,11 @@ async function runScript(scriptPath) {
|
|
|
2440
2968
|
});
|
|
2441
2969
|
});
|
|
2442
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
|
+
*/
|
|
2443
2976
|
async function main() {
|
|
2444
2977
|
const __filename = fileURLToPath(import.meta.url);
|
|
2445
2978
|
const __dirname = path.dirname(__filename);
|
|
@@ -2630,4 +3163,4 @@ main().catch((err) => {
|
|
|
2630
3163
|
);
|
|
2631
3164
|
console.error(err.stack);
|
|
2632
3165
|
process.exit(1);
|
|
2633
|
-
});
|
|
3166
|
+
});
|