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