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