@arghajit/dummy 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -77
- package/dist/reporter/attachment-utils.js +41 -33
- package/dist/reporter/playwright-pulse-reporter.d.ts +5 -0
- package/dist/reporter/playwright-pulse-reporter.js +269 -145
- package/dist/reporter/tsconfig.reporter.tsbuildinfo +1 -0
- package/dist/types/index.d.ts +26 -2
- package/package.json +17 -5
- package/scripts/generate-email-report.mjs +714 -0
- package/scripts/generate-report.mjs +3034 -0
- package/scripts/generate-static-report.mjs +2186 -1285
- package/scripts/generate-trend.mjs +1 -1
- package/scripts/merge-pulse-report.js +1 -0
- package/scripts/{sendReport.js → sendReport.mjs} +143 -76
- package/screenshots/127-0-0-1-5500-pulse-report-output-playwright-pulse-static-report-html-i-Phone-14-Pro-Max-1.png +0 -0
- package/screenshots/127-0-0-1-5500-pulse-report-output-playwright-pulse-static-report-html-i-Phone-14-Pro-Max.png +0 -0
- package/screenshots/Email-report.jpg +0 -0
- package/screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html-1.png +0 -0
- package/screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html-2.png +0 -0
- package/screenshots/Users-arghajitsingha-Downloads-pulse-report-1-playwright-pulse-static-report-html.png +0 -0
- package/screenshots/image.png +0 -0
|
@@ -0,0 +1,3034 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as fs from "fs/promises";
|
|
4
|
+
import { readFileSync, existsSync as fsExistsSync } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fork } from "child_process";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
// Use dynamic import for chalk as it's ESM only
|
|
10
|
+
let chalk;
|
|
11
|
+
try {
|
|
12
|
+
chalk = (await import("chalk")).default;
|
|
13
|
+
} catch (e) {
|
|
14
|
+
console.warn("Chalk could not be imported. Using plain console logs.");
|
|
15
|
+
chalk = {
|
|
16
|
+
green: (text) => text,
|
|
17
|
+
red: (text) => text,
|
|
18
|
+
yellow: (text) => text,
|
|
19
|
+
blue: (text) => text,
|
|
20
|
+
bold: (text) => text,
|
|
21
|
+
gray: (text) => text,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// Default configuration
|
|
25
|
+
const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
26
|
+
const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
|
|
27
|
+
const DEFAULT_HTML_FILE = "playwright-pulse-report.html";
|
|
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
|
+
}
|
|
152
|
+
function sanitizeHTML(str) {
|
|
153
|
+
if (str === null || str === undefined) return "";
|
|
154
|
+
return String(str).replace(/[&<>"']/g, (match) => {
|
|
155
|
+
const replacements = {
|
|
156
|
+
"&": "&",
|
|
157
|
+
"<": "<",
|
|
158
|
+
">": ">",
|
|
159
|
+
'"': '"',
|
|
160
|
+
"'": "'",
|
|
161
|
+
};
|
|
162
|
+
return replacements[match] || match;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function capitalize(str) {
|
|
166
|
+
if (!str) return "";
|
|
167
|
+
return str[0].toUpperCase() + str.slice(1).toLowerCase();
|
|
168
|
+
}
|
|
169
|
+
function formatPlaywrightError(error) {
|
|
170
|
+
const commandOutput = ansiToHtml(error || error.message);
|
|
171
|
+
return convertPlaywrightErrorToHTML(commandOutput);
|
|
172
|
+
}
|
|
173
|
+
function convertPlaywrightErrorToHTML(str) {
|
|
174
|
+
if (!str) return "";
|
|
175
|
+
return str
|
|
176
|
+
.replace(/^(\s+)/gm, (match) =>
|
|
177
|
+
match.replace(/ /g, " ").replace(/\t/g, " ")
|
|
178
|
+
)
|
|
179
|
+
.replace(/<red>/g, '<span style="color: red;">')
|
|
180
|
+
.replace(/<green>/g, '<span style="color: green;">')
|
|
181
|
+
.replace(/<dim>/g, '<span style="opacity: 0.6;">')
|
|
182
|
+
.replace(/<intensity>/g, '<span style="font-weight: bold;">')
|
|
183
|
+
.replace(/<\/color>/g, "</span>")
|
|
184
|
+
.replace(/<\/intensity>/g, "</span>")
|
|
185
|
+
.replace(/\n/g, "<br>");
|
|
186
|
+
}
|
|
187
|
+
function formatDuration(ms, options = {}) {
|
|
188
|
+
const {
|
|
189
|
+
precision = 1,
|
|
190
|
+
invalidInputReturn = "N/A",
|
|
191
|
+
defaultForNullUndefinedNegative = null,
|
|
192
|
+
} = options;
|
|
193
|
+
|
|
194
|
+
const validPrecision = Math.max(0, Math.floor(precision));
|
|
195
|
+
const zeroWithPrecision = (0).toFixed(validPrecision) + "s";
|
|
196
|
+
const resolvedNullUndefNegReturn =
|
|
197
|
+
defaultForNullUndefinedNegative === null
|
|
198
|
+
? zeroWithPrecision
|
|
199
|
+
: defaultForNullUndefinedNegative;
|
|
200
|
+
|
|
201
|
+
if (ms === undefined || ms === null) {
|
|
202
|
+
return resolvedNullUndefNegReturn;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const numMs = Number(ms);
|
|
206
|
+
|
|
207
|
+
if (Number.isNaN(numMs) || !Number.isFinite(numMs)) {
|
|
208
|
+
return invalidInputReturn;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (numMs < 0) {
|
|
212
|
+
return resolvedNullUndefNegReturn;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (numMs === 0) {
|
|
216
|
+
return zeroWithPrecision;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const MS_PER_SECOND = 1000;
|
|
220
|
+
const SECONDS_PER_MINUTE = 60;
|
|
221
|
+
const MINUTES_PER_HOUR = 60;
|
|
222
|
+
const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
|
|
223
|
+
|
|
224
|
+
const totalRawSeconds = numMs / MS_PER_SECOND;
|
|
225
|
+
|
|
226
|
+
if (
|
|
227
|
+
totalRawSeconds < SECONDS_PER_MINUTE &&
|
|
228
|
+
Math.ceil(totalRawSeconds) < SECONDS_PER_MINUTE
|
|
229
|
+
) {
|
|
230
|
+
return `${totalRawSeconds.toFixed(validPrecision)}s`;
|
|
231
|
+
} else {
|
|
232
|
+
const totalMsRoundedUpToSecond =
|
|
233
|
+
Math.ceil(numMs / MS_PER_SECOND) * MS_PER_SECOND;
|
|
234
|
+
|
|
235
|
+
let remainingMs = totalMsRoundedUpToSecond;
|
|
236
|
+
|
|
237
|
+
const h = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_HOUR));
|
|
238
|
+
remainingMs %= MS_PER_SECOND * SECONDS_PER_HOUR;
|
|
239
|
+
|
|
240
|
+
const m = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_MINUTE));
|
|
241
|
+
remainingMs %= MS_PER_SECOND * SECONDS_PER_MINUTE;
|
|
242
|
+
|
|
243
|
+
const s = Math.floor(remainingMs / MS_PER_SECOND);
|
|
244
|
+
|
|
245
|
+
const parts = [];
|
|
246
|
+
if (h > 0) {
|
|
247
|
+
parts.push(`${h}h`);
|
|
248
|
+
}
|
|
249
|
+
if (h > 0 || m > 0 || numMs >= MS_PER_SECOND * SECONDS_PER_MINUTE) {
|
|
250
|
+
parts.push(`${m}m`);
|
|
251
|
+
}
|
|
252
|
+
parts.push(`${s}s`);
|
|
253
|
+
|
|
254
|
+
return parts.join(" ");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function generateTestTrendsChart(trendData) {
|
|
258
|
+
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
259
|
+
return '<div class="no-data">No overall trend data available for test counts.</div>';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const chartId = `testTrendsChart-${Date.now()}-${Math.random()
|
|
263
|
+
.toString(36)
|
|
264
|
+
.substring(2, 7)}`;
|
|
265
|
+
const renderFunctionName = `renderTestTrendsChart_${chartId.replace(
|
|
266
|
+
/-/g,
|
|
267
|
+
"_"
|
|
268
|
+
)}`;
|
|
269
|
+
const runs = trendData.overall;
|
|
270
|
+
|
|
271
|
+
const series = [
|
|
272
|
+
{
|
|
273
|
+
name: "Total",
|
|
274
|
+
data: runs.map((r) => r.totalTests),
|
|
275
|
+
color: "var(--primary-color)",
|
|
276
|
+
marker: { symbol: "circle" },
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: "Passed",
|
|
280
|
+
data: runs.map((r) => r.passed),
|
|
281
|
+
color: "var(--success-color)",
|
|
282
|
+
marker: { symbol: "circle" },
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: "Failed",
|
|
286
|
+
data: runs.map((r) => r.failed),
|
|
287
|
+
color: "var(--danger-color)",
|
|
288
|
+
marker: { symbol: "circle" },
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: "Skipped",
|
|
292
|
+
data: runs.map((r) => r.skipped || 0),
|
|
293
|
+
color: "var(--warning-color)",
|
|
294
|
+
marker: { symbol: "circle" },
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
const runsForTooltip = runs.map((r) => ({
|
|
298
|
+
runId: r.runId,
|
|
299
|
+
timestamp: r.timestamp,
|
|
300
|
+
duration: r.duration,
|
|
301
|
+
}));
|
|
302
|
+
|
|
303
|
+
const categoriesString = JSON.stringify(runs.map((run, i) => `Run ${i + 1}`));
|
|
304
|
+
const seriesString = JSON.stringify(series);
|
|
305
|
+
const runsForTooltipString = JSON.stringify(runsForTooltip);
|
|
306
|
+
|
|
307
|
+
return `
|
|
308
|
+
<div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
|
|
309
|
+
<div class="no-data">Loading Test Volume Trends...</div>
|
|
310
|
+
</div>
|
|
311
|
+
<script>
|
|
312
|
+
window.${renderFunctionName} = function() {
|
|
313
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
314
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found for lazy loading."); return; }
|
|
315
|
+
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
316
|
+
try {
|
|
317
|
+
chartContainer.innerHTML = ''; // Clear placeholder
|
|
318
|
+
const chartOptions = {
|
|
319
|
+
chart: { type: "line", height: 350, backgroundColor: "transparent" },
|
|
320
|
+
title: { text: null },
|
|
321
|
+
xAxis: { categories: ${categoriesString}, crosshair: true, labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}},
|
|
322
|
+
yAxis: { title: { text: "Test Count", style: { color: 'var(--text-color)'} }, min: 0, labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}},
|
|
323
|
+
legend: { layout: "horizontal", align: "center", verticalAlign: "bottom", itemStyle: { fontSize: "12px", color: 'var(--text-color)' }},
|
|
324
|
+
plotOptions: { series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}}, line: { lineWidth: 2.5 }},
|
|
325
|
+
tooltip: {
|
|
326
|
+
shared: true, useHTML: true, backgroundColor: 'rgba(10,10,10,0.92)', borderColor: 'rgba(10,10,10,0.92)', style: { color: '#f5f5f5' },
|
|
327
|
+
formatter: function () {
|
|
328
|
+
const runsData = ${runsForTooltipString};
|
|
329
|
+
const pointIndex = this.points[0].point.x;
|
|
330
|
+
const run = runsData[pointIndex];
|
|
331
|
+
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' + 'Date: ' + new Date(run.timestamp).toLocaleString() + '<br><br>';
|
|
332
|
+
this.points.forEach(point => { tooltip += '<span style="color:' + point.color + '">●</span> ' + point.series.name + ': <b>' + point.y + '</b><br>'; });
|
|
333
|
+
tooltip += '<br>Duration: ' + formatDuration(run.duration);
|
|
334
|
+
return tooltip;
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
series: ${seriesString},
|
|
338
|
+
credits: { enabled: false }
|
|
339
|
+
};
|
|
340
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
341
|
+
} catch (e) {
|
|
342
|
+
console.error("Error rendering chart ${chartId} (lazy):", e);
|
|
343
|
+
chartContainer.innerHTML = '<div class="no-data">Error rendering test trends chart.</div>';
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
chartContainer.innerHTML = '<div class="no-data">Charting library not available for test trends.</div>';
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
</script>
|
|
350
|
+
`;
|
|
351
|
+
}
|
|
352
|
+
function generateDurationTrendChart(trendData) {
|
|
353
|
+
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
354
|
+
return '<div class="no-data">No overall trend data available for durations.</div>';
|
|
355
|
+
}
|
|
356
|
+
const chartId = `durationTrendChart-${Date.now()}-${Math.random()
|
|
357
|
+
.toString(36)
|
|
358
|
+
.substring(2, 7)}`;
|
|
359
|
+
const renderFunctionName = `renderDurationTrendChart_${chartId.replace(
|
|
360
|
+
/-/g,
|
|
361
|
+
"_"
|
|
362
|
+
)}`;
|
|
363
|
+
const runs = trendData.overall;
|
|
364
|
+
|
|
365
|
+
const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
|
|
366
|
+
|
|
367
|
+
const chartDataString = JSON.stringify(runs.map((run) => run.duration));
|
|
368
|
+
const categoriesString = JSON.stringify(runs.map((run, i) => `Run ${i + 1}`));
|
|
369
|
+
const runsForTooltip = runs.map((r) => ({
|
|
370
|
+
runId: r.runId,
|
|
371
|
+
timestamp: r.timestamp,
|
|
372
|
+
duration: r.duration,
|
|
373
|
+
totalTests: r.totalTests,
|
|
374
|
+
}));
|
|
375
|
+
const runsForTooltipString = JSON.stringify(runsForTooltip);
|
|
376
|
+
|
|
377
|
+
const seriesStringForRender = `[{
|
|
378
|
+
name: 'Duration',
|
|
379
|
+
data: ${chartDataString},
|
|
380
|
+
color: 'var(--accent-color-alt)',
|
|
381
|
+
type: 'area',
|
|
382
|
+
marker: { symbol: 'circle', enabled: true, radius: 4, states: { hover: { radius: 6, lineWidthPlus: 0 } } },
|
|
383
|
+
fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(${accentColorAltRGB}, 0.4)'], [1, 'rgba(${accentColorAltRGB}, 0.05)']] },
|
|
384
|
+
lineWidth: 2.5
|
|
385
|
+
}]`;
|
|
386
|
+
|
|
387
|
+
return `
|
|
388
|
+
<div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
|
|
389
|
+
<div class="no-data">Loading Duration Trends...</div>
|
|
390
|
+
</div>
|
|
391
|
+
<script>
|
|
392
|
+
window.${renderFunctionName} = function() {
|
|
393
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
394
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found for lazy loading."); return; }
|
|
395
|
+
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
396
|
+
try {
|
|
397
|
+
chartContainer.innerHTML = ''; // Clear placeholder
|
|
398
|
+
const chartOptions = {
|
|
399
|
+
chart: { type: 'area', height: 350, backgroundColor: 'transparent' },
|
|
400
|
+
title: { text: null },
|
|
401
|
+
xAxis: { categories: ${categoriesString}, crosshair: true, labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}},
|
|
402
|
+
yAxis: {
|
|
403
|
+
title: { text: 'Duration', style: { color: 'var(--text-color)' } },
|
|
404
|
+
labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)', fontSize: '12px' }},
|
|
405
|
+
min: 0
|
|
406
|
+
},
|
|
407
|
+
legend: { layout: 'horizontal', align: 'center', verticalAlign: 'bottom', itemStyle: { fontSize: '12px', color: 'var(--text-color)' }},
|
|
408
|
+
plotOptions: { area: { lineWidth: 2.5, states: { hover: { lineWidthPlus: 0 } }, threshold: null }},
|
|
409
|
+
tooltip: {
|
|
410
|
+
shared: true, useHTML: true, backgroundColor: 'rgba(10,10,10,0.92)', borderColor: 'rgba(10,10,10,0.92)', style: { color: '#f5f5f5' },
|
|
411
|
+
formatter: function () {
|
|
412
|
+
const runsData = ${runsForTooltipString};
|
|
413
|
+
const pointIndex = this.points[0].point.x;
|
|
414
|
+
const run = runsData[pointIndex];
|
|
415
|
+
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' + 'Date: ' + new Date(run.timestamp).toLocaleString() + '<br>';
|
|
416
|
+
this.points.forEach(point => { tooltip += '<span style="color:' + point.series.color + '">●</span> ' + point.series.name + ': <b>' + formatDuration(point.y) + '</b><br>'; });
|
|
417
|
+
tooltip += '<br>Tests: ' + run.totalTests;
|
|
418
|
+
return tooltip;
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
series: ${seriesStringForRender}, // This is already a string representation of an array
|
|
422
|
+
credits: { enabled: false }
|
|
423
|
+
};
|
|
424
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
425
|
+
} catch (e) {
|
|
426
|
+
console.error("Error rendering chart ${chartId} (lazy):", e);
|
|
427
|
+
chartContainer.innerHTML = '<div class="no-data">Error rendering duration trend chart.</div>';
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
chartContainer.innerHTML = '<div class="no-data">Charting library not available for duration trends.</div>';
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
</script>
|
|
434
|
+
`;
|
|
435
|
+
}
|
|
436
|
+
function formatDate(dateStrOrDate) {
|
|
437
|
+
if (!dateStrOrDate) return "N/A";
|
|
438
|
+
try {
|
|
439
|
+
const date = new Date(dateStrOrDate);
|
|
440
|
+
if (isNaN(date.getTime())) return "Invalid Date";
|
|
441
|
+
return (
|
|
442
|
+
date.toLocaleDateString(undefined, {
|
|
443
|
+
year: "2-digit",
|
|
444
|
+
month: "2-digit",
|
|
445
|
+
day: "2-digit",
|
|
446
|
+
}) +
|
|
447
|
+
" " +
|
|
448
|
+
date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
|
|
449
|
+
);
|
|
450
|
+
} catch (e) {
|
|
451
|
+
return "Invalid Date Format";
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function generateTestHistoryChart(history) {
|
|
455
|
+
if (!history || history.length === 0)
|
|
456
|
+
return '<div class="no-data-chart">No data for chart</div>';
|
|
457
|
+
const validHistory = history.filter(
|
|
458
|
+
(h) => h && typeof h.duration === "number" && h.duration >= 0
|
|
459
|
+
);
|
|
460
|
+
if (validHistory.length === 0)
|
|
461
|
+
return '<div class="no-data-chart">No valid data for chart</div>';
|
|
462
|
+
|
|
463
|
+
const chartId = `testHistoryChart-${Date.now()}-${Math.random()
|
|
464
|
+
.toString(36)
|
|
465
|
+
.substring(2, 7)}`;
|
|
466
|
+
const renderFunctionName = `renderTestHistoryChart_${chartId.replace(
|
|
467
|
+
/-/g,
|
|
468
|
+
"_"
|
|
469
|
+
)}`;
|
|
470
|
+
|
|
471
|
+
const seriesDataPoints = validHistory.map((run) => {
|
|
472
|
+
let color;
|
|
473
|
+
switch (String(run.status).toLowerCase()) {
|
|
474
|
+
case "passed":
|
|
475
|
+
color = "var(--success-color)";
|
|
476
|
+
break;
|
|
477
|
+
case "failed":
|
|
478
|
+
color = "var(--danger-color)";
|
|
479
|
+
break;
|
|
480
|
+
case "skipped":
|
|
481
|
+
color = "var(--warning-color)";
|
|
482
|
+
break;
|
|
483
|
+
default:
|
|
484
|
+
color = "var(--dark-gray-color)";
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
y: run.duration,
|
|
488
|
+
marker: {
|
|
489
|
+
fillColor: color,
|
|
490
|
+
symbol: "circle",
|
|
491
|
+
radius: 3.5,
|
|
492
|
+
states: { hover: { radius: 5 } },
|
|
493
|
+
},
|
|
494
|
+
status: run.status,
|
|
495
|
+
runId: run.runId,
|
|
496
|
+
};
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const accentColorRGB = "103, 58, 183"; // Assuming var(--accent-color) is Deep Purple #673ab7
|
|
500
|
+
|
|
501
|
+
const categoriesString = JSON.stringify(
|
|
502
|
+
validHistory.map((_, i) => `R${i + 1}`)
|
|
503
|
+
);
|
|
504
|
+
const seriesDataPointsString = JSON.stringify(seriesDataPoints);
|
|
505
|
+
|
|
506
|
+
return `
|
|
507
|
+
<div id="${chartId}" style="width: 320px; height: 100px;" class="lazy-load-chart" data-render-function-name="${renderFunctionName}">
|
|
508
|
+
<div class="no-data-chart">Loading History...</div>
|
|
509
|
+
</div>
|
|
510
|
+
<script>
|
|
511
|
+
window.${renderFunctionName} = function() {
|
|
512
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
513
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found for lazy loading."); return; }
|
|
514
|
+
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
515
|
+
try {
|
|
516
|
+
chartContainer.innerHTML = ''; // Clear placeholder
|
|
517
|
+
const chartOptions = {
|
|
518
|
+
chart: { type: 'area', height: 100, width: 320, backgroundColor: 'transparent', spacing: [10,10,15,35] },
|
|
519
|
+
title: { text: null },
|
|
520
|
+
xAxis: { categories: ${categoriesString}, labels: { style: { fontSize: '10px', color: 'var(--text-color-secondary)' }}},
|
|
521
|
+
yAxis: {
|
|
522
|
+
title: { text: null },
|
|
523
|
+
labels: { formatter: function() { return formatDuration(this.value); }, style: { fontSize: '10px', color: 'var(--text-color-secondary)' }, align: 'left', x: -35, y: 3 },
|
|
524
|
+
min: 0, gridLineWidth: 0, tickAmount: 4
|
|
525
|
+
},
|
|
526
|
+
legend: { enabled: false },
|
|
527
|
+
plotOptions: {
|
|
528
|
+
area: {
|
|
529
|
+
lineWidth: 2, lineColor: 'var(--accent-color)',
|
|
530
|
+
fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(${accentColorRGB}, 0.4)'],[1, 'rgba(${accentColorRGB}, 0)']]},
|
|
531
|
+
marker: { enabled: true }, threshold: null
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
tooltip: {
|
|
535
|
+
useHTML: true, backgroundColor: 'rgba(10,10,10,0.92)', borderColor: 'rgba(10,10,10,0.92)', style: { color: '#f5f5f5', padding: '8px' },
|
|
536
|
+
formatter: function() {
|
|
537
|
+
const pointData = this.point;
|
|
538
|
+
let statusBadgeHtml = '<span style="padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; background-color: ';
|
|
539
|
+
switch(String(pointData.status).toLowerCase()) {
|
|
540
|
+
case 'passed': statusBadgeHtml += 'var(--success-color)'; break;
|
|
541
|
+
case 'failed': statusBadgeHtml += 'var(--danger-color)'; break;
|
|
542
|
+
case 'skipped': statusBadgeHtml += 'var(--warning-color)'; break;
|
|
543
|
+
default: statusBadgeHtml += 'var(--dark-gray-color)';
|
|
544
|
+
}
|
|
545
|
+
statusBadgeHtml += ';">' + String(pointData.status).toUpperCase() + '</span>';
|
|
546
|
+
return '<strong>Run ' + (pointData.runId || (this.point.index + 1)) + '</strong><br>' + 'Status: ' + statusBadgeHtml + '<br>' + 'Duration: ' + formatDuration(pointData.y);
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
series: [{ data: ${seriesDataPointsString}, showInLegend: false }],
|
|
550
|
+
credits: { enabled: false }
|
|
551
|
+
};
|
|
552
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
553
|
+
} catch (e) {
|
|
554
|
+
console.error("Error rendering chart ${chartId} (lazy):", e);
|
|
555
|
+
chartContainer.innerHTML = '<div class="no-data-chart">Error rendering history chart.</div>';
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
chartContainer.innerHTML = '<div class="no-data-chart">Charting library not available for history.</div>';
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
</script>
|
|
562
|
+
`;
|
|
563
|
+
}
|
|
564
|
+
function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
565
|
+
const total = data.reduce((sum, d) => sum + d.value, 0);
|
|
566
|
+
if (total === 0) {
|
|
567
|
+
return '<div class="pie-chart-wrapper"><h3>Test Distribution</h3><div class="no-data">No data for Test Distribution chart.</div></div>';
|
|
568
|
+
}
|
|
569
|
+
const passedEntry = data.find((d) => d.label === "Passed");
|
|
570
|
+
const passedPercentage = Math.round(
|
|
571
|
+
((passedEntry ? passedEntry.value : 0) / total) * 100
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
const chartId = `pieChart-${Date.now()}-${Math.random()
|
|
575
|
+
.toString(36)
|
|
576
|
+
.substring(2, 7)}`;
|
|
577
|
+
|
|
578
|
+
const seriesData = [
|
|
579
|
+
{
|
|
580
|
+
name: "Tests", // Changed from 'Test Distribution' for tooltip clarity
|
|
581
|
+
data: data
|
|
582
|
+
.filter((d) => d.value > 0)
|
|
583
|
+
.map((d) => {
|
|
584
|
+
let color;
|
|
585
|
+
switch (d.label) {
|
|
586
|
+
case "Passed":
|
|
587
|
+
color = "var(--success-color)";
|
|
588
|
+
break;
|
|
589
|
+
case "Failed":
|
|
590
|
+
color = "var(--danger-color)";
|
|
591
|
+
break;
|
|
592
|
+
case "Skipped":
|
|
593
|
+
color = "var(--warning-color)";
|
|
594
|
+
break;
|
|
595
|
+
default:
|
|
596
|
+
color = "#CCCCCC"; // A neutral default color
|
|
597
|
+
}
|
|
598
|
+
return { name: d.label, y: d.value, color: color };
|
|
599
|
+
}),
|
|
600
|
+
size: "100%",
|
|
601
|
+
innerSize: "55%",
|
|
602
|
+
dataLabels: { enabled: false },
|
|
603
|
+
showInLegend: true,
|
|
604
|
+
},
|
|
605
|
+
];
|
|
606
|
+
|
|
607
|
+
// Approximate font size for center text, can be adjusted or made dynamic with more client-side JS
|
|
608
|
+
const centerTitleFontSize =
|
|
609
|
+
Math.max(12, Math.min(chartWidth, chartHeight) / 12) + "px";
|
|
610
|
+
const centerSubtitleFontSize =
|
|
611
|
+
Math.max(10, Math.min(chartWidth, chartHeight) / 18) + "px";
|
|
612
|
+
|
|
613
|
+
const optionsObjectString = `
|
|
614
|
+
{
|
|
615
|
+
chart: {
|
|
616
|
+
type: 'pie',
|
|
617
|
+
width: ${chartWidth},
|
|
618
|
+
height: ${chartHeight - 40
|
|
619
|
+
}, // Adjusted height to make space for legend if chartHeight is for the whole wrapper
|
|
620
|
+
backgroundColor: 'transparent',
|
|
621
|
+
plotShadow: false,
|
|
622
|
+
spacingBottom: 40 // Ensure space for legend
|
|
623
|
+
},
|
|
624
|
+
title: {
|
|
625
|
+
text: '${passedPercentage}%',
|
|
626
|
+
align: 'center',
|
|
627
|
+
verticalAlign: 'middle',
|
|
628
|
+
y: 5,
|
|
629
|
+
style: { fontSize: '${centerTitleFontSize}', fontWeight: 'bold', color: 'var(--primary-color)' }
|
|
630
|
+
},
|
|
631
|
+
subtitle: {
|
|
632
|
+
text: 'Passed',
|
|
633
|
+
align: 'center',
|
|
634
|
+
verticalAlign: 'middle',
|
|
635
|
+
y: 25,
|
|
636
|
+
style: { fontSize: '${centerSubtitleFontSize}', color: 'var(--text-color-secondary)' }
|
|
637
|
+
},
|
|
638
|
+
tooltip: {
|
|
639
|
+
pointFormat: '{series.name}: <b>{point.percentage:.1f}%</b> ({point.y})',
|
|
640
|
+
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
641
|
+
borderColor: 'rgba(10,10,10,0.92)',
|
|
642
|
+
style: { color: '#f5f5f5' }
|
|
643
|
+
},
|
|
644
|
+
legend: {
|
|
645
|
+
layout: 'horizontal',
|
|
646
|
+
align: 'center',
|
|
647
|
+
verticalAlign: 'bottom',
|
|
648
|
+
itemStyle: { color: 'var(--text-color)', fontWeight: 'normal', fontSize: '12px' }
|
|
649
|
+
},
|
|
650
|
+
plotOptions: {
|
|
651
|
+
pie: {
|
|
652
|
+
allowPointSelect: true,
|
|
653
|
+
cursor: 'pointer',
|
|
654
|
+
borderWidth: 3,
|
|
655
|
+
borderColor: 'var(--card-background-color)', // Match D3 style
|
|
656
|
+
states: {
|
|
657
|
+
hover: {
|
|
658
|
+
// Using default Highcharts halo which is generally good
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
series: ${JSON.stringify(seriesData)},
|
|
664
|
+
credits: { enabled: false }
|
|
665
|
+
}
|
|
666
|
+
`;
|
|
667
|
+
|
|
668
|
+
return `
|
|
669
|
+
<div class="pie-chart-wrapper" style="align-items: center; max-height: 450px">
|
|
670
|
+
<div style="display: flex; align-items: start; width: 100%;"><h3>Test Distribution</h3></div>
|
|
671
|
+
<div id="${chartId}" style="width: ${chartWidth}px; height: ${chartHeight - 40
|
|
672
|
+
}px;"></div>
|
|
673
|
+
<script>
|
|
674
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
675
|
+
if (typeof Highcharts !== 'undefined') {
|
|
676
|
+
try {
|
|
677
|
+
const chartOptions = ${optionsObjectString};
|
|
678
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
679
|
+
} catch (e) {
|
|
680
|
+
console.error("Error rendering chart ${chartId}:", e);
|
|
681
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data">Error rendering pie chart.</div>';
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
684
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data">Charting library not available.</div>';
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
</script>
|
|
688
|
+
</div>
|
|
689
|
+
`;
|
|
690
|
+
}
|
|
691
|
+
function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
692
|
+
// Format memory for display
|
|
693
|
+
const formattedMemory = environment.memory.replace(/(\d+\.\d{2})GB/, "$1 GB");
|
|
694
|
+
|
|
695
|
+
// Generate a unique ID for the dashboard
|
|
696
|
+
const dashboardId = `envDashboard-${Date.now()}-${Math.random()
|
|
697
|
+
.toString(36)
|
|
698
|
+
.substring(2, 7)}`;
|
|
699
|
+
|
|
700
|
+
const cardHeight = Math.floor(dashboardHeight * 0.44);
|
|
701
|
+
const cardContentPadding = 16; // px
|
|
702
|
+
|
|
703
|
+
return `
|
|
704
|
+
<div class="environment-dashboard-wrapper" id="${dashboardId}">
|
|
705
|
+
<style>
|
|
706
|
+
.environment-dashboard-wrapper *,
|
|
707
|
+
.environment-dashboard-wrapper *::before,
|
|
708
|
+
.environment-dashboard-wrapper *::after {
|
|
709
|
+
box-sizing: border-box;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.environment-dashboard-wrapper {
|
|
713
|
+
--primary-color: #007bff;
|
|
714
|
+
--primary-light-color: #e6f2ff;
|
|
715
|
+
--secondary-color: #6c757d;
|
|
716
|
+
--success-color: #28a745;
|
|
717
|
+
--success-light-color: #eaf6ec;
|
|
718
|
+
--warning-color: #ffc107;
|
|
719
|
+
--warning-light-color: #fff9e6;
|
|
720
|
+
--danger-color: #dc3545;
|
|
721
|
+
|
|
722
|
+
--background-color: #ffffff;
|
|
723
|
+
--card-background-color: #ffffff;
|
|
724
|
+
--text-color: #212529;
|
|
725
|
+
--text-color-secondary: #6c757d;
|
|
726
|
+
--border-color: #dee2e6;
|
|
727
|
+
--border-light-color: #f1f3f5;
|
|
728
|
+
--icon-color: #495057;
|
|
729
|
+
--chip-background: #e9ecef;
|
|
730
|
+
--chip-text: #495057;
|
|
731
|
+
--shadow-color: rgba(0, 0, 0, 0.075);
|
|
732
|
+
|
|
733
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
|
|
734
|
+
background-color: var(--background-color);
|
|
735
|
+
border-radius: 12px;
|
|
736
|
+
box-shadow: 0 6px 12px var(--shadow-color);
|
|
737
|
+
padding: 24px;
|
|
738
|
+
color: var(--text-color);
|
|
739
|
+
display: grid;
|
|
740
|
+
grid-template-columns: 1fr 1fr;
|
|
741
|
+
grid-template-rows: auto 1fr;
|
|
742
|
+
gap: 20px;
|
|
743
|
+
font-size: 14px;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.env-dashboard-header {
|
|
747
|
+
grid-column: 1 / -1;
|
|
748
|
+
display: flex;
|
|
749
|
+
justify-content: space-between;
|
|
750
|
+
align-items: center;
|
|
751
|
+
border-bottom: 1px solid var(--border-color);
|
|
752
|
+
padding-bottom: 16px;
|
|
753
|
+
margin-bottom: 8px;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.env-dashboard-title {
|
|
757
|
+
font-size: 1.5rem;
|
|
758
|
+
font-weight: 600;
|
|
759
|
+
color: var(--text-color);
|
|
760
|
+
margin: 0;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.env-dashboard-subtitle {
|
|
764
|
+
font-size: 0.875rem;
|
|
765
|
+
color: var(--text-color-secondary);
|
|
766
|
+
margin-top: 4px;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.env-card {
|
|
770
|
+
background-color: var(--card-background-color);
|
|
771
|
+
border-radius: 8px;
|
|
772
|
+
padding: ${cardContentPadding}px;
|
|
773
|
+
box-shadow: 0 3px 6px var(--shadow-color);
|
|
774
|
+
height: ${cardHeight}px;
|
|
775
|
+
display: flex;
|
|
776
|
+
flex-direction: column;
|
|
777
|
+
overflow: hidden;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.env-card-header {
|
|
781
|
+
font-weight: 600;
|
|
782
|
+
font-size: 1rem;
|
|
783
|
+
margin-bottom: 12px;
|
|
784
|
+
color: var(--text-color);
|
|
785
|
+
display: flex;
|
|
786
|
+
align-items: center;
|
|
787
|
+
padding-bottom: 8px;
|
|
788
|
+
border-bottom: 1px solid var(--border-light-color);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.env-card-header svg {
|
|
792
|
+
margin-right: 10px;
|
|
793
|
+
width: 18px;
|
|
794
|
+
height: 18px;
|
|
795
|
+
fill: var(--icon-color);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.env-card-content {
|
|
799
|
+
flex-grow: 1;
|
|
800
|
+
overflow-y: auto;
|
|
801
|
+
padding-right: 5px;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.env-detail-row {
|
|
805
|
+
display: flex;
|
|
806
|
+
justify-content: space-between;
|
|
807
|
+
align-items: center;
|
|
808
|
+
padding: 10px 0;
|
|
809
|
+
border-bottom: 1px solid var(--border-light-color);
|
|
810
|
+
font-size: 0.875rem;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.env-detail-row:last-child {
|
|
814
|
+
border-bottom: none;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.env-detail-label {
|
|
818
|
+
color: var(--text-color-secondary);
|
|
819
|
+
font-weight: 500;
|
|
820
|
+
margin-right: 10px;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.env-detail-value {
|
|
824
|
+
color: var(--text-color);
|
|
825
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
826
|
+
text-align: right;
|
|
827
|
+
word-break: break-all;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.env-chip {
|
|
831
|
+
display: inline-block;
|
|
832
|
+
padding: 4px 10px;
|
|
833
|
+
border-radius: 16px;
|
|
834
|
+
font-size: 0.75rem;
|
|
835
|
+
font-weight: 500;
|
|
836
|
+
line-height: 1.2;
|
|
837
|
+
background-color: var(--chip-background);
|
|
838
|
+
color: var(--chip-text);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.env-chip-primary {
|
|
842
|
+
background-color: var(--primary-light-color);
|
|
843
|
+
color: var(--primary-color);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.env-chip-success {
|
|
847
|
+
background-color: var(--success-light-color);
|
|
848
|
+
color: var(--success-color);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.env-chip-warning {
|
|
852
|
+
background-color: var(--warning-light-color);
|
|
853
|
+
color: var(--warning-color);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.env-cpu-cores {
|
|
857
|
+
display: flex;
|
|
858
|
+
align-items: center;
|
|
859
|
+
gap: 6px;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
.env-core-indicator {
|
|
863
|
+
width: 12px;
|
|
864
|
+
height: 12px;
|
|
865
|
+
border-radius: 50%;
|
|
866
|
+
background-color: var(--success-color);
|
|
867
|
+
border: 1px solid rgba(0,0,0,0.1);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.env-core-indicator.inactive {
|
|
871
|
+
background-color: var(--border-light-color);
|
|
872
|
+
opacity: 0.7;
|
|
873
|
+
border-color: var(--border-color);
|
|
874
|
+
}
|
|
875
|
+
</style>
|
|
876
|
+
|
|
877
|
+
<div class="env-dashboard-header">
|
|
878
|
+
<div>
|
|
879
|
+
<h3 class="env-dashboard-title">System Environment</h3>
|
|
880
|
+
<p class="env-dashboard-subtitle">Snapshot of the execution environment</p>
|
|
881
|
+
</div>
|
|
882
|
+
<span class="env-chip env-chip-primary">${environment.host}</span>
|
|
883
|
+
</div>
|
|
884
|
+
|
|
885
|
+
<div class="env-card">
|
|
886
|
+
<div class="env-card-header">
|
|
887
|
+
<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>
|
|
888
|
+
Hardware
|
|
889
|
+
</div>
|
|
890
|
+
<div class="env-card-content">
|
|
891
|
+
<div class="env-detail-row">
|
|
892
|
+
<span class="env-detail-label">CPU Model</span>
|
|
893
|
+
<span class="env-detail-value">${environment.cpu.model}</span>
|
|
894
|
+
</div>
|
|
895
|
+
<div class="env-detail-row">
|
|
896
|
+
<span class="env-detail-label">CPU Cores</span>
|
|
897
|
+
<span class="env-detail-value">
|
|
898
|
+
<div class="env-cpu-cores">
|
|
899
|
+
${Array.from(
|
|
900
|
+
{ length: Math.max(0, environment.cpu.cores || 0) },
|
|
901
|
+
(_, i) =>
|
|
902
|
+
`<div class="env-core-indicator ${i >=
|
|
903
|
+
(environment.cpu.cores >= 8 ? 8 : environment.cpu.cores)
|
|
904
|
+
? "inactive"
|
|
905
|
+
: ""
|
|
906
|
+
}" title="Core ${i + 1}"></div>`
|
|
907
|
+
).join("")}
|
|
908
|
+
<span>${environment.cpu.cores || "N/A"} cores</span>
|
|
909
|
+
</div>
|
|
910
|
+
</span>
|
|
911
|
+
</div>
|
|
912
|
+
<div class="env-detail-row">
|
|
913
|
+
<span class="env-detail-label">Memory</span>
|
|
914
|
+
<span class="env-detail-value">${formattedMemory}</span>
|
|
915
|
+
</div>
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
918
|
+
|
|
919
|
+
<div class="env-card">
|
|
920
|
+
<div class="env-card-header">
|
|
921
|
+
<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>
|
|
922
|
+
Operating System
|
|
923
|
+
</div>
|
|
924
|
+
<div class="env-card-content">
|
|
925
|
+
<div class="env-detail-row">
|
|
926
|
+
<span class="env-detail-label">OS Type</span>
|
|
927
|
+
<span class="env-detail-value">${environment.os.split(" ")[0] === "darwin"
|
|
928
|
+
? "darwin (macOS)"
|
|
929
|
+
: environment.os.split(" ")[0] || "Unknown"
|
|
930
|
+
}</span>
|
|
931
|
+
</div>
|
|
932
|
+
<div class="env-detail-row">
|
|
933
|
+
<span class="env-detail-label">OS Version</span>
|
|
934
|
+
<span class="env-detail-value">${environment.os.split(" ")[1] || "N/A"
|
|
935
|
+
}</span>
|
|
936
|
+
</div>
|
|
937
|
+
<div class="env-detail-row">
|
|
938
|
+
<span class="env-detail-label">Hostname</span>
|
|
939
|
+
<span class="env-detail-value" title="${environment.host}">${environment.host
|
|
940
|
+
}</span>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
</div>
|
|
944
|
+
|
|
945
|
+
<div class="env-card">
|
|
946
|
+
<div class="env-card-header">
|
|
947
|
+
<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>
|
|
948
|
+
Node.js Runtime
|
|
949
|
+
</div>
|
|
950
|
+
<div class="env-card-content">
|
|
951
|
+
<div class="env-detail-row">
|
|
952
|
+
<span class="env-detail-label">Node Version</span>
|
|
953
|
+
<span class="env-detail-value">${environment.node}</span>
|
|
954
|
+
</div>
|
|
955
|
+
<div class="env-detail-row">
|
|
956
|
+
<span class="env-detail-label">V8 Engine</span>
|
|
957
|
+
<span class="env-detail-value">${environment.v8}</span>
|
|
958
|
+
</div>
|
|
959
|
+
<div class="env-detail-row">
|
|
960
|
+
<span class="env-detail-label">Working Dir</span>
|
|
961
|
+
<span class="env-detail-value" title="${environment.cwd}">${environment.cwd.length > 25
|
|
962
|
+
? "..." + environment.cwd.slice(-22)
|
|
963
|
+
: environment.cwd
|
|
964
|
+
}</span>
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
</div>
|
|
968
|
+
|
|
969
|
+
<div class="env-card">
|
|
970
|
+
<div class="env-card-header">
|
|
971
|
+
<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>
|
|
972
|
+
System Summary
|
|
973
|
+
</div>
|
|
974
|
+
<div class="env-card-content">
|
|
975
|
+
<div class="env-detail-row">
|
|
976
|
+
<span class="env-detail-label">Platform Arch</span>
|
|
977
|
+
<span class="env-detail-value">
|
|
978
|
+
<span class="env-chip ${environment.os.includes("darwin") &&
|
|
979
|
+
environment.cpu.model.toLowerCase().includes("apple")
|
|
980
|
+
? "env-chip-success"
|
|
981
|
+
: "env-chip-warning"
|
|
982
|
+
}">
|
|
983
|
+
${environment.os.includes("darwin") &&
|
|
984
|
+
environment.cpu.model.toLowerCase().includes("apple")
|
|
985
|
+
? "Apple Silicon"
|
|
986
|
+
: environment.cpu.model.toLowerCase().includes("arm") ||
|
|
987
|
+
environment.cpu.model.toLowerCase().includes("aarch64")
|
|
988
|
+
? "ARM-based"
|
|
989
|
+
: "x86/Other"
|
|
990
|
+
}
|
|
991
|
+
</span>
|
|
992
|
+
</span>
|
|
993
|
+
</div>
|
|
994
|
+
<div class="env-detail-row">
|
|
995
|
+
<span class="env-detail-label">Memory per Core</span>
|
|
996
|
+
<span class="env-detail-value">${environment.cpu.cores > 0
|
|
997
|
+
? (
|
|
998
|
+
parseFloat(environment.memory) / environment.cpu.cores
|
|
999
|
+
).toFixed(2) + " GB"
|
|
1000
|
+
: "N/A"
|
|
1001
|
+
}</span>
|
|
1002
|
+
</div>
|
|
1003
|
+
<div class="env-detail-row">
|
|
1004
|
+
<span class="env-detail-label">Run Context</span>
|
|
1005
|
+
<span class="env-detail-value">CI/Local Test</span>
|
|
1006
|
+
</div>
|
|
1007
|
+
</div>
|
|
1008
|
+
</div>
|
|
1009
|
+
</div>
|
|
1010
|
+
`;
|
|
1011
|
+
}
|
|
1012
|
+
function generateWorkerDistributionChart(results) {
|
|
1013
|
+
if (!results || results.length === 0) {
|
|
1014
|
+
return '<div class="no-data">No test results data available to display worker distribution.</div>';
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// 1. Sort results by startTime to ensure chronological order
|
|
1018
|
+
const sortedResults = [...results].sort((a, b) => {
|
|
1019
|
+
const timeA = a.startTime ? new Date(a.startTime).getTime() : 0;
|
|
1020
|
+
const timeB = b.startTime ? new Date(b.startTime).getTime() : 0;
|
|
1021
|
+
return timeA - timeB;
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
const workerData = sortedResults.reduce((acc, test) => {
|
|
1025
|
+
const workerId =
|
|
1026
|
+
typeof test.workerId !== "undefined" ? test.workerId : "N/A";
|
|
1027
|
+
if (!acc[workerId]) {
|
|
1028
|
+
acc[workerId] = { passed: 0, failed: 0, skipped: 0, tests: [] };
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const status = String(test.status).toLowerCase();
|
|
1032
|
+
if (status === "passed" || status === "failed" || status === "skipped") {
|
|
1033
|
+
acc[workerId][status]++;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const testTitleParts = test.name.split(" > ");
|
|
1037
|
+
const testTitle =
|
|
1038
|
+
testTitleParts[testTitleParts.length - 1] || "Unnamed Test";
|
|
1039
|
+
// Store both name and status for each test
|
|
1040
|
+
acc[workerId].tests.push({ name: testTitle, status: status });
|
|
1041
|
+
|
|
1042
|
+
return acc;
|
|
1043
|
+
}, {});
|
|
1044
|
+
|
|
1045
|
+
const workerIds = Object.keys(workerData).sort((a, b) => {
|
|
1046
|
+
if (a === "N/A") return 1;
|
|
1047
|
+
if (b === "N/A") return -1;
|
|
1048
|
+
return parseInt(a, 10) - parseInt(b, 10);
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
if (workerIds.length === 0) {
|
|
1052
|
+
return '<div class="no-data">Could not determine worker distribution from test data.</div>';
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const chartId = `workerDistChart-${Date.now()}-${Math.random()
|
|
1056
|
+
.toString(36)
|
|
1057
|
+
.substring(2, 7)}`;
|
|
1058
|
+
const renderFunctionName = `renderWorkerDistChart_${chartId.replace(
|
|
1059
|
+
/-/g,
|
|
1060
|
+
"_"
|
|
1061
|
+
)}`;
|
|
1062
|
+
const modalJsNamespace = `modal_funcs_${chartId.replace(/-/g, "_")}`;
|
|
1063
|
+
|
|
1064
|
+
// The categories now just need the name for the axis labels
|
|
1065
|
+
const categories = workerIds.map((id) => `Worker ${id}`);
|
|
1066
|
+
|
|
1067
|
+
// We pass the full data separately to the script
|
|
1068
|
+
const fullWorkerData = workerIds.map((id) => ({
|
|
1069
|
+
id: id,
|
|
1070
|
+
name: `Worker ${id}`,
|
|
1071
|
+
tests: workerData[id].tests,
|
|
1072
|
+
}));
|
|
1073
|
+
|
|
1074
|
+
const passedData = workerIds.map((id) => workerData[id].passed);
|
|
1075
|
+
const failedData = workerIds.map((id) => workerData[id].failed);
|
|
1076
|
+
const skippedData = workerIds.map((id) => workerData[id].skipped);
|
|
1077
|
+
|
|
1078
|
+
const categoriesString = JSON.stringify(categories);
|
|
1079
|
+
const fullDataString = JSON.stringify(fullWorkerData);
|
|
1080
|
+
const seriesString = JSON.stringify([
|
|
1081
|
+
{ name: "Passed", data: passedData, color: "var(--success-color)" },
|
|
1082
|
+
{ name: "Failed", data: failedData, color: "var(--danger-color)" },
|
|
1083
|
+
{ name: "Skipped", data: skippedData, color: "var(--warning-color)" },
|
|
1084
|
+
]);
|
|
1085
|
+
|
|
1086
|
+
// The HTML now includes the chart container, the modal, and styles for the modal
|
|
1087
|
+
return `
|
|
1088
|
+
<style>
|
|
1089
|
+
.worker-modal-overlay {
|
|
1090
|
+
position: fixed; z-index: 1050; left: 0; top: 0; width: 100%; height: 100%;
|
|
1091
|
+
overflow: auto; background-color: rgba(0,0,0,0.6);
|
|
1092
|
+
display: none; align-items: center; justify-content: center;
|
|
1093
|
+
}
|
|
1094
|
+
.worker-modal-content {
|
|
1095
|
+
background-color: #3d4043;
|
|
1096
|
+
color: var(--card-background-color);
|
|
1097
|
+
margin: auto; padding: 20px; border: 1px solid var(--border-color, #888);
|
|
1098
|
+
width: 80%; max-width: 700px; border-radius: 8px;
|
|
1099
|
+
position: relative; box-shadow: 0 5px 15px rgba(0,0,0,0.5);
|
|
1100
|
+
}
|
|
1101
|
+
.worker-modal-close {
|
|
1102
|
+
position: absolute; top: 10px; right: 20px;
|
|
1103
|
+
font-size: 28px; font-weight: bold; cursor: pointer;
|
|
1104
|
+
line-height: 1;
|
|
1105
|
+
}
|
|
1106
|
+
.worker-modal-close:hover, .worker-modal-close:focus {
|
|
1107
|
+
color: var(--text-color, #000);
|
|
1108
|
+
}
|
|
1109
|
+
#worker-modal-body-${chartId} ul {
|
|
1110
|
+
list-style-type: none; padding-left: 0; margin-top: 15px; max-height: 45vh; overflow-y: auto;
|
|
1111
|
+
}
|
|
1112
|
+
#worker-modal-body-${chartId} li {
|
|
1113
|
+
padding: 8px 5px; border-bottom: 1px solid var(--border-color, #eee);
|
|
1114
|
+
font-size: 0.9em;
|
|
1115
|
+
}
|
|
1116
|
+
#worker-modal-body-${chartId} li:last-child {
|
|
1117
|
+
border-bottom: none;
|
|
1118
|
+
}
|
|
1119
|
+
#worker-modal-body-${chartId} li > span {
|
|
1120
|
+
display: inline-block;
|
|
1121
|
+
width: 70px;
|
|
1122
|
+
font-weight: bold;
|
|
1123
|
+
text-align: right;
|
|
1124
|
+
margin-right: 10px;
|
|
1125
|
+
}
|
|
1126
|
+
</style>
|
|
1127
|
+
|
|
1128
|
+
<div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}" style="min-height: 350px;">
|
|
1129
|
+
<div class="no-data">Loading Worker Distribution Chart...</div>
|
|
1130
|
+
</div>
|
|
1131
|
+
|
|
1132
|
+
<div id="worker-modal-${chartId}" class="worker-modal-overlay">
|
|
1133
|
+
<div class="worker-modal-content">
|
|
1134
|
+
<span class="worker-modal-close">×</span>
|
|
1135
|
+
<h3 id="worker-modal-title-${chartId}" style="text-align: center; margin-top: 0; margin-bottom: 25px; font-size: 1.25em; font-weight: 600; color: #fff"></h3>
|
|
1136
|
+
<div id="worker-modal-body-${chartId}"></div>
|
|
1137
|
+
</div>
|
|
1138
|
+
</div>
|
|
1139
|
+
|
|
1140
|
+
<script>
|
|
1141
|
+
// Namespace for modal functions to avoid global scope pollution
|
|
1142
|
+
window.${modalJsNamespace} = {};
|
|
1143
|
+
|
|
1144
|
+
window.${renderFunctionName} = function() {
|
|
1145
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
1146
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found."); return; }
|
|
1147
|
+
|
|
1148
|
+
// --- Modal Setup ---
|
|
1149
|
+
const modal = document.getElementById('worker-modal-${chartId}');
|
|
1150
|
+
const modalTitle = document.getElementById('worker-modal-title-${chartId}');
|
|
1151
|
+
const modalBody = document.getElementById('worker-modal-body-${chartId}');
|
|
1152
|
+
const closeModalBtn = modal.querySelector('.worker-modal-close');
|
|
1153
|
+
|
|
1154
|
+
window.${modalJsNamespace}.open = function(worker) {
|
|
1155
|
+
if (!worker) return;
|
|
1156
|
+
modalTitle.textContent = 'Test Details for ' + worker.name;
|
|
1157
|
+
|
|
1158
|
+
let testListHtml = '<ul>';
|
|
1159
|
+
if (worker.tests && worker.tests.length > 0) {
|
|
1160
|
+
worker.tests.forEach(test => {
|
|
1161
|
+
let color = 'inherit';
|
|
1162
|
+
if (test.status === 'passed') color = 'var(--success-color)';
|
|
1163
|
+
else if (test.status === 'failed') color = 'var(--danger-color)';
|
|
1164
|
+
else if (test.status === 'skipped') color = 'var(--warning-color)';
|
|
1165
|
+
|
|
1166
|
+
const escapedName = test.name.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1167
|
+
testListHtml += \`<li style="color: \${color};"><span style="color: \${color}">[\${test.status.toUpperCase()}]</span> \${escapedName}</li>\`;
|
|
1168
|
+
});
|
|
1169
|
+
} else {
|
|
1170
|
+
testListHtml += '<li>No detailed test data available for this worker.</li>';
|
|
1171
|
+
}
|
|
1172
|
+
testListHtml += '</ul>';
|
|
1173
|
+
|
|
1174
|
+
modalBody.innerHTML = testListHtml;
|
|
1175
|
+
modal.style.display = 'flex';
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
const closeModal = function() {
|
|
1179
|
+
modal.style.display = 'none';
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
closeModalBtn.onclick = closeModal;
|
|
1183
|
+
modal.onclick = function(event) {
|
|
1184
|
+
// Close if clicked on the dark overlay background
|
|
1185
|
+
if (event.target == modal) {
|
|
1186
|
+
closeModal();
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
// --- Highcharts Setup ---
|
|
1192
|
+
if (typeof Highcharts !== 'undefined') {
|
|
1193
|
+
try {
|
|
1194
|
+
chartContainer.innerHTML = '';
|
|
1195
|
+
const fullData = ${fullDataString};
|
|
1196
|
+
|
|
1197
|
+
const chartOptions = {
|
|
1198
|
+
chart: { type: 'bar', height: 350, backgroundColor: 'transparent' },
|
|
1199
|
+
title: { text: null },
|
|
1200
|
+
xAxis: {
|
|
1201
|
+
categories: ${categoriesString},
|
|
1202
|
+
title: { text: 'Worker ID' },
|
|
1203
|
+
labels: { style: { color: 'var(--text-color-secondary)' }}
|
|
1204
|
+
},
|
|
1205
|
+
yAxis: {
|
|
1206
|
+
min: 0,
|
|
1207
|
+
title: { text: 'Number of Tests' },
|
|
1208
|
+
labels: { style: { color: 'var(--text-color-secondary)' }},
|
|
1209
|
+
stackLabels: { enabled: true, style: { fontWeight: 'bold', color: 'var(--text-color)' } }
|
|
1210
|
+
},
|
|
1211
|
+
legend: { reversed: true, itemStyle: { fontSize: "12px", color: 'var(--text-color)' } },
|
|
1212
|
+
plotOptions: {
|
|
1213
|
+
series: {
|
|
1214
|
+
stacking: 'normal',
|
|
1215
|
+
cursor: 'pointer',
|
|
1216
|
+
point: {
|
|
1217
|
+
events: {
|
|
1218
|
+
click: function () {
|
|
1219
|
+
// 'this.x' is the index of the category
|
|
1220
|
+
const workerData = fullData[this.x];
|
|
1221
|
+
window.${modalJsNamespace}.open(workerData);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
},
|
|
1227
|
+
tooltip: {
|
|
1228
|
+
shared: true,
|
|
1229
|
+
headerFormat: '<b>{point.key}</b> (Click for details)<br/>',
|
|
1230
|
+
pointFormat: '<span style="color:{series.color}">●</span> {series.name}: <b>{point.y}</b><br/>',
|
|
1231
|
+
footerFormat: 'Total: <b>{point.total}</b>'
|
|
1232
|
+
},
|
|
1233
|
+
series: ${seriesString},
|
|
1234
|
+
credits: { enabled: false }
|
|
1235
|
+
};
|
|
1236
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
console.error("Error rendering chart ${chartId}:", e);
|
|
1239
|
+
chartContainer.innerHTML = '<div class="no-data">Error rendering worker distribution chart.</div>';
|
|
1240
|
+
}
|
|
1241
|
+
} else {
|
|
1242
|
+
chartContainer.innerHTML = '<div class="no-data">Charting library not available for worker distribution.</div>';
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
</script>
|
|
1246
|
+
`;
|
|
1247
|
+
}
|
|
1248
|
+
const infoTooltip = `
|
|
1249
|
+
<span class="info-tooltip" style="display: inline-block; margin-left: 8px;">
|
|
1250
|
+
<span class="info-icon"
|
|
1251
|
+
style="cursor: pointer; font-size: 1.25rem;"
|
|
1252
|
+
onclick="window.workerInfoPrompt()">ℹ️</span>
|
|
1253
|
+
</span>
|
|
1254
|
+
<script>
|
|
1255
|
+
window.workerInfoPrompt = function() {
|
|
1256
|
+
const message = 'Why is worker -1 special?\\n\\n' +
|
|
1257
|
+
'Playwright assigns skipped tests to worker -1 because:\\n' +
|
|
1258
|
+
'1. They don\\'t require browser execution\\n' +
|
|
1259
|
+
'2. This keeps real workers focused on actual tests\\n' +
|
|
1260
|
+
'3. Maintains clean reporting\\n\\n' +
|
|
1261
|
+
'This is an intentional optimization by Playwright.';
|
|
1262
|
+
alert(message);
|
|
1263
|
+
}
|
|
1264
|
+
</script>
|
|
1265
|
+
`;
|
|
1266
|
+
function generateTestHistoryContent(trendData) {
|
|
1267
|
+
if (
|
|
1268
|
+
!trendData ||
|
|
1269
|
+
!trendData.testRuns ||
|
|
1270
|
+
Object.keys(trendData.testRuns).length === 0
|
|
1271
|
+
) {
|
|
1272
|
+
return '<div class="no-data">No historical test data available.</div>';
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const allTestNamesAndPaths = new Map();
|
|
1276
|
+
Object.values(trendData.testRuns).forEach((run) => {
|
|
1277
|
+
if (Array.isArray(run)) {
|
|
1278
|
+
run.forEach((test) => {
|
|
1279
|
+
if (test && test.testName && !allTestNamesAndPaths.has(test.testName)) {
|
|
1280
|
+
const parts = test.testName.split(" > ");
|
|
1281
|
+
const title = parts[parts.length - 1];
|
|
1282
|
+
allTestNamesAndPaths.set(test.testName, title);
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
if (allTestNamesAndPaths.size === 0) {
|
|
1289
|
+
return '<div class="no-data">No historical test data found after processing.</div>';
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const testHistory = Array.from(allTestNamesAndPaths.entries())
|
|
1293
|
+
.map(([fullTestName, testTitle]) => {
|
|
1294
|
+
const history = [];
|
|
1295
|
+
(trendData.overall || []).forEach((overallRun, index) => {
|
|
1296
|
+
const runKey = overallRun.runId
|
|
1297
|
+
? `test run ${overallRun.runId}`
|
|
1298
|
+
: `test run ${index + 1}`;
|
|
1299
|
+
const testRunForThisOverallRun = trendData.testRuns[runKey]?.find(
|
|
1300
|
+
(t) => t && t.testName === fullTestName
|
|
1301
|
+
);
|
|
1302
|
+
if (testRunForThisOverallRun) {
|
|
1303
|
+
history.push({
|
|
1304
|
+
runId: overallRun.runId || index + 1,
|
|
1305
|
+
status: testRunForThisOverallRun.status || "unknown",
|
|
1306
|
+
duration: testRunForThisOverallRun.duration || 0,
|
|
1307
|
+
timestamp:
|
|
1308
|
+
testRunForThisOverallRun.timestamp ||
|
|
1309
|
+
overallRun.timestamp ||
|
|
1310
|
+
new Date(),
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
return { fullTestName, testTitle, history };
|
|
1315
|
+
})
|
|
1316
|
+
.filter((item) => item.history.length > 0);
|
|
1317
|
+
|
|
1318
|
+
return `
|
|
1319
|
+
<div class="test-history-container">
|
|
1320
|
+
<div class="filters" style="border-color: black; border-style: groove;">
|
|
1321
|
+
<input type="text" id="history-filter-name" placeholder="Search by test title..." style="border-color: black; border-style: outset;">
|
|
1322
|
+
<select id="history-filter-status">
|
|
1323
|
+
<option value="">All Statuses</option>
|
|
1324
|
+
<option value="passed">Passed</option>
|
|
1325
|
+
<option value="failed">Failed</option>
|
|
1326
|
+
<option value="skipped">Skipped</option>
|
|
1327
|
+
</select>
|
|
1328
|
+
<button id="clear-history-filters" class="clear-filters-btn">Clear Filters</button>
|
|
1329
|
+
</div>
|
|
1330
|
+
|
|
1331
|
+
<div class="test-history-grid">
|
|
1332
|
+
${testHistory
|
|
1333
|
+
.map((test) => {
|
|
1334
|
+
const latestRun =
|
|
1335
|
+
test.history.length > 0
|
|
1336
|
+
? test.history[test.history.length - 1]
|
|
1337
|
+
: { status: "unknown" };
|
|
1338
|
+
return `
|
|
1339
|
+
<div class="test-history-card" data-test-name="${sanitizeHTML(
|
|
1340
|
+
test.testTitle.toLowerCase()
|
|
1341
|
+
)}" data-latest-status="${latestRun.status}">
|
|
1342
|
+
<div class="test-history-header">
|
|
1343
|
+
<p title="${sanitizeHTML(test.testTitle)}">${capitalize(
|
|
1344
|
+
sanitizeHTML(test.testTitle)
|
|
1345
|
+
)}</p>
|
|
1346
|
+
<span class="status-badge ${getStatusClass(latestRun.status)}">
|
|
1347
|
+
${String(latestRun.status).toUpperCase()}
|
|
1348
|
+
</span>
|
|
1349
|
+
</div>
|
|
1350
|
+
<div class="test-history-trend">
|
|
1351
|
+
${generateTestHistoryChart(test.history)}
|
|
1352
|
+
</div>
|
|
1353
|
+
<details class="test-history-details-collapsible">
|
|
1354
|
+
<summary>Show Run Details (${test.history.length})</summary>
|
|
1355
|
+
<div class="test-history-details">
|
|
1356
|
+
<table>
|
|
1357
|
+
<thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Date</th></tr></thead>
|
|
1358
|
+
<tbody>
|
|
1359
|
+
${test.history
|
|
1360
|
+
.slice()
|
|
1361
|
+
.reverse()
|
|
1362
|
+
.map(
|
|
1363
|
+
(run) => `
|
|
1364
|
+
<tr>
|
|
1365
|
+
<td>${run.runId}</td>
|
|
1366
|
+
<td><span class="status-badge-small ${getStatusClass(
|
|
1367
|
+
run.status
|
|
1368
|
+
)}">${String(run.status).toUpperCase()}</span></td>
|
|
1369
|
+
<td>${formatDuration(run.duration)}</td>
|
|
1370
|
+
<td>${formatDate(run.timestamp)}</td>
|
|
1371
|
+
</tr>`
|
|
1372
|
+
)
|
|
1373
|
+
.join("")}
|
|
1374
|
+
</tbody>
|
|
1375
|
+
</table>
|
|
1376
|
+
</div>
|
|
1377
|
+
</details>
|
|
1378
|
+
</div>`;
|
|
1379
|
+
})
|
|
1380
|
+
.join("")}
|
|
1381
|
+
</div>
|
|
1382
|
+
</div>
|
|
1383
|
+
`;
|
|
1384
|
+
}
|
|
1385
|
+
function getStatusClass(status) {
|
|
1386
|
+
switch (String(status).toLowerCase()) {
|
|
1387
|
+
case "passed":
|
|
1388
|
+
return "status-passed";
|
|
1389
|
+
case "failed":
|
|
1390
|
+
return "status-failed";
|
|
1391
|
+
case "skipped":
|
|
1392
|
+
return "status-skipped";
|
|
1393
|
+
default:
|
|
1394
|
+
return "status-unknown";
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function getStatusIcon(status) {
|
|
1398
|
+
switch (String(status).toLowerCase()) {
|
|
1399
|
+
case "passed":
|
|
1400
|
+
return "✅";
|
|
1401
|
+
case "failed":
|
|
1402
|
+
return "❌";
|
|
1403
|
+
case "skipped":
|
|
1404
|
+
return "⏭️";
|
|
1405
|
+
default:
|
|
1406
|
+
return "❓";
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
function getSuitesData(results) {
|
|
1410
|
+
const suitesMap = new Map();
|
|
1411
|
+
if (!results || results.length === 0) return [];
|
|
1412
|
+
|
|
1413
|
+
results.forEach((test) => {
|
|
1414
|
+
const browser = test.browser || "unknown";
|
|
1415
|
+
const suiteParts = test.name.split(" > ");
|
|
1416
|
+
let suiteNameCandidate = "Default Suite";
|
|
1417
|
+
if (suiteParts.length > 2) {
|
|
1418
|
+
suiteNameCandidate = suiteParts[1];
|
|
1419
|
+
} else if (suiteParts.length > 1) {
|
|
1420
|
+
suiteNameCandidate = suiteParts[0]
|
|
1421
|
+
.split(path.sep)
|
|
1422
|
+
.pop()
|
|
1423
|
+
.replace(/\.(spec|test)\.(ts|js|mjs|cjs)$/, "");
|
|
1424
|
+
} else {
|
|
1425
|
+
suiteNameCandidate = test.name
|
|
1426
|
+
.split(path.sep)
|
|
1427
|
+
.pop()
|
|
1428
|
+
.replace(/\.(spec|test)\.(ts|js|mjs|cjs)$/, "");
|
|
1429
|
+
}
|
|
1430
|
+
const suiteName = suiteNameCandidate;
|
|
1431
|
+
const key = `${suiteName}|${browser}`;
|
|
1432
|
+
|
|
1433
|
+
if (!suitesMap.has(key)) {
|
|
1434
|
+
suitesMap.set(key, {
|
|
1435
|
+
id: test.id || key,
|
|
1436
|
+
name: suiteName,
|
|
1437
|
+
browser: browser,
|
|
1438
|
+
passed: 0,
|
|
1439
|
+
failed: 0,
|
|
1440
|
+
skipped: 0,
|
|
1441
|
+
count: 0,
|
|
1442
|
+
statusOverall: "passed",
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
const suite = suitesMap.get(key);
|
|
1446
|
+
suite.count++;
|
|
1447
|
+
const currentStatus = String(test.status).toLowerCase();
|
|
1448
|
+
if (currentStatus && suite[currentStatus] !== undefined) {
|
|
1449
|
+
suite[currentStatus]++;
|
|
1450
|
+
}
|
|
1451
|
+
if (currentStatus === "failed") suite.statusOverall = "failed";
|
|
1452
|
+
else if (currentStatus === "skipped" && suite.statusOverall !== "failed")
|
|
1453
|
+
suite.statusOverall = "skipped";
|
|
1454
|
+
});
|
|
1455
|
+
return Array.from(suitesMap.values());
|
|
1456
|
+
}
|
|
1457
|
+
function generateSuitesWidget(suitesData) {
|
|
1458
|
+
if (!suitesData || suitesData.length === 0) {
|
|
1459
|
+
return `<div class="suites-widget"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
|
|
1460
|
+
}
|
|
1461
|
+
return `
|
|
1462
|
+
<div class="suites-widget">
|
|
1463
|
+
<div class="suites-header">
|
|
1464
|
+
<h2>Test Suites</h2>
|
|
1465
|
+
<span class="summary-badge">${suitesData.length
|
|
1466
|
+
} suites • ${suitesData.reduce(
|
|
1467
|
+
(sum, suite) => sum + suite.count,
|
|
1468
|
+
0
|
|
1469
|
+
)} tests</span>
|
|
1470
|
+
</div>
|
|
1471
|
+
<div class="suites-grid">
|
|
1472
|
+
${suitesData
|
|
1473
|
+
.map(
|
|
1474
|
+
(suite) => `
|
|
1475
|
+
<div class="suite-card status-${suite.statusOverall}">
|
|
1476
|
+
<div class="suite-card-header">
|
|
1477
|
+
<h3 class="suite-name" title="${sanitizeHTML(
|
|
1478
|
+
suite.name
|
|
1479
|
+
)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
|
|
1480
|
+
</div>
|
|
1481
|
+
<div>🖥️ <span class="browser-tag">${sanitizeHTML(
|
|
1482
|
+
suite.browser
|
|
1483
|
+
)}</span></div>
|
|
1484
|
+
<div class="suite-card-body">
|
|
1485
|
+
<span class="test-count">${suite.count} test${suite.count !== 1 ? "s" : ""
|
|
1486
|
+
}</span>
|
|
1487
|
+
<div class="suite-stats">
|
|
1488
|
+
${suite.passed > 0
|
|
1489
|
+
? `<span class="stat-passed" title="Passed"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/></svg> ${suite.passed}</span>`
|
|
1490
|
+
: ""
|
|
1491
|
+
}
|
|
1492
|
+
${suite.failed > 0
|
|
1493
|
+
? `<span class="stat-failed" title="Failed"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/></svg> ${suite.failed}</span>`
|
|
1494
|
+
: ""
|
|
1495
|
+
}
|
|
1496
|
+
${suite.skipped > 0
|
|
1497
|
+
? `<span class="stat-skipped" title="Skipped"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-exclamation-triangle-fill" viewBox="0 0 16 16"><path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/></svg> ${suite.skipped}</span>`
|
|
1498
|
+
: ""
|
|
1499
|
+
}
|
|
1500
|
+
</div>
|
|
1501
|
+
</div>
|
|
1502
|
+
</div>`
|
|
1503
|
+
)
|
|
1504
|
+
.join("")}
|
|
1505
|
+
</div>
|
|
1506
|
+
</div>`;
|
|
1507
|
+
}
|
|
1508
|
+
function getAttachmentIcon(contentType) {
|
|
1509
|
+
if (!contentType) return "📎"; // Handle undefined/null
|
|
1510
|
+
|
|
1511
|
+
const normalizedType = contentType.toLowerCase();
|
|
1512
|
+
|
|
1513
|
+
if (normalizedType.includes("pdf")) return "📄";
|
|
1514
|
+
if (normalizedType.includes("json")) return "{ }";
|
|
1515
|
+
if (/html/.test(normalizedType)) return "🌐"; // Fixed: regex for any HTML type
|
|
1516
|
+
if (normalizedType.includes("xml")) return "<>";
|
|
1517
|
+
if (normalizedType.includes("csv")) return "📊";
|
|
1518
|
+
if (normalizedType.startsWith("text/")) return "📝";
|
|
1519
|
+
return "📎";
|
|
1520
|
+
}
|
|
1521
|
+
function generateAIFailureAnalyzerTab(results) {
|
|
1522
|
+
const failedTests = (results || []).filter(test => test.status === 'failed');
|
|
1523
|
+
|
|
1524
|
+
if (failedTests.length === 0) {
|
|
1525
|
+
return `
|
|
1526
|
+
<h2 class="tab-main-title">AI Failure Analysis</h2>
|
|
1527
|
+
<div class="no-data">Congratulations! No failed tests in this run.</div>
|
|
1528
|
+
`;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// btoa is not available in Node.js environment, so we define a simple polyfill for it.
|
|
1532
|
+
const btoa = (str) => Buffer.from(str).toString('base64');
|
|
1533
|
+
|
|
1534
|
+
return `
|
|
1535
|
+
<h2 class="tab-main-title">AI Failure Analysis</h2>
|
|
1536
|
+
<div class="ai-analyzer-stats">
|
|
1537
|
+
<div class="stat-item">
|
|
1538
|
+
<span class="stat-number">${failedTests.length}</span>
|
|
1539
|
+
<span class="stat-label">Failed Tests</span>
|
|
1540
|
+
</div>
|
|
1541
|
+
<div class="stat-item">
|
|
1542
|
+
<span class="stat-number">${new Set(failedTests.map(t => t.browser)).size}</span>
|
|
1543
|
+
<span class="stat-label">Browsers</span>
|
|
1544
|
+
</div>
|
|
1545
|
+
<div class="stat-item">
|
|
1546
|
+
<span class="stat-number">${(Math.round(failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) / 1000))}s</span>
|
|
1547
|
+
<span class="stat-label">Total Duration</span>
|
|
1548
|
+
</div>
|
|
1549
|
+
</div>
|
|
1550
|
+
<p class="ai-analyzer-description">
|
|
1551
|
+
Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for specific failed test.
|
|
1552
|
+
</p>
|
|
1553
|
+
|
|
1554
|
+
<div class="compact-failure-list">
|
|
1555
|
+
${failedTests.map(test => {
|
|
1556
|
+
const testTitle = test.name.split(" > ").pop() || "Unnamed Test";
|
|
1557
|
+
const testJson = btoa(JSON.stringify(test)); // Base64 encode the test object
|
|
1558
|
+
const truncatedError = (test.errorMessage || "No error message").slice(0, 150) +
|
|
1559
|
+
(test.errorMessage && test.errorMessage.length > 150 ? "..." : "");
|
|
1560
|
+
|
|
1561
|
+
return `
|
|
1562
|
+
<div class="compact-failure-item">
|
|
1563
|
+
<div class="failure-header">
|
|
1564
|
+
<div class="failure-main-info">
|
|
1565
|
+
<h3 class="failure-title" title="${sanitizeHTML(test.name)}">${sanitizeHTML(testTitle)}</h3>
|
|
1566
|
+
<div class="failure-meta">
|
|
1567
|
+
<span class="browser-indicator">${sanitizeHTML(test.browser || 'unknown')}</span>
|
|
1568
|
+
<span class="duration-indicator">${formatDuration(test.duration)}</span>
|
|
1569
|
+
</div>
|
|
1570
|
+
</div>
|
|
1571
|
+
<button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
|
|
1572
|
+
<span class="ai-text">AI Fix</span>
|
|
1573
|
+
</button>
|
|
1574
|
+
</div>
|
|
1575
|
+
<div class="failure-error-preview">
|
|
1576
|
+
<div class="error-snippet">${formatPlaywrightError(truncatedError)}</div>
|
|
1577
|
+
<button class="expand-error-btn" onclick="toggleErrorDetails(this)">
|
|
1578
|
+
<span class="expand-text">Show Full Error</span>
|
|
1579
|
+
<span class="expand-icon">▼</span>
|
|
1580
|
+
</button>
|
|
1581
|
+
</div>
|
|
1582
|
+
<div class="full-error-details" style="display: none;">
|
|
1583
|
+
<div class="full-error-content">
|
|
1584
|
+
${formatPlaywrightError(test.errorMessage || "No detailed error message available")}
|
|
1585
|
+
</div>
|
|
1586
|
+
</div>
|
|
1587
|
+
</div>
|
|
1588
|
+
`
|
|
1589
|
+
}).join('')}
|
|
1590
|
+
</div>
|
|
1591
|
+
|
|
1592
|
+
<!-- AI Fix Modal -->
|
|
1593
|
+
<div id="ai-fix-modal" class="ai-modal-overlay" onclick="closeAiModal()">
|
|
1594
|
+
<div class="ai-modal-content" onclick="event.stopPropagation()">
|
|
1595
|
+
<div class="ai-modal-header">
|
|
1596
|
+
<h3 id="ai-fix-modal-title">AI Analysis</h3>
|
|
1597
|
+
<span class="ai-modal-close" onclick="closeAiModal()">×</span>
|
|
1598
|
+
</div>
|
|
1599
|
+
<div class="ai-modal-body" id="ai-fix-modal-content">
|
|
1600
|
+
<!-- Content will be injected by JavaScript -->
|
|
1601
|
+
</div>
|
|
1602
|
+
</div>
|
|
1603
|
+
</div>
|
|
1604
|
+
`;
|
|
1605
|
+
}
|
|
1606
|
+
function generateHTML(reportData, trendData = null) {
|
|
1607
|
+
const { run, results } = reportData;
|
|
1608
|
+
const suitesData = getSuitesData(reportData.results || []);
|
|
1609
|
+
const runSummary = run || {
|
|
1610
|
+
totalTests: 0,
|
|
1611
|
+
passed: 0,
|
|
1612
|
+
failed: 0,
|
|
1613
|
+
skipped: 0,
|
|
1614
|
+
duration: 0,
|
|
1615
|
+
timestamp: new Date().toISOString(),
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
const fixPath = (p) => {
|
|
1619
|
+
if (!p) return "";
|
|
1620
|
+
// This regex handles both forward slashes and backslashes
|
|
1621
|
+
return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`), '');
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
const totalTestsOr1 = runSummary.totalTests || 1;
|
|
1625
|
+
const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
|
|
1626
|
+
const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
|
|
1627
|
+
const skipPercentage = Math.round(
|
|
1628
|
+
((runSummary.skipped || 0) / totalTestsOr1) * 100
|
|
1629
|
+
);
|
|
1630
|
+
const avgTestDuration =
|
|
1631
|
+
runSummary.totalTests > 0
|
|
1632
|
+
? formatDuration(runSummary.duration / runSummary.totalTests)
|
|
1633
|
+
: "0.0s";
|
|
1634
|
+
function generateTestCasesHTML() {
|
|
1635
|
+
if (!results || results.length === 0)
|
|
1636
|
+
return '<div class="no-tests">No test results found in this run.</div>';
|
|
1637
|
+
return results
|
|
1638
|
+
.map((test, index) => {
|
|
1639
|
+
const browser = test.browser || "unknown";
|
|
1640
|
+
const testFileParts = test.name.split(" > ");
|
|
1641
|
+
const testTitle =
|
|
1642
|
+
testFileParts[testFileParts.length - 1] || "Unnamed Test";
|
|
1643
|
+
const generateStepsHTML = (steps, depth = 0) => {
|
|
1644
|
+
if (!steps || steps.length === 0)
|
|
1645
|
+
return "<div class='no-steps'>No steps recorded for this test.</div>";
|
|
1646
|
+
return steps
|
|
1647
|
+
.map((step) => {
|
|
1648
|
+
const hasNestedSteps = step.steps && step.steps.length > 0;
|
|
1649
|
+
const isHook = step.hookType;
|
|
1650
|
+
const stepClass = isHook
|
|
1651
|
+
? `step-hook step-hook-${step.hookType}`
|
|
1652
|
+
: "";
|
|
1653
|
+
const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
|
|
1654
|
+
return `
|
|
1655
|
+
<div class="step-item" style="--depth: ${depth};">
|
|
1656
|
+
<div class="step-header ${stepClass}" role="button" aria-expanded="false">
|
|
1657
|
+
<span class="step-icon">${getStatusIcon(step.status)}</span>
|
|
1658
|
+
<span class="step-title">${sanitizeHTML(
|
|
1659
|
+
step.title
|
|
1660
|
+
)}${hookIndicator}</span>
|
|
1661
|
+
<span class="step-duration">${formatDuration(
|
|
1662
|
+
step.duration
|
|
1663
|
+
)}</span>
|
|
1664
|
+
</div>
|
|
1665
|
+
<div class="step-details" style="display: none;">
|
|
1666
|
+
${step.codeLocation
|
|
1667
|
+
? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
|
|
1668
|
+
step.codeLocation
|
|
1669
|
+
)}</div>`
|
|
1670
|
+
: ""
|
|
1671
|
+
}
|
|
1672
|
+
${step.errorMessage
|
|
1673
|
+
? `<div class="test-error-summary">
|
|
1674
|
+
${step.stackTrace
|
|
1675
|
+
? `<div class="stack-trace">${formatPlaywrightError(
|
|
1676
|
+
step.stackTrace
|
|
1677
|
+
)}</div>`
|
|
1678
|
+
: ""
|
|
1679
|
+
}
|
|
1680
|
+
<button
|
|
1681
|
+
class="copy-error-btn"
|
|
1682
|
+
onclick="copyErrorToClipboard(this)"
|
|
1683
|
+
style="
|
|
1684
|
+
margin-top: 8px;
|
|
1685
|
+
padding: 4px 8px;
|
|
1686
|
+
background: #f0f0f0;
|
|
1687
|
+
border: 2px solid #ccc;
|
|
1688
|
+
border-radius: 4px;
|
|
1689
|
+
cursor: pointer;
|
|
1690
|
+
font-size: 12px;
|
|
1691
|
+
border-color: #8B0000;
|
|
1692
|
+
color: #8B0000;
|
|
1693
|
+
"
|
|
1694
|
+
onmouseover="this.style.background='#e0e0e0'"
|
|
1695
|
+
onmouseout="this.style.background='#f0f0f0'"
|
|
1696
|
+
>
|
|
1697
|
+
Copy Error Prompt
|
|
1698
|
+
</button>
|
|
1699
|
+
</div>`
|
|
1700
|
+
: ""
|
|
1701
|
+
}
|
|
1702
|
+
${hasNestedSteps
|
|
1703
|
+
? `<div class="nested-steps">${generateStepsHTML(
|
|
1704
|
+
step.steps,
|
|
1705
|
+
depth + 1
|
|
1706
|
+
)}</div>`
|
|
1707
|
+
: ""
|
|
1708
|
+
}
|
|
1709
|
+
</div>
|
|
1710
|
+
</div>`;
|
|
1711
|
+
})
|
|
1712
|
+
.join("");
|
|
1713
|
+
};
|
|
1714
|
+
|
|
1715
|
+
return `
|
|
1716
|
+
<div class="test-case" data-status="${test.status
|
|
1717
|
+
}" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
|
|
1718
|
+
.join(",")
|
|
1719
|
+
.toLowerCase()}">
|
|
1720
|
+
<div class="test-case-header" role="button" aria-expanded="false">
|
|
1721
|
+
<div class="test-case-summary">
|
|
1722
|
+
<span class="status-badge ${getStatusClass(test.status)}">${String(
|
|
1723
|
+
test.status
|
|
1724
|
+
).toUpperCase()}</span>
|
|
1725
|
+
<span class="test-case-title" title="${sanitizeHTML(
|
|
1726
|
+
test.name
|
|
1727
|
+
)}">${sanitizeHTML(testTitle)}</span>
|
|
1728
|
+
<span class="test-case-browser">(${sanitizeHTML(browser)})</span>
|
|
1729
|
+
</div>
|
|
1730
|
+
<div class="test-case-meta">
|
|
1731
|
+
${test.tags && test.tags.length > 0
|
|
1732
|
+
? test.tags
|
|
1733
|
+
.map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
|
|
1734
|
+
.join(" ")
|
|
1735
|
+
: ""
|
|
1736
|
+
}
|
|
1737
|
+
<span class="test-duration">${formatDuration(test.duration)}</span>
|
|
1738
|
+
</div>
|
|
1739
|
+
</div>
|
|
1740
|
+
<div class="test-case-content" style="display: none;">
|
|
1741
|
+
<p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
|
|
1742
|
+
<p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
|
|
1743
|
+
test.workerId
|
|
1744
|
+
)} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
|
|
1745
|
+
test.totalWorkers
|
|
1746
|
+
)}]</p>
|
|
1747
|
+
${test.errorMessage
|
|
1748
|
+
? `<div class="test-error-summary">${formatPlaywrightError(
|
|
1749
|
+
test.errorMessage
|
|
1750
|
+
)}
|
|
1751
|
+
<button
|
|
1752
|
+
class="copy-error-btn"
|
|
1753
|
+
onclick="copyErrorToClipboard(this)"
|
|
1754
|
+
style="
|
|
1755
|
+
margin-top: 8px;
|
|
1756
|
+
padding: 4px 8px;
|
|
1757
|
+
background: #f0f0f0;
|
|
1758
|
+
border: 2px solid #ccc;
|
|
1759
|
+
border-radius: 4px;
|
|
1760
|
+
cursor: pointer;
|
|
1761
|
+
font-size: 12px;
|
|
1762
|
+
border-color: #8B0000;
|
|
1763
|
+
color: #8B0000;
|
|
1764
|
+
"
|
|
1765
|
+
onmouseover="this.style.background='#e0e0e0'"
|
|
1766
|
+
onmouseout="this.style.background='#f0f0f0'"
|
|
1767
|
+
>
|
|
1768
|
+
Copy Error Prompt
|
|
1769
|
+
</button>
|
|
1770
|
+
</div>`
|
|
1771
|
+
: ""
|
|
1772
|
+
}
|
|
1773
|
+
${test.snippet
|
|
1774
|
+
? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
|
|
1775
|
+
test.snippet
|
|
1776
|
+
)}</code></pre></div>`
|
|
1777
|
+
: ""
|
|
1778
|
+
}
|
|
1779
|
+
<h4>Steps</h4>
|
|
1780
|
+
<div class="steps-list">${generateStepsHTML(test.steps)}</div>
|
|
1781
|
+
${(() => {
|
|
1782
|
+
if (!test.stdout || test.stdout.length === 0) return "";
|
|
1783
|
+
// Create a unique ID for the <pre> element to target it for copying
|
|
1784
|
+
const logId = `stdout-log-${test.id || index}`;
|
|
1785
|
+
return `<div class="console-output-section">
|
|
1786
|
+
<h4>Console Output (stdout)
|
|
1787
|
+
<button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy Console</button>
|
|
1788
|
+
</h4>
|
|
1789
|
+
<div class="log-wrapper">
|
|
1790
|
+
<pre id="${logId}" class="console-log stdout-log" style="background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
|
|
1791
|
+
test.stdout.map((line) => sanitizeHTML(line)).join("\n")
|
|
1792
|
+
)}</pre>
|
|
1793
|
+
</div>
|
|
1794
|
+
</div>`;
|
|
1795
|
+
})()}
|
|
1796
|
+
${test.stderr && test.stderr.length > 0
|
|
1797
|
+
? `<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;">${formatPlaywrightError(
|
|
1798
|
+
test.stderr.map((line) => sanitizeHTML(line)).join("\n")
|
|
1799
|
+
)}</pre></div>`
|
|
1800
|
+
: ""
|
|
1801
|
+
}
|
|
1802
|
+
${test.screenshots && test.screenshots.length > 0
|
|
1803
|
+
? `
|
|
1804
|
+
<div class="attachments-section">
|
|
1805
|
+
<h4>Screenshots</h4>
|
|
1806
|
+
<div class="attachments-grid">
|
|
1807
|
+
${test.screenshots
|
|
1808
|
+
.map(
|
|
1809
|
+
(screenshot, index) => `
|
|
1810
|
+
<div class="attachment-item">
|
|
1811
|
+
<img src="${fixPath(screenshot)}" alt="Screenshot ${index + 1}">
|
|
1812
|
+
<div class="attachment-info">
|
|
1813
|
+
<div class="trace-actions">
|
|
1814
|
+
<a href="${fixPath(screenshot)}" target="_blank" class="view-full">View Full Image</a>
|
|
1815
|
+
<a href="${fixPath(screenshot)}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
|
|
1816
|
+
</div>
|
|
1817
|
+
</div>
|
|
1818
|
+
</div>
|
|
1819
|
+
`
|
|
1820
|
+
)
|
|
1821
|
+
.join("")}
|
|
1822
|
+
</div>
|
|
1823
|
+
</div>
|
|
1824
|
+
`
|
|
1825
|
+
: ""
|
|
1826
|
+
}
|
|
1827
|
+
${test.videoPath && test.videoPath.length > 0
|
|
1828
|
+
? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
|
|
1829
|
+
.map((videoUrl, index) => {
|
|
1830
|
+
const fixedVideoUrl = fixPath(videoUrl);
|
|
1831
|
+
const fileExtension = String(fixedVideoUrl)
|
|
1832
|
+
.split(".")
|
|
1833
|
+
.pop()
|
|
1834
|
+
.toLowerCase();
|
|
1835
|
+
const mimeType =
|
|
1836
|
+
{
|
|
1837
|
+
mp4: "video/mp4",
|
|
1838
|
+
webm: "video/webm",
|
|
1839
|
+
ogg: "video/ogg",
|
|
1840
|
+
mov: "video/quicktime",
|
|
1841
|
+
avi: "video/x-msvideo",
|
|
1842
|
+
}[fileExtension] || "video/mp4";
|
|
1843
|
+
return `<div class="attachment-item video-item">
|
|
1844
|
+
<video controls width="100%" height="auto" title="Video ${index + 1
|
|
1845
|
+
}">
|
|
1846
|
+
<source src="${sanitizeHTML(
|
|
1847
|
+
fixedVideoUrl
|
|
1848
|
+
)}" type="${mimeType}">
|
|
1849
|
+
Your browser does not support the video tag.
|
|
1850
|
+
</video>
|
|
1851
|
+
<div class="attachment-info">
|
|
1852
|
+
<div class="trace-actions">
|
|
1853
|
+
<a href="${sanitizeHTML(
|
|
1854
|
+
fixedVideoUrl
|
|
1855
|
+
)}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
|
|
1856
|
+
</div>
|
|
1857
|
+
</div>
|
|
1858
|
+
</div>`;
|
|
1859
|
+
})
|
|
1860
|
+
.join("")}</div></div>`
|
|
1861
|
+
: ""
|
|
1862
|
+
}
|
|
1863
|
+
${test.tracePath
|
|
1864
|
+
? `
|
|
1865
|
+
<div class="attachments-section">
|
|
1866
|
+
<h4>Trace Files</h4>
|
|
1867
|
+
<div class="attachments-grid">
|
|
1868
|
+
<div class="attachment-item trace-item">
|
|
1869
|
+
<div class="trace-preview">
|
|
1870
|
+
<span class="trace-icon">📄</span>
|
|
1871
|
+
<span class="trace-name">${sanitizeHTML(
|
|
1872
|
+
path.basename(test.tracePath)
|
|
1873
|
+
)}</span>
|
|
1874
|
+
</div>
|
|
1875
|
+
<div class="attachment-info">
|
|
1876
|
+
<div class="trace-actions">
|
|
1877
|
+
<a href="${sanitizeHTML(
|
|
1878
|
+
fixPath(test.tracePath)
|
|
1879
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
1880
|
+
path.basename(test.tracePath)
|
|
1881
|
+
)}" class="download-trace">Download Trace</a>
|
|
1882
|
+
</div>
|
|
1883
|
+
</div>
|
|
1884
|
+
</div>
|
|
1885
|
+
</div>
|
|
1886
|
+
</div>
|
|
1887
|
+
`
|
|
1888
|
+
: ""
|
|
1889
|
+
}
|
|
1890
|
+
${test.attachments && test.attachments.length > 0
|
|
1891
|
+
? `
|
|
1892
|
+
<div class="attachments-section">
|
|
1893
|
+
<h4>Other Attachments</h4>
|
|
1894
|
+
<div class="attachments-grid">
|
|
1895
|
+
${test.attachments
|
|
1896
|
+
.map(
|
|
1897
|
+
(attachment) => `
|
|
1898
|
+
<div class="attachment-item generic-attachment">
|
|
1899
|
+
<div class="attachment-icon">${getAttachmentIcon(
|
|
1900
|
+
attachment.contentType
|
|
1901
|
+
)}</div>
|
|
1902
|
+
<div class="attachment-caption">
|
|
1903
|
+
<span class="attachment-name" title="${sanitizeHTML(
|
|
1904
|
+
attachment.name
|
|
1905
|
+
)}">${sanitizeHTML(attachment.name)}</span>
|
|
1906
|
+
<span class="attachment-type">${sanitizeHTML(
|
|
1907
|
+
attachment.contentType
|
|
1908
|
+
)}</span>
|
|
1909
|
+
</div>
|
|
1910
|
+
<div class="attachment-info">
|
|
1911
|
+
<div class="trace-actions">
|
|
1912
|
+
<a href="${sanitizeHTML(
|
|
1913
|
+
fixPath(attachment.path)
|
|
1914
|
+
)}" target="_blank" class="view-full">View</a>
|
|
1915
|
+
<a href="${sanitizeHTML(
|
|
1916
|
+
fixPath(attachment.path)
|
|
1917
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
1918
|
+
attachment.name
|
|
1919
|
+
)}" class="download-trace">Download</a>
|
|
1920
|
+
</div>
|
|
1921
|
+
</div>
|
|
1922
|
+
</div>
|
|
1923
|
+
`
|
|
1924
|
+
)
|
|
1925
|
+
.join("")}
|
|
1926
|
+
</div>
|
|
1927
|
+
</div>
|
|
1928
|
+
`
|
|
1929
|
+
: ""
|
|
1930
|
+
}
|
|
1931
|
+
${test.codeSnippet
|
|
1932
|
+
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
|
|
1933
|
+
sanitizeHTML(test.codeSnippet)
|
|
1934
|
+
)}</code></pre></div>`
|
|
1935
|
+
: ""
|
|
1936
|
+
}
|
|
1937
|
+
</div>
|
|
1938
|
+
</div>`;
|
|
1939
|
+
})
|
|
1940
|
+
.join("");
|
|
1941
|
+
}
|
|
1942
|
+
return `
|
|
1943
|
+
<!DOCTYPE html>
|
|
1944
|
+
<html lang="en">
|
|
1945
|
+
<head>
|
|
1946
|
+
<meta charset="UTF-8">
|
|
1947
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1948
|
+
<link rel="icon" type="image/png" href="https://i.postimg.cc/v817w4sg/logo.png">
|
|
1949
|
+
<link rel="apple-touch-icon" href="https://i.postimg.cc/v817w4sg/logo.png">
|
|
1950
|
+
<script src="https://code.highcharts.com/highcharts.js" defer></script>
|
|
1951
|
+
<title>Playwright Pulse Report</title>
|
|
1952
|
+
<style>
|
|
1953
|
+
:root {
|
|
1954
|
+
--primary-color: #3f51b5; --secondary-color: #ff4081; --accent-color: #673ab7; --accent-color-alt: #FF9800;
|
|
1955
|
+
--success-color: #4CAF50; --danger-color: #F44336; --warning-color: #FFC107; --info-color: #2196F3;
|
|
1956
|
+
--light-gray-color: #f5f5f5; --medium-gray-color: #e0e0e0; --dark-gray-color: #757575;
|
|
1957
|
+
--text-color: #333; --text-color-secondary: #555; --border-color: #ddd; --background-color: #f8f9fa;
|
|
1958
|
+
--card-background-color: #fff; --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
1959
|
+
--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);
|
|
1960
|
+
}
|
|
1961
|
+
.trend-chart-container, .test-history-trend div[id^="testHistoryChart-"] { min-height: 100px; }
|
|
1962
|
+
.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); }
|
|
1963
|
+
.highcharts-background { fill: transparent; }
|
|
1964
|
+
.highcharts-title, .highcharts-subtitle { font-family: var(--font-family); }
|
|
1965
|
+
.highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
|
|
1966
|
+
.highcharts-axis-title { fill: var(--text-color) !important; }
|
|
1967
|
+
.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; }
|
|
1968
|
+
body { font-family: var(--font-family); margin: 0; background-color: var(--background-color); color: var(--text-color); line-height: 1.65; font-size: 16px; }
|
|
1969
|
+
.container { padding: 30px; border-radius: var(--border-radius); box-shadow: var(--box-shadow); background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec); }
|
|
1970
|
+
.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; }
|
|
1971
|
+
.header-title { display: flex; align-items: center; gap: 15px; }
|
|
1972
|
+
.header h1 { margin: 0; font-size: 1.85em; font-weight: 600; color: var(--primary-color); }
|
|
1973
|
+
#report-logo { height: 40px; width: 55px; }
|
|
1974
|
+
.run-info { font-size: 0.9em; text-align: right; color: var(--text-color-secondary); line-height:1.5;}
|
|
1975
|
+
.run-info strong { color: var(--text-color); }
|
|
1976
|
+
.tabs { display: flex; border-bottom: 2px solid var(--border-color); margin-bottom: 30px; overflow-x: auto; }
|
|
1977
|
+
.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; }
|
|
1978
|
+
.tab-button:hover { color: var(--accent-color); }
|
|
1979
|
+
.tab-button.active { color: var(--primary-color); border-bottom-color: var(--primary-color); }
|
|
1980
|
+
.tab-content { display: none; animation: fadeIn 0.4s ease-out; }
|
|
1981
|
+
.tab-content.active { display: block; }
|
|
1982
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
1983
|
+
.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 22px; margin-bottom: 35px; }
|
|
1984
|
+
.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; }
|
|
1985
|
+
.summary-card:hover { transform: translateY(-5px); box-shadow: var(--box-shadow); }
|
|
1986
|
+
.summary-card h3 { margin: 0 0 10px; font-size: 1.05em; font-weight: 500; color: var(--text-color-secondary); }
|
|
1987
|
+
.summary-card .value { font-size: 2.4em; font-weight: 600; margin-bottom: 8px; }
|
|
1988
|
+
.summary-card .trend-percentage { font-size: 1em; color: var(--dark-gray-color); }
|
|
1989
|
+
.status-passed .value, .stat-passed svg { color: var(--success-color); }
|
|
1990
|
+
.status-failed .value, .stat-failed svg { color: var(--danger-color); }
|
|
1991
|
+
.status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
|
|
1992
|
+
.dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: stretch; }
|
|
1993
|
+
.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; }
|
|
1994
|
+
.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); }
|
|
1995
|
+
.trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { flex-grow: 1; min-height: 250px; }
|
|
1996
|
+
.status-badge-small-tooltip { padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; }
|
|
1997
|
+
.status-badge-small-tooltip.status-passed { background-color: var(--success-color); }
|
|
1998
|
+
.status-badge-small-tooltip.status-failed { background-color: var(--danger-color); }
|
|
1999
|
+
.status-badge-small-tooltip.status-skipped { background-color: var(--warning-color); }
|
|
2000
|
+
.status-badge-small-tooltip.status-unknown { background-color: var(--dark-gray-color); }
|
|
2001
|
+
.suites-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
|
2002
|
+
.summary-badge { background-color: var(--light-gray-color); color: var(--text-color-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
|
|
2003
|
+
.suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
|
|
2004
|
+
.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; }
|
|
2005
|
+
.suite-card:hover { box-shadow: var(--box-shadow); }
|
|
2006
|
+
.suite-card.status-passed { border-left-color: var(--success-color); }
|
|
2007
|
+
.suite-card.status-failed { border-left-color: var(--danger-color); }
|
|
2008
|
+
.suite-card.status-skipped { border-left-color: var(--warning-color); }
|
|
2009
|
+
.suite-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
|
|
2010
|
+
.suite-name { font-weight: 600; font-size: 1.05em; color: var(--text-color); margin-right: 10px; word-break: break-word;}
|
|
2011
|
+
.browser-tag { font-size: 0.8em; background-color: var(--medium-gray-color); color: var(--text-color-secondary); padding: 3px 8px; border-radius: 4px; white-space: nowrap;}
|
|
2012
|
+
.suite-card-body .test-count { font-size: 0.95em; color: var(--text-color-secondary); display: block; margin-bottom: 10px; }
|
|
2013
|
+
.suite-stats { display: flex; gap: 14px; font-size: 0.95em; align-items: center; }
|
|
2014
|
+
.suite-stats span { display: flex; align-items: center; gap: 6px; }
|
|
2015
|
+
.suite-stats svg { vertical-align: middle; font-size: 1.15em; }
|
|
2016
|
+
.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; }
|
|
2017
|
+
.filters input, .filters select, .filters button { padding: 11px 15px; border: 1px solid var(--border-color); border-radius: 6px; font-size: 1em; }
|
|
2018
|
+
.filters input { flex-grow: 1; min-width: 240px;}
|
|
2019
|
+
.filters select {min-width: 180px;}
|
|
2020
|
+
.filters button { background-color: var(--primary-color); color: white; cursor: pointer; transition: background-color 0.2s ease, box-shadow 0.2s ease; border: none; }
|
|
2021
|
+
.filters button:hover { background-color: var(--accent-color); box-shadow: 0 2px 5px rgba(0,0,0,0.15);}
|
|
2022
|
+
.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; }
|
|
2023
|
+
.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; }
|
|
2024
|
+
.test-case-header:hover { background-color: #f4f6f8; }
|
|
2025
|
+
.test-case-header[aria-expanded="true"] { border-bottom-color: var(--border-color); background-color: #f9fafb; }
|
|
2026
|
+
.test-case-summary { display: flex; align-items: center; gap: 14px; flex-grow: 1; flex-wrap: wrap;}
|
|
2027
|
+
.test-case-title { font-weight: 600; color: var(--text-color); font-size: 1em; }
|
|
2028
|
+
.test-case-browser { font-size: 0.9em; color: var(--text-color-secondary); }
|
|
2029
|
+
.test-case-meta { display: flex; align-items: center; gap: 12px; font-size: 0.9em; color: var(--text-color-secondary); flex-shrink: 0; }
|
|
2030
|
+
.test-duration { background-color: var(--light-gray-color); padding: 4px 10px; border-radius: 12px; font-size: 0.9em;}
|
|
2031
|
+
.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); }
|
|
2032
|
+
.status-badge.status-passed { background-color: var(--success-color); }
|
|
2033
|
+
.status-badge.status-failed { background-color: var(--danger-color); }
|
|
2034
|
+
.status-badge.status-skipped { background-color: var(--warning-color); }
|
|
2035
|
+
.status-badge.status-unknown { background-color: var(--dark-gray-color); }
|
|
2036
|
+
.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; }
|
|
2037
|
+
.test-case-content { display: none; padding: 20px; border-top: 1px solid var(--border-color); background-color: #fcfdff; }
|
|
2038
|
+
.test-case-content h4 { margin-top: 22px; margin-bottom: 14px; font-size: 1.15em; color: var(--primary-color); }
|
|
2039
|
+
.test-case-content p { margin-bottom: 10px; font-size: 1em; }
|
|
2040
|
+
.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; }
|
|
2041
|
+
.test-error-summary h4 { color: var(--danger-color); margin-top:0;}
|
|
2042
|
+
.test-error-summary pre { white-space: pre-wrap; word-break: break-all; color: var(--danger-color); font-size: 0.95em;}
|
|
2043
|
+
.steps-list { margin: 18px 0; }
|
|
2044
|
+
.step-item { margin-bottom: 8px; padding-left: calc(var(--depth, 0) * 28px); }
|
|
2045
|
+
.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; }
|
|
2046
|
+
.step-header:hover { background-color: #f0f2f5; border-color: var(--medium-gray-color); box-shadow: var(--box-shadow-inset); }
|
|
2047
|
+
.step-icon { margin-right: 12px; width: 20px; text-align: center; font-size: 1.1em; }
|
|
2048
|
+
.step-title { flex: 1; font-size: 1em; }
|
|
2049
|
+
.step-duration { color: var(--dark-gray-color); font-size: 0.9em; }
|
|
2050
|
+
.step-details { display: none; padding: 14px; margin-top: 8px; background: #fdfdfd; border-radius: 6px; font-size: 0.95em; border: 1px solid var(--light-gray-color); }
|
|
2051
|
+
.step-info { margin-bottom: 8px; }
|
|
2052
|
+
.test-error-summary { color: var(--danger-color); margin-top: 12px; padding: 14px; background: rgba(244,67,54,0.05); border-radius: 4px; font-size: 0.95em; border-left: 3px solid var(--danger-color); }
|
|
2053
|
+
.test-error-summary pre.stack-trace { margin-top: 10px; padding: 12px; background-color: rgba(0,0,0,0.03); border-radius: 4px; font-size:0.9em; max-height: 280px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; }
|
|
2054
|
+
.step-hook { background-color: rgba(33,150,243,0.04); border-left: 3px solid var(--info-color) !important; }
|
|
2055
|
+
.step-hook .step-title { font-style: italic; color: var(--info-color)}
|
|
2056
|
+
.nested-steps { margin-top: 12px; }
|
|
2057
|
+
.attachments-section { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--light-gray-color); }
|
|
2058
|
+
.attachments-section h4 { margin-top: 0; margin-bottom: 20px; font-size: 1.1em; color: var(--text-color); }
|
|
2059
|
+
.attachments-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 22px; }
|
|
2060
|
+
.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; }
|
|
2061
|
+
.attachment-item:hover { transform: translateY(-4px); box-shadow: var(--box-shadow); }
|
|
2062
|
+
.attachment-item img { width: 100%; height: 180px; object-fit: cover; display: block; border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease; }
|
|
2063
|
+
.attachment-info { padding: 12px; margin-top: auto; background-color: #fafafa;}
|
|
2064
|
+
.attachment-item a:hover img { opacity: 0.85; }
|
|
2065
|
+
.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); }
|
|
2066
|
+
.video-item a, .trace-item a { display: block; margin-bottom: 8px; color: var(--primary-color); text-decoration: none; font-weight: 500; }
|
|
2067
|
+
.video-item a:hover, .trace-item a:hover { text-decoration: underline; }
|
|
2068
|
+
.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;}
|
|
2069
|
+
.trace-actions { display: flex; justify-content: center; }
|
|
2070
|
+
.trace-actions a { text-decoration: none; color: var(--primary-color); font-weight: 500; font-size: 0.9em; }
|
|
2071
|
+
.generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
|
|
2072
|
+
.attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
|
|
2073
|
+
.attachment-caption { display: flex; flex-direction: column; }
|
|
2074
|
+
.attachment-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2075
|
+
.attachment-type { font-size: 0.8rem; color: var(--text-color-secondary); }
|
|
2076
|
+
.trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
|
|
2077
|
+
.test-history-container h2.tab-main-title, .ai-analyzer-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;}
|
|
2078
|
+
.test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
|
|
2079
|
+
.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; }
|
|
2080
|
+
.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); }
|
|
2081
|
+
.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 */
|
|
2082
|
+
.test-history-header p { font-weight: 500 } /* Added this */
|
|
2083
|
+
.test-history-trend { margin-bottom: 20px; min-height: 110px; }
|
|
2084
|
+
.test-history-trend div[id^="testHistoryChart-"] { display: block; margin: 0 auto; max-width:100%; height: 100px; width: 320px; }
|
|
2085
|
+
.test-history-details-collapsible summary { cursor: pointer; font-size: 1em; color: var(--primary-color); margin-bottom: 10px; font-weight:500; }
|
|
2086
|
+
.test-history-details-collapsible summary:hover {text-decoration: underline;}
|
|
2087
|
+
.test-history-details table { width: 100%; border-collapse: collapse; font-size: 0.95em; }
|
|
2088
|
+
.test-history-details th, .test-history-details td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--light-gray-color); }
|
|
2089
|
+
.test-history-details th { background-color: var(--light-gray-color); font-weight: 600; }
|
|
2090
|
+
.status-badge-small { padding: 3px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase; display: inline-block; }
|
|
2091
|
+
.status-badge-small.status-passed { background-color: var(--success-color); }
|
|
2092
|
+
.status-badge-small.status-failed { background-color: var(--danger-color); }
|
|
2093
|
+
.status-badge-small.status-skipped { background-color: var(--warning-color); }
|
|
2094
|
+
.status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
|
|
2095
|
+
.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); }
|
|
2096
|
+
.no-data-chart {font-size: 0.95em; padding: 18px;}
|
|
2097
|
+
.ai-failure-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 22px; }
|
|
2098
|
+
.ai-failure-card { background: var(--card-background-color); border: 1px solid var(--border-color); border-left: 5px solid var(--danger-color); border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); display: flex; flex-direction: column; }
|
|
2099
|
+
.ai-failure-card-header { padding: 15px 20px; border-bottom: 1px solid var(--light-gray-color); display: flex; align-items: center; justify-content: space-between; gap: 15px; }
|
|
2100
|
+
.ai-failure-card-header h3 { margin: 0; font-size: 1.1em; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2101
|
+
.ai-failure-card-body { padding: 20px; }
|
|
2102
|
+
.ai-fix-btn { background-color: var(--primary-color); color: white; border: none; padding: 10px 18px; font-size: 1em; font-weight: 600; border-radius: 6px; cursor: pointer; transition: background-color 0.2s ease, transform 0.2s ease; display: inline-flex; align-items: center; gap: 8px; }
|
|
2103
|
+
.ai-fix-btn:hover { background-color: var(--accent-color); transform: translateY(-2px); }
|
|
2104
|
+
.ai-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.65); display: none; align-items: center; justify-content: center; z-index: 1050; animation: fadeIn 0.3s; }
|
|
2105
|
+
.ai-modal-content { background-color: var(--card-background-color); color: var(--text-color); border-radius: var(--border-radius); width: 90%; max-width: 800px; max-height: 90vh; box-shadow: 0 10px 30px rgba(0,0,0,0.2); display: flex; flex-direction: column; overflow: hidden; }
|
|
2106
|
+
.ai-modal-header { padding: 18px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
|
|
2107
|
+
.ai-modal-header h3 { margin: 0; font-size: 1.25em; }
|
|
2108
|
+
.ai-modal-close { font-size: 2rem; font-weight: 300; cursor: pointer; color: var(--dark-gray-color); line-height: 1; transition: color 0.2s; }
|
|
2109
|
+
.ai-modal-close:hover { color: var(--danger-color); }
|
|
2110
|
+
.ai-modal-body { padding: 25px; overflow-y: auto; }
|
|
2111
|
+
.ai-modal-body h4 { margin-top: 18px; margin-bottom: 10px; font-size: 1.1em; color: var(--primary-color); }
|
|
2112
|
+
.ai-modal-body p { margin-bottom: 15px; }
|
|
2113
|
+
.ai-loader { margin: 40px auto; border: 5px solid #f3f3f3; border-top: 5px solid var(--primary-color); border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite; }
|
|
2114
|
+
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
2115
|
+
.trace-preview { padding: 1rem; text-align: center; background: #f5f5f5; border-bottom: 1px solid #e1e1e1; }
|
|
2116
|
+
.trace-icon { font-size: 2rem; display: block; margin-bottom: 0.5rem; }
|
|
2117
|
+
.trace-name { word-break: break-word; font-size: 0.9rem; }
|
|
2118
|
+
.trace-actions { display: flex; gap: 0.5rem; }
|
|
2119
|
+
.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; }
|
|
2120
|
+
.view-trace { background: #3182ce; color: white; }
|
|
2121
|
+
.view-trace:hover { background: #2c5282; }
|
|
2122
|
+
.download-trace { background: #e2e8f0; color: #2d3748; }
|
|
2123
|
+
.download-trace:hover { background: #cbd5e0; }
|
|
2124
|
+
.filters button.clear-filters-btn { background-color: var(--medium-gray-color); color: var(--text-color); }
|
|
2125
|
+
.filters button.clear-filters-btn:hover { background-color: var(--dark-gray-color); color: #fff; }
|
|
2126
|
+
.copy-btn {color: var(--primary-color); background: #fefefe; border-radius: 8px; cursor: pointer; border-color: var(--primary-color); font-size: 1em; margin-left: 93%; font-weight: 600;}
|
|
2127
|
+
/* Compact AI Failure Analyzer Styles */
|
|
2128
|
+
.ai-analyzer-stats {
|
|
2129
|
+
display: flex;
|
|
2130
|
+
gap: 20px;
|
|
2131
|
+
margin-bottom: 25px;
|
|
2132
|
+
padding: 20px;
|
|
2133
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
2134
|
+
border-radius: var(--border-radius);
|
|
2135
|
+
justify-content: center;
|
|
2136
|
+
}
|
|
2137
|
+
.stat-item {
|
|
2138
|
+
text-align: center;
|
|
2139
|
+
color: white;
|
|
2140
|
+
}
|
|
2141
|
+
.stat-number {
|
|
2142
|
+
display: block;
|
|
2143
|
+
font-size: 2em;
|
|
2144
|
+
font-weight: 700;
|
|
2145
|
+
line-height: 1;
|
|
2146
|
+
}
|
|
2147
|
+
.stat-label {
|
|
2148
|
+
font-size: 0.9em;
|
|
2149
|
+
opacity: 0.9;
|
|
2150
|
+
font-weight: 500;
|
|
2151
|
+
}
|
|
2152
|
+
.ai-analyzer-description {
|
|
2153
|
+
margin-bottom: 25px;
|
|
2154
|
+
font-size: 1em;
|
|
2155
|
+
color: var(--text-color-secondary);
|
|
2156
|
+
text-align: center;
|
|
2157
|
+
max-width: 600px;
|
|
2158
|
+
margin-left: auto;
|
|
2159
|
+
margin-right: auto;
|
|
2160
|
+
}
|
|
2161
|
+
.compact-failure-list {
|
|
2162
|
+
display: flex;
|
|
2163
|
+
flex-direction: column;
|
|
2164
|
+
gap: 15px;
|
|
2165
|
+
}
|
|
2166
|
+
.compact-failure-item {
|
|
2167
|
+
background: var(--card-background-color);
|
|
2168
|
+
border: 1px solid var(--border-color);
|
|
2169
|
+
border-left: 4px solid var(--danger-color);
|
|
2170
|
+
border-radius: var(--border-radius);
|
|
2171
|
+
box-shadow: var(--box-shadow-light);
|
|
2172
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
2173
|
+
}
|
|
2174
|
+
.compact-failure-item:hover {
|
|
2175
|
+
transform: translateY(-2px);
|
|
2176
|
+
box-shadow: var(--box-shadow);
|
|
2177
|
+
}
|
|
2178
|
+
.failure-header {
|
|
2179
|
+
display: flex;
|
|
2180
|
+
justify-content: space-between;
|
|
2181
|
+
align-items: center;
|
|
2182
|
+
padding: 18px 20px;
|
|
2183
|
+
gap: 15px;
|
|
2184
|
+
}
|
|
2185
|
+
.failure-main-info {
|
|
2186
|
+
flex: 1;
|
|
2187
|
+
min-width: 0;
|
|
2188
|
+
}
|
|
2189
|
+
.failure-title {
|
|
2190
|
+
margin: 0 0 8px 0;
|
|
2191
|
+
font-size: 1.1em;
|
|
2192
|
+
font-weight: 600;
|
|
2193
|
+
color: var(--text-color);
|
|
2194
|
+
white-space: nowrap;
|
|
2195
|
+
overflow: hidden;
|
|
2196
|
+
text-overflow: ellipsis;
|
|
2197
|
+
}
|
|
2198
|
+
.failure-meta {
|
|
2199
|
+
display: flex;
|
|
2200
|
+
gap: 12px;
|
|
2201
|
+
align-items: center;
|
|
2202
|
+
}
|
|
2203
|
+
.browser-indicator, .duration-indicator {
|
|
2204
|
+
font-size: 0.85em;
|
|
2205
|
+
padding: 3px 8px;
|
|
2206
|
+
border-radius: 12px;
|
|
2207
|
+
font-weight: 500;
|
|
2208
|
+
}
|
|
2209
|
+
.browser-indicator {
|
|
2210
|
+
background: var(--info-color);
|
|
2211
|
+
color: white;
|
|
2212
|
+
}
|
|
2213
|
+
.duration-indicator {
|
|
2214
|
+
background: var(--medium-gray-color);
|
|
2215
|
+
color: var(--text-color);
|
|
2216
|
+
}
|
|
2217
|
+
.compact-ai-btn {
|
|
2218
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
2219
|
+
color: white;
|
|
2220
|
+
border: none;
|
|
2221
|
+
padding: 12px 18px;
|
|
2222
|
+
border-radius: 6px;
|
|
2223
|
+
cursor: pointer;
|
|
2224
|
+
font-weight: 600;
|
|
2225
|
+
display: flex;
|
|
2226
|
+
align-items: center;
|
|
2227
|
+
gap: 8px;
|
|
2228
|
+
transition: all 0.3s ease;
|
|
2229
|
+
white-space: nowrap;
|
|
2230
|
+
}
|
|
2231
|
+
.compact-ai-btn:hover {
|
|
2232
|
+
transform: translateY(-2px);
|
|
2233
|
+
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
|
2234
|
+
}
|
|
2235
|
+
.ai-icon {
|
|
2236
|
+
font-size: 1.2em;
|
|
2237
|
+
}
|
|
2238
|
+
.ai-text {
|
|
2239
|
+
font-size: 0.95em;
|
|
2240
|
+
}
|
|
2241
|
+
.failure-error-preview {
|
|
2242
|
+
padding: 0 20px 18px 20px;
|
|
2243
|
+
border-top: 1px solid var(--light-gray-color);
|
|
2244
|
+
}
|
|
2245
|
+
.error-snippet {
|
|
2246
|
+
background: rgba(244, 67, 54, 0.05);
|
|
2247
|
+
border: 1px solid rgba(244, 67, 54, 0.2);
|
|
2248
|
+
border-radius: 6px;
|
|
2249
|
+
padding: 12px;
|
|
2250
|
+
margin-bottom: 12px;
|
|
2251
|
+
font-family: monospace;
|
|
2252
|
+
font-size: 0.9em;
|
|
2253
|
+
color: var(--danger-color);
|
|
2254
|
+
line-height: 1.4;
|
|
2255
|
+
}
|
|
2256
|
+
.expand-error-btn {
|
|
2257
|
+
background: none;
|
|
2258
|
+
border: 1px solid var(--border-color);
|
|
2259
|
+
color: var(--text-color-secondary);
|
|
2260
|
+
padding: 6px 12px;
|
|
2261
|
+
border-radius: 4px;
|
|
2262
|
+
cursor: pointer;
|
|
2263
|
+
font-size: 0.85em;
|
|
2264
|
+
display: flex;
|
|
2265
|
+
align-items: center;
|
|
2266
|
+
gap: 6px;
|
|
2267
|
+
transition: all 0.2s ease;
|
|
2268
|
+
}
|
|
2269
|
+
.expand-error-btn:hover {
|
|
2270
|
+
background: var(--light-gray-color);
|
|
2271
|
+
border-color: var(--medium-gray-color);
|
|
2272
|
+
}
|
|
2273
|
+
.expand-icon {
|
|
2274
|
+
transition: transform 0.2s ease;
|
|
2275
|
+
font-size: 0.8em;
|
|
2276
|
+
}
|
|
2277
|
+
.expand-error-btn.expanded .expand-icon {
|
|
2278
|
+
transform: rotate(180deg);
|
|
2279
|
+
}
|
|
2280
|
+
.full-error-details {
|
|
2281
|
+
padding: 0 20px 20px 20px;
|
|
2282
|
+
border-top: 1px solid var(--light-gray-color);
|
|
2283
|
+
margin-top: 0;
|
|
2284
|
+
}
|
|
2285
|
+
.full-error-content {
|
|
2286
|
+
background: rgba(244, 67, 54, 0.05);
|
|
2287
|
+
border: 1px solid rgba(244, 67, 54, 0.2);
|
|
2288
|
+
border-radius: 6px;
|
|
2289
|
+
padding: 15px;
|
|
2290
|
+
font-family: monospace;
|
|
2291
|
+
font-size: 0.9em;
|
|
2292
|
+
color: var(--danger-color);
|
|
2293
|
+
line-height: 1.4;
|
|
2294
|
+
max-height: 300px;
|
|
2295
|
+
overflow-y: auto;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
/* Responsive adjustments for compact design */
|
|
2299
|
+
@media (max-width: 768px) {
|
|
2300
|
+
.ai-analyzer-stats {
|
|
2301
|
+
flex-direction: column;
|
|
2302
|
+
gap: 15px;
|
|
2303
|
+
text-align: center;
|
|
2304
|
+
}
|
|
2305
|
+
.failure-header {
|
|
2306
|
+
flex-direction: column;
|
|
2307
|
+
align-items: stretch;
|
|
2308
|
+
gap: 15px;
|
|
2309
|
+
}
|
|
2310
|
+
.failure-main-info {
|
|
2311
|
+
text-align: center;
|
|
2312
|
+
}
|
|
2313
|
+
.failure-meta {
|
|
2314
|
+
justify-content: center;
|
|
2315
|
+
}
|
|
2316
|
+
.compact-ai-btn {
|
|
2317
|
+
justify-content: center;
|
|
2318
|
+
padding: 12px 20px;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
@media (max-width: 480px) {
|
|
2322
|
+
.stat-item .stat-number {
|
|
2323
|
+
font-size: 1.5em;
|
|
2324
|
+
}
|
|
2325
|
+
.failure-header {
|
|
2326
|
+
padding: 15px;
|
|
2327
|
+
}
|
|
2328
|
+
.failure-error-preview, .full-error-details {
|
|
2329
|
+
padding-left: 15px;
|
|
2330
|
+
padding-right: 15px;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
@media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
|
|
2335
|
+
@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; } }
|
|
2336
|
+
@media (max-width: 768px) { body { font-size: 15px; } .container { margin: 10px; padding: 20px; } .header { flex-direction: column; align-items: flex-start; gap: 15px; } .header h1 { font-size: 1.6em; } .run-info { text-align: left; font-size:0.9em; } .tabs { margin-bottom: 25px;} .tab-button { padding: 12px 20px; font-size: 1.05em;} .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;} .summary-card .value {font-size: 2em;} .summary-card h3 {font-size: 0.95em;} .filters { flex-direction: column; padding: 18px; gap: 12px;} .filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;} .test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; } .test-case-summary {gap: 10px;} .test-case-title {font-size: 1.05em;} .test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;} .attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;} .test-history-grid {grid-template-columns: 1fr;} .pie-chart-wrapper {min-height: auto;} .ai-failure-cards-grid { grid-template-columns: 1fr; } }
|
|
2337
|
+
@media (max-width: 480px) { body {font-size: 14px;} .container {padding: 15px;} .header h1 {font-size: 1.4em;} #report-logo { height: 35px; width: 50px; } .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;} }
|
|
2338
|
+
</style>
|
|
2339
|
+
</head>
|
|
2340
|
+
<body>
|
|
2341
|
+
<div class="container">
|
|
2342
|
+
<header class="header">
|
|
2343
|
+
<div class="header-title">
|
|
2344
|
+
<img id="report-logo" src="https://i.postimg.cc/v817w4sg/logo.png" alt="Report Logo">
|
|
2345
|
+
<h1>Playwright Pulse Report</h1>
|
|
2346
|
+
</div>
|
|
2347
|
+
<div class="run-info"><strong>Run Date:</strong> ${formatDate(
|
|
2348
|
+
runSummary.timestamp
|
|
2349
|
+
)}<br><strong>Total Duration:</strong> ${formatDuration(
|
|
2350
|
+
runSummary.duration
|
|
2351
|
+
)}</div>
|
|
2352
|
+
</header>
|
|
2353
|
+
<div class="tabs">
|
|
2354
|
+
<button class="tab-button active" data-tab="dashboard">Dashboard</button>
|
|
2355
|
+
<button class="tab-button" data-tab="test-runs">Test Run Summary</button>
|
|
2356
|
+
<button class="tab-button" data-tab="test-history">Test History</button>
|
|
2357
|
+
<button class="tab-button" data-tab="ai-failure-analyzer">AI Failure Analyzer</button>
|
|
2358
|
+
</div>
|
|
2359
|
+
<div id="dashboard" class="tab-content active">
|
|
2360
|
+
<div class="dashboard-grid">
|
|
2361
|
+
<div class="summary-card"><h3>Total Tests</h3><div class="value">${runSummary.totalTests
|
|
2362
|
+
}</div></div>
|
|
2363
|
+
<div class="summary-card status-passed"><h3>Passed</h3><div class="value">${runSummary.passed
|
|
2364
|
+
}</div><div class="trend-percentage">${passPercentage}%</div></div>
|
|
2365
|
+
<div class="summary-card status-failed"><h3>Failed</h3><div class="value">${runSummary.failed
|
|
2366
|
+
}</div><div class="trend-percentage">${failPercentage}%</div></div>
|
|
2367
|
+
<div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${runSummary.skipped || 0
|
|
2368
|
+
}</div><div class="trend-percentage">${skipPercentage}%</div></div>
|
|
2369
|
+
<div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
|
|
2370
|
+
<div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
|
|
2371
|
+
runSummary.duration
|
|
2372
|
+
)}</div></div>
|
|
2373
|
+
</div>
|
|
2374
|
+
<div class="dashboard-bottom-row">
|
|
2375
|
+
<div style="display: grid; gap: 20px">
|
|
2376
|
+
${generatePieChart(
|
|
2377
|
+
[
|
|
2378
|
+
{ label: "Passed", value: runSummary.passed },
|
|
2379
|
+
{ label: "Failed", value: runSummary.failed },
|
|
2380
|
+
{ label: "Skipped", value: runSummary.skipped || 0 },
|
|
2381
|
+
],
|
|
2382
|
+
400,
|
|
2383
|
+
390
|
|
2384
|
+
)}
|
|
2385
|
+
${runSummary.environment &&
|
|
2386
|
+
Object.keys(runSummary.environment).length > 0
|
|
2387
|
+
? generateEnvironmentDashboard(runSummary.environment)
|
|
2388
|
+
: '<div class="no-data">Environment data not available.</div>'
|
|
2389
|
+
}
|
|
2390
|
+
</div>
|
|
2391
|
+
${generateSuitesWidget(suitesData)}
|
|
2392
|
+
</div>
|
|
2393
|
+
</div>
|
|
2394
|
+
<div id="test-runs" class="tab-content">
|
|
2395
|
+
<div class="filters">
|
|
2396
|
+
<input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
|
|
2397
|
+
<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>
|
|
2398
|
+
<select id="filter-browser"><option value="">All Browsers</option>${Array.from(
|
|
2399
|
+
new Set(
|
|
2400
|
+
(results || []).map((test) => test.browser || "unknown")
|
|
2401
|
+
)
|
|
2402
|
+
)
|
|
2403
|
+
.map(
|
|
2404
|
+
(browser) =>
|
|
2405
|
+
`<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
|
|
2406
|
+
browser
|
|
2407
|
+
)}</option>`
|
|
2408
|
+
)
|
|
2409
|
+
.join("")}</select>
|
|
2410
|
+
<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>
|
|
2411
|
+
</div>
|
|
2412
|
+
<div class="test-cases-list">${generateTestCasesHTML()}</div>
|
|
2413
|
+
</div>
|
|
2414
|
+
<div id="test-history" class="tab-content">
|
|
2415
|
+
<h2 class="tab-main-title">Execution Trends</h2>
|
|
2416
|
+
<div class="trend-charts-row">
|
|
2417
|
+
<div class="trend-chart"><h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
|
|
2418
|
+
${trendData && trendData.overall && trendData.overall.length > 0
|
|
2419
|
+
? generateTestTrendsChart(trendData)
|
|
2420
|
+
: '<div class="no-data">Overall trend data not available for test counts.</div>'
|
|
2421
|
+
}
|
|
2422
|
+
</div>
|
|
2423
|
+
<div class="trend-chart"><h3 class="chart-title-header">Execution Duration Trends</h3>
|
|
2424
|
+
${trendData && trendData.overall && trendData.overall.length > 0
|
|
2425
|
+
? generateDurationTrendChart(trendData)
|
|
2426
|
+
: '<div class="no-data">Overall trend data not available for durations.</div>'
|
|
2427
|
+
}
|
|
2428
|
+
</div>
|
|
2429
|
+
</div>
|
|
2430
|
+
<h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
|
|
2431
|
+
<div class="trend-charts-row">
|
|
2432
|
+
<div class="trend-chart">
|
|
2433
|
+
${generateWorkerDistributionChart(results)}
|
|
2434
|
+
</div>
|
|
2435
|
+
</div>
|
|
2436
|
+
<h2 class="tab-main-title">Individual Test History</h2>
|
|
2437
|
+
${trendData &&
|
|
2438
|
+
trendData.testRuns &&
|
|
2439
|
+
Object.keys(trendData.testRuns).length > 0
|
|
2440
|
+
? generateTestHistoryContent(trendData)
|
|
2441
|
+
: '<div class="no-data">Individual test history data not available.</div>'
|
|
2442
|
+
}
|
|
2443
|
+
</div>
|
|
2444
|
+
<div id="ai-failure-analyzer" class="tab-content">
|
|
2445
|
+
${generateAIFailureAnalyzerTab(results)}
|
|
2446
|
+
</div>
|
|
2447
|
+
<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;">
|
|
2448
|
+
<div style="display: inline-flex; align-items: center; gap: 0.5rem; color: #333; font-size: 0.9rem; font-weight: 600; letter-spacing: 0.5px;">
|
|
2449
|
+
<span>Created by</span>
|
|
2450
|
+
<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>
|
|
2451
|
+
</div>
|
|
2452
|
+
<div style="margin-top: 0.5rem; font-size: 0.75rem; color: #666;">Crafted with precision</div>
|
|
2453
|
+
</footer>
|
|
2454
|
+
</div>
|
|
2455
|
+
<script>
|
|
2456
|
+
// Ensure formatDuration is globally available
|
|
2457
|
+
if (typeof formatDuration === 'undefined') {
|
|
2458
|
+
function formatDuration(ms) {
|
|
2459
|
+
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
2460
|
+
return (ms / 1000).toFixed(1) + "s";
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
function copyLogContent(elementId, button) {
|
|
2464
|
+
const logElement = document.getElementById(elementId);
|
|
2465
|
+
if (!logElement) {
|
|
2466
|
+
console.error('Could not find log element with ID:', elementId);
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
navigator.clipboard.writeText(logElement.innerText).then(() => {
|
|
2470
|
+
button.textContent = 'Copied!';
|
|
2471
|
+
setTimeout(() => { button.textContent = 'Copy'; }, 2000);
|
|
2472
|
+
}).catch(err => {
|
|
2473
|
+
console.error('Failed to copy log content:', err);
|
|
2474
|
+
button.textContent = 'Failed';
|
|
2475
|
+
setTimeout(() => { button.textContent = 'Copy'; }, 2000);
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// --- AI Failure Analyzer Functions ---
|
|
2480
|
+
function getAIFix(button) {
|
|
2481
|
+
const modal = document.getElementById('ai-fix-modal');
|
|
2482
|
+
const modalContent = document.getElementById('ai-fix-modal-content');
|
|
2483
|
+
const modalTitle = document.getElementById('ai-fix-modal-title');
|
|
2484
|
+
|
|
2485
|
+
modal.style.display = 'flex';
|
|
2486
|
+
modalTitle.textContent = 'Analyzing...';
|
|
2487
|
+
modalContent.innerHTML = '<div class="ai-loader"></div>';
|
|
2488
|
+
|
|
2489
|
+
try {
|
|
2490
|
+
const testJson = button.dataset.testJson;
|
|
2491
|
+
const test = JSON.parse(atob(testJson));
|
|
2492
|
+
|
|
2493
|
+
const testName = test.name || 'Unknown Test';
|
|
2494
|
+
const failureLogsAndErrors = [
|
|
2495
|
+
'Error Message:',
|
|
2496
|
+
test.errorMessage || 'Not available.',
|
|
2497
|
+
'\\n\\n--- stdout ---',
|
|
2498
|
+
(test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
|
|
2499
|
+
'\\n\\n--- stderr ---',
|
|
2500
|
+
(test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
|
|
2501
|
+
].join('\\n');
|
|
2502
|
+
const codeSnippet = test.snippet || '';
|
|
2503
|
+
|
|
2504
|
+
const shortTestName = testName.split(' > ').pop();
|
|
2505
|
+
modalTitle.textContent = \`Analysis for: \${shortTestName}\`;
|
|
2506
|
+
|
|
2507
|
+
const apiUrl = 'https://ai-test-analyser.netlify.app/api/analyze';
|
|
2508
|
+
fetch(apiUrl, {
|
|
2509
|
+
method: 'POST',
|
|
2510
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2511
|
+
body: JSON.stringify({
|
|
2512
|
+
testName: testName,
|
|
2513
|
+
failureLogsAndErrors: failureLogsAndErrors,
|
|
2514
|
+
codeSnippet: codeSnippet,
|
|
2515
|
+
}),
|
|
2516
|
+
})
|
|
2517
|
+
.then(response => {
|
|
2518
|
+
if (!response.ok) {
|
|
2519
|
+
return response.text().then(text => {
|
|
2520
|
+
throw new Error(\`API request failed with status \${response.status}: \${text || response.statusText}\`);
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
return response.text();
|
|
2524
|
+
})
|
|
2525
|
+
.then(text => {
|
|
2526
|
+
if (!text) {
|
|
2527
|
+
throw new Error("The AI analyzer returned an empty response. This might happen during high load or if the request was blocked. Please try again in a moment.");
|
|
2528
|
+
}
|
|
2529
|
+
try {
|
|
2530
|
+
return JSON.parse(text);
|
|
2531
|
+
} catch (e) {
|
|
2532
|
+
console.error("Failed to parse JSON:", text);
|
|
2533
|
+
throw new Error(\`The AI analyzer returned an invalid response. \${e.message}\`);
|
|
2534
|
+
}
|
|
2535
|
+
})
|
|
2536
|
+
.then(data => {
|
|
2537
|
+
// Helper function to prevent XSS by escaping HTML characters
|
|
2538
|
+
const escapeHtml = (unsafe) => {
|
|
2539
|
+
if (typeof unsafe !== 'string') return '';
|
|
2540
|
+
return unsafe
|
|
2541
|
+
.replace(/&/g, "&")
|
|
2542
|
+
.replace(/</g, "<")
|
|
2543
|
+
.replace(/>/g, ">")
|
|
2544
|
+
.replace(/"/g, """)
|
|
2545
|
+
.replace(/'/g, "'");
|
|
2546
|
+
};
|
|
2547
|
+
|
|
2548
|
+
// Build the "Analysis" part from the 'rootCause' field
|
|
2549
|
+
const analysisHtml = \`<h4>Analysis</h4><p>\${escapeHtml(data.rootCause) || 'No analysis provided.'}</p>\`;
|
|
2550
|
+
|
|
2551
|
+
// Build the "Suggestions" part by iterating through the 'suggestedFixes' array
|
|
2552
|
+
let suggestionsHtml = '<h4>Suggestions</h4>';
|
|
2553
|
+
if (data.suggestedFixes && data.suggestedFixes.length > 0) {
|
|
2554
|
+
suggestionsHtml += '<div class="suggestions-list" style="margin-top: 15px;">';
|
|
2555
|
+
data.suggestedFixes.forEach(fix => {
|
|
2556
|
+
suggestionsHtml += \`
|
|
2557
|
+
<div class="suggestion-item" style="margin-bottom: 22px; border-left: 3px solid var(--accent-color-alt); padding-left: 15px;">
|
|
2558
|
+
<p style="margin: 0 0 8px 0; font-weight: 500;">\${escapeHtml(fix.description)}</p>
|
|
2559
|
+
\${fix.codeSnippet ? \`<div class="code-section"><pre><code>\${escapeHtml(fix.codeSnippet)}</code></pre></div>\` : ''}
|
|
2560
|
+
</div>
|
|
2561
|
+
\`;
|
|
2562
|
+
});
|
|
2563
|
+
suggestionsHtml += '</div>';
|
|
2564
|
+
} else {
|
|
2565
|
+
// Fallback if there are no suggestions
|
|
2566
|
+
suggestionsHtml += \`<div class="code-section"><pre><code>No suggestion provided.</code></pre></div>\`;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
// Combine both parts and set the modal content
|
|
2570
|
+
modalContent.innerHTML = analysisHtml + suggestionsHtml;
|
|
2571
|
+
})
|
|
2572
|
+
.catch(err => {
|
|
2573
|
+
console.error('AI Fix Error:', err);
|
|
2574
|
+
modalContent.innerHTML = \`<div class="test-error-summary"><strong>Error:</strong> Failed to get AI analysis. Please check the console for details. <br><br> \${err.message}</div>\`;
|
|
2575
|
+
});
|
|
2576
|
+
|
|
2577
|
+
} catch (e) {
|
|
2578
|
+
console.error('Error processing test data for AI Fix:', e);
|
|
2579
|
+
modalTitle.textContent = 'Error';
|
|
2580
|
+
modalContent.innerHTML = \`<div class="test-error-summary">Could not process test data. Is it formatted correctly?</div>\`;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
|
|
2585
|
+
function closeAiModal() {
|
|
2586
|
+
const modal = document.getElementById('ai-fix-modal');
|
|
2587
|
+
if(modal) modal.style.display = 'none';
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
function toggleErrorDetails(button) {
|
|
2591
|
+
const errorDetails = button.closest('.compact-failure-item').querySelector('.full-error-details');
|
|
2592
|
+
const expandText = button.querySelector('.expand-text');
|
|
2593
|
+
const expandIcon = button.querySelector('.expand-icon');
|
|
2594
|
+
|
|
2595
|
+
if (errorDetails.style.display === 'none' || !errorDetails.style.display) {
|
|
2596
|
+
errorDetails.style.display = 'block';
|
|
2597
|
+
expandText.textContent = 'Hide Full Error';
|
|
2598
|
+
button.classList.add('expanded');
|
|
2599
|
+
} else {
|
|
2600
|
+
errorDetails.style.display = 'none';
|
|
2601
|
+
expandText.textContent = 'Show Full Error';
|
|
2602
|
+
button.classList.remove('expanded');
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
function initializeReportInteractivity() {
|
|
2607
|
+
const tabButtons = document.querySelectorAll('.tab-button');
|
|
2608
|
+
const tabContents = document.querySelectorAll('.tab-content');
|
|
2609
|
+
tabButtons.forEach(button => {
|
|
2610
|
+
button.addEventListener('click', () => {
|
|
2611
|
+
tabButtons.forEach(btn => btn.classList.remove('active'));
|
|
2612
|
+
tabContents.forEach(content => content.classList.remove('active'));
|
|
2613
|
+
button.classList.add('active');
|
|
2614
|
+
const tabId = button.getAttribute('data-tab');
|
|
2615
|
+
const activeContent = document.getElementById(tabId);
|
|
2616
|
+
if (activeContent) {
|
|
2617
|
+
activeContent.classList.add('active');
|
|
2618
|
+
if ('IntersectionObserver' in window) {
|
|
2619
|
+
// Handled by observer
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
});
|
|
2623
|
+
});
|
|
2624
|
+
// --- Test Run Summary Filters ---
|
|
2625
|
+
const nameFilter = document.getElementById('filter-name');
|
|
2626
|
+
const statusFilter = document.getElementById('filter-status');
|
|
2627
|
+
const browserFilter = document.getElementById('filter-browser');
|
|
2628
|
+
const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters');
|
|
2629
|
+
function filterTestCases() {
|
|
2630
|
+
const nameValue = nameFilter ? nameFilter.value.toLowerCase() : "";
|
|
2631
|
+
const statusValue = statusFilter ? statusFilter.value : "";
|
|
2632
|
+
const browserValue = browserFilter ? browserFilter.value : "";
|
|
2633
|
+
document.querySelectorAll('#test-runs .test-case').forEach(testCaseElement => {
|
|
2634
|
+
const titleElement = testCaseElement.querySelector('.test-case-title');
|
|
2635
|
+
const fullTestName = titleElement ? titleElement.getAttribute('title').toLowerCase() : "";
|
|
2636
|
+
const status = testCaseElement.getAttribute('data-status');
|
|
2637
|
+
const browser = testCaseElement.getAttribute('data-browser');
|
|
2638
|
+
const nameMatch = fullTestName.includes(nameValue);
|
|
2639
|
+
const statusMatch = !statusValue || status === statusValue;
|
|
2640
|
+
const browserMatch = !browserValue || browser === browserValue;
|
|
2641
|
+
testCaseElement.style.display = (nameMatch && statusMatch && browserMatch) ? '' : 'none';
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
if(nameFilter) nameFilter.addEventListener('input', filterTestCases);
|
|
2645
|
+
if(statusFilter) statusFilter.addEventListener('change', filterTestCases);
|
|
2646
|
+
if(browserFilter) browserFilter.addEventListener('change', filterTestCases);
|
|
2647
|
+
if(clearRunSummaryFiltersBtn) clearRunSummaryFiltersBtn.addEventListener('click', () => {
|
|
2648
|
+
if(nameFilter) nameFilter.value = ''; if(statusFilter) statusFilter.value = ''; if(browserFilter) browserFilter.value = '';
|
|
2649
|
+
filterTestCases();
|
|
2650
|
+
});
|
|
2651
|
+
// --- Test History Filters ---
|
|
2652
|
+
const historyNameFilter = document.getElementById('history-filter-name');
|
|
2653
|
+
const historyStatusFilter = document.getElementById('history-filter-status');
|
|
2654
|
+
const clearHistoryFiltersBtn = document.getElementById('clear-history-filters');
|
|
2655
|
+
function filterTestHistoryCards() {
|
|
2656
|
+
const nameValue = historyNameFilter ? historyNameFilter.value.toLowerCase() : "";
|
|
2657
|
+
const statusValue = historyStatusFilter ? historyStatusFilter.value : "";
|
|
2658
|
+
document.querySelectorAll('.test-history-card').forEach(card => {
|
|
2659
|
+
const testTitle = card.getAttribute('data-test-name').toLowerCase();
|
|
2660
|
+
const latestStatus = card.getAttribute('data-latest-status');
|
|
2661
|
+
const nameMatch = testTitle.includes(nameValue);
|
|
2662
|
+
const statusMatch = !statusValue || latestStatus === statusValue;
|
|
2663
|
+
card.style.display = (nameMatch && statusMatch) ? '' : 'none';
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
if(historyNameFilter) historyNameFilter.addEventListener('input', filterTestHistoryCards);
|
|
2667
|
+
if(historyStatusFilter) historyStatusFilter.addEventListener('change', filterTestHistoryCards);
|
|
2668
|
+
if(clearHistoryFiltersBtn) clearHistoryFiltersBtn.addEventListener('click', () => {
|
|
2669
|
+
if(historyNameFilter) historyNameFilter.value = ''; if(historyStatusFilter) historyStatusFilter.value = '';
|
|
2670
|
+
filterTestHistoryCards();
|
|
2671
|
+
});
|
|
2672
|
+
// --- Expand/Collapse and Toggle Details Logic ---
|
|
2673
|
+
function toggleElementDetails(headerElement, contentSelector) {
|
|
2674
|
+
let contentElement;
|
|
2675
|
+
if (headerElement.classList.contains('test-case-header')) {
|
|
2676
|
+
contentElement = headerElement.parentElement.querySelector('.test-case-content');
|
|
2677
|
+
} else if (headerElement.classList.contains('step-header')) {
|
|
2678
|
+
contentElement = headerElement.nextElementSibling;
|
|
2679
|
+
if (!contentElement || !contentElement.matches(contentSelector || '.step-details')) {
|
|
2680
|
+
contentElement = null;
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
if (contentElement) {
|
|
2684
|
+
const isExpanded = contentElement.style.display === 'block';
|
|
2685
|
+
contentElement.style.display = isExpanded ? 'none' : 'block';
|
|
2686
|
+
headerElement.setAttribute('aria-expanded', String(!isExpanded));
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
document.querySelectorAll('#test-runs .test-case-header').forEach(header => {
|
|
2690
|
+
header.addEventListener('click', () => toggleElementDetails(header));
|
|
2691
|
+
});
|
|
2692
|
+
document.querySelectorAll('#test-runs .step-header').forEach(header => {
|
|
2693
|
+
header.addEventListener('click', () => toggleElementDetails(header, '.step-details'));
|
|
2694
|
+
});
|
|
2695
|
+
const expandAllBtn = document.getElementById('expand-all-tests');
|
|
2696
|
+
const collapseAllBtn = document.getElementById('collapse-all-tests');
|
|
2697
|
+
function setAllTestRunDetailsVisibility(displayMode, ariaState) {
|
|
2698
|
+
document.querySelectorAll('#test-runs .test-case-content').forEach(el => el.style.display = displayMode);
|
|
2699
|
+
document.querySelectorAll('#test-runs .step-details').forEach(el => el.style.display = displayMode);
|
|
2700
|
+
document.querySelectorAll('#test-runs .test-case-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
|
|
2701
|
+
document.querySelectorAll('#test-runs .step-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
|
|
2702
|
+
}
|
|
2703
|
+
if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
|
|
2704
|
+
if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
|
|
2705
|
+
// --- Intersection Observer for Lazy Loading ---
|
|
2706
|
+
const lazyLoadElements = document.querySelectorAll('.lazy-load-chart');
|
|
2707
|
+
if ('IntersectionObserver' in window) {
|
|
2708
|
+
let lazyObserver = new IntersectionObserver((entries, observer) => {
|
|
2709
|
+
entries.forEach(entry => {
|
|
2710
|
+
if (entry.isIntersecting) {
|
|
2711
|
+
const element = entry.target;
|
|
2712
|
+
if (element.classList.contains('lazy-load-chart')) {
|
|
2713
|
+
const renderFunctionName = element.dataset.renderFunctionName;
|
|
2714
|
+
if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
|
|
2715
|
+
try {
|
|
2716
|
+
console.log('Lazy loading chart with function:', renderFunctionName);
|
|
2717
|
+
window[renderFunctionName](); // Call the render function
|
|
2718
|
+
} catch (e) {
|
|
2719
|
+
console.error(\`Error lazy-loading chart \${element.id} using \${renderFunctionName}:\`, e);
|
|
2720
|
+
element.innerHTML = '<div class="no-data-chart">Error lazy-loading chart.</div>';
|
|
2721
|
+
}
|
|
2722
|
+
} else {
|
|
2723
|
+
console.warn(\`Render function \${renderFunctionName} not found or not a function for chart:\`, element.id);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
observer.unobserve(element); // Important: stop observing once loaded
|
|
2727
|
+
}
|
|
2728
|
+
});
|
|
2729
|
+
}, {
|
|
2730
|
+
rootMargin: "0px 0px 200px 0px" // Start loading when element is 200px from viewport bottom
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
lazyLoadElements.forEach(el => {
|
|
2734
|
+
lazyObserver.observe(el);
|
|
2735
|
+
});
|
|
2736
|
+
} else { // Fallback for browsers without IntersectionObserver
|
|
2737
|
+
console.warn("IntersectionObserver not supported. Loading all items immediately.");
|
|
2738
|
+
lazyLoadElements.forEach(element => {
|
|
2739
|
+
if (element.classList.contains('lazy-load-chart')) {
|
|
2740
|
+
const renderFunctionName = element.dataset.renderFunctionName;
|
|
2741
|
+
if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
|
|
2742
|
+
try {
|
|
2743
|
+
window[renderFunctionName]();
|
|
2744
|
+
} catch (e) {
|
|
2745
|
+
console.error(\`Error loading chart (fallback) \${element.id} using \${renderFunctionName}:\`, e);
|
|
2746
|
+
element.innerHTML = '<div class="no-data-chart">Error loading chart (fallback).</div>';
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
|
|
2754
|
+
|
|
2755
|
+
function copyErrorToClipboard(button) {
|
|
2756
|
+
// 1. Find the main error container, which should always be present.
|
|
2757
|
+
const errorContainer = button.closest('.test-error-summary');
|
|
2758
|
+
if (!errorContainer) {
|
|
2759
|
+
console.error("Could not find '.test-error-summary' container. The report's HTML structure might have changed.");
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
let errorText;
|
|
2764
|
+
|
|
2765
|
+
// 2. First, try to find the preferred .stack-trace element (the "happy path").
|
|
2766
|
+
const stackTraceElement = errorContainer.querySelector('.stack-trace');
|
|
2767
|
+
|
|
2768
|
+
if (stackTraceElement) {
|
|
2769
|
+
// If it exists, use its text content. This handles standard assertion errors.
|
|
2770
|
+
errorText = stackTraceElement.textContent;
|
|
2771
|
+
} else {
|
|
2772
|
+
// 3. FALLBACK: If .stack-trace doesn't exist, this is likely an unstructured error.
|
|
2773
|
+
// We clone the container to avoid manipulating the live DOM or copying the button's own text.
|
|
2774
|
+
const clonedContainer = errorContainer.cloneNode(true);
|
|
2775
|
+
|
|
2776
|
+
// Remove the button from our clone before extracting the text.
|
|
2777
|
+
const buttonInClone = clonedContainer.querySelector('button');
|
|
2778
|
+
if (buttonInClone) {
|
|
2779
|
+
buttonInClone.remove();
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// Use the text content of the cleaned container as the fallback.
|
|
2783
|
+
errorText = clonedContainer.textContent;
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
// 4. Proceed with the clipboard logic, ensuring text is not null and is trimmed.
|
|
2787
|
+
if (!errorText) {
|
|
2788
|
+
console.error('Could not extract error text.');
|
|
2789
|
+
button.textContent = 'Nothing to copy';
|
|
2790
|
+
setTimeout(() => { button.textContent = 'Copy Error'; }, 2000);
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
const textarea = document.createElement('textarea');
|
|
2795
|
+
textarea.value = errorText.trim(); // Trim whitespace for a cleaner copy.
|
|
2796
|
+
textarea.style.position = 'fixed'; // Prevent screen scroll
|
|
2797
|
+
textarea.style.top = '-9999px';
|
|
2798
|
+
document.body.appendChild(textarea);
|
|
2799
|
+
textarea.select();
|
|
2800
|
+
|
|
2801
|
+
try {
|
|
2802
|
+
const successful = document.execCommand('copy');
|
|
2803
|
+
const originalText = button.textContent;
|
|
2804
|
+
button.textContent = successful ? 'Copied!' : 'Failed';
|
|
2805
|
+
setTimeout(() => {
|
|
2806
|
+
button.textContent = originalText;
|
|
2807
|
+
}, 2000);
|
|
2808
|
+
} catch (err) {
|
|
2809
|
+
console.error('Failed to copy: ', err);
|
|
2810
|
+
button.textContent = 'Failed';
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
document.body.removeChild(textarea);
|
|
2814
|
+
}
|
|
2815
|
+
</script>
|
|
2816
|
+
</body>
|
|
2817
|
+
</html>
|
|
2818
|
+
`;
|
|
2819
|
+
}
|
|
2820
|
+
async function runScript(scriptPath) {
|
|
2821
|
+
return new Promise((resolve, reject) => {
|
|
2822
|
+
console.log(chalk.blue(`Executing script: ${scriptPath}...`));
|
|
2823
|
+
const process = fork(scriptPath, [], {
|
|
2824
|
+
stdio: "inherit",
|
|
2825
|
+
});
|
|
2826
|
+
|
|
2827
|
+
process.on("error", (err) => {
|
|
2828
|
+
console.error(chalk.red(`Failed to start script: ${scriptPath}`), err);
|
|
2829
|
+
reject(err);
|
|
2830
|
+
});
|
|
2831
|
+
|
|
2832
|
+
process.on("exit", (code) => {
|
|
2833
|
+
if (code === 0) {
|
|
2834
|
+
console.log(chalk.green(`Script ${scriptPath} finished successfully.`));
|
|
2835
|
+
resolve();
|
|
2836
|
+
} else {
|
|
2837
|
+
const errorMessage = `Script ${scriptPath} exited with code ${code}.`;
|
|
2838
|
+
console.error(chalk.red(errorMessage));
|
|
2839
|
+
reject(new Error(errorMessage));
|
|
2840
|
+
}
|
|
2841
|
+
});
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
async function main() {
|
|
2845
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
2846
|
+
const __dirname = path.dirname(__filename);
|
|
2847
|
+
|
|
2848
|
+
// Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
|
|
2849
|
+
const archiveRunScriptPath = path.resolve(
|
|
2850
|
+
__dirname,
|
|
2851
|
+
"generate-trend.mjs" // Keeping the filename as per your request
|
|
2852
|
+
);
|
|
2853
|
+
|
|
2854
|
+
const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2855
|
+
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
|
|
2856
|
+
const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
|
|
2857
|
+
|
|
2858
|
+
const historyDir = path.join(outputDir, "history"); // Directory for historical JSON files
|
|
2859
|
+
const HISTORY_FILE_PREFIX = "trend-"; // Match prefix used in archiving script
|
|
2860
|
+
const MAX_HISTORY_FILES_TO_LOAD_FOR_REPORT = 15; // How many historical runs to show in the report
|
|
2861
|
+
|
|
2862
|
+
console.log(chalk.blue(`Starting static HTML report generation...`));
|
|
2863
|
+
console.log(chalk.blue(`Output directory set to: ${outputDir}`));
|
|
2864
|
+
|
|
2865
|
+
// Step 1: Ensure current run data is archived to the history folder
|
|
2866
|
+
try {
|
|
2867
|
+
await runScript(archiveRunScriptPath); // This script now handles JSON history
|
|
2868
|
+
console.log(
|
|
2869
|
+
chalk.green("Current run data archiving to history completed.")
|
|
2870
|
+
);
|
|
2871
|
+
} catch (error) {
|
|
2872
|
+
console.error(
|
|
2873
|
+
chalk.red(
|
|
2874
|
+
"Failed to archive current run data. Report might use stale or incomplete historical trends."
|
|
2875
|
+
),
|
|
2876
|
+
error
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
// Step 2: Load current run's data (for non-trend sections of the report)
|
|
2881
|
+
let currentRunReportData;
|
|
2882
|
+
try {
|
|
2883
|
+
const jsonData = await fs.readFile(reportJsonPath, "utf-8");
|
|
2884
|
+
currentRunReportData = JSON.parse(jsonData);
|
|
2885
|
+
if (
|
|
2886
|
+
!currentRunReportData ||
|
|
2887
|
+
typeof currentRunReportData !== "object" ||
|
|
2888
|
+
!currentRunReportData.results
|
|
2889
|
+
) {
|
|
2890
|
+
throw new Error(
|
|
2891
|
+
"Invalid report JSON structure. 'results' field is missing or invalid."
|
|
2892
|
+
);
|
|
2893
|
+
}
|
|
2894
|
+
if (!Array.isArray(currentRunReportData.results)) {
|
|
2895
|
+
currentRunReportData.results = [];
|
|
2896
|
+
console.warn(
|
|
2897
|
+
chalk.yellow(
|
|
2898
|
+
"Warning: 'results' field in current run JSON was not an array. Treated as empty."
|
|
2899
|
+
)
|
|
2900
|
+
);
|
|
2901
|
+
}
|
|
2902
|
+
} catch (error) {
|
|
2903
|
+
console.error(
|
|
2904
|
+
chalk.red(
|
|
2905
|
+
`Critical Error: Could not read or parse main report JSON at ${reportJsonPath}: ${error.message}`
|
|
2906
|
+
)
|
|
2907
|
+
);
|
|
2908
|
+
process.exit(1);
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
// Step 3: Load historical data for trends
|
|
2912
|
+
let historicalRuns = [];
|
|
2913
|
+
try {
|
|
2914
|
+
await fs.access(historyDir);
|
|
2915
|
+
const allHistoryFiles = await fs.readdir(historyDir);
|
|
2916
|
+
|
|
2917
|
+
const jsonHistoryFiles = allHistoryFiles
|
|
2918
|
+
.filter(
|
|
2919
|
+
(file) => file.startsWith(HISTORY_FILE_PREFIX) && file.endsWith(".json")
|
|
2920
|
+
)
|
|
2921
|
+
.map((file) => {
|
|
2922
|
+
const timestampPart = file
|
|
2923
|
+
.replace(HISTORY_FILE_PREFIX, "")
|
|
2924
|
+
.replace(".json", "");
|
|
2925
|
+
return {
|
|
2926
|
+
name: file,
|
|
2927
|
+
path: path.join(historyDir, file),
|
|
2928
|
+
timestamp: parseInt(timestampPart, 10),
|
|
2929
|
+
};
|
|
2930
|
+
})
|
|
2931
|
+
.filter((file) => !isNaN(file.timestamp))
|
|
2932
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
2933
|
+
|
|
2934
|
+
const filesToLoadForTrend = jsonHistoryFiles.slice(
|
|
2935
|
+
0,
|
|
2936
|
+
MAX_HISTORY_FILES_TO_LOAD_FOR_REPORT
|
|
2937
|
+
);
|
|
2938
|
+
|
|
2939
|
+
for (const fileMeta of filesToLoadForTrend) {
|
|
2940
|
+
try {
|
|
2941
|
+
const fileContent = await fs.readFile(fileMeta.path, "utf-8");
|
|
2942
|
+
const runJsonData = JSON.parse(fileContent);
|
|
2943
|
+
historicalRuns.push(runJsonData);
|
|
2944
|
+
} catch (fileReadError) {
|
|
2945
|
+
console.warn(
|
|
2946
|
+
chalk.yellow(
|
|
2947
|
+
`Could not read/parse history file ${fileMeta.name}: ${fileReadError.message}`
|
|
2948
|
+
)
|
|
2949
|
+
);
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
historicalRuns.reverse(); // Oldest first for charts
|
|
2953
|
+
console.log(
|
|
2954
|
+
chalk.green(
|
|
2955
|
+
`Loaded ${historicalRuns.length} historical run(s) for trend analysis.`
|
|
2956
|
+
)
|
|
2957
|
+
);
|
|
2958
|
+
} catch (error) {
|
|
2959
|
+
if (error.code === "ENOENT") {
|
|
2960
|
+
console.warn(
|
|
2961
|
+
chalk.yellow(
|
|
2962
|
+
`History directory '${historyDir}' not found. No historical trends will be displayed.`
|
|
2963
|
+
)
|
|
2964
|
+
);
|
|
2965
|
+
} else {
|
|
2966
|
+
console.warn(
|
|
2967
|
+
chalk.yellow(
|
|
2968
|
+
`Error loading historical data from '${historyDir}': ${error.message}`
|
|
2969
|
+
)
|
|
2970
|
+
);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
// Step 4: Prepare trendData object
|
|
2975
|
+
const trendData = {
|
|
2976
|
+
overall: [],
|
|
2977
|
+
testRuns: {},
|
|
2978
|
+
};
|
|
2979
|
+
|
|
2980
|
+
if (historicalRuns.length > 0) {
|
|
2981
|
+
historicalRuns.forEach((histRunReport) => {
|
|
2982
|
+
if (histRunReport.run) {
|
|
2983
|
+
const runTimestamp = new Date(histRunReport.run.timestamp);
|
|
2984
|
+
trendData.overall.push({
|
|
2985
|
+
runId: runTimestamp.getTime(),
|
|
2986
|
+
timestamp: runTimestamp,
|
|
2987
|
+
duration: histRunReport.run.duration,
|
|
2988
|
+
totalTests: histRunReport.run.totalTests,
|
|
2989
|
+
passed: histRunReport.run.passed,
|
|
2990
|
+
failed: histRunReport.run.failed,
|
|
2991
|
+
skipped: histRunReport.run.skipped || 0,
|
|
2992
|
+
});
|
|
2993
|
+
|
|
2994
|
+
if (histRunReport.results && Array.isArray(histRunReport.results)) {
|
|
2995
|
+
const runKeyForTestHistory = `test run ${runTimestamp.getTime()}`;
|
|
2996
|
+
trendData.testRuns[runKeyForTestHistory] = histRunReport.results.map(
|
|
2997
|
+
(test) => ({
|
|
2998
|
+
testName: test.name,
|
|
2999
|
+
duration: test.duration,
|
|
3000
|
+
status: test.status,
|
|
3001
|
+
timestamp: new Date(test.startTime),
|
|
3002
|
+
})
|
|
3003
|
+
);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
});
|
|
3007
|
+
trendData.overall.sort(
|
|
3008
|
+
(a, b) => a.timestamp.getTime() - b.timestamp.getTime()
|
|
3009
|
+
);
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
// Step 5: Generate and write HTML
|
|
3013
|
+
try {
|
|
3014
|
+
const htmlContent = generateHTML(currentRunReportData, trendData);
|
|
3015
|
+
await fs.writeFile(reportHtmlPath, htmlContent, "utf-8");
|
|
3016
|
+
console.log(
|
|
3017
|
+
chalk.green.bold(
|
|
3018
|
+
`🎉 Pulse report generated successfully at: ${reportHtmlPath}`
|
|
3019
|
+
)
|
|
3020
|
+
);
|
|
3021
|
+
console.log(chalk.gray(`(You can open this file in your browser)`));
|
|
3022
|
+
} catch (error) {
|
|
3023
|
+
console.error(chalk.red(`Error generating HTML report: ${error.message}`));
|
|
3024
|
+
console.error(chalk.red(error.stack));
|
|
3025
|
+
process.exit(1);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
main().catch((err) => {
|
|
3029
|
+
console.error(
|
|
3030
|
+
chalk.red.bold(`Unhandled error during script execution: ${err.message}`)
|
|
3031
|
+
);
|
|
3032
|
+
console.error(err.stack);
|
|
3033
|
+
process.exit(1);
|
|
3034
|
+
});
|