@arghajit/playwright-pulse-report 0.2.1 → 0.2.2
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 +120 -56
- package/dist/reporter/playwright-pulse-reporter.d.ts +2 -0
- package/dist/reporter/playwright-pulse-reporter.js +118 -13
- package/dist/types/index.d.ts +17 -0
- package/package.json +11 -51
- package/scripts/generate-email-report.mjs +714 -0
- package/scripts/generate-report.mjs +2277 -0
- package/scripts/generate-static-report.mjs +1137 -945
- package/scripts/merge-pulse-report.js +1 -0
- package/scripts/{sendReport.js → sendReport.mjs} +138 -71
- 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,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
|
+
import { readFileSync, existsSync as fsExistsSync } from "fs"; // ADD THIS LINE
|
|
4
5
|
import path from "path";
|
|
5
6
|
import { fork } from "child_process"; // Add this
|
|
6
7
|
import { fileURLToPath } from "url"; // Add this for resolving path in ESM
|
|
@@ -20,133 +21,256 @@ try {
|
|
|
20
21
|
gray: (text) => text,
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
|
-
|
|
24
24
|
// Default configuration
|
|
25
25
|
const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
26
26
|
const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
|
|
27
27
|
const DEFAULT_HTML_FILE = "playwright-pulse-static-report.html";
|
|
28
|
-
|
|
29
28
|
// Helper functions
|
|
29
|
+
export function ansiToHtml(text) {
|
|
30
|
+
if (!text) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const codes = {
|
|
35
|
+
0: "color:inherit;font-weight:normal;font-style:normal;text-decoration:none;opacity:1;background-color:inherit;",
|
|
36
|
+
1: "font-weight:bold",
|
|
37
|
+
2: "opacity:0.6",
|
|
38
|
+
3: "font-style:italic",
|
|
39
|
+
4: "text-decoration:underline",
|
|
40
|
+
30: "color:#000", // black
|
|
41
|
+
31: "color:#d00", // red
|
|
42
|
+
32: "color:#0a0", // green
|
|
43
|
+
33: "color:#aa0", // yellow
|
|
44
|
+
34: "color:#00d", // blue
|
|
45
|
+
35: "color:#a0a", // magenta
|
|
46
|
+
36: "color:#0aa", // cyan
|
|
47
|
+
37: "color:#aaa", // light grey
|
|
48
|
+
39: "color:inherit", // default foreground color
|
|
49
|
+
40: "background-color:#000", // black background
|
|
50
|
+
41: "background-color:#d00", // red background
|
|
51
|
+
42: "background-color:#0a0", // green background
|
|
52
|
+
43: "background-color:#aa0", // yellow background
|
|
53
|
+
44: "background-color:#00d", // blue background
|
|
54
|
+
45: "background-color:#a0a", // magenta background
|
|
55
|
+
46: "background-color:#0aa", // cyan background
|
|
56
|
+
47: "background-color:#aaa", // light grey background
|
|
57
|
+
49: "background-color:inherit", // default background color
|
|
58
|
+
90: "color:#555", // dark grey
|
|
59
|
+
91: "color:#f55", // light red
|
|
60
|
+
92: "color:#5f5", // light green
|
|
61
|
+
93: "color:#ff5", // light yellow
|
|
62
|
+
94: "color:#55f", // light blue
|
|
63
|
+
95: "color:#f5f", // light magenta
|
|
64
|
+
96: "color:#5ff", // light cyan
|
|
65
|
+
97: "color:#fff", // white
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let currentStylesArray = [];
|
|
69
|
+
let html = "";
|
|
70
|
+
let openSpan = false;
|
|
71
|
+
|
|
72
|
+
const applyStyles = () => {
|
|
73
|
+
if (openSpan) {
|
|
74
|
+
html += "</span>";
|
|
75
|
+
openSpan = false;
|
|
76
|
+
}
|
|
77
|
+
if (currentStylesArray.length > 0) {
|
|
78
|
+
const styleString = currentStylesArray.filter((s) => s).join(";");
|
|
79
|
+
if (styleString) {
|
|
80
|
+
html += `<span style="${styleString}">`;
|
|
81
|
+
openSpan = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const resetAndApplyNewCodes = (newCodesStr) => {
|
|
87
|
+
const newCodes = newCodesStr.split(";");
|
|
88
|
+
|
|
89
|
+
if (newCodes.includes("0")) {
|
|
90
|
+
currentStylesArray = [];
|
|
91
|
+
if (codes["0"]) currentStylesArray.push(codes["0"]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const code of newCodes) {
|
|
95
|
+
if (code === "0") continue;
|
|
96
|
+
|
|
97
|
+
if (codes[code]) {
|
|
98
|
+
if (code === "39") {
|
|
99
|
+
currentStylesArray = currentStylesArray.filter(
|
|
100
|
+
(s) => !s.startsWith("color:")
|
|
101
|
+
);
|
|
102
|
+
currentStylesArray.push("color:inherit");
|
|
103
|
+
} else if (code === "49") {
|
|
104
|
+
currentStylesArray = currentStylesArray.filter(
|
|
105
|
+
(s) => !s.startsWith("background-color:")
|
|
106
|
+
);
|
|
107
|
+
currentStylesArray.push("background-color:inherit");
|
|
108
|
+
} else {
|
|
109
|
+
currentStylesArray.push(codes[code]);
|
|
110
|
+
}
|
|
111
|
+
} else if (code.startsWith("38;2;") || code.startsWith("48;2;")) {
|
|
112
|
+
const parts = code.split(";");
|
|
113
|
+
const type = parts[0] === "38" ? "color" : "background-color";
|
|
114
|
+
if (parts.length === 5) {
|
|
115
|
+
currentStylesArray = currentStylesArray.filter(
|
|
116
|
+
(s) => !s.startsWith(type + ":")
|
|
117
|
+
);
|
|
118
|
+
currentStylesArray.push(
|
|
119
|
+
`${type}:rgb(${parts[2]},${parts[3]},${parts[4]})`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
applyStyles();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const segments = text.split(/(\x1b\[[0-9;]*m)/g);
|
|
128
|
+
|
|
129
|
+
for (const segment of segments) {
|
|
130
|
+
if (!segment) continue;
|
|
131
|
+
|
|
132
|
+
if (segment.startsWith("\x1b[") && segment.endsWith("m")) {
|
|
133
|
+
const command = segment.slice(2, -1);
|
|
134
|
+
resetAndApplyNewCodes(command);
|
|
135
|
+
} else {
|
|
136
|
+
const escapedContent = segment
|
|
137
|
+
.replace(/&/g, "&")
|
|
138
|
+
.replace(/</g, "<")
|
|
139
|
+
.replace(/>/g, ">")
|
|
140
|
+
.replace(/"/g, """)
|
|
141
|
+
.replace(/'/g, "'");
|
|
142
|
+
html += escapedContent;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (openSpan) {
|
|
147
|
+
html += "</span>";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return html;
|
|
151
|
+
}
|
|
30
152
|
function sanitizeHTML(str) {
|
|
31
|
-
// User's provided version (note: this doesn't escape HTML special chars correctly)
|
|
32
153
|
if (str === null || str === undefined) return "";
|
|
33
|
-
return String(str)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
154
|
+
return String(str).replace(/[&<>"']/g, (match) => {
|
|
155
|
+
const replacements = {
|
|
156
|
+
"&": "&",
|
|
157
|
+
"<": "<",
|
|
158
|
+
">": ">",
|
|
159
|
+
'"': '"',
|
|
160
|
+
"'": "'", // or '
|
|
161
|
+
};
|
|
162
|
+
return replacements[match] || match;
|
|
163
|
+
});
|
|
39
164
|
}
|
|
40
165
|
function capitalize(str) {
|
|
41
166
|
if (!str) return ""; // Handle empty string
|
|
42
167
|
return str[0].toUpperCase() + str.slice(1).toLowerCase();
|
|
43
168
|
}
|
|
44
|
-
|
|
45
169
|
function formatPlaywrightError(error) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
170
|
+
const commandOutput = ansiToHtml(error || error.message);
|
|
171
|
+
return convertPlaywrightErrorToHTML(commandOutput);
|
|
172
|
+
}
|
|
173
|
+
function convertPlaywrightErrorToHTML(str) {
|
|
174
|
+
return (
|
|
175
|
+
str
|
|
176
|
+
// Convert leading spaces to and tabs to
|
|
177
|
+
.replace(/^(\s+)/gm, (match) =>
|
|
178
|
+
match.replace(/ /g, " ").replace(/\t/g, " ")
|
|
179
|
+
)
|
|
180
|
+
// Color and style replacements
|
|
181
|
+
.replace(/<red>/g, '<span style="color: red;">')
|
|
182
|
+
.replace(/<green>/g, '<span style="color: green;">')
|
|
183
|
+
.replace(/<dim>/g, '<span style="opacity: 0.6;">')
|
|
184
|
+
.replace(/<intensity>/g, '<span style="font-weight: bold;">') // Changed to apply bold
|
|
185
|
+
.replace(/<\/color>/g, "</span>")
|
|
186
|
+
.replace(/<\/intensity>/g, "</span>")
|
|
187
|
+
// Convert newlines to <br> after processing other replacements
|
|
188
|
+
.replace(/\n/g, "<br>")
|
|
58
189
|
);
|
|
190
|
+
}
|
|
191
|
+
function formatDuration(ms, options = {}) {
|
|
192
|
+
const {
|
|
193
|
+
precision = 1,
|
|
194
|
+
invalidInputReturn = "N/A",
|
|
195
|
+
defaultForNullUndefinedNegative = null,
|
|
196
|
+
} = options;
|
|
197
|
+
|
|
198
|
+
const validPrecision = Math.max(0, Math.floor(precision));
|
|
199
|
+
const zeroWithPrecision = (0).toFixed(validPrecision) + "s";
|
|
200
|
+
const resolvedNullUndefNegReturn =
|
|
201
|
+
defaultForNullUndefinedNegative === null
|
|
202
|
+
? zeroWithPrecision
|
|
203
|
+
: defaultForNullUndefinedNegative;
|
|
204
|
+
|
|
205
|
+
if (ms === undefined || ms === null) {
|
|
206
|
+
return resolvedNullUndefNegReturn;
|
|
207
|
+
}
|
|
59
208
|
|
|
60
|
-
|
|
61
|
-
const escapeHtml = (str) => {
|
|
62
|
-
if (!str) return "";
|
|
63
|
-
return str.replace(
|
|
64
|
-
/[&<>'"]/g,
|
|
65
|
-
(tag) =>
|
|
66
|
-
({
|
|
67
|
-
"&": "&",
|
|
68
|
-
"<": "<",
|
|
69
|
-
">": ">",
|
|
70
|
-
"'": "'",
|
|
71
|
-
'"': """,
|
|
72
|
-
}[tag])
|
|
73
|
-
);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
// Build HTML output
|
|
77
|
-
let html = `<div class="playwright-error">
|
|
78
|
-
<div class="error-header">Test Error</div>`;
|
|
209
|
+
const numMs = Number(ms);
|
|
79
210
|
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
timeoutMatch[1]
|
|
83
|
-
)}ms</div>`;
|
|
211
|
+
if (Number.isNaN(numMs) || !Number.isFinite(numMs)) {
|
|
212
|
+
return invalidInputReturn;
|
|
84
213
|
}
|
|
85
214
|
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
assertionMatch[1]
|
|
89
|
-
)}).${escapeHtml(assertionMatch[2])}()</div>`;
|
|
215
|
+
if (numMs < 0) {
|
|
216
|
+
return resolvedNullUndefNegReturn;
|
|
90
217
|
}
|
|
91
218
|
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
expectedMatch[1]
|
|
95
|
-
)}</div>`;
|
|
219
|
+
if (numMs === 0) {
|
|
220
|
+
return zeroWithPrecision;
|
|
96
221
|
}
|
|
97
222
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
223
|
+
const MS_PER_SECOND = 1000;
|
|
224
|
+
const SECONDS_PER_MINUTE = 60;
|
|
225
|
+
const MINUTES_PER_HOUR = 60;
|
|
226
|
+
const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
|
|
103
227
|
|
|
104
|
-
|
|
105
|
-
const callLogStart = cleanMessage.indexOf("Call log:");
|
|
106
|
-
if (callLogStart !== -1) {
|
|
107
|
-
const callLogEnd =
|
|
108
|
-
cleanMessage.indexOf("\n\n", callLogStart) || cleanMessage.length;
|
|
109
|
-
const callLogSection = cleanMessage
|
|
110
|
-
.slice(callLogStart + 9, callLogEnd)
|
|
111
|
-
.trim();
|
|
112
|
-
|
|
113
|
-
html += `<div class="error-call-log">
|
|
114
|
-
<div class="call-log-header">📜 Call Log:</div>
|
|
115
|
-
<ul class="call-log-items">${callLogSection
|
|
116
|
-
.split("\n")
|
|
117
|
-
.map((line) => line.trim())
|
|
118
|
-
.filter((line) => line)
|
|
119
|
-
.map((line) => `<li>${escapeHtml(line.replace(/^-\s*/, ""))}</li>`)
|
|
120
|
-
.join("")}</ul>
|
|
121
|
-
</div>`;
|
|
122
|
-
}
|
|
228
|
+
const totalRawSeconds = numMs / MS_PER_SECOND;
|
|
123
229
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
if
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
230
|
+
// Decision: Are we going to display hours or minutes?
|
|
231
|
+
// This happens if the duration is inherently >= 1 minute OR
|
|
232
|
+
// if it's < 1 minute but ceiling the seconds makes it >= 1 minute.
|
|
233
|
+
if (
|
|
234
|
+
totalRawSeconds < SECONDS_PER_MINUTE &&
|
|
235
|
+
Math.ceil(totalRawSeconds) < SECONDS_PER_MINUTE
|
|
236
|
+
) {
|
|
237
|
+
// Strictly seconds-only display, use precision.
|
|
238
|
+
return `${totalRawSeconds.toFixed(validPrecision)}s`;
|
|
239
|
+
} else {
|
|
240
|
+
// Display will include minutes and/or hours, or seconds round up to a minute.
|
|
241
|
+
// Seconds part should be an integer (ceiling).
|
|
242
|
+
// Round the total milliseconds UP to the nearest full second.
|
|
243
|
+
const totalMsRoundedUpToSecond =
|
|
244
|
+
Math.ceil(numMs / MS_PER_SECOND) * MS_PER_SECOND;
|
|
138
245
|
|
|
139
|
-
|
|
246
|
+
let remainingMs = totalMsRoundedUpToSecond;
|
|
140
247
|
|
|
141
|
-
|
|
142
|
-
|
|
248
|
+
const h = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_HOUR));
|
|
249
|
+
remainingMs %= MS_PER_SECOND * SECONDS_PER_HOUR;
|
|
143
250
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
251
|
+
const m = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_MINUTE));
|
|
252
|
+
remainingMs %= MS_PER_SECOND * SECONDS_PER_MINUTE;
|
|
253
|
+
|
|
254
|
+
const s = Math.floor(remainingMs / MS_PER_SECOND); // This will be an integer
|
|
149
255
|
|
|
256
|
+
const parts = [];
|
|
257
|
+
if (h > 0) {
|
|
258
|
+
parts.push(`${h}h`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Show minutes if:
|
|
262
|
+
// - hours are present (e.g., "1h 0m 5s")
|
|
263
|
+
// - OR minutes themselves are > 0 (e.g., "5m 10s")
|
|
264
|
+
// - OR the original duration was >= 1 minute (ensures "1m 0s" for 60000ms)
|
|
265
|
+
if (h > 0 || m > 0 || numMs >= MS_PER_SECOND * SECONDS_PER_MINUTE) {
|
|
266
|
+
parts.push(`${m}m`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
parts.push(`${s}s`);
|
|
270
|
+
|
|
271
|
+
return parts.join(" ");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
150
274
|
function generateTestTrendsChart(trendData) {
|
|
151
275
|
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
152
276
|
return '<div class="no-data">No overall trend data available for test counts.</div>';
|
|
@@ -155,107 +279,93 @@ function generateTestTrendsChart(trendData) {
|
|
|
155
279
|
const chartId = `testTrendsChart-${Date.now()}-${Math.random()
|
|
156
280
|
.toString(36)
|
|
157
281
|
.substring(2, 7)}`;
|
|
282
|
+
const renderFunctionName = `renderTestTrendsChart_${chartId.replace(
|
|
283
|
+
/-/g,
|
|
284
|
+
"_"
|
|
285
|
+
)}`;
|
|
158
286
|
const runs = trendData.overall;
|
|
159
287
|
|
|
160
288
|
const series = [
|
|
161
289
|
{
|
|
162
290
|
name: "Total",
|
|
163
291
|
data: runs.map((r) => r.totalTests),
|
|
164
|
-
color: "var(--primary-color)",
|
|
292
|
+
color: "var(--primary-color)",
|
|
165
293
|
marker: { symbol: "circle" },
|
|
166
294
|
},
|
|
167
295
|
{
|
|
168
296
|
name: "Passed",
|
|
169
297
|
data: runs.map((r) => r.passed),
|
|
170
|
-
color: "var(--success-color)",
|
|
298
|
+
color: "var(--success-color)",
|
|
171
299
|
marker: { symbol: "circle" },
|
|
172
300
|
},
|
|
173
301
|
{
|
|
174
302
|
name: "Failed",
|
|
175
303
|
data: runs.map((r) => r.failed),
|
|
176
|
-
color: "var(--danger-color)",
|
|
304
|
+
color: "var(--danger-color)",
|
|
177
305
|
marker: { symbol: "circle" },
|
|
178
306
|
},
|
|
179
307
|
{
|
|
180
308
|
name: "Skipped",
|
|
181
309
|
data: runs.map((r) => r.skipped || 0),
|
|
182
|
-
color: "var(--warning-color)",
|
|
310
|
+
color: "var(--warning-color)",
|
|
183
311
|
marker: { symbol: "circle" },
|
|
184
312
|
},
|
|
185
313
|
];
|
|
186
|
-
|
|
187
|
-
// Data needed by the tooltip formatter, stringified to be embedded in the client-side script
|
|
188
314
|
const runsForTooltip = runs.map((r) => ({
|
|
189
315
|
runId: r.runId,
|
|
190
316
|
timestamp: r.timestamp,
|
|
191
317
|
duration: r.duration,
|
|
192
318
|
}));
|
|
193
319
|
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
title: { text: null },
|
|
198
|
-
xAxis: {
|
|
199
|
-
categories: ${JSON.stringify(runs.map((run, i) => `Run ${i + 1}`))},
|
|
200
|
-
crosshair: true,
|
|
201
|
-
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}
|
|
202
|
-
},
|
|
203
|
-
yAxis: {
|
|
204
|
-
title: { text: "Test Count", style: { color: 'var(--text-color)'} },
|
|
205
|
-
min: 0,
|
|
206
|
-
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}
|
|
207
|
-
},
|
|
208
|
-
legend: {
|
|
209
|
-
layout: "horizontal", align: "center", verticalAlign: "bottom",
|
|
210
|
-
itemStyle: { fontSize: "12px", color: 'var(--text-color)' }
|
|
211
|
-
},
|
|
212
|
-
plotOptions: {
|
|
213
|
-
series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}},
|
|
214
|
-
line: { lineWidth: 2.5 } // fillOpacity was 0.1, but for line charts, area fill is usually separate (area chart type)
|
|
215
|
-
},
|
|
216
|
-
tooltip: {
|
|
217
|
-
shared: true, useHTML: true,
|
|
218
|
-
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
219
|
-
borderColor: 'rgba(10,10,10,0.92)',
|
|
220
|
-
style: { color: '#f5f5f5' },
|
|
221
|
-
formatter: function () {
|
|
222
|
-
const runsData = ${JSON.stringify(runsForTooltip)};
|
|
223
|
-
const pointIndex = this.points[0].point.x; // Get index from point
|
|
224
|
-
const run = runsData[pointIndex];
|
|
225
|
-
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' +
|
|
226
|
-
'Date: ' + new Date(run.timestamp).toLocaleString() + '<br><br>';
|
|
227
|
-
this.points.forEach(point => {
|
|
228
|
-
tooltip += '<span style="color:' + point.color + '">●</span> ' + point.series.name + ': <b>' + point.y + '</b><br>';
|
|
229
|
-
});
|
|
230
|
-
tooltip += '<br>Duration: ' + formatDuration(run.duration);
|
|
231
|
-
return tooltip;
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
series: ${JSON.stringify(series)},
|
|
235
|
-
credits: { enabled: false }
|
|
236
|
-
}
|
|
237
|
-
`;
|
|
320
|
+
const categoriesString = JSON.stringify(runs.map((run, i) => `Run ${i + 1}`));
|
|
321
|
+
const seriesString = JSON.stringify(series);
|
|
322
|
+
const runsForTooltipString = JSON.stringify(runsForTooltip);
|
|
238
323
|
|
|
239
324
|
return `
|
|
240
|
-
<div id="${chartId}" class="trend-chart-container"
|
|
325
|
+
<div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
|
|
326
|
+
<div class="no-data">Loading Test Volume Trends...</div>
|
|
327
|
+
</div>
|
|
241
328
|
<script>
|
|
242
|
-
|
|
329
|
+
window.${renderFunctionName} = function() {
|
|
330
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
331
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found for lazy loading."); return; }
|
|
243
332
|
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
244
333
|
try {
|
|
245
|
-
|
|
334
|
+
chartContainer.innerHTML = ''; // Clear placeholder
|
|
335
|
+
const chartOptions = {
|
|
336
|
+
chart: { type: "line", height: 350, backgroundColor: "transparent" },
|
|
337
|
+
title: { text: null },
|
|
338
|
+
xAxis: { categories: ${categoriesString}, crosshair: true, labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}},
|
|
339
|
+
yAxis: { title: { text: "Test Count", style: { color: 'var(--text-color)'} }, min: 0, labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}},
|
|
340
|
+
legend: { layout: "horizontal", align: "center", verticalAlign: "bottom", itemStyle: { fontSize: "12px", color: 'var(--text-color)' }},
|
|
341
|
+
plotOptions: { series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}}, line: { lineWidth: 2.5 }},
|
|
342
|
+
tooltip: {
|
|
343
|
+
shared: true, useHTML: true, backgroundColor: 'rgba(10,10,10,0.92)', borderColor: 'rgba(10,10,10,0.92)', style: { color: '#f5f5f5' },
|
|
344
|
+
formatter: function () {
|
|
345
|
+
const runsData = ${runsForTooltipString};
|
|
346
|
+
const pointIndex = this.points[0].point.x;
|
|
347
|
+
const run = runsData[pointIndex];
|
|
348
|
+
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' + 'Date: ' + new Date(run.timestamp).toLocaleString() + '<br><br>';
|
|
349
|
+
this.points.forEach(point => { tooltip += '<span style="color:' + point.color + '">●</span> ' + point.series.name + ': <b>' + point.y + '</b><br>'; });
|
|
350
|
+
tooltip += '<br>Duration: ' + formatDuration(run.duration);
|
|
351
|
+
return tooltip;
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
series: ${seriesString},
|
|
355
|
+
credits: { enabled: false }
|
|
356
|
+
};
|
|
246
357
|
Highcharts.chart('${chartId}', chartOptions);
|
|
247
358
|
} catch (e) {
|
|
248
|
-
console.error("Error rendering chart ${chartId}:", e);
|
|
249
|
-
|
|
359
|
+
console.error("Error rendering chart ${chartId} (lazy):", e);
|
|
360
|
+
chartContainer.innerHTML = '<div class="no-data">Error rendering test trends chart.</div>';
|
|
250
361
|
}
|
|
251
362
|
} else {
|
|
252
|
-
|
|
363
|
+
chartContainer.innerHTML = '<div class="no-data">Charting library not available for test trends.</div>';
|
|
253
364
|
}
|
|
254
|
-
}
|
|
365
|
+
};
|
|
255
366
|
</script>
|
|
256
367
|
`;
|
|
257
368
|
}
|
|
258
|
-
|
|
259
369
|
function generateDurationTrendChart(trendData) {
|
|
260
370
|
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
261
371
|
return '<div class="no-data">No overall trend data available for durations.</div>';
|
|
@@ -263,109 +373,83 @@ function generateDurationTrendChart(trendData) {
|
|
|
263
373
|
const chartId = `durationTrendChart-${Date.now()}-${Math.random()
|
|
264
374
|
.toString(36)
|
|
265
375
|
.substring(2, 7)}`;
|
|
376
|
+
const renderFunctionName = `renderDurationTrendChart_${chartId.replace(
|
|
377
|
+
/-/g,
|
|
378
|
+
"_"
|
|
379
|
+
)}`;
|
|
266
380
|
const runs = trendData.overall;
|
|
267
381
|
|
|
268
|
-
// Assuming var(--accent-color-alt) is Orange #FF9800
|
|
269
|
-
const accentColorAltRGB = "255, 152, 0";
|
|
270
|
-
|
|
271
|
-
const seriesString = `[{
|
|
272
|
-
name: 'Duration',
|
|
273
|
-
data: ${JSON.stringify(runs.map((run) => run.duration))},
|
|
274
|
-
color: 'var(--accent-color-alt)',
|
|
275
|
-
type: 'area',
|
|
276
|
-
marker: {
|
|
277
|
-
symbol: 'circle', enabled: true, radius: 4,
|
|
278
|
-
states: { hover: { radius: 6, lineWidthPlus: 0 } }
|
|
279
|
-
},
|
|
280
|
-
fillColor: {
|
|
281
|
-
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
282
|
-
stops: [
|
|
283
|
-
[0, 'rgba(${accentColorAltRGB}, 0.4)'],
|
|
284
|
-
[1, 'rgba(${accentColorAltRGB}, 0.05)']
|
|
285
|
-
]
|
|
286
|
-
},
|
|
287
|
-
lineWidth: 2.5
|
|
288
|
-
}]`;
|
|
382
|
+
const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
|
|
289
383
|
|
|
384
|
+
const chartDataString = JSON.stringify(runs.map((run) => run.duration));
|
|
385
|
+
const categoriesString = JSON.stringify(runs.map((run, i) => `Run ${i + 1}`));
|
|
290
386
|
const runsForTooltip = runs.map((r) => ({
|
|
291
387
|
runId: r.runId,
|
|
292
388
|
timestamp: r.timestamp,
|
|
293
389
|
duration: r.duration,
|
|
294
390
|
totalTests: r.totalTests,
|
|
295
391
|
}));
|
|
392
|
+
const runsForTooltipString = JSON.stringify(runsForTooltip);
|
|
296
393
|
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
yAxis: {
|
|
307
|
-
title: { text: 'Duration', style: { color: 'var(--text-color)' } },
|
|
308
|
-
labels: {
|
|
309
|
-
formatter: function() { return formatDuration(this.value); },
|
|
310
|
-
style: { color: 'var(--text-color-secondary)', fontSize: '12px' }
|
|
311
|
-
},
|
|
312
|
-
min: 0
|
|
313
|
-
},
|
|
314
|
-
legend: {
|
|
315
|
-
layout: 'horizontal', align: 'center', verticalAlign: 'bottom',
|
|
316
|
-
itemStyle: { fontSize: '12px', color: 'var(--text-color)' }
|
|
317
|
-
},
|
|
318
|
-
plotOptions: {
|
|
319
|
-
area: {
|
|
320
|
-
lineWidth: 2.5,
|
|
321
|
-
states: { hover: { lineWidthPlus: 0 } },
|
|
322
|
-
threshold: null
|
|
323
|
-
}
|
|
324
|
-
},
|
|
325
|
-
tooltip: {
|
|
326
|
-
shared: true, useHTML: true,
|
|
327
|
-
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
328
|
-
borderColor: 'rgba(10,10,10,0.92)',
|
|
329
|
-
style: { color: '#f5f5f5' },
|
|
330
|
-
formatter: function () {
|
|
331
|
-
const runsData = ${JSON.stringify(runsForTooltip)};
|
|
332
|
-
const pointIndex = this.points[0].point.x;
|
|
333
|
-
const run = runsData[pointIndex];
|
|
334
|
-
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' +
|
|
335
|
-
'Date: ' + new Date(run.timestamp).toLocaleString() + '<br>';
|
|
336
|
-
this.points.forEach(point => {
|
|
337
|
-
tooltip += '<span style="color:' + point.series.color + '">●</span> ' +
|
|
338
|
-
point.series.name + ': <b>' + formatDuration(point.y) + '</b><br>';
|
|
339
|
-
});
|
|
340
|
-
tooltip += '<br>Tests: ' + run.totalTests;
|
|
341
|
-
return tooltip;
|
|
342
|
-
}
|
|
343
|
-
},
|
|
344
|
-
series: ${seriesString},
|
|
345
|
-
credits: { enabled: false }
|
|
346
|
-
}
|
|
347
|
-
`;
|
|
394
|
+
const seriesStringForRender = `[{
|
|
395
|
+
name: 'Duration',
|
|
396
|
+
data: ${chartDataString},
|
|
397
|
+
color: 'var(--accent-color-alt)',
|
|
398
|
+
type: 'area',
|
|
399
|
+
marker: { symbol: 'circle', enabled: true, radius: 4, states: { hover: { radius: 6, lineWidthPlus: 0 } } },
|
|
400
|
+
fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(${accentColorAltRGB}, 0.4)'], [1, 'rgba(${accentColorAltRGB}, 0.05)']] },
|
|
401
|
+
lineWidth: 2.5
|
|
402
|
+
}]`;
|
|
348
403
|
|
|
349
404
|
return `
|
|
350
|
-
<div id="${chartId}" class="trend-chart-container"
|
|
405
|
+
<div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
|
|
406
|
+
<div class="no-data">Loading Duration Trends...</div>
|
|
407
|
+
</div>
|
|
351
408
|
<script>
|
|
352
|
-
|
|
409
|
+
window.${renderFunctionName} = function() {
|
|
410
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
411
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found for lazy loading."); return; }
|
|
353
412
|
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
354
413
|
try {
|
|
355
|
-
|
|
414
|
+
chartContainer.innerHTML = ''; // Clear placeholder
|
|
415
|
+
const chartOptions = {
|
|
416
|
+
chart: { type: 'area', height: 350, backgroundColor: 'transparent' },
|
|
417
|
+
title: { text: null },
|
|
418
|
+
xAxis: { categories: ${categoriesString}, crosshair: true, labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}},
|
|
419
|
+
yAxis: {
|
|
420
|
+
title: { text: 'Duration', style: { color: 'var(--text-color)' } },
|
|
421
|
+
labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)', fontSize: '12px' }},
|
|
422
|
+
min: 0
|
|
423
|
+
},
|
|
424
|
+
legend: { layout: 'horizontal', align: 'center', verticalAlign: 'bottom', itemStyle: { fontSize: '12px', color: 'var(--text-color)' }},
|
|
425
|
+
plotOptions: { area: { lineWidth: 2.5, states: { hover: { lineWidthPlus: 0 } }, threshold: null }},
|
|
426
|
+
tooltip: {
|
|
427
|
+
shared: true, useHTML: true, backgroundColor: 'rgba(10,10,10,0.92)', borderColor: 'rgba(10,10,10,0.92)', style: { color: '#f5f5f5' },
|
|
428
|
+
formatter: function () {
|
|
429
|
+
const runsData = ${runsForTooltipString};
|
|
430
|
+
const pointIndex = this.points[0].point.x;
|
|
431
|
+
const run = runsData[pointIndex];
|
|
432
|
+
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' + 'Date: ' + new Date(run.timestamp).toLocaleString() + '<br>';
|
|
433
|
+
this.points.forEach(point => { tooltip += '<span style="color:' + point.series.color + '">●</span> ' + point.series.name + ': <b>' + formatDuration(point.y) + '</b><br>'; });
|
|
434
|
+
tooltip += '<br>Tests: ' + run.totalTests;
|
|
435
|
+
return tooltip;
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
series: ${seriesStringForRender}, // This is already a string representation of an array
|
|
439
|
+
credits: { enabled: false }
|
|
440
|
+
};
|
|
356
441
|
Highcharts.chart('${chartId}', chartOptions);
|
|
357
442
|
} catch (e) {
|
|
358
|
-
console.error("Error rendering chart ${chartId}:", e);
|
|
359
|
-
|
|
443
|
+
console.error("Error rendering chart ${chartId} (lazy):", e);
|
|
444
|
+
chartContainer.innerHTML = '<div class="no-data">Error rendering duration trend chart.</div>';
|
|
360
445
|
}
|
|
361
446
|
} else {
|
|
362
|
-
|
|
447
|
+
chartContainer.innerHTML = '<div class="no-data">Charting library not available for duration trends.</div>';
|
|
363
448
|
}
|
|
364
|
-
}
|
|
449
|
+
};
|
|
365
450
|
</script>
|
|
366
451
|
`;
|
|
367
452
|
}
|
|
368
|
-
|
|
369
453
|
function formatDate(dateStrOrDate) {
|
|
370
454
|
if (!dateStrOrDate) return "N/A";
|
|
371
455
|
try {
|
|
@@ -384,11 +468,9 @@ function formatDate(dateStrOrDate) {
|
|
|
384
468
|
return "Invalid Date Format";
|
|
385
469
|
}
|
|
386
470
|
}
|
|
387
|
-
|
|
388
471
|
function generateTestHistoryChart(history) {
|
|
389
472
|
if (!history || history.length === 0)
|
|
390
473
|
return '<div class="no-data-chart">No data for chart</div>';
|
|
391
|
-
|
|
392
474
|
const validHistory = history.filter(
|
|
393
475
|
(h) => h && typeof h.duration === "number" && h.duration >= 0
|
|
394
476
|
);
|
|
@@ -398,6 +480,10 @@ function generateTestHistoryChart(history) {
|
|
|
398
480
|
const chartId = `testHistoryChart-${Date.now()}-${Math.random()
|
|
399
481
|
.toString(36)
|
|
400
482
|
.substring(2, 7)}`;
|
|
483
|
+
const renderFunctionName = `renderTestHistoryChart_${chartId.replace(
|
|
484
|
+
/-/g,
|
|
485
|
+
"_"
|
|
486
|
+
)}`;
|
|
401
487
|
|
|
402
488
|
const seriesDataPoints = validHistory.map((run) => {
|
|
403
489
|
let color;
|
|
@@ -427,94 +513,71 @@ function generateTestHistoryChart(history) {
|
|
|
427
513
|
};
|
|
428
514
|
});
|
|
429
515
|
|
|
430
|
-
// Assuming var(--accent-color) is Deep Purple #673ab7
|
|
431
|
-
const accentColorRGB = "103, 58, 183";
|
|
516
|
+
const accentColorRGB = "103, 58, 183"; // Assuming var(--accent-color) is Deep Purple #673ab7
|
|
432
517
|
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
xAxis: {
|
|
438
|
-
categories: ${JSON.stringify(
|
|
439
|
-
validHistory.map((_, i) => `R${i + 1}`)
|
|
440
|
-
)},
|
|
441
|
-
labels: { style: { fontSize: '10px', color: 'var(--text-color-secondary)' } }
|
|
442
|
-
},
|
|
443
|
-
yAxis: {
|
|
444
|
-
title: { text: null },
|
|
445
|
-
labels: {
|
|
446
|
-
formatter: function() { return formatDuration(this.value); },
|
|
447
|
-
style: { fontSize: '10px', color: 'var(--text-color-secondary)' },
|
|
448
|
-
align: 'left', x: -35, y: 3
|
|
449
|
-
},
|
|
450
|
-
min: 0,
|
|
451
|
-
gridLineWidth: 0,
|
|
452
|
-
tickAmount: 4
|
|
453
|
-
},
|
|
454
|
-
legend: { enabled: false },
|
|
455
|
-
plotOptions: {
|
|
456
|
-
area: {
|
|
457
|
-
lineWidth: 2,
|
|
458
|
-
lineColor: 'var(--accent-color)',
|
|
459
|
-
fillColor: {
|
|
460
|
-
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
461
|
-
stops: [
|
|
462
|
-
[0, 'rgba(${accentColorRGB}, 0.4)'],
|
|
463
|
-
[1, 'rgba(${accentColorRGB}, 0)']
|
|
464
|
-
]
|
|
465
|
-
},
|
|
466
|
-
marker: { enabled: true },
|
|
467
|
-
threshold: null
|
|
468
|
-
}
|
|
469
|
-
},
|
|
470
|
-
tooltip: {
|
|
471
|
-
useHTML: true,
|
|
472
|
-
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
473
|
-
borderColor: 'rgba(10,10,10,0.92)',
|
|
474
|
-
style: { color: '#f5f5f5', padding: '8px' },
|
|
475
|
-
formatter: function() {
|
|
476
|
-
const pointData = this.point;
|
|
477
|
-
let statusBadgeHtml = '<span style="padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; background-color: ';
|
|
478
|
-
switch(String(pointData.status).toLowerCase()) {
|
|
479
|
-
case 'passed': statusBadgeHtml += 'var(--success-color)'; break;
|
|
480
|
-
case 'failed': statusBadgeHtml += 'var(--danger-color)'; break;
|
|
481
|
-
case 'skipped': statusBadgeHtml += 'var(--warning-color)'; break;
|
|
482
|
-
default: statusBadgeHtml += 'var(--dark-gray-color)';
|
|
483
|
-
}
|
|
484
|
-
statusBadgeHtml += ';">' + String(pointData.status).toUpperCase() + '</span>';
|
|
518
|
+
const categoriesString = JSON.stringify(
|
|
519
|
+
validHistory.map((_, i) => `R${i + 1}`)
|
|
520
|
+
);
|
|
521
|
+
const seriesDataPointsString = JSON.stringify(seriesDataPoints);
|
|
485
522
|
|
|
486
|
-
return '<strong>Run ' + (pointData.runId || (this.point.index + 1)) + '</strong><br>' +
|
|
487
|
-
'Status: ' + statusBadgeHtml + '<br>' +
|
|
488
|
-
'Duration: ' + formatDuration(pointData.y);
|
|
489
|
-
}
|
|
490
|
-
},
|
|
491
|
-
series: [{
|
|
492
|
-
data: ${JSON.stringify(seriesDataPoints)},
|
|
493
|
-
showInLegend: false
|
|
494
|
-
}],
|
|
495
|
-
credits: { enabled: false }
|
|
496
|
-
}
|
|
497
|
-
`;
|
|
498
523
|
return `
|
|
499
|
-
<div id="${chartId}" style="width: 320px; height: 100px;"
|
|
524
|
+
<div id="${chartId}" style="width: 320px; height: 100px;" class="lazy-load-chart" data-render-function-name="${renderFunctionName}">
|
|
525
|
+
<div class="no-data-chart">Loading History...</div>
|
|
526
|
+
</div>
|
|
500
527
|
<script>
|
|
501
|
-
|
|
528
|
+
window.${renderFunctionName} = function() {
|
|
529
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
530
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found for lazy loading."); return; }
|
|
502
531
|
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
503
532
|
try {
|
|
504
|
-
|
|
533
|
+
chartContainer.innerHTML = ''; // Clear placeholder
|
|
534
|
+
const chartOptions = {
|
|
535
|
+
chart: { type: 'area', height: 100, width: 320, backgroundColor: 'transparent', spacing: [10,10,15,35] },
|
|
536
|
+
title: { text: null },
|
|
537
|
+
xAxis: { categories: ${categoriesString}, labels: { style: { fontSize: '10px', color: 'var(--text-color-secondary)' }}},
|
|
538
|
+
yAxis: {
|
|
539
|
+
title: { text: null },
|
|
540
|
+
labels: { formatter: function() { return formatDuration(this.value); }, style: { fontSize: '10px', color: 'var(--text-color-secondary)' }, align: 'left', x: -35, y: 3 },
|
|
541
|
+
min: 0, gridLineWidth: 0, tickAmount: 4
|
|
542
|
+
},
|
|
543
|
+
legend: { enabled: false },
|
|
544
|
+
plotOptions: {
|
|
545
|
+
area: {
|
|
546
|
+
lineWidth: 2, lineColor: 'var(--accent-color)',
|
|
547
|
+
fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(${accentColorRGB}, 0.4)'],[1, 'rgba(${accentColorRGB}, 0)']]},
|
|
548
|
+
marker: { enabled: true }, threshold: null
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
tooltip: {
|
|
552
|
+
useHTML: true, backgroundColor: 'rgba(10,10,10,0.92)', borderColor: 'rgba(10,10,10,0.92)', style: { color: '#f5f5f5', padding: '8px' },
|
|
553
|
+
formatter: function() {
|
|
554
|
+
const pointData = this.point;
|
|
555
|
+
let statusBadgeHtml = '<span style="padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; background-color: ';
|
|
556
|
+
switch(String(pointData.status).toLowerCase()) {
|
|
557
|
+
case 'passed': statusBadgeHtml += 'var(--success-color)'; break;
|
|
558
|
+
case 'failed': statusBadgeHtml += 'var(--danger-color)'; break;
|
|
559
|
+
case 'skipped': statusBadgeHtml += 'var(--warning-color)'; break;
|
|
560
|
+
default: statusBadgeHtml += 'var(--dark-gray-color)';
|
|
561
|
+
}
|
|
562
|
+
statusBadgeHtml += ';">' + String(pointData.status).toUpperCase() + '</span>';
|
|
563
|
+
return '<strong>Run ' + (pointData.runId || (this.point.index + 1)) + '</strong><br>' + 'Status: ' + statusBadgeHtml + '<br>' + 'Duration: ' + formatDuration(pointData.y);
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
series: [{ data: ${seriesDataPointsString}, showInLegend: false }],
|
|
567
|
+
credits: { enabled: false }
|
|
568
|
+
};
|
|
505
569
|
Highcharts.chart('${chartId}', chartOptions);
|
|
506
570
|
} catch (e) {
|
|
507
|
-
console.error("Error rendering chart ${chartId}:", e);
|
|
508
|
-
|
|
571
|
+
console.error("Error rendering chart ${chartId} (lazy):", e);
|
|
572
|
+
chartContainer.innerHTML = '<div class="no-data-chart">Error rendering history chart.</div>';
|
|
509
573
|
}
|
|
510
574
|
} else {
|
|
511
|
-
|
|
575
|
+
chartContainer.innerHTML = '<div class="no-data-chart">Charting library not available for history.</div>';
|
|
512
576
|
}
|
|
513
|
-
}
|
|
577
|
+
};
|
|
514
578
|
</script>
|
|
515
579
|
`;
|
|
516
580
|
}
|
|
517
|
-
|
|
518
581
|
function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
519
582
|
const total = data.reduce((sum, d) => sum + d.value, 0);
|
|
520
583
|
if (total === 0) {
|
|
@@ -621,7 +684,7 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
621
684
|
`;
|
|
622
685
|
|
|
623
686
|
return `
|
|
624
|
-
<div class="pie-chart-wrapper" style="align-items: center">
|
|
687
|
+
<div class="pie-chart-wrapper" style="align-items: center; max-height: 450px">
|
|
625
688
|
<div style="display: flex; align-items: start; width: 100%;"><h3>Test Distribution</h3></div>
|
|
626
689
|
<div id="${chartId}" style="width: ${chartWidth}px; height: ${
|
|
627
690
|
chartHeight - 40
|
|
@@ -644,7 +707,335 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
644
707
|
</div>
|
|
645
708
|
`;
|
|
646
709
|
}
|
|
710
|
+
function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
711
|
+
// Format memory for display
|
|
712
|
+
const formattedMemory = environment.memory.replace(/(\d+\.\d{2})GB/, "$1 GB");
|
|
713
|
+
|
|
714
|
+
// Generate a unique ID for the dashboard
|
|
715
|
+
const dashboardId = `envDashboard-${Date.now()}-${Math.random()
|
|
716
|
+
.toString(36)
|
|
717
|
+
.substring(2, 7)}`;
|
|
718
|
+
|
|
719
|
+
const cardHeight = Math.floor(dashboardHeight * 0.44);
|
|
720
|
+
const cardContentPadding = 16; // px
|
|
721
|
+
|
|
722
|
+
return `
|
|
723
|
+
<div class="environment-dashboard-wrapper" id="${dashboardId}">
|
|
724
|
+
<style>
|
|
725
|
+
.environment-dashboard-wrapper *,
|
|
726
|
+
.environment-dashboard-wrapper *::before,
|
|
727
|
+
.environment-dashboard-wrapper *::after {
|
|
728
|
+
box-sizing: border-box;
|
|
729
|
+
}
|
|
647
730
|
|
|
731
|
+
.environment-dashboard-wrapper {
|
|
732
|
+
--primary-color: #007bff;
|
|
733
|
+
--primary-light-color: #e6f2ff;
|
|
734
|
+
--secondary-color: #6c757d;
|
|
735
|
+
--success-color: #28a745;
|
|
736
|
+
--success-light-color: #eaf6ec;
|
|
737
|
+
--warning-color: #ffc107;
|
|
738
|
+
--warning-light-color: #fff9e6;
|
|
739
|
+
--danger-color: #dc3545;
|
|
740
|
+
|
|
741
|
+
--background-color: #ffffff;
|
|
742
|
+
--card-background-color: #ffffff;
|
|
743
|
+
--text-color: #212529;
|
|
744
|
+
--text-color-secondary: #6c757d;
|
|
745
|
+
--border-color: #dee2e6;
|
|
746
|
+
--border-light-color: #f1f3f5;
|
|
747
|
+
--icon-color: #495057;
|
|
748
|
+
--chip-background: #e9ecef;
|
|
749
|
+
--chip-text: #495057;
|
|
750
|
+
--shadow-color: rgba(0, 0, 0, 0.075);
|
|
751
|
+
|
|
752
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
|
|
753
|
+
background-color: var(--background-color);
|
|
754
|
+
border-radius: 12px;
|
|
755
|
+
box-shadow: 0 6px 12px var(--shadow-color);
|
|
756
|
+
padding: 24px;
|
|
757
|
+
color: var(--text-color);
|
|
758
|
+
display: grid;
|
|
759
|
+
grid-template-columns: 1fr 1fr;
|
|
760
|
+
grid-template-rows: auto 1fr;
|
|
761
|
+
gap: 20px;
|
|
762
|
+
font-size: 14px;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
.env-dashboard-header {
|
|
766
|
+
grid-column: 1 / -1;
|
|
767
|
+
display: flex;
|
|
768
|
+
justify-content: space-between;
|
|
769
|
+
align-items: center;
|
|
770
|
+
border-bottom: 1px solid var(--border-color);
|
|
771
|
+
padding-bottom: 16px;
|
|
772
|
+
margin-bottom: 8px;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.env-dashboard-title {
|
|
776
|
+
font-size: 1.5rem;
|
|
777
|
+
font-weight: 600;
|
|
778
|
+
color: var(--text-color);
|
|
779
|
+
margin: 0;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
.env-dashboard-subtitle {
|
|
783
|
+
font-size: 0.875rem;
|
|
784
|
+
color: var(--text-color-secondary);
|
|
785
|
+
margin-top: 4px;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.env-card {
|
|
789
|
+
background-color: var(--card-background-color);
|
|
790
|
+
border-radius: 8px;
|
|
791
|
+
padding: ${cardContentPadding}px;
|
|
792
|
+
box-shadow: 0 3px 6px var(--shadow-color);
|
|
793
|
+
height: ${cardHeight}px;
|
|
794
|
+
display: flex;
|
|
795
|
+
flex-direction: column;
|
|
796
|
+
overflow: hidden;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.env-card-header {
|
|
800
|
+
font-weight: 600;
|
|
801
|
+
font-size: 1rem;
|
|
802
|
+
margin-bottom: 12px;
|
|
803
|
+
color: var(--text-color);
|
|
804
|
+
display: flex;
|
|
805
|
+
align-items: center;
|
|
806
|
+
padding-bottom: 8px;
|
|
807
|
+
border-bottom: 1px solid var(--border-light-color);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
.env-card-header svg {
|
|
811
|
+
margin-right: 10px;
|
|
812
|
+
width: 18px;
|
|
813
|
+
height: 18px;
|
|
814
|
+
fill: var(--icon-color);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.env-card-content {
|
|
818
|
+
flex-grow: 1;
|
|
819
|
+
overflow-y: auto;
|
|
820
|
+
padding-right: 5px;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.env-detail-row {
|
|
824
|
+
display: flex;
|
|
825
|
+
justify-content: space-between;
|
|
826
|
+
align-items: center;
|
|
827
|
+
padding: 10px 0;
|
|
828
|
+
border-bottom: 1px solid var(--border-light-color);
|
|
829
|
+
font-size: 0.875rem;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.env-detail-row:last-child {
|
|
833
|
+
border-bottom: none;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.env-detail-label {
|
|
837
|
+
color: var(--text-color-secondary);
|
|
838
|
+
font-weight: 500;
|
|
839
|
+
margin-right: 10px;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.env-detail-value {
|
|
843
|
+
color: var(--text-color);
|
|
844
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
845
|
+
text-align: right;
|
|
846
|
+
word-break: break-all;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.env-chip {
|
|
850
|
+
display: inline-block;
|
|
851
|
+
padding: 4px 10px;
|
|
852
|
+
border-radius: 16px;
|
|
853
|
+
font-size: 0.75rem;
|
|
854
|
+
font-weight: 500;
|
|
855
|
+
line-height: 1.2;
|
|
856
|
+
background-color: var(--chip-background);
|
|
857
|
+
color: var(--chip-text);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.env-chip-primary {
|
|
861
|
+
background-color: var(--primary-light-color);
|
|
862
|
+
color: var(--primary-color);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.env-chip-success {
|
|
866
|
+
background-color: var(--success-light-color);
|
|
867
|
+
color: var(--success-color);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.env-chip-warning {
|
|
871
|
+
background-color: var(--warning-light-color);
|
|
872
|
+
color: var(--warning-color);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
.env-cpu-cores {
|
|
876
|
+
display: flex;
|
|
877
|
+
align-items: center;
|
|
878
|
+
gap: 6px;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.env-core-indicator {
|
|
882
|
+
width: 12px;
|
|
883
|
+
height: 12px;
|
|
884
|
+
border-radius: 50%;
|
|
885
|
+
background-color: var(--success-color);
|
|
886
|
+
border: 1px solid rgba(0,0,0,0.1);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.env-core-indicator.inactive {
|
|
890
|
+
background-color: var(--border-light-color);
|
|
891
|
+
opacity: 0.7;
|
|
892
|
+
border-color: var(--border-color);
|
|
893
|
+
}
|
|
894
|
+
</style>
|
|
895
|
+
|
|
896
|
+
<div class="env-dashboard-header">
|
|
897
|
+
<div>
|
|
898
|
+
<h3 class="env-dashboard-title">System Environment</h3>
|
|
899
|
+
<p class="env-dashboard-subtitle">Snapshot of the execution environment</p>
|
|
900
|
+
</div>
|
|
901
|
+
<span class="env-chip env-chip-primary">${environment.host}</span>
|
|
902
|
+
</div>
|
|
903
|
+
|
|
904
|
+
<div class="env-card">
|
|
905
|
+
<div class="env-card-header">
|
|
906
|
+
<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>
|
|
907
|
+
Hardware
|
|
908
|
+
</div>
|
|
909
|
+
<div class="env-card-content">
|
|
910
|
+
<div class="env-detail-row">
|
|
911
|
+
<span class="env-detail-label">CPU Model</span>
|
|
912
|
+
<span class="env-detail-value">${environment.cpu.model}</span>
|
|
913
|
+
</div>
|
|
914
|
+
<div class="env-detail-row">
|
|
915
|
+
<span class="env-detail-label">CPU Cores</span>
|
|
916
|
+
<span class="env-detail-value">
|
|
917
|
+
<div class="env-cpu-cores">
|
|
918
|
+
${Array.from(
|
|
919
|
+
{ length: Math.max(0, environment.cpu.cores || 0) },
|
|
920
|
+
(_, i) =>
|
|
921
|
+
`<div class="env-core-indicator ${
|
|
922
|
+
i >=
|
|
923
|
+
(environment.cpu.cores >= 8 ? 8 : environment.cpu.cores)
|
|
924
|
+
? "inactive"
|
|
925
|
+
: ""
|
|
926
|
+
}" title="Core ${i + 1}"></div>`
|
|
927
|
+
).join("")}
|
|
928
|
+
<span>${environment.cpu.cores || "N/A"} cores</span>
|
|
929
|
+
</div>
|
|
930
|
+
</span>
|
|
931
|
+
</div>
|
|
932
|
+
<div class="env-detail-row">
|
|
933
|
+
<span class="env-detail-label">Memory</span>
|
|
934
|
+
<span class="env-detail-value">${formattedMemory}</span>
|
|
935
|
+
</div>
|
|
936
|
+
</div>
|
|
937
|
+
</div>
|
|
938
|
+
|
|
939
|
+
<div class="env-card">
|
|
940
|
+
<div class="env-card-header">
|
|
941
|
+
<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>
|
|
942
|
+
Operating System
|
|
943
|
+
</div>
|
|
944
|
+
<div class="env-card-content">
|
|
945
|
+
<div class="env-detail-row">
|
|
946
|
+
<span class="env-detail-label">OS Type</span>
|
|
947
|
+
<span class="env-detail-value">${
|
|
948
|
+
environment.os.split(" ")[0] === "darwin"
|
|
949
|
+
? "darwin (macOS)"
|
|
950
|
+
: environment.os.split(" ")[0] || "Unknown"
|
|
951
|
+
}</span>
|
|
952
|
+
</div>
|
|
953
|
+
<div class="env-detail-row">
|
|
954
|
+
<span class="env-detail-label">OS Version</span>
|
|
955
|
+
<span class="env-detail-value">${
|
|
956
|
+
environment.os.split(" ")[1] || "N/A"
|
|
957
|
+
}</span>
|
|
958
|
+
</div>
|
|
959
|
+
<div class="env-detail-row">
|
|
960
|
+
<span class="env-detail-label">Hostname</span>
|
|
961
|
+
<span class="env-detail-value" title="${environment.host}">${
|
|
962
|
+
environment.host
|
|
963
|
+
}</span>
|
|
964
|
+
</div>
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
|
|
968
|
+
<div class="env-card">
|
|
969
|
+
<div class="env-card-header">
|
|
970
|
+
<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>
|
|
971
|
+
Node.js Runtime
|
|
972
|
+
</div>
|
|
973
|
+
<div class="env-card-content">
|
|
974
|
+
<div class="env-detail-row">
|
|
975
|
+
<span class="env-detail-label">Node Version</span>
|
|
976
|
+
<span class="env-detail-value">${environment.node}</span>
|
|
977
|
+
</div>
|
|
978
|
+
<div class="env-detail-row">
|
|
979
|
+
<span class="env-detail-label">V8 Engine</span>
|
|
980
|
+
<span class="env-detail-value">${environment.v8}</span>
|
|
981
|
+
</div>
|
|
982
|
+
<div class="env-detail-row">
|
|
983
|
+
<span class="env-detail-label">Working Dir</span>
|
|
984
|
+
<span class="env-detail-value" title="${environment.cwd}">${
|
|
985
|
+
environment.cwd.length > 25
|
|
986
|
+
? "..." + environment.cwd.slice(-22)
|
|
987
|
+
: environment.cwd
|
|
988
|
+
}</span>
|
|
989
|
+
</div>
|
|
990
|
+
</div>
|
|
991
|
+
</div>
|
|
992
|
+
|
|
993
|
+
<div class="env-card">
|
|
994
|
+
<div class="env-card-header">
|
|
995
|
+
<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>
|
|
996
|
+
System Summary
|
|
997
|
+
</div>
|
|
998
|
+
<div class="env-card-content">
|
|
999
|
+
<div class="env-detail-row">
|
|
1000
|
+
<span class="env-detail-label">Platform Arch</span>
|
|
1001
|
+
<span class="env-detail-value">
|
|
1002
|
+
<span class="env-chip ${
|
|
1003
|
+
environment.os.includes("darwin") &&
|
|
1004
|
+
environment.cpu.model.toLowerCase().includes("apple")
|
|
1005
|
+
? "env-chip-success"
|
|
1006
|
+
: "env-chip-warning"
|
|
1007
|
+
}">
|
|
1008
|
+
${
|
|
1009
|
+
environment.os.includes("darwin") &&
|
|
1010
|
+
environment.cpu.model.toLowerCase().includes("apple")
|
|
1011
|
+
? "Apple Silicon"
|
|
1012
|
+
: environment.cpu.model.toLowerCase().includes("arm") ||
|
|
1013
|
+
environment.cpu.model.toLowerCase().includes("aarch64")
|
|
1014
|
+
? "ARM-based"
|
|
1015
|
+
: "x86/Other"
|
|
1016
|
+
}
|
|
1017
|
+
</span>
|
|
1018
|
+
</span>
|
|
1019
|
+
</div>
|
|
1020
|
+
<div class="env-detail-row">
|
|
1021
|
+
<span class="env-detail-label">Memory per Core</span>
|
|
1022
|
+
<span class="env-detail-value">${
|
|
1023
|
+
environment.cpu.cores > 0
|
|
1024
|
+
? (
|
|
1025
|
+
parseFloat(environment.memory) / environment.cpu.cores
|
|
1026
|
+
).toFixed(2) + " GB"
|
|
1027
|
+
: "N/A"
|
|
1028
|
+
}</span>
|
|
1029
|
+
</div>
|
|
1030
|
+
<div class="env-detail-row">
|
|
1031
|
+
<span class="env-detail-label">Run Context</span>
|
|
1032
|
+
<span class="env-detail-value">CI/Local Test</span>
|
|
1033
|
+
</div>
|
|
1034
|
+
</div>
|
|
1035
|
+
</div>
|
|
1036
|
+
</div>
|
|
1037
|
+
`;
|
|
1038
|
+
}
|
|
648
1039
|
function generateTestHistoryContent(trendData) {
|
|
649
1040
|
if (
|
|
650
1041
|
!trendData ||
|
|
@@ -730,7 +1121,7 @@ function generateTestHistoryContent(trendData) {
|
|
|
730
1121
|
</span>
|
|
731
1122
|
</div>
|
|
732
1123
|
<div class="test-history-trend">
|
|
733
|
-
${generateTestHistoryChart(test.history)}
|
|
1124
|
+
${generateTestHistoryChart(test.history)}
|
|
734
1125
|
</div>
|
|
735
1126
|
<details class="test-history-details-collapsible">
|
|
736
1127
|
<summary>Show Run Details (${test.history.length})</summary>
|
|
@@ -764,7 +1155,6 @@ function generateTestHistoryContent(trendData) {
|
|
|
764
1155
|
</div>
|
|
765
1156
|
`;
|
|
766
1157
|
}
|
|
767
|
-
|
|
768
1158
|
function getStatusClass(status) {
|
|
769
1159
|
switch (String(status).toLowerCase()) {
|
|
770
1160
|
case "passed":
|
|
@@ -777,7 +1167,6 @@ function getStatusClass(status) {
|
|
|
777
1167
|
return "status-unknown";
|
|
778
1168
|
}
|
|
779
1169
|
}
|
|
780
|
-
|
|
781
1170
|
function getStatusIcon(status) {
|
|
782
1171
|
switch (String(status).toLowerCase()) {
|
|
783
1172
|
case "passed":
|
|
@@ -790,7 +1179,6 @@ function getStatusIcon(status) {
|
|
|
790
1179
|
return "❓";
|
|
791
1180
|
}
|
|
792
1181
|
}
|
|
793
|
-
|
|
794
1182
|
function getSuitesData(results) {
|
|
795
1183
|
const suitesMap = new Map();
|
|
796
1184
|
if (!results || results.length === 0) return [];
|
|
@@ -833,19 +1221,12 @@ function getSuitesData(results) {
|
|
|
833
1221
|
if (currentStatus && suite[currentStatus] !== undefined) {
|
|
834
1222
|
suite[currentStatus]++;
|
|
835
1223
|
}
|
|
836
|
-
|
|
837
|
-
if (currentStatus === "failed")
|
|
838
|
-
suite.statusOverall = "failed";
|
|
839
|
-
} else if (
|
|
840
|
-
currentStatus === "skipped" &&
|
|
841
|
-
suite.statusOverall !== "failed"
|
|
842
|
-
) {
|
|
1224
|
+
if (currentStatus === "failed") suite.statusOverall = "failed";
|
|
1225
|
+
else if (currentStatus === "skipped" && suite.statusOverall !== "failed")
|
|
843
1226
|
suite.statusOverall = "skipped";
|
|
844
|
-
}
|
|
845
1227
|
});
|
|
846
1228
|
return Array.from(suitesMap.values());
|
|
847
1229
|
}
|
|
848
|
-
|
|
849
1230
|
function generateSuitesWidget(suitesData) {
|
|
850
1231
|
if (!suitesData || suitesData.length === 0) {
|
|
851
1232
|
return `<div class="suites-widget"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
|
|
@@ -854,12 +1235,12 @@ function generateSuitesWidget(suitesData) {
|
|
|
854
1235
|
<div class="suites-widget">
|
|
855
1236
|
<div class="suites-header">
|
|
856
1237
|
<h2>Test Suites</h2>
|
|
857
|
-
<span class="summary-badge"
|
|
858
|
-
|
|
1238
|
+
<span class="summary-badge">${
|
|
1239
|
+
suitesData.length
|
|
1240
|
+
} suites • ${suitesData.reduce(
|
|
859
1241
|
(sum, suite) => sum + suite.count,
|
|
860
1242
|
0
|
|
861
|
-
)} tests
|
|
862
|
-
</span>
|
|
1243
|
+
)} tests</span>
|
|
863
1244
|
</div>
|
|
864
1245
|
<div class="suites-grid">
|
|
865
1246
|
${suitesData
|
|
@@ -870,8 +1251,10 @@ function generateSuitesWidget(suitesData) {
|
|
|
870
1251
|
<h3 class="suite-name" title="${sanitizeHTML(
|
|
871
1252
|
suite.name
|
|
872
1253
|
)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
|
|
873
|
-
<span class="browser-tag">${sanitizeHTML(suite.browser)}</span>
|
|
874
1254
|
</div>
|
|
1255
|
+
<div>🖥️ <span class="browser-tag">${sanitizeHTML(
|
|
1256
|
+
suite.browser
|
|
1257
|
+
)}</span></div>
|
|
875
1258
|
<div class="suite-card-body">
|
|
876
1259
|
<span class="test-count">${suite.count} test${
|
|
877
1260
|
suite.count !== 1 ? "s" : ""
|
|
@@ -900,7 +1283,6 @@ function generateSuitesWidget(suitesData) {
|
|
|
900
1283
|
</div>
|
|
901
1284
|
</div>`;
|
|
902
1285
|
}
|
|
903
|
-
|
|
904
1286
|
function generateHTML(reportData, trendData = null) {
|
|
905
1287
|
const { run, results } = reportData;
|
|
906
1288
|
const suitesData = getSuitesData(reportData.results || []);
|
|
@@ -912,8 +1294,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
912
1294
|
duration: 0,
|
|
913
1295
|
timestamp: new Date().toISOString(),
|
|
914
1296
|
};
|
|
915
|
-
|
|
916
|
-
const totalTestsOr1 = runSummary.totalTests || 1; // Avoid division by zero
|
|
1297
|
+
const totalTestsOr1 = runSummary.totalTests || 1;
|
|
917
1298
|
const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
|
|
918
1299
|
const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
|
|
919
1300
|
const skipPercentage = Math.round(
|
|
@@ -923,19 +1304,15 @@ function generateHTML(reportData, trendData = null) {
|
|
|
923
1304
|
runSummary.totalTests > 0
|
|
924
1305
|
? formatDuration(runSummary.duration / runSummary.totalTests)
|
|
925
1306
|
: "0.0s";
|
|
926
|
-
|
|
927
1307
|
function generateTestCasesHTML() {
|
|
928
|
-
if (!results || results.length === 0)
|
|
1308
|
+
if (!results || results.length === 0)
|
|
929
1309
|
return '<div class="no-tests">No test results found in this run.</div>';
|
|
930
|
-
}
|
|
931
|
-
|
|
932
1310
|
return results
|
|
933
1311
|
.map((test, index) => {
|
|
934
1312
|
const browser = test.browser || "unknown";
|
|
935
1313
|
const testFileParts = test.name.split(" > ");
|
|
936
1314
|
const testTitle =
|
|
937
1315
|
testFileParts[testFileParts.length - 1] || "Unnamed Test";
|
|
938
|
-
|
|
939
1316
|
const generateStepsHTML = (steps, depth = 0) => {
|
|
940
1317
|
if (!steps || steps.length === 0)
|
|
941
1318
|
return "<div class='no-steps'>No steps recorded for this test.</div>";
|
|
@@ -947,7 +1324,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
947
1324
|
? `step-hook step-hook-${step.hookType}`
|
|
948
1325
|
: "";
|
|
949
1326
|
const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
|
|
950
|
-
|
|
951
1327
|
return `
|
|
952
1328
|
<div class="step-item" style="--depth: ${depth};">
|
|
953
1329
|
<div class="step-header ${stepClass}" role="button" aria-expanded="false">
|
|
@@ -969,16 +1345,34 @@ function generateHTML(reportData, trendData = null) {
|
|
|
969
1345
|
}
|
|
970
1346
|
${
|
|
971
1347
|
step.errorMessage
|
|
972
|
-
?
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1348
|
+
? `<div class="step-error">
|
|
1349
|
+
${
|
|
1350
|
+
step.stackTrace
|
|
1351
|
+
? `<div class="stack-trace">${formatPlaywrightError(
|
|
1352
|
+
step.stackTrace
|
|
1353
|
+
)}</div>`
|
|
1354
|
+
: ""
|
|
1355
|
+
}
|
|
1356
|
+
<button
|
|
1357
|
+
class="copy-error-btn"
|
|
1358
|
+
onclick="copyErrorToClipboard(this)"
|
|
1359
|
+
style="
|
|
1360
|
+
margin-top: 8px;
|
|
1361
|
+
padding: 4px 8px;
|
|
1362
|
+
background: #f0f0f0;
|
|
1363
|
+
border: 2px solid #ccc;
|
|
1364
|
+
border-radius: 4px;
|
|
1365
|
+
cursor: pointer;
|
|
1366
|
+
font-size: 12px;
|
|
1367
|
+
border-color: #8B0000;
|
|
1368
|
+
color: #8B0000;
|
|
1369
|
+
"
|
|
1370
|
+
onmouseover="this.style.background='#e0e0e0'"
|
|
1371
|
+
onmouseout="this.style.background='#f0f0f0'"
|
|
1372
|
+
>
|
|
1373
|
+
Copy Error Prompt
|
|
1374
|
+
</button>
|
|
1375
|
+
</div>`
|
|
982
1376
|
: ""
|
|
983
1377
|
}
|
|
984
1378
|
${
|
|
@@ -995,6 +1389,16 @@ function generateHTML(reportData, trendData = null) {
|
|
|
995
1389
|
.join("");
|
|
996
1390
|
};
|
|
997
1391
|
|
|
1392
|
+
// Local escapeHTML for screenshot rendering part, ensuring it uses proper entities
|
|
1393
|
+
const escapeHTMLForScreenshots = (str) => {
|
|
1394
|
+
if (str === null || str === undefined) return "";
|
|
1395
|
+
return String(str).replace(
|
|
1396
|
+
/[&<>"']/g,
|
|
1397
|
+
(match) =>
|
|
1398
|
+
({ "&": "&", "<": "<", ">": ">", '"': '"', "'": "'" }[match] ||
|
|
1399
|
+
match)
|
|
1400
|
+
);
|
|
1401
|
+
};
|
|
998
1402
|
return `
|
|
999
1403
|
<div class="test-case" data-status="${
|
|
1000
1404
|
test.status
|
|
@@ -1025,72 +1429,140 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1025
1429
|
<div class="test-case-content" style="display: none;">
|
|
1026
1430
|
<p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
|
|
1027
1431
|
${
|
|
1028
|
-
test.
|
|
1029
|
-
? `<div class="test-error-summary"
|
|
1030
|
-
|
|
1031
|
-
|
|
1432
|
+
test.errorMessage
|
|
1433
|
+
? `<div class="test-error-summary">${formatPlaywrightError(
|
|
1434
|
+
test.errorMessage
|
|
1435
|
+
)}
|
|
1436
|
+
<button
|
|
1437
|
+
class="copy-error-btn"
|
|
1438
|
+
onclick="copyErrorToClipboard(this)"
|
|
1439
|
+
style="
|
|
1440
|
+
margin-top: 8px;
|
|
1441
|
+
padding: 4px 8px;
|
|
1442
|
+
background: #f0f0f0;
|
|
1443
|
+
border: 2px solid #ccc;
|
|
1444
|
+
border-radius: 4px;
|
|
1445
|
+
cursor: pointer;
|
|
1446
|
+
font-size: 12px;
|
|
1447
|
+
border-color: #8B0000;
|
|
1448
|
+
color: #8B0000;
|
|
1449
|
+
"
|
|
1450
|
+
onmouseover="this.style.background='#e0e0e0'"
|
|
1451
|
+
onmouseout="this.style.background='#f0f0f0'"
|
|
1452
|
+
>
|
|
1453
|
+
Copy Error Prompt
|
|
1454
|
+
</button>
|
|
1455
|
+
</div>`
|
|
1032
1456
|
: ""
|
|
1033
1457
|
}
|
|
1034
|
-
|
|
1035
1458
|
<h4>Steps</h4>
|
|
1036
1459
|
<div class="steps-list">${generateStepsHTML(test.steps)}</div>
|
|
1037
|
-
|
|
1038
1460
|
${
|
|
1039
1461
|
test.stdout && test.stdout.length > 0
|
|
1040
|
-
?
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
<pre class="console-log stdout-log" style="background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${test.stdout
|
|
1044
|
-
.map((line) => sanitizeHTML(line))
|
|
1045
|
-
.join("\n")}</pre>
|
|
1046
|
-
</div>`
|
|
1462
|
+
? `<div class="console-output-section"><h4>Console Output (stdout)</h4><pre class="console-log stdout-log" style="background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${test.stdout
|
|
1463
|
+
.map((line) => sanitizeHTML(line))
|
|
1464
|
+
.join("\n")}</pre></div>`
|
|
1047
1465
|
: ""
|
|
1048
1466
|
}
|
|
1049
1467
|
${
|
|
1050
1468
|
test.stderr && test.stderr.length > 0
|
|
1051
|
-
?
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
<pre class="console-log stderr-log" style="background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${test.stderr
|
|
1055
|
-
.map((line) => sanitizeHTML(line))
|
|
1056
|
-
.join("\n")}</pre>
|
|
1057
|
-
</div>`
|
|
1469
|
+
? `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log" style="background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${test.stderr
|
|
1470
|
+
.map((line) => sanitizeHTML(line))
|
|
1471
|
+
.join("\n")}</pre></div>`
|
|
1058
1472
|
: ""
|
|
1059
1473
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1474
|
+
${(() => {
|
|
1475
|
+
// Screenshots
|
|
1476
|
+
if (!test.screenshots || test.screenshots.length === 0) return "";
|
|
1477
|
+
const baseOutputDir = path.resolve(
|
|
1478
|
+
process.cwd(),
|
|
1479
|
+
DEFAULT_OUTPUT_DIR
|
|
1480
|
+
);
|
|
1481
|
+
|
|
1482
|
+
const renderScreenshot = (screenshotPathOrData, index) => {
|
|
1483
|
+
let base64ImageData = "";
|
|
1484
|
+
const uniqueSuffix = `${Date.now()}-${index}-${Math.random()
|
|
1485
|
+
.toString(36)
|
|
1486
|
+
.substring(2, 7)}`;
|
|
1487
|
+
try {
|
|
1488
|
+
if (
|
|
1489
|
+
typeof screenshotPathOrData === "string" &&
|
|
1490
|
+
!screenshotPathOrData.startsWith("data:image")
|
|
1491
|
+
) {
|
|
1492
|
+
const imagePath = path.resolve(
|
|
1493
|
+
baseOutputDir,
|
|
1494
|
+
screenshotPathOrData
|
|
1495
|
+
);
|
|
1496
|
+
if (fsExistsSync(imagePath))
|
|
1497
|
+
base64ImageData =
|
|
1498
|
+
readFileSync(imagePath).toString("base64");
|
|
1499
|
+
else {
|
|
1500
|
+
console.warn(
|
|
1501
|
+
chalk.yellow(
|
|
1502
|
+
`[Reporter] Screenshot file not found: ${imagePath}`
|
|
1503
|
+
)
|
|
1504
|
+
);
|
|
1505
|
+
return `<div class="attachment-item error" style="padding:10px; color:red;">Screenshot not found: ${escapeHTMLForScreenshots(
|
|
1506
|
+
screenshotPathOrData
|
|
1507
|
+
)}</div>`;
|
|
1508
|
+
}
|
|
1509
|
+
} else if (
|
|
1510
|
+
typeof screenshotPathOrData === "string" &&
|
|
1511
|
+
screenshotPathOrData.startsWith("data:image/png;base64,")
|
|
1512
|
+
)
|
|
1513
|
+
base64ImageData = screenshotPathOrData.substring(
|
|
1514
|
+
"data:image/png;base64,".length
|
|
1515
|
+
);
|
|
1516
|
+
else if (typeof screenshotPathOrData === "string")
|
|
1517
|
+
base64ImageData = screenshotPathOrData;
|
|
1518
|
+
else {
|
|
1519
|
+
console.warn(
|
|
1520
|
+
chalk.yellow(
|
|
1521
|
+
`[Reporter] Invalid screenshot data type for item at index ${index}.`
|
|
1522
|
+
)
|
|
1523
|
+
);
|
|
1524
|
+
return `<div class="attachment-item error" style="padding:10px; color:red;">Invalid screenshot data</div>`;
|
|
1525
|
+
}
|
|
1526
|
+
if (!base64ImageData) {
|
|
1527
|
+
console.warn(
|
|
1528
|
+
chalk.yellow(
|
|
1529
|
+
`[Reporter] Could not obtain base64 data for screenshot: ${escapeHTMLForScreenshots(
|
|
1530
|
+
String(screenshotPathOrData)
|
|
1531
|
+
)}`
|
|
1532
|
+
)
|
|
1533
|
+
);
|
|
1534
|
+
return `<div class="attachment-item error" style="padding:10px; color:red;">Error loading screenshot: ${escapeHTMLForScreenshots(
|
|
1535
|
+
String(screenshotPathOrData)
|
|
1536
|
+
)}</div>`;
|
|
1537
|
+
}
|
|
1538
|
+
return `<div class="attachment-item"><img src="data:image/png;base64,${base64ImageData}" alt="Screenshot ${
|
|
1539
|
+
index + 1
|
|
1540
|
+
}" loading="lazy" onerror="this.alt='Error displaying embedded image'; this.style.display='none'; this.parentElement.innerHTML='<p style=\\'color:red;padding:10px;\\'>Error displaying screenshot ${
|
|
1541
|
+
index + 1
|
|
1542
|
+
}.</p>';"><div class="attachment-info"><div class="trace-actions"><a href="data:image/png;base64,${base64ImageData}" target="_blank" class="view-full">View Full Image</a><a href="data:image/png;base64,${base64ImageData}" target="_blank" download="screenshot-${uniqueSuffix}.png">Download</a></div></div></div>`;
|
|
1543
|
+
} catch (e) {
|
|
1544
|
+
console.error(
|
|
1545
|
+
chalk.red(
|
|
1546
|
+
`[Reporter] Error processing screenshot ${escapeHTMLForScreenshots(
|
|
1547
|
+
String(screenshotPathOrData)
|
|
1548
|
+
)}: ${e.message}`
|
|
1078
1549
|
)
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1550
|
+
);
|
|
1551
|
+
return `<div class="attachment-item error" style="padding:10px; color:red;">Failed to load screenshot: ${escapeHTMLForScreenshots(
|
|
1552
|
+
String(screenshotPathOrData)
|
|
1553
|
+
)}</div>`;
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
return `<div class="attachments-section"><h4>Screenshots (${
|
|
1557
|
+
test.screenshots.length
|
|
1558
|
+
})</h4><div class="attachments-grid">${test.screenshots
|
|
1559
|
+
.map(renderScreenshot)
|
|
1560
|
+
.join("")}</div></div>`;
|
|
1561
|
+
})()}
|
|
1086
1562
|
${
|
|
1087
1563
|
test.videoPath
|
|
1088
|
-
?
|
|
1089
|
-
|
|
1090
|
-
<h4>Videos</h4>
|
|
1091
|
-
<div class="attachments-grid">
|
|
1092
|
-
${(() => {
|
|
1093
|
-
// Handle both string and array cases
|
|
1564
|
+
? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${(() => {
|
|
1565
|
+
// Videos
|
|
1094
1566
|
const videos = Array.isArray(test.videoPath)
|
|
1095
1567
|
? test.videoPath
|
|
1096
1568
|
: [test.videoPath];
|
|
@@ -1101,7 +1573,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1101
1573
|
mov: "video/quicktime",
|
|
1102
1574
|
avi: "video/x-msvideo",
|
|
1103
1575
|
};
|
|
1104
|
-
|
|
1105
1576
|
return videos
|
|
1106
1577
|
.map((video, index) => {
|
|
1107
1578
|
const videoUrl =
|
|
@@ -1110,82 +1581,53 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1110
1581
|
typeof video === "object"
|
|
1111
1582
|
? video.name || `Video ${index + 1}`
|
|
1112
1583
|
: `Video ${index + 1}`;
|
|
1113
|
-
const fileExtension = videoUrl
|
|
1584
|
+
const fileExtension = String(videoUrl)
|
|
1114
1585
|
.split(".")
|
|
1115
1586
|
.pop()
|
|
1116
1587
|
.toLowerCase();
|
|
1117
1588
|
const mimeType = mimeTypes[fileExtension] || "video/mp4";
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
<a href="${videoUrl}" target="_blank" download="${videoName}.${fileExtension}">
|
|
1128
|
-
Download
|
|
1129
|
-
</a>
|
|
1130
|
-
</div>
|
|
1131
|
-
</div>
|
|
1132
|
-
</div>
|
|
1133
|
-
`;
|
|
1589
|
+
return `<div class="attachment-item"><video controls width="100%" height="auto" title="${sanitizeHTML(
|
|
1590
|
+
videoName
|
|
1591
|
+
)}"><source src="${sanitizeHTML(
|
|
1592
|
+
videoUrl
|
|
1593
|
+
)}" type="${mimeType}">Your browser does not support the video tag.</video><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
|
|
1594
|
+
videoUrl
|
|
1595
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
1596
|
+
videoName
|
|
1597
|
+
)}.${fileExtension}">Download</a></div></div></div>`;
|
|
1134
1598
|
})
|
|
1135
1599
|
.join("");
|
|
1136
|
-
})()}
|
|
1137
|
-
</div>
|
|
1138
|
-
</div>
|
|
1139
|
-
`
|
|
1600
|
+
})()}</div></div>`
|
|
1140
1601
|
: ""
|
|
1141
1602
|
}
|
|
1142
|
-
|
|
1143
1603
|
${
|
|
1144
1604
|
test.tracePath
|
|
1145
|
-
?
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
<span class="trace-name">${traceName}</span>
|
|
1170
|
-
</div>
|
|
1171
|
-
<div class="attachment-info">
|
|
1172
|
-
<div class="trace-actions">
|
|
1173
|
-
<a href="${traceUrl}" target="_blank" download="${traceFileName}" class="download-trace">
|
|
1174
|
-
Download
|
|
1175
|
-
</a>
|
|
1176
|
-
</div>
|
|
1177
|
-
</div>
|
|
1178
|
-
</div>
|
|
1179
|
-
`;
|
|
1180
|
-
})
|
|
1181
|
-
.join("");
|
|
1182
|
-
})()}
|
|
1183
|
-
</div>
|
|
1184
|
-
</div>
|
|
1185
|
-
`
|
|
1605
|
+
? `<div class="attachments-section"><h4>Trace Files</h4><div class="attachments-grid">${(() => {
|
|
1606
|
+
// Traces
|
|
1607
|
+
const traces = Array.isArray(test.tracePath)
|
|
1608
|
+
? test.tracePath
|
|
1609
|
+
: [test.tracePath];
|
|
1610
|
+
return traces
|
|
1611
|
+
.map((trace, index) => {
|
|
1612
|
+
const traceUrl =
|
|
1613
|
+
typeof trace === "object" ? trace.url || "" : trace;
|
|
1614
|
+
const traceName =
|
|
1615
|
+
typeof trace === "object"
|
|
1616
|
+
? trace.name || `Trace ${index + 1}`
|
|
1617
|
+
: `Trace ${index + 1}`;
|
|
1618
|
+
const traceFileName = String(traceUrl).split("/").pop();
|
|
1619
|
+
return `<div class="attachment-item"><div class="trace-preview"><span class="trace-icon">📄</span><span class="trace-name">${sanitizeHTML(
|
|
1620
|
+
traceName
|
|
1621
|
+
)}</span></div><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
|
|
1622
|
+
traceUrl
|
|
1623
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
1624
|
+
traceFileName
|
|
1625
|
+
)}" class="download-trace">Download</a></div></div></div>`;
|
|
1626
|
+
})
|
|
1627
|
+
.join("");
|
|
1628
|
+
})()}</div></div>`
|
|
1186
1629
|
: ""
|
|
1187
1630
|
}
|
|
1188
|
-
|
|
1189
1631
|
${
|
|
1190
1632
|
test.codeSnippet
|
|
1191
1633
|
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${sanitizeHTML(
|
|
@@ -1198,7 +1640,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1198
1640
|
})
|
|
1199
1641
|
.join("");
|
|
1200
1642
|
}
|
|
1201
|
-
|
|
1202
1643
|
return `
|
|
1203
1644
|
<!DOCTYPE html>
|
|
1204
1645
|
<html lang="en">
|
|
@@ -1207,33 +1648,20 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1207
1648
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1208
1649
|
<link rel="icon" type="image/png" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
|
|
1209
1650
|
<link rel="apple-touch-icon" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
|
|
1210
|
-
<script src="https://code.highcharts.com/highcharts.js"></script>
|
|
1651
|
+
<script src="https://code.highcharts.com/highcharts.js" defer></script>
|
|
1211
1652
|
<title>Playwright Pulse Report</title>
|
|
1212
1653
|
<style>
|
|
1213
|
-
:root {
|
|
1214
|
-
--primary-color: #3f51b5;
|
|
1215
|
-
--
|
|
1216
|
-
--
|
|
1217
|
-
--
|
|
1218
|
-
--
|
|
1219
|
-
--
|
|
1220
|
-
--warning-color: #FFC107; /* Amber */
|
|
1221
|
-
--info-color: #2196F3; /* Blue */
|
|
1222
|
-
--light-gray-color: #f5f5f5;
|
|
1223
|
-
--medium-gray-color: #e0e0e0;
|
|
1224
|
-
--dark-gray-color: #757575;
|
|
1225
|
-
--text-color: #333;
|
|
1226
|
-
--text-color-secondary: #555;
|
|
1227
|
-
--border-color: #ddd;
|
|
1228
|
-
--background-color: #f8f9fa;
|
|
1229
|
-
--card-background-color: #fff;
|
|
1230
|
-
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
1231
|
-
--border-radius: 8px;
|
|
1232
|
-
--box-shadow: 0 5px 15px rgba(0,0,0,0.08);
|
|
1233
|
-
--box-shadow-light: 0 3px 8px rgba(0,0,0,0.05);
|
|
1234
|
-
--box-shadow-inset: inset 0 1px 3px rgba(0,0,0,0.07);
|
|
1654
|
+
:root {
|
|
1655
|
+
--primary-color: #3f51b5; --secondary-color: #ff4081; --accent-color: #673ab7; --accent-color-alt: #FF9800;
|
|
1656
|
+
--success-color: #4CAF50; --danger-color: #F44336; --warning-color: #FFC107; --info-color: #2196F3;
|
|
1657
|
+
--light-gray-color: #f5f5f5; --medium-gray-color: #e0e0e0; --dark-gray-color: #757575;
|
|
1658
|
+
--text-color: #333; --text-color-secondary: #555; --border-color: #ddd; --background-color: #f8f9fa;
|
|
1659
|
+
--card-background-color: #fff; --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
1660
|
+
--border-radius: 8px; --box-shadow: 0 5px 15px rgba(0,0,0,0.08); --box-shadow-light: 0 3px 8px rgba(0,0,0,0.05); --box-shadow-inset: inset 0 1px 3px rgba(0,0,0,0.07);
|
|
1235
1661
|
}
|
|
1236
|
-
|
|
1662
|
+
.trend-chart-container, .test-history-trend div[id^="testHistoryChart-"] { min-height: 100px; }
|
|
1663
|
+
.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); }
|
|
1664
|
+
|
|
1237
1665
|
/* General Highcharts styling */
|
|
1238
1666
|
.highcharts-background { fill: transparent; }
|
|
1239
1667
|
.highcharts-title, .highcharts-subtitle { font-family: var(--font-family); }
|
|
@@ -1241,60 +1669,23 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1241
1669
|
.highcharts-axis-title { fill: var(--text-color) !important; }
|
|
1242
1670
|
.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; }
|
|
1243
1671
|
|
|
1244
|
-
body {
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
background-color: var(--background-color);
|
|
1248
|
-
color: var(--text-color);
|
|
1249
|
-
line-height: 1.65;
|
|
1250
|
-
font-size: 16px;
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
.container {
|
|
1254
|
-
max-width: 1600px;
|
|
1255
|
-
padding: 30px;
|
|
1256
|
-
border-radius: var(--border-radius);
|
|
1257
|
-
box-shadow: var(--box-shadow);
|
|
1258
|
-
background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec);
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
.header {
|
|
1262
|
-
display: flex;
|
|
1263
|
-
justify-content: space-between;
|
|
1264
|
-
align-items: center;
|
|
1265
|
-
flex-wrap: wrap;
|
|
1266
|
-
padding-bottom: 25px;
|
|
1267
|
-
border-bottom: 1px solid var(--border-color);
|
|
1268
|
-
margin-bottom: 25px;
|
|
1269
|
-
}
|
|
1672
|
+
body { font-family: var(--font-family); margin: 0; background-color: var(--background-color); color: var(--text-color); line-height: 1.65; font-size: 16px; }
|
|
1673
|
+
.container { padding: 30px; border-radius: var(--border-radius); box-shadow: var(--box-shadow); background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec); }
|
|
1674
|
+
.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; }
|
|
1270
1675
|
.header-title { display: flex; align-items: center; gap: 15px; }
|
|
1271
1676
|
.header h1 { margin: 0; font-size: 1.85em; font-weight: 600; color: var(--primary-color); }
|
|
1272
1677
|
#report-logo { height: 40px; width: 40px; border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.1);}
|
|
1273
1678
|
.run-info { font-size: 0.9em; text-align: right; color: var(--text-color-secondary); line-height:1.5;}
|
|
1274
1679
|
.run-info strong { color: var(--text-color); }
|
|
1275
|
-
|
|
1276
1680
|
.tabs { display: flex; border-bottom: 2px solid var(--border-color); margin-bottom: 30px; overflow-x: auto; }
|
|
1277
|
-
.tab-button {
|
|
1278
|
-
padding: 15px 25px; background: none; border: none; border-bottom: 3px solid transparent;
|
|
1279
|
-
cursor: pointer; font-size: 1.1em; font-weight: 600; color: black;
|
|
1280
|
-
transition: color 0.2s ease, border-color 0.2s ease; white-space: nowrap;
|
|
1281
|
-
}
|
|
1681
|
+
.tab-button { padding: 15px 25px; background: none; border: none; border-bottom: 3px solid transparent; cursor: pointer; font-size: 1.1em; font-weight: 600; color: black; transition: color 0.2s ease, border-color 0.2s ease; white-space: nowrap; }
|
|
1282
1682
|
.tab-button:hover { color: var(--accent-color); }
|
|
1283
1683
|
.tab-button.active { color: var(--primary-color); border-bottom-color: var(--primary-color); }
|
|
1284
1684
|
.tab-content { display: none; animation: fadeIn 0.4s ease-out; }
|
|
1285
1685
|
.tab-content.active { display: block; }
|
|
1286
1686
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
1287
|
-
|
|
1288
|
-
.
|
|
1289
|
-
display: grid;
|
|
1290
|
-
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
|
1291
|
-
gap: 22px; margin-bottom: 35px;
|
|
1292
|
-
}
|
|
1293
|
-
.summary-card {
|
|
1294
|
-
background-color: var(--card-background-color); border: 1px solid var(--border-color);
|
|
1295
|
-
border-radius: var(--border-radius); padding: 22px; text-align: center;
|
|
1296
|
-
box-shadow: var(--box-shadow-light); transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
1297
|
-
}
|
|
1687
|
+
.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 22px; margin-bottom: 35px; }
|
|
1688
|
+
.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; }
|
|
1298
1689
|
.summary-card:hover { transform: translateY(-5px); box-shadow: var(--box-shadow); }
|
|
1299
1690
|
.summary-card h3 { margin: 0 0 10px; font-size: 1.05em; font-weight: 500; color: var(--text-color-secondary); }
|
|
1300
1691
|
.summary-card .value { font-size: 2.4em; font-weight: 600; margin-bottom: 8px; }
|
|
@@ -1302,43 +1693,19 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1302
1693
|
.status-passed .value, .stat-passed svg { color: var(--success-color); }
|
|
1303
1694
|
.status-failed .value, .stat-failed svg { color: var(--danger-color); }
|
|
1304
1695
|
.status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
|
|
1305
|
-
|
|
1306
|
-
.
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
}
|
|
1310
|
-
.pie-chart-wrapper, .suites-widget, .trend-chart {
|
|
1311
|
-
background-color: var(--card-background-color); padding: 28px;
|
|
1312
|
-
border-radius: var(--border-radius); box-shadow: var(--box-shadow-light);
|
|
1313
|
-
display: flex; flex-direction: column;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
.pie-chart-wrapper h3, .suites-header h2, .trend-chart h3 {
|
|
1317
|
-
text-align: center; margin-top: 0; margin-bottom: 25px;
|
|
1318
|
-
font-size: 1.25em; font-weight: 600; color: var(--text-color);
|
|
1319
|
-
}
|
|
1320
|
-
.trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { /* For Highcharts containers */
|
|
1321
|
-
flex-grow: 1;
|
|
1322
|
-
min-height: 250px; /* Ensure charts have some min height */
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
.chart-tooltip { /* This class was for D3, Highcharts has its own tooltip styling via JS/SVG */
|
|
1326
|
-
/* Basic styling for Highcharts HTML tooltips can be done via .highcharts-tooltip span */
|
|
1327
|
-
}
|
|
1696
|
+
.dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: stretch; }
|
|
1697
|
+
.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; }
|
|
1698
|
+
.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); }
|
|
1699
|
+
.trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { flex-grow: 1; min-height: 250px; }
|
|
1328
1700
|
.status-badge-small-tooltip { padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; }
|
|
1329
1701
|
.status-badge-small-tooltip.status-passed { background-color: var(--success-color); }
|
|
1330
1702
|
.status-badge-small-tooltip.status-failed { background-color: var(--danger-color); }
|
|
1331
1703
|
.status-badge-small-tooltip.status-skipped { background-color: var(--warning-color); }
|
|
1332
1704
|
.status-badge-small-tooltip.status-unknown { background-color: var(--dark-gray-color); }
|
|
1333
|
-
|
|
1334
1705
|
.suites-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
|
1335
1706
|
.summary-badge { background-color: var(--light-gray-color); color: var(--text-color-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
|
|
1336
1707
|
.suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
|
|
1337
|
-
.suite-card {
|
|
1338
|
-
border: 1px solid var(--border-color); border-left-width: 5px;
|
|
1339
|
-
border-radius: calc(var(--border-radius) / 1.5); padding: 20px;
|
|
1340
|
-
background-color: var(--card-background-color); transition: box-shadow 0.2s ease, border-left-color 0.2s ease;
|
|
1341
|
-
}
|
|
1708
|
+
.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; }
|
|
1342
1709
|
.suite-card:hover { box-shadow: var(--box-shadow); }
|
|
1343
1710
|
.suite-card.status-passed { border-left-color: var(--success-color); }
|
|
1344
1711
|
.suite-card.status-failed { border-left-color: var(--danger-color); }
|
|
@@ -1350,67 +1717,36 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1350
1717
|
.suite-stats { display: flex; gap: 14px; font-size: 0.95em; align-items: center; }
|
|
1351
1718
|
.suite-stats span { display: flex; align-items: center; gap: 6px; }
|
|
1352
1719
|
.suite-stats svg { vertical-align: middle; font-size: 1.15em; }
|
|
1353
|
-
|
|
1354
|
-
.filters {
|
|
1355
|
-
display: flex; flex-wrap: wrap; gap: 18px; margin-bottom: 28px;
|
|
1356
|
-
padding: 20px; background-color: var(--light-gray-color); border-radius: var(--border-radius);
|
|
1357
|
-
box-shadow: var(--box-shadow-inset); border-color: black; border-style: groove;
|
|
1358
|
-
}
|
|
1359
|
-
.filters input, .filters select, .filters button {
|
|
1360
|
-
padding: 11px 15px; border: 1px solid var(--border-color);
|
|
1361
|
-
border-radius: 6px; font-size: 1em;
|
|
1362
|
-
}
|
|
1720
|
+
.filters { display: flex; flex-wrap: wrap; gap: 18px; margin-bottom: 28px; padding: 20px; background-color: var(--light-gray-color); border-radius: var(--border-radius); box-shadow: var(--box-shadow-inset); border-color: black; border-style: groove; }
|
|
1721
|
+
.filters input, .filters select, .filters button { padding: 11px 15px; border: 1px solid var(--border-color); border-radius: 6px; font-size: 1em; }
|
|
1363
1722
|
.filters input { flex-grow: 1; min-width: 240px;}
|
|
1364
1723
|
.filters select {min-width: 180px;}
|
|
1365
1724
|
.filters button { background-color: var(--primary-color); color: white; cursor: pointer; transition: background-color 0.2s ease, box-shadow 0.2s ease; border: none; }
|
|
1366
1725
|
.filters button:hover { background-color: var(--accent-color); box-shadow: 0 2px 5px rgba(0,0,0,0.15);}
|
|
1367
|
-
|
|
1368
|
-
.test-case {
|
|
1369
|
-
margin-bottom: 15px; border: 1px solid var(--border-color);
|
|
1370
|
-
border-radius: var(--border-radius); background-color: var(--card-background-color);
|
|
1371
|
-
box-shadow: var(--box-shadow-light); overflow: hidden;
|
|
1372
|
-
}
|
|
1373
|
-
.test-case-header {
|
|
1374
|
-
padding: 10px 15px; background-color: #fff; cursor: pointer;
|
|
1375
|
-
display: flex; justify-content: space-between; align-items: center;
|
|
1376
|
-
border-bottom: 1px solid transparent;
|
|
1377
|
-
transition: background-color 0.2s ease;
|
|
1378
|
-
}
|
|
1726
|
+
.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; }
|
|
1727
|
+
.test-case-header { padding: 10px 15px; background-color: #fff; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid transparent; transition: background-color 0.2s ease; }
|
|
1379
1728
|
.test-case-header:hover { background-color: #f4f6f8; }
|
|
1380
1729
|
.test-case-header[aria-expanded="true"] { border-bottom-color: var(--border-color); background-color: #f9fafb; }
|
|
1381
|
-
|
|
1382
1730
|
.test-case-summary { display: flex; align-items: center; gap: 14px; flex-grow: 1; flex-wrap: wrap;}
|
|
1383
1731
|
.test-case-title { font-weight: 600; color: var(--text-color); font-size: 1em; }
|
|
1384
1732
|
.test-case-browser { font-size: 0.9em; color: var(--text-color-secondary); }
|
|
1385
1733
|
.test-case-meta { display: flex; align-items: center; gap: 12px; font-size: 0.9em; color: var(--text-color-secondary); flex-shrink: 0; }
|
|
1386
1734
|
.test-duration { background-color: var(--light-gray-color); padding: 4px 10px; border-radius: 12px; font-size: 0.9em;}
|
|
1387
|
-
|
|
1388
|
-
.status-badge {
|
|
1389
|
-
padding: 5px; border-radius: 6px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase;
|
|
1390
|
-
min-width: 70px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
1391
|
-
}
|
|
1735
|
+
.status-badge { padding: 5px; border-radius: 6px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase; min-width: 70px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
1392
1736
|
.status-badge.status-passed { background-color: var(--success-color); }
|
|
1393
1737
|
.status-badge.status-failed { background-color: var(--danger-color); }
|
|
1394
1738
|
.status-badge.status-skipped { background-color: var(--warning-color); }
|
|
1395
1739
|
.status-badge.status-unknown { background-color: var(--dark-gray-color); }
|
|
1396
|
-
|
|
1397
1740
|
.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; }
|
|
1398
|
-
|
|
1399
1741
|
.test-case-content { display: none; padding: 20px; border-top: 1px solid var(--border-color); background-color: #fcfdff; }
|
|
1400
1742
|
.test-case-content h4 { margin-top: 22px; margin-bottom: 14px; font-size: 1.15em; color: var(--primary-color); }
|
|
1401
1743
|
.test-case-content p { margin-bottom: 10px; font-size: 1em; }
|
|
1402
1744
|
.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; }
|
|
1403
1745
|
.test-error-summary h4 { color: var(--danger-color); margin-top:0;}
|
|
1404
1746
|
.test-error-summary pre { white-space: pre-wrap; word-break: break-all; color: var(--danger-color); font-size: 0.95em;}
|
|
1405
|
-
|
|
1406
1747
|
.steps-list { margin: 18px 0; }
|
|
1407
1748
|
.step-item { margin-bottom: 8px; padding-left: calc(var(--depth, 0) * 28px); }
|
|
1408
|
-
.step-header {
|
|
1409
|
-
display: flex; align-items: center; cursor: pointer;
|
|
1410
|
-
padding: 10px 14px; border-radius: 6px; background-color: #fff;
|
|
1411
|
-
border: 1px solid var(--light-gray-color);
|
|
1412
|
-
transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
|
1413
|
-
}
|
|
1749
|
+
.step-header { display: flex; align-items: center; cursor: pointer; padding: 10px 14px; border-radius: 6px; background-color: #fff; border: 1px solid var(--light-gray-color); transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; }
|
|
1414
1750
|
.step-header:hover { background-color: #f0f2f5; border-color: var(--medium-gray-color); box-shadow: var(--box-shadow-inset); }
|
|
1415
1751
|
.step-icon { margin-right: 12px; width: 20px; text-align: center; font-size: 1.1em; }
|
|
1416
1752
|
.step-title { flex: 1; font-size: 1em; }
|
|
@@ -1422,176 +1758,55 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1422
1758
|
.step-hook { background-color: rgba(33,150,243,0.04); border-left: 3px solid var(--info-color) !important; }
|
|
1423
1759
|
.step-hook .step-title { font-style: italic; color: var(--info-color)}
|
|
1424
1760
|
.nested-steps { margin-top: 12px; }
|
|
1425
|
-
|
|
1426
1761
|
.attachments-section { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--light-gray-color); }
|
|
1427
1762
|
.attachments-section h4 { margin-top: 0; margin-bottom: 20px; font-size: 1.1em; color: var(--text-color); }
|
|
1428
1763
|
.attachments-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 22px; }
|
|
1429
|
-
.attachment-item {
|
|
1430
|
-
border: 1px solid var(--border-color); border-radius: var(--border-radius); background-color: #fff;
|
|
1431
|
-
box-shadow: var(--box-shadow-light); overflow: hidden; display: flex; flex-direction: column;
|
|
1432
|
-
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
|
|
1433
|
-
}
|
|
1764
|
+
.attachment-item { border: 1px solid var(--border-color); border-radius: var(--border-radius); background-color: #fff; box-shadow: var(--box-shadow-light); overflow: hidden; display: flex; flex-direction: column; transition: transform 0.2s ease-out, box-shadow 0.2s ease-out; }
|
|
1434
1765
|
.attachment-item:hover { transform: translateY(-4px); box-shadow: var(--box-shadow); }
|
|
1435
|
-
.attachment-item img {
|
|
1436
|
-
width: 100%; height: 180px; object-fit: cover; display: block;
|
|
1437
|
-
border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease;
|
|
1438
|
-
}
|
|
1766
|
+
.attachment-item img { width: 100%; height: 180px; object-fit: cover; display: block; border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease; }
|
|
1439
1767
|
.attachment-item a:hover img { opacity: 0.85; }
|
|
1440
|
-
.attachment-caption {
|
|
1441
|
-
padding: 12px 15px; font-size: 0.9em; text-align: center;
|
|
1442
|
-
color: var(--text-color-secondary); word-break: break-word; background-color: var(--light-gray-color);
|
|
1443
|
-
}
|
|
1768
|
+
.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); }
|
|
1444
1769
|
.video-item a, .trace-item a { display: block; margin-bottom: 8px; color: var(--primary-color); text-decoration: none; font-weight: 500; }
|
|
1445
1770
|
.video-item a:hover, .trace-item a:hover { text-decoration: underline; }
|
|
1446
1771
|
.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;}
|
|
1447
|
-
|
|
1448
1772
|
.trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
|
|
1449
|
-
/* Removed D3 specific .chart-axis, .main-chart-title, .chart-line.* rules */
|
|
1450
|
-
/* Highcharts styles its elements with classes like .highcharts-axis, .highcharts-title etc. */
|
|
1451
|
-
|
|
1452
1773
|
.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;}
|
|
1453
1774
|
.test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
|
|
1454
|
-
.test-history-card {
|
|
1455
|
-
background: var(--card-background-color); border: 1px solid var(--border-color); border-radius: var(--border-radius);
|
|
1456
|
-
padding: 22px; box-shadow: var(--box-shadow-light); display: flex; flex-direction: column;
|
|
1457
|
-
}
|
|
1775
|
+
.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; }
|
|
1458
1776
|
.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); }
|
|
1459
|
-
.test-history-header h3 { margin: 0; font-size: 1.15em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1460
|
-
.test-history-header p { font-weight: 500 }
|
|
1777
|
+
.test-history-header h3 { margin: 0; font-size: 1.15em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* This was h3, changed to p for consistency with user file */
|
|
1778
|
+
.test-history-header p { font-weight: 500 } /* Added this */
|
|
1461
1779
|
.test-history-trend { margin-bottom: 20px; min-height: 110px; }
|
|
1462
|
-
.test-history-trend div[id^="testHistoryChart-"] {
|
|
1463
|
-
display: block; margin: 0 auto; max-width:100%; height: 100px; width: 320px; /* Match JS config */
|
|
1464
|
-
}
|
|
1465
|
-
/* .test-history-trend .small-axis text {font-size: 11px;} Removed D3 specific */
|
|
1780
|
+
.test-history-trend div[id^="testHistoryChart-"] { display: block; margin: 0 auto; max-width:100%; height: 100px; width: 320px; }
|
|
1466
1781
|
.test-history-details-collapsible summary { cursor: pointer; font-size: 1em; color: var(--primary-color); margin-bottom: 10px; font-weight:500; }
|
|
1467
1782
|
.test-history-details-collapsible summary:hover {text-decoration: underline;}
|
|
1468
1783
|
.test-history-details table { width: 100%; border-collapse: collapse; font-size: 0.95em; }
|
|
1469
1784
|
.test-history-details th, .test-history-details td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--light-gray-color); }
|
|
1470
1785
|
.test-history-details th { background-color: var(--light-gray-color); font-weight: 600; }
|
|
1471
|
-
.status-badge-small {
|
|
1472
|
-
padding: 3px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 600;
|
|
1473
|
-
color: white; text-transform: uppercase; display: inline-block;
|
|
1474
|
-
}
|
|
1786
|
+
.status-badge-small { padding: 3px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase; display: inline-block; }
|
|
1475
1787
|
.status-badge-small.status-passed { background-color: var(--success-color); }
|
|
1476
1788
|
.status-badge-small.status-failed { background-color: var(--danger-color); }
|
|
1477
1789
|
.status-badge-small.status-skipped { background-color: var(--warning-color); }
|
|
1478
1790
|
.status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
|
|
1479
|
-
|
|
1480
|
-
.no-data, .no-tests, .no-steps, .no-data-chart {
|
|
1481
|
-
padding: 28px; text-align: center; color: var(--dark-gray-color); font-style: italic; font-size:1.1em;
|
|
1482
|
-
background-color: var(--light-gray-color); border-radius: var(--border-radius); margin: 18px 0;
|
|
1483
|
-
border: 1px dashed var(--medium-gray-color);
|
|
1484
|
-
}
|
|
1791
|
+
.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); }
|
|
1485
1792
|
.no-data-chart {font-size: 0.95em; padding: 18px;}
|
|
1486
|
-
|
|
1487
1793
|
#test-ai iframe { border: 1px solid var(--border-color); width: 100%; height: 85vh; border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); }
|
|
1488
1794
|
#test-ai p {margin-bottom: 18px; font-size: 1em; color: var(--text-color-secondary);}
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
.trace-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
.
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
.trace-name {
|
|
1506
|
-
word-break: break-word;
|
|
1507
|
-
font-size: 0.9rem;
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
.trace-actions {
|
|
1511
|
-
display: flex;
|
|
1512
|
-
gap: 0.5rem;
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
.trace-actions a {
|
|
1516
|
-
flex: 1;
|
|
1517
|
-
text-align: center;
|
|
1518
|
-
padding: 0.25rem 0.5rem;
|
|
1519
|
-
font-size: 0.85rem;
|
|
1520
|
-
border-radius: 4px;
|
|
1521
|
-
text-decoration: none;
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
.view-trace {
|
|
1525
|
-
background: #3182ce;
|
|
1526
|
-
color: white;
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
.view-trace:hover {
|
|
1530
|
-
background: #2c5282;
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
.download-trace {
|
|
1534
|
-
background: #e2e8f0;
|
|
1535
|
-
color: #2d3748;
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
.download-trace:hover {
|
|
1539
|
-
background: #cbd5e0;
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
.filters button.clear-filters-btn {
|
|
1543
|
-
background-color: var(--medium-gray-color); /* Or any other suitable color */
|
|
1544
|
-
color: var(--text-color);
|
|
1545
|
-
/* Add other styling as per your .filters button style if needed */
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
.filters button.clear-filters-btn:hover {
|
|
1549
|
-
background-color: var(--dark-gray-color); /* Darker on hover */
|
|
1550
|
-
color: #fff;
|
|
1551
|
-
}
|
|
1552
|
-
@media (max-width: 1200px) {
|
|
1553
|
-
.trend-charts-row { grid-template-columns: 1fr; }
|
|
1554
|
-
}
|
|
1555
|
-
@media (max-width: 992px) {
|
|
1556
|
-
.dashboard-bottom-row { grid-template-columns: 1fr; }
|
|
1557
|
-
.pie-chart-wrapper div[id^="pieChart-"] { max-width: 350px; margin: 0 auto; }
|
|
1558
|
-
.filters input { min-width: 180px; }
|
|
1559
|
-
.filters select { min-width: 150px; }
|
|
1560
|
-
}
|
|
1561
|
-
@media (max-width: 768px) {
|
|
1562
|
-
body { font-size: 15px; }
|
|
1563
|
-
.container { margin: 10px; padding: 20px; }
|
|
1564
|
-
.header { flex-direction: column; align-items: flex-start; gap: 15px; }
|
|
1565
|
-
.header h1 { font-size: 1.6em; }
|
|
1566
|
-
.run-info { text-align: left; font-size:0.9em; }
|
|
1567
|
-
.tabs { margin-bottom: 25px;}
|
|
1568
|
-
.tab-button { padding: 12px 20px; font-size: 1.05em;}
|
|
1569
|
-
.dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;}
|
|
1570
|
-
.summary-card .value {font-size: 2em;}
|
|
1571
|
-
.summary-card h3 {font-size: 0.95em;}
|
|
1572
|
-
.filters { flex-direction: column; padding: 18px; gap: 12px;}
|
|
1573
|
-
.filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;}
|
|
1574
|
-
.test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; }
|
|
1575
|
-
.test-case-summary {gap: 10px;}
|
|
1576
|
-
.test-case-title {font-size: 1.05em;}
|
|
1577
|
-
.test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;}
|
|
1578
|
-
.attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;}
|
|
1579
|
-
.test-history-grid {grid-template-columns: 1fr;}
|
|
1580
|
-
.pie-chart-wrapper {min-height: auto;}
|
|
1581
|
-
}
|
|
1582
|
-
@media (max-width: 480px) {
|
|
1583
|
-
body {font-size: 14px;}
|
|
1584
|
-
.container {padding: 15px;}
|
|
1585
|
-
.header h1 {font-size: 1.4em;}
|
|
1586
|
-
#report-logo { height: 35px; width: 35px; }
|
|
1587
|
-
.tab-button {padding: 10px 15px; font-size: 1em;}
|
|
1588
|
-
.summary-card .value {font-size: 1.8em;}
|
|
1589
|
-
.attachments-grid {grid-template-columns: 1fr;}
|
|
1590
|
-
.step-item {padding-left: calc(var(--depth, 0) * 18px);}
|
|
1591
|
-
.test-case-content, .step-details {padding: 15px;}
|
|
1592
|
-
.trend-charts-row {gap: 20px;}
|
|
1593
|
-
.trend-chart {padding: 20px;}
|
|
1594
|
-
}
|
|
1795
|
+
.trace-preview { padding: 1rem; text-align: center; background: #f5f5f5; border-bottom: 1px solid #e1e1e1; }
|
|
1796
|
+
.trace-icon { font-size: 2rem; display: block; margin-bottom: 0.5rem; }
|
|
1797
|
+
.trace-name { word-break: break-word; font-size: 0.9rem; }
|
|
1798
|
+
.trace-actions { display: flex; gap: 0.5rem; }
|
|
1799
|
+
.trace-actions a { flex: 1; text-align: center; padding: 0.25rem 0.5rem; font-size: 0.85rem; border-radius: 4px; text-decoration: none; background: cornflowerblue; color: aliceblue; }
|
|
1800
|
+
.view-trace { background: #3182ce; color: white; }
|
|
1801
|
+
.view-trace:hover { background: #2c5282; }
|
|
1802
|
+
.download-trace { background: #e2e8f0; color: #2d3748; }
|
|
1803
|
+
.download-trace:hover { background: #cbd5e0; }
|
|
1804
|
+
.filters button.clear-filters-btn { background-color: var(--medium-gray-color); color: var(--text-color); }
|
|
1805
|
+
.filters button.clear-filters-btn:hover { background-color: var(--dark-gray-color); color: #fff; }
|
|
1806
|
+
@media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
|
|
1807
|
+
@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; } }
|
|
1808
|
+
@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;} }
|
|
1809
|
+
@media (max-width: 480px) { body {font-size: 14px;} .container {padding: 15px;} .header h1 {font-size: 1.4em;} #report-logo { height: 35px; width: 35px; } .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;} }
|
|
1595
1810
|
</style>
|
|
1596
1811
|
</head>
|
|
1597
1812
|
<body>
|
|
@@ -1601,113 +1816,89 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1601
1816
|
<img id="report-logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMNCA3bDggNSA4LTUtOC01eiIgZmlsbD0iIzNmNTFiNSIvPjxwYXRoIGQ9Ik0xMiA2TDQgMTFsOCA1IDgtNS04LTV6IiBmaWxsPSIjNDI4NWY0Ii8+PHBhdGggZD0iTTEyIDEwbC04IDUgOCA1IDgtNS04LTV6IiBmaWxsPSIjM2Q1NWI0Ii8+PC9zdmc+" alt="Report Logo">
|
|
1602
1817
|
<h1>Playwright Pulse Report</h1>
|
|
1603
1818
|
</div>
|
|
1604
|
-
<div class="run-info">
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
runSummary.duration
|
|
1610
|
-
)}
|
|
1611
|
-
</div>
|
|
1819
|
+
<div class="run-info"><strong>Run Date:</strong> ${formatDate(
|
|
1820
|
+
runSummary.timestamp
|
|
1821
|
+
)}<br><strong>Total Duration:</strong> ${formatDuration(
|
|
1822
|
+
runSummary.duration
|
|
1823
|
+
)}</div>
|
|
1612
1824
|
</header>
|
|
1613
|
-
|
|
1614
1825
|
<div class="tabs">
|
|
1615
1826
|
<button class="tab-button active" data-tab="dashboard">Dashboard</button>
|
|
1616
1827
|
<button class="tab-button" data-tab="test-runs">Test Run Summary</button>
|
|
1617
1828
|
<button class="tab-button" data-tab="test-history">Test History</button>
|
|
1618
1829
|
<button class="tab-button" data-tab="test-ai">AI Analysis</button>
|
|
1619
1830
|
</div>
|
|
1620
|
-
|
|
1621
1831
|
<div id="dashboard" class="tab-content active">
|
|
1622
1832
|
<div class="dashboard-grid">
|
|
1623
|
-
<div class="summary-card">
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
</div>
|
|
1632
|
-
<div class="summary-card status-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
</div>
|
|
1636
|
-
<div class="summary-card
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
}</div>
|
|
1640
|
-
<div class="trend-percentage">${skipPercentage}%</div>
|
|
1641
|
-
</div>
|
|
1642
|
-
<div class="summary-card">
|
|
1643
|
-
<h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div>
|
|
1644
|
-
</div>
|
|
1645
|
-
<div class="summary-card">
|
|
1646
|
-
<h3>Run Duration</h3><div class="value">${formatDuration(
|
|
1647
|
-
runSummary.duration
|
|
1648
|
-
)}</div>
|
|
1649
|
-
</div>
|
|
1833
|
+
<div class="summary-card"><h3>Total Tests</h3><div class="value">${
|
|
1834
|
+
runSummary.totalTests
|
|
1835
|
+
}</div></div>
|
|
1836
|
+
<div class="summary-card status-passed"><h3>Passed</h3><div class="value">${
|
|
1837
|
+
runSummary.passed
|
|
1838
|
+
}</div><div class="trend-percentage">${passPercentage}%</div></div>
|
|
1839
|
+
<div class="summary-card status-failed"><h3>Failed</h3><div class="value">${
|
|
1840
|
+
runSummary.failed
|
|
1841
|
+
}</div><div class="trend-percentage">${failPercentage}%</div></div>
|
|
1842
|
+
<div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
|
|
1843
|
+
runSummary.skipped || 0
|
|
1844
|
+
}</div><div class="trend-percentage">${skipPercentage}%</div></div>
|
|
1845
|
+
<div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
|
|
1846
|
+
<div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
|
|
1847
|
+
runSummary.duration
|
|
1848
|
+
)}</div></div>
|
|
1650
1849
|
</div>
|
|
1651
1850
|
<div class="dashboard-bottom-row">
|
|
1851
|
+
<div style="display: grid; gap: 20px">
|
|
1652
1852
|
${generatePieChart(
|
|
1653
|
-
// Changed from generatePieChartD3
|
|
1654
1853
|
[
|
|
1655
1854
|
{ label: "Passed", value: runSummary.passed },
|
|
1656
1855
|
{ label: "Failed", value: runSummary.failed },
|
|
1657
1856
|
{ label: "Skipped", value: runSummary.skipped || 0 },
|
|
1658
1857
|
],
|
|
1659
|
-
400,
|
|
1660
|
-
390
|
|
1858
|
+
400,
|
|
1859
|
+
390
|
|
1661
1860
|
)}
|
|
1861
|
+
${
|
|
1862
|
+
runSummary.environment &&
|
|
1863
|
+
Object.keys(runSummary.environment).length > 0
|
|
1864
|
+
? generateEnvironmentDashboard(runSummary.environment)
|
|
1865
|
+
: '<div class="no-data">Environment data not available.</div>'
|
|
1866
|
+
}
|
|
1867
|
+
</div>
|
|
1662
1868
|
${generateSuitesWidget(suitesData)}
|
|
1663
1869
|
</div>
|
|
1664
1870
|
</div>
|
|
1665
|
-
|
|
1666
1871
|
<div id="test-runs" class="tab-content">
|
|
1667
1872
|
<div class="filters">
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
`<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
|
|
1684
|
-
browser
|
|
1685
|
-
)}</option>`
|
|
1686
|
-
)
|
|
1687
|
-
.join("")}
|
|
1688
|
-
</select>
|
|
1689
|
-
<button id="expand-all-tests">Expand All</button>
|
|
1690
|
-
<button id="collapse-all-tests">Collapse All</button>
|
|
1691
|
-
<button id="clear-run-summary-filters" class="clear-filters-btn">Clear Filters</button>
|
|
1692
|
-
</div>
|
|
1693
|
-
<div class="test-cases-list">
|
|
1694
|
-
${generateTestCasesHTML()}
|
|
1873
|
+
<input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
|
|
1874
|
+
<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>
|
|
1875
|
+
<select id="filter-browser"><option value="">All Browsers</option>${Array.from(
|
|
1876
|
+
new Set(
|
|
1877
|
+
(results || []).map((test) => test.browser || "unknown")
|
|
1878
|
+
)
|
|
1879
|
+
)
|
|
1880
|
+
.map(
|
|
1881
|
+
(browser) =>
|
|
1882
|
+
`<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
|
|
1883
|
+
browser
|
|
1884
|
+
)}</option>`
|
|
1885
|
+
)
|
|
1886
|
+
.join("")}</select>
|
|
1887
|
+
<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>
|
|
1695
1888
|
</div>
|
|
1889
|
+
<div class="test-cases-list">${generateTestCasesHTML()}</div>
|
|
1696
1890
|
</div>
|
|
1697
|
-
|
|
1698
1891
|
<div id="test-history" class="tab-content">
|
|
1699
1892
|
<h2 class="tab-main-title">Execution Trends</h2>
|
|
1700
1893
|
<div class="trend-charts-row">
|
|
1701
|
-
<div class="trend-chart">
|
|
1702
|
-
<h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
|
|
1894
|
+
<div class="trend-chart"><h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
|
|
1703
1895
|
${
|
|
1704
1896
|
trendData && trendData.overall && trendData.overall.length > 0
|
|
1705
1897
|
? generateTestTrendsChart(trendData)
|
|
1706
1898
|
: '<div class="no-data">Overall trend data not available for test counts.</div>'
|
|
1707
1899
|
}
|
|
1708
1900
|
</div>
|
|
1709
|
-
<div class="trend-chart">
|
|
1710
|
-
<h3 class="chart-title-header">Execution Duration Trends</h3>
|
|
1901
|
+
<div class="trend-chart"><h3 class="chart-title-header">Execution Duration Trends</h3>
|
|
1711
1902
|
${
|
|
1712
1903
|
trendData && trendData.overall && trendData.overall.length > 0
|
|
1713
1904
|
? generateDurationTrendChart(trendData)
|
|
@@ -1724,69 +1915,26 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1724
1915
|
: '<div class="no-data">Individual test history data not available.</div>'
|
|
1725
1916
|
}
|
|
1726
1917
|
</div>
|
|
1727
|
-
|
|
1728
1918
|
<div id="test-ai" class="tab-content">
|
|
1729
|
-
<iframe
|
|
1730
|
-
src="https://ai-test-analyser.netlify.app/"
|
|
1731
|
-
width="100%"
|
|
1732
|
-
height="100%"
|
|
1733
|
-
frameborder="0"
|
|
1734
|
-
allowfullscreen
|
|
1735
|
-
style="border: none; height: 100vh;">
|
|
1736
|
-
</iframe>
|
|
1919
|
+
<iframe data-src="https://ai-test-analyser.netlify.app/" width="100%" height="100%" frameborder="0" allowfullscreen class="lazy-load-iframe" title="AI Test Analyser" style="border: none; height: 100vh;"></iframe>
|
|
1737
1920
|
</div>
|
|
1738
|
-
<footer style="
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
align-items: center;
|
|
1747
|
-
gap: 0.5rem;
|
|
1748
|
-
color: #333;
|
|
1749
|
-
font-size: 0.9rem;
|
|
1750
|
-
font-weight: 600;
|
|
1751
|
-
letter-spacing: 0.5px;
|
|
1752
|
-
">
|
|
1753
|
-
<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"/>
|
|
1754
|
-
<span>Created by</span>
|
|
1755
|
-
<a href="https://github.com/Arghajit47"
|
|
1756
|
-
target="_blank"
|
|
1757
|
-
rel="noopener noreferrer"
|
|
1758
|
-
style="
|
|
1759
|
-
color: #7737BF;
|
|
1760
|
-
font-weight: 700;
|
|
1761
|
-
font-style: italic;
|
|
1762
|
-
text-decoration: none;
|
|
1763
|
-
transition: all 0.2s ease;
|
|
1764
|
-
"
|
|
1765
|
-
onmouseover="this.style.color='#BF5C37'"
|
|
1766
|
-
onmouseout="this.style.color='#7737BF'">
|
|
1767
|
-
Arghajit Singha
|
|
1768
|
-
</a>
|
|
1769
|
-
</div>
|
|
1770
|
-
<div style="
|
|
1771
|
-
margin-top: 0.5rem;
|
|
1772
|
-
font-size: 0.75rem;
|
|
1773
|
-
color: #666;
|
|
1774
|
-
">
|
|
1775
|
-
Crafted with precision
|
|
1776
|
-
</div>
|
|
1777
|
-
</footer>
|
|
1921
|
+
<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;">
|
|
1922
|
+
<div style="display: inline-flex; align-items: center; gap: 0.5rem; color: #333; font-size: 0.9rem; font-weight: 600; letter-spacing: 0.5px;">
|
|
1923
|
+
<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"/>
|
|
1924
|
+
<span>Created by</span>
|
|
1925
|
+
<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>
|
|
1926
|
+
</div>
|
|
1927
|
+
<div style="margin-top: 0.5rem; font-size: 0.75rem; color: #666;">Crafted with precision</div>
|
|
1928
|
+
</footer>
|
|
1778
1929
|
</div>
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
1930
|
<script>
|
|
1782
1931
|
// Ensure formatDuration is globally available
|
|
1783
|
-
if (typeof formatDuration === 'undefined') {
|
|
1784
|
-
function formatDuration(ms) {
|
|
1785
|
-
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
1786
|
-
return (ms / 1000).toFixed(1) + "s";
|
|
1932
|
+
if (typeof formatDuration === 'undefined') {
|
|
1933
|
+
function formatDuration(ms) {
|
|
1934
|
+
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
1935
|
+
return (ms / 1000).toFixed(1) + "s";
|
|
1787
1936
|
}
|
|
1788
1937
|
}
|
|
1789
|
-
|
|
1790
1938
|
function initializeReportInteractivity() {
|
|
1791
1939
|
const tabButtons = document.querySelectorAll('.tab-button');
|
|
1792
1940
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
@@ -1797,82 +1945,64 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1797
1945
|
button.classList.add('active');
|
|
1798
1946
|
const tabId = button.getAttribute('data-tab');
|
|
1799
1947
|
const activeContent = document.getElementById(tabId);
|
|
1800
|
-
if (activeContent)
|
|
1948
|
+
if (activeContent) {
|
|
1949
|
+
activeContent.classList.add('active');
|
|
1950
|
+
// Check if IntersectionObserver is already handling elements in this tab
|
|
1951
|
+
// For simplicity, we assume if an element is observed, it will be handled when it becomes visible.
|
|
1952
|
+
// If IntersectionObserver is not supported, already-visible elements would have been loaded by fallback.
|
|
1953
|
+
}
|
|
1801
1954
|
});
|
|
1802
1955
|
});
|
|
1803
|
-
|
|
1804
1956
|
// --- Test Run Summary Filters ---
|
|
1805
1957
|
const nameFilter = document.getElementById('filter-name');
|
|
1806
1958
|
const statusFilter = document.getElementById('filter-status');
|
|
1807
1959
|
const browserFilter = document.getElementById('filter-browser');
|
|
1808
|
-
const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters');
|
|
1809
|
-
|
|
1810
|
-
function filterTestCases() {
|
|
1960
|
+
const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters');
|
|
1961
|
+
function filterTestCases() {
|
|
1811
1962
|
const nameValue = nameFilter ? nameFilter.value.toLowerCase() : "";
|
|
1812
1963
|
const statusValue = statusFilter ? statusFilter.value : "";
|
|
1813
1964
|
const browserValue = browserFilter ? browserFilter.value : "";
|
|
1814
|
-
|
|
1815
1965
|
document.querySelectorAll('#test-runs .test-case').forEach(testCaseElement => {
|
|
1816
1966
|
const titleElement = testCaseElement.querySelector('.test-case-title');
|
|
1817
1967
|
const fullTestName = titleElement ? titleElement.getAttribute('title').toLowerCase() : "";
|
|
1818
1968
|
const status = testCaseElement.getAttribute('data-status');
|
|
1819
1969
|
const browser = testCaseElement.getAttribute('data-browser');
|
|
1820
|
-
|
|
1821
1970
|
const nameMatch = fullTestName.includes(nameValue);
|
|
1822
1971
|
const statusMatch = !statusValue || status === statusValue;
|
|
1823
1972
|
const browserMatch = !browserValue || browser === browserValue;
|
|
1824
|
-
|
|
1825
1973
|
testCaseElement.style.display = (nameMatch && statusMatch && browserMatch) ? '' : 'none';
|
|
1826
1974
|
});
|
|
1827
1975
|
}
|
|
1828
1976
|
if(nameFilter) nameFilter.addEventListener('input', filterTestCases);
|
|
1829
1977
|
if(statusFilter) statusFilter.addEventListener('change', filterTestCases);
|
|
1830
1978
|
if(browserFilter) browserFilter.addEventListener('change', filterTestCases);
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
if (nameFilter) nameFilter.value = '';
|
|
1836
|
-
if (statusFilter) statusFilter.value = '';
|
|
1837
|
-
if (browserFilter) browserFilter.value = '';
|
|
1838
|
-
filterTestCases(); // Re-apply filters (which will show all)
|
|
1839
|
-
});
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1979
|
+
if(clearRunSummaryFiltersBtn) clearRunSummaryFiltersBtn.addEventListener('click', () => {
|
|
1980
|
+
if(nameFilter) nameFilter.value = ''; if(statusFilter) statusFilter.value = ''; if(browserFilter) browserFilter.value = '';
|
|
1981
|
+
filterTestCases();
|
|
1982
|
+
});
|
|
1842
1983
|
// --- Test History Filters ---
|
|
1843
1984
|
const historyNameFilter = document.getElementById('history-filter-name');
|
|
1844
1985
|
const historyStatusFilter = document.getElementById('history-filter-status');
|
|
1845
|
-
const clearHistoryFiltersBtn = document.getElementById('clear-history-filters');
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
function filterTestHistoryCards() {
|
|
1986
|
+
const clearHistoryFiltersBtn = document.getElementById('clear-history-filters');
|
|
1987
|
+
function filterTestHistoryCards() {
|
|
1849
1988
|
const nameValue = historyNameFilter ? historyNameFilter.value.toLowerCase() : "";
|
|
1850
1989
|
const statusValue = historyStatusFilter ? historyStatusFilter.value : "";
|
|
1851
|
-
|
|
1852
1990
|
document.querySelectorAll('.test-history-card').forEach(card => {
|
|
1853
1991
|
const testTitle = card.getAttribute('data-test-name').toLowerCase();
|
|
1854
1992
|
const latestStatus = card.getAttribute('data-latest-status');
|
|
1855
|
-
|
|
1856
1993
|
const nameMatch = testTitle.includes(nameValue);
|
|
1857
1994
|
const statusMatch = !statusValue || latestStatus === statusValue;
|
|
1858
|
-
|
|
1859
1995
|
card.style.display = (nameMatch && statusMatch) ? '' : 'none';
|
|
1860
1996
|
});
|
|
1861
1997
|
}
|
|
1862
1998
|
if(historyNameFilter) historyNameFilter.addEventListener('input', filterTestHistoryCards);
|
|
1863
1999
|
if(historyStatusFilter) historyStatusFilter.addEventListener('change', filterTestHistoryCards);
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
filterTestHistoryCards(); // Re-apply filters (which will show all)
|
|
1871
|
-
});
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
// --- Expand/Collapse and Toggle Details Logic (remains the same) ---
|
|
1875
|
-
function toggleElementDetails(headerElement, contentSelector) {
|
|
2000
|
+
if(clearHistoryFiltersBtn) clearHistoryFiltersBtn.addEventListener('click', () => {
|
|
2001
|
+
if(historyNameFilter) historyNameFilter.value = ''; if(historyStatusFilter) historyStatusFilter.value = '';
|
|
2002
|
+
filterTestHistoryCards();
|
|
2003
|
+
});
|
|
2004
|
+
// --- Expand/Collapse and Toggle Details Logic ---
|
|
2005
|
+
function toggleElementDetails(headerElement, contentSelector) {
|
|
1876
2006
|
let contentElement;
|
|
1877
2007
|
if (headerElement.classList.contains('test-case-header')) {
|
|
1878
2008
|
contentElement = headerElement.parentElement.querySelector('.test-case-content');
|
|
@@ -1882,41 +2012,114 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1882
2012
|
contentElement = null;
|
|
1883
2013
|
}
|
|
1884
2014
|
}
|
|
1885
|
-
|
|
1886
2015
|
if (contentElement) {
|
|
1887
2016
|
const isExpanded = contentElement.style.display === 'block';
|
|
1888
2017
|
contentElement.style.display = isExpanded ? 'none' : 'block';
|
|
1889
2018
|
headerElement.setAttribute('aria-expanded', String(!isExpanded));
|
|
1890
2019
|
}
|
|
1891
2020
|
}
|
|
1892
|
-
|
|
1893
2021
|
document.querySelectorAll('#test-runs .test-case-header').forEach(header => {
|
|
1894
2022
|
header.addEventListener('click', () => toggleElementDetails(header));
|
|
1895
2023
|
});
|
|
1896
2024
|
document.querySelectorAll('#test-runs .step-header').forEach(header => {
|
|
1897
2025
|
header.addEventListener('click', () => toggleElementDetails(header, '.step-details'));
|
|
1898
2026
|
});
|
|
1899
|
-
|
|
1900
2027
|
const expandAllBtn = document.getElementById('expand-all-tests');
|
|
1901
2028
|
const collapseAllBtn = document.getElementById('collapse-all-tests');
|
|
1902
|
-
|
|
1903
2029
|
function setAllTestRunDetailsVisibility(displayMode, ariaState) {
|
|
1904
2030
|
document.querySelectorAll('#test-runs .test-case-content').forEach(el => el.style.display = displayMode);
|
|
1905
2031
|
document.querySelectorAll('#test-runs .step-details').forEach(el => el.style.display = displayMode);
|
|
1906
2032
|
document.querySelectorAll('#test-runs .test-case-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
|
|
1907
2033
|
document.querySelectorAll('#test-runs .step-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
|
|
1908
2034
|
}
|
|
1909
|
-
|
|
1910
2035
|
if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
|
|
1911
2036
|
if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
|
|
2037
|
+
// --- Intersection Observer for Lazy Loading ---
|
|
2038
|
+
const lazyLoadElements = document.querySelectorAll('.lazy-load-chart, .lazy-load-iframe');
|
|
2039
|
+
if ('IntersectionObserver' in window) {
|
|
2040
|
+
let lazyObserver = new IntersectionObserver((entries, observer) => {
|
|
2041
|
+
entries.forEach(entry => {
|
|
2042
|
+
if (entry.isIntersecting) {
|
|
2043
|
+
const element = entry.target;
|
|
2044
|
+
if (element.classList.contains('lazy-load-iframe')) {
|
|
2045
|
+
if (element.dataset.src) {
|
|
2046
|
+
element.src = element.dataset.src;
|
|
2047
|
+
element.removeAttribute('data-src'); // Optional: remove data-src after loading
|
|
2048
|
+
console.log('Lazy loaded iframe:', element.title || 'Untitled Iframe');
|
|
2049
|
+
}
|
|
2050
|
+
} else if (element.classList.contains('lazy-load-chart')) {
|
|
2051
|
+
const renderFunctionName = element.dataset.renderFunctionName;
|
|
2052
|
+
if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
|
|
2053
|
+
try {
|
|
2054
|
+
console.log('Lazy loading chart with function:', renderFunctionName);
|
|
2055
|
+
window[renderFunctionName](); // Call the render function
|
|
2056
|
+
} catch (e) {
|
|
2057
|
+
console.error(\`Error lazy-loading chart \${element.id} using \${renderFunctionName}:\`, e);
|
|
2058
|
+
element.innerHTML = '<div class="no-data-chart">Error lazy-loading chart.</div>';
|
|
2059
|
+
}
|
|
2060
|
+
} else {
|
|
2061
|
+
console.warn(\`Render function \${renderFunctionName} not found or not a function for chart:\`, element.id);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
observer.unobserve(element); // Important: stop observing once loaded
|
|
2065
|
+
}
|
|
2066
|
+
});
|
|
2067
|
+
}, {
|
|
2068
|
+
rootMargin: "0px 0px 200px 0px" // Start loading when element is 200px from viewport bottom
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
lazyLoadElements.forEach(el => {
|
|
2072
|
+
lazyObserver.observe(el);
|
|
2073
|
+
});
|
|
2074
|
+
} else { // Fallback for browsers without IntersectionObserver
|
|
2075
|
+
console.warn("IntersectionObserver not supported. Loading all items immediately.");
|
|
2076
|
+
lazyLoadElements.forEach(element => {
|
|
2077
|
+
if (element.classList.contains('lazy-load-iframe')) {
|
|
2078
|
+
if (element.dataset.src) {
|
|
2079
|
+
element.src = element.dataset.src;
|
|
2080
|
+
element.removeAttribute('data-src');
|
|
2081
|
+
}
|
|
2082
|
+
} else if (element.classList.contains('lazy-load-chart')) {
|
|
2083
|
+
const renderFunctionName = element.dataset.renderFunctionName;
|
|
2084
|
+
if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
|
|
2085
|
+
try {
|
|
2086
|
+
window[renderFunctionName]();
|
|
2087
|
+
} catch (e) {
|
|
2088
|
+
console.error(\`Error loading chart (fallback) \${element.id} using \${renderFunctionName}:\`, e);
|
|
2089
|
+
element.innerHTML = '<div class="no-data-chart">Error loading chart (fallback).</div>';
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
1912
2095
|
}
|
|
1913
2096
|
document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
|
|
2097
|
+
|
|
2098
|
+
function copyErrorToClipboard(button) {
|
|
2099
|
+
const errorContainer = button.closest('.step-error');
|
|
2100
|
+
const errorText = errorContainer.querySelector('.stack-trace').textContent;
|
|
2101
|
+
const textarea = document.createElement('textarea');
|
|
2102
|
+
textarea.value = errorText;
|
|
2103
|
+
document.body.appendChild(textarea);
|
|
2104
|
+
textarea.select();
|
|
2105
|
+
try {
|
|
2106
|
+
const successful = document.execCommand('copy');
|
|
2107
|
+
const originalText = button.textContent;
|
|
2108
|
+
button.textContent = successful ? 'Copied!' : 'Failed to copy';
|
|
2109
|
+
setTimeout(() => {
|
|
2110
|
+
button.textContent = originalText;
|
|
2111
|
+
}, 2000);
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
console.error('Failed to copy: ', err);
|
|
2114
|
+
button.textContent = 'Failed to copy';
|
|
2115
|
+
}
|
|
2116
|
+
document.body.removeChild(textarea);
|
|
2117
|
+
}
|
|
1914
2118
|
</script>
|
|
1915
2119
|
</body>
|
|
1916
2120
|
</html>
|
|
1917
2121
|
`;
|
|
1918
2122
|
}
|
|
1919
|
-
|
|
1920
2123
|
async function runScript(scriptPath) {
|
|
1921
2124
|
return new Promise((resolve, reject) => {
|
|
1922
2125
|
console.log(chalk.blue(`Executing script: ${scriptPath}...`));
|
|
@@ -1941,7 +2144,6 @@ async function runScript(scriptPath) {
|
|
|
1941
2144
|
});
|
|
1942
2145
|
});
|
|
1943
2146
|
}
|
|
1944
|
-
|
|
1945
2147
|
async function main() {
|
|
1946
2148
|
const __filename = fileURLToPath(import.meta.url);
|
|
1947
2149
|
const __dirname = path.dirname(__filename);
|
|
@@ -1976,11 +2178,10 @@ async function main() {
|
|
|
1976
2178
|
),
|
|
1977
2179
|
error
|
|
1978
2180
|
);
|
|
1979
|
-
// You might decide to proceed or exit depending on the importance of historical data
|
|
1980
2181
|
}
|
|
1981
2182
|
|
|
1982
2183
|
// Step 2: Load current run's data (for non-trend sections of the report)
|
|
1983
|
-
let currentRunReportData;
|
|
2184
|
+
let currentRunReportData;
|
|
1984
2185
|
try {
|
|
1985
2186
|
const jsonData = await fs.readFile(reportJsonPath, "utf-8");
|
|
1986
2187
|
currentRunReportData = JSON.parse(jsonData);
|
|
@@ -2007,13 +2208,13 @@ async function main() {
|
|
|
2007
2208
|
`Critical Error: Could not read or parse main report JSON at ${reportJsonPath}: ${error.message}`
|
|
2008
2209
|
)
|
|
2009
2210
|
);
|
|
2010
|
-
process.exit(1);
|
|
2211
|
+
process.exit(1);
|
|
2011
2212
|
}
|
|
2012
2213
|
|
|
2013
2214
|
// Step 3: Load historical data for trends
|
|
2014
|
-
let historicalRuns = [];
|
|
2215
|
+
let historicalRuns = [];
|
|
2015
2216
|
try {
|
|
2016
|
-
await fs.access(historyDir);
|
|
2217
|
+
await fs.access(historyDir);
|
|
2017
2218
|
const allHistoryFiles = await fs.readdir(historyDir);
|
|
2018
2219
|
|
|
2019
2220
|
const jsonHistoryFiles = allHistoryFiles
|
|
@@ -2031,7 +2232,7 @@ async function main() {
|
|
|
2031
2232
|
};
|
|
2032
2233
|
})
|
|
2033
2234
|
.filter((file) => !isNaN(file.timestamp))
|
|
2034
|
-
.sort((a, b) => b.timestamp - a.timestamp);
|
|
2235
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
2035
2236
|
|
|
2036
2237
|
const filesToLoadForTrend = jsonHistoryFiles.slice(
|
|
2037
2238
|
0,
|
|
@@ -2041,7 +2242,7 @@ async function main() {
|
|
|
2041
2242
|
for (const fileMeta of filesToLoadForTrend) {
|
|
2042
2243
|
try {
|
|
2043
2244
|
const fileContent = await fs.readFile(fileMeta.path, "utf-8");
|
|
2044
|
-
const runJsonData = JSON.parse(fileContent);
|
|
2245
|
+
const runJsonData = JSON.parse(fileContent);
|
|
2045
2246
|
historicalRuns.push(runJsonData);
|
|
2046
2247
|
} catch (fileReadError) {
|
|
2047
2248
|
console.warn(
|
|
@@ -2051,8 +2252,7 @@ async function main() {
|
|
|
2051
2252
|
);
|
|
2052
2253
|
}
|
|
2053
2254
|
}
|
|
2054
|
-
//
|
|
2055
|
-
historicalRuns.reverse();
|
|
2255
|
+
historicalRuns.reverse(); // Oldest first for charts
|
|
2056
2256
|
console.log(
|
|
2057
2257
|
chalk.green(
|
|
2058
2258
|
`Loaded ${historicalRuns.length} historical run(s) for trend analysis.`
|
|
@@ -2074,20 +2274,18 @@ async function main() {
|
|
|
2074
2274
|
}
|
|
2075
2275
|
}
|
|
2076
2276
|
|
|
2077
|
-
// Step 4: Prepare trendData object
|
|
2277
|
+
// Step 4: Prepare trendData object
|
|
2078
2278
|
const trendData = {
|
|
2079
|
-
overall: [],
|
|
2080
|
-
testRuns: {},
|
|
2279
|
+
overall: [],
|
|
2280
|
+
testRuns: {},
|
|
2081
2281
|
};
|
|
2082
2282
|
|
|
2083
2283
|
if (historicalRuns.length > 0) {
|
|
2084
2284
|
historicalRuns.forEach((histRunReport) => {
|
|
2085
|
-
// histRunReport is a full PlaywrightPulseReport object from a past run
|
|
2086
2285
|
if (histRunReport.run) {
|
|
2087
|
-
// Ensure timestamp is a Date object for correct sorting/comparison later if needed by charts
|
|
2088
2286
|
const runTimestamp = new Date(histRunReport.run.timestamp);
|
|
2089
2287
|
trendData.overall.push({
|
|
2090
|
-
runId: runTimestamp.getTime(),
|
|
2288
|
+
runId: runTimestamp.getTime(),
|
|
2091
2289
|
timestamp: runTimestamp,
|
|
2092
2290
|
duration: histRunReport.run.duration,
|
|
2093
2291
|
totalTests: histRunReport.run.totalTests,
|
|
@@ -2096,21 +2294,19 @@ async function main() {
|
|
|
2096
2294
|
skipped: histRunReport.run.skipped || 0,
|
|
2097
2295
|
});
|
|
2098
2296
|
|
|
2099
|
-
// For generateTestHistoryContent
|
|
2100
2297
|
if (histRunReport.results && Array.isArray(histRunReport.results)) {
|
|
2101
|
-
const runKeyForTestHistory = `test run ${runTimestamp.getTime()}`;
|
|
2298
|
+
const runKeyForTestHistory = `test run ${runTimestamp.getTime()}`;
|
|
2102
2299
|
trendData.testRuns[runKeyForTestHistory] = histRunReport.results.map(
|
|
2103
2300
|
(test) => ({
|
|
2104
|
-
testName: test.name,
|
|
2301
|
+
testName: test.name,
|
|
2105
2302
|
duration: test.duration,
|
|
2106
2303
|
status: test.status,
|
|
2107
|
-
timestamp: new Date(test.startTime),
|
|
2304
|
+
timestamp: new Date(test.startTime),
|
|
2108
2305
|
})
|
|
2109
2306
|
);
|
|
2110
2307
|
}
|
|
2111
2308
|
}
|
|
2112
2309
|
});
|
|
2113
|
-
// Ensure trendData.overall is sorted by timestamp if not already
|
|
2114
2310
|
trendData.overall.sort(
|
|
2115
2311
|
(a, b) => a.timestamp.getTime() - b.timestamp.getTime()
|
|
2116
2312
|
);
|
|
@@ -2118,8 +2314,6 @@ async function main() {
|
|
|
2118
2314
|
|
|
2119
2315
|
// Step 5: Generate and write HTML
|
|
2120
2316
|
try {
|
|
2121
|
-
// currentRunReportData is for the main content (test list, summary cards of *this* run)
|
|
2122
|
-
// trendData is for the historical charts and test history section
|
|
2123
2317
|
const htmlContent = generateHTML(currentRunReportData, trendData);
|
|
2124
2318
|
await fs.writeFile(reportHtmlPath, htmlContent, "utf-8");
|
|
2125
2319
|
console.log(
|
|
@@ -2130,12 +2324,10 @@ async function main() {
|
|
|
2130
2324
|
console.log(chalk.gray(`(You can open this file in your browser)`));
|
|
2131
2325
|
} catch (error) {
|
|
2132
2326
|
console.error(chalk.red(`Error generating HTML report: ${error.message}`));
|
|
2133
|
-
console.error(chalk.red(error.stack));
|
|
2327
|
+
console.error(chalk.red(error.stack));
|
|
2134
2328
|
process.exit(1);
|
|
2135
2329
|
}
|
|
2136
2330
|
}
|
|
2137
|
-
|
|
2138
|
-
// Make sure main() is called at the end of your script
|
|
2139
2331
|
main().catch((err) => {
|
|
2140
2332
|
console.error(
|
|
2141
2333
|
chalk.red.bold(`Unhandled error during script execution: ${err.message}`)
|