@arghajit/dummy 0.1.0 → 0.1.1
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 +136 -77
- package/dist/reporter/attachment-utils.js +41 -33
- package/dist/reporter/playwright-pulse-reporter.d.ts +5 -0
- package/dist/reporter/playwright-pulse-reporter.js +269 -145
- package/dist/reporter/tsconfig.reporter.tsbuildinfo +1 -0
- package/dist/types/index.d.ts +26 -2
- package/package.json +17 -5
- package/scripts/generate-email-report.mjs +714 -0
- package/scripts/generate-report.mjs +3034 -0
- package/scripts/generate-static-report.mjs +2186 -1285
- package/scripts/generate-trend.mjs +1 -1
- package/scripts/merge-pulse-report.js +1 -0
- package/scripts/{sendReport.js → sendReport.mjs} +143 -76
- package/screenshots/127-0-0-1-5500-pulse-report-output-playwright-pulse-static-report-html-i-Phone-14-Pro-Max-1.png +0 -0
- package/screenshots/127-0-0-1-5500-pulse-report-output-playwright-pulse-static-report-html-i-Phone-14-Pro-Max.png +0 -0
- package/screenshots/Email-report.jpg +0 -0
- package/screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html-1.png +0 -0
- package/screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html-2.png +0 -0
- package/screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html.png +0 -0
- package/screenshots/image.png +0 -0
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
|
-
import { readFileSync, existsSync as fsExistsSync } from "fs";
|
|
4
|
+
import { readFileSync, existsSync as fsExistsSync } from "fs";
|
|
5
5
|
import path from "path";
|
|
6
|
-
import { fork } from "child_process";
|
|
7
|
-
import { fileURLToPath } from "url";
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import { fork } from "child_process";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
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,135 +26,278 @@ 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
|
+
*/
|
|
53
|
+
export function ansiToHtml(text) {
|
|
54
|
+
if (!text) {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
const codes = {
|
|
58
|
+
0: "color:inherit;font-weight:normal;font-style:normal;text-decoration:none;opacity:1;background-color:inherit;",
|
|
59
|
+
1: "font-weight:bold",
|
|
60
|
+
2: "opacity:0.6",
|
|
61
|
+
3: "font-style:italic",
|
|
62
|
+
4: "text-decoration:underline",
|
|
63
|
+
30: "color:#000", // black
|
|
64
|
+
31: "color:#d00", // red
|
|
65
|
+
32: "color:#0a0", // green
|
|
66
|
+
33: "color:#aa0", // yellow
|
|
67
|
+
34: "color:#00d", // blue
|
|
68
|
+
35: "color:#a0a", // magenta
|
|
69
|
+
36: "color:#0aa", // cyan
|
|
70
|
+
37: "color:#aaa", // light grey
|
|
71
|
+
39: "color:inherit", // default foreground color
|
|
72
|
+
40: "background-color:#000", // black background
|
|
73
|
+
41: "background-color:#d00", // red background
|
|
74
|
+
42: "background-color:#0a0", // green background
|
|
75
|
+
43: "background-color:#aa0", // yellow background
|
|
76
|
+
44: "background-color:#00d", // blue background
|
|
77
|
+
45: "background-color:#a0a", // magenta background
|
|
78
|
+
46: "background-color:#0aa", // cyan background
|
|
79
|
+
47: "background-color:#aaa", // light grey background
|
|
80
|
+
49: "background-color:inherit", // default background color
|
|
81
|
+
90: "color:#555", // dark grey
|
|
82
|
+
91: "color:#f55", // light red
|
|
83
|
+
92: "color:#5f5", // light green
|
|
84
|
+
93: "color:#ff5", // light yellow
|
|
85
|
+
94: "color:#55f", // light blue
|
|
86
|
+
95: "color:#f5f", // light magenta
|
|
87
|
+
96: "color:#5ff", // light cyan
|
|
88
|
+
97: "color:#fff", // white
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
let currentStylesArray = [];
|
|
92
|
+
let html = "";
|
|
93
|
+
let openSpan = false;
|
|
94
|
+
|
|
95
|
+
const applyStyles = () => {
|
|
96
|
+
if (openSpan) {
|
|
97
|
+
html += "</span>";
|
|
98
|
+
openSpan = false;
|
|
99
|
+
}
|
|
100
|
+
if (currentStylesArray.length > 0) {
|
|
101
|
+
const styleString = currentStylesArray.filter((s) => s).join(";");
|
|
102
|
+
if (styleString) {
|
|
103
|
+
html += `<span style="${styleString}">`;
|
|
104
|
+
openSpan = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const resetAndApplyNewCodes = (newCodesStr) => {
|
|
109
|
+
const newCodes = newCodesStr.split(";");
|
|
110
|
+
if (newCodes.includes("0")) {
|
|
111
|
+
currentStylesArray = [];
|
|
112
|
+
if (codes["0"]) currentStylesArray.push(codes["0"]);
|
|
113
|
+
}
|
|
114
|
+
for (const code of newCodes) {
|
|
115
|
+
if (code === "0") continue;
|
|
116
|
+
if (codes[code]) {
|
|
117
|
+
if (code === "39") {
|
|
118
|
+
currentStylesArray = currentStylesArray.filter(
|
|
119
|
+
(s) => !s.startsWith("color:")
|
|
120
|
+
);
|
|
121
|
+
currentStylesArray.push("color:inherit");
|
|
122
|
+
} else if (code === "49") {
|
|
123
|
+
currentStylesArray = currentStylesArray.filter(
|
|
124
|
+
(s) => !s.startsWith("background-color:")
|
|
125
|
+
);
|
|
126
|
+
currentStylesArray.push("background-color:inherit");
|
|
127
|
+
} else {
|
|
128
|
+
currentStylesArray.push(codes[code]);
|
|
129
|
+
}
|
|
130
|
+
} else if (code.startsWith("38;2;") || code.startsWith("48;2;")) {
|
|
131
|
+
const parts = code.split(";");
|
|
132
|
+
const type = parts[0] === "38" ? "color" : "background-color";
|
|
133
|
+
if (parts.length === 5) {
|
|
134
|
+
currentStylesArray = currentStylesArray.filter(
|
|
135
|
+
(s) => !s.startsWith(type + ":")
|
|
136
|
+
);
|
|
137
|
+
currentStylesArray.push(
|
|
138
|
+
`${type}:rgb(${parts[2]},${parts[3]},${parts[4]})`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
applyStyles();
|
|
144
|
+
};
|
|
145
|
+
const segments = text.split(/(\x1b\[[0-9;]*m)/g);
|
|
146
|
+
for (const segment of segments) {
|
|
147
|
+
if (!segment) continue;
|
|
148
|
+
if (segment.startsWith("\x1b[") && segment.endsWith("m")) {
|
|
149
|
+
const command = segment.slice(2, -1);
|
|
150
|
+
resetAndApplyNewCodes(command);
|
|
151
|
+
} else {
|
|
152
|
+
const escapedContent = segment
|
|
153
|
+
.replace(/&/g, "&")
|
|
154
|
+
.replace(/</g, "<")
|
|
155
|
+
.replace(/>/g, ">")
|
|
156
|
+
.replace(/"/g, """)
|
|
157
|
+
.replace(/'/g, "'");
|
|
158
|
+
html += escapedContent;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (openSpan) {
|
|
162
|
+
html += "</span>";
|
|
163
|
+
}
|
|
164
|
+
return html;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Sanitizes an HTML string by replacing special characters with their corresponding HTML entities.
|
|
168
|
+
* @param {string} str The HTML string to sanitize.
|
|
169
|
+
* @returns {string} The sanitized HTML string.
|
|
170
|
+
*/
|
|
31
171
|
function sanitizeHTML(str) {
|
|
32
172
|
if (str === null || str === undefined) return "";
|
|
33
|
-
return String(str).replace(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"<": "<",
|
|
37
|
-
|
|
38
|
-
'"': '"',
|
|
39
|
-
"'": "'", // or '
|
|
40
|
-
};
|
|
41
|
-
return replacements[match] || match;
|
|
42
|
-
});
|
|
173
|
+
return String(str).replace(
|
|
174
|
+
/[&<>"']/g,
|
|
175
|
+
(match) =>
|
|
176
|
+
({ "&": "&", "<": "<", ">": ">", '"': '"', "'": "'" }[match] || match)
|
|
177
|
+
);
|
|
43
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Capitalizes the first letter of a string and converts the rest to lowercase.
|
|
181
|
+
* @param {string} str The string to capitalize.
|
|
182
|
+
* @returns {string} The capitalized string.
|
|
183
|
+
*/
|
|
44
184
|
function capitalize(str) {
|
|
45
|
-
if (!str) return "";
|
|
185
|
+
if (!str) return "";
|
|
46
186
|
return str[0].toUpperCase() + str.slice(1).toLowerCase();
|
|
47
187
|
}
|
|
48
|
-
|
|
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
|
+
*/
|
|
49
193
|
function formatPlaywrightError(error) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
194
|
+
const commandOutput = ansiToHtml(error || error.message);
|
|
195
|
+
return convertPlaywrightErrorToHTML(commandOutput);
|
|
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
|
+
*/
|
|
202
|
+
function convertPlaywrightErrorToHTML(str) {
|
|
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>");
|
|
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
|
+
*/
|
|
225
|
+
function formatDuration(ms, options = {}) {
|
|
226
|
+
const {
|
|
227
|
+
precision = 1,
|
|
228
|
+
invalidInputReturn = "N/A",
|
|
229
|
+
defaultForNullUndefinedNegative = null,
|
|
230
|
+
} = options;
|
|
231
|
+
|
|
232
|
+
const validPrecision = Math.max(0, Math.floor(precision));
|
|
233
|
+
const zeroWithPrecision = (0).toFixed(validPrecision) + "s";
|
|
234
|
+
const resolvedNullUndefNegReturn =
|
|
235
|
+
defaultForNullUndefinedNegative === null
|
|
236
|
+
? zeroWithPrecision
|
|
237
|
+
: defaultForNullUndefinedNegative;
|
|
238
|
+
|
|
239
|
+
if (ms === undefined || ms === null) {
|
|
240
|
+
return resolvedNullUndefNegReturn;
|
|
241
|
+
}
|
|
79
242
|
|
|
80
|
-
|
|
81
|
-
let html = `<div class="playwright-error">
|
|
82
|
-
<div class="error-header">Test Error</div>`;
|
|
243
|
+
const numMs = Number(ms);
|
|
83
244
|
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
timeoutMatch[1]
|
|
87
|
-
)}ms</div>`;
|
|
245
|
+
if (Number.isNaN(numMs) || !Number.isFinite(numMs)) {
|
|
246
|
+
return invalidInputReturn;
|
|
88
247
|
}
|
|
89
248
|
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
assertionMatch[1]
|
|
93
|
-
)}).${escapeHtml(assertionMatch[2])}()</div>`;
|
|
249
|
+
if (numMs < 0) {
|
|
250
|
+
return resolvedNullUndefNegReturn;
|
|
94
251
|
}
|
|
95
252
|
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
expectedMatch[1]
|
|
99
|
-
)}</div>`;
|
|
253
|
+
if (numMs === 0) {
|
|
254
|
+
return zeroWithPrecision;
|
|
100
255
|
}
|
|
101
256
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
257
|
+
const MS_PER_SECOND = 1000;
|
|
258
|
+
const SECONDS_PER_MINUTE = 60;
|
|
259
|
+
const MINUTES_PER_HOUR = 60;
|
|
260
|
+
const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
|
|
107
261
|
|
|
108
|
-
|
|
109
|
-
const callLogStart = cleanMessage.indexOf("Call log:");
|
|
110
|
-
if (callLogStart !== -1) {
|
|
111
|
-
const callLogEnd =
|
|
112
|
-
cleanMessage.indexOf("\n\n", callLogStart) || cleanMessage.length;
|
|
113
|
-
const callLogSection = cleanMessage
|
|
114
|
-
.slice(callLogStart + 9, callLogEnd)
|
|
115
|
-
.trim();
|
|
116
|
-
|
|
117
|
-
html += `<div class="error-call-log">
|
|
118
|
-
<div class="call-log-header">📜 Call Log:</div>
|
|
119
|
-
<ul class="call-log-items">${callLogSection
|
|
120
|
-
.split("\n")
|
|
121
|
-
.map((line) => line.trim())
|
|
122
|
-
.filter((line) => line)
|
|
123
|
-
.map((line) => `<li>${escapeHtml(line.replace(/^-\s*/, ""))}</li>`)
|
|
124
|
-
.join("")}</ul>
|
|
125
|
-
</div>`;
|
|
126
|
-
}
|
|
262
|
+
const totalRawSeconds = numMs / MS_PER_SECOND;
|
|
127
263
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
.map((line) => line.trim())
|
|
137
|
-
.filter((line) => line)
|
|
138
|
-
.map((line) => `<li>${escapeHtml(line)}</li>`)
|
|
139
|
-
.join("")}</ul>
|
|
140
|
-
</div>`;
|
|
141
|
-
}
|
|
264
|
+
if (
|
|
265
|
+
totalRawSeconds < SECONDS_PER_MINUTE &&
|
|
266
|
+
Math.ceil(totalRawSeconds) < SECONDS_PER_MINUTE
|
|
267
|
+
) {
|
|
268
|
+
return `${totalRawSeconds.toFixed(validPrecision)}s`;
|
|
269
|
+
} else {
|
|
270
|
+
const totalMsRoundedUpToSecond =
|
|
271
|
+
Math.ceil(numMs / MS_PER_SECOND) * MS_PER_SECOND;
|
|
142
272
|
|
|
143
|
-
|
|
273
|
+
let remainingMs = totalMsRoundedUpToSecond;
|
|
144
274
|
|
|
145
|
-
|
|
146
|
-
|
|
275
|
+
const h = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_HOUR));
|
|
276
|
+
remainingMs %= MS_PER_SECOND * SECONDS_PER_HOUR;
|
|
147
277
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
278
|
+
const m = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_MINUTE));
|
|
279
|
+
remainingMs %= MS_PER_SECOND * SECONDS_PER_MINUTE;
|
|
280
|
+
|
|
281
|
+
const s = Math.floor(remainingMs / MS_PER_SECOND);
|
|
153
282
|
|
|
283
|
+
const parts = [];
|
|
284
|
+
if (h > 0) {
|
|
285
|
+
parts.push(`${h}h`);
|
|
286
|
+
}
|
|
287
|
+
if (h > 0 || m > 0 || numMs >= MS_PER_SECOND * SECONDS_PER_MINUTE) {
|
|
288
|
+
parts.push(`${m}m`);
|
|
289
|
+
}
|
|
290
|
+
parts.push(`${s}s`);
|
|
291
|
+
|
|
292
|
+
return parts.join(" ");
|
|
293
|
+
}
|
|
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
|
+
*/
|
|
154
301
|
function generateTestTrendsChart(trendData) {
|
|
155
302
|
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
156
303
|
return '<div class="no-data">No overall trend data available for test counts.</div>';
|
|
@@ -159,107 +306,99 @@ function generateTestTrendsChart(trendData) {
|
|
|
159
306
|
const chartId = `testTrendsChart-${Date.now()}-${Math.random()
|
|
160
307
|
.toString(36)
|
|
161
308
|
.substring(2, 7)}`;
|
|
309
|
+
const renderFunctionName = `renderTestTrendsChart_${chartId.replace(
|
|
310
|
+
/-/g,
|
|
311
|
+
"_"
|
|
312
|
+
)}`;
|
|
162
313
|
const runs = trendData.overall;
|
|
163
314
|
|
|
164
315
|
const series = [
|
|
165
316
|
{
|
|
166
317
|
name: "Total",
|
|
167
318
|
data: runs.map((r) => r.totalTests),
|
|
168
|
-
color: "var(--primary-color)",
|
|
319
|
+
color: "var(--primary-color)",
|
|
169
320
|
marker: { symbol: "circle" },
|
|
170
321
|
},
|
|
171
322
|
{
|
|
172
323
|
name: "Passed",
|
|
173
324
|
data: runs.map((r) => r.passed),
|
|
174
|
-
color: "var(--success-color)",
|
|
325
|
+
color: "var(--success-color)",
|
|
175
326
|
marker: { symbol: "circle" },
|
|
176
327
|
},
|
|
177
328
|
{
|
|
178
329
|
name: "Failed",
|
|
179
330
|
data: runs.map((r) => r.failed),
|
|
180
|
-
color: "var(--danger-color)",
|
|
331
|
+
color: "var(--danger-color)",
|
|
181
332
|
marker: { symbol: "circle" },
|
|
182
333
|
},
|
|
183
334
|
{
|
|
184
335
|
name: "Skipped",
|
|
185
336
|
data: runs.map((r) => r.skipped || 0),
|
|
186
|
-
color: "var(--warning-color)",
|
|
337
|
+
color: "var(--warning-color)",
|
|
187
338
|
marker: { symbol: "circle" },
|
|
188
339
|
},
|
|
189
340
|
];
|
|
190
|
-
|
|
191
|
-
// Data needed by the tooltip formatter, stringified to be embedded in the client-side script
|
|
192
341
|
const runsForTooltip = runs.map((r) => ({
|
|
193
342
|
runId: r.runId,
|
|
194
343
|
timestamp: r.timestamp,
|
|
195
344
|
duration: r.duration,
|
|
196
345
|
}));
|
|
197
346
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
title: { text: null },
|
|
202
|
-
xAxis: {
|
|
203
|
-
categories: ${JSON.stringify(runs.map((run, i) => `Run ${i + 1}`))},
|
|
204
|
-
crosshair: true,
|
|
205
|
-
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}
|
|
206
|
-
},
|
|
207
|
-
yAxis: {
|
|
208
|
-
title: { text: "Test Count", style: { color: 'var(--text-color)'} },
|
|
209
|
-
min: 0,
|
|
210
|
-
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}
|
|
211
|
-
},
|
|
212
|
-
legend: {
|
|
213
|
-
layout: "horizontal", align: "center", verticalAlign: "bottom",
|
|
214
|
-
itemStyle: { fontSize: "12px", color: 'var(--text-color)' }
|
|
215
|
-
},
|
|
216
|
-
plotOptions: {
|
|
217
|
-
series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}},
|
|
218
|
-
line: { lineWidth: 2.5 } // fillOpacity was 0.1, but for line charts, area fill is usually separate (area chart type)
|
|
219
|
-
},
|
|
220
|
-
tooltip: {
|
|
221
|
-
shared: true, useHTML: true,
|
|
222
|
-
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
223
|
-
borderColor: 'rgba(10,10,10,0.92)',
|
|
224
|
-
style: { color: '#f5f5f5' },
|
|
225
|
-
formatter: function () {
|
|
226
|
-
const runsData = ${JSON.stringify(runsForTooltip)};
|
|
227
|
-
const pointIndex = this.points[0].point.x; // Get index from point
|
|
228
|
-
const run = runsData[pointIndex];
|
|
229
|
-
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' +
|
|
230
|
-
'Date: ' + new Date(run.timestamp).toLocaleString() + '<br><br>';
|
|
231
|
-
this.points.forEach(point => {
|
|
232
|
-
tooltip += '<span style="color:' + point.color + '">●</span> ' + point.series.name + ': <b>' + point.y + '</b><br>';
|
|
233
|
-
});
|
|
234
|
-
tooltip += '<br>Duration: ' + formatDuration(run.duration);
|
|
235
|
-
return tooltip;
|
|
236
|
-
}
|
|
237
|
-
},
|
|
238
|
-
series: ${JSON.stringify(series)},
|
|
239
|
-
credits: { enabled: false }
|
|
240
|
-
}
|
|
241
|
-
`;
|
|
347
|
+
const categoriesString = JSON.stringify(runs.map((run, i) => `Run ${i + 1}`));
|
|
348
|
+
const seriesString = JSON.stringify(series);
|
|
349
|
+
const runsForTooltipString = JSON.stringify(runsForTooltip);
|
|
242
350
|
|
|
243
351
|
return `
|
|
244
|
-
<div id="${chartId}" class="trend-chart-container"
|
|
352
|
+
<div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
|
|
353
|
+
<div class="no-data">Loading Test Volume Trends...</div>
|
|
354
|
+
</div>
|
|
245
355
|
<script>
|
|
246
|
-
|
|
356
|
+
window.${renderFunctionName} = function() {
|
|
357
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
358
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found for lazy loading."); return; }
|
|
247
359
|
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
248
360
|
try {
|
|
249
|
-
|
|
361
|
+
chartContainer.innerHTML = ''; // Clear placeholder
|
|
362
|
+
const chartOptions = {
|
|
363
|
+
chart: { type: "line", height: 350, backgroundColor: "transparent" },
|
|
364
|
+
title: { text: null },
|
|
365
|
+
xAxis: { categories: ${categoriesString}, crosshair: true, labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}},
|
|
366
|
+
yAxis: { title: { text: "Test Count", style: { color: 'var(--text-color)'} }, min: 0, labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}},
|
|
367
|
+
legend: { layout: "horizontal", align: "center", verticalAlign: "bottom", itemStyle: { fontSize: "12px", color: 'var(--text-color)' }},
|
|
368
|
+
plotOptions: { series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}}, line: { lineWidth: 2.5 }},
|
|
369
|
+
tooltip: {
|
|
370
|
+
shared: true, useHTML: true, backgroundColor: 'rgba(10,10,10,0.92)', borderColor: 'rgba(10,10,10,0.92)', style: { color: '#f5f5f5' },
|
|
371
|
+
formatter: function () {
|
|
372
|
+
const runsData = ${runsForTooltipString};
|
|
373
|
+
const pointIndex = this.points[0].point.x;
|
|
374
|
+
const run = runsData[pointIndex];
|
|
375
|
+
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' + 'Date: ' + new Date(run.timestamp).toLocaleString() + '<br><br>';
|
|
376
|
+
this.points.forEach(point => { tooltip += '<span style="color:' + point.color + '">●</span> ' + point.series.name + ': <b>' + point.y + '</b><br>'; });
|
|
377
|
+
tooltip += '<br>Duration: ' + formatDuration(run.duration);
|
|
378
|
+
return tooltip;
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
series: ${seriesString},
|
|
382
|
+
credits: { enabled: false }
|
|
383
|
+
};
|
|
250
384
|
Highcharts.chart('${chartId}', chartOptions);
|
|
251
385
|
} catch (e) {
|
|
252
|
-
console.error("Error rendering chart ${chartId}:", e);
|
|
253
|
-
|
|
386
|
+
console.error("Error rendering chart ${chartId} (lazy):", e);
|
|
387
|
+
chartContainer.innerHTML = '<div class="no-data">Error rendering test trends chart.</div>';
|
|
254
388
|
}
|
|
255
389
|
} else {
|
|
256
|
-
|
|
390
|
+
chartContainer.innerHTML = '<div class="no-data">Charting library not available for test trends.</div>';
|
|
257
391
|
}
|
|
258
|
-
}
|
|
392
|
+
};
|
|
259
393
|
</script>
|
|
260
394
|
`;
|
|
261
395
|
}
|
|
262
|
-
|
|
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
|
+
*/
|
|
263
402
|
function generateDurationTrendChart(trendData) {
|
|
264
403
|
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
265
404
|
return '<div class="no-data">No overall trend data available for durations.</div>';
|
|
@@ -267,109 +406,88 @@ function generateDurationTrendChart(trendData) {
|
|
|
267
406
|
const chartId = `durationTrendChart-${Date.now()}-${Math.random()
|
|
268
407
|
.toString(36)
|
|
269
408
|
.substring(2, 7)}`;
|
|
409
|
+
const renderFunctionName = `renderDurationTrendChart_${chartId.replace(
|
|
410
|
+
/-/g,
|
|
411
|
+
"_"
|
|
412
|
+
)}`;
|
|
270
413
|
const runs = trendData.overall;
|
|
271
414
|
|
|
272
|
-
// Assuming var(--accent-color-alt) is Orange #FF9800
|
|
273
|
-
const accentColorAltRGB = "255, 152, 0";
|
|
274
|
-
|
|
275
|
-
const seriesString = `[{
|
|
276
|
-
name: 'Duration',
|
|
277
|
-
data: ${JSON.stringify(runs.map((run) => run.duration))},
|
|
278
|
-
color: 'var(--accent-color-alt)',
|
|
279
|
-
type: 'area',
|
|
280
|
-
marker: {
|
|
281
|
-
symbol: 'circle', enabled: true, radius: 4,
|
|
282
|
-
states: { hover: { radius: 6, lineWidthPlus: 0 } }
|
|
283
|
-
},
|
|
284
|
-
fillColor: {
|
|
285
|
-
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
286
|
-
stops: [
|
|
287
|
-
[0, 'rgba(${accentColorAltRGB}, 0.4)'],
|
|
288
|
-
[1, 'rgba(${accentColorAltRGB}, 0.05)']
|
|
289
|
-
]
|
|
290
|
-
},
|
|
291
|
-
lineWidth: 2.5
|
|
292
|
-
}]`;
|
|
415
|
+
const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
|
|
293
416
|
|
|
417
|
+
const chartDataString = JSON.stringify(runs.map((run) => run.duration));
|
|
418
|
+
const categoriesString = JSON.stringify(runs.map((run, i) => `Run ${i + 1}`));
|
|
294
419
|
const runsForTooltip = runs.map((r) => ({
|
|
295
420
|
runId: r.runId,
|
|
296
421
|
timestamp: r.timestamp,
|
|
297
422
|
duration: r.duration,
|
|
298
423
|
totalTests: r.totalTests,
|
|
299
424
|
}));
|
|
425
|
+
const runsForTooltipString = JSON.stringify(runsForTooltip);
|
|
300
426
|
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
yAxis: {
|
|
311
|
-
title: { text: 'Duration', style: { color: 'var(--text-color)' } },
|
|
312
|
-
labels: {
|
|
313
|
-
formatter: function() { return formatDuration(this.value); },
|
|
314
|
-
style: { color: 'var(--text-color-secondary)', fontSize: '12px' }
|
|
315
|
-
},
|
|
316
|
-
min: 0
|
|
317
|
-
},
|
|
318
|
-
legend: {
|
|
319
|
-
layout: 'horizontal', align: 'center', verticalAlign: 'bottom',
|
|
320
|
-
itemStyle: { fontSize: '12px', color: 'var(--text-color)' }
|
|
321
|
-
},
|
|
322
|
-
plotOptions: {
|
|
323
|
-
area: {
|
|
324
|
-
lineWidth: 2.5,
|
|
325
|
-
states: { hover: { lineWidthPlus: 0 } },
|
|
326
|
-
threshold: null
|
|
327
|
-
}
|
|
328
|
-
},
|
|
329
|
-
tooltip: {
|
|
330
|
-
shared: true, useHTML: true,
|
|
331
|
-
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
332
|
-
borderColor: 'rgba(10,10,10,0.92)',
|
|
333
|
-
style: { color: '#f5f5f5' },
|
|
334
|
-
formatter: function () {
|
|
335
|
-
const runsData = ${JSON.stringify(runsForTooltip)};
|
|
336
|
-
const pointIndex = this.points[0].point.x;
|
|
337
|
-
const run = runsData[pointIndex];
|
|
338
|
-
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' +
|
|
339
|
-
'Date: ' + new Date(run.timestamp).toLocaleString() + '<br>';
|
|
340
|
-
this.points.forEach(point => {
|
|
341
|
-
tooltip += '<span style="color:' + point.series.color + '">●</span> ' +
|
|
342
|
-
point.series.name + ': <b>' + formatDuration(point.y) + '</b><br>';
|
|
343
|
-
});
|
|
344
|
-
tooltip += '<br>Tests: ' + run.totalTests;
|
|
345
|
-
return tooltip;
|
|
346
|
-
}
|
|
347
|
-
},
|
|
348
|
-
series: ${seriesString},
|
|
349
|
-
credits: { enabled: false }
|
|
350
|
-
}
|
|
351
|
-
`;
|
|
427
|
+
const seriesStringForRender = `[{
|
|
428
|
+
name: 'Duration',
|
|
429
|
+
data: ${chartDataString},
|
|
430
|
+
color: 'var(--accent-color-alt)',
|
|
431
|
+
type: 'area',
|
|
432
|
+
marker: { symbol: 'circle', enabled: true, radius: 4, states: { hover: { radius: 6, lineWidthPlus: 0 } } },
|
|
433
|
+
fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(${accentColorAltRGB}, 0.4)'], [1, 'rgba(${accentColorAltRGB}, 0.05)']] },
|
|
434
|
+
lineWidth: 2.5
|
|
435
|
+
}]`;
|
|
352
436
|
|
|
353
437
|
return `
|
|
354
|
-
<div id="${chartId}" class="trend-chart-container"
|
|
438
|
+
<div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
|
|
439
|
+
<div class="no-data">Loading Duration Trends...</div>
|
|
440
|
+
</div>
|
|
355
441
|
<script>
|
|
356
|
-
|
|
442
|
+
window.${renderFunctionName} = function() {
|
|
443
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
444
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found for lazy loading."); return; }
|
|
357
445
|
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
358
446
|
try {
|
|
359
|
-
|
|
447
|
+
chartContainer.innerHTML = ''; // Clear placeholder
|
|
448
|
+
const chartOptions = {
|
|
449
|
+
chart: { type: 'area', height: 350, backgroundColor: 'transparent' },
|
|
450
|
+
title: { text: null },
|
|
451
|
+
xAxis: { categories: ${categoriesString}, crosshair: true, labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}},
|
|
452
|
+
yAxis: {
|
|
453
|
+
title: { text: 'Duration', style: { color: 'var(--text-color)' } },
|
|
454
|
+
labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)', fontSize: '12px' }},
|
|
455
|
+
min: 0
|
|
456
|
+
},
|
|
457
|
+
legend: { layout: 'horizontal', align: 'center', verticalAlign: 'bottom', itemStyle: { fontSize: '12px', color: 'var(--text-color)' }},
|
|
458
|
+
plotOptions: { area: { lineWidth: 2.5, states: { hover: { lineWidthPlus: 0 } }, threshold: null }},
|
|
459
|
+
tooltip: {
|
|
460
|
+
shared: true, useHTML: true, backgroundColor: 'rgba(10,10,10,0.92)', borderColor: 'rgba(10,10,10,0.92)', style: { color: '#f5f5f5' },
|
|
461
|
+
formatter: function () {
|
|
462
|
+
const runsData = ${runsForTooltipString};
|
|
463
|
+
const pointIndex = this.points[0].point.x;
|
|
464
|
+
const run = runsData[pointIndex];
|
|
465
|
+
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' + 'Date: ' + new Date(run.timestamp).toLocaleString() + '<br>';
|
|
466
|
+
this.points.forEach(point => { tooltip += '<span style="color:' + point.series.color + '">●</span> ' + point.series.name + ': <b>' + formatDuration(point.y) + '</b><br>'; });
|
|
467
|
+
tooltip += '<br>Tests: ' + run.totalTests;
|
|
468
|
+
return tooltip;
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
series: ${seriesStringForRender}, // This is already a string representation of an array
|
|
472
|
+
credits: { enabled: false }
|
|
473
|
+
};
|
|
360
474
|
Highcharts.chart('${chartId}', chartOptions);
|
|
361
475
|
} catch (e) {
|
|
362
|
-
console.error("Error rendering chart ${chartId}:", e);
|
|
363
|
-
|
|
476
|
+
console.error("Error rendering chart ${chartId} (lazy):", e);
|
|
477
|
+
chartContainer.innerHTML = '<div class="no-data">Error rendering duration trend chart.</div>';
|
|
364
478
|
}
|
|
365
479
|
} else {
|
|
366
|
-
|
|
480
|
+
chartContainer.innerHTML = '<div class="no-data">Charting library not available for duration trends.</div>';
|
|
367
481
|
}
|
|
368
|
-
}
|
|
482
|
+
};
|
|
369
483
|
</script>
|
|
370
484
|
`;
|
|
371
485
|
}
|
|
372
|
-
|
|
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
|
+
*/
|
|
373
491
|
function formatDate(dateStrOrDate) {
|
|
374
492
|
if (!dateStrOrDate) return "N/A";
|
|
375
493
|
try {
|
|
@@ -388,11 +506,15 @@ function formatDate(dateStrOrDate) {
|
|
|
388
506
|
return "Invalid Date Format";
|
|
389
507
|
}
|
|
390
508
|
}
|
|
391
|
-
|
|
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
|
+
*/
|
|
392
515
|
function generateTestHistoryChart(history) {
|
|
393
516
|
if (!history || history.length === 0)
|
|
394
517
|
return '<div class="no-data-chart">No data for chart</div>';
|
|
395
|
-
|
|
396
518
|
const validHistory = history.filter(
|
|
397
519
|
(h) => h && typeof h.duration === "number" && h.duration >= 0
|
|
398
520
|
);
|
|
@@ -402,6 +524,10 @@ function generateTestHistoryChart(history) {
|
|
|
402
524
|
const chartId = `testHistoryChart-${Date.now()}-${Math.random()
|
|
403
525
|
.toString(36)
|
|
404
526
|
.substring(2, 7)}`;
|
|
527
|
+
const renderFunctionName = `renderTestHistoryChart_${chartId.replace(
|
|
528
|
+
/-/g,
|
|
529
|
+
"_"
|
|
530
|
+
)}`;
|
|
405
531
|
|
|
406
532
|
const seriesDataPoints = validHistory.map((run) => {
|
|
407
533
|
let color;
|
|
@@ -431,94 +557,78 @@ function generateTestHistoryChart(history) {
|
|
|
431
557
|
};
|
|
432
558
|
});
|
|
433
559
|
|
|
434
|
-
// Assuming var(--accent-color) is Deep Purple #673ab7
|
|
435
|
-
const accentColorRGB = "103, 58, 183";
|
|
560
|
+
const accentColorRGB = "103, 58, 183"; // Assuming var(--accent-color) is Deep Purple #673ab7
|
|
436
561
|
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
xAxis: {
|
|
442
|
-
categories: ${JSON.stringify(
|
|
443
|
-
validHistory.map((_, i) => `R${i + 1}`)
|
|
444
|
-
)},
|
|
445
|
-
labels: { style: { fontSize: '10px', color: 'var(--text-color-secondary)' } }
|
|
446
|
-
},
|
|
447
|
-
yAxis: {
|
|
448
|
-
title: { text: null },
|
|
449
|
-
labels: {
|
|
450
|
-
formatter: function() { return formatDuration(this.value); },
|
|
451
|
-
style: { fontSize: '10px', color: 'var(--text-color-secondary)' },
|
|
452
|
-
align: 'left', x: -35, y: 3
|
|
453
|
-
},
|
|
454
|
-
min: 0,
|
|
455
|
-
gridLineWidth: 0,
|
|
456
|
-
tickAmount: 4
|
|
457
|
-
},
|
|
458
|
-
legend: { enabled: false },
|
|
459
|
-
plotOptions: {
|
|
460
|
-
area: {
|
|
461
|
-
lineWidth: 2,
|
|
462
|
-
lineColor: 'var(--accent-color)',
|
|
463
|
-
fillColor: {
|
|
464
|
-
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
465
|
-
stops: [
|
|
466
|
-
[0, 'rgba(${accentColorRGB}, 0.4)'],
|
|
467
|
-
[1, 'rgba(${accentColorRGB}, 0)']
|
|
468
|
-
]
|
|
469
|
-
},
|
|
470
|
-
marker: { enabled: true },
|
|
471
|
-
threshold: null
|
|
472
|
-
}
|
|
473
|
-
},
|
|
474
|
-
tooltip: {
|
|
475
|
-
useHTML: true,
|
|
476
|
-
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
477
|
-
borderColor: 'rgba(10,10,10,0.92)',
|
|
478
|
-
style: { color: '#f5f5f5', padding: '8px' },
|
|
479
|
-
formatter: function() {
|
|
480
|
-
const pointData = this.point;
|
|
481
|
-
let statusBadgeHtml = '<span style="padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; background-color: ';
|
|
482
|
-
switch(String(pointData.status).toLowerCase()) {
|
|
483
|
-
case 'passed': statusBadgeHtml += 'var(--success-color)'; break;
|
|
484
|
-
case 'failed': statusBadgeHtml += 'var(--danger-color)'; break;
|
|
485
|
-
case 'skipped': statusBadgeHtml += 'var(--warning-color)'; break;
|
|
486
|
-
default: statusBadgeHtml += 'var(--dark-gray-color)';
|
|
487
|
-
}
|
|
488
|
-
statusBadgeHtml += ';">' + String(pointData.status).toUpperCase() + '</span>';
|
|
562
|
+
const categoriesString = JSON.stringify(
|
|
563
|
+
validHistory.map((_, i) => `R${i + 1}`)
|
|
564
|
+
);
|
|
565
|
+
const seriesDataPointsString = JSON.stringify(seriesDataPoints);
|
|
489
566
|
|
|
490
|
-
return '<strong>Run ' + (pointData.runId || (this.point.index + 1)) + '</strong><br>' +
|
|
491
|
-
'Status: ' + statusBadgeHtml + '<br>' +
|
|
492
|
-
'Duration: ' + formatDuration(pointData.y);
|
|
493
|
-
}
|
|
494
|
-
},
|
|
495
|
-
series: [{
|
|
496
|
-
data: ${JSON.stringify(seriesDataPoints)},
|
|
497
|
-
showInLegend: false
|
|
498
|
-
}],
|
|
499
|
-
credits: { enabled: false }
|
|
500
|
-
}
|
|
501
|
-
`;
|
|
502
567
|
return `
|
|
503
|
-
<div id="${chartId}" style="width: 320px; height: 100px;"
|
|
568
|
+
<div id="${chartId}" style="width: 320px; height: 100px;" class="lazy-load-chart" data-render-function-name="${renderFunctionName}">
|
|
569
|
+
<div class="no-data-chart">Loading History...</div>
|
|
570
|
+
</div>
|
|
504
571
|
<script>
|
|
505
|
-
|
|
572
|
+
window.${renderFunctionName} = function() {
|
|
573
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
574
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found for lazy loading."); return; }
|
|
506
575
|
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
507
576
|
try {
|
|
508
|
-
|
|
577
|
+
chartContainer.innerHTML = ''; // Clear placeholder
|
|
578
|
+
const chartOptions = {
|
|
579
|
+
chart: { type: 'area', height: 100, width: 320, backgroundColor: 'transparent', spacing: [10,10,15,35] },
|
|
580
|
+
title: { text: null },
|
|
581
|
+
xAxis: { categories: ${categoriesString}, labels: { style: { fontSize: '10px', color: 'var(--text-color-secondary)' }}},
|
|
582
|
+
yAxis: {
|
|
583
|
+
title: { text: null },
|
|
584
|
+
labels: { formatter: function() { return formatDuration(this.value); }, style: { fontSize: '10px', color: 'var(--text-color-secondary)' }, align: 'left', x: -35, y: 3 },
|
|
585
|
+
min: 0, gridLineWidth: 0, tickAmount: 4
|
|
586
|
+
},
|
|
587
|
+
legend: { enabled: false },
|
|
588
|
+
plotOptions: {
|
|
589
|
+
area: {
|
|
590
|
+
lineWidth: 2, lineColor: 'var(--accent-color)',
|
|
591
|
+
fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(${accentColorRGB}, 0.4)'],[1, 'rgba(${accentColorRGB}, 0)']]},
|
|
592
|
+
marker: { enabled: true }, threshold: null
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
tooltip: {
|
|
596
|
+
useHTML: true, backgroundColor: 'rgba(10,10,10,0.92)', borderColor: 'rgba(10,10,10,0.92)', style: { color: '#f5f5f5', padding: '8px' },
|
|
597
|
+
formatter: function() {
|
|
598
|
+
const pointData = this.point;
|
|
599
|
+
let statusBadgeHtml = '<span style="padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; background-color: ';
|
|
600
|
+
switch(String(pointData.status).toLowerCase()) {
|
|
601
|
+
case 'passed': statusBadgeHtml += 'var(--success-color)'; break;
|
|
602
|
+
case 'failed': statusBadgeHtml += 'var(--danger-color)'; break;
|
|
603
|
+
case 'skipped': statusBadgeHtml += 'var(--warning-color)'; break;
|
|
604
|
+
default: statusBadgeHtml += 'var(--dark-gray-color)';
|
|
605
|
+
}
|
|
606
|
+
statusBadgeHtml += ';">' + String(pointData.status).toUpperCase() + '</span>';
|
|
607
|
+
return '<strong>Run ' + (pointData.runId || (this.point.index + 1)) + '</strong><br>' + 'Status: ' + statusBadgeHtml + '<br>' + 'Duration: ' + formatDuration(pointData.y);
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
series: [{ data: ${seriesDataPointsString}, showInLegend: false }],
|
|
611
|
+
credits: { enabled: false }
|
|
612
|
+
};
|
|
509
613
|
Highcharts.chart('${chartId}', chartOptions);
|
|
510
614
|
} catch (e) {
|
|
511
|
-
console.error("Error rendering chart ${chartId}:", e);
|
|
512
|
-
|
|
615
|
+
console.error("Error rendering chart ${chartId} (lazy):", e);
|
|
616
|
+
chartContainer.innerHTML = '<div class="no-data-chart">Error rendering history chart.</div>';
|
|
513
617
|
}
|
|
514
618
|
} else {
|
|
515
|
-
|
|
619
|
+
chartContainer.innerHTML = '<div class="no-data-chart">Charting library not available for history.</div>';
|
|
516
620
|
}
|
|
517
|
-
}
|
|
621
|
+
};
|
|
518
622
|
</script>
|
|
519
623
|
`;
|
|
520
624
|
}
|
|
521
|
-
|
|
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
|
+
*/
|
|
522
632
|
function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
523
633
|
const total = data.reduce((sum, d) => sum + d.value, 0);
|
|
524
634
|
if (total === 0) {
|
|
@@ -625,7 +735,7 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
625
735
|
`;
|
|
626
736
|
|
|
627
737
|
return `
|
|
628
|
-
<div class="pie-chart-wrapper" style="align-items: center">
|
|
738
|
+
<div class="pie-chart-wrapper" style="align-items: center; max-height: 450px">
|
|
629
739
|
<div style="display: flex; align-items: start; width: 100%;"><h3>Test Distribution</h3></div>
|
|
630
740
|
<div id="${chartId}" style="width: ${chartWidth}px; height: ${
|
|
631
741
|
chartHeight - 40
|
|
@@ -648,7 +758,631 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
648
758
|
</div>
|
|
649
759
|
`;
|
|
650
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
|
+
*/
|
|
767
|
+
function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
768
|
+
// Format memory for display
|
|
769
|
+
const formattedMemory = environment.memory.replace(/(\d+\.\d{2})GB/, "$1 GB");
|
|
770
|
+
|
|
771
|
+
// Generate a unique ID for the dashboard
|
|
772
|
+
const dashboardId = `envDashboard-${Date.now()}-${Math.random()
|
|
773
|
+
.toString(36)
|
|
774
|
+
.substring(2, 7)}`;
|
|
775
|
+
|
|
776
|
+
const cardHeight = Math.floor(dashboardHeight * 0.44);
|
|
777
|
+
const cardContentPadding = 16; // px
|
|
778
|
+
|
|
779
|
+
return `
|
|
780
|
+
<div class="environment-dashboard-wrapper" id="${dashboardId}">
|
|
781
|
+
<style>
|
|
782
|
+
.environment-dashboard-wrapper *,
|
|
783
|
+
.environment-dashboard-wrapper *::before,
|
|
784
|
+
.environment-dashboard-wrapper *::after {
|
|
785
|
+
box-sizing: border-box;
|
|
786
|
+
}
|
|
787
|
+
|
|
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
|
+
}
|
|
821
|
+
|
|
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
|
+
}
|
|
651
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>
|
|
952
|
+
|
|
953
|
+
<div class="env-dashboard-header">
|
|
954
|
+
<div>
|
|
955
|
+
<h3 class="env-dashboard-title">System Environment</h3>
|
|
956
|
+
<p class="env-dashboard-subtitle">Snapshot of the execution environment</p>
|
|
957
|
+
</div>
|
|
958
|
+
<span class="env-chip env-chip-primary">${environment.host}</span>
|
|
959
|
+
</div>
|
|
960
|
+
|
|
961
|
+
<div class="env-card">
|
|
962
|
+
<div class="env-card-header">
|
|
963
|
+
<svg viewBox="0 0 24 24"><path d="M4 6h16V4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8h-2v10H4V6zm18-2h-4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2H6a2 2 0 0 0-2 2v2h20V6a2 2 0 0 0-2-2zM8 12h8v2H8v-2zm0 4h8v2H8v-2z"/></svg>
|
|
964
|
+
Hardware
|
|
965
|
+
</div>
|
|
966
|
+
<div class="env-card-content">
|
|
967
|
+
<div class="env-detail-row">
|
|
968
|
+
<span class="env-detail-label">CPU Model</span>
|
|
969
|
+
<span class="env-detail-value">${environment.cpu.model}</span>
|
|
970
|
+
</div>
|
|
971
|
+
<div class="env-detail-row">
|
|
972
|
+
<span class="env-detail-label">CPU Cores</span>
|
|
973
|
+
<span class="env-detail-value">
|
|
974
|
+
<div class="env-cpu-cores">
|
|
975
|
+
${Array.from(
|
|
976
|
+
{ length: Math.max(0, environment.cpu.cores || 0) },
|
|
977
|
+
(_, i) =>
|
|
978
|
+
`<div class="env-core-indicator ${
|
|
979
|
+
i >=
|
|
980
|
+
(environment.cpu.cores >= 8 ? 8 : environment.cpu.cores)
|
|
981
|
+
? "inactive"
|
|
982
|
+
: ""
|
|
983
|
+
}" title="Core ${i + 1}"></div>`
|
|
984
|
+
).join("")}
|
|
985
|
+
<span>${environment.cpu.cores || "N/A"} cores</span>
|
|
986
|
+
</div>
|
|
987
|
+
</span>
|
|
988
|
+
</div>
|
|
989
|
+
<div class="env-detail-row">
|
|
990
|
+
<span class="env-detail-label">Memory</span>
|
|
991
|
+
<span class="env-detail-value">${formattedMemory}</span>
|
|
992
|
+
</div>
|
|
993
|
+
</div>
|
|
994
|
+
</div>
|
|
995
|
+
|
|
996
|
+
<div class="env-card">
|
|
997
|
+
<div class="env-card-header">
|
|
998
|
+
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-0.01 18c-2.76 0-5.26-1.12-7.07-2.93A7.973 7.973 0 0 1 4 12c0-2.21.9-4.21 2.36-5.64A7.994 7.994 0 0 1 11.99 4c4.41 0 8 3.59 8 8 0 2.76-1.12 5.26-2.93 7.07A7.973 7.973 0 0 1 11.99 20zM12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"/></svg>
|
|
999
|
+
Operating System
|
|
1000
|
+
</div>
|
|
1001
|
+
<div class="env-card-content">
|
|
1002
|
+
<div class="env-detail-row">
|
|
1003
|
+
<span class="env-detail-label">OS Type</span>
|
|
1004
|
+
<span class="env-detail-value">${
|
|
1005
|
+
environment.os.split(" ")[0] === "darwin"
|
|
1006
|
+
? "darwin (macOS)"
|
|
1007
|
+
: environment.os.split(" ")[0] || "Unknown"
|
|
1008
|
+
}</span>
|
|
1009
|
+
</div>
|
|
1010
|
+
<div class="env-detail-row">
|
|
1011
|
+
<span class="env-detail-label">OS Version</span>
|
|
1012
|
+
<span class="env-detail-value">${
|
|
1013
|
+
environment.os.split(" ")[1] || "N/A"
|
|
1014
|
+
}</span>
|
|
1015
|
+
</div>
|
|
1016
|
+
<div class="env-detail-row">
|
|
1017
|
+
<span class="env-detail-label">Hostname</span>
|
|
1018
|
+
<span class="env-detail-value" title="${environment.host}">${
|
|
1019
|
+
environment.host
|
|
1020
|
+
}</span>
|
|
1021
|
+
</div>
|
|
1022
|
+
</div>
|
|
1023
|
+
</div>
|
|
1024
|
+
|
|
1025
|
+
<div class="env-card">
|
|
1026
|
+
<div class="env-card-header">
|
|
1027
|
+
<svg viewBox="0 0 24 24"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
|
|
1028
|
+
Node.js Runtime
|
|
1029
|
+
</div>
|
|
1030
|
+
<div class="env-card-content">
|
|
1031
|
+
<div class="env-detail-row">
|
|
1032
|
+
<span class="env-detail-label">Node Version</span>
|
|
1033
|
+
<span class="env-detail-value">${environment.node}</span>
|
|
1034
|
+
</div>
|
|
1035
|
+
<div class="env-detail-row">
|
|
1036
|
+
<span class="env-detail-label">V8 Engine</span>
|
|
1037
|
+
<span class="env-detail-value">${environment.v8}</span>
|
|
1038
|
+
</div>
|
|
1039
|
+
<div class="env-detail-row">
|
|
1040
|
+
<span class="env-detail-label">Working Dir</span>
|
|
1041
|
+
<span class="env-detail-value" title="${environment.cwd}">${
|
|
1042
|
+
environment.cwd.length > 25
|
|
1043
|
+
? "..." + environment.cwd.slice(-22)
|
|
1044
|
+
: environment.cwd
|
|
1045
|
+
}</span>
|
|
1046
|
+
</div>
|
|
1047
|
+
</div>
|
|
1048
|
+
</div>
|
|
1049
|
+
|
|
1050
|
+
<div class="env-card">
|
|
1051
|
+
<div class="env-card-header">
|
|
1052
|
+
<svg viewBox="0 0 24 24"><path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4s1.79-4 4-4h.71C7.37 8.69 9.48 7 12 7c2.76 0 5 2.24 5 5v1h2c1.66 0 3 1.34 3 3s-1.34 3-3 3z"/></svg>
|
|
1053
|
+
System Summary
|
|
1054
|
+
</div>
|
|
1055
|
+
<div class="env-card-content">
|
|
1056
|
+
<div class="env-detail-row">
|
|
1057
|
+
<span class="env-detail-label">Platform Arch</span>
|
|
1058
|
+
<span class="env-detail-value">
|
|
1059
|
+
<span class="env-chip ${
|
|
1060
|
+
environment.os.includes("darwin") &&
|
|
1061
|
+
environment.cpu.model.toLowerCase().includes("apple")
|
|
1062
|
+
? "env-chip-success"
|
|
1063
|
+
: "env-chip-warning"
|
|
1064
|
+
}">
|
|
1065
|
+
${
|
|
1066
|
+
environment.os.includes("darwin") &&
|
|
1067
|
+
environment.cpu.model.toLowerCase().includes("apple")
|
|
1068
|
+
? "Apple Silicon"
|
|
1069
|
+
: environment.cpu.model.toLowerCase().includes("arm") ||
|
|
1070
|
+
environment.cpu.model.toLowerCase().includes("aarch64")
|
|
1071
|
+
? "ARM-based"
|
|
1072
|
+
: "x86/Other"
|
|
1073
|
+
}
|
|
1074
|
+
</span>
|
|
1075
|
+
</span>
|
|
1076
|
+
</div>
|
|
1077
|
+
<div class="env-detail-row">
|
|
1078
|
+
<span class="env-detail-label">Memory per Core</span>
|
|
1079
|
+
<span class="env-detail-value">${
|
|
1080
|
+
environment.cpu.cores > 0
|
|
1081
|
+
? (
|
|
1082
|
+
parseFloat(environment.memory) / environment.cpu.cores
|
|
1083
|
+
).toFixed(2) + " GB"
|
|
1084
|
+
: "N/A"
|
|
1085
|
+
}</span>
|
|
1086
|
+
</div>
|
|
1087
|
+
<div class="env-detail-row">
|
|
1088
|
+
<span class="env-detail-label">Run Context</span>
|
|
1089
|
+
<span class="env-detail-value">CI/Local Test</span>
|
|
1090
|
+
</div>
|
|
1091
|
+
</div>
|
|
1092
|
+
</div>
|
|
1093
|
+
</div>
|
|
1094
|
+
`;
|
|
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
|
+
*/
|
|
652
1386
|
function generateTestHistoryContent(trendData) {
|
|
653
1387
|
if (
|
|
654
1388
|
!trendData ||
|
|
@@ -734,7 +1468,7 @@ function generateTestHistoryContent(trendData) {
|
|
|
734
1468
|
</span>
|
|
735
1469
|
</div>
|
|
736
1470
|
<div class="test-history-trend">
|
|
737
|
-
${generateTestHistoryChart(test.history)}
|
|
1471
|
+
${generateTestHistoryChart(test.history)}
|
|
738
1472
|
</div>
|
|
739
1473
|
<details class="test-history-details-collapsible">
|
|
740
1474
|
<summary>Show Run Details (${test.history.length})</summary>
|
|
@@ -768,7 +1502,11 @@ function generateTestHistoryContent(trendData) {
|
|
|
768
1502
|
</div>
|
|
769
1503
|
`;
|
|
770
1504
|
}
|
|
771
|
-
|
|
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
|
+
*/
|
|
772
1510
|
function getStatusClass(status) {
|
|
773
1511
|
switch (String(status).toLowerCase()) {
|
|
774
1512
|
case "passed":
|
|
@@ -781,7 +1519,11 @@ function getStatusClass(status) {
|
|
|
781
1519
|
return "status-unknown";
|
|
782
1520
|
}
|
|
783
1521
|
}
|
|
784
|
-
|
|
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
|
+
*/
|
|
785
1527
|
function getStatusIcon(status) {
|
|
786
1528
|
switch (String(status).toLowerCase()) {
|
|
787
1529
|
case "passed":
|
|
@@ -794,7 +1536,11 @@ function getStatusIcon(status) {
|
|
|
794
1536
|
return "❓";
|
|
795
1537
|
}
|
|
796
1538
|
}
|
|
797
|
-
|
|
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
|
+
*/
|
|
798
1544
|
function getSuitesData(results) {
|
|
799
1545
|
const suitesMap = new Map();
|
|
800
1546
|
if (!results || results.length === 0) return [];
|
|
@@ -837,19 +1583,45 @@ function getSuitesData(results) {
|
|
|
837
1583
|
if (currentStatus && suite[currentStatus] !== undefined) {
|
|
838
1584
|
suite[currentStatus]++;
|
|
839
1585
|
}
|
|
840
|
-
|
|
841
|
-
if (currentStatus === "failed")
|
|
842
|
-
suite.statusOverall = "failed";
|
|
843
|
-
} else if (
|
|
844
|
-
currentStatus === "skipped" &&
|
|
845
|
-
suite.statusOverall !== "failed"
|
|
846
|
-
) {
|
|
1586
|
+
if (currentStatus === "failed") suite.statusOverall = "failed";
|
|
1587
|
+
else if (currentStatus === "skipped" && suite.statusOverall !== "failed")
|
|
847
1588
|
suite.statusOverall = "skipped";
|
|
848
|
-
}
|
|
849
1589
|
});
|
|
850
1590
|
return Array.from(suitesMap.values());
|
|
851
1591
|
}
|
|
852
|
-
|
|
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
|
+
*/
|
|
853
1625
|
function generateSuitesWidget(suitesData) {
|
|
854
1626
|
if (!suitesData || suitesData.length === 0) {
|
|
855
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>`;
|
|
@@ -858,12 +1630,12 @@ function generateSuitesWidget(suitesData) {
|
|
|
858
1630
|
<div class="suites-widget">
|
|
859
1631
|
<div class="suites-header">
|
|
860
1632
|
<h2>Test Suites</h2>
|
|
861
|
-
<span class="summary-badge"
|
|
862
|
-
|
|
1633
|
+
<span class="summary-badge">${
|
|
1634
|
+
suitesData.length
|
|
1635
|
+
} suites • ${suitesData.reduce(
|
|
863
1636
|
(sum, suite) => sum + suite.count,
|
|
864
1637
|
0
|
|
865
|
-
)} tests
|
|
866
|
-
</span>
|
|
1638
|
+
)} tests</span>
|
|
867
1639
|
</div>
|
|
868
1640
|
<div class="suites-grid">
|
|
869
1641
|
${suitesData
|
|
@@ -874,8 +1646,10 @@ function generateSuitesWidget(suitesData) {
|
|
|
874
1646
|
<h3 class="suite-name" title="${sanitizeHTML(
|
|
875
1647
|
suite.name
|
|
876
1648
|
)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
|
|
877
|
-
<span class="browser-tag">${sanitizeHTML(suite.browser)}</span>
|
|
878
1649
|
</div>
|
|
1650
|
+
<div>🖥️ <span class="browser-tag">${sanitizeHTML(
|
|
1651
|
+
suite.browser
|
|
1652
|
+
)}</span></div>
|
|
879
1653
|
<div class="suite-card-body">
|
|
880
1654
|
<span class="test-count">${suite.count} test${
|
|
881
1655
|
suite.count !== 1 ? "s" : ""
|
|
@@ -904,7 +1678,109 @@ function generateSuitesWidget(suitesData) {
|
|
|
904
1678
|
</div>
|
|
905
1679
|
</div>`;
|
|
906
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
|
+
}
|
|
907
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
|
+
*/
|
|
908
1784
|
function generateHTML(reportData, trendData = null) {
|
|
909
1785
|
const { run, results } = reportData;
|
|
910
1786
|
const suitesData = getSuitesData(reportData.results || []);
|
|
@@ -916,8 +1792,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
916
1792
|
duration: 0,
|
|
917
1793
|
timestamp: new Date().toISOString(),
|
|
918
1794
|
};
|
|
919
|
-
|
|
920
|
-
const totalTestsOr1 = runSummary.totalTests || 1; // Avoid division by zero
|
|
1795
|
+
const totalTestsOr1 = runSummary.totalTests || 1;
|
|
921
1796
|
const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
|
|
922
1797
|
const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
|
|
923
1798
|
const skipPercentage = Math.round(
|
|
@@ -928,18 +1803,20 @@ function generateHTML(reportData, trendData = null) {
|
|
|
928
1803
|
? formatDuration(runSummary.duration / runSummary.totalTests)
|
|
929
1804
|
: "0.0s";
|
|
930
1805
|
|
|
931
|
-
|
|
932
|
-
|
|
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) {
|
|
1811
|
+
if (!results || results.length === 0)
|
|
933
1812
|
return '<div class="no-tests">No test results found in this run.</div>';
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
.map((test, index) => {
|
|
1813
|
+
return subset
|
|
1814
|
+
.map((test, i) => {
|
|
1815
|
+
const testIndex = baseIndex + i;
|
|
938
1816
|
const browser = test.browser || "unknown";
|
|
939
1817
|
const testFileParts = test.name.split(" > ");
|
|
940
1818
|
const testTitle =
|
|
941
1819
|
testFileParts[testFileParts.length - 1] || "Unnamed Test";
|
|
942
|
-
|
|
943
1820
|
const generateStepsHTML = (steps, depth = 0) => {
|
|
944
1821
|
if (!steps || steps.length === 0)
|
|
945
1822
|
return "<div class='no-steps'>No steps recorded for this test.</div>";
|
|
@@ -951,902 +1828,683 @@ function generateHTML(reportData, trendData = null) {
|
|
|
951
1828
|
? `step-hook step-hook-${step.hookType}`
|
|
952
1829
|
: "";
|
|
953
1830
|
const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
<div class="step-header ${stepClass}" role="button" aria-expanded="false">
|
|
958
|
-
<span class="step-icon">${getStatusIcon(step.status)}</span>
|
|
959
|
-
<span class="step-title">${sanitizeHTML(
|
|
1831
|
+
return `<div class="step-item" style="--depth: ${depth};"><div class="step-header ${stepClass}" role="button" aria-expanded="false"><span class="step-icon">${getStatusIcon(
|
|
1832
|
+
step.status
|
|
1833
|
+
)}</span><span class="step-title">${sanitizeHTML(
|
|
960
1834
|
step.title
|
|
961
|
-
)}${hookIndicator}</span
|
|
962
|
-
<span class="step-duration">${formatDuration(
|
|
1835
|
+
)}${hookIndicator}</span><span class="step-duration">${formatDuration(
|
|
963
1836
|
step.duration
|
|
964
|
-
)}</span
|
|
965
|
-
</div>
|
|
966
|
-
<div class="step-details" style="display: none;">
|
|
967
|
-
${
|
|
1837
|
+
)}</span></div><div class="step-details" style="display: none;">${
|
|
968
1838
|
step.codeLocation
|
|
969
|
-
? `<div class="step-info"><strong>Location:</strong> ${sanitizeHTML(
|
|
1839
|
+
? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
|
|
970
1840
|
step.codeLocation
|
|
971
1841
|
)}</div>`
|
|
972
1842
|
: ""
|
|
973
|
-
}
|
|
974
|
-
${
|
|
1843
|
+
}${
|
|
975
1844
|
step.errorMessage
|
|
976
|
-
?
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
: ""
|
|
984
|
-
}
|
|
985
|
-
</div>`
|
|
1845
|
+
? `<div class="test-error-summary">${
|
|
1846
|
+
step.stackTrace
|
|
1847
|
+
? `<div class="stack-trace">${formatPlaywrightError(
|
|
1848
|
+
step.stackTrace
|
|
1849
|
+
)}</div>`
|
|
1850
|
+
: ""
|
|
1851
|
+
}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
|
|
986
1852
|
: ""
|
|
987
|
-
}
|
|
988
|
-
|
|
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
|
+
})()
|
|
1889
|
+
}${
|
|
989
1890
|
hasNestedSteps
|
|
990
1891
|
? `<div class="nested-steps">${generateStepsHTML(
|
|
991
1892
|
step.steps,
|
|
992
1893
|
depth + 1
|
|
993
1894
|
)}</div>`
|
|
994
1895
|
: ""
|
|
995
|
-
}
|
|
996
|
-
</div>
|
|
997
|
-
</div>`;
|
|
1896
|
+
}</div></div>`;
|
|
998
1897
|
})
|
|
999
1898
|
.join("");
|
|
1000
1899
|
};
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1900
|
+
return `<div class="test-case" data-status="${
|
|
1901
|
+
test.status
|
|
1902
|
+
}" data-browser="${sanitizeHTML(browser)}" data-tags="${(
|
|
1903
|
+
test.tags || []
|
|
1904
|
+
)
|
|
1006
1905
|
.join(",")
|
|
1007
|
-
.toLowerCase()}">
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1906
|
+
.toLowerCase()}" data-test-id="${sanitizeHTML(String(test.id || testIndex))}">
|
|
1907
|
+
<div class="test-case-header" role="button" aria-expanded="false"><div class="test-case-summary"><span class="status-badge ${getStatusClass(
|
|
1908
|
+
test.status
|
|
1909
|
+
)}">${String(
|
|
1011
1910
|
test.status
|
|
1012
|
-
).toUpperCase()}</span
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
</
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
"'": "'",
|
|
1091
|
-
};
|
|
1092
|
-
return replacements[match] || match;
|
|
1093
|
-
}
|
|
1094
|
-
);
|
|
1095
|
-
};
|
|
1096
|
-
|
|
1097
|
-
const renderScreenshot = (
|
|
1098
|
-
screenshotPathOrData,
|
|
1099
|
-
index
|
|
1100
|
-
) => {
|
|
1101
|
-
let base64ImageData = "";
|
|
1102
|
-
const uniqueSuffix = `${Date.now()}-${index}-${Math.random()
|
|
1103
|
-
.toString(36)
|
|
1104
|
-
.substring(2, 7)}`;
|
|
1105
|
-
|
|
1911
|
+
).toUpperCase()}</span><span class="test-case-title" title="${sanitizeHTML(
|
|
1912
|
+
test.name
|
|
1913
|
+
)}">${sanitizeHTML(
|
|
1914
|
+
testTitle
|
|
1915
|
+
)}</span><span class="test-case-browser">(${sanitizeHTML(
|
|
1916
|
+
browser
|
|
1917
|
+
)})</span></div><div class="test-case-meta">${
|
|
1918
|
+
test.tags && test.tags.length > 0
|
|
1919
|
+
? test.tags
|
|
1920
|
+
.map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
|
|
1921
|
+
.join(" ")
|
|
1922
|
+
: ""
|
|
1923
|
+
}<span class="test-duration">${formatDuration(
|
|
1924
|
+
test.duration
|
|
1925
|
+
)}</span></div></div>
|
|
1926
|
+
<div class="test-case-content" style="display: none;">
|
|
1927
|
+
<p><strong>Full Path:</strong> ${sanitizeHTML(
|
|
1928
|
+
test.name
|
|
1929
|
+
)}</p>
|
|
1930
|
+
<p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
|
|
1931
|
+
test.workerId
|
|
1932
|
+
)} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
|
|
1933
|
+
test.totalWorkers
|
|
1934
|
+
)}]</p>
|
|
1935
|
+
${
|
|
1936
|
+
test.errorMessage
|
|
1937
|
+
? `<div class="test-error-summary">${formatPlaywrightError(
|
|
1938
|
+
test.errorMessage
|
|
1939
|
+
)}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
|
|
1940
|
+
: ""
|
|
1941
|
+
}
|
|
1942
|
+
${
|
|
1943
|
+
test.snippet
|
|
1944
|
+
? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
|
|
1945
|
+
test.snippet
|
|
1946
|
+
)}</code></pre></div>`
|
|
1947
|
+
: ""
|
|
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
|
+
})()}
|
|
1970
|
+
${
|
|
1971
|
+
test.stderr && test.stderr.length > 0
|
|
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
|
+
})()
|
|
1978
|
+
: ""
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
${(() => {
|
|
1982
|
+
if (
|
|
1983
|
+
!test.screenshots ||
|
|
1984
|
+
test.screenshots.length === 0
|
|
1985
|
+
)
|
|
1986
|
+
return "";
|
|
1987
|
+
return `<div class="attachments-section"><h4>Screenshots</h4><div class="attachments-grid">${test.screenshots
|
|
1988
|
+
.map((screenshotPath, index) => {
|
|
1106
1989
|
try {
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
)
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
// Assume it's raw Base64 data if it's a string but not a known path or full data URI
|
|
1147
|
-
base64ImageData = screenshotPathOrData;
|
|
1148
|
-
} else {
|
|
1149
|
-
console.warn(
|
|
1150
|
-
chalk.yellow(
|
|
1151
|
-
`[Reporter] Invalid screenshot data type for item at index ${index}.`
|
|
1152
|
-
)
|
|
1990
|
+
const imagePath = path.resolve(
|
|
1991
|
+
DEFAULT_OUTPUT_DIR,
|
|
1992
|
+
screenshotPath
|
|
1993
|
+
);
|
|
1994
|
+
if (!fsExistsSync(imagePath))
|
|
1995
|
+
return `<div class="attachment-item error">Screenshot not found: ${sanitizeHTML(
|
|
1996
|
+
screenshotPath
|
|
1997
|
+
)}</div>`;
|
|
1998
|
+
const base64ImageData =
|
|
1999
|
+
readFileSync(imagePath).toString("base64");
|
|
2000
|
+
return `<div class="attachment-item"><img src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" data-src="data:image/png;base64,${base64ImageData}" alt="Screenshot ${
|
|
2001
|
+
index + 1
|
|
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>`;
|
|
2003
|
+
} catch (e) {
|
|
2004
|
+
return `<div class="attachment-item error">Failed to load screenshot: ${sanitizeHTML(
|
|
2005
|
+
screenshotPath
|
|
2006
|
+
)}</div>`;
|
|
2007
|
+
}
|
|
2008
|
+
})
|
|
2009
|
+
.join("")}</div></div>`;
|
|
2010
|
+
})()}
|
|
2011
|
+
|
|
2012
|
+
${(() => {
|
|
2013
|
+
if (!test.videoPath || test.videoPath.length === 0)
|
|
2014
|
+
return "";
|
|
2015
|
+
return `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
|
|
2016
|
+
.map((videoPath, index) => {
|
|
2017
|
+
try {
|
|
2018
|
+
const videoFilePath = path.resolve(
|
|
2019
|
+
DEFAULT_OUTPUT_DIR,
|
|
2020
|
+
videoPath
|
|
2021
|
+
);
|
|
2022
|
+
if (!fsExistsSync(videoFilePath))
|
|
2023
|
+
return `<div class="attachment-item error">Video not found: ${sanitizeHTML(
|
|
2024
|
+
videoPath
|
|
2025
|
+
)}</div>`;
|
|
2026
|
+
const videoBase64 =
|
|
2027
|
+
readFileSync(videoFilePath).toString(
|
|
2028
|
+
"base64"
|
|
1153
2029
|
);
|
|
1154
|
-
|
|
1155
|
-
|
|
2030
|
+
const fileExtension = path
|
|
2031
|
+
.extname(videoPath)
|
|
2032
|
+
.slice(1)
|
|
2033
|
+
.toLowerCase();
|
|
2034
|
+
const mimeType =
|
|
2035
|
+
{
|
|
2036
|
+
mp4: "video/mp4",
|
|
2037
|
+
webm: "video/webm",
|
|
2038
|
+
ogg: "video/ogg",
|
|
2039
|
+
mov: "video/quicktime",
|
|
2040
|
+
avi: "video/x-msvideo",
|
|
2041
|
+
}[fileExtension] || "video/mp4";
|
|
2042
|
+
const videoDataUri = `data:${mimeType};base64,${videoBase64}`;
|
|
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>`;
|
|
2044
|
+
} catch (e) {
|
|
2045
|
+
return `<div class="attachment-item error">Failed to load video: ${sanitizeHTML(
|
|
2046
|
+
videoPath
|
|
2047
|
+
)}</div>`;
|
|
2048
|
+
}
|
|
2049
|
+
})
|
|
2050
|
+
.join("")}</div></div>`;
|
|
2051
|
+
})()}
|
|
2052
|
+
|
|
2053
|
+
${(() => {
|
|
2054
|
+
if (!test.tracePath) return "";
|
|
2055
|
+
try {
|
|
2056
|
+
const traceFilePath = path.resolve(
|
|
2057
|
+
DEFAULT_OUTPUT_DIR,
|
|
2058
|
+
test.tracePath
|
|
2059
|
+
);
|
|
2060
|
+
if (!fsExistsSync(traceFilePath))
|
|
2061
|
+
return `<div class="attachments-section"><h4>Trace File</h4><div class="attachment-item error">Trace file not found: ${sanitizeHTML(
|
|
2062
|
+
test.tracePath
|
|
2063
|
+
)}</div></div>`;
|
|
2064
|
+
const traceBase64 =
|
|
2065
|
+
readFileSync(traceFilePath).toString("base64");
|
|
2066
|
+
const traceDataUri = `data:application/zip;base64,${traceBase64}`;
|
|
2067
|
+
return `<div class="attachments-section"><h4>Trace File</h4><div class="attachments-grid"><div class="attachment-item generic-attachment"><div class="attachment-icon">📄</div><div class="attachment-caption"><span class="attachment-name">trace.zip</span></div><div class="attachment-info"><div class="trace-actions"><a href="#" data-href="${traceDataUri}" class="lazy-load-attachment" download="trace.zip">Download Trace</a></div></div></div></div></div>`;
|
|
2068
|
+
} catch (e) {
|
|
2069
|
+
return `<div class="attachments-section"><h4>Trace File</h4><div class="attachment-item error">Failed to load trace file.</div></div>`;
|
|
2070
|
+
}
|
|
2071
|
+
})()}
|
|
2072
|
+
|
|
2073
|
+
${(() => {
|
|
2074
|
+
if (
|
|
2075
|
+
!test.attachments ||
|
|
2076
|
+
test.attachments.length === 0
|
|
2077
|
+
)
|
|
2078
|
+
return "";
|
|
2079
|
+
|
|
2080
|
+
return `<div class="attachments-section"><h4>Other Attachments</h4><div class="attachments-grid">${test.attachments
|
|
2081
|
+
.map((attachment) => {
|
|
2082
|
+
try {
|
|
2083
|
+
const attachmentPath = path.resolve(
|
|
2084
|
+
DEFAULT_OUTPUT_DIR,
|
|
2085
|
+
attachment.path
|
|
2086
|
+
);
|
|
1156
2087
|
|
|
1157
|
-
if (!
|
|
1158
|
-
// This case should ideally be caught above, but as a fallback:
|
|
2088
|
+
if (!fsExistsSync(attachmentPath)) {
|
|
1159
2089
|
console.warn(
|
|
1160
|
-
|
|
1161
|
-
`[Reporter] Could not obtain base64 data for screenshot: ${escapeHTML(
|
|
1162
|
-
String(screenshotPathOrData)
|
|
1163
|
-
)}`
|
|
1164
|
-
)
|
|
2090
|
+
`Attachment not found at: ${attachmentPath}`
|
|
1165
2091
|
);
|
|
1166
|
-
return `<div class="attachment-item error"
|
|
1167
|
-
|
|
2092
|
+
return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
|
|
2093
|
+
attachment.name
|
|
1168
2094
|
)}</div>`;
|
|
1169
2095
|
}
|
|
1170
2096
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
2097
|
+
const attachmentBase64 =
|
|
2098
|
+
readFileSync(attachmentPath).toString(
|
|
2099
|
+
"base64"
|
|
2100
|
+
);
|
|
2101
|
+
const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
|
|
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(
|
|
2111
|
+
attachment.name
|
|
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(
|
|
2121
|
+
attachment.name
|
|
2122
|
+
)}">Download</a>
|
|
2123
|
+
</div>
|
|
2124
|
+
</div>
|
|
2125
|
+
</div>`;
|
|
1194
2126
|
} catch (e) {
|
|
1195
2127
|
console.error(
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
String(screenshotPathOrData)
|
|
1199
|
-
)}: ${e.message}`
|
|
1200
|
-
)
|
|
2128
|
+
`Failed to process attachment "${attachment.name}":`,
|
|
2129
|
+
e
|
|
1201
2130
|
);
|
|
1202
|
-
return `<div class="attachment-item error"
|
|
1203
|
-
|
|
2131
|
+
return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(
|
|
2132
|
+
attachment.name
|
|
1204
2133
|
)}</div>`;
|
|
1205
2134
|
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
2135
|
+
})
|
|
2136
|
+
.join("")}</div></div>`;
|
|
2137
|
+
})()}
|
|
2138
|
+
|
|
2139
|
+
${
|
|
2140
|
+
test.codeSnippet
|
|
2141
|
+
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${sanitizeHTML(
|
|
2142
|
+
test.codeSnippet
|
|
2143
|
+
)}</code></pre></div>`
|
|
2144
|
+
: ""
|
|
2145
|
+
}
|
|
1214
2146
|
</div>
|
|
1215
|
-
|
|
1216
|
-
})()}
|
|
1217
|
-
|
|
1218
|
-
${
|
|
1219
|
-
test.videoPath
|
|
1220
|
-
? `
|
|
1221
|
-
<div class="attachments-section">
|
|
1222
|
-
<h4>Videos</h4>
|
|
1223
|
-
<div class="attachments-grid">
|
|
1224
|
-
${(() => {
|
|
1225
|
-
// Handle both string and array cases
|
|
1226
|
-
const videos = Array.isArray(test.videoPath)
|
|
1227
|
-
? test.videoPath
|
|
1228
|
-
: [test.videoPath];
|
|
1229
|
-
const mimeTypes = {
|
|
1230
|
-
mp4: "video/mp4",
|
|
1231
|
-
webm: "video/webm",
|
|
1232
|
-
ogg: "video/ogg",
|
|
1233
|
-
mov: "video/quicktime",
|
|
1234
|
-
avi: "video/x-msvideo",
|
|
1235
|
-
};
|
|
1236
|
-
|
|
1237
|
-
return videos
|
|
1238
|
-
.map((video, index) => {
|
|
1239
|
-
const videoUrl =
|
|
1240
|
-
typeof video === "object" ? video.url || "" : video;
|
|
1241
|
-
const videoName =
|
|
1242
|
-
typeof video === "object"
|
|
1243
|
-
? video.name || `Video ${index + 1}`
|
|
1244
|
-
: `Video ${index + 1}`;
|
|
1245
|
-
const fileExtension = videoUrl
|
|
1246
|
-
.split(".")
|
|
1247
|
-
.pop()
|
|
1248
|
-
.toLowerCase();
|
|
1249
|
-
const mimeType = mimeTypes[fileExtension] || "video/mp4";
|
|
1250
|
-
|
|
1251
|
-
return `
|
|
1252
|
-
<div class="attachment-item">
|
|
1253
|
-
<video controls width="100%" height="auto" title="${videoName}">
|
|
1254
|
-
<source src="${videoUrl}" type="${mimeType}">
|
|
1255
|
-
Your browser does not support the video tag.
|
|
1256
|
-
</video>
|
|
1257
|
-
<div class="attachment-info">
|
|
1258
|
-
<div class="trace-actions">
|
|
1259
|
-
<a href="${videoUrl}" target="_blank" download="${videoName}.${fileExtension}">
|
|
1260
|
-
Download
|
|
1261
|
-
</a>
|
|
1262
|
-
</div>
|
|
1263
|
-
</div>
|
|
1264
|
-
</div>
|
|
1265
|
-
`;
|
|
1266
|
-
})
|
|
1267
|
-
.join("");
|
|
1268
|
-
})()}
|
|
1269
|
-
</div>
|
|
1270
|
-
</div>
|
|
1271
|
-
`
|
|
1272
|
-
: ""
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
${
|
|
1276
|
-
test.tracePath
|
|
1277
|
-
? `
|
|
1278
|
-
<div class="attachments-section">
|
|
1279
|
-
<h4>Trace Files</h4>
|
|
1280
|
-
<div class="attachments-grid">
|
|
1281
|
-
${(() => {
|
|
1282
|
-
// Handle both string and array cases
|
|
1283
|
-
const traces = Array.isArray(test.tracePath)
|
|
1284
|
-
? test.tracePath
|
|
1285
|
-
: [test.tracePath];
|
|
1286
|
-
|
|
1287
|
-
return traces
|
|
1288
|
-
.map((trace, index) => {
|
|
1289
|
-
const traceUrl =
|
|
1290
|
-
typeof trace === "object" ? trace.url || "" : trace;
|
|
1291
|
-
const traceName =
|
|
1292
|
-
typeof trace === "object"
|
|
1293
|
-
? trace.name || `Trace ${index + 1}`
|
|
1294
|
-
: `Trace ${index + 1}`;
|
|
1295
|
-
const traceFileName = traceUrl.split("/").pop();
|
|
1296
|
-
|
|
1297
|
-
return `
|
|
1298
|
-
<div class="attachment-item">
|
|
1299
|
-
<div class="trace-preview">
|
|
1300
|
-
<span class="trace-icon">📄</span>
|
|
1301
|
-
<span class="trace-name">${traceName}</span>
|
|
1302
|
-
</div>
|
|
1303
|
-
<div class="attachment-info">
|
|
1304
|
-
<div class="trace-actions">
|
|
1305
|
-
<a href="${traceUrl}" target="_blank" download="${traceFileName}" class="download-trace">
|
|
1306
|
-
Download
|
|
1307
|
-
</a>
|
|
1308
|
-
</div>
|
|
1309
|
-
</div>
|
|
1310
|
-
</div>
|
|
1311
|
-
`;
|
|
1312
|
-
})
|
|
1313
|
-
.join("");
|
|
1314
|
-
})()}
|
|
1315
|
-
</div>
|
|
1316
|
-
</div>
|
|
1317
|
-
`
|
|
1318
|
-
: ""
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
${
|
|
1322
|
-
test.codeSnippet
|
|
1323
|
-
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${sanitizeHTML(
|
|
1324
|
-
test.codeSnippet
|
|
1325
|
-
)}</code></pre></div>`
|
|
1326
|
-
: ""
|
|
1327
|
-
}
|
|
1328
|
-
</div>
|
|
1329
|
-
</div>`;
|
|
2147
|
+
</div>`;
|
|
1330
2148
|
})
|
|
1331
2149
|
.join("");
|
|
1332
2150
|
}
|
|
1333
|
-
|
|
1334
2151
|
return `
|
|
1335
2152
|
<!DOCTYPE html>
|
|
1336
2153
|
<html lang="en">
|
|
1337
2154
|
<head>
|
|
1338
2155
|
<meta charset="UTF-8">
|
|
1339
2156
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1340
|
-
<link rel="icon" type="image/png" href="https://i.postimg.cc/
|
|
1341
|
-
<link rel="apple-touch-icon" href="https://i.postimg.cc/
|
|
1342
|
-
<script src="https://code.highcharts.com/highcharts.js"></script>
|
|
1343
|
-
<title>Playwright Pulse Report</title>
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
--info-color: #2196F3; /* Blue */
|
|
1354
|
-
--light-gray-color: #f5f5f5;
|
|
1355
|
-
--medium-gray-color: #e0e0e0;
|
|
1356
|
-
--dark-gray-color: #757575;
|
|
1357
|
-
--text-color: #333;
|
|
1358
|
-
--text-color-secondary: #555;
|
|
1359
|
-
--border-color: #ddd;
|
|
1360
|
-
--background-color: #f8f9fa;
|
|
1361
|
-
--card-background-color: #fff;
|
|
1362
|
-
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
1363
|
-
--border-radius: 8px;
|
|
1364
|
-
--box-shadow: 0 5px 15px rgba(0,0,0,0.08);
|
|
1365
|
-
--box-shadow-light: 0 3px 8px rgba(0,0,0,0.05);
|
|
1366
|
-
--box-shadow-inset: inset 0 1px 3px rgba(0,0,0,0.07);
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
/* General Highcharts styling */
|
|
1370
|
-
.highcharts-background { fill: transparent; }
|
|
1371
|
-
.highcharts-title, .highcharts-subtitle { font-family: var(--font-family); }
|
|
1372
|
-
.highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
|
|
1373
|
-
.highcharts-axis-title { fill: var(--text-color) !important; }
|
|
1374
|
-
.highcharts-tooltip > span { background-color: rgba(10,10,10,0.92) !important; border-color: rgba(10,10,10,0.92) !important; color: #f5f5f5 !important; padding: 10px !important; border-radius: 6px !important; }
|
|
1375
|
-
|
|
1376
|
-
body {
|
|
1377
|
-
font-family: var(--font-family);
|
|
1378
|
-
margin: 0;
|
|
1379
|
-
background-color: var(--background-color);
|
|
1380
|
-
color: var(--text-color);
|
|
1381
|
-
line-height: 1.65;
|
|
1382
|
-
font-size: 16px;
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
.container {
|
|
1386
|
-
max-width: 1600px;
|
|
1387
|
-
padding: 30px;
|
|
1388
|
-
border-radius: var(--border-radius);
|
|
1389
|
-
box-shadow: var(--box-shadow);
|
|
1390
|
-
background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec);
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
.header {
|
|
1394
|
-
display: flex;
|
|
1395
|
-
justify-content: space-between;
|
|
1396
|
-
align-items: center;
|
|
1397
|
-
flex-wrap: wrap;
|
|
1398
|
-
padding-bottom: 25px;
|
|
1399
|
-
border-bottom: 1px solid var(--border-color);
|
|
1400
|
-
margin-bottom: 25px;
|
|
1401
|
-
}
|
|
1402
|
-
.header-title { display: flex; align-items: center; gap: 15px; }
|
|
1403
|
-
.header h1 { margin: 0; font-size: 1.85em; font-weight: 600; color: var(--primary-color); }
|
|
1404
|
-
#report-logo { height: 40px; width: 40px; border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.1);}
|
|
1405
|
-
.run-info { font-size: 0.9em; text-align: right; color: var(--text-color-secondary); line-height:1.5;}
|
|
1406
|
-
.run-info strong { color: var(--text-color); }
|
|
1407
|
-
|
|
1408
|
-
.tabs { display: flex; border-bottom: 2px solid var(--border-color); margin-bottom: 30px; overflow-x: auto; }
|
|
1409
|
-
.tab-button {
|
|
1410
|
-
padding: 15px 25px; background: none; border: none; border-bottom: 3px solid transparent;
|
|
1411
|
-
cursor: pointer; font-size: 1.1em; font-weight: 600; color: black;
|
|
1412
|
-
transition: color 0.2s ease, border-color 0.2s ease; white-space: nowrap;
|
|
1413
|
-
}
|
|
1414
|
-
.tab-button:hover { color: var(--accent-color); }
|
|
1415
|
-
.tab-button.active { color: var(--primary-color); border-bottom-color: var(--primary-color); }
|
|
1416
|
-
.tab-content { display: none; animation: fadeIn 0.4s ease-out; }
|
|
1417
|
-
.tab-content.active { display: block; }
|
|
1418
|
-
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
1419
|
-
|
|
1420
|
-
.dashboard-grid {
|
|
1421
|
-
display: grid;
|
|
1422
|
-
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
|
1423
|
-
gap: 22px; margin-bottom: 35px;
|
|
1424
|
-
}
|
|
1425
|
-
.summary-card {
|
|
1426
|
-
background-color: var(--card-background-color); border: 1px solid var(--border-color);
|
|
1427
|
-
border-radius: var(--border-radius); padding: 22px; text-align: center;
|
|
1428
|
-
box-shadow: var(--box-shadow-light); transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
1429
|
-
}
|
|
1430
|
-
.summary-card:hover { transform: translateY(-5px); box-shadow: var(--box-shadow); }
|
|
1431
|
-
.summary-card h3 { margin: 0 0 10px; font-size: 1.05em; font-weight: 500; color: var(--text-color-secondary); }
|
|
1432
|
-
.summary-card .value { font-size: 2.4em; font-weight: 600; margin-bottom: 8px; }
|
|
1433
|
-
.summary-card .trend-percentage { font-size: 1em; color: var(--dark-gray-color); }
|
|
1434
|
-
.status-passed .value, .stat-passed svg { color: var(--success-color); }
|
|
1435
|
-
.status-failed .value, .stat-failed svg { color: var(--danger-color); }
|
|
1436
|
-
.status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
|
|
1437
|
-
|
|
1438
|
-
.dashboard-bottom-row {
|
|
1439
|
-
display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
1440
|
-
gap: 28px; align-items: stretch;
|
|
1441
|
-
}
|
|
1442
|
-
.pie-chart-wrapper, .suites-widget, .trend-chart {
|
|
1443
|
-
background-color: var(--card-background-color); padding: 28px;
|
|
1444
|
-
border-radius: var(--border-radius); box-shadow: var(--box-shadow-light);
|
|
1445
|
-
display: flex; flex-direction: column;
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
.pie-chart-wrapper h3, .suites-header h2, .trend-chart h3 {
|
|
1449
|
-
text-align: center; margin-top: 0; margin-bottom: 25px;
|
|
1450
|
-
font-size: 1.25em; font-weight: 600; color: var(--text-color);
|
|
1451
|
-
}
|
|
1452
|
-
.trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { /* For Highcharts containers */
|
|
1453
|
-
flex-grow: 1;
|
|
1454
|
-
min-height: 250px; /* Ensure charts have some min height */
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
.chart-tooltip { /* This class was for D3, Highcharts has its own tooltip styling via JS/SVG */
|
|
1458
|
-
/* Basic styling for Highcharts HTML tooltips can be done via .highcharts-tooltip span */
|
|
1459
|
-
}
|
|
1460
|
-
.status-badge-small-tooltip { padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; }
|
|
1461
|
-
.status-badge-small-tooltip.status-passed { background-color: var(--success-color); }
|
|
1462
|
-
.status-badge-small-tooltip.status-failed { background-color: var(--danger-color); }
|
|
1463
|
-
.status-badge-small-tooltip.status-skipped { background-color: var(--warning-color); }
|
|
1464
|
-
.status-badge-small-tooltip.status-unknown { background-color: var(--dark-gray-color); }
|
|
1465
|
-
|
|
1466
|
-
.suites-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
|
1467
|
-
.summary-badge { background-color: var(--light-gray-color); color: var(--text-color-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
|
|
1468
|
-
.suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
|
|
1469
|
-
.suite-card {
|
|
1470
|
-
border: 1px solid var(--border-color); border-left-width: 5px;
|
|
1471
|
-
border-radius: calc(var(--border-radius) / 1.5); padding: 20px;
|
|
1472
|
-
background-color: var(--card-background-color); transition: box-shadow 0.2s ease, border-left-color 0.2s ease;
|
|
1473
|
-
}
|
|
1474
|
-
.suite-card:hover { box-shadow: var(--box-shadow); }
|
|
1475
|
-
.suite-card.status-passed { border-left-color: var(--success-color); }
|
|
1476
|
-
.suite-card.status-failed { border-left-color: var(--danger-color); }
|
|
1477
|
-
.suite-card.status-skipped { border-left-color: var(--warning-color); }
|
|
1478
|
-
.suite-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
|
|
1479
|
-
.suite-name { font-weight: 600; font-size: 1.05em; color: var(--text-color); margin-right: 10px; word-break: break-word;}
|
|
1480
|
-
.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;}
|
|
1481
|
-
.suite-card-body .test-count { font-size: 0.95em; color: var(--text-color-secondary); display: block; margin-bottom: 10px; }
|
|
1482
|
-
.suite-stats { display: flex; gap: 14px; font-size: 0.95em; align-items: center; }
|
|
1483
|
-
.suite-stats span { display: flex; align-items: center; gap: 6px; }
|
|
1484
|
-
.suite-stats svg { vertical-align: middle; font-size: 1.15em; }
|
|
1485
|
-
|
|
1486
|
-
.filters {
|
|
1487
|
-
display: flex; flex-wrap: wrap; gap: 18px; margin-bottom: 28px;
|
|
1488
|
-
padding: 20px; background-color: var(--light-gray-color); border-radius: var(--border-radius);
|
|
1489
|
-
box-shadow: var(--box-shadow-inset); border-color: black; border-style: groove;
|
|
1490
|
-
}
|
|
1491
|
-
.filters input, .filters select, .filters button {
|
|
1492
|
-
padding: 11px 15px; border: 1px solid var(--border-color);
|
|
1493
|
-
border-radius: 6px; font-size: 1em;
|
|
1494
|
-
}
|
|
1495
|
-
.filters input { flex-grow: 1; min-width: 240px;}
|
|
1496
|
-
.filters select {min-width: 180px;}
|
|
1497
|
-
.filters button { background-color: var(--primary-color); color: white; cursor: pointer; transition: background-color 0.2s ease, box-shadow 0.2s ease; border: none; }
|
|
1498
|
-
.filters button:hover { background-color: var(--accent-color); box-shadow: 0 2px 5px rgba(0,0,0,0.15);}
|
|
1499
|
-
|
|
1500
|
-
.test-case {
|
|
1501
|
-
margin-bottom: 15px; border: 1px solid var(--border-color);
|
|
1502
|
-
border-radius: var(--border-radius); background-color: var(--card-background-color);
|
|
1503
|
-
box-shadow: var(--box-shadow-light); overflow: hidden;
|
|
1504
|
-
}
|
|
1505
|
-
.test-case-header {
|
|
1506
|
-
padding: 10px 15px; background-color: #fff; cursor: pointer;
|
|
1507
|
-
display: flex; justify-content: space-between; align-items: center;
|
|
1508
|
-
border-bottom: 1px solid transparent;
|
|
1509
|
-
transition: background-color 0.2s ease;
|
|
1510
|
-
}
|
|
1511
|
-
.test-case-header:hover { background-color: #f4f6f8; }
|
|
1512
|
-
.test-case-header[aria-expanded="true"] { border-bottom-color: var(--border-color); background-color: #f9fafb; }
|
|
1513
|
-
|
|
1514
|
-
.test-case-summary { display: flex; align-items: center; gap: 14px; flex-grow: 1; flex-wrap: wrap;}
|
|
1515
|
-
.test-case-title { font-weight: 600; color: var(--text-color); font-size: 1em; }
|
|
1516
|
-
.test-case-browser { font-size: 0.9em; color: var(--text-color-secondary); }
|
|
1517
|
-
.test-case-meta { display: flex; align-items: center; gap: 12px; font-size: 0.9em; color: var(--text-color-secondary); flex-shrink: 0; }
|
|
1518
|
-
.test-duration { background-color: var(--light-gray-color); padding: 4px 10px; border-radius: 12px; font-size: 0.9em;}
|
|
1519
|
-
|
|
1520
|
-
.status-badge {
|
|
1521
|
-
padding: 5px; border-radius: 6px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase;
|
|
1522
|
-
min-width: 70px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
1523
|
-
}
|
|
1524
|
-
.status-badge.status-passed { background-color: var(--success-color); }
|
|
1525
|
-
.status-badge.status-failed { background-color: var(--danger-color); }
|
|
1526
|
-
.status-badge.status-skipped { background-color: var(--warning-color); }
|
|
1527
|
-
.status-badge.status-unknown { background-color: var(--dark-gray-color); }
|
|
1528
|
-
|
|
1529
|
-
.tag { display: inline-block; background: linear-gradient( #fff, #333, #000); color: #fff; padding: 3px 10px; border-radius: 12px; font-size: 0.85em; margin-right: 6px; font-weight: 400; }
|
|
1530
|
-
|
|
1531
|
-
.test-case-content { display: none; padding: 20px; border-top: 1px solid var(--border-color); background-color: #fcfdff; }
|
|
1532
|
-
.test-case-content h4 { margin-top: 22px; margin-bottom: 14px; font-size: 1.15em; color: var(--primary-color); }
|
|
1533
|
-
.test-case-content p { margin-bottom: 10px; font-size: 1em; }
|
|
1534
|
-
.test-error-summary { margin-bottom: 20px; padding: 14px; background-color: rgba(244,67,54,0.05); border: 1px solid rgba(244,67,54,0.2); border-left: 4px solid var(--danger-color); border-radius: 4px; }
|
|
1535
|
-
.test-error-summary h4 { color: var(--danger-color); margin-top:0;}
|
|
1536
|
-
.test-error-summary pre { white-space: pre-wrap; word-break: break-all; color: var(--danger-color); font-size: 0.95em;}
|
|
1537
|
-
|
|
1538
|
-
.steps-list { margin: 18px 0; }
|
|
1539
|
-
.step-item { margin-bottom: 8px; padding-left: calc(var(--depth, 0) * 28px); }
|
|
1540
|
-
.step-header {
|
|
1541
|
-
display: flex; align-items: center; cursor: pointer;
|
|
1542
|
-
padding: 10px 14px; border-radius: 6px; background-color: #fff;
|
|
1543
|
-
border: 1px solid var(--light-gray-color);
|
|
1544
|
-
transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
|
1545
|
-
}
|
|
1546
|
-
.step-header:hover { background-color: #f0f2f5; border-color: var(--medium-gray-color); box-shadow: var(--box-shadow-inset); }
|
|
1547
|
-
.step-icon { margin-right: 12px; width: 20px; text-align: center; font-size: 1.1em; }
|
|
1548
|
-
.step-title { flex: 1; font-size: 1em; }
|
|
1549
|
-
.step-duration { color: var(--dark-gray-color); font-size: 0.9em; }
|
|
1550
|
-
.step-details { display: none; padding: 14px; margin-top: 8px; background: #fdfdfd; border-radius: 6px; font-size: 0.95em; border: 1px solid var(--light-gray-color); }
|
|
1551
|
-
.step-info { margin-bottom: 8px; }
|
|
1552
|
-
.step-error { color: var(--danger-color); margin-top: 12px; padding: 14px; background: rgba(244,67,54,0.05); border-radius: 4px; font-size: 0.95em; border-left: 3px solid var(--danger-color); }
|
|
1553
|
-
.step-error pre.stack-trace { margin-top: 10px; padding: 12px; background-color: rgba(0,0,0,0.03); border-radius: 4px; font-size:0.9em; max-height: 280px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; }
|
|
1554
|
-
.step-hook { background-color: rgba(33,150,243,0.04); border-left: 3px solid var(--info-color) !important; }
|
|
1555
|
-
.step-hook .step-title { font-style: italic; color: var(--info-color)}
|
|
1556
|
-
.nested-steps { margin-top: 12px; }
|
|
1557
|
-
|
|
1558
|
-
.attachments-section { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--light-gray-color); }
|
|
1559
|
-
.attachments-section h4 { margin-top: 0; margin-bottom: 20px; font-size: 1.1em; color: var(--text-color); }
|
|
1560
|
-
.attachments-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 22px; }
|
|
1561
|
-
.attachment-item {
|
|
1562
|
-
border: 1px solid var(--border-color); border-radius: var(--border-radius); background-color: #fff;
|
|
1563
|
-
box-shadow: var(--box-shadow-light); overflow: hidden; display: flex; flex-direction: column;
|
|
1564
|
-
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
|
|
1565
|
-
}
|
|
1566
|
-
.attachment-item:hover { transform: translateY(-4px); box-shadow: var(--box-shadow); }
|
|
1567
|
-
.attachment-item img {
|
|
1568
|
-
width: 100%; height: 180px; object-fit: cover; display: block;
|
|
1569
|
-
border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease;
|
|
1570
|
-
}
|
|
1571
|
-
.attachment-item a:hover img { opacity: 0.85; }
|
|
1572
|
-
.attachment-caption {
|
|
1573
|
-
padding: 12px 15px; font-size: 0.9em; text-align: center;
|
|
1574
|
-
color: var(--text-color-secondary); word-break: break-word; background-color: var(--light-gray-color);
|
|
1575
|
-
}
|
|
1576
|
-
.video-item a, .trace-item a { display: block; margin-bottom: 8px; color: var(--primary-color); text-decoration: none; font-weight: 500; }
|
|
1577
|
-
.video-item a:hover, .trace-item a:hover { text-decoration: underline; }
|
|
1578
|
-
.code-section pre { background-color: #2d2d2d; color: #f0f0f0; padding: 20px; border-radius: 6px; overflow-x: auto; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 0.95em; line-height:1.6;}
|
|
1579
|
-
|
|
1580
|
-
.trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
|
|
1581
|
-
/* Removed D3 specific .chart-axis, .main-chart-title, .chart-line.* rules */
|
|
1582
|
-
/* Highcharts styles its elements with classes like .highcharts-axis, .highcharts-title etc. */
|
|
1583
|
-
|
|
1584
|
-
.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;}
|
|
1585
|
-
.test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
|
|
1586
|
-
.test-history-card {
|
|
1587
|
-
background: var(--card-background-color); border: 1px solid var(--border-color); border-radius: var(--border-radius);
|
|
1588
|
-
padding: 22px; box-shadow: var(--box-shadow-light); display: flex; flex-direction: column;
|
|
1589
|
-
}
|
|
1590
|
-
.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); }
|
|
1591
|
-
.test-history-header h3 { margin: 0; font-size: 1.15em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1592
|
-
.test-history-header p { font-weight: 500 }
|
|
1593
|
-
.test-history-trend { margin-bottom: 20px; min-height: 110px; }
|
|
1594
|
-
.test-history-trend div[id^="testHistoryChart-"] { /* Highcharts container for history */
|
|
1595
|
-
display: block; margin: 0 auto; max-width:100%; height: 100px; width: 320px; /* Match JS config */
|
|
1596
|
-
}
|
|
1597
|
-
/* .test-history-trend .small-axis text {font-size: 11px;} Removed D3 specific */
|
|
1598
|
-
.test-history-details-collapsible summary { cursor: pointer; font-size: 1em; color: var(--primary-color); margin-bottom: 10px; font-weight:500; }
|
|
1599
|
-
.test-history-details-collapsible summary:hover {text-decoration: underline;}
|
|
1600
|
-
.test-history-details table { width: 100%; border-collapse: collapse; font-size: 0.95em; }
|
|
1601
|
-
.test-history-details th, .test-history-details td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--light-gray-color); }
|
|
1602
|
-
.test-history-details th { background-color: var(--light-gray-color); font-weight: 600; }
|
|
1603
|
-
.status-badge-small {
|
|
1604
|
-
padding: 3px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 600;
|
|
1605
|
-
color: white; text-transform: uppercase; display: inline-block;
|
|
1606
|
-
}
|
|
1607
|
-
.status-badge-small.status-passed { background-color: var(--success-color); }
|
|
1608
|
-
.status-badge-small.status-failed { background-color: var(--danger-color); }
|
|
1609
|
-
.status-badge-small.status-skipped { background-color: var(--warning-color); }
|
|
1610
|
-
.status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
|
|
1611
|
-
|
|
1612
|
-
.no-data, .no-tests, .no-steps, .no-data-chart {
|
|
1613
|
-
padding: 28px; text-align: center; color: var(--dark-gray-color); font-style: italic; font-size:1.1em;
|
|
1614
|
-
background-color: var(--light-gray-color); border-radius: var(--border-radius); margin: 18px 0;
|
|
1615
|
-
border: 1px dashed var(--medium-gray-color);
|
|
1616
|
-
}
|
|
1617
|
-
.no-data-chart {font-size: 0.95em; padding: 18px;}
|
|
1618
|
-
|
|
1619
|
-
#test-ai iframe { border: 1px solid var(--border-color); width: 100%; height: 85vh; border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); }
|
|
1620
|
-
#test-ai p {margin-bottom: 18px; font-size: 1em; color: var(--text-color-secondary);}
|
|
1621
|
-
pre .stdout-log { background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2; }
|
|
1622
|
-
pre .stderr-log { background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2; }
|
|
1623
|
-
|
|
1624
|
-
.trace-preview {
|
|
1625
|
-
padding: 1rem;
|
|
1626
|
-
text-align: center;
|
|
1627
|
-
background: #f5f5f5;
|
|
1628
|
-
border-bottom: 1px solid #e1e1e1;
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
.trace-icon {
|
|
1632
|
-
font-size: 2rem;
|
|
1633
|
-
display: block;
|
|
1634
|
-
margin-bottom: 0.5rem;
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
.trace-name {
|
|
1638
|
-
word-break: break-word;
|
|
1639
|
-
font-size: 0.9rem;
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
.trace-actions {
|
|
1643
|
-
display: flex;
|
|
1644
|
-
gap: 0.5rem;
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
.trace-actions a {
|
|
1648
|
-
flex: 1;
|
|
1649
|
-
text-align: center;
|
|
1650
|
-
padding: 0.25rem 0.5rem;
|
|
1651
|
-
font-size: 0.85rem;
|
|
1652
|
-
border-radius: 4px;
|
|
1653
|
-
text-decoration: none;
|
|
1654
|
-
background: cornflowerblue;
|
|
1655
|
-
color: aliceblue;
|
|
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">
|
|
2159
|
+
<script src="https://code.highcharts.com/highcharts.js" defer></script>
|
|
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);
|
|
1656
2170
|
}
|
|
1657
|
-
|
|
1658
|
-
.
|
|
1659
|
-
|
|
1660
|
-
|
|
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;
|
|
1661
2263
|
}
|
|
1662
|
-
|
|
1663
|
-
.view-trace:hover {
|
|
1664
|
-
background: #2c5282;
|
|
1665
2264
|
}
|
|
1666
|
-
|
|
1667
|
-
.
|
|
1668
|
-
|
|
1669
|
-
|
|
2265
|
+
.test-case,
|
|
2266
|
+
.test-history-card,
|
|
2267
|
+
.suite-card,
|
|
2268
|
+
.attachments-section {
|
|
2269
|
+
contain: content;
|
|
1670
2270
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
2271
|
+
.attachments-grid .attachment-item img.lazy-load-image {
|
|
2272
|
+
width: 100%;
|
|
2273
|
+
aspect-ratio: 4 / 3;
|
|
2274
|
+
object-fit: cover;
|
|
1674
2275
|
}
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
background-color: var(--medium-gray-color); /* Or any other suitable color */
|
|
1678
|
-
color: var(--text-color);
|
|
1679
|
-
/* Add other styling as per your .filters button style if needed */
|
|
2276
|
+
.attachments-grid .attachment-item.video-item {
|
|
2277
|
+
aspect-ratio: 16 / 9;
|
|
1680
2278
|
}
|
|
1681
2279
|
|
|
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
|
-
|
|
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
|
-
<div class="run-info">
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
runSummary.duration
|
|
1744
|
-
)}
|
|
1745
|
-
</div>
|
|
2403
|
+
<div class="run-info"><strong>Run Date:</strong> ${formatDate(
|
|
2404
|
+
runSummary.timestamp
|
|
2405
|
+
)}<br><strong>Total Duration:</strong> ${formatDuration(
|
|
2406
|
+
runSummary.duration
|
|
2407
|
+
)}</div>
|
|
1746
2408
|
</header>
|
|
1747
|
-
|
|
1748
2409
|
<div class="tabs">
|
|
1749
2410
|
<button class="tab-button active" data-tab="dashboard">Dashboard</button>
|
|
1750
2411
|
<button class="tab-button" data-tab="test-runs">Test Run Summary</button>
|
|
1751
2412
|
<button class="tab-button" data-tab="test-history">Test History</button>
|
|
1752
|
-
<button class="tab-button" data-tab="
|
|
2413
|
+
<button class="tab-button" data-tab="ai-failure-analyzer">AI Failure Analyzer</button>
|
|
1753
2414
|
</div>
|
|
1754
|
-
|
|
1755
2415
|
<div id="dashboard" class="tab-content active">
|
|
1756
2416
|
<div class="dashboard-grid">
|
|
1757
|
-
<div class="summary-card">
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
</div>
|
|
1766
|
-
<div class="summary-card status-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
</div>
|
|
1770
|
-
<div class="summary-card
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
}</div>
|
|
1774
|
-
<div class="trend-percentage">${skipPercentage}%</div>
|
|
1775
|
-
</div>
|
|
1776
|
-
<div class="summary-card">
|
|
1777
|
-
<h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div>
|
|
1778
|
-
</div>
|
|
1779
|
-
<div class="summary-card">
|
|
1780
|
-
<h3>Run Duration</h3><div class="value">${formatDuration(
|
|
1781
|
-
runSummary.duration
|
|
1782
|
-
)}</div>
|
|
1783
|
-
</div>
|
|
2417
|
+
<div class="summary-card"><h3>Total Tests</h3><div class="value">${
|
|
2418
|
+
runSummary.totalTests
|
|
2419
|
+
}</div></div>
|
|
2420
|
+
<div class="summary-card status-passed"><h3>Passed</h3><div class="value">${
|
|
2421
|
+
runSummary.passed
|
|
2422
|
+
}</div><div class="trend-percentage">${passPercentage}%</div></div>
|
|
2423
|
+
<div class="summary-card status-failed"><h3>Failed</h3><div class="value">${
|
|
2424
|
+
runSummary.failed
|
|
2425
|
+
}</div><div class="trend-percentage">${failPercentage}%</div></div>
|
|
2426
|
+
<div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
|
|
2427
|
+
runSummary.skipped || 0
|
|
2428
|
+
}</div><div class="trend-percentage">${skipPercentage}%</div></div>
|
|
2429
|
+
<div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
|
|
2430
|
+
<div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
|
|
2431
|
+
runSummary.duration
|
|
2432
|
+
)}</div></div>
|
|
1784
2433
|
</div>
|
|
1785
2434
|
<div class="dashboard-bottom-row">
|
|
2435
|
+
<div style="display: grid; gap: 20px">
|
|
1786
2436
|
${generatePieChart(
|
|
1787
|
-
// Changed from generatePieChartD3
|
|
1788
2437
|
[
|
|
1789
2438
|
{ label: "Passed", value: runSummary.passed },
|
|
1790
2439
|
{ label: "Failed", value: runSummary.failed },
|
|
1791
2440
|
{ label: "Skipped", value: runSummary.skipped || 0 },
|
|
1792
2441
|
],
|
|
1793
|
-
400,
|
|
1794
|
-
390
|
|
2442
|
+
400,
|
|
2443
|
+
390
|
|
1795
2444
|
)}
|
|
2445
|
+
${
|
|
2446
|
+
runSummary.environment &&
|
|
2447
|
+
Object.keys(runSummary.environment).length > 0
|
|
2448
|
+
? generateEnvironmentDashboard(runSummary.environment)
|
|
2449
|
+
: '<div class="no-data">Environment data not available.</div>'
|
|
2450
|
+
}
|
|
2451
|
+
</div>
|
|
1796
2452
|
${generateSuitesWidget(suitesData)}
|
|
1797
2453
|
</div>
|
|
1798
2454
|
</div>
|
|
1799
|
-
|
|
1800
2455
|
<div id="test-runs" class="tab-content">
|
|
1801
2456
|
<div class="filters">
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
`<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
|
|
1818
|
-
browser
|
|
1819
|
-
)}</option>`
|
|
1820
|
-
)
|
|
1821
|
-
.join("")}
|
|
1822
|
-
</select>
|
|
1823
|
-
<button id="expand-all-tests">Expand All</button>
|
|
1824
|
-
<button id="collapse-all-tests">Collapse All</button>
|
|
1825
|
-
<button id="clear-run-summary-filters" class="clear-filters-btn">Clear Filters</button>
|
|
1826
|
-
</div>
|
|
1827
|
-
<div class="test-cases-list">
|
|
1828
|
-
${generateTestCasesHTML()}
|
|
2457
|
+
<input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
|
|
2458
|
+
<select id="filter-status"><option value="">All Statuses</option><option value="passed">Passed</option><option value="failed">Failed</option><option value="skipped">Skipped</option></select>
|
|
2459
|
+
<select id="filter-browser"><option value="">All Browsers</option>${Array.from(
|
|
2460
|
+
new Set(
|
|
2461
|
+
(results || []).map((test) => test.browser || "unknown")
|
|
2462
|
+
)
|
|
2463
|
+
)
|
|
2464
|
+
.map(
|
|
2465
|
+
(browser) =>
|
|
2466
|
+
`<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
|
|
2467
|
+
browser
|
|
2468
|
+
)}</option>`
|
|
2469
|
+
)
|
|
2470
|
+
.join("")}</select>
|
|
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>
|
|
1829
2472
|
</div>
|
|
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
|
+
}
|
|
1830
2485
|
</div>
|
|
1831
|
-
|
|
1832
2486
|
<div id="test-history" class="tab-content">
|
|
1833
2487
|
<h2 class="tab-main-title">Execution Trends</h2>
|
|
1834
2488
|
<div class="trend-charts-row">
|
|
1835
|
-
<div class="trend-chart">
|
|
1836
|
-
<h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
|
|
2489
|
+
<div class="trend-chart"><h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
|
|
1837
2490
|
${
|
|
1838
2491
|
trendData && trendData.overall && trendData.overall.length > 0
|
|
1839
2492
|
? generateTestTrendsChart(trendData)
|
|
1840
2493
|
: '<div class="no-data">Overall trend data not available for test counts.</div>'
|
|
1841
2494
|
}
|
|
1842
2495
|
</div>
|
|
1843
|
-
<div class="trend-chart">
|
|
1844
|
-
<h3 class="chart-title-header">Execution Duration Trends</h3>
|
|
2496
|
+
<div class="trend-chart"><h3 class="chart-title-header">Execution Duration Trends</h3>
|
|
1845
2497
|
${
|
|
1846
2498
|
trendData && trendData.overall && trendData.overall.length > 0
|
|
1847
2499
|
? generateDurationTrendChart(trendData)
|
|
1848
2500
|
: '<div class="no-data">Overall trend data not available for durations.</div>'
|
|
1849
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)}
|
|
1850
2508
|
</div>
|
|
1851
2509
|
</div>
|
|
1852
2510
|
<h2 class="tab-main-title">Individual Test History</h2>
|
|
@@ -1858,69 +2516,162 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1858
2516
|
: '<div class="no-data">Individual test history data not available.</div>'
|
|
1859
2517
|
}
|
|
1860
2518
|
</div>
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
<iframe
|
|
1864
|
-
src="https://ai-test-analyser.netlify.app/"
|
|
1865
|
-
width="100%"
|
|
1866
|
-
height="100%"
|
|
1867
|
-
frameborder="0"
|
|
1868
|
-
allowfullscreen
|
|
1869
|
-
style="border: none; height: 100vh;">
|
|
1870
|
-
</iframe>
|
|
2519
|
+
<div id="ai-failure-analyzer" class="tab-content">
|
|
2520
|
+
${generateAIFailureAnalyzerTab(results)}
|
|
1871
2521
|
</div>
|
|
1872
|
-
<footer style="
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
">
|
|
1878
|
-
|
|
1879
|
-
display: inline-flex;
|
|
1880
|
-
align-items: center;
|
|
1881
|
-
gap: 0.5rem;
|
|
1882
|
-
color: #333;
|
|
1883
|
-
font-size: 0.9rem;
|
|
1884
|
-
font-weight: 600;
|
|
1885
|
-
letter-spacing: 0.5px;
|
|
1886
|
-
">
|
|
1887
|
-
<img width="48" height="48" src="https://img.icons8.com/emoji/48/index-pointing-at-the-viewer-light-skin-tone-emoji.png" alt="index-pointing-at-the-viewer-light-skin-tone-emoji"/>
|
|
1888
|
-
<span>Created by</span>
|
|
1889
|
-
<a href="https://github.com/Arghajit47"
|
|
1890
|
-
target="_blank"
|
|
1891
|
-
rel="noopener noreferrer"
|
|
1892
|
-
style="
|
|
1893
|
-
color: #7737BF;
|
|
1894
|
-
font-weight: 700;
|
|
1895
|
-
font-style: italic;
|
|
1896
|
-
text-decoration: none;
|
|
1897
|
-
transition: all 0.2s ease;
|
|
1898
|
-
"
|
|
1899
|
-
onmouseover="this.style.color='#BF5C37'"
|
|
1900
|
-
onmouseout="this.style.color='#7737BF'">
|
|
1901
|
-
Arghajit Singha
|
|
1902
|
-
</a>
|
|
1903
|
-
</div>
|
|
1904
|
-
<div style="
|
|
1905
|
-
margin-top: 0.5rem;
|
|
1906
|
-
font-size: 0.75rem;
|
|
1907
|
-
color: #666;
|
|
1908
|
-
">
|
|
1909
|
-
Crafted with precision
|
|
1910
|
-
</div>
|
|
1911
|
-
</footer>
|
|
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;">
|
|
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;">
|
|
2524
|
+
<span class="footer-text">Created by</span>
|
|
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>
|
|
2526
|
+
</div>
|
|
2527
|
+
<div style="margin-top: 0.5rem; font-size: 0.75rem; color: #666;">Crafted with precision</div>
|
|
2528
|
+
</footer>
|
|
1912
2529
|
</div>
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
2530
|
<script>
|
|
1916
2531
|
// Ensure formatDuration is globally available
|
|
1917
|
-
if (typeof formatDuration === 'undefined') {
|
|
1918
|
-
function formatDuration(ms) {
|
|
1919
|
-
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
1920
|
-
return (ms / 1000).toFixed(1) + "s";
|
|
2532
|
+
if (typeof formatDuration === 'undefined') {
|
|
2533
|
+
function formatDuration(ms) {
|
|
2534
|
+
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
2535
|
+
return (ms / 1000).toFixed(1) + "s";
|
|
2536
|
+
}
|
|
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>\`;
|
|
1921
2651
|
}
|
|
1922
2652
|
}
|
|
1923
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
|
+
|
|
1924
2675
|
function initializeReportInteractivity() {
|
|
1925
2676
|
const tabButtons = document.querySelectorAll('.tab-button');
|
|
1926
2677
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
@@ -1931,82 +2682,100 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1931
2682
|
button.classList.add('active');
|
|
1932
2683
|
const tabId = button.getAttribute('data-tab');
|
|
1933
2684
|
const activeContent = document.getElementById(tabId);
|
|
1934
|
-
if (activeContent)
|
|
2685
|
+
if (activeContent) {
|
|
2686
|
+
activeContent.classList.add('active');
|
|
2687
|
+
}
|
|
1935
2688
|
});
|
|
1936
2689
|
});
|
|
1937
|
-
|
|
1938
2690
|
// --- Test Run Summary Filters ---
|
|
1939
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
|
+
|
|
1940
2727
|
const statusFilter = document.getElementById('filter-status');
|
|
1941
2728
|
const browserFilter = document.getElementById('filter-browser');
|
|
1942
|
-
const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters');
|
|
1943
|
-
|
|
1944
|
-
|
|
2729
|
+
const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters');
|
|
2730
|
+
function filterTestCases() {
|
|
2731
|
+
ensureAllTestsAppended();
|
|
1945
2732
|
const nameValue = nameFilter ? nameFilter.value.toLowerCase() : "";
|
|
1946
2733
|
const statusValue = statusFilter ? statusFilter.value : "";
|
|
1947
2734
|
const browserValue = browserFilter ? browserFilter.value : "";
|
|
1948
|
-
|
|
1949
2735
|
document.querySelectorAll('#test-runs .test-case').forEach(testCaseElement => {
|
|
1950
2736
|
const titleElement = testCaseElement.querySelector('.test-case-title');
|
|
1951
2737
|
const fullTestName = titleElement ? titleElement.getAttribute('title').toLowerCase() : "";
|
|
1952
2738
|
const status = testCaseElement.getAttribute('data-status');
|
|
1953
2739
|
const browser = testCaseElement.getAttribute('data-browser');
|
|
1954
|
-
|
|
1955
2740
|
const nameMatch = fullTestName.includes(nameValue);
|
|
1956
2741
|
const statusMatch = !statusValue || status === statusValue;
|
|
1957
2742
|
const browserMatch = !browserValue || browser === browserValue;
|
|
1958
|
-
|
|
1959
2743
|
testCaseElement.style.display = (nameMatch && statusMatch && browserMatch) ? '' : 'none';
|
|
1960
2744
|
});
|
|
1961
2745
|
}
|
|
1962
2746
|
if(nameFilter) nameFilter.addEventListener('input', filterTestCases);
|
|
1963
2747
|
if(statusFilter) statusFilter.addEventListener('change', filterTestCases);
|
|
1964
2748
|
if(browserFilter) browserFilter.addEventListener('change', filterTestCases);
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
filterTestCases(); // Re-apply filters (which will show all)
|
|
1973
|
-
});
|
|
1974
|
-
}
|
|
1975
|
-
|
|
2749
|
+
if(clearRunSummaryFiltersBtn) clearRunSummaryFiltersBtn.addEventListener('click', () => {
|
|
2750
|
+
ensureAllTestsAppended();
|
|
2751
|
+
if(nameFilter) nameFilter.value = '';
|
|
2752
|
+
if(statusFilter) statusFilter.value = '';
|
|
2753
|
+
if(browserFilter) browserFilter.value = '';
|
|
2754
|
+
filterTestCases();
|
|
2755
|
+
});
|
|
1976
2756
|
// --- Test History Filters ---
|
|
1977
2757
|
const historyNameFilter = document.getElementById('history-filter-name');
|
|
1978
2758
|
const historyStatusFilter = document.getElementById('history-filter-status');
|
|
1979
|
-
const clearHistoryFiltersBtn = document.getElementById('clear-history-filters');
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
function filterTestHistoryCards() {
|
|
2759
|
+
const clearHistoryFiltersBtn = document.getElementById('clear-history-filters');
|
|
2760
|
+
function filterTestHistoryCards() {
|
|
1983
2761
|
const nameValue = historyNameFilter ? historyNameFilter.value.toLowerCase() : "";
|
|
1984
2762
|
const statusValue = historyStatusFilter ? historyStatusFilter.value : "";
|
|
1985
|
-
|
|
1986
2763
|
document.querySelectorAll('.test-history-card').forEach(card => {
|
|
1987
2764
|
const testTitle = card.getAttribute('data-test-name').toLowerCase();
|
|
1988
2765
|
const latestStatus = card.getAttribute('data-latest-status');
|
|
1989
|
-
|
|
1990
2766
|
const nameMatch = testTitle.includes(nameValue);
|
|
1991
2767
|
const statusMatch = !statusValue || latestStatus === statusValue;
|
|
1992
|
-
|
|
1993
2768
|
card.style.display = (nameMatch && statusMatch) ? '' : 'none';
|
|
1994
2769
|
});
|
|
1995
2770
|
}
|
|
1996
2771
|
if(historyNameFilter) historyNameFilter.addEventListener('input', filterTestHistoryCards);
|
|
1997
2772
|
if(historyStatusFilter) historyStatusFilter.addEventListener('change', filterTestHistoryCards);
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
filterTestHistoryCards(); // Re-apply filters (which will show all)
|
|
2005
|
-
});
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
// --- Expand/Collapse and Toggle Details Logic (remains the same) ---
|
|
2009
|
-
function toggleElementDetails(headerElement, contentSelector) {
|
|
2773
|
+
if(clearHistoryFiltersBtn) clearHistoryFiltersBtn.addEventListener('click', () => {
|
|
2774
|
+
if(historyNameFilter) historyNameFilter.value = ''; if(historyStatusFilter) historyStatusFilter.value = '';
|
|
2775
|
+
filterTestHistoryCards();
|
|
2776
|
+
});
|
|
2777
|
+
// --- Expand/Collapse and Toggle Details Logic ---
|
|
2778
|
+
function toggleElementDetails(headerElement, contentSelector) {
|
|
2010
2779
|
let contentElement;
|
|
2011
2780
|
if (headerElement.classList.contains('test-case-header')) {
|
|
2012
2781
|
contentElement = headerElement.parentElement.querySelector('.test-case-content');
|
|
@@ -2016,41 +2785,179 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2016
2785
|
contentElement = null;
|
|
2017
2786
|
}
|
|
2018
2787
|
}
|
|
2019
|
-
|
|
2020
2788
|
if (contentElement) {
|
|
2021
2789
|
const isExpanded = contentElement.style.display === 'block';
|
|
2022
2790
|
contentElement.style.display = isExpanded ? 'none' : 'block';
|
|
2023
2791
|
headerElement.setAttribute('aria-expanded', String(!isExpanded));
|
|
2024
2792
|
}
|
|
2025
2793
|
}
|
|
2026
|
-
|
|
2027
|
-
document.querySelectorAll('#test-runs .test-case-header').forEach(header => {
|
|
2028
|
-
header.addEventListener('click', () => toggleElementDetails(header));
|
|
2029
|
-
});
|
|
2030
|
-
document.querySelectorAll('#test-runs .step-header').forEach(header => {
|
|
2031
|
-
header.addEventListener('click', () => toggleElementDetails(header, '.step-details'));
|
|
2032
|
-
});
|
|
2033
|
-
|
|
2034
2794
|
const expandAllBtn = document.getElementById('expand-all-tests');
|
|
2035
2795
|
const collapseAllBtn = document.getElementById('collapse-all-tests');
|
|
2036
|
-
|
|
2037
2796
|
function setAllTestRunDetailsVisibility(displayMode, ariaState) {
|
|
2038
2797
|
document.querySelectorAll('#test-runs .test-case-content').forEach(el => el.style.display = displayMode);
|
|
2039
2798
|
document.querySelectorAll('#test-runs .step-details').forEach(el => el.style.display = displayMode);
|
|
2040
2799
|
document.querySelectorAll('#test-runs .test-case-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
|
|
2041
2800
|
document.querySelectorAll('#test-runs .step-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
|
|
2042
2801
|
}
|
|
2043
|
-
|
|
2044
2802
|
if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
|
|
2045
2803
|
if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
|
|
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');
|
|
2881
|
+
if ('IntersectionObserver' in window) {
|
|
2882
|
+
let lazyObserver = new IntersectionObserver((entries, observer) => {
|
|
2883
|
+
entries.forEach(entry => {
|
|
2884
|
+
if (entry.isIntersecting) {
|
|
2885
|
+
const element = entry.target;
|
|
2886
|
+
if (element.classList.contains('lazy-load-iframe')) {
|
|
2887
|
+
if (element.dataset.src) {
|
|
2888
|
+
element.src = element.dataset.src;
|
|
2889
|
+
element.removeAttribute('data-src');
|
|
2890
|
+
}
|
|
2891
|
+
} else if (element.classList.contains('lazy-load-chart')) {
|
|
2892
|
+
const renderFunctionName = element.dataset.renderFunctionName;
|
|
2893
|
+
if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
|
|
2894
|
+
window[renderFunctionName]();
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
observer.unobserve(element);
|
|
2898
|
+
}
|
|
2899
|
+
});
|
|
2900
|
+
}, { rootMargin: "0px 0px 200px 0px" });
|
|
2901
|
+
lazyLoadElements.forEach(el => lazyObserver.observe(el));
|
|
2902
|
+
} else {
|
|
2903
|
+
lazyLoadElements.forEach(element => {
|
|
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](); }
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2046
2908
|
}
|
|
2047
2909
|
document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
|
|
2910
|
+
|
|
2911
|
+
function copyErrorToClipboard(button) {
|
|
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
|
+
}
|
|
2927
|
+
|
|
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
|
+
});
|
|
2941
|
+
}
|
|
2048
2942
|
</script>
|
|
2943
|
+
|
|
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>
|
|
2956
|
+
|
|
2049
2957
|
</body>
|
|
2050
2958
|
</html>
|
|
2051
2959
|
`;
|
|
2052
2960
|
}
|
|
2053
|
-
|
|
2054
2961
|
async function runScript(scriptPath) {
|
|
2055
2962
|
return new Promise((resolve, reject) => {
|
|
2056
2963
|
console.log(chalk.blue(`Executing script: ${scriptPath}...`));
|
|
@@ -2075,7 +2982,11 @@ async function runScript(scriptPath) {
|
|
|
2075
2982
|
});
|
|
2076
2983
|
});
|
|
2077
2984
|
}
|
|
2078
|
-
|
|
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
|
+
*/
|
|
2079
2990
|
async function main() {
|
|
2080
2991
|
const __filename = fileURLToPath(import.meta.url);
|
|
2081
2992
|
const __dirname = path.dirname(__filename);
|
|
@@ -2110,11 +3021,10 @@ async function main() {
|
|
|
2110
3021
|
),
|
|
2111
3022
|
error
|
|
2112
3023
|
);
|
|
2113
|
-
// You might decide to proceed or exit depending on the importance of historical data
|
|
2114
3024
|
}
|
|
2115
3025
|
|
|
2116
3026
|
// Step 2: Load current run's data (for non-trend sections of the report)
|
|
2117
|
-
let currentRunReportData;
|
|
3027
|
+
let currentRunReportData;
|
|
2118
3028
|
try {
|
|
2119
3029
|
const jsonData = await fs.readFile(reportJsonPath, "utf-8");
|
|
2120
3030
|
currentRunReportData = JSON.parse(jsonData);
|
|
@@ -2141,13 +3051,13 @@ async function main() {
|
|
|
2141
3051
|
`Critical Error: Could not read or parse main report JSON at ${reportJsonPath}: ${error.message}`
|
|
2142
3052
|
)
|
|
2143
3053
|
);
|
|
2144
|
-
process.exit(1);
|
|
3054
|
+
process.exit(1);
|
|
2145
3055
|
}
|
|
2146
3056
|
|
|
2147
3057
|
// Step 3: Load historical data for trends
|
|
2148
|
-
let historicalRuns = [];
|
|
3058
|
+
let historicalRuns = [];
|
|
2149
3059
|
try {
|
|
2150
|
-
await fs.access(historyDir);
|
|
3060
|
+
await fs.access(historyDir);
|
|
2151
3061
|
const allHistoryFiles = await fs.readdir(historyDir);
|
|
2152
3062
|
|
|
2153
3063
|
const jsonHistoryFiles = allHistoryFiles
|
|
@@ -2165,7 +3075,7 @@ async function main() {
|
|
|
2165
3075
|
};
|
|
2166
3076
|
})
|
|
2167
3077
|
.filter((file) => !isNaN(file.timestamp))
|
|
2168
|
-
.sort((a, b) => b.timestamp - a.timestamp);
|
|
3078
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
2169
3079
|
|
|
2170
3080
|
const filesToLoadForTrend = jsonHistoryFiles.slice(
|
|
2171
3081
|
0,
|
|
@@ -2175,7 +3085,7 @@ async function main() {
|
|
|
2175
3085
|
for (const fileMeta of filesToLoadForTrend) {
|
|
2176
3086
|
try {
|
|
2177
3087
|
const fileContent = await fs.readFile(fileMeta.path, "utf-8");
|
|
2178
|
-
const runJsonData = JSON.parse(fileContent);
|
|
3088
|
+
const runJsonData = JSON.parse(fileContent);
|
|
2179
3089
|
historicalRuns.push(runJsonData);
|
|
2180
3090
|
} catch (fileReadError) {
|
|
2181
3091
|
console.warn(
|
|
@@ -2185,8 +3095,7 @@ async function main() {
|
|
|
2185
3095
|
);
|
|
2186
3096
|
}
|
|
2187
3097
|
}
|
|
2188
|
-
//
|
|
2189
|
-
historicalRuns.reverse();
|
|
3098
|
+
historicalRuns.reverse(); // Oldest first for charts
|
|
2190
3099
|
console.log(
|
|
2191
3100
|
chalk.green(
|
|
2192
3101
|
`Loaded ${historicalRuns.length} historical run(s) for trend analysis.`
|
|
@@ -2208,20 +3117,18 @@ async function main() {
|
|
|
2208
3117
|
}
|
|
2209
3118
|
}
|
|
2210
3119
|
|
|
2211
|
-
// Step 4: Prepare trendData object
|
|
3120
|
+
// Step 4: Prepare trendData object
|
|
2212
3121
|
const trendData = {
|
|
2213
|
-
overall: [],
|
|
2214
|
-
testRuns: {},
|
|
3122
|
+
overall: [],
|
|
3123
|
+
testRuns: {},
|
|
2215
3124
|
};
|
|
2216
3125
|
|
|
2217
3126
|
if (historicalRuns.length > 0) {
|
|
2218
3127
|
historicalRuns.forEach((histRunReport) => {
|
|
2219
|
-
// histRunReport is a full PlaywrightPulseReport object from a past run
|
|
2220
3128
|
if (histRunReport.run) {
|
|
2221
|
-
// Ensure timestamp is a Date object for correct sorting/comparison later if needed by charts
|
|
2222
3129
|
const runTimestamp = new Date(histRunReport.run.timestamp);
|
|
2223
3130
|
trendData.overall.push({
|
|
2224
|
-
runId: runTimestamp.getTime(),
|
|
3131
|
+
runId: runTimestamp.getTime(),
|
|
2225
3132
|
timestamp: runTimestamp,
|
|
2226
3133
|
duration: histRunReport.run.duration,
|
|
2227
3134
|
totalTests: histRunReport.run.totalTests,
|
|
@@ -2230,21 +3137,19 @@ async function main() {
|
|
|
2230
3137
|
skipped: histRunReport.run.skipped || 0,
|
|
2231
3138
|
});
|
|
2232
3139
|
|
|
2233
|
-
// For generateTestHistoryContent
|
|
2234
3140
|
if (histRunReport.results && Array.isArray(histRunReport.results)) {
|
|
2235
|
-
const runKeyForTestHistory = `test run ${runTimestamp.getTime()}`;
|
|
3141
|
+
const runKeyForTestHistory = `test run ${runTimestamp.getTime()}`;
|
|
2236
3142
|
trendData.testRuns[runKeyForTestHistory] = histRunReport.results.map(
|
|
2237
3143
|
(test) => ({
|
|
2238
|
-
testName: test.name,
|
|
3144
|
+
testName: test.name,
|
|
2239
3145
|
duration: test.duration,
|
|
2240
3146
|
status: test.status,
|
|
2241
|
-
timestamp: new Date(test.startTime),
|
|
3147
|
+
timestamp: new Date(test.startTime),
|
|
2242
3148
|
})
|
|
2243
3149
|
);
|
|
2244
3150
|
}
|
|
2245
3151
|
}
|
|
2246
3152
|
});
|
|
2247
|
-
// Ensure trendData.overall is sorted by timestamp if not already
|
|
2248
3153
|
trendData.overall.sort(
|
|
2249
3154
|
(a, b) => a.timestamp.getTime() - b.timestamp.getTime()
|
|
2250
3155
|
);
|
|
@@ -2252,8 +3157,6 @@ async function main() {
|
|
|
2252
3157
|
|
|
2253
3158
|
// Step 5: Generate and write HTML
|
|
2254
3159
|
try {
|
|
2255
|
-
// currentRunReportData is for the main content (test list, summary cards of *this* run)
|
|
2256
|
-
// trendData is for the historical charts and test history section
|
|
2257
3160
|
const htmlContent = generateHTML(currentRunReportData, trendData);
|
|
2258
3161
|
await fs.writeFile(reportHtmlPath, htmlContent, "utf-8");
|
|
2259
3162
|
console.log(
|
|
@@ -2264,16 +3167,14 @@ async function main() {
|
|
|
2264
3167
|
console.log(chalk.gray(`(You can open this file in your browser)`));
|
|
2265
3168
|
} catch (error) {
|
|
2266
3169
|
console.error(chalk.red(`Error generating HTML report: ${error.message}`));
|
|
2267
|
-
console.error(chalk.red(error.stack));
|
|
3170
|
+
console.error(chalk.red(error.stack));
|
|
2268
3171
|
process.exit(1);
|
|
2269
3172
|
}
|
|
2270
3173
|
}
|
|
2271
|
-
|
|
2272
|
-
// Make sure main() is called at the end of your script
|
|
2273
3174
|
main().catch((err) => {
|
|
2274
3175
|
console.error(
|
|
2275
3176
|
chalk.red.bold(`Unhandled error during script execution: ${err.message}`)
|
|
2276
3177
|
);
|
|
2277
3178
|
console.error(err.stack);
|
|
2278
3179
|
process.exit(1);
|
|
2279
|
-
});
|
|
3180
|
+
});
|