@arghajit/dummy 0.1.0 → 0.1.2-beta-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 +8 -0
- package/dist/reporter/playwright-pulse-reporter.js +360 -151
- package/dist/reporter/tsconfig.reporter.tsbuildinfo +1 -0
- package/dist/types/index.d.ts +31 -4
- package/package.json +17 -6
- package/scripts/generate-email-report.mjs +714 -0
- package/scripts/generate-report.mjs +3034 -0
- package/scripts/generate-static-report.mjs +2201 -1286
- 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
|
+
}
|
|
651
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
|
+
}
|
|
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
|
+
);
|
|
907
1690
|
|
|
1691
|
+
if (failedTests.length === 0) {
|
|
1692
|
+
return `
|
|
1693
|
+
<h2 class="tab-main-title">AI Failure Analysis</h2>
|
|
1694
|
+
<div class="no-data">Congratulations! No failed tests in this run.</div>
|
|
1695
|
+
`;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// btoa is not available in Node.js environment, so we define a simple polyfill for it.
|
|
1699
|
+
const btoa = (str) => Buffer.from(str).toString("base64");
|
|
1700
|
+
|
|
1701
|
+
return `
|
|
1702
|
+
<h2 class="tab-main-title">AI Failure Analysis</h2>
|
|
1703
|
+
<div class="ai-analyzer-stats">
|
|
1704
|
+
<div class="stat-item">
|
|
1705
|
+
<span class="stat-number">${failedTests.length}</span>
|
|
1706
|
+
<span class="stat-label">Failed Tests</span>
|
|
1707
|
+
</div>
|
|
1708
|
+
<div class="stat-item">
|
|
1709
|
+
<span class="stat-number">${
|
|
1710
|
+
new Set(failedTests.map((t) => t.browser)).size
|
|
1711
|
+
}</span>
|
|
1712
|
+
<span class="stat-label">Browsers</span>
|
|
1713
|
+
</div>
|
|
1714
|
+
<div class="stat-item">
|
|
1715
|
+
<span class="stat-number">${Math.round(
|
|
1716
|
+
failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) /
|
|
1717
|
+
1000
|
|
1718
|
+
)}s</span>
|
|
1719
|
+
<span class="stat-label">Total Duration</span>
|
|
1720
|
+
</div>
|
|
1721
|
+
</div>
|
|
1722
|
+
<p class="ai-analyzer-description">
|
|
1723
|
+
Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for specific failed test.
|
|
1724
|
+
</p>
|
|
1725
|
+
|
|
1726
|
+
<div class="compact-failure-list">
|
|
1727
|
+
${failedTests
|
|
1728
|
+
.map((test) => {
|
|
1729
|
+
const testTitle = test.name.split(" > ").pop() || "Unnamed Test";
|
|
1730
|
+
const testJson = btoa(JSON.stringify(test)); // Base64 encode the test object
|
|
1731
|
+
const truncatedError =
|
|
1732
|
+
(test.errorMessage || "No error message").slice(0, 150) +
|
|
1733
|
+
(test.errorMessage && test.errorMessage.length > 150 ? "..." : "");
|
|
1734
|
+
|
|
1735
|
+
return `
|
|
1736
|
+
<div class="compact-failure-item">
|
|
1737
|
+
<div class="failure-header">
|
|
1738
|
+
<div class="failure-main-info">
|
|
1739
|
+
<h3 class="failure-title" title="${sanitizeHTML(
|
|
1740
|
+
test.name
|
|
1741
|
+
)}">${sanitizeHTML(testTitle)}</h3>
|
|
1742
|
+
<div class="failure-meta">
|
|
1743
|
+
<span class="browser-indicator">${sanitizeHTML(
|
|
1744
|
+
test.browser || "unknown"
|
|
1745
|
+
)}</span>
|
|
1746
|
+
<span class="duration-indicator">${formatDuration(
|
|
1747
|
+
test.duration
|
|
1748
|
+
)}</span>
|
|
1749
|
+
</div>
|
|
1750
|
+
</div>
|
|
1751
|
+
<button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
|
|
1752
|
+
<span class="ai-text">AI Fix</span>
|
|
1753
|
+
</button>
|
|
1754
|
+
</div>
|
|
1755
|
+
<div class="failure-error-preview">
|
|
1756
|
+
<div class="error-snippet">${formatPlaywrightError(
|
|
1757
|
+
truncatedError
|
|
1758
|
+
)}</div>
|
|
1759
|
+
<button class="expand-error-btn" onclick="toggleErrorDetails(this)">
|
|
1760
|
+
<span class="expand-text">Show Full Error</span>
|
|
1761
|
+
<span class="expand-icon">▼</span>
|
|
1762
|
+
</button>
|
|
1763
|
+
</div>
|
|
1764
|
+
<div class="full-error-details" style="display: none;">
|
|
1765
|
+
<div class="full-error-content">
|
|
1766
|
+
${formatPlaywrightError(
|
|
1767
|
+
test.errorMessage || "No detailed error message available"
|
|
1768
|
+
)}
|
|
1769
|
+
</div>
|
|
1770
|
+
</div>
|
|
1771
|
+
</div>
|
|
1772
|
+
`;
|
|
1773
|
+
})
|
|
1774
|
+
.join("")}
|
|
1775
|
+
</div>
|
|
1776
|
+
`;
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Generates the HTML report.
|
|
1780
|
+
* @param {object} reportData - The data for the report.
|
|
1781
|
+
* @param {object} trendData - The data for the trend chart.
|
|
1782
|
+
* @returns {string} The HTML report.
|
|
1783
|
+
*/
|
|
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,697 @@ 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
|
+
if (!step.attachments || step.attachments.length === 0)
|
|
1855
|
+
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 =
|
|
1869
|
+
readFileSync(attachmentPath).toString("base64");
|
|
1870
|
+
const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
|
|
1871
|
+
return `<div class="attachment-item generic-attachment">
|
|
1872
|
+
<div class="attachment-icon">${getAttachmentIcon(
|
|
1873
|
+
attachment.contentType
|
|
1874
|
+
)}</div>
|
|
1875
|
+
<div class="attachment-caption">
|
|
1876
|
+
<span class="attachment-name" title="${sanitizeHTML(
|
|
1877
|
+
attachment.name
|
|
1878
|
+
)}">${sanitizeHTML(attachment.name)}</span>
|
|
1879
|
+
<span class="attachment-type">${sanitizeHTML(
|
|
1880
|
+
attachment.contentType
|
|
1881
|
+
)}</span>
|
|
1882
|
+
</div>
|
|
1883
|
+
<div class="attachment-info">
|
|
1884
|
+
<div class="trace-actions">
|
|
1885
|
+
<a href="#" data-href="${attachmentDataUri}" class="view-full lazy-load-attachment" target="_blank">View</a>
|
|
1886
|
+
<a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
|
|
1887
|
+
attachment.name
|
|
1888
|
+
)}">Download</a>
|
|
1889
|
+
</div>
|
|
1890
|
+
</div>
|
|
1891
|
+
</div>`;
|
|
1892
|
+
} catch (e) {
|
|
1893
|
+
return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(
|
|
1894
|
+
attachment.name
|
|
1895
|
+
)}</div>`;
|
|
1896
|
+
}
|
|
1897
|
+
})
|
|
1898
|
+
.join("")}</div></div>`;
|
|
1899
|
+
})()}${
|
|
989
1900
|
hasNestedSteps
|
|
990
1901
|
? `<div class="nested-steps">${generateStepsHTML(
|
|
991
1902
|
step.steps,
|
|
992
1903
|
depth + 1
|
|
993
1904
|
)}</div>`
|
|
994
|
-
: ""
|
|
995
|
-
}
|
|
996
|
-
</div>
|
|
997
|
-
</div>`;
|
|
1905
|
+
: ""
|
|
1906
|
+
}</div></div>`;
|
|
998
1907
|
})
|
|
999
1908
|
.join("");
|
|
1000
1909
|
};
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1910
|
+
return `<div class="test-case" data-status="${
|
|
1911
|
+
test.status
|
|
1912
|
+
}" data-browser="${sanitizeHTML(browser)}" data-tags="${(
|
|
1913
|
+
test.tags || []
|
|
1914
|
+
)
|
|
1006
1915
|
.join(",")
|
|
1007
|
-
.toLowerCase()}"
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1916
|
+
.toLowerCase()}" data-test-id="${sanitizeHTML(
|
|
1917
|
+
String(test.id || testIndex)
|
|
1918
|
+
)}">
|
|
1919
|
+
<div class="test-case-header" role="button" aria-expanded="false"><div class="test-case-summary"><span class="status-badge ${getStatusClass(
|
|
1920
|
+
test.status
|
|
1921
|
+
)}">${String(
|
|
1011
1922
|
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
|
-
|
|
1923
|
+
).toUpperCase()}</span><span class="test-case-title" title="${sanitizeHTML(
|
|
1924
|
+
test.name
|
|
1925
|
+
)}">${sanitizeHTML(
|
|
1926
|
+
testTitle
|
|
1927
|
+
)}</span><span class="test-case-browser">(${sanitizeHTML(
|
|
1928
|
+
browser
|
|
1929
|
+
)})</span></div><div class="test-case-meta">${
|
|
1930
|
+
test.tags && test.tags.length > 0
|
|
1931
|
+
? test.tags
|
|
1932
|
+
.map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
|
|
1933
|
+
.join(" ")
|
|
1934
|
+
: ""
|
|
1935
|
+
}<span class="test-duration">${formatDuration(
|
|
1936
|
+
test.duration
|
|
1937
|
+
)}</span></div></div>
|
|
1938
|
+
<div class="test-case-content" style="display: none;">
|
|
1939
|
+
<p><strong>Full Path:</strong> ${sanitizeHTML(
|
|
1940
|
+
test.name
|
|
1941
|
+
)}</p>
|
|
1942
|
+
<p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
|
|
1943
|
+
test.workerId
|
|
1944
|
+
)} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
|
|
1945
|
+
test.totalWorkers
|
|
1946
|
+
)}]</p>
|
|
1947
|
+
${
|
|
1948
|
+
test.errorMessage
|
|
1949
|
+
? `<div class="test-error-summary">${formatPlaywrightError(
|
|
1950
|
+
test.errorMessage
|
|
1951
|
+
)}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
|
|
1952
|
+
: ""
|
|
1953
|
+
}
|
|
1954
|
+
${
|
|
1955
|
+
test.snippet
|
|
1956
|
+
? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
|
|
1957
|
+
test.snippet
|
|
1958
|
+
)}</code></pre></div>`
|
|
1959
|
+
: ""
|
|
1960
|
+
}
|
|
1961
|
+
<h4>Steps</h4><div class="steps-list">${generateStepsHTML(
|
|
1962
|
+
test.steps
|
|
1963
|
+
)}</div>
|
|
1964
|
+
${(() => {
|
|
1965
|
+
if (!test.stdout || test.stdout.length === 0)
|
|
1966
|
+
return "";
|
|
1967
|
+
// Create a unique ID for the <pre> element to target it for copying
|
|
1968
|
+
const logId = `stdout-log-${test.id || testIndex}`;
|
|
1969
|
+
return `<div class="console-output-section">
|
|
1970
|
+
<h4>Console Output (stdout)
|
|
1971
|
+
<button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy Console</button>
|
|
1972
|
+
</h4>
|
|
1973
|
+
<div class="log-wrapper">
|
|
1974
|
+
<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(
|
|
1975
|
+
test.stdout
|
|
1976
|
+
.map((line) => sanitizeHTML(line))
|
|
1977
|
+
.join("\n")
|
|
1978
|
+
)}</pre>
|
|
1979
|
+
</div>
|
|
1980
|
+
</div>`;
|
|
1981
|
+
})()}
|
|
1982
|
+
${
|
|
1983
|
+
test.stderr && test.stderr.length > 0
|
|
1984
|
+
? (() => {
|
|
1985
|
+
const logId = `stderr-log-${
|
|
1986
|
+
test.id || testIndex
|
|
1987
|
+
}`;
|
|
1988
|
+
return `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre id="${logId}" class="console-log stderr-log">${test.stderr
|
|
1989
|
+
.map((line) => sanitizeHTML(line))
|
|
1990
|
+
.join("\\n")}</pre></div>`;
|
|
1991
|
+
})()
|
|
1992
|
+
: ""
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
${(() => {
|
|
1996
|
+
if (
|
|
1997
|
+
!test.screenshots ||
|
|
1998
|
+
test.screenshots.length === 0
|
|
1999
|
+
)
|
|
2000
|
+
return "";
|
|
2001
|
+
return `<div class="attachments-section"><h4>Screenshots (Click to load Images)</h4><div class="attachments-grid">${test.screenshots
|
|
2002
|
+
.map((screenshotPath, index) => {
|
|
1106
2003
|
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
|
-
)
|
|
2004
|
+
const imagePath = path.resolve(
|
|
2005
|
+
DEFAULT_OUTPUT_DIR,
|
|
2006
|
+
screenshotPath
|
|
2007
|
+
);
|
|
2008
|
+
if (!fsExistsSync(imagePath))
|
|
2009
|
+
return `<div class="attachment-item error">Screenshot not found: ${sanitizeHTML(
|
|
2010
|
+
screenshotPath
|
|
2011
|
+
)}</div>`;
|
|
2012
|
+
const base64ImageData =
|
|
2013
|
+
readFileSync(imagePath).toString("base64");
|
|
2014
|
+
return `<div class="attachment-item"><img src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" data-src="data:image/png;base64,${base64ImageData}" alt="Screenshot ${
|
|
2015
|
+
index + 1
|
|
2016
|
+
}" 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>`;
|
|
2017
|
+
} catch (e) {
|
|
2018
|
+
return `<div class="attachment-item error">Failed to load screenshot: ${sanitizeHTML(
|
|
2019
|
+
screenshotPath
|
|
2020
|
+
)}</div>`;
|
|
2021
|
+
}
|
|
2022
|
+
})
|
|
2023
|
+
.join("")}</div></div>`;
|
|
2024
|
+
})()}
|
|
2025
|
+
|
|
2026
|
+
${(() => {
|
|
2027
|
+
if (!test.videoPath || test.videoPath.length === 0)
|
|
2028
|
+
return "";
|
|
2029
|
+
return `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
|
|
2030
|
+
.map((videoPath, index) => {
|
|
2031
|
+
try {
|
|
2032
|
+
const videoFilePath = path.resolve(
|
|
2033
|
+
DEFAULT_OUTPUT_DIR,
|
|
2034
|
+
videoPath
|
|
2035
|
+
);
|
|
2036
|
+
if (!fsExistsSync(videoFilePath))
|
|
2037
|
+
return `<div class="attachment-item error">Video not found: ${sanitizeHTML(
|
|
2038
|
+
videoPath
|
|
2039
|
+
)}</div>`;
|
|
2040
|
+
const videoBase64 =
|
|
2041
|
+
readFileSync(videoFilePath).toString(
|
|
2042
|
+
"base64"
|
|
1153
2043
|
);
|
|
1154
|
-
|
|
1155
|
-
|
|
2044
|
+
const fileExtension = path
|
|
2045
|
+
.extname(videoPath)
|
|
2046
|
+
.slice(1)
|
|
2047
|
+
.toLowerCase();
|
|
2048
|
+
const mimeType =
|
|
2049
|
+
{
|
|
2050
|
+
mp4: "video/mp4",
|
|
2051
|
+
webm: "video/webm",
|
|
2052
|
+
ogg: "video/ogg",
|
|
2053
|
+
mov: "video/quicktime",
|
|
2054
|
+
avi: "video/x-msvideo",
|
|
2055
|
+
}[fileExtension] || "video/mp4";
|
|
2056
|
+
const videoDataUri = `data:${mimeType};base64,${videoBase64}`;
|
|
2057
|
+
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>`;
|
|
2058
|
+
} catch (e) {
|
|
2059
|
+
return `<div class="attachment-item error">Failed to load video: ${sanitizeHTML(
|
|
2060
|
+
videoPath
|
|
2061
|
+
)}</div>`;
|
|
2062
|
+
}
|
|
2063
|
+
})
|
|
2064
|
+
.join("")}</div></div>`;
|
|
2065
|
+
})()}
|
|
2066
|
+
|
|
2067
|
+
${(() => {
|
|
2068
|
+
if (!test.tracePath) return "";
|
|
2069
|
+
try {
|
|
2070
|
+
const traceFilePath = path.resolve(
|
|
2071
|
+
DEFAULT_OUTPUT_DIR,
|
|
2072
|
+
test.tracePath
|
|
2073
|
+
);
|
|
2074
|
+
if (!fsExistsSync(traceFilePath))
|
|
2075
|
+
return `<div class="attachments-section"><h4>Trace File</h4><div class="attachment-item error">Trace file not found: ${sanitizeHTML(
|
|
2076
|
+
test.tracePath
|
|
2077
|
+
)}</div></div>`;
|
|
2078
|
+
const traceBase64 =
|
|
2079
|
+
readFileSync(traceFilePath).toString("base64");
|
|
2080
|
+
const traceDataUri = `data:application/zip;base64,${traceBase64}`;
|
|
2081
|
+
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>`;
|
|
2082
|
+
} catch (e) {
|
|
2083
|
+
return `<div class="attachments-section"><h4>Trace File</h4><div class="attachment-item error">Failed to load trace file.</div></div>`;
|
|
2084
|
+
}
|
|
2085
|
+
})()}
|
|
2086
|
+
|
|
2087
|
+
${(() => {
|
|
2088
|
+
if (
|
|
2089
|
+
!test.attachments ||
|
|
2090
|
+
test.attachments.length === 0
|
|
2091
|
+
)
|
|
2092
|
+
return "";
|
|
2093
|
+
|
|
2094
|
+
return `<div class="attachments-section"><h4>Other Attachments</h4><div class="attachments-grid">${test.attachments
|
|
2095
|
+
.map((attachment) => {
|
|
2096
|
+
try {
|
|
2097
|
+
const attachmentPath = path.resolve(
|
|
2098
|
+
DEFAULT_OUTPUT_DIR,
|
|
2099
|
+
attachment.path
|
|
2100
|
+
);
|
|
1156
2101
|
|
|
1157
|
-
if (!
|
|
1158
|
-
// This case should ideally be caught above, but as a fallback:
|
|
2102
|
+
if (!fsExistsSync(attachmentPath)) {
|
|
1159
2103
|
console.warn(
|
|
1160
|
-
|
|
1161
|
-
`[Reporter] Could not obtain base64 data for screenshot: ${escapeHTML(
|
|
1162
|
-
String(screenshotPathOrData)
|
|
1163
|
-
)}`
|
|
1164
|
-
)
|
|
2104
|
+
`Attachment not found at: ${attachmentPath}`
|
|
1165
2105
|
);
|
|
1166
|
-
return `<div class="attachment-item error"
|
|
1167
|
-
|
|
2106
|
+
return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
|
|
2107
|
+
attachment.name
|
|
1168
2108
|
)}</div>`;
|
|
1169
2109
|
}
|
|
1170
2110
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
2111
|
+
const attachmentBase64 =
|
|
2112
|
+
readFileSync(attachmentPath).toString(
|
|
2113
|
+
"base64"
|
|
2114
|
+
);
|
|
2115
|
+
const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
|
|
2116
|
+
|
|
2117
|
+
return `<div class="attachment-item generic-attachment">
|
|
2118
|
+
<div class="attachment-icon">${getAttachmentIcon(
|
|
2119
|
+
attachment.contentType
|
|
2120
|
+
)}</div>
|
|
2121
|
+
<div class="attachment-caption">
|
|
2122
|
+
<span class="attachment-name" title="${sanitizeHTML(
|
|
2123
|
+
attachment.name
|
|
2124
|
+
)}">${sanitizeHTML(
|
|
2125
|
+
attachment.name
|
|
2126
|
+
)}</span>
|
|
2127
|
+
<span class="attachment-type">${sanitizeHTML(
|
|
2128
|
+
attachment.contentType
|
|
2129
|
+
)}</span>
|
|
2130
|
+
</div>
|
|
2131
|
+
<div class="attachment-info">
|
|
2132
|
+
<div class="trace-actions">
|
|
2133
|
+
<a href="#" data-href="${attachmentDataUri}" class="view-full lazy-load-attachment" target="_blank">View</a>
|
|
2134
|
+
<a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
|
|
2135
|
+
attachment.name
|
|
2136
|
+
)}">Download</a>
|
|
2137
|
+
</div>
|
|
2138
|
+
</div>
|
|
2139
|
+
</div>`;
|
|
1194
2140
|
} catch (e) {
|
|
1195
2141
|
console.error(
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
String(screenshotPathOrData)
|
|
1199
|
-
)}: ${e.message}`
|
|
1200
|
-
)
|
|
2142
|
+
`Failed to process attachment "${attachment.name}":`,
|
|
2143
|
+
e
|
|
1201
2144
|
);
|
|
1202
|
-
return `<div class="attachment-item error"
|
|
1203
|
-
|
|
2145
|
+
return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(
|
|
2146
|
+
attachment.name
|
|
1204
2147
|
)}</div>`;
|
|
1205
2148
|
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
2149
|
+
})
|
|
2150
|
+
.join("")}</div></div>`;
|
|
2151
|
+
})()}
|
|
2152
|
+
|
|
2153
|
+
${
|
|
2154
|
+
test.codeSnippet
|
|
2155
|
+
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${sanitizeHTML(
|
|
2156
|
+
test.codeSnippet
|
|
2157
|
+
)}</code></pre></div>`
|
|
2158
|
+
: ""
|
|
2159
|
+
}
|
|
1214
2160
|
</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>`;
|
|
2161
|
+
</div>`;
|
|
1330
2162
|
})
|
|
1331
2163
|
.join("");
|
|
1332
2164
|
}
|
|
1333
|
-
|
|
1334
2165
|
return `
|
|
1335
2166
|
<!DOCTYPE html>
|
|
1336
2167
|
<html lang="en">
|
|
1337
2168
|
<head>
|
|
1338
2169
|
<meta charset="UTF-8">
|
|
1339
2170
|
<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;
|
|
2171
|
+
<link rel="icon" type="image/png" href="https://i.postimg.cc/v817w4sg/logo.png">
|
|
2172
|
+
<link rel="apple-touch-icon" href="https://i.postimg.cc/v817w4sg/logo.png">
|
|
2173
|
+
<script src="https://code.highcharts.com/highcharts.js" defer></script>
|
|
2174
|
+
<title>Playwright Pulse Report (Static Report)</title>
|
|
2175
|
+
|
|
2176
|
+
<style>
|
|
2177
|
+
:root {
|
|
2178
|
+
--primary-color: #60a5fa; --secondary-color: #f472b6; --accent-color: #a78bfa; --accent-color-alt: #fb923c;
|
|
2179
|
+
--success-color: #34d399; --danger-color: #f87171; --warning-color: #fbbf24; --info-color: #60a5fa;
|
|
2180
|
+
--light-gray-color: #374151; --medium-gray-color: #4b5563; --dark-gray-color: #9ca3af;
|
|
2181
|
+
--text-color: #f9fafb; --text-color-secondary: #d1d5db; --border-color: #4b5563; --background-color: #111827;
|
|
2182
|
+
--card-background-color: #1f2937; --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
2183
|
+
--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
2184
|
}
|
|
1657
|
-
|
|
1658
|
-
.
|
|
1659
|
-
|
|
1660
|
-
|
|
2185
|
+
.trend-chart-container, .test-history-trend div[id^="testHistoryChart-"] { min-height: 100px; }
|
|
2186
|
+
.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); }
|
|
2187
|
+
.highcharts-background { fill: transparent; }
|
|
2188
|
+
.highcharts-title, .highcharts-subtitle { font-family: var(--font-family); }
|
|
2189
|
+
.highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
|
|
2190
|
+
.highcharts-axis-title { fill: var(--text-color) !important; }
|
|
2191
|
+
.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; }
|
|
2192
|
+
body { font-family: var(--font-family); margin: 0; background-color: var(--background-color); color: var(--text-color); line-height: 1.65; font-size: 16px; }
|
|
2193
|
+
.container { padding: 30px; border-radius: var(--border-radius); box-shadow: var(--box-shadow); background: repeating-linear-gradient(#1f2937, #374151, #1f2937); }
|
|
2194
|
+
.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; }
|
|
2195
|
+
.header-title { display: flex; align-items: center; gap: 15px; }
|
|
2196
|
+
.header h1 { margin: 0; font-size: 1.85em; font-weight: 600; color: var(--primary-color); }
|
|
2197
|
+
#report-logo { height: 40px; width: 55px; }
|
|
2198
|
+
.run-info { font-size: 0.9em; text-align: right; color: var(--text-color-secondary); line-height:1.5;}
|
|
2199
|
+
.run-info strong { color: var(--text-color); }
|
|
2200
|
+
.tabs { display: flex; border-bottom: 2px solid var(--border-color); margin-bottom: 30px; overflow-x: auto; }
|
|
2201
|
+
.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; }
|
|
2202
|
+
.tab-button:hover { color: var(--accent-color); }
|
|
2203
|
+
.tab-button.active { color: var(--primary-color); border-bottom-color: var(--primary-color); }
|
|
2204
|
+
.tab-content { display: none; animation: fadeIn 0.4s ease-out; }
|
|
2205
|
+
.tab-content.active { display: block; }
|
|
2206
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
2207
|
+
.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 22px; margin-bottom: 35px; }
|
|
2208
|
+
.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; }
|
|
2209
|
+
.summary-card:hover { transform: translateY(-5px); box-shadow: var(--box-shadow); }
|
|
2210
|
+
.summary-card h3 { margin: 0 0 10px; font-size: 1.05em; font-weight: 500; color: var(--text-color-secondary); }
|
|
2211
|
+
.summary-card .value { font-size: 2.4em; font-weight: 600; margin-bottom: 8px; }
|
|
2212
|
+
.summary-card .trend-percentage { font-size: 1em; color: var(--dark-gray-color); }
|
|
2213
|
+
.status-passed .value, .stat-passed svg { color: var(--success-color); }
|
|
2214
|
+
.status-failed .value, .stat-failed svg { color: var(--danger-color); }
|
|
2215
|
+
.status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
|
|
2216
|
+
.dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: stretch; }
|
|
2217
|
+
.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; }
|
|
2218
|
+
.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); }
|
|
2219
|
+
.trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { flex-grow: 1; min-height: 250px; }
|
|
2220
|
+
.status-badge-small-tooltip { padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; }
|
|
2221
|
+
.status-badge-small-tooltip.status-passed { background-color: var(--success-color); }
|
|
2222
|
+
.status-badge-small-tooltip.status-failed { background-color: var(--danger-color); }
|
|
2223
|
+
.status-badge-small-tooltip.status-skipped { background-color: var(--warning-color); }
|
|
2224
|
+
.status-badge-small-tooltip.status-unknown { background-color: var(--dark-gray-color); }
|
|
2225
|
+
.suites-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
|
2226
|
+
.summary-badge { background-color: var(--light-gray-color); color: var(--text-color-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
|
|
2227
|
+
.suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
|
|
2228
|
+
.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; }
|
|
2229
|
+
.suite-card:hover { box-shadow: var(--box-shadow); }
|
|
2230
|
+
.suite-card.status-passed { border-left-color: var(--success-color); }
|
|
2231
|
+
.suite-card.status-failed { border-left-color: var(--danger-color); }
|
|
2232
|
+
.suite-card.status-skipped { border-left-color: var(--warning-color); }
|
|
2233
|
+
.suite-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
|
|
2234
|
+
.suite-name { font-weight: 600; font-size: 1.05em; color: var(--text-color); margin-right: 10px; word-break: break-word;}
|
|
2235
|
+
.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;}
|
|
2236
|
+
.suite-card-body .test-count { font-size: 0.95em; color: var(--text-color-secondary); display: block; margin-bottom: 10px; }
|
|
2237
|
+
.suite-stats { display: flex; gap: 14px; font-size: 0.95em; align-items: center; }
|
|
2238
|
+
.suite-stats span { display: flex; align-items: center; gap: 6px; }
|
|
2239
|
+
.suite-stats svg { vertical-align: middle; font-size: 1.15em; }
|
|
2240
|
+
.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); }
|
|
2241
|
+
.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); }
|
|
2242
|
+
.filters input { flex-grow: 1; min-width: 240px;}
|
|
2243
|
+
.filters select {min-width: 180px;}
|
|
2244
|
+
.filters button { background-color: var(--primary-color); color: white; cursor: pointer; transition: background-color 0.2s ease, box-shadow 0.2s ease; border: none; }
|
|
2245
|
+
.filters button:hover { background-color: var(--accent-color); box-shadow: 0 2px 5px rgba(0,0,0,0.3);}
|
|
2246
|
+
.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; }
|
|
2247
|
+
.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; }
|
|
2248
|
+
.test-case-header:hover { background-color: var(--light-gray-color); }
|
|
2249
|
+
.test-case-header[aria-expanded="true"] { border-bottom-color: var(--border-color); background-color: var(--light-gray-color); }
|
|
2250
|
+
.test-case-summary { display: flex; align-items: center; gap: 14px; flex-grow: 1; flex-wrap: wrap;}
|
|
2251
|
+
.test-case-title { font-weight: 600; color: var(--text-color); font-size: 1em; }
|
|
2252
|
+
.test-case-browser { font-size: 0.9em; color: var(--text-color-secondary); }
|
|
2253
|
+
.test-case-meta { display: flex; align-items: center; gap: 12px; font-size: 0.9em; color: var(--text-color-secondary); flex-shrink: 0; }
|
|
2254
|
+
.test-duration { background-color: var(--light-gray-color); padding: 4px 10px; border-radius: 12px; font-size: 0.9em;}
|
|
2255
|
+
.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); }
|
|
2256
|
+
.status-badge.status-passed { background-color: var(--success-color); }
|
|
2257
|
+
.status-badge.status-failed { background-color: var(--danger-color); }
|
|
2258
|
+
.status-badge.status-skipped { background-color: var(--warning-color); }
|
|
2259
|
+
.status-badge.status-unknown { background-color: var(--dark-gray-color); }
|
|
2260
|
+
.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; }
|
|
2261
|
+
.test-case-content { display: none; padding: 20px; border-top: 1px solid var(--border-color); background-color: var(--light-gray-color); }
|
|
2262
|
+
.test-case-content h4 { margin-top: 22px; margin-bottom: 14px; font-size: 1.15em; color: var(--primary-color); }
|
|
2263
|
+
.test-case-content p { margin-bottom: 10px; font-size: 1em; }
|
|
2264
|
+
.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; }
|
|
2265
|
+
.test-error-summary h4 { color: var(--danger-color); margin-top:0;}
|
|
2266
|
+
.test-error-summary pre { white-space: pre-wrap; word-break: break-all; color: var(--danger-color); font-size: 0.95em;}
|
|
2267
|
+
.steps-list { margin: 18px 0; }
|
|
2268
|
+
@supports (content-visibility: auto) {
|
|
2269
|
+
.tab-content,
|
|
2270
|
+
#test-runs .test-case,
|
|
2271
|
+
.attachments-section,
|
|
2272
|
+
.test-history-card,
|
|
2273
|
+
.trend-chart,
|
|
2274
|
+
.suite-card {
|
|
2275
|
+
content-visibility: auto;
|
|
2276
|
+
contain-intrinsic-size: 1px 600px;
|
|
1661
2277
|
}
|
|
1662
|
-
|
|
1663
|
-
.view-trace:hover {
|
|
1664
|
-
background: #2c5282;
|
|
1665
2278
|
}
|
|
1666
|
-
|
|
1667
|
-
.
|
|
1668
|
-
|
|
1669
|
-
|
|
2279
|
+
.test-case,
|
|
2280
|
+
.test-history-card,
|
|
2281
|
+
.suite-card,
|
|
2282
|
+
.attachments-section {
|
|
2283
|
+
contain: content;
|
|
1670
2284
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
2285
|
+
.attachments-grid .attachment-item img.lazy-load-image {
|
|
2286
|
+
width: 100%;
|
|
2287
|
+
aspect-ratio: 4 / 3;
|
|
2288
|
+
object-fit: cover;
|
|
1674
2289
|
}
|
|
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 */
|
|
2290
|
+
.attachments-grid .attachment-item.video-item {
|
|
2291
|
+
aspect-ratio: 16 / 9;
|
|
1680
2292
|
}
|
|
1681
2293
|
|
|
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
|
-
|
|
2294
|
+
.step-item { margin-bottom: 8px; padding-left: calc(var(--depth, 0) * 28px); }
|
|
2295
|
+
.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; }
|
|
2296
|
+
.step-header:hover { background-color: var(--light-gray-color); border-color: var(--medium-gray-color); box-shadow: var(--box-shadow-inset); }
|
|
2297
|
+
.step-icon { margin-right: 12px; width: 20px; text-align: center; font-size: 1.1em; }
|
|
2298
|
+
.step-title { flex: 1; font-size: 1em; }
|
|
2299
|
+
.step-duration { color: var(--dark-gray-color); font-size: 0.9em; }
|
|
2300
|
+
.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); }
|
|
2301
|
+
.step-info { margin-bottom: 8px; }
|
|
2302
|
+
.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); }
|
|
2303
|
+
.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; }
|
|
2304
|
+
.step-hook { background-color: rgba(96,165,250,0.1); border-left: 3px solid var(--info-color) !important; }
|
|
2305
|
+
.step-hook .step-title { font-style: italic; color: var(--info-color)}
|
|
2306
|
+
.nested-steps { margin-top: 12px; }
|
|
2307
|
+
.attachments-section { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--light-gray-color); }
|
|
2308
|
+
.attachments-section h4 { margin-top: 0; margin-bottom: 20px; font-size: 1.1em; color: var(--text-color); }
|
|
2309
|
+
.attachments-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 22px; }
|
|
2310
|
+
.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; }
|
|
2311
|
+
.attachment-item:hover { transform: translateY(-4px); box-shadow: var(--box-shadow); }
|
|
2312
|
+
.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; }
|
|
2313
|
+
.attachment-info { padding: 12px; margin-top: auto; background-color: var(--light-gray-color);}
|
|
2314
|
+
.attachment-item a:hover img { opacity: 0.85; }
|
|
2315
|
+
.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); }
|
|
2316
|
+
.video-item a, .trace-item a { display: block; margin-bottom: 8px; color: var(--primary-color); text-decoration: none; font-weight: 500; }
|
|
2317
|
+
.video-item a:hover, .trace-item a:hover { text-decoration: underline; }
|
|
2318
|
+
.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;}
|
|
2319
|
+
.trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
|
|
2320
|
+
.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;}
|
|
2321
|
+
.test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
|
|
2322
|
+
.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; }
|
|
2323
|
+
.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); }
|
|
2324
|
+
.test-history-header h3 { margin: 0; font-size: 1.15em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2325
|
+
.test-history-header p { font-weight: 500 }
|
|
2326
|
+
.test-history-trend { margin-bottom: 20px; min-height: 110px; }
|
|
2327
|
+
.test-history-trend div[id^="testHistoryChart-"] { display: block; margin: 0 auto; max-width:100%; height: 100px; width: 320px; }
|
|
2328
|
+
.test-history-details-collapsible summary { cursor: pointer; font-size: 1em; color: var(--primary-color); margin-bottom: 10px; font-weight:500; }
|
|
2329
|
+
.test-history-details-collapsible summary:hover {text-decoration: underline;}
|
|
2330
|
+
.test-history-details table { width: 100%; border-collapse: collapse; font-size: 0.95em; }
|
|
2331
|
+
.test-history-details th, .test-history-details td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--light-gray-color); }
|
|
2332
|
+
.test-history-details th { background-color: var(--light-gray-color); font-weight: 600; }
|
|
2333
|
+
.status-badge-small { padding: 3px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase; display: inline-block; }
|
|
2334
|
+
.status-badge-small.status-passed { background-color: var(--success-color); }
|
|
2335
|
+
.status-badge-small.status-failed { background-color: var(--danger-color); }
|
|
2336
|
+
.status-badge-small.status-skipped { background-color: var(--warning-color); }
|
|
2337
|
+
.status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
|
|
2338
|
+
.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); }
|
|
2339
|
+
.no-data-chart {font-size: 0.95em; padding: 18px;}
|
|
2340
|
+
.ai-failure-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 22px; }
|
|
2341
|
+
.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; }
|
|
2342
|
+
.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; }
|
|
2343
|
+
.ai-failure-card-header h3 { margin: 0; font-size: 1.1em; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2344
|
+
.ai-failure-card-body { padding: 20px; }
|
|
2345
|
+
.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; }
|
|
2346
|
+
.ai-fix-btn:hover { background-color: var(--accent-color); transform: translateY(-2px); }
|
|
2347
|
+
.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; }
|
|
2348
|
+
.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; }
|
|
2349
|
+
.ai-modal-header { padding: 18px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
|
|
2350
|
+
.ai-modal-header h3 { margin: 0; font-size: 1.25em; }
|
|
2351
|
+
.ai-modal-close { font-size: 2rem; font-weight: 300; cursor: pointer; color: var(--dark-gray-color); line-height: 1; transition: color 0.2s; }
|
|
2352
|
+
.ai-modal-close:hover { color: var(--danger-color); }
|
|
2353
|
+
.ai-modal-body { padding: 25px; overflow-y: auto; }
|
|
2354
|
+
.ai-modal-body h4 { margin-top: 18px; margin-bottom: 10px; font-size: 1.1em; color: var(--primary-color); }
|
|
2355
|
+
.ai-modal-body p { margin-bottom: 15px; }
|
|
2356
|
+
.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; }
|
|
2357
|
+
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
2358
|
+
.trace-preview { padding: 1rem; text-align: center; background: var(--light-gray-color); border-bottom: 1px solid var(--border-color); }
|
|
2359
|
+
.trace-icon { font-size: 2rem; display: block; margin-bottom: 0.5rem; }
|
|
2360
|
+
.trace-name { word-break: break-word; font-size: 0.9rem; }
|
|
2361
|
+
.trace-actions { display: flex; gap: 0.5rem; }
|
|
2362
|
+
.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; }
|
|
2363
|
+
.view-trace { background: var(--primary-color); color: white; }
|
|
2364
|
+
.view-trace:hover { background: var(--accent-color); }
|
|
2365
|
+
.download-trace { background: var(--medium-gray-color); color: var(--text-color); }
|
|
2366
|
+
.download-trace:hover { background: var(--dark-gray-color); }
|
|
2367
|
+
.filters button.clear-filters-btn { background-color: var(--medium-gray-color); color: var(--text-color); }
|
|
2368
|
+
.filters button.clear-filters-btn:hover { background-color: var(--dark-gray-color); color: #fff; }
|
|
2369
|
+
.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;}
|
|
2370
|
+
.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; }
|
|
2371
|
+
.stat-item { text-align: center; color: white; }
|
|
2372
|
+
.stat-number { display: block; font-size: 2em; font-weight: 700; line-height: 1;}
|
|
2373
|
+
.stat-label { font-size: 0.9em; opacity: 0.9; font-weight: 500;}
|
|
2374
|
+
.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;}
|
|
2375
|
+
.compact-failure-list { display: flex; flex-direction: column; gap: 15px; }
|
|
2376
|
+
.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;}
|
|
2377
|
+
.compact-failure-item:hover { transform: translateY(-2px); box-shadow: var(--box-shadow); }
|
|
2378
|
+
.failure-header { display: flex; justify-content: space-between; align-items: center; padding: 18px 20px; gap: 15px;}
|
|
2379
|
+
.failure-main-info { flex: 1; min-width: 0; }
|
|
2380
|
+
.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;}
|
|
2381
|
+
.failure-meta { display: flex; gap: 12px; align-items: center;}
|
|
2382
|
+
.browser-indicator, .duration-indicator { font-size: 0.85em; padding: 3px 8px; border-radius: 12px; font-weight: 500;}
|
|
2383
|
+
.browser-indicator { background: var(--info-color); color: white; }
|
|
2384
|
+
#load-more-tests { font-size: 16px; padding: 4px; background-color: var(--light-gray-color); border-radius: 4px; color: var(--text-color); }
|
|
2385
|
+
.duration-indicator { background: var(--medium-gray-color); color: var(--text-color); }
|
|
2386
|
+
.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;}
|
|
2387
|
+
.compact-ai-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(55, 65, 81, 0.4); }
|
|
2388
|
+
.ai-text { font-size: 0.95em; }
|
|
2389
|
+
.failure-error-preview { padding: 0 20px 18px 20px; border-top: 1px solid var(--light-gray-color);}
|
|
2390
|
+
.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;}
|
|
2391
|
+
.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;}
|
|
2392
|
+
.expand-error-btn:hover { background: var(--light-gray-color); border-color: var(--medium-gray-color); }
|
|
2393
|
+
.expand-icon { transition: transform 0.2s ease; font-size: 0.8em;}
|
|
2394
|
+
.expand-error-btn.expanded .expand-icon { transform: rotate(180deg); }
|
|
2395
|
+
.full-error-details { padding: 0 20px 20px 20px; border-top: 1px solid var(--light-gray-color); margin-top: 0;}
|
|
2396
|
+
.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;}
|
|
2397
|
+
@media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
|
|
2398
|
+
@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; } }
|
|
2399
|
+
@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; } }
|
|
2400
|
+
@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; } }
|
|
2401
|
+
.trace-actions a { text-decoration: none; font-weight: 500; font-size: 0.9em; }
|
|
2402
|
+
.generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
|
|
2403
|
+
.attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
|
|
2404
|
+
.attachment-caption { display: flex; flex-direction: column; align-items: center; justify-content: center; flex-grow: 1; }
|
|
2405
|
+
.attachment-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
|
2406
|
+
.attachment-type { font-size: 0.8rem; color: var(--text-color-secondary); }
|
|
2407
|
+
.footer-text { color: white }
|
|
2408
|
+
</style>
|
|
1730
2409
|
</head>
|
|
1731
2410
|
<body>
|
|
1732
2411
|
<div class="container">
|
|
1733
2412
|
<header class="header">
|
|
1734
2413
|
<div class="header-title">
|
|
1735
|
-
<img id="report-logo" src="
|
|
2414
|
+
<img id="report-logo" src="https://i.postimg.cc/v817w4sg/logo.png" alt="Report Logo">
|
|
1736
2415
|
<h1>Playwright Pulse Report</h1>
|
|
1737
2416
|
</div>
|
|
1738
|
-
<div class="run-info">
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
runSummary.duration
|
|
1744
|
-
)}
|
|
1745
|
-
</div>
|
|
2417
|
+
<div class="run-info"><strong>Run Date:</strong> ${formatDate(
|
|
2418
|
+
runSummary.timestamp
|
|
2419
|
+
)}<br><strong>Total Duration:</strong> ${formatDuration(
|
|
2420
|
+
runSummary.duration
|
|
2421
|
+
)}</div>
|
|
1746
2422
|
</header>
|
|
1747
|
-
|
|
1748
2423
|
<div class="tabs">
|
|
1749
2424
|
<button class="tab-button active" data-tab="dashboard">Dashboard</button>
|
|
1750
2425
|
<button class="tab-button" data-tab="test-runs">Test Run Summary</button>
|
|
1751
2426
|
<button class="tab-button" data-tab="test-history">Test History</button>
|
|
1752
|
-
<button class="tab-button" data-tab="
|
|
2427
|
+
<button class="tab-button" data-tab="ai-failure-analyzer">AI Failure Analyzer</button>
|
|
1753
2428
|
</div>
|
|
1754
|
-
|
|
1755
2429
|
<div id="dashboard" class="tab-content active">
|
|
1756
2430
|
<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>
|
|
2431
|
+
<div class="summary-card"><h3>Total Tests</h3><div class="value">${
|
|
2432
|
+
runSummary.totalTests
|
|
2433
|
+
}</div></div>
|
|
2434
|
+
<div class="summary-card status-passed"><h3>Passed</h3><div class="value">${
|
|
2435
|
+
runSummary.passed
|
|
2436
|
+
}</div><div class="trend-percentage">${passPercentage}%</div></div>
|
|
2437
|
+
<div class="summary-card status-failed"><h3>Failed</h3><div class="value">${
|
|
2438
|
+
runSummary.failed
|
|
2439
|
+
}</div><div class="trend-percentage">${failPercentage}%</div></div>
|
|
2440
|
+
<div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
|
|
2441
|
+
runSummary.skipped || 0
|
|
2442
|
+
}</div><div class="trend-percentage">${skipPercentage}%</div></div>
|
|
2443
|
+
<div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
|
|
2444
|
+
<div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
|
|
2445
|
+
runSummary.duration
|
|
2446
|
+
)}</div></div>
|
|
1784
2447
|
</div>
|
|
1785
2448
|
<div class="dashboard-bottom-row">
|
|
2449
|
+
<div style="display: grid; gap: 20px">
|
|
1786
2450
|
${generatePieChart(
|
|
1787
|
-
// Changed from generatePieChartD3
|
|
1788
2451
|
[
|
|
1789
2452
|
{ label: "Passed", value: runSummary.passed },
|
|
1790
2453
|
{ label: "Failed", value: runSummary.failed },
|
|
1791
2454
|
{ label: "Skipped", value: runSummary.skipped || 0 },
|
|
1792
2455
|
],
|
|
1793
|
-
400,
|
|
1794
|
-
390
|
|
2456
|
+
400,
|
|
2457
|
+
390
|
|
1795
2458
|
)}
|
|
2459
|
+
${
|
|
2460
|
+
runSummary.environment &&
|
|
2461
|
+
Object.keys(runSummary.environment).length > 0
|
|
2462
|
+
? generateEnvironmentDashboard(runSummary.environment)
|
|
2463
|
+
: '<div class="no-data">Environment data not available.</div>'
|
|
2464
|
+
}
|
|
2465
|
+
</div>
|
|
1796
2466
|
${generateSuitesWidget(suitesData)}
|
|
1797
2467
|
</div>
|
|
1798
2468
|
</div>
|
|
1799
|
-
|
|
1800
2469
|
<div id="test-runs" class="tab-content">
|
|
1801
2470
|
<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()}
|
|
2471
|
+
<input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
|
|
2472
|
+
<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>
|
|
2473
|
+
<select id="filter-browser"><option value="">All Browsers</option>${Array.from(
|
|
2474
|
+
new Set(
|
|
2475
|
+
(results || []).map((test) => test.browser || "unknown")
|
|
2476
|
+
)
|
|
2477
|
+
)
|
|
2478
|
+
.map(
|
|
2479
|
+
(browser) =>
|
|
2480
|
+
`<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
|
|
2481
|
+
browser
|
|
2482
|
+
)}</option>`
|
|
2483
|
+
)
|
|
2484
|
+
.join("")}</select>
|
|
2485
|
+
<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
2486
|
</div>
|
|
2487
|
+
<div class="test-cases-list">${generateTestCasesHTML(
|
|
2488
|
+
results.slice(0, 50),
|
|
2489
|
+
0
|
|
2490
|
+
)}</div>
|
|
2491
|
+
${
|
|
2492
|
+
results.length > 50
|
|
2493
|
+
? `<div class="load-more-wrapper"><button id="load-more-tests">Load more</button></div><script type="application/json" id="remaining-tests-b64">${Buffer.from(
|
|
2494
|
+
generateTestCasesHTML(results.slice(50), 50),
|
|
2495
|
+
"utf8"
|
|
2496
|
+
).toString("base64")}</script>`
|
|
2497
|
+
: ``
|
|
2498
|
+
}
|
|
1830
2499
|
</div>
|
|
1831
|
-
|
|
1832
2500
|
<div id="test-history" class="tab-content">
|
|
1833
2501
|
<h2 class="tab-main-title">Execution Trends</h2>
|
|
1834
2502
|
<div class="trend-charts-row">
|
|
1835
|
-
<div class="trend-chart">
|
|
1836
|
-
<h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
|
|
2503
|
+
<div class="trend-chart"><h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
|
|
1837
2504
|
${
|
|
1838
2505
|
trendData && trendData.overall && trendData.overall.length > 0
|
|
1839
2506
|
? generateTestTrendsChart(trendData)
|
|
1840
2507
|
: '<div class="no-data">Overall trend data not available for test counts.</div>'
|
|
1841
2508
|
}
|
|
1842
2509
|
</div>
|
|
1843
|
-
<div class="trend-chart">
|
|
1844
|
-
<h3 class="chart-title-header">Execution Duration Trends</h3>
|
|
2510
|
+
<div class="trend-chart"><h3 class="chart-title-header">Execution Duration Trends</h3>
|
|
1845
2511
|
${
|
|
1846
2512
|
trendData && trendData.overall && trendData.overall.length > 0
|
|
1847
2513
|
? generateDurationTrendChart(trendData)
|
|
1848
2514
|
: '<div class="no-data">Overall trend data not available for durations.</div>'
|
|
1849
2515
|
}
|
|
2516
|
+
</div>
|
|
2517
|
+
</div>
|
|
2518
|
+
<h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
|
|
2519
|
+
<div class="trend-charts-row">
|
|
2520
|
+
<div class="trend-chart">
|
|
2521
|
+
${generateWorkerDistributionChart(results)}
|
|
1850
2522
|
</div>
|
|
1851
2523
|
</div>
|
|
1852
2524
|
<h2 class="tab-main-title">Individual Test History</h2>
|
|
@@ -1858,69 +2530,162 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1858
2530
|
: '<div class="no-data">Individual test history data not available.</div>'
|
|
1859
2531
|
}
|
|
1860
2532
|
</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>
|
|
2533
|
+
<div id="ai-failure-analyzer" class="tab-content">
|
|
2534
|
+
${generateAIFailureAnalyzerTab(results)}
|
|
1871
2535
|
</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>
|
|
2536
|
+
<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;">
|
|
2537
|
+
<div style="display: inline-flex; align-items: center; gap: 0.5rem; color: #333; font-size: 0.9rem; font-weight: 600; letter-spacing: 0.5px;">
|
|
2538
|
+
<span class="footer-text">Created by</span>
|
|
2539
|
+
<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>
|
|
2540
|
+
</div>
|
|
2541
|
+
<div style="margin-top: 0.5rem; font-size: 0.75rem; color: #666;">Crafted with precision</div>
|
|
2542
|
+
</footer>
|
|
1912
2543
|
</div>
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
2544
|
<script>
|
|
1916
2545
|
// 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";
|
|
2546
|
+
if (typeof formatDuration === 'undefined') {
|
|
2547
|
+
function formatDuration(ms) {
|
|
2548
|
+
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
2549
|
+
return (ms / 1000).toFixed(1) + "s";
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
function copyLogContent(elementId, button) {
|
|
2553
|
+
const logElement = document.getElementById(elementId);
|
|
2554
|
+
if (!logElement) {
|
|
2555
|
+
console.error('Could not find log element with ID:', elementId);
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
navigator.clipboard.writeText(logElement.innerText).then(() => {
|
|
2559
|
+
button.textContent = 'Copied!';
|
|
2560
|
+
setTimeout(() => { button.textContent = 'Copy'; }, 2000);
|
|
2561
|
+
}).catch(err => {
|
|
2562
|
+
console.error('Failed to copy log content:', err);
|
|
2563
|
+
button.textContent = 'Failed';
|
|
2564
|
+
setTimeout(() => { button.textContent = 'Copy'; }, 2000);
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
function getAIFix(button) {
|
|
2569
|
+
const modal = document.getElementById('ai-fix-modal');
|
|
2570
|
+
const modalContent = document.getElementById('ai-fix-modal-content');
|
|
2571
|
+
const modalTitle = document.getElementById('ai-fix-modal-title');
|
|
2572
|
+
|
|
2573
|
+
modal.style.display = 'flex';
|
|
2574
|
+
document.body.style.setProperty('overflow', 'hidden', 'important');
|
|
2575
|
+
modalTitle.textContent = 'Analyzing...';
|
|
2576
|
+
modalContent.innerHTML = '<div class="ai-loader"></div>';
|
|
2577
|
+
|
|
2578
|
+
try {
|
|
2579
|
+
const testJson = button.dataset.testJson;
|
|
2580
|
+
const test = JSON.parse(atob(testJson));
|
|
2581
|
+
|
|
2582
|
+
const testName = test.name || 'Unknown Test';
|
|
2583
|
+
const failureLogsAndErrors = [
|
|
2584
|
+
'Error Message:',
|
|
2585
|
+
test.errorMessage || 'Not available.',
|
|
2586
|
+
'\\n\\n--- stdout ---',
|
|
2587
|
+
(test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
|
|
2588
|
+
'\\n\\n--- stderr ---',
|
|
2589
|
+
(test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
|
|
2590
|
+
].join('\\n');
|
|
2591
|
+
const codeSnippet = test.snippet || '';
|
|
2592
|
+
|
|
2593
|
+
const shortTestName = testName.split(' > ').pop();
|
|
2594
|
+
modalTitle.textContent = \`Analysis for: \${shortTestName}\`;
|
|
2595
|
+
|
|
2596
|
+
const apiUrl = 'https://ai-test-analyser.netlify.app/api/analyze';
|
|
2597
|
+
fetch(apiUrl, {
|
|
2598
|
+
method: 'POST',
|
|
2599
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2600
|
+
body: JSON.stringify({
|
|
2601
|
+
testName: testName,
|
|
2602
|
+
failureLogsAndErrors: failureLogsAndErrors,
|
|
2603
|
+
codeSnippet: codeSnippet,
|
|
2604
|
+
}),
|
|
2605
|
+
})
|
|
2606
|
+
.then(response => {
|
|
2607
|
+
if (!response.ok) {
|
|
2608
|
+
return response.text().then(text => {
|
|
2609
|
+
throw new Error(\`API request failed with status \${response.status}: \${text || response.statusText}\`);
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
return response.text();
|
|
2613
|
+
})
|
|
2614
|
+
.then(text => {
|
|
2615
|
+
if (!text) {
|
|
2616
|
+
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.");
|
|
2617
|
+
}
|
|
2618
|
+
try {
|
|
2619
|
+
return JSON.parse(text);
|
|
2620
|
+
} catch (e) {
|
|
2621
|
+
console.error("Failed to parse JSON:", text);
|
|
2622
|
+
throw new Error(\`The AI analyzer returned an invalid response. \${e.message}\`);
|
|
2623
|
+
}
|
|
2624
|
+
})
|
|
2625
|
+
.then(data => {
|
|
2626
|
+
const escapeHtml = (unsafe) => {
|
|
2627
|
+
if (typeof unsafe !== 'string') return '';
|
|
2628
|
+
return unsafe
|
|
2629
|
+
.replace(/&/g, "&")
|
|
2630
|
+
.replace(/</g, "<")
|
|
2631
|
+
.replace(/>/g, ">")
|
|
2632
|
+
.replace(/"/g, """)
|
|
2633
|
+
.replace(/'/g, "'");
|
|
2634
|
+
};
|
|
2635
|
+
|
|
2636
|
+
const analysisHtml = \`<h4>Analysis</h4><p>\${escapeHtml(data.rootCause) || 'No analysis provided.'}</p>\`;
|
|
2637
|
+
|
|
2638
|
+
let suggestionsHtml = '<h4>Suggestions</h4>';
|
|
2639
|
+
if (data.suggestedFixes && data.suggestedFixes.length > 0) {
|
|
2640
|
+
suggestionsHtml += '<div class="suggestions-list" style="margin-top: 15px;">';
|
|
2641
|
+
data.suggestedFixes.forEach(fix => {
|
|
2642
|
+
suggestionsHtml += \`
|
|
2643
|
+
<div class="suggestion-item" style="margin-bottom: 22px; border-left: 3px solid var(--accent-color-alt); padding-left: 15px;">
|
|
2644
|
+
<p style="margin: 0 0 8px 0; font-weight: 500;">\${escapeHtml(fix.description)}</p>
|
|
2645
|
+
\${fix.codeSnippet ? \`<div class="code-section"><pre><code>\${escapeHtml(fix.codeSnippet)}</code></pre></div>\` : ''}
|
|
2646
|
+
</div>
|
|
2647
|
+
\`;
|
|
2648
|
+
});
|
|
2649
|
+
suggestionsHtml += '</div>';
|
|
2650
|
+
} else {
|
|
2651
|
+
suggestionsHtml += \`<div class="code-section"><pre><code>No suggestion provided.</code></pre></div>\`;
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
modalContent.innerHTML = analysisHtml + suggestionsHtml;
|
|
2655
|
+
})
|
|
2656
|
+
.catch(err => {
|
|
2657
|
+
console.error('AI Fix Error:', err);
|
|
2658
|
+
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>\`;
|
|
2659
|
+
});
|
|
2660
|
+
|
|
2661
|
+
} catch (e) {
|
|
2662
|
+
console.error('Error processing test data for AI Fix:', e);
|
|
2663
|
+
modalTitle.textContent = 'Error';
|
|
2664
|
+
modalContent.innerHTML = \`<div class="test-error-summary">Could not process test data. Is it formatted correctly?</div>\`;
|
|
1921
2665
|
}
|
|
1922
2666
|
}
|
|
1923
2667
|
|
|
2668
|
+
function closeAiModal() {
|
|
2669
|
+
const modal = document.getElementById('ai-fix-modal');
|
|
2670
|
+
if(modal) modal.style.display = 'none';
|
|
2671
|
+
document.body.style.setProperty('overflow', '', 'important');
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
function toggleErrorDetails(button) {
|
|
2675
|
+
const errorDetails = button.closest('.compact-failure-item').querySelector('.full-error-details');
|
|
2676
|
+
const expandText = button.querySelector('.expand-text');
|
|
2677
|
+
|
|
2678
|
+
if (errorDetails.style.display === 'none' || !errorDetails.style.display) {
|
|
2679
|
+
errorDetails.style.display = 'block';
|
|
2680
|
+
expandText.textContent = 'Hide Full Error';
|
|
2681
|
+
button.classList.add('expanded');
|
|
2682
|
+
} else {
|
|
2683
|
+
errorDetails.style.display = 'none';
|
|
2684
|
+
expandText.textContent = 'Show Full Error';
|
|
2685
|
+
button.classList.remove('expanded');
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
|
|
1924
2689
|
function initializeReportInteractivity() {
|
|
1925
2690
|
const tabButtons = document.querySelectorAll('.tab-button');
|
|
1926
2691
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
@@ -1931,82 +2696,100 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1931
2696
|
button.classList.add('active');
|
|
1932
2697
|
const tabId = button.getAttribute('data-tab');
|
|
1933
2698
|
const activeContent = document.getElementById(tabId);
|
|
1934
|
-
if (activeContent)
|
|
2699
|
+
if (activeContent) {
|
|
2700
|
+
activeContent.classList.add('active');
|
|
2701
|
+
}
|
|
1935
2702
|
});
|
|
1936
2703
|
});
|
|
1937
|
-
|
|
1938
2704
|
// --- Test Run Summary Filters ---
|
|
1939
2705
|
const nameFilter = document.getElementById('filter-name');
|
|
2706
|
+
function ensureAllTestsAppended() {
|
|
2707
|
+
const node = document.getElementById('remaining-tests-b64');
|
|
2708
|
+
const loadMoreBtn = document.getElementById('load-more-tests');
|
|
2709
|
+
if (!node) return;
|
|
2710
|
+
const b64 = (node.textContent || '').trim();
|
|
2711
|
+
function b64ToUtf8(b64Str) {
|
|
2712
|
+
try { return decodeURIComponent(escape(window.atob(b64Str))); }
|
|
2713
|
+
catch (e) { return window.atob(b64Str); }
|
|
2714
|
+
}
|
|
2715
|
+
const html = b64ToUtf8(b64);
|
|
2716
|
+
const container = document.querySelector('#test-runs .test-cases-list');
|
|
2717
|
+
if (container) container.insertAdjacentHTML('beforeend', html);
|
|
2718
|
+
if (loadMoreBtn) loadMoreBtn.remove();
|
|
2719
|
+
node.remove();
|
|
2720
|
+
}
|
|
2721
|
+
const loadMoreBtn = document.getElementById('load-more-tests');
|
|
2722
|
+
if (loadMoreBtn) {
|
|
2723
|
+
loadMoreBtn.addEventListener('click', () => {
|
|
2724
|
+
const node = document.getElementById('remaining-tests-b64');
|
|
2725
|
+
if (!node) return;
|
|
2726
|
+
const b64 = (node.textContent || '').trim();
|
|
2727
|
+
function b64ToUtf8(b64Str) {
|
|
2728
|
+
try { return decodeURIComponent(escape(window.atob(b64Str))); }
|
|
2729
|
+
catch (e) { return window.atob(b64Str); }
|
|
2730
|
+
}
|
|
2731
|
+
const html = b64ToUtf8(b64);
|
|
2732
|
+
const container = document.querySelector('#test-runs .test-cases-list');
|
|
2733
|
+
if (container) container.insertAdjacentHTML('beforeend', html);
|
|
2734
|
+
loadMoreBtn.remove();
|
|
2735
|
+
node.remove();
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
|
|
2740
|
+
|
|
1940
2741
|
const statusFilter = document.getElementById('filter-status');
|
|
1941
2742
|
const browserFilter = document.getElementById('filter-browser');
|
|
1942
|
-
const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters');
|
|
1943
|
-
|
|
1944
|
-
|
|
2743
|
+
const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters');
|
|
2744
|
+
function filterTestCases() {
|
|
2745
|
+
ensureAllTestsAppended();
|
|
1945
2746
|
const nameValue = nameFilter ? nameFilter.value.toLowerCase() : "";
|
|
1946
2747
|
const statusValue = statusFilter ? statusFilter.value : "";
|
|
1947
2748
|
const browserValue = browserFilter ? browserFilter.value : "";
|
|
1948
|
-
|
|
1949
2749
|
document.querySelectorAll('#test-runs .test-case').forEach(testCaseElement => {
|
|
1950
2750
|
const titleElement = testCaseElement.querySelector('.test-case-title');
|
|
1951
2751
|
const fullTestName = titleElement ? titleElement.getAttribute('title').toLowerCase() : "";
|
|
1952
2752
|
const status = testCaseElement.getAttribute('data-status');
|
|
1953
2753
|
const browser = testCaseElement.getAttribute('data-browser');
|
|
1954
|
-
|
|
1955
2754
|
const nameMatch = fullTestName.includes(nameValue);
|
|
1956
2755
|
const statusMatch = !statusValue || status === statusValue;
|
|
1957
2756
|
const browserMatch = !browserValue || browser === browserValue;
|
|
1958
|
-
|
|
1959
2757
|
testCaseElement.style.display = (nameMatch && statusMatch && browserMatch) ? '' : 'none';
|
|
1960
2758
|
});
|
|
1961
2759
|
}
|
|
1962
2760
|
if(nameFilter) nameFilter.addEventListener('input', filterTestCases);
|
|
1963
2761
|
if(statusFilter) statusFilter.addEventListener('change', filterTestCases);
|
|
1964
2762
|
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
|
-
|
|
2763
|
+
if(clearRunSummaryFiltersBtn) clearRunSummaryFiltersBtn.addEventListener('click', () => {
|
|
2764
|
+
ensureAllTestsAppended();
|
|
2765
|
+
if(nameFilter) nameFilter.value = '';
|
|
2766
|
+
if(statusFilter) statusFilter.value = '';
|
|
2767
|
+
if(browserFilter) browserFilter.value = '';
|
|
2768
|
+
filterTestCases();
|
|
2769
|
+
});
|
|
1976
2770
|
// --- Test History Filters ---
|
|
1977
2771
|
const historyNameFilter = document.getElementById('history-filter-name');
|
|
1978
2772
|
const historyStatusFilter = document.getElementById('history-filter-status');
|
|
1979
|
-
const clearHistoryFiltersBtn = document.getElementById('clear-history-filters');
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
function filterTestHistoryCards() {
|
|
2773
|
+
const clearHistoryFiltersBtn = document.getElementById('clear-history-filters');
|
|
2774
|
+
function filterTestHistoryCards() {
|
|
1983
2775
|
const nameValue = historyNameFilter ? historyNameFilter.value.toLowerCase() : "";
|
|
1984
2776
|
const statusValue = historyStatusFilter ? historyStatusFilter.value : "";
|
|
1985
|
-
|
|
1986
2777
|
document.querySelectorAll('.test-history-card').forEach(card => {
|
|
1987
2778
|
const testTitle = card.getAttribute('data-test-name').toLowerCase();
|
|
1988
2779
|
const latestStatus = card.getAttribute('data-latest-status');
|
|
1989
|
-
|
|
1990
2780
|
const nameMatch = testTitle.includes(nameValue);
|
|
1991
2781
|
const statusMatch = !statusValue || latestStatus === statusValue;
|
|
1992
|
-
|
|
1993
2782
|
card.style.display = (nameMatch && statusMatch) ? '' : 'none';
|
|
1994
2783
|
});
|
|
1995
2784
|
}
|
|
1996
2785
|
if(historyNameFilter) historyNameFilter.addEventListener('input', filterTestHistoryCards);
|
|
1997
2786
|
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) {
|
|
2787
|
+
if(clearHistoryFiltersBtn) clearHistoryFiltersBtn.addEventListener('click', () => {
|
|
2788
|
+
if(historyNameFilter) historyNameFilter.value = ''; if(historyStatusFilter) historyStatusFilter.value = '';
|
|
2789
|
+
filterTestHistoryCards();
|
|
2790
|
+
});
|
|
2791
|
+
// --- Expand/Collapse and Toggle Details Logic ---
|
|
2792
|
+
function toggleElementDetails(headerElement, contentSelector) {
|
|
2010
2793
|
let contentElement;
|
|
2011
2794
|
if (headerElement.classList.contains('test-case-header')) {
|
|
2012
2795
|
contentElement = headerElement.parentElement.querySelector('.test-case-content');
|
|
@@ -2016,41 +2799,179 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2016
2799
|
contentElement = null;
|
|
2017
2800
|
}
|
|
2018
2801
|
}
|
|
2019
|
-
|
|
2020
2802
|
if (contentElement) {
|
|
2021
2803
|
const isExpanded = contentElement.style.display === 'block';
|
|
2022
2804
|
contentElement.style.display = isExpanded ? 'none' : 'block';
|
|
2023
2805
|
headerElement.setAttribute('aria-expanded', String(!isExpanded));
|
|
2024
2806
|
}
|
|
2025
2807
|
}
|
|
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
2808
|
const expandAllBtn = document.getElementById('expand-all-tests');
|
|
2035
2809
|
const collapseAllBtn = document.getElementById('collapse-all-tests');
|
|
2036
|
-
|
|
2037
2810
|
function setAllTestRunDetailsVisibility(displayMode, ariaState) {
|
|
2038
2811
|
document.querySelectorAll('#test-runs .test-case-content').forEach(el => el.style.display = displayMode);
|
|
2039
2812
|
document.querySelectorAll('#test-runs .step-details').forEach(el => el.style.display = displayMode);
|
|
2040
2813
|
document.querySelectorAll('#test-runs .test-case-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
|
|
2041
2814
|
document.querySelectorAll('#test-runs .step-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
|
|
2042
2815
|
}
|
|
2043
|
-
|
|
2044
2816
|
if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
|
|
2045
2817
|
if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
|
|
2818
|
+
document.addEventListener('click', (e) => {
|
|
2819
|
+
const inHighcharts = e.target && e.target.closest && e.target.closest('.highcharts-container');
|
|
2820
|
+
if (inHighcharts) {
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
const header = e.target.closest('#test-runs .test-case-header');
|
|
2824
|
+
if (header) {
|
|
2825
|
+
let contentElement = header.parentElement.querySelector('.test-case-content');
|
|
2826
|
+
if (contentElement) {
|
|
2827
|
+
const isExpanded = contentElement.style.display === 'block';
|
|
2828
|
+
contentElement.style.display = isExpanded ? 'none' : 'block';
|
|
2829
|
+
header.setAttribute('aria-expanded', String(!isExpanded));
|
|
2830
|
+
}
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
const stepHeader = e.target.closest('#test-runs .step-header');
|
|
2834
|
+
if (stepHeader) {
|
|
2835
|
+
let details = stepHeader.nextElementSibling;
|
|
2836
|
+
if (details && details.matches('.step-details')) {
|
|
2837
|
+
const isExpanded = details.style.display === 'block';
|
|
2838
|
+
details.style.display = isExpanded ? 'none' : 'block';
|
|
2839
|
+
stepHeader.setAttribute('aria-expanded', String(!isExpanded));
|
|
2840
|
+
}
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
const img = e.target.closest('img.lazy-load-image');
|
|
2844
|
+
if (img && img.dataset && img.dataset.src) {
|
|
2845
|
+
if (e.preventDefault) e.preventDefault();
|
|
2846
|
+
img.src = img.dataset.src;
|
|
2847
|
+
img.removeAttribute('data-src');
|
|
2848
|
+
const parentLink = img.closest('a.lazy-load-attachment');
|
|
2849
|
+
if (parentLink && parentLink.dataset && parentLink.dataset.href) {
|
|
2850
|
+
parentLink.href = parentLink.dataset.href;
|
|
2851
|
+
parentLink.removeAttribute('data-href');
|
|
2852
|
+
}
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
const video = e.target.closest('video.lazy-load-video');
|
|
2856
|
+
if (video) {
|
|
2857
|
+
if (e.preventDefault) e.preventDefault();
|
|
2858
|
+
const s = video.querySelector('source');
|
|
2859
|
+
if (s && s.dataset && s.dataset.src && !s.src) {
|
|
2860
|
+
s.src = s.dataset.src;
|
|
2861
|
+
s.removeAttribute('data-src');
|
|
2862
|
+
video.load();
|
|
2863
|
+
} else if (video.dataset && video.dataset.src && !video.src) {
|
|
2864
|
+
video.src = video.dataset.src;
|
|
2865
|
+
video.removeAttribute('data-src');
|
|
2866
|
+
video.load();
|
|
2867
|
+
}
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
const a = e.target.closest('a.lazy-load-attachment');
|
|
2871
|
+
if (a && a.dataset && a.dataset.href) {
|
|
2872
|
+
e.preventDefault();
|
|
2873
|
+
a.href = a.dataset.href;
|
|
2874
|
+
a.removeAttribute('data-href');
|
|
2875
|
+
a.click();
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
});
|
|
2879
|
+
document.addEventListener('play', (e) => {
|
|
2880
|
+
const video = e.target && e.target.closest ? e.target.closest('video.lazy-load-video') : null;
|
|
2881
|
+
if (video) {
|
|
2882
|
+
const s = video.querySelector('source');
|
|
2883
|
+
if (s && s.dataset && s.dataset.src && !s.src) {
|
|
2884
|
+
s.src = s.dataset.src;
|
|
2885
|
+
s.removeAttribute('data-src');
|
|
2886
|
+
video.load();
|
|
2887
|
+
} else if (video.dataset && video.dataset.src && !video.src) {
|
|
2888
|
+
video.src = video.dataset.src;
|
|
2889
|
+
video.removeAttribute('data-src');
|
|
2890
|
+
video.load();
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
}, true);
|
|
2894
|
+
const lazyLoadElements = document.querySelectorAll('.lazy-load-chart, .lazy-load-iframe');
|
|
2895
|
+
if ('IntersectionObserver' in window) {
|
|
2896
|
+
let lazyObserver = new IntersectionObserver((entries, observer) => {
|
|
2897
|
+
entries.forEach(entry => {
|
|
2898
|
+
if (entry.isIntersecting) {
|
|
2899
|
+
const element = entry.target;
|
|
2900
|
+
if (element.classList.contains('lazy-load-iframe')) {
|
|
2901
|
+
if (element.dataset.src) {
|
|
2902
|
+
element.src = element.dataset.src;
|
|
2903
|
+
element.removeAttribute('data-src');
|
|
2904
|
+
}
|
|
2905
|
+
} else if (element.classList.contains('lazy-load-chart')) {
|
|
2906
|
+
const renderFunctionName = element.dataset.renderFunctionName;
|
|
2907
|
+
if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
|
|
2908
|
+
window[renderFunctionName]();
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
observer.unobserve(element);
|
|
2912
|
+
}
|
|
2913
|
+
});
|
|
2914
|
+
}, { rootMargin: "0px 0px 200px 0px" });
|
|
2915
|
+
lazyLoadElements.forEach(el => lazyObserver.observe(el));
|
|
2916
|
+
} else {
|
|
2917
|
+
lazyLoadElements.forEach(element => {
|
|
2918
|
+
if (element.classList.contains('lazy-load-iframe') && element.dataset.src) element.src = element.dataset.src;
|
|
2919
|
+
else if (element.classList.contains('lazy-load-chart')) { const renderFn = element.dataset.renderFunctionName; if (renderFn && window[renderFn]) window[renderFn](); }
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2046
2922
|
}
|
|
2047
2923
|
document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
|
|
2924
|
+
|
|
2925
|
+
function copyErrorToClipboard(button) {
|
|
2926
|
+
const errorContainer = button.closest('.test-error-summary');
|
|
2927
|
+
if (!errorContainer) {
|
|
2928
|
+
console.error("Could not find '.test-error-summary' container.");
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
let errorText;
|
|
2932
|
+
const stackTraceElement = errorContainer.querySelector('.stack-trace');
|
|
2933
|
+
if (stackTraceElement) {
|
|
2934
|
+
errorText = stackTraceElement.textContent;
|
|
2935
|
+
} else {
|
|
2936
|
+
const clonedContainer = errorContainer.cloneNode(true);
|
|
2937
|
+
const buttonInClone = clonedContainer.querySelector('button');
|
|
2938
|
+
if (buttonInClone) buttonInClone.remove();
|
|
2939
|
+
errorText = clonedContainer.textContent;
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
if (!errorText) {
|
|
2943
|
+
button.textContent = 'Nothing to copy';
|
|
2944
|
+
setTimeout(() => { button.textContent = 'Copy Error'; }, 2000);
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
navigator.clipboard.writeText(errorText.trim()).then(() => {
|
|
2948
|
+
const originalText = button.textContent;
|
|
2949
|
+
button.textContent = 'Copied!';
|
|
2950
|
+
setTimeout(() => { button.textContent = originalText; }, 2000);
|
|
2951
|
+
}).catch(err => {
|
|
2952
|
+
console.error('Failed to copy: ', err);
|
|
2953
|
+
button.textContent = 'Failed';
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2048
2956
|
</script>
|
|
2957
|
+
|
|
2958
|
+
<!-- AI Fix Modal -->
|
|
2959
|
+
<div id="ai-fix-modal" class="ai-modal-overlay" onclick="closeAiModal()">
|
|
2960
|
+
<div class="ai-modal-content" onclick="event.stopPropagation()">
|
|
2961
|
+
<div class="ai-modal-header">
|
|
2962
|
+
<h3 id="ai-fix-modal-title">AI Analysis</h3>
|
|
2963
|
+
<span class="ai-modal-close" onclick="closeAiModal()">×</span>
|
|
2964
|
+
</div>
|
|
2965
|
+
<div class="ai-modal-body" id="ai-fix-modal-content">
|
|
2966
|
+
<!-- Content will be injected by JavaScript -->
|
|
2967
|
+
</div>
|
|
2968
|
+
</div>
|
|
2969
|
+
</div>
|
|
2970
|
+
|
|
2049
2971
|
</body>
|
|
2050
2972
|
</html>
|
|
2051
2973
|
`;
|
|
2052
2974
|
}
|
|
2053
|
-
|
|
2054
2975
|
async function runScript(scriptPath) {
|
|
2055
2976
|
return new Promise((resolve, reject) => {
|
|
2056
2977
|
console.log(chalk.blue(`Executing script: ${scriptPath}...`));
|
|
@@ -2075,7 +2996,11 @@ async function runScript(scriptPath) {
|
|
|
2075
2996
|
});
|
|
2076
2997
|
});
|
|
2077
2998
|
}
|
|
2078
|
-
|
|
2999
|
+
/**
|
|
3000
|
+
* The main function that orchestrates the generation of the static HTML report.
|
|
3001
|
+
* It reads the latest test run data, loads historical data for trend analysis,
|
|
3002
|
+
* prepares the data, and then generates and writes the final HTML report file.
|
|
3003
|
+
*/
|
|
2079
3004
|
async function main() {
|
|
2080
3005
|
const __filename = fileURLToPath(import.meta.url);
|
|
2081
3006
|
const __dirname = path.dirname(__filename);
|
|
@@ -2110,11 +3035,10 @@ async function main() {
|
|
|
2110
3035
|
),
|
|
2111
3036
|
error
|
|
2112
3037
|
);
|
|
2113
|
-
// You might decide to proceed or exit depending on the importance of historical data
|
|
2114
3038
|
}
|
|
2115
3039
|
|
|
2116
3040
|
// Step 2: Load current run's data (for non-trend sections of the report)
|
|
2117
|
-
let currentRunReportData;
|
|
3041
|
+
let currentRunReportData;
|
|
2118
3042
|
try {
|
|
2119
3043
|
const jsonData = await fs.readFile(reportJsonPath, "utf-8");
|
|
2120
3044
|
currentRunReportData = JSON.parse(jsonData);
|
|
@@ -2141,13 +3065,13 @@ async function main() {
|
|
|
2141
3065
|
`Critical Error: Could not read or parse main report JSON at ${reportJsonPath}: ${error.message}`
|
|
2142
3066
|
)
|
|
2143
3067
|
);
|
|
2144
|
-
process.exit(1);
|
|
3068
|
+
process.exit(1);
|
|
2145
3069
|
}
|
|
2146
3070
|
|
|
2147
3071
|
// Step 3: Load historical data for trends
|
|
2148
|
-
let historicalRuns = [];
|
|
3072
|
+
let historicalRuns = [];
|
|
2149
3073
|
try {
|
|
2150
|
-
await fs.access(historyDir);
|
|
3074
|
+
await fs.access(historyDir);
|
|
2151
3075
|
const allHistoryFiles = await fs.readdir(historyDir);
|
|
2152
3076
|
|
|
2153
3077
|
const jsonHistoryFiles = allHistoryFiles
|
|
@@ -2165,7 +3089,7 @@ async function main() {
|
|
|
2165
3089
|
};
|
|
2166
3090
|
})
|
|
2167
3091
|
.filter((file) => !isNaN(file.timestamp))
|
|
2168
|
-
.sort((a, b) => b.timestamp - a.timestamp);
|
|
3092
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
2169
3093
|
|
|
2170
3094
|
const filesToLoadForTrend = jsonHistoryFiles.slice(
|
|
2171
3095
|
0,
|
|
@@ -2175,7 +3099,7 @@ async function main() {
|
|
|
2175
3099
|
for (const fileMeta of filesToLoadForTrend) {
|
|
2176
3100
|
try {
|
|
2177
3101
|
const fileContent = await fs.readFile(fileMeta.path, "utf-8");
|
|
2178
|
-
const runJsonData = JSON.parse(fileContent);
|
|
3102
|
+
const runJsonData = JSON.parse(fileContent);
|
|
2179
3103
|
historicalRuns.push(runJsonData);
|
|
2180
3104
|
} catch (fileReadError) {
|
|
2181
3105
|
console.warn(
|
|
@@ -2185,8 +3109,7 @@ async function main() {
|
|
|
2185
3109
|
);
|
|
2186
3110
|
}
|
|
2187
3111
|
}
|
|
2188
|
-
//
|
|
2189
|
-
historicalRuns.reverse();
|
|
3112
|
+
historicalRuns.reverse(); // Oldest first for charts
|
|
2190
3113
|
console.log(
|
|
2191
3114
|
chalk.green(
|
|
2192
3115
|
`Loaded ${historicalRuns.length} historical run(s) for trend analysis.`
|
|
@@ -2208,20 +3131,18 @@ async function main() {
|
|
|
2208
3131
|
}
|
|
2209
3132
|
}
|
|
2210
3133
|
|
|
2211
|
-
// Step 4: Prepare trendData object
|
|
3134
|
+
// Step 4: Prepare trendData object
|
|
2212
3135
|
const trendData = {
|
|
2213
|
-
overall: [],
|
|
2214
|
-
testRuns: {},
|
|
3136
|
+
overall: [],
|
|
3137
|
+
testRuns: {},
|
|
2215
3138
|
};
|
|
2216
3139
|
|
|
2217
3140
|
if (historicalRuns.length > 0) {
|
|
2218
3141
|
historicalRuns.forEach((histRunReport) => {
|
|
2219
|
-
// histRunReport is a full PlaywrightPulseReport object from a past run
|
|
2220
3142
|
if (histRunReport.run) {
|
|
2221
|
-
// Ensure timestamp is a Date object for correct sorting/comparison later if needed by charts
|
|
2222
3143
|
const runTimestamp = new Date(histRunReport.run.timestamp);
|
|
2223
3144
|
trendData.overall.push({
|
|
2224
|
-
runId: runTimestamp.getTime(),
|
|
3145
|
+
runId: runTimestamp.getTime(),
|
|
2225
3146
|
timestamp: runTimestamp,
|
|
2226
3147
|
duration: histRunReport.run.duration,
|
|
2227
3148
|
totalTests: histRunReport.run.totalTests,
|
|
@@ -2230,21 +3151,19 @@ async function main() {
|
|
|
2230
3151
|
skipped: histRunReport.run.skipped || 0,
|
|
2231
3152
|
});
|
|
2232
3153
|
|
|
2233
|
-
// For generateTestHistoryContent
|
|
2234
3154
|
if (histRunReport.results && Array.isArray(histRunReport.results)) {
|
|
2235
|
-
const runKeyForTestHistory = `test run ${runTimestamp.getTime()}`;
|
|
3155
|
+
const runKeyForTestHistory = `test run ${runTimestamp.getTime()}`;
|
|
2236
3156
|
trendData.testRuns[runKeyForTestHistory] = histRunReport.results.map(
|
|
2237
3157
|
(test) => ({
|
|
2238
|
-
testName: test.name,
|
|
3158
|
+
testName: test.name,
|
|
2239
3159
|
duration: test.duration,
|
|
2240
3160
|
status: test.status,
|
|
2241
|
-
timestamp: new Date(test.startTime),
|
|
3161
|
+
timestamp: new Date(test.startTime),
|
|
2242
3162
|
})
|
|
2243
3163
|
);
|
|
2244
3164
|
}
|
|
2245
3165
|
}
|
|
2246
3166
|
});
|
|
2247
|
-
// Ensure trendData.overall is sorted by timestamp if not already
|
|
2248
3167
|
trendData.overall.sort(
|
|
2249
3168
|
(a, b) => a.timestamp.getTime() - b.timestamp.getTime()
|
|
2250
3169
|
);
|
|
@@ -2252,8 +3171,6 @@ async function main() {
|
|
|
2252
3171
|
|
|
2253
3172
|
// Step 5: Generate and write HTML
|
|
2254
3173
|
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
3174
|
const htmlContent = generateHTML(currentRunReportData, trendData);
|
|
2258
3175
|
await fs.writeFile(reportHtmlPath, htmlContent, "utf-8");
|
|
2259
3176
|
console.log(
|
|
@@ -2264,16 +3181,14 @@ async function main() {
|
|
|
2264
3181
|
console.log(chalk.gray(`(You can open this file in your browser)`));
|
|
2265
3182
|
} catch (error) {
|
|
2266
3183
|
console.error(chalk.red(`Error generating HTML report: ${error.message}`));
|
|
2267
|
-
console.error(chalk.red(error.stack));
|
|
3184
|
+
console.error(chalk.red(error.stack));
|
|
2268
3185
|
process.exit(1);
|
|
2269
3186
|
}
|
|
2270
3187
|
}
|
|
2271
|
-
|
|
2272
|
-
// Make sure main() is called at the end of your script
|
|
2273
3188
|
main().catch((err) => {
|
|
2274
3189
|
console.error(
|
|
2275
3190
|
chalk.red.bold(`Unhandled error during script execution: ${err.message}`)
|
|
2276
3191
|
);
|
|
2277
3192
|
console.error(err.stack);
|
|
2278
3193
|
process.exit(1);
|
|
2279
|
-
});
|
|
3194
|
+
});
|