@arghajit/playwright-pulse-report 0.2.4 → 0.2.6
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 +33 -34
- package/dist/reporter/playwright-pulse-reporter.js +5 -4
- package/dist/reporter/tsconfig.reporter.tsbuildinfo +1 -0
- package/dist/types/index.d.ts +2 -1
- package/package.json +1 -1
- package/scripts/generate-email-report.mjs +6 -6
- package/scripts/generate-report.mjs +1017 -324
- package/scripts/generate-static-report.mjs +1390 -497
|
@@ -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,7 +163,11 @@ export function ansiToHtml(text) {
|
|
|
141
163
|
}
|
|
142
164
|
return html;
|
|
143
165
|
}
|
|
144
|
-
|
|
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
|
+
*/
|
|
145
171
|
function sanitizeHTML(str) {
|
|
146
172
|
if (str === null || str === undefined) return "";
|
|
147
173
|
return String(str).replace(
|
|
@@ -150,33 +176,52 @@ function sanitizeHTML(str) {
|
|
|
150
176
|
({ "&": "&", "<": "<", ">": ">", '"': '"', "'": "'" }[match] || match)
|
|
151
177
|
);
|
|
152
178
|
}
|
|
153
|
-
|
|
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
|
+
*/
|
|
154
184
|
function capitalize(str) {
|
|
155
185
|
if (!str) return "";
|
|
156
186
|
return str[0].toUpperCase() + str.slice(1).toLowerCase();
|
|
157
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
|
+
*/
|
|
158
193
|
function formatPlaywrightError(error) {
|
|
159
194
|
const commandOutput = ansiToHtml(error || error.message);
|
|
160
195
|
return convertPlaywrightErrorToHTML(commandOutput);
|
|
161
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
|
+
*/
|
|
162
202
|
function convertPlaywrightErrorToHTML(str) {
|
|
163
|
-
return
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
.replace(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
.replace(/<\/intensity>/g, "</span>")
|
|
176
|
-
// Convert newlines to <br> after processing other replacements
|
|
177
|
-
.replace(/\n/g, "<br>")
|
|
178
|
-
);
|
|
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>");
|
|
179
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
|
+
*/
|
|
180
225
|
function formatDuration(ms, options = {}) {
|
|
181
226
|
const {
|
|
182
227
|
precision = 1,
|
|
@@ -216,19 +261,12 @@ function formatDuration(ms, options = {}) {
|
|
|
216
261
|
|
|
217
262
|
const totalRawSeconds = numMs / MS_PER_SECOND;
|
|
218
263
|
|
|
219
|
-
// Decision: Are we going to display hours or minutes?
|
|
220
|
-
// This happens if the duration is inherently >= 1 minute OR
|
|
221
|
-
// if it's < 1 minute but ceiling the seconds makes it >= 1 minute.
|
|
222
264
|
if (
|
|
223
265
|
totalRawSeconds < SECONDS_PER_MINUTE &&
|
|
224
266
|
Math.ceil(totalRawSeconds) < SECONDS_PER_MINUTE
|
|
225
267
|
) {
|
|
226
|
-
// Strictly seconds-only display, use precision.
|
|
227
268
|
return `${totalRawSeconds.toFixed(validPrecision)}s`;
|
|
228
269
|
} else {
|
|
229
|
-
// Display will include minutes and/or hours, or seconds round up to a minute.
|
|
230
|
-
// Seconds part should be an integer (ceiling).
|
|
231
|
-
// Round the total milliseconds UP to the nearest full second.
|
|
232
270
|
const totalMsRoundedUpToSecond =
|
|
233
271
|
Math.ceil(numMs / MS_PER_SECOND) * MS_PER_SECOND;
|
|
234
272
|
|
|
@@ -240,26 +278,26 @@ function formatDuration(ms, options = {}) {
|
|
|
240
278
|
const m = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_MINUTE));
|
|
241
279
|
remainingMs %= MS_PER_SECOND * SECONDS_PER_MINUTE;
|
|
242
280
|
|
|
243
|
-
const s = Math.floor(remainingMs / MS_PER_SECOND);
|
|
281
|
+
const s = Math.floor(remainingMs / MS_PER_SECOND);
|
|
244
282
|
|
|
245
283
|
const parts = [];
|
|
246
284
|
if (h > 0) {
|
|
247
285
|
parts.push(`${h}h`);
|
|
248
286
|
}
|
|
249
|
-
|
|
250
|
-
// Show minutes if:
|
|
251
|
-
// - hours are present (e.g., "1h 0m 5s")
|
|
252
|
-
// - OR minutes themselves are > 0 (e.g., "5m 10s")
|
|
253
|
-
// - OR the original duration was >= 1 minute (ensures "1m 0s" for 60000ms)
|
|
254
287
|
if (h > 0 || m > 0 || numMs >= MS_PER_SECOND * SECONDS_PER_MINUTE) {
|
|
255
288
|
parts.push(`${m}m`);
|
|
256
289
|
}
|
|
257
|
-
|
|
258
290
|
parts.push(`${s}s`);
|
|
259
291
|
|
|
260
292
|
return parts.join(" ");
|
|
261
293
|
}
|
|
262
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
|
+
*/
|
|
263
301
|
function generateTestTrendsChart(trendData) {
|
|
264
302
|
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
265
303
|
return '<div class="no-data">No overall trend data available for test counts.</div>';
|
|
@@ -355,6 +393,12 @@ function generateTestTrendsChart(trendData) {
|
|
|
355
393
|
</script>
|
|
356
394
|
`;
|
|
357
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
|
+
*/
|
|
358
402
|
function generateDurationTrendChart(trendData) {
|
|
359
403
|
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
360
404
|
return '<div class="no-data">No overall trend data available for durations.</div>';
|
|
@@ -439,6 +483,11 @@ function generateDurationTrendChart(trendData) {
|
|
|
439
483
|
</script>
|
|
440
484
|
`;
|
|
441
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
|
+
*/
|
|
442
491
|
function formatDate(dateStrOrDate) {
|
|
443
492
|
if (!dateStrOrDate) return "N/A";
|
|
444
493
|
try {
|
|
@@ -457,6 +506,12 @@ function formatDate(dateStrOrDate) {
|
|
|
457
506
|
return "Invalid Date Format";
|
|
458
507
|
}
|
|
459
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
|
+
*/
|
|
460
515
|
function generateTestHistoryChart(history) {
|
|
461
516
|
if (!history || history.length === 0)
|
|
462
517
|
return '<div class="no-data-chart">No data for chart</div>';
|
|
@@ -567,6 +622,13 @@ function generateTestHistoryChart(history) {
|
|
|
567
622
|
</script>
|
|
568
623
|
`;
|
|
569
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
|
+
*/
|
|
570
632
|
function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
571
633
|
const total = data.reduce((sum, d) => sum + d.value, 0);
|
|
572
634
|
if (total === 0) {
|
|
@@ -696,6 +758,12 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
696
758
|
</div>
|
|
697
759
|
`;
|
|
698
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
|
+
*/
|
|
699
767
|
function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
700
768
|
// Format memory for display
|
|
701
769
|
const formattedMemory = environment.memory.replace(/(\d+\.\d{2})GB/, "$1 GB");
|
|
@@ -711,176 +779,176 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
711
779
|
return `
|
|
712
780
|
<div class="environment-dashboard-wrapper" id="${dashboardId}">
|
|
713
781
|
<style>
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
782
|
+
.environment-dashboard-wrapper *,
|
|
783
|
+
.environment-dashboard-wrapper *::before,
|
|
784
|
+
.environment-dashboard-wrapper *::after {
|
|
785
|
+
box-sizing: border-box;
|
|
786
|
+
}
|
|
719
787
|
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
.env-dashboard-header {
|
|
755
|
-
grid-column: 1 / -1;
|
|
756
|
-
display: flex;
|
|
757
|
-
justify-content: space-between;
|
|
758
|
-
align-items: center;
|
|
759
|
-
border-bottom: 1px solid var(--border-color);
|
|
760
|
-
padding-bottom: 16px;
|
|
761
|
-
margin-bottom: 8px;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
.env-dashboard-title {
|
|
765
|
-
font-size: 1.5rem;
|
|
766
|
-
font-weight: 600;
|
|
767
|
-
color: var(--text-color);
|
|
768
|
-
margin: 0;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
.env-dashboard-subtitle {
|
|
772
|
-
font-size: 0.875rem;
|
|
773
|
-
color: var(--text-color-secondary);
|
|
774
|
-
margin-top: 4px;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
.env-card {
|
|
778
|
-
background-color: var(--card-background-color);
|
|
779
|
-
border-radius: 8px;
|
|
780
|
-
padding: ${cardContentPadding}px;
|
|
781
|
-
box-shadow: 0 3px 6px var(--shadow-color);
|
|
782
|
-
height: ${cardHeight}px;
|
|
783
|
-
display: flex;
|
|
784
|
-
flex-direction: column;
|
|
785
|
-
overflow: hidden;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
.env-card-header {
|
|
789
|
-
font-weight: 600;
|
|
790
|
-
font-size: 1rem;
|
|
791
|
-
margin-bottom: 12px;
|
|
792
|
-
color: var(--text-color);
|
|
793
|
-
display: flex;
|
|
794
|
-
align-items: center;
|
|
795
|
-
padding-bottom: 8px;
|
|
796
|
-
border-bottom: 1px solid var(--border-light-color);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
.env-card-header svg {
|
|
800
|
-
margin-right: 10px;
|
|
801
|
-
width: 18px;
|
|
802
|
-
height: 18px;
|
|
803
|
-
fill: var(--icon-color);
|
|
804
|
-
}
|
|
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
|
+
}
|
|
805
821
|
|
|
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
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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>
|
|
884
952
|
|
|
885
953
|
<div class="env-dashboard-header">
|
|
886
954
|
<div>
|
|
@@ -1025,6 +1093,296 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
1025
1093
|
</div>
|
|
1026
1094
|
`;
|
|
1027
1095
|
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Generates a Highcharts bar chart to visualize the distribution of test results across different workers.
|
|
1098
|
+
* @param {Array<object>} results The test results data.
|
|
1099
|
+
* @returns {string} The HTML string for the worker distribution chart and its associated modal.
|
|
1100
|
+
*/
|
|
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>';
|
|
1104
|
+
}
|
|
1105
|
+
|
|
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]++;
|
|
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);
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
if (workerIds.length === 0) {
|
|
1141
|
+
return '<div class="no-data">Could not determine worker distribution from test data.</div>';
|
|
1142
|
+
}
|
|
1143
|
+
|
|
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
|
+
]);
|
|
1174
|
+
|
|
1175
|
+
// The HTML now includes the chart container, the modal, and styles for the modal
|
|
1176
|
+
return `
|
|
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>
|
|
1357
|
+
`;
|
|
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
|
+
*/
|
|
1028
1386
|
function generateTestHistoryContent(trendData) {
|
|
1029
1387
|
if (
|
|
1030
1388
|
!trendData ||
|
|
@@ -1144,6 +1502,11 @@ function generateTestHistoryContent(trendData) {
|
|
|
1144
1502
|
</div>
|
|
1145
1503
|
`;
|
|
1146
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
|
+
*/
|
|
1147
1510
|
function getStatusClass(status) {
|
|
1148
1511
|
switch (String(status).toLowerCase()) {
|
|
1149
1512
|
case "passed":
|
|
@@ -1156,6 +1519,11 @@ function getStatusClass(status) {
|
|
|
1156
1519
|
return "status-unknown";
|
|
1157
1520
|
}
|
|
1158
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
|
+
*/
|
|
1159
1527
|
function getStatusIcon(status) {
|
|
1160
1528
|
switch (String(status).toLowerCase()) {
|
|
1161
1529
|
case "passed":
|
|
@@ -1168,6 +1536,11 @@ function getStatusIcon(status) {
|
|
|
1168
1536
|
return "❓";
|
|
1169
1537
|
}
|
|
1170
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
|
+
*/
|
|
1171
1544
|
function getSuitesData(results) {
|
|
1172
1545
|
const suitesMap = new Map();
|
|
1173
1546
|
if (!results || results.length === 0) return [];
|
|
@@ -1216,6 +1589,39 @@ function getSuitesData(results) {
|
|
|
1216
1589
|
});
|
|
1217
1590
|
return Array.from(suitesMap.values());
|
|
1218
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
|
+
*/
|
|
1602
|
+
function getAttachmentIcon(contentType) {
|
|
1603
|
+
if (!contentType) return "📎"; // Handle undefined/null
|
|
1604
|
+
|
|
1605
|
+
const normalizedType = contentType.toLowerCase();
|
|
1606
|
+
|
|
1607
|
+
if (normalizedType.includes("pdf")) return "📄";
|
|
1608
|
+
if (normalizedType.includes("json")) return "{ }";
|
|
1609
|
+
if (/html/.test(normalizedType)) return "🌐"; // Fixed: regex for any HTML type
|
|
1610
|
+
if (normalizedType.includes("xml")) return "<>";
|
|
1611
|
+
if (normalizedType.includes("csv")) return "📊";
|
|
1612
|
+
if (normalizedType.startsWith("text/")) return "📝";
|
|
1613
|
+
return "📎";
|
|
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
|
+
*/
|
|
1219
1625
|
function generateSuitesWidget(suitesData) {
|
|
1220
1626
|
if (!suitesData || suitesData.length === 0) {
|
|
1221
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>`;
|
|
@@ -1272,6 +1678,109 @@ function generateSuitesWidget(suitesData) {
|
|
|
1272
1678
|
</div>
|
|
1273
1679
|
</div>`;
|
|
1274
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
|
+
);
|
|
1690
|
+
|
|
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
|
+
`;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
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");
|
|
1700
|
+
|
|
1701
|
+
return `
|
|
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("")}
|
|
1775
|
+
</div>
|
|
1776
|
+
`;
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Generates the HTML report.
|
|
1780
|
+
* @param {object} reportData - The data for the report.
|
|
1781
|
+
* @param {object} trendData - The data for the trend chart.
|
|
1782
|
+
* @returns {string} The HTML report.
|
|
1783
|
+
*/
|
|
1275
1784
|
function generateHTML(reportData, trendData = null) {
|
|
1276
1785
|
const { run, results } = reportData;
|
|
1277
1786
|
const suitesData = getSuitesData(reportData.results || []);
|
|
@@ -1294,11 +1803,16 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1294
1803
|
? formatDuration(runSummary.duration / runSummary.totalTests)
|
|
1295
1804
|
: "0.0s";
|
|
1296
1805
|
|
|
1297
|
-
|
|
1806
|
+
/**
|
|
1807
|
+
* Generates the HTML for the test cases.
|
|
1808
|
+
* @returns {string} The HTML for the test cases.
|
|
1809
|
+
*/
|
|
1810
|
+
function generateTestCasesHTML(subset = results, baseIndex = 0) {
|
|
1298
1811
|
if (!results || results.length === 0)
|
|
1299
1812
|
return '<div class="no-tests">No test results found in this run.</div>';
|
|
1300
|
-
return
|
|
1301
|
-
.map((test) => {
|
|
1813
|
+
return subset
|
|
1814
|
+
.map((test, i) => {
|
|
1815
|
+
const testIndex = baseIndex + i;
|
|
1302
1816
|
const browser = test.browser || "unknown";
|
|
1303
1817
|
const testFileParts = test.name.split(" > ");
|
|
1304
1818
|
const testTitle =
|
|
@@ -1322,13 +1836,13 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1322
1836
|
step.duration
|
|
1323
1837
|
)}</span></div><div class="step-details" style="display: none;">${
|
|
1324
1838
|
step.codeLocation
|
|
1325
|
-
? `<div class="step-info"><strong>Location:</strong> ${sanitizeHTML(
|
|
1839
|
+
? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
|
|
1326
1840
|
step.codeLocation
|
|
1327
1841
|
)}</div>`
|
|
1328
1842
|
: ""
|
|
1329
1843
|
}${
|
|
1330
1844
|
step.errorMessage
|
|
1331
|
-
? `<div class="
|
|
1845
|
+
? `<div class="test-error-summary">${
|
|
1332
1846
|
step.stackTrace
|
|
1333
1847
|
? `<div class="stack-trace">${formatPlaywrightError(
|
|
1334
1848
|
step.stackTrace
|
|
@@ -1336,6 +1850,42 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1336
1850
|
: ""
|
|
1337
1851
|
}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
|
|
1338
1852
|
: ""
|
|
1853
|
+
}${
|
|
1854
|
+
(() => {
|
|
1855
|
+
if (!step.attachments || step.attachments.length === 0) return "";
|
|
1856
|
+
return `<div class="attachments-section"><h4>Step Attachments</h4><div class="attachments-grid">${step.attachments
|
|
1857
|
+
.map((attachment) => {
|
|
1858
|
+
try {
|
|
1859
|
+
const attachmentPath = path.resolve(
|
|
1860
|
+
DEFAULT_OUTPUT_DIR,
|
|
1861
|
+
attachment.path
|
|
1862
|
+
);
|
|
1863
|
+
if (!fsExistsSync(attachmentPath)) {
|
|
1864
|
+
return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
|
|
1865
|
+
attachment.name
|
|
1866
|
+
)}</div>`;
|
|
1867
|
+
}
|
|
1868
|
+
const attachmentBase64 = readFileSync(attachmentPath).toString("base64");
|
|
1869
|
+
const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
|
|
1870
|
+
return `<div class="attachment-item generic-attachment">
|
|
1871
|
+
<div class="attachment-icon">${getAttachmentIcon(attachment.contentType)}</div>
|
|
1872
|
+
<div class="attachment-caption">
|
|
1873
|
+
<span class="attachment-name" title="${sanitizeHTML(attachment.name)}">${sanitizeHTML(attachment.name)}</span>
|
|
1874
|
+
<span class="attachment-type">${sanitizeHTML(attachment.contentType)}</span>
|
|
1875
|
+
</div>
|
|
1876
|
+
<div class="attachment-info">
|
|
1877
|
+
<div class="trace-actions">
|
|
1878
|
+
<a href="#" data-href="${attachmentDataUri}" class="view-full lazy-load-attachment" target="_blank">View</a>
|
|
1879
|
+
<a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(attachment.name)}">Download</a>
|
|
1880
|
+
</div>
|
|
1881
|
+
</div>
|
|
1882
|
+
</div>`;
|
|
1883
|
+
} catch (e) {
|
|
1884
|
+
return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(attachment.name)}</div>`;
|
|
1885
|
+
}
|
|
1886
|
+
})
|
|
1887
|
+
.join("")}</div></div>`;
|
|
1888
|
+
})()
|
|
1339
1889
|
}${
|
|
1340
1890
|
hasNestedSteps
|
|
1341
1891
|
? `<div class="nested-steps">${generateStepsHTML(
|
|
@@ -1353,7 +1903,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1353
1903
|
test.tags || []
|
|
1354
1904
|
)
|
|
1355
1905
|
.join(",")
|
|
1356
|
-
.toLowerCase()}">
|
|
1906
|
+
.toLowerCase()}" data-test-id="${sanitizeHTML(String(test.id || testIndex))}">
|
|
1357
1907
|
<div class="test-case-header" role="button" aria-expanded="false"><div class="test-case-summary"><span class="status-badge ${getStatusClass(
|
|
1358
1908
|
test.status
|
|
1359
1909
|
)}">${String(
|
|
@@ -1389,21 +1939,42 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1389
1939
|
)}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
|
|
1390
1940
|
: ""
|
|
1391
1941
|
}
|
|
1392
|
-
<h4>Steps</h4><div class="steps-list">${generateStepsHTML(
|
|
1393
|
-
test.steps
|
|
1394
|
-
)}</div>
|
|
1395
1942
|
${
|
|
1396
|
-
test.
|
|
1397
|
-
? `<div class="
|
|
1398
|
-
test.
|
|
1399
|
-
)}</pre></div>`
|
|
1943
|
+
test.snippet
|
|
1944
|
+
? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
|
|
1945
|
+
test.snippet
|
|
1946
|
+
)}</code></pre></div>`
|
|
1400
1947
|
: ""
|
|
1401
1948
|
}
|
|
1949
|
+
<h4>Steps</h4><div class="steps-list">${generateStepsHTML(
|
|
1950
|
+
test.steps
|
|
1951
|
+
)}</div>
|
|
1952
|
+
${(() => {
|
|
1953
|
+
if (!test.stdout || test.stdout.length === 0)
|
|
1954
|
+
return "";
|
|
1955
|
+
// Create a unique ID for the <pre> element to target it for copying
|
|
1956
|
+
const logId = `stdout-log-${test.id || testIndex}`;
|
|
1957
|
+
return `<div class="console-output-section">
|
|
1958
|
+
<h4>Console Output (stdout)
|
|
1959
|
+
<button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy Console</button>
|
|
1960
|
+
</h4>
|
|
1961
|
+
<div class="log-wrapper">
|
|
1962
|
+
<pre id="${logId}" class="console-log stdout-log" style="background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
|
|
1963
|
+
test.stdout
|
|
1964
|
+
.map((line) => sanitizeHTML(line))
|
|
1965
|
+
.join("\n")
|
|
1966
|
+
)}</pre>
|
|
1967
|
+
</div>
|
|
1968
|
+
</div>`;
|
|
1969
|
+
})()}
|
|
1402
1970
|
${
|
|
1403
1971
|
test.stderr && test.stderr.length > 0
|
|
1404
|
-
?
|
|
1405
|
-
.
|
|
1406
|
-
|
|
1972
|
+
? (() => {
|
|
1973
|
+
const logId = `stderr-log-${test.id || testIndex}`;
|
|
1974
|
+
return `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre id="${logId}" class="console-log stderr-log">${test.stderr
|
|
1975
|
+
.map((line) => sanitizeHTML(line))
|
|
1976
|
+
.join("\\n")}</pre></div>`;
|
|
1977
|
+
})()
|
|
1407
1978
|
: ""
|
|
1408
1979
|
}
|
|
1409
1980
|
|
|
@@ -1428,7 +1999,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1428
1999
|
readFileSync(imagePath).toString("base64");
|
|
1429
2000
|
return `<div class="attachment-item"><img src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" data-src="data:image/png;base64,${base64ImageData}" alt="Screenshot ${
|
|
1430
2001
|
index + 1
|
|
1431
|
-
}" 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>`;
|
|
2002
|
+
}" 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>`;
|
|
1432
2003
|
} catch (e) {
|
|
1433
2004
|
return `<div class="attachment-item error">Failed to load screenshot: ${sanitizeHTML(
|
|
1434
2005
|
screenshotPath
|
|
@@ -1469,7 +2040,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1469
2040
|
avi: "video/x-msvideo",
|
|
1470
2041
|
}[fileExtension] || "video/mp4";
|
|
1471
2042
|
const videoDataUri = `data:${mimeType};base64,${videoBase64}`;
|
|
1472
|
-
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>`;
|
|
2043
|
+
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>`;
|
|
1473
2044
|
} catch (e) {
|
|
1474
2045
|
return `<div class="attachment-item error">Failed to load video: ${sanitizeHTML(
|
|
1475
2046
|
videoPath
|
|
@@ -1505,6 +2076,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1505
2076
|
test.attachments.length === 0
|
|
1506
2077
|
)
|
|
1507
2078
|
return "";
|
|
2079
|
+
|
|
1508
2080
|
return `<div class="attachments-section"><h4>Other Attachments</h4><div class="attachments-grid">${test.attachments
|
|
1509
2081
|
.map((attachment) => {
|
|
1510
2082
|
try {
|
|
@@ -1512,27 +2084,50 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1512
2084
|
DEFAULT_OUTPUT_DIR,
|
|
1513
2085
|
attachment.path
|
|
1514
2086
|
);
|
|
1515
|
-
|
|
2087
|
+
|
|
2088
|
+
if (!fsExistsSync(attachmentPath)) {
|
|
2089
|
+
console.warn(
|
|
2090
|
+
`Attachment not found at: ${attachmentPath}`
|
|
2091
|
+
);
|
|
1516
2092
|
return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
|
|
1517
2093
|
attachment.name
|
|
1518
2094
|
)}</div>`;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
1519
2097
|
const attachmentBase64 =
|
|
1520
2098
|
readFileSync(attachmentPath).toString(
|
|
1521
2099
|
"base64"
|
|
1522
2100
|
);
|
|
1523
2101
|
const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
2102
|
+
|
|
2103
|
+
return `<div class="attachment-item generic-attachment">
|
|
2104
|
+
<div class="attachment-icon">${getAttachmentIcon(
|
|
2105
|
+
attachment.contentType
|
|
2106
|
+
)}</div>
|
|
2107
|
+
<div class="attachment-caption">
|
|
2108
|
+
<span class="attachment-name" title="${sanitizeHTML(
|
|
2109
|
+
attachment.name
|
|
2110
|
+
)}">${sanitizeHTML(
|
|
1529
2111
|
attachment.name
|
|
1530
|
-
)}</span
|
|
1531
|
-
|
|
1532
|
-
|
|
2112
|
+
)}</span>
|
|
2113
|
+
<span class="attachment-type">${sanitizeHTML(
|
|
2114
|
+
attachment.contentType
|
|
2115
|
+
)}</span>
|
|
2116
|
+
</div>
|
|
2117
|
+
<div class="attachment-info">
|
|
2118
|
+
<div class="trace-actions">
|
|
2119
|
+
<a href="#" data-href="${attachmentDataUri}" class="view-full lazy-load-attachment" target="_blank">View</a>
|
|
2120
|
+
<a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
|
|
1533
2121
|
attachment.name
|
|
1534
|
-
)}">Download</a
|
|
2122
|
+
)}">Download</a>
|
|
2123
|
+
</div>
|
|
2124
|
+
</div>
|
|
2125
|
+
</div>`;
|
|
1535
2126
|
} catch (e) {
|
|
2127
|
+
console.error(
|
|
2128
|
+
`Failed to process attachment "${attachment.name}":`,
|
|
2129
|
+
e
|
|
2130
|
+
);
|
|
1536
2131
|
return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(
|
|
1537
2132
|
attachment.name
|
|
1538
2133
|
)}</div>`;
|
|
@@ -1559,180 +2154,250 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1559
2154
|
<head>
|
|
1560
2155
|
<meta charset="UTF-8">
|
|
1561
2156
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1562
|
-
<link rel="icon" type="image/png" href="https://i.postimg.cc/
|
|
1563
|
-
<link rel="apple-touch-icon" href="https://i.postimg.cc/
|
|
2157
|
+
<link rel="icon" type="image/png" href="https://i.postimg.cc/v817w4sg/logo.png">
|
|
2158
|
+
<link rel="apple-touch-icon" href="https://i.postimg.cc/v817w4sg/logo.png">
|
|
1564
2159
|
<script src="https://code.highcharts.com/highcharts.js" defer></script>
|
|
1565
|
-
<title>Playwright Pulse Report</title>
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
2160
|
+
<title>Playwright Pulse Report (Static Report)</title>
|
|
2161
|
+
|
|
2162
|
+
<style>
|
|
2163
|
+
:root {
|
|
2164
|
+
--primary-color: #60a5fa; --secondary-color: #f472b6; --accent-color: #a78bfa; --accent-color-alt: #fb923c;
|
|
2165
|
+
--success-color: #34d399; --danger-color: #f87171; --warning-color: #fbbf24; --info-color: #60a5fa;
|
|
2166
|
+
--light-gray-color: #374151; --medium-gray-color: #4b5563; --dark-gray-color: #9ca3af;
|
|
2167
|
+
--text-color: #f9fafb; --text-color-secondary: #d1d5db; --border-color: #4b5563; --background-color: #111827;
|
|
2168
|
+
--card-background-color: #1f2937; --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
2169
|
+
--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);
|
|
2170
|
+
}
|
|
2171
|
+
.trend-chart-container, .test-history-trend div[id^="testHistoryChart-"] { min-height: 100px; }
|
|
2172
|
+
.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); }
|
|
2173
|
+
.highcharts-background { fill: transparent; }
|
|
2174
|
+
.highcharts-title, .highcharts-subtitle { font-family: var(--font-family); }
|
|
2175
|
+
.highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
|
|
2176
|
+
.highcharts-axis-title { fill: var(--text-color) !important; }
|
|
2177
|
+
.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; }
|
|
2178
|
+
body { font-family: var(--font-family); margin: 0; background-color: var(--background-color); color: var(--text-color); line-height: 1.65; font-size: 16px; }
|
|
2179
|
+
.container { padding: 30px; border-radius: var(--border-radius); box-shadow: var(--box-shadow); background: repeating-linear-gradient(#1f2937, #374151, #1f2937); }
|
|
2180
|
+
.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; }
|
|
2181
|
+
.header-title { display: flex; align-items: center; gap: 15px; }
|
|
2182
|
+
.header h1 { margin: 0; font-size: 1.85em; font-weight: 600; color: var(--primary-color); }
|
|
2183
|
+
#report-logo { height: 40px; width: 55px; }
|
|
2184
|
+
.run-info { font-size: 0.9em; text-align: right; color: var(--text-color-secondary); line-height:1.5;}
|
|
2185
|
+
.run-info strong { color: var(--text-color); }
|
|
2186
|
+
.tabs { display: flex; border-bottom: 2px solid var(--border-color); margin-bottom: 30px; overflow-x: auto; }
|
|
2187
|
+
.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; }
|
|
2188
|
+
.tab-button:hover { color: var(--accent-color); }
|
|
2189
|
+
.tab-button.active { color: var(--primary-color); border-bottom-color: var(--primary-color); }
|
|
2190
|
+
.tab-content { display: none; animation: fadeIn 0.4s ease-out; }
|
|
2191
|
+
.tab-content.active { display: block; }
|
|
2192
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
2193
|
+
.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 22px; margin-bottom: 35px; }
|
|
2194
|
+
.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; }
|
|
2195
|
+
.summary-card:hover { transform: translateY(-5px); box-shadow: var(--box-shadow); }
|
|
2196
|
+
.summary-card h3 { margin: 0 0 10px; font-size: 1.05em; font-weight: 500; color: var(--text-color-secondary); }
|
|
2197
|
+
.summary-card .value { font-size: 2.4em; font-weight: 600; margin-bottom: 8px; }
|
|
2198
|
+
.summary-card .trend-percentage { font-size: 1em; color: var(--dark-gray-color); }
|
|
2199
|
+
.status-passed .value, .stat-passed svg { color: var(--success-color); }
|
|
2200
|
+
.status-failed .value, .stat-failed svg { color: var(--danger-color); }
|
|
2201
|
+
.status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
|
|
2202
|
+
.dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: stretch; }
|
|
2203
|
+
.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; }
|
|
2204
|
+
.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); }
|
|
2205
|
+
.trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { flex-grow: 1; min-height: 250px; }
|
|
2206
|
+
.status-badge-small-tooltip { padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; }
|
|
2207
|
+
.status-badge-small-tooltip.status-passed { background-color: var(--success-color); }
|
|
2208
|
+
.status-badge-small-tooltip.status-failed { background-color: var(--danger-color); }
|
|
2209
|
+
.status-badge-small-tooltip.status-skipped { background-color: var(--warning-color); }
|
|
2210
|
+
.status-badge-small-tooltip.status-unknown { background-color: var(--dark-gray-color); }
|
|
2211
|
+
.suites-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
|
2212
|
+
.summary-badge { background-color: var(--light-gray-color); color: var(--text-color-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
|
|
2213
|
+
.suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
|
|
2214
|
+
.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; }
|
|
2215
|
+
.suite-card:hover { box-shadow: var(--box-shadow); }
|
|
2216
|
+
.suite-card.status-passed { border-left-color: var(--success-color); }
|
|
2217
|
+
.suite-card.status-failed { border-left-color: var(--danger-color); }
|
|
2218
|
+
.suite-card.status-skipped { border-left-color: var(--warning-color); }
|
|
2219
|
+
.suite-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
|
|
2220
|
+
.suite-name { font-weight: 600; font-size: 1.05em; color: var(--text-color); margin-right: 10px; word-break: break-word;}
|
|
2221
|
+
.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;}
|
|
2222
|
+
.suite-card-body .test-count { font-size: 0.95em; color: var(--text-color-secondary); display: block; margin-bottom: 10px; }
|
|
2223
|
+
.suite-stats { display: flex; gap: 14px; font-size: 0.95em; align-items: center; }
|
|
2224
|
+
.suite-stats span { display: flex; align-items: center; gap: 6px; }
|
|
2225
|
+
.suite-stats svg { vertical-align: middle; font-size: 1.15em; }
|
|
2226
|
+
.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); }
|
|
2227
|
+
.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); }
|
|
2228
|
+
.filters input { flex-grow: 1; min-width: 240px;}
|
|
2229
|
+
.filters select {min-width: 180px;}
|
|
2230
|
+
.filters button { background-color: var(--primary-color); color: white; cursor: pointer; transition: background-color 0.2s ease, box-shadow 0.2s ease; border: none; }
|
|
2231
|
+
.filters button:hover { background-color: var(--accent-color); box-shadow: 0 2px 5px rgba(0,0,0,0.3);}
|
|
2232
|
+
.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; }
|
|
2233
|
+
.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; }
|
|
2234
|
+
.test-case-header:hover { background-color: var(--light-gray-color); }
|
|
2235
|
+
.test-case-header[aria-expanded="true"] { border-bottom-color: var(--border-color); background-color: var(--light-gray-color); }
|
|
2236
|
+
.test-case-summary { display: flex; align-items: center; gap: 14px; flex-grow: 1; flex-wrap: wrap;}
|
|
2237
|
+
.test-case-title { font-weight: 600; color: var(--text-color); font-size: 1em; }
|
|
2238
|
+
.test-case-browser { font-size: 0.9em; color: var(--text-color-secondary); }
|
|
2239
|
+
.test-case-meta { display: flex; align-items: center; gap: 12px; font-size: 0.9em; color: var(--text-color-secondary); flex-shrink: 0; }
|
|
2240
|
+
.test-duration { background-color: var(--light-gray-color); padding: 4px 10px; border-radius: 12px; font-size: 0.9em;}
|
|
2241
|
+
.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); }
|
|
2242
|
+
.status-badge.status-passed { background-color: var(--success-color); }
|
|
2243
|
+
.status-badge.status-failed { background-color: var(--danger-color); }
|
|
2244
|
+
.status-badge.status-skipped { background-color: var(--warning-color); }
|
|
2245
|
+
.status-badge.status-unknown { background-color: var(--dark-gray-color); }
|
|
2246
|
+
.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; }
|
|
2247
|
+
.test-case-content { display: none; padding: 20px; border-top: 1px solid var(--border-color); background-color: var(--light-gray-color); }
|
|
2248
|
+
.test-case-content h4 { margin-top: 22px; margin-bottom: 14px; font-size: 1.15em; color: var(--primary-color); }
|
|
2249
|
+
.test-case-content p { margin-bottom: 10px; font-size: 1em; }
|
|
2250
|
+
.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; }
|
|
2251
|
+
.test-error-summary h4 { color: var(--danger-color); margin-top:0;}
|
|
2252
|
+
.test-error-summary pre { white-space: pre-wrap; word-break: break-all; color: var(--danger-color); font-size: 0.95em;}
|
|
2253
|
+
.steps-list { margin: 18px 0; }
|
|
2254
|
+
@supports (content-visibility: auto) {
|
|
2255
|
+
.tab-content,
|
|
2256
|
+
#test-runs .test-case,
|
|
2257
|
+
.attachments-section,
|
|
2258
|
+
.test-history-card,
|
|
2259
|
+
.trend-chart,
|
|
2260
|
+
.suite-card {
|
|
2261
|
+
content-visibility: auto;
|
|
2262
|
+
contain-intrinsic-size: 1px 600px;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
.test-case,
|
|
2266
|
+
.test-history-card,
|
|
2267
|
+
.suite-card,
|
|
2268
|
+
.attachments-section {
|
|
2269
|
+
contain: content;
|
|
2270
|
+
}
|
|
2271
|
+
.attachments-grid .attachment-item img.lazy-load-image {
|
|
2272
|
+
width: 100%;
|
|
2273
|
+
aspect-ratio: 4 / 3;
|
|
2274
|
+
object-fit: cover;
|
|
2275
|
+
}
|
|
2276
|
+
.attachments-grid .attachment-item.video-item {
|
|
2277
|
+
aspect-ratio: 16 / 9;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
.step-item { margin-bottom: 8px; padding-left: calc(var(--depth, 0) * 28px); }
|
|
2281
|
+
.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; }
|
|
2282
|
+
.step-header:hover { background-color: var(--light-gray-color); border-color: var(--medium-gray-color); box-shadow: var(--box-shadow-inset); }
|
|
2283
|
+
.step-icon { margin-right: 12px; width: 20px; text-align: center; font-size: 1.1em; }
|
|
2284
|
+
.step-title { flex: 1; font-size: 1em; }
|
|
2285
|
+
.step-duration { color: var(--dark-gray-color); font-size: 0.9em; }
|
|
2286
|
+
.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); }
|
|
2287
|
+
.step-info { margin-bottom: 8px; }
|
|
2288
|
+
.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); }
|
|
2289
|
+
.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; }
|
|
2290
|
+
.step-hook { background-color: rgba(96,165,250,0.1); border-left: 3px solid var(--info-color) !important; }
|
|
2291
|
+
.step-hook .step-title { font-style: italic; color: var(--info-color)}
|
|
2292
|
+
.nested-steps { margin-top: 12px; }
|
|
2293
|
+
.attachments-section { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--light-gray-color); }
|
|
2294
|
+
.attachments-section h4 { margin-top: 0; margin-bottom: 20px; font-size: 1.1em; color: var(--text-color); }
|
|
2295
|
+
.attachments-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 22px; }
|
|
2296
|
+
.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; }
|
|
2297
|
+
.attachment-item:hover { transform: translateY(-4px); box-shadow: var(--box-shadow); }
|
|
2298
|
+
.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; }
|
|
2299
|
+
.attachment-info { padding: 12px; margin-top: auto; background-color: var(--light-gray-color);}
|
|
2300
|
+
.attachment-item a:hover img { opacity: 0.85; }
|
|
2301
|
+
.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); }
|
|
2302
|
+
.video-item a, .trace-item a { display: block; margin-bottom: 8px; color: var(--primary-color); text-decoration: none; font-weight: 500; }
|
|
2303
|
+
.video-item a:hover, .trace-item a:hover { text-decoration: underline; }
|
|
2304
|
+
.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;}
|
|
2305
|
+
.trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
|
|
2306
|
+
.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;}
|
|
2307
|
+
.test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
|
|
2308
|
+
.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; }
|
|
2309
|
+
.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); }
|
|
2310
|
+
.test-history-header h3 { margin: 0; font-size: 1.15em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2311
|
+
.test-history-header p { font-weight: 500 }
|
|
2312
|
+
.test-history-trend { margin-bottom: 20px; min-height: 110px; }
|
|
2313
|
+
.test-history-trend div[id^="testHistoryChart-"] { display: block; margin: 0 auto; max-width:100%; height: 100px; width: 320px; }
|
|
2314
|
+
.test-history-details-collapsible summary { cursor: pointer; font-size: 1em; color: var(--primary-color); margin-bottom: 10px; font-weight:500; }
|
|
2315
|
+
.test-history-details-collapsible summary:hover {text-decoration: underline;}
|
|
2316
|
+
.test-history-details table { width: 100%; border-collapse: collapse; font-size: 0.95em; }
|
|
2317
|
+
.test-history-details th, .test-history-details td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--light-gray-color); }
|
|
2318
|
+
.test-history-details th { background-color: var(--light-gray-color); font-weight: 600; }
|
|
2319
|
+
.status-badge-small { padding: 3px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase; display: inline-block; }
|
|
2320
|
+
.status-badge-small.status-passed { background-color: var(--success-color); }
|
|
2321
|
+
.status-badge-small.status-failed { background-color: var(--danger-color); }
|
|
2322
|
+
.status-badge-small.status-skipped { background-color: var(--warning-color); }
|
|
2323
|
+
.status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
|
|
2324
|
+
.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); }
|
|
2325
|
+
.no-data-chart {font-size: 0.95em; padding: 18px;}
|
|
2326
|
+
.ai-failure-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 22px; }
|
|
2327
|
+
.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; }
|
|
2328
|
+
.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; }
|
|
2329
|
+
.ai-failure-card-header h3 { margin: 0; font-size: 1.1em; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2330
|
+
.ai-failure-card-body { padding: 20px; }
|
|
2331
|
+
.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; }
|
|
2332
|
+
.ai-fix-btn:hover { background-color: var(--accent-color); transform: translateY(-2px); }
|
|
2333
|
+
.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; }
|
|
2334
|
+
.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; }
|
|
2335
|
+
.ai-modal-header { padding: 18px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
|
|
2336
|
+
.ai-modal-header h3 { margin: 0; font-size: 1.25em; }
|
|
2337
|
+
.ai-modal-close { font-size: 2rem; font-weight: 300; cursor: pointer; color: var(--dark-gray-color); line-height: 1; transition: color 0.2s; }
|
|
2338
|
+
.ai-modal-close:hover { color: var(--danger-color); }
|
|
2339
|
+
.ai-modal-body { padding: 25px; overflow-y: auto; }
|
|
2340
|
+
.ai-modal-body h4 { margin-top: 18px; margin-bottom: 10px; font-size: 1.1em; color: var(--primary-color); }
|
|
2341
|
+
.ai-modal-body p { margin-bottom: 15px; }
|
|
2342
|
+
.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; }
|
|
2343
|
+
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
2344
|
+
.trace-preview { padding: 1rem; text-align: center; background: var(--light-gray-color); border-bottom: 1px solid var(--border-color); }
|
|
2345
|
+
.trace-icon { font-size: 2rem; display: block; margin-bottom: 0.5rem; }
|
|
2346
|
+
.trace-name { word-break: break-word; font-size: 0.9rem; }
|
|
2347
|
+
.trace-actions { display: flex; gap: 0.5rem; }
|
|
2348
|
+
.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; }
|
|
2349
|
+
.view-trace { background: var(--primary-color); color: white; }
|
|
2350
|
+
.view-trace:hover { background: var(--accent-color); }
|
|
2351
|
+
.download-trace { background: var(--medium-gray-color); color: var(--text-color); }
|
|
2352
|
+
.download-trace:hover { background: var(--dark-gray-color); }
|
|
2353
|
+
.filters button.clear-filters-btn { background-color: var(--medium-gray-color); color: var(--text-color); }
|
|
2354
|
+
.filters button.clear-filters-btn:hover { background-color: var(--dark-gray-color); color: #fff; }
|
|
2355
|
+
.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;}
|
|
2356
|
+
.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; }
|
|
2357
|
+
.stat-item { text-align: center; color: white; }
|
|
2358
|
+
.stat-number { display: block; font-size: 2em; font-weight: 700; line-height: 1;}
|
|
2359
|
+
.stat-label { font-size: 0.9em; opacity: 0.9; font-weight: 500;}
|
|
2360
|
+
.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;}
|
|
2361
|
+
.compact-failure-list { display: flex; flex-direction: column; gap: 15px; }
|
|
2362
|
+
.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;}
|
|
2363
|
+
.compact-failure-item:hover { transform: translateY(-2px); box-shadow: var(--box-shadow); }
|
|
2364
|
+
.failure-header { display: flex; justify-content: space-between; align-items: center; padding: 18px 20px; gap: 15px;}
|
|
2365
|
+
.failure-main-info { flex: 1; min-width: 0; }
|
|
2366
|
+
.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;}
|
|
2367
|
+
.failure-meta { display: flex; gap: 12px; align-items: center;}
|
|
2368
|
+
.browser-indicator, .duration-indicator { font-size: 0.85em; padding: 3px 8px; border-radius: 12px; font-weight: 500;}
|
|
2369
|
+
.browser-indicator { background: var(--info-color); color: white; }
|
|
2370
|
+
#load-more-tests { font-size: 16px; padding: 4px; background-color: var(--light-gray-color); border-radius: 4px; color: var(--text-color); }
|
|
2371
|
+
.duration-indicator { background: var(--medium-gray-color); color: var(--text-color); }
|
|
2372
|
+
.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;}
|
|
2373
|
+
.compact-ai-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(55, 65, 81, 0.4); }
|
|
2374
|
+
.ai-text { font-size: 0.95em; }
|
|
2375
|
+
.failure-error-preview { padding: 0 20px 18px 20px; border-top: 1px solid var(--light-gray-color);}
|
|
2376
|
+
.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;}
|
|
2377
|
+
.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;}
|
|
2378
|
+
.expand-error-btn:hover { background: var(--light-gray-color); border-color: var(--medium-gray-color); }
|
|
2379
|
+
.expand-icon { transition: transform 0.2s ease; font-size: 0.8em;}
|
|
2380
|
+
.expand-error-btn.expanded .expand-icon { transform: rotate(180deg); }
|
|
2381
|
+
.full-error-details { padding: 0 20px 20px 20px; border-top: 1px solid var(--light-gray-color); margin-top: 0;}
|
|
2382
|
+
.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;}
|
|
2383
|
+
@media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
|
|
2384
|
+
@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; } }
|
|
2385
|
+
@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; } }
|
|
2386
|
+
@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; } }
|
|
2387
|
+
.trace-actions a { text-decoration: none; color: var(--primary-color); font-weight: 500; font-size: 0.9em; }
|
|
2388
|
+
.generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
|
|
2389
|
+
.attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
|
|
2390
|
+
.attachment-caption { display: flex; flex-direction: column; align-items: center; justify-content: center; flex-grow: 1; }
|
|
2391
|
+
.attachment-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
|
2392
|
+
.attachment-type { font-size: 0.8rem; color: var(--text-color-secondary); }
|
|
2393
|
+
.footer-text { color: white }
|
|
2394
|
+
</style>
|
|
1730
2395
|
</head>
|
|
1731
2396
|
<body>
|
|
1732
2397
|
<div class="container">
|
|
1733
2398
|
<header class="header">
|
|
1734
2399
|
<div class="header-title">
|
|
1735
|
-
<img id="report-logo" src="
|
|
2400
|
+
<img id="report-logo" src="https://i.postimg.cc/v817w4sg/logo.png" alt="Report Logo">
|
|
1736
2401
|
<h1>Playwright Pulse Report</h1>
|
|
1737
2402
|
</div>
|
|
1738
2403
|
<div class="run-info"><strong>Run Date:</strong> ${formatDate(
|
|
@@ -1745,7 +2410,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1745
2410
|
<button class="tab-button active" data-tab="dashboard">Dashboard</button>
|
|
1746
2411
|
<button class="tab-button" data-tab="test-runs">Test Run Summary</button>
|
|
1747
2412
|
<button class="tab-button" data-tab="test-history">Test History</button>
|
|
1748
|
-
<button class="tab-button" data-tab="
|
|
2413
|
+
<button class="tab-button" data-tab="ai-failure-analyzer">AI Failure Analyzer</button>
|
|
1749
2414
|
</div>
|
|
1750
2415
|
<div id="dashboard" class="tab-content active">
|
|
1751
2416
|
<div class="dashboard-grid">
|
|
@@ -1805,7 +2470,18 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1805
2470
|
.join("")}</select>
|
|
1806
2471
|
<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>
|
|
1807
2472
|
</div>
|
|
1808
|
-
<div class="test-cases-list">${generateTestCasesHTML(
|
|
2473
|
+
<div class="test-cases-list">${generateTestCasesHTML(
|
|
2474
|
+
results.slice(0, 50),
|
|
2475
|
+
0
|
|
2476
|
+
)}</div>
|
|
2477
|
+
${
|
|
2478
|
+
results.length > 50
|
|
2479
|
+
? `<div class="load-more-wrapper"><button id="load-more-tests">Load more</button></div><script type="application/json" id="remaining-tests-b64">${Buffer.from(
|
|
2480
|
+
generateTestCasesHTML(results.slice(50), 50),
|
|
2481
|
+
"utf8"
|
|
2482
|
+
).toString("base64")}</script>`
|
|
2483
|
+
: ``
|
|
2484
|
+
}
|
|
1809
2485
|
</div>
|
|
1810
2486
|
<div id="test-history" class="tab-content">
|
|
1811
2487
|
<h2 class="tab-main-title">Execution Trends</h2>
|
|
@@ -1823,6 +2499,12 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1823
2499
|
? generateDurationTrendChart(trendData)
|
|
1824
2500
|
: '<div class="no-data">Overall trend data not available for durations.</div>'
|
|
1825
2501
|
}
|
|
2502
|
+
</div>
|
|
2503
|
+
</div>
|
|
2504
|
+
<h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
|
|
2505
|
+
<div class="trend-charts-row">
|
|
2506
|
+
<div class="trend-chart">
|
|
2507
|
+
${generateWorkerDistributionChart(results)}
|
|
1826
2508
|
</div>
|
|
1827
2509
|
</div>
|
|
1828
2510
|
<h2 class="tab-main-title">Individual Test History</h2>
|
|
@@ -1834,13 +2516,12 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1834
2516
|
: '<div class="no-data">Individual test history data not available.</div>'
|
|
1835
2517
|
}
|
|
1836
2518
|
</div>
|
|
1837
|
-
<div id="
|
|
1838
|
-
|
|
2519
|
+
<div id="ai-failure-analyzer" class="tab-content">
|
|
2520
|
+
${generateAIFailureAnalyzerTab(results)}
|
|
1839
2521
|
</div>
|
|
1840
2522
|
<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;">
|
|
1841
2523
|
<div style="display: inline-flex; align-items: center; gap: 0.5rem; color: #333; font-size: 0.9rem; font-weight: 600; letter-spacing: 0.5px;">
|
|
1842
|
-
<
|
|
1843
|
-
<span>Created by</span>
|
|
2524
|
+
<span class="footer-text">Created by</span>
|
|
1844
2525
|
<a href="https://github.com/Arghajit47" target="_blank" rel="noopener noreferrer" style="color: #7737BF; font-weight: 700; font-style: italic; text-decoration: none; transition: all 0.2s ease;" onmouseover="this.style.color='#BF5C37'" onmouseout="this.style.color='#7737BF'">Arghajit Singha</a>
|
|
1845
2526
|
</div>
|
|
1846
2527
|
<div style="margin-top: 0.5rem; font-size: 0.75rem; color: #666;">Crafted with precision</div>
|
|
@@ -1854,6 +2535,143 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1854
2535
|
return (ms / 1000).toFixed(1) + "s";
|
|
1855
2536
|
}
|
|
1856
2537
|
}
|
|
2538
|
+
function copyLogContent(elementId, button) {
|
|
2539
|
+
const logElement = document.getElementById(elementId);
|
|
2540
|
+
if (!logElement) {
|
|
2541
|
+
console.error('Could not find log element with ID:', elementId);
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
navigator.clipboard.writeText(logElement.innerText).then(() => {
|
|
2545
|
+
button.textContent = 'Copied!';
|
|
2546
|
+
setTimeout(() => { button.textContent = 'Copy'; }, 2000);
|
|
2547
|
+
}).catch(err => {
|
|
2548
|
+
console.error('Failed to copy log content:', err);
|
|
2549
|
+
button.textContent = 'Failed';
|
|
2550
|
+
setTimeout(() => { button.textContent = 'Copy'; }, 2000);
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
function getAIFix(button) {
|
|
2555
|
+
const modal = document.getElementById('ai-fix-modal');
|
|
2556
|
+
const modalContent = document.getElementById('ai-fix-modal-content');
|
|
2557
|
+
const modalTitle = document.getElementById('ai-fix-modal-title');
|
|
2558
|
+
|
|
2559
|
+
modal.style.display = 'flex';
|
|
2560
|
+
document.body.style.setProperty('overflow', 'hidden', 'important');
|
|
2561
|
+
modalTitle.textContent = 'Analyzing...';
|
|
2562
|
+
modalContent.innerHTML = '<div class="ai-loader"></div>';
|
|
2563
|
+
|
|
2564
|
+
try {
|
|
2565
|
+
const testJson = button.dataset.testJson;
|
|
2566
|
+
const test = JSON.parse(atob(testJson));
|
|
2567
|
+
|
|
2568
|
+
const testName = test.name || 'Unknown Test';
|
|
2569
|
+
const failureLogsAndErrors = [
|
|
2570
|
+
'Error Message:',
|
|
2571
|
+
test.errorMessage || 'Not available.',
|
|
2572
|
+
'\\n\\n--- stdout ---',
|
|
2573
|
+
(test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
|
|
2574
|
+
'\\n\\n--- stderr ---',
|
|
2575
|
+
(test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
|
|
2576
|
+
].join('\\n');
|
|
2577
|
+
const codeSnippet = test.snippet || '';
|
|
2578
|
+
|
|
2579
|
+
const shortTestName = testName.split(' > ').pop();
|
|
2580
|
+
modalTitle.textContent = \`Analysis for: \${shortTestName}\`;
|
|
2581
|
+
|
|
2582
|
+
const apiUrl = 'https://ai-test-analyser.netlify.app/api/analyze';
|
|
2583
|
+
fetch(apiUrl, {
|
|
2584
|
+
method: 'POST',
|
|
2585
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2586
|
+
body: JSON.stringify({
|
|
2587
|
+
testName: testName,
|
|
2588
|
+
failureLogsAndErrors: failureLogsAndErrors,
|
|
2589
|
+
codeSnippet: codeSnippet,
|
|
2590
|
+
}),
|
|
2591
|
+
})
|
|
2592
|
+
.then(response => {
|
|
2593
|
+
if (!response.ok) {
|
|
2594
|
+
return response.text().then(text => {
|
|
2595
|
+
throw new Error(\`API request failed with status \${response.status}: \${text || response.statusText}\`);
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
return response.text();
|
|
2599
|
+
})
|
|
2600
|
+
.then(text => {
|
|
2601
|
+
if (!text) {
|
|
2602
|
+
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.");
|
|
2603
|
+
}
|
|
2604
|
+
try {
|
|
2605
|
+
return JSON.parse(text);
|
|
2606
|
+
} catch (e) {
|
|
2607
|
+
console.error("Failed to parse JSON:", text);
|
|
2608
|
+
throw new Error(\`The AI analyzer returned an invalid response. \${e.message}\`);
|
|
2609
|
+
}
|
|
2610
|
+
})
|
|
2611
|
+
.then(data => {
|
|
2612
|
+
const escapeHtml = (unsafe) => {
|
|
2613
|
+
if (typeof unsafe !== 'string') return '';
|
|
2614
|
+
return unsafe
|
|
2615
|
+
.replace(/&/g, "&")
|
|
2616
|
+
.replace(/</g, "<")
|
|
2617
|
+
.replace(/>/g, ">")
|
|
2618
|
+
.replace(/"/g, """)
|
|
2619
|
+
.replace(/'/g, "'");
|
|
2620
|
+
};
|
|
2621
|
+
|
|
2622
|
+
const analysisHtml = \`<h4>Analysis</h4><p>\${escapeHtml(data.rootCause) || 'No analysis provided.'}</p>\`;
|
|
2623
|
+
|
|
2624
|
+
let suggestionsHtml = '<h4>Suggestions</h4>';
|
|
2625
|
+
if (data.suggestedFixes && data.suggestedFixes.length > 0) {
|
|
2626
|
+
suggestionsHtml += '<div class="suggestions-list" style="margin-top: 15px;">';
|
|
2627
|
+
data.suggestedFixes.forEach(fix => {
|
|
2628
|
+
suggestionsHtml += \`
|
|
2629
|
+
<div class="suggestion-item" style="margin-bottom: 22px; border-left: 3px solid var(--accent-color-alt); padding-left: 15px;">
|
|
2630
|
+
<p style="margin: 0 0 8px 0; font-weight: 500;">\${escapeHtml(fix.description)}</p>
|
|
2631
|
+
\${fix.codeSnippet ? \`<div class="code-section"><pre><code>\${escapeHtml(fix.codeSnippet)}</code></pre></div>\` : ''}
|
|
2632
|
+
</div>
|
|
2633
|
+
\`;
|
|
2634
|
+
});
|
|
2635
|
+
suggestionsHtml += '</div>';
|
|
2636
|
+
} else {
|
|
2637
|
+
suggestionsHtml += \`<div class="code-section"><pre><code>No suggestion provided.</code></pre></div>\`;
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
modalContent.innerHTML = analysisHtml + suggestionsHtml;
|
|
2641
|
+
})
|
|
2642
|
+
.catch(err => {
|
|
2643
|
+
console.error('AI Fix Error:', err);
|
|
2644
|
+
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>\`;
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
} catch (e) {
|
|
2648
|
+
console.error('Error processing test data for AI Fix:', e);
|
|
2649
|
+
modalTitle.textContent = 'Error';
|
|
2650
|
+
modalContent.innerHTML = \`<div class="test-error-summary">Could not process test data. Is it formatted correctly?</div>\`;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
function closeAiModal() {
|
|
2655
|
+
const modal = document.getElementById('ai-fix-modal');
|
|
2656
|
+
if(modal) modal.style.display = 'none';
|
|
2657
|
+
document.body.style.setProperty('overflow', '', 'important');
|
|
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
|
+
|
|
1857
2675
|
function initializeReportInteractivity() {
|
|
1858
2676
|
const tabButtons = document.querySelectorAll('.tab-button');
|
|
1859
2677
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
@@ -1866,18 +2684,51 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1866
2684
|
const activeContent = document.getElementById(tabId);
|
|
1867
2685
|
if (activeContent) {
|
|
1868
2686
|
activeContent.classList.add('active');
|
|
1869
|
-
// Check if IntersectionObserver is already handling elements in this tab
|
|
1870
|
-
// For simplicity, we assume if an element is observed, it will be handled when it becomes visible.
|
|
1871
|
-
// If IntersectionObserver is not supported, already-visible elements would have been loaded by fallback.
|
|
1872
2687
|
}
|
|
1873
2688
|
});
|
|
1874
2689
|
});
|
|
1875
2690
|
// --- Test Run Summary Filters ---
|
|
1876
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
|
+
|
|
1877
2727
|
const statusFilter = document.getElementById('filter-status');
|
|
1878
2728
|
const browserFilter = document.getElementById('filter-browser');
|
|
1879
2729
|
const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters');
|
|
1880
2730
|
function filterTestCases() {
|
|
2731
|
+
ensureAllTestsAppended();
|
|
1881
2732
|
const nameValue = nameFilter ? nameFilter.value.toLowerCase() : "";
|
|
1882
2733
|
const statusValue = statusFilter ? statusFilter.value : "";
|
|
1883
2734
|
const browserValue = browserFilter ? browserFilter.value : "";
|
|
@@ -1896,7 +2747,10 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1896
2747
|
if(statusFilter) statusFilter.addEventListener('change', filterTestCases);
|
|
1897
2748
|
if(browserFilter) browserFilter.addEventListener('change', filterTestCases);
|
|
1898
2749
|
if(clearRunSummaryFiltersBtn) clearRunSummaryFiltersBtn.addEventListener('click', () => {
|
|
1899
|
-
|
|
2750
|
+
ensureAllTestsAppended();
|
|
2751
|
+
if(nameFilter) nameFilter.value = '';
|
|
2752
|
+
if(statusFilter) statusFilter.value = '';
|
|
2753
|
+
if(browserFilter) browserFilter.value = '';
|
|
1900
2754
|
filterTestCases();
|
|
1901
2755
|
});
|
|
1902
2756
|
// --- Test History Filters ---
|
|
@@ -1937,12 +2791,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1937
2791
|
headerElement.setAttribute('aria-expanded', String(!isExpanded));
|
|
1938
2792
|
}
|
|
1939
2793
|
}
|
|
1940
|
-
document.querySelectorAll('#test-runs .test-case-header').forEach(header => {
|
|
1941
|
-
header.addEventListener('click', () => toggleElementDetails(header));
|
|
1942
|
-
});
|
|
1943
|
-
document.querySelectorAll('#test-runs .step-header').forEach(header => {
|
|
1944
|
-
header.addEventListener('click', () => toggleElementDetails(header, '.step-details'));
|
|
1945
|
-
});
|
|
1946
2794
|
const expandAllBtn = document.getElementById('expand-all-tests');
|
|
1947
2795
|
const collapseAllBtn = document.getElementById('collapse-all-tests');
|
|
1948
2796
|
function setAllTestRunDetailsVisibility(displayMode, ariaState) {
|
|
@@ -1953,31 +2801,89 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1953
2801
|
}
|
|
1954
2802
|
if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
|
|
1955
2803
|
if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
|
|
1956
|
-
|
|
1957
|
-
|
|
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');
|
|
1958
2881
|
if ('IntersectionObserver' in window) {
|
|
1959
2882
|
let lazyObserver = new IntersectionObserver((entries, observer) => {
|
|
1960
2883
|
entries.forEach(entry => {
|
|
1961
2884
|
if (entry.isIntersecting) {
|
|
1962
2885
|
const element = entry.target;
|
|
1963
|
-
if (element.classList.contains('lazy-load-
|
|
1964
|
-
if (element.dataset.src) {
|
|
1965
|
-
element.src = element.dataset.src;
|
|
1966
|
-
element.removeAttribute('data-src');
|
|
1967
|
-
}
|
|
1968
|
-
} else if (element.classList.contains('lazy-load-video')) {
|
|
1969
|
-
const source = element.querySelector('source');
|
|
1970
|
-
if (source && source.dataset.src) {
|
|
1971
|
-
source.src = source.dataset.src;
|
|
1972
|
-
source.removeAttribute('data-src');
|
|
1973
|
-
element.load();
|
|
1974
|
-
}
|
|
1975
|
-
} else if (element.classList.contains('lazy-load-attachment')) {
|
|
1976
|
-
if (element.dataset.href) {
|
|
1977
|
-
element.href = element.dataset.href;
|
|
1978
|
-
element.removeAttribute('data-href');
|
|
1979
|
-
}
|
|
1980
|
-
} else if (element.classList.contains('lazy-load-iframe')) {
|
|
2886
|
+
if (element.classList.contains('lazy-load-iframe')) {
|
|
1981
2887
|
if (element.dataset.src) {
|
|
1982
2888
|
element.src = element.dataset.src;
|
|
1983
2889
|
element.removeAttribute('data-src');
|
|
@@ -1993,79 +2899,61 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1993
2899
|
});
|
|
1994
2900
|
}, { rootMargin: "0px 0px 200px 0px" });
|
|
1995
2901
|
lazyLoadElements.forEach(el => lazyObserver.observe(el));
|
|
1996
|
-
} else {
|
|
2902
|
+
} else {
|
|
1997
2903
|
lazyLoadElements.forEach(element => {
|
|
1998
|
-
if (element.classList.contains('lazy-load-
|
|
1999
|
-
else if (element.classList.contains('lazy-load-
|
|
2000
|
-
else if (element.classList.contains('lazy-load-attachment') && element.dataset.href) element.href = element.dataset.href;
|
|
2001
|
-
else if (element.classList.contains('lazy-load-iframe') && element.dataset.src) element.src = element.dataset.src;
|
|
2002
|
-
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](); }
|
|
2003
2906
|
});
|
|
2004
2907
|
}
|
|
2005
2908
|
}
|
|
2006
2909
|
document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
|
|
2007
2910
|
|
|
2008
2911
|
function copyErrorToClipboard(button) {
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
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
|
+
}
|
|
2020
2927
|
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
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
|
+
});
|
|
2033
2941
|
}
|
|
2034
|
-
|
|
2035
|
-
// Use the text content of the cleaned container as the fallback.
|
|
2036
|
-
errorText = clonedContainer.textContent;
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
// 4. Proceed with the clipboard logic, ensuring text is not null and is trimmed.
|
|
2040
|
-
if (!errorText) {
|
|
2041
|
-
console.error('Could not extract error text.');
|
|
2042
|
-
button.textContent = 'Nothing to copy';
|
|
2043
|
-
setTimeout(() => { button.textContent = 'Copy Error'; }, 2000);
|
|
2044
|
-
return;
|
|
2045
|
-
}
|
|
2942
|
+
</script>
|
|
2046
2943
|
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2944
|
+
<!-- AI Fix Modal -->
|
|
2945
|
+
<div id="ai-fix-modal" class="ai-modal-overlay" onclick="closeAiModal()">
|
|
2946
|
+
<div class="ai-modal-content" onclick="event.stopPropagation()">
|
|
2947
|
+
<div class="ai-modal-header">
|
|
2948
|
+
<h3 id="ai-fix-modal-title">AI Analysis</h3>
|
|
2949
|
+
<span class="ai-modal-close" onclick="closeAiModal()">×</span>
|
|
2950
|
+
</div>
|
|
2951
|
+
<div class="ai-modal-body" id="ai-fix-modal-content">
|
|
2952
|
+
<!-- Content will be injected by JavaScript -->
|
|
2953
|
+
</div>
|
|
2954
|
+
</div>
|
|
2955
|
+
</div>
|
|
2053
2956
|
|
|
2054
|
-
try {
|
|
2055
|
-
const successful = document.execCommand('copy');
|
|
2056
|
-
const originalText = button.textContent;
|
|
2057
|
-
button.textContent = successful ? 'Copied!' : 'Failed';
|
|
2058
|
-
setTimeout(() => {
|
|
2059
|
-
button.textContent = originalText;
|
|
2060
|
-
}, 2000);
|
|
2061
|
-
} catch (err) {
|
|
2062
|
-
console.error('Failed to copy: ', err);
|
|
2063
|
-
button.textContent = 'Failed';
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
document.body.removeChild(textarea);
|
|
2067
|
-
}
|
|
2068
|
-
</script>
|
|
2069
2957
|
</body>
|
|
2070
2958
|
</html>
|
|
2071
2959
|
`;
|
|
@@ -2094,6 +2982,11 @@ async function runScript(scriptPath) {
|
|
|
2094
2982
|
});
|
|
2095
2983
|
});
|
|
2096
2984
|
}
|
|
2985
|
+
/**
|
|
2986
|
+
* The main function that orchestrates the generation of the static HTML report.
|
|
2987
|
+
* It reads the latest test run data, loads historical data for trend analysis,
|
|
2988
|
+
* prepares the data, and then generates and writes the final HTML report file.
|
|
2989
|
+
*/
|
|
2097
2990
|
async function main() {
|
|
2098
2991
|
const __filename = fileURLToPath(import.meta.url);
|
|
2099
2992
|
const __dirname = path.dirname(__filename);
|
|
@@ -2284,4 +3177,4 @@ main().catch((err) => {
|
|
|
2284
3177
|
);
|
|
2285
3178
|
console.error(err.stack);
|
|
2286
3179
|
process.exit(1);
|
|
2287
|
-
});
|
|
3180
|
+
});
|