@arghajit/playwright-pulse-report 0.1.6 → 0.2.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 +23 -6
- package/dist/reporter/playwright-pulse-reporter.js +148 -118
- package/dist/types/index.d.ts +3 -1
- package/package.json +5 -2
- 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
- package/scripts/generate-static-report.mjs +1848 -1262
- package/scripts/generate-trend.mjs +165 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
5
|
+
import { fork } from "child_process"; // Add this
|
|
6
|
+
import { fileURLToPath } from "url"; // Add this for resolving path in ESM
|
|
7
|
+
|
|
7
8
|
// Use dynamic import for chalk as it's ESM only
|
|
8
9
|
let chalk;
|
|
9
10
|
try {
|
|
@@ -27,33 +28,745 @@ const DEFAULT_HTML_FILE = "playwright-pulse-static-report.html";
|
|
|
27
28
|
|
|
28
29
|
// Helper functions
|
|
29
30
|
function sanitizeHTML(str) {
|
|
31
|
+
// User's provided version (note: this doesn't escape HTML special chars correctly)
|
|
30
32
|
if (str === null || str === undefined) return "";
|
|
31
33
|
return String(str)
|
|
32
|
-
.replace(/&/g, "&
|
|
33
|
-
.replace(/</g, "
|
|
34
|
-
.replace(/>/g, "
|
|
35
|
-
.replace(/"/g, "
|
|
36
|
-
.replace(/'/g, "
|
|
34
|
+
.replace(/&/g, "&")
|
|
35
|
+
.replace(/</g, "<")
|
|
36
|
+
.replace(/>/g, ">")
|
|
37
|
+
.replace(/"/g, `"`)
|
|
38
|
+
.replace(/'/g, "'");
|
|
39
|
+
}
|
|
40
|
+
function capitalize(str) {
|
|
41
|
+
if (!str) return ""; // Handle empty string
|
|
42
|
+
return str[0].toUpperCase() + str.slice(1).toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatPlaywrightError(error) {
|
|
46
|
+
// Get the error message and clean ANSI codes
|
|
47
|
+
const rawMessage = error.stack || error.message || error.toString();
|
|
48
|
+
const cleanMessage = rawMessage.replace(/\x1B\[[0-9;]*[mGKH]/g, "");
|
|
49
|
+
|
|
50
|
+
// Parse error components
|
|
51
|
+
const timeoutMatch = cleanMessage.match(/Timed out (\d+)ms waiting for/);
|
|
52
|
+
const assertionMatch = cleanMessage.match(/expect\((.*?)\)\.(.*?)\(/);
|
|
53
|
+
const expectedMatch = cleanMessage.match(
|
|
54
|
+
/Expected (?:pattern|string|regexp|value): (.*?)(?:\n|Call log|$)/s
|
|
55
|
+
);
|
|
56
|
+
const actualMatch = cleanMessage.match(
|
|
57
|
+
/Received (?:string|value): (.*?)(?:\n|Call log|$)/s
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// HTML escape function
|
|
61
|
+
const escapeHtml = (str) => {
|
|
62
|
+
if (!str) return "";
|
|
63
|
+
return str.replace(
|
|
64
|
+
/[&<>'"]/g,
|
|
65
|
+
(tag) =>
|
|
66
|
+
({
|
|
67
|
+
"&": "&",
|
|
68
|
+
"<": "<",
|
|
69
|
+
">": ">",
|
|
70
|
+
"'": "'",
|
|
71
|
+
'"': """,
|
|
72
|
+
}[tag])
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Build HTML output
|
|
77
|
+
let html = `<div class="playwright-error">
|
|
78
|
+
<div class="error-header">Test Error</div>`;
|
|
79
|
+
|
|
80
|
+
if (timeoutMatch) {
|
|
81
|
+
html += `<div class="error-timeout">⏱ Timeout: ${escapeHtml(
|
|
82
|
+
timeoutMatch[1]
|
|
83
|
+
)}ms</div>`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (assertionMatch) {
|
|
87
|
+
html += `<div class="error-assertion">🔍 Assertion: expect(${escapeHtml(
|
|
88
|
+
assertionMatch[1]
|
|
89
|
+
)}).${escapeHtml(assertionMatch[2])}()</div>`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (expectedMatch) {
|
|
93
|
+
html += `<div class="error-expected">✅ Expected: ${escapeHtml(
|
|
94
|
+
expectedMatch[1]
|
|
95
|
+
)}</div>`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (actualMatch) {
|
|
99
|
+
html += `<div class="error-actual">❌ Actual: ${escapeHtml(
|
|
100
|
+
actualMatch[1]
|
|
101
|
+
)}</div>`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Add call log if present
|
|
105
|
+
const callLogStart = cleanMessage.indexOf("Call log:");
|
|
106
|
+
if (callLogStart !== -1) {
|
|
107
|
+
const callLogEnd =
|
|
108
|
+
cleanMessage.indexOf("\n\n", callLogStart) || cleanMessage.length;
|
|
109
|
+
const callLogSection = cleanMessage
|
|
110
|
+
.slice(callLogStart + 9, callLogEnd)
|
|
111
|
+
.trim();
|
|
112
|
+
|
|
113
|
+
html += `<div class="error-call-log">
|
|
114
|
+
<div class="call-log-header">📜 Call Log:</div>
|
|
115
|
+
<ul class="call-log-items">${callLogSection
|
|
116
|
+
.split("\n")
|
|
117
|
+
.map((line) => line.trim())
|
|
118
|
+
.filter((line) => line)
|
|
119
|
+
.map((line) => `<li>${escapeHtml(line.replace(/^-\s*/, ""))}</li>`)
|
|
120
|
+
.join("")}</ul>
|
|
121
|
+
</div>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add stack trace if present
|
|
125
|
+
const stackTraceMatch = cleanMessage.match(/\n\s*at\s.*/gs);
|
|
126
|
+
if (stackTraceMatch) {
|
|
127
|
+
html += `<div class="error-stack-trace">
|
|
128
|
+
<div class="stack-trace-header">🔎 Stack Trace:</div>
|
|
129
|
+
<ul class="stack-trace-items">${stackTraceMatch[0]
|
|
130
|
+
.trim()
|
|
131
|
+
.split("\n")
|
|
132
|
+
.map((line) => line.trim())
|
|
133
|
+
.filter((line) => line)
|
|
134
|
+
.map((line) => `<li>${escapeHtml(line)}</li>`)
|
|
135
|
+
.join("")}</ul>
|
|
136
|
+
</div>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
html += `</div>`;
|
|
140
|
+
|
|
141
|
+
return html;
|
|
37
142
|
}
|
|
38
143
|
|
|
144
|
+
// User-provided formatDuration function
|
|
39
145
|
function formatDuration(ms) {
|
|
40
146
|
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
41
147
|
return (ms / 1000).toFixed(1) + "s";
|
|
42
148
|
}
|
|
43
149
|
|
|
150
|
+
function generateTestTrendsChart(trendData) {
|
|
151
|
+
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
152
|
+
return '<div class="no-data">No overall trend data available for test counts.</div>';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const chartId = `testTrendsChart-${Date.now()}-${Math.random()
|
|
156
|
+
.toString(36)
|
|
157
|
+
.substring(2, 7)}`;
|
|
158
|
+
const runs = trendData.overall;
|
|
159
|
+
|
|
160
|
+
const series = [
|
|
161
|
+
{
|
|
162
|
+
name: "Total",
|
|
163
|
+
data: runs.map((r) => r.totalTests),
|
|
164
|
+
color: "var(--primary-color)", // Blue
|
|
165
|
+
marker: { symbol: "circle" },
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "Passed",
|
|
169
|
+
data: runs.map((r) => r.passed),
|
|
170
|
+
color: "var(--success-color)", // Green
|
|
171
|
+
marker: { symbol: "circle" },
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "Failed",
|
|
175
|
+
data: runs.map((r) => r.failed),
|
|
176
|
+
color: "var(--danger-color)", // Red
|
|
177
|
+
marker: { symbol: "circle" },
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "Skipped",
|
|
181
|
+
data: runs.map((r) => r.skipped || 0),
|
|
182
|
+
color: "var(--warning-color)", // Yellow
|
|
183
|
+
marker: { symbol: "circle" },
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
// Data needed by the tooltip formatter, stringified to be embedded in the client-side script
|
|
188
|
+
const runsForTooltip = runs.map((r) => ({
|
|
189
|
+
runId: r.runId,
|
|
190
|
+
timestamp: r.timestamp,
|
|
191
|
+
duration: r.duration,
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
const optionsObjectString = `
|
|
195
|
+
{
|
|
196
|
+
chart: { type: "line", height: 350, backgroundColor: "transparent" },
|
|
197
|
+
title: { text: null },
|
|
198
|
+
xAxis: {
|
|
199
|
+
categories: ${JSON.stringify(runs.map((run, i) => `Run ${i + 1}`))},
|
|
200
|
+
crosshair: true,
|
|
201
|
+
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}
|
|
202
|
+
},
|
|
203
|
+
yAxis: {
|
|
204
|
+
title: { text: "Test Count", style: { color: 'var(--text-color)'} },
|
|
205
|
+
min: 0,
|
|
206
|
+
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}
|
|
207
|
+
},
|
|
208
|
+
legend: {
|
|
209
|
+
layout: "horizontal", align: "center", verticalAlign: "bottom",
|
|
210
|
+
itemStyle: { fontSize: "12px", color: 'var(--text-color)' }
|
|
211
|
+
},
|
|
212
|
+
plotOptions: {
|
|
213
|
+
series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}},
|
|
214
|
+
line: { lineWidth: 2.5 } // fillOpacity was 0.1, but for line charts, area fill is usually separate (area chart type)
|
|
215
|
+
},
|
|
216
|
+
tooltip: {
|
|
217
|
+
shared: true, useHTML: true,
|
|
218
|
+
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
219
|
+
borderColor: 'rgba(10,10,10,0.92)',
|
|
220
|
+
style: { color: '#f5f5f5' },
|
|
221
|
+
formatter: function () {
|
|
222
|
+
const runsData = ${JSON.stringify(runsForTooltip)};
|
|
223
|
+
const pointIndex = this.points[0].point.x; // Get index from point
|
|
224
|
+
const run = runsData[pointIndex];
|
|
225
|
+
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' +
|
|
226
|
+
'Date: ' + new Date(run.timestamp).toLocaleString() + '<br><br>';
|
|
227
|
+
this.points.forEach(point => {
|
|
228
|
+
tooltip += '<span style="color:' + point.color + '">●</span> ' + point.series.name + ': <b>' + point.y + '</b><br>';
|
|
229
|
+
});
|
|
230
|
+
tooltip += '<br>Duration: ' + formatDuration(run.duration);
|
|
231
|
+
return tooltip;
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
series: ${JSON.stringify(series)},
|
|
235
|
+
credits: { enabled: false }
|
|
236
|
+
}
|
|
237
|
+
`;
|
|
238
|
+
|
|
239
|
+
return `
|
|
240
|
+
<div id="${chartId}" class="trend-chart-container"></div>
|
|
241
|
+
<script>
|
|
242
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
243
|
+
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
244
|
+
try {
|
|
245
|
+
const chartOptions = ${optionsObjectString};
|
|
246
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
console.error("Error rendering chart ${chartId}:", e);
|
|
249
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data">Error rendering test trends chart.</div>';
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data">Charting library not available.</div>';
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
</script>
|
|
256
|
+
`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function generateDurationTrendChart(trendData) {
|
|
260
|
+
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
261
|
+
return '<div class="no-data">No overall trend data available for durations.</div>';
|
|
262
|
+
}
|
|
263
|
+
const chartId = `durationTrendChart-${Date.now()}-${Math.random()
|
|
264
|
+
.toString(36)
|
|
265
|
+
.substring(2, 7)}`;
|
|
266
|
+
const runs = trendData.overall;
|
|
267
|
+
|
|
268
|
+
// Assuming var(--accent-color-alt) is Orange #FF9800
|
|
269
|
+
const accentColorAltRGB = "255, 152, 0";
|
|
270
|
+
|
|
271
|
+
const seriesString = `[{
|
|
272
|
+
name: 'Duration',
|
|
273
|
+
data: ${JSON.stringify(runs.map((run) => run.duration))},
|
|
274
|
+
color: 'var(--accent-color-alt)',
|
|
275
|
+
type: 'area',
|
|
276
|
+
marker: {
|
|
277
|
+
symbol: 'circle', enabled: true, radius: 4,
|
|
278
|
+
states: { hover: { radius: 6, lineWidthPlus: 0 } }
|
|
279
|
+
},
|
|
280
|
+
fillColor: {
|
|
281
|
+
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
282
|
+
stops: [
|
|
283
|
+
[0, 'rgba(${accentColorAltRGB}, 0.4)'],
|
|
284
|
+
[1, 'rgba(${accentColorAltRGB}, 0.05)']
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
lineWidth: 2.5
|
|
288
|
+
}]`;
|
|
289
|
+
|
|
290
|
+
const runsForTooltip = runs.map((r) => ({
|
|
291
|
+
runId: r.runId,
|
|
292
|
+
timestamp: r.timestamp,
|
|
293
|
+
duration: r.duration,
|
|
294
|
+
totalTests: r.totalTests,
|
|
295
|
+
}));
|
|
296
|
+
|
|
297
|
+
const optionsObjectString = `
|
|
298
|
+
{
|
|
299
|
+
chart: { type: 'area', height: 350, backgroundColor: 'transparent' },
|
|
300
|
+
title: { text: null },
|
|
301
|
+
xAxis: {
|
|
302
|
+
categories: ${JSON.stringify(runs.map((run, i) => `Run ${i + 1}`))},
|
|
303
|
+
crosshair: true,
|
|
304
|
+
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' } }
|
|
305
|
+
},
|
|
306
|
+
yAxis: {
|
|
307
|
+
title: { text: 'Duration', style: { color: 'var(--text-color)' } },
|
|
308
|
+
labels: {
|
|
309
|
+
formatter: function() { return formatDuration(this.value); },
|
|
310
|
+
style: { color: 'var(--text-color-secondary)', fontSize: '12px' }
|
|
311
|
+
},
|
|
312
|
+
min: 0
|
|
313
|
+
},
|
|
314
|
+
legend: {
|
|
315
|
+
layout: 'horizontal', align: 'center', verticalAlign: 'bottom',
|
|
316
|
+
itemStyle: { fontSize: '12px', color: 'var(--text-color)' }
|
|
317
|
+
},
|
|
318
|
+
plotOptions: {
|
|
319
|
+
area: {
|
|
320
|
+
lineWidth: 2.5,
|
|
321
|
+
states: { hover: { lineWidthPlus: 0 } },
|
|
322
|
+
threshold: null
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
tooltip: {
|
|
326
|
+
shared: true, useHTML: true,
|
|
327
|
+
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
328
|
+
borderColor: 'rgba(10,10,10,0.92)',
|
|
329
|
+
style: { color: '#f5f5f5' },
|
|
330
|
+
formatter: function () {
|
|
331
|
+
const runsData = ${JSON.stringify(runsForTooltip)};
|
|
332
|
+
const pointIndex = this.points[0].point.x;
|
|
333
|
+
const run = runsData[pointIndex];
|
|
334
|
+
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' +
|
|
335
|
+
'Date: ' + new Date(run.timestamp).toLocaleString() + '<br>';
|
|
336
|
+
this.points.forEach(point => {
|
|
337
|
+
tooltip += '<span style="color:' + point.series.color + '">●</span> ' +
|
|
338
|
+
point.series.name + ': <b>' + formatDuration(point.y) + '</b><br>';
|
|
339
|
+
});
|
|
340
|
+
tooltip += '<br>Tests: ' + run.totalTests;
|
|
341
|
+
return tooltip;
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
series: ${seriesString},
|
|
345
|
+
credits: { enabled: false }
|
|
346
|
+
}
|
|
347
|
+
`;
|
|
348
|
+
|
|
349
|
+
return `
|
|
350
|
+
<div id="${chartId}" class="trend-chart-container"></div>
|
|
351
|
+
<script>
|
|
352
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
353
|
+
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
354
|
+
try {
|
|
355
|
+
const chartOptions = ${optionsObjectString};
|
|
356
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
357
|
+
} catch (e) {
|
|
358
|
+
console.error("Error rendering chart ${chartId}:", e);
|
|
359
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data">Error rendering duration trend chart.</div>';
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data">Charting library not available.</div>';
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
</script>
|
|
366
|
+
`;
|
|
367
|
+
}
|
|
368
|
+
|
|
44
369
|
function formatDate(dateStrOrDate) {
|
|
45
370
|
if (!dateStrOrDate) return "N/A";
|
|
46
371
|
try {
|
|
47
372
|
const date = new Date(dateStrOrDate);
|
|
48
373
|
if (isNaN(date.getTime())) return "Invalid Date";
|
|
49
|
-
return
|
|
374
|
+
return (
|
|
375
|
+
date.toLocaleDateString(undefined, {
|
|
376
|
+
year: "2-digit",
|
|
377
|
+
month: "2-digit",
|
|
378
|
+
day: "2-digit",
|
|
379
|
+
}) +
|
|
380
|
+
" " +
|
|
381
|
+
date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
|
|
382
|
+
);
|
|
50
383
|
} catch (e) {
|
|
51
|
-
return "Invalid Date";
|
|
384
|
+
return "Invalid Date Format";
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function generateTestHistoryChart(history) {
|
|
389
|
+
if (!history || history.length === 0)
|
|
390
|
+
return '<div class="no-data-chart">No data for chart</div>';
|
|
391
|
+
|
|
392
|
+
const validHistory = history.filter(
|
|
393
|
+
(h) => h && typeof h.duration === "number" && h.duration >= 0
|
|
394
|
+
);
|
|
395
|
+
if (validHistory.length === 0)
|
|
396
|
+
return '<div class="no-data-chart">No valid data for chart</div>';
|
|
397
|
+
|
|
398
|
+
const chartId = `testHistoryChart-${Date.now()}-${Math.random()
|
|
399
|
+
.toString(36)
|
|
400
|
+
.substring(2, 7)}`;
|
|
401
|
+
|
|
402
|
+
const seriesDataPoints = validHistory.map((run) => {
|
|
403
|
+
let color;
|
|
404
|
+
switch (String(run.status).toLowerCase()) {
|
|
405
|
+
case "passed":
|
|
406
|
+
color = "var(--success-color)";
|
|
407
|
+
break;
|
|
408
|
+
case "failed":
|
|
409
|
+
color = "var(--danger-color)";
|
|
410
|
+
break;
|
|
411
|
+
case "skipped":
|
|
412
|
+
color = "var(--warning-color)";
|
|
413
|
+
break;
|
|
414
|
+
default:
|
|
415
|
+
color = "var(--dark-gray-color)";
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
y: run.duration,
|
|
419
|
+
marker: {
|
|
420
|
+
fillColor: color,
|
|
421
|
+
symbol: "circle",
|
|
422
|
+
radius: 3.5,
|
|
423
|
+
states: { hover: { radius: 5 } },
|
|
424
|
+
},
|
|
425
|
+
status: run.status,
|
|
426
|
+
runId: run.runId,
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Assuming var(--accent-color) is Deep Purple #673ab7 -> RGB 103, 58, 183
|
|
431
|
+
const accentColorRGB = "103, 58, 183";
|
|
432
|
+
|
|
433
|
+
const optionsObjectString = `
|
|
434
|
+
{
|
|
435
|
+
chart: { type: 'area', height: 100, width: 320, backgroundColor: 'transparent', spacing: [10,10,15,35] },
|
|
436
|
+
title: { text: null },
|
|
437
|
+
xAxis: {
|
|
438
|
+
categories: ${JSON.stringify(
|
|
439
|
+
validHistory.map((_, i) => `R${i + 1}`)
|
|
440
|
+
)},
|
|
441
|
+
labels: { style: { fontSize: '10px', color: 'var(--text-color-secondary)' } }
|
|
442
|
+
},
|
|
443
|
+
yAxis: {
|
|
444
|
+
title: { text: null },
|
|
445
|
+
labels: {
|
|
446
|
+
formatter: function() { return formatDuration(this.value); },
|
|
447
|
+
style: { fontSize: '10px', color: 'var(--text-color-secondary)' },
|
|
448
|
+
align: 'left', x: -35, y: 3
|
|
449
|
+
},
|
|
450
|
+
min: 0,
|
|
451
|
+
gridLineWidth: 0,
|
|
452
|
+
tickAmount: 4
|
|
453
|
+
},
|
|
454
|
+
legend: { enabled: false },
|
|
455
|
+
plotOptions: {
|
|
456
|
+
area: {
|
|
457
|
+
lineWidth: 2,
|
|
458
|
+
lineColor: 'var(--accent-color)',
|
|
459
|
+
fillColor: {
|
|
460
|
+
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
461
|
+
stops: [
|
|
462
|
+
[0, 'rgba(${accentColorRGB}, 0.4)'],
|
|
463
|
+
[1, 'rgba(${accentColorRGB}, 0)']
|
|
464
|
+
]
|
|
465
|
+
},
|
|
466
|
+
marker: { enabled: true },
|
|
467
|
+
threshold: null
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
tooltip: {
|
|
471
|
+
useHTML: true,
|
|
472
|
+
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
473
|
+
borderColor: 'rgba(10,10,10,0.92)',
|
|
474
|
+
style: { color: '#f5f5f5', padding: '8px' },
|
|
475
|
+
formatter: function() {
|
|
476
|
+
const pointData = this.point;
|
|
477
|
+
let statusBadgeHtml = '<span style="padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; background-color: ';
|
|
478
|
+
switch(String(pointData.status).toLowerCase()) {
|
|
479
|
+
case 'passed': statusBadgeHtml += 'var(--success-color)'; break;
|
|
480
|
+
case 'failed': statusBadgeHtml += 'var(--danger-color)'; break;
|
|
481
|
+
case 'skipped': statusBadgeHtml += 'var(--warning-color)'; break;
|
|
482
|
+
default: statusBadgeHtml += 'var(--dark-gray-color)';
|
|
483
|
+
}
|
|
484
|
+
statusBadgeHtml += ';">' + String(pointData.status).toUpperCase() + '</span>';
|
|
485
|
+
|
|
486
|
+
return '<strong>Run ' + (pointData.runId || (this.point.index + 1)) + '</strong><br>' +
|
|
487
|
+
'Status: ' + statusBadgeHtml + '<br>' +
|
|
488
|
+
'Duration: ' + formatDuration(pointData.y);
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
series: [{
|
|
492
|
+
data: ${JSON.stringify(seriesDataPoints)},
|
|
493
|
+
showInLegend: false
|
|
494
|
+
}],
|
|
495
|
+
credits: { enabled: false }
|
|
496
|
+
}
|
|
497
|
+
`;
|
|
498
|
+
return `
|
|
499
|
+
<div id="${chartId}" style="width: 320px; height: 100px;"></div>
|
|
500
|
+
<script>
|
|
501
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
502
|
+
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
503
|
+
try {
|
|
504
|
+
const chartOptions = ${optionsObjectString};
|
|
505
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
506
|
+
} catch (e) {
|
|
507
|
+
console.error("Error rendering chart ${chartId}:", e);
|
|
508
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data-chart">Error rendering history chart.</div>';
|
|
509
|
+
}
|
|
510
|
+
} else {
|
|
511
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data-chart">Charting library not available.</div>';
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
</script>
|
|
515
|
+
`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
519
|
+
const total = data.reduce((sum, d) => sum + d.value, 0);
|
|
520
|
+
if (total === 0) {
|
|
521
|
+
return '<div class="pie-chart-wrapper"><h3>Test Distribution</h3><div class="no-data">No data for Test Distribution chart.</div></div>';
|
|
522
|
+
}
|
|
523
|
+
const passedEntry = data.find((d) => d.label === "Passed");
|
|
524
|
+
const passedPercentage = Math.round(
|
|
525
|
+
((passedEntry ? passedEntry.value : 0) / total) * 100
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const chartId = `pieChart-${Date.now()}-${Math.random()
|
|
529
|
+
.toString(36)
|
|
530
|
+
.substring(2, 7)}`;
|
|
531
|
+
|
|
532
|
+
const seriesData = [
|
|
533
|
+
{
|
|
534
|
+
name: "Tests", // Changed from 'Test Distribution' for tooltip clarity
|
|
535
|
+
data: data
|
|
536
|
+
.filter((d) => d.value > 0)
|
|
537
|
+
.map((d) => {
|
|
538
|
+
let color;
|
|
539
|
+
switch (d.label) {
|
|
540
|
+
case "Passed":
|
|
541
|
+
color = "var(--success-color)";
|
|
542
|
+
break;
|
|
543
|
+
case "Failed":
|
|
544
|
+
color = "var(--danger-color)";
|
|
545
|
+
break;
|
|
546
|
+
case "Skipped":
|
|
547
|
+
color = "var(--warning-color)";
|
|
548
|
+
break;
|
|
549
|
+
default:
|
|
550
|
+
color = "#CCCCCC"; // A neutral default color
|
|
551
|
+
}
|
|
552
|
+
return { name: d.label, y: d.value, color: color };
|
|
553
|
+
}),
|
|
554
|
+
size: "100%",
|
|
555
|
+
innerSize: "55%",
|
|
556
|
+
dataLabels: { enabled: false },
|
|
557
|
+
showInLegend: true,
|
|
558
|
+
},
|
|
559
|
+
];
|
|
560
|
+
|
|
561
|
+
// Approximate font size for center text, can be adjusted or made dynamic with more client-side JS
|
|
562
|
+
const centerTitleFontSize =
|
|
563
|
+
Math.max(12, Math.min(chartWidth, chartHeight) / 12) + "px";
|
|
564
|
+
const centerSubtitleFontSize =
|
|
565
|
+
Math.max(10, Math.min(chartWidth, chartHeight) / 18) + "px";
|
|
566
|
+
|
|
567
|
+
const optionsObjectString = `
|
|
568
|
+
{
|
|
569
|
+
chart: {
|
|
570
|
+
type: 'pie',
|
|
571
|
+
width: ${chartWidth},
|
|
572
|
+
height: ${
|
|
573
|
+
chartHeight - 40
|
|
574
|
+
}, // Adjusted height to make space for legend if chartHeight is for the whole wrapper
|
|
575
|
+
backgroundColor: 'transparent',
|
|
576
|
+
plotShadow: false,
|
|
577
|
+
spacingBottom: 40 // Ensure space for legend
|
|
578
|
+
},
|
|
579
|
+
title: {
|
|
580
|
+
text: '${passedPercentage}%',
|
|
581
|
+
align: 'center',
|
|
582
|
+
verticalAlign: 'middle',
|
|
583
|
+
y: 5,
|
|
584
|
+
style: { fontSize: '${centerTitleFontSize}', fontWeight: 'bold', color: 'var(--primary-color)' }
|
|
585
|
+
},
|
|
586
|
+
subtitle: {
|
|
587
|
+
text: 'Passed',
|
|
588
|
+
align: 'center',
|
|
589
|
+
verticalAlign: 'middle',
|
|
590
|
+
y: 25,
|
|
591
|
+
style: { fontSize: '${centerSubtitleFontSize}', color: 'var(--text-color-secondary)' }
|
|
592
|
+
},
|
|
593
|
+
tooltip: {
|
|
594
|
+
pointFormat: '{series.name}: <b>{point.percentage:.1f}%</b> ({point.y})',
|
|
595
|
+
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
596
|
+
borderColor: 'rgba(10,10,10,0.92)',
|
|
597
|
+
style: { color: '#f5f5f5' }
|
|
598
|
+
},
|
|
599
|
+
legend: {
|
|
600
|
+
layout: 'horizontal',
|
|
601
|
+
align: 'center',
|
|
602
|
+
verticalAlign: 'bottom',
|
|
603
|
+
itemStyle: { color: 'var(--text-color)', fontWeight: 'normal', fontSize: '12px' }
|
|
604
|
+
},
|
|
605
|
+
plotOptions: {
|
|
606
|
+
pie: {
|
|
607
|
+
allowPointSelect: true,
|
|
608
|
+
cursor: 'pointer',
|
|
609
|
+
borderWidth: 3,
|
|
610
|
+
borderColor: 'var(--card-background-color)', // Match D3 style
|
|
611
|
+
states: {
|
|
612
|
+
hover: {
|
|
613
|
+
// Using default Highcharts halo which is generally good
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
series: ${JSON.stringify(seriesData)},
|
|
619
|
+
credits: { enabled: false }
|
|
620
|
+
}
|
|
621
|
+
`;
|
|
622
|
+
|
|
623
|
+
return `
|
|
624
|
+
<div class="pie-chart-wrapper" style="align-items: center">
|
|
625
|
+
<div style="display: flex; align-items: start; width: 100%;"><h3>Test Distribution</h3></div>
|
|
626
|
+
<div id="${chartId}" style="width: ${chartWidth}px; height: ${
|
|
627
|
+
chartHeight - 40
|
|
628
|
+
}px;"></div>
|
|
629
|
+
<script>
|
|
630
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
631
|
+
if (typeof Highcharts !== 'undefined') {
|
|
632
|
+
try {
|
|
633
|
+
const chartOptions = ${optionsObjectString};
|
|
634
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
635
|
+
} catch (e) {
|
|
636
|
+
console.error("Error rendering chart ${chartId}:", e);
|
|
637
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data">Error rendering pie chart.</div>';
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data">Charting library not available.</div>';
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
</script>
|
|
644
|
+
</div>
|
|
645
|
+
`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function generateTestHistoryContent(trendData) {
|
|
649
|
+
if (
|
|
650
|
+
!trendData ||
|
|
651
|
+
!trendData.testRuns ||
|
|
652
|
+
Object.keys(trendData.testRuns).length === 0
|
|
653
|
+
) {
|
|
654
|
+
return '<div class="no-data">No historical test data available.</div>';
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const allTestNamesAndPaths = new Map();
|
|
658
|
+
Object.values(trendData.testRuns).forEach((run) => {
|
|
659
|
+
if (Array.isArray(run)) {
|
|
660
|
+
run.forEach((test) => {
|
|
661
|
+
if (test && test.testName && !allTestNamesAndPaths.has(test.testName)) {
|
|
662
|
+
const parts = test.testName.split(" > ");
|
|
663
|
+
const title = parts[parts.length - 1];
|
|
664
|
+
allTestNamesAndPaths.set(test.testName, title);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
if (allTestNamesAndPaths.size === 0) {
|
|
671
|
+
return '<div class="no-data">No historical test data found after processing.</div>';
|
|
52
672
|
}
|
|
673
|
+
|
|
674
|
+
const testHistory = Array.from(allTestNamesAndPaths.entries())
|
|
675
|
+
.map(([fullTestName, testTitle]) => {
|
|
676
|
+
const history = [];
|
|
677
|
+
(trendData.overall || []).forEach((overallRun, index) => {
|
|
678
|
+
const runKey = overallRun.runId
|
|
679
|
+
? `test run ${overallRun.runId}`
|
|
680
|
+
: `test run ${index + 1}`;
|
|
681
|
+
const testRunForThisOverallRun = trendData.testRuns[runKey]?.find(
|
|
682
|
+
(t) => t && t.testName === fullTestName
|
|
683
|
+
);
|
|
684
|
+
if (testRunForThisOverallRun) {
|
|
685
|
+
history.push({
|
|
686
|
+
runId: overallRun.runId || index + 1,
|
|
687
|
+
status: testRunForThisOverallRun.status || "unknown",
|
|
688
|
+
duration: testRunForThisOverallRun.duration || 0,
|
|
689
|
+
timestamp:
|
|
690
|
+
testRunForThisOverallRun.timestamp ||
|
|
691
|
+
overallRun.timestamp ||
|
|
692
|
+
new Date(),
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
return { fullTestName, testTitle, history };
|
|
697
|
+
})
|
|
698
|
+
.filter((item) => item.history.length > 0);
|
|
699
|
+
|
|
700
|
+
return `
|
|
701
|
+
<div class="test-history-container">
|
|
702
|
+
<div class="filters" style="border-color: black; border-style: groove;">
|
|
703
|
+
<input type="text" id="history-filter-name" placeholder="Search by test title..." style="border-color: black; border-style: outset;">
|
|
704
|
+
<select id="history-filter-status">
|
|
705
|
+
<option value="">All Statuses</option>
|
|
706
|
+
<option value="passed">Passed</option>
|
|
707
|
+
<option value="failed">Failed</option>
|
|
708
|
+
<option value="skipped">Skipped</option>
|
|
709
|
+
</select>
|
|
710
|
+
<button id="clear-history-filters" class="clear-filters-btn">Clear Filters</button>
|
|
711
|
+
</div>
|
|
712
|
+
|
|
713
|
+
<div class="test-history-grid">
|
|
714
|
+
${testHistory
|
|
715
|
+
.map((test) => {
|
|
716
|
+
const latestRun =
|
|
717
|
+
test.history.length > 0
|
|
718
|
+
? test.history[test.history.length - 1]
|
|
719
|
+
: { status: "unknown" };
|
|
720
|
+
return `
|
|
721
|
+
<div class="test-history-card" data-test-name="${sanitizeHTML(
|
|
722
|
+
test.testTitle.toLowerCase()
|
|
723
|
+
)}" data-latest-status="${latestRun.status}">
|
|
724
|
+
<div class="test-history-header">
|
|
725
|
+
<p title="${sanitizeHTML(test.testTitle)}">${capitalize(
|
|
726
|
+
sanitizeHTML(test.testTitle)
|
|
727
|
+
)}</p>
|
|
728
|
+
<span class="status-badge ${getStatusClass(latestRun.status)}">
|
|
729
|
+
${String(latestRun.status).toUpperCase()}
|
|
730
|
+
</span>
|
|
731
|
+
</div>
|
|
732
|
+
<div class="test-history-trend">
|
|
733
|
+
${generateTestHistoryChart(test.history)}
|
|
734
|
+
</div>
|
|
735
|
+
<details class="test-history-details-collapsible">
|
|
736
|
+
<summary>Show Run Details (${test.history.length})</summary>
|
|
737
|
+
<div class="test-history-details">
|
|
738
|
+
<table>
|
|
739
|
+
<thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Date</th></tr></thead>
|
|
740
|
+
<tbody>
|
|
741
|
+
${test.history
|
|
742
|
+
.slice()
|
|
743
|
+
.reverse()
|
|
744
|
+
.map(
|
|
745
|
+
(run) => `
|
|
746
|
+
<tr>
|
|
747
|
+
<td>${run.runId}</td>
|
|
748
|
+
<td><span class="status-badge-small ${getStatusClass(
|
|
749
|
+
run.status
|
|
750
|
+
)}">${String(run.status).toUpperCase()}</span></td>
|
|
751
|
+
<td>${formatDuration(run.duration)}</td>
|
|
752
|
+
<td>${formatDate(run.timestamp)}</td>
|
|
753
|
+
</tr>`
|
|
754
|
+
)
|
|
755
|
+
.join("")}
|
|
756
|
+
</tbody>
|
|
757
|
+
</table>
|
|
758
|
+
</div>
|
|
759
|
+
</details>
|
|
760
|
+
</div>`;
|
|
761
|
+
})
|
|
762
|
+
.join("")}
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
`;
|
|
53
766
|
}
|
|
54
767
|
|
|
55
768
|
function getStatusClass(status) {
|
|
56
|
-
switch (status) {
|
|
769
|
+
switch (String(status).toLowerCase()) {
|
|
57
770
|
case "passed":
|
|
58
771
|
return "status-passed";
|
|
59
772
|
case "failed":
|
|
@@ -61,12 +774,12 @@ function getStatusClass(status) {
|
|
|
61
774
|
case "skipped":
|
|
62
775
|
return "status-skipped";
|
|
63
776
|
default:
|
|
64
|
-
return "";
|
|
777
|
+
return "status-unknown";
|
|
65
778
|
}
|
|
66
779
|
}
|
|
67
780
|
|
|
68
781
|
function getStatusIcon(status) {
|
|
69
|
-
switch (status) {
|
|
782
|
+
switch (String(status).toLowerCase()) {
|
|
70
783
|
case "passed":
|
|
71
784
|
return "✅";
|
|
72
785
|
case "failed":
|
|
@@ -78,1146 +791,815 @@ function getStatusIcon(status) {
|
|
|
78
791
|
}
|
|
79
792
|
}
|
|
80
793
|
|
|
81
|
-
function generatePieChartD3(data, width = 300, height = 300) {
|
|
82
|
-
const { document } = new JSDOM().window;
|
|
83
|
-
const body = d3.select(document.body);
|
|
84
|
-
|
|
85
|
-
// Calculate passed percentage
|
|
86
|
-
const total = data.reduce((sum, d) => sum + d.value, 0);
|
|
87
|
-
const passedPercentage =
|
|
88
|
-
total > 0
|
|
89
|
-
? Math.round(
|
|
90
|
-
((data.find((d) => d.label === "Passed")?.value || 0) / total) * 100
|
|
91
|
-
)
|
|
92
|
-
: 0;
|
|
93
|
-
|
|
94
|
-
// Chart dimensions
|
|
95
|
-
const radius = Math.min(width, height) / 2 - 50; // Reduced radius for legend space
|
|
96
|
-
const legendRectSize = 15;
|
|
97
|
-
const legendSpacing = 8;
|
|
98
|
-
|
|
99
|
-
// Pie generator
|
|
100
|
-
const pie = d3
|
|
101
|
-
.pie()
|
|
102
|
-
.value((d) => d.value)
|
|
103
|
-
.sort(null);
|
|
104
|
-
const arc = d3.arc().innerRadius(0).outerRadius(radius);
|
|
105
|
-
|
|
106
|
-
// Colors
|
|
107
|
-
const color = d3
|
|
108
|
-
.scaleOrdinal()
|
|
109
|
-
.domain(data.map((d) => d.label))
|
|
110
|
-
.range(["#4CAF50", "#F44336", "#FFC107"]);
|
|
111
|
-
|
|
112
|
-
// Create SVG with more width for legend
|
|
113
|
-
const svg = body
|
|
114
|
-
.append("svg")
|
|
115
|
-
.attr("width", width + 100) // Extra width for legend
|
|
116
|
-
.attr("height", height)
|
|
117
|
-
.append("g")
|
|
118
|
-
.attr("transform", `translate(${width / 2},${height / 2})`);
|
|
119
|
-
|
|
120
|
-
// Tooltip setup
|
|
121
|
-
const tooltip = body
|
|
122
|
-
.append("div")
|
|
123
|
-
.style("opacity", 0)
|
|
124
|
-
.style("position", "absolute")
|
|
125
|
-
.style("background", "white")
|
|
126
|
-
.style("padding", "5px 10px")
|
|
127
|
-
.style("border-radius", "4px")
|
|
128
|
-
.style("box-shadow", "0 2px 5px rgba(0,0,0,0.1)");
|
|
129
|
-
|
|
130
|
-
// Draw pie slices
|
|
131
|
-
const arcs = svg
|
|
132
|
-
.selectAll(".arc")
|
|
133
|
-
.data(pie(data))
|
|
134
|
-
.enter()
|
|
135
|
-
.append("g")
|
|
136
|
-
.attr("class", "arc");
|
|
137
|
-
|
|
138
|
-
arcs
|
|
139
|
-
.append("path")
|
|
140
|
-
.attr("d", arc)
|
|
141
|
-
.attr("fill", (d) => color(d.data.label))
|
|
142
|
-
.style("stroke", "#fff")
|
|
143
|
-
.style("stroke-width", 2)
|
|
144
|
-
.on("mouseover", function (event, d) {
|
|
145
|
-
tooltip.transition().style("opacity", 1);
|
|
146
|
-
tooltip
|
|
147
|
-
.html(
|
|
148
|
-
`${d.data.label}: ${d.data.value} (${Math.round(
|
|
149
|
-
(d.data.value / total) * 100
|
|
150
|
-
)}%)`
|
|
151
|
-
)
|
|
152
|
-
.style("left", event.pageX + 10 + "px")
|
|
153
|
-
.style("top", event.pageY - 28 + "px");
|
|
154
|
-
})
|
|
155
|
-
.on("mouseout", () => tooltip.transition().style("opacity", 0));
|
|
156
|
-
|
|
157
|
-
// Center percentage
|
|
158
|
-
svg
|
|
159
|
-
.append("text")
|
|
160
|
-
.attr("text-anchor", "middle")
|
|
161
|
-
.attr("dy", ".3em")
|
|
162
|
-
.style("font-size", "24px")
|
|
163
|
-
.style("font-weight", "bold")
|
|
164
|
-
.text(`${passedPercentage}%`);
|
|
165
|
-
|
|
166
|
-
// Legend - positioned to the right
|
|
167
|
-
const legend = svg
|
|
168
|
-
.selectAll(".legend")
|
|
169
|
-
.data(color.domain())
|
|
170
|
-
.enter()
|
|
171
|
-
.append("g")
|
|
172
|
-
.attr("class", "legend")
|
|
173
|
-
.attr(
|
|
174
|
-
"transform",
|
|
175
|
-
(d, i) =>
|
|
176
|
-
`translate(${radius + 20},${i * (legendRectSize + legendSpacing) - 40})`
|
|
177
|
-
); // Moved right
|
|
178
|
-
|
|
179
|
-
legend
|
|
180
|
-
.append("rect")
|
|
181
|
-
.attr("width", legendRectSize)
|
|
182
|
-
.attr("height", legendRectSize)
|
|
183
|
-
.style("fill", color)
|
|
184
|
-
.style("stroke", color);
|
|
185
|
-
|
|
186
|
-
legend
|
|
187
|
-
.append("text")
|
|
188
|
-
.attr("x", legendRectSize + 5)
|
|
189
|
-
.attr("y", legendRectSize - 2)
|
|
190
|
-
.text((d) => d)
|
|
191
|
-
.style("font-size", "12px")
|
|
192
|
-
.style("text-anchor", "start");
|
|
193
|
-
|
|
194
|
-
return `
|
|
195
|
-
<div class="pie-chart-container">
|
|
196
|
-
<h3>Test Distribution Chart</h3>
|
|
197
|
-
${body.html()}
|
|
198
|
-
<style>
|
|
199
|
-
.pie-chart-container {
|
|
200
|
-
display: flex;
|
|
201
|
-
justify-content: center;
|
|
202
|
-
margin: 20px 0;
|
|
203
|
-
}
|
|
204
|
-
.pie-chart-container svg {
|
|
205
|
-
display: block;
|
|
206
|
-
margin: 0 auto;
|
|
207
|
-
}
|
|
208
|
-
.pie-chart-container h3 {
|
|
209
|
-
text-align: center;
|
|
210
|
-
margin: 0 0 10px;
|
|
211
|
-
font-size: 16px;
|
|
212
|
-
color: var(--text-color);
|
|
213
|
-
}
|
|
214
|
-
</style>
|
|
215
|
-
</div>
|
|
216
|
-
`;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Process the JSON data to extract suites information
|
|
220
794
|
function getSuitesData(results) {
|
|
221
795
|
const suitesMap = new Map();
|
|
796
|
+
if (!results || results.length === 0) return [];
|
|
222
797
|
|
|
223
798
|
results.forEach((test) => {
|
|
224
|
-
const browser = test.browser
|
|
225
|
-
const
|
|
799
|
+
const browser = test.browser || "unknown";
|
|
800
|
+
const suiteParts = test.name.split(" > ");
|
|
801
|
+
let suiteNameCandidate = "Default Suite";
|
|
802
|
+
if (suiteParts.length > 2) {
|
|
803
|
+
suiteNameCandidate = suiteParts[1];
|
|
804
|
+
} else if (suiteParts.length > 1) {
|
|
805
|
+
suiteNameCandidate = suiteParts[0]
|
|
806
|
+
.split(path.sep)
|
|
807
|
+
.pop()
|
|
808
|
+
.replace(/\.(spec|test)\.(ts|js|mjs|cjs)$/, "");
|
|
809
|
+
} else {
|
|
810
|
+
suiteNameCandidate = test.name
|
|
811
|
+
.split(path.sep)
|
|
812
|
+
.pop()
|
|
813
|
+
.replace(/\.(spec|test)\.(ts|js|mjs|cjs)$/, "");
|
|
814
|
+
}
|
|
815
|
+
const suiteName = suiteNameCandidate;
|
|
226
816
|
const key = `${suiteName}|${browser}`;
|
|
227
817
|
|
|
228
818
|
if (!suitesMap.has(key)) {
|
|
229
819
|
suitesMap.set(key, {
|
|
230
|
-
id: test.id,
|
|
231
|
-
name:
|
|
232
|
-
status: test.status,
|
|
233
|
-
count: 0,
|
|
820
|
+
id: test.id || key,
|
|
821
|
+
name: suiteName,
|
|
234
822
|
browser: browser,
|
|
823
|
+
passed: 0,
|
|
824
|
+
failed: 0,
|
|
825
|
+
skipped: 0,
|
|
826
|
+
count: 0,
|
|
827
|
+
statusOverall: "passed",
|
|
235
828
|
});
|
|
236
829
|
}
|
|
237
|
-
suitesMap.get(key)
|
|
238
|
-
|
|
830
|
+
const suite = suitesMap.get(key);
|
|
831
|
+
suite.count++;
|
|
832
|
+
const currentStatus = String(test.status).toLowerCase();
|
|
833
|
+
if (currentStatus && suite[currentStatus] !== undefined) {
|
|
834
|
+
suite[currentStatus]++;
|
|
835
|
+
}
|
|
239
836
|
|
|
837
|
+
if (currentStatus === "failed") {
|
|
838
|
+
suite.statusOverall = "failed";
|
|
839
|
+
} else if (
|
|
840
|
+
currentStatus === "skipped" &&
|
|
841
|
+
suite.statusOverall !== "failed"
|
|
842
|
+
) {
|
|
843
|
+
suite.statusOverall = "skipped";
|
|
844
|
+
}
|
|
845
|
+
});
|
|
240
846
|
return Array.from(suitesMap.values());
|
|
241
847
|
}
|
|
242
848
|
|
|
243
|
-
// Generate suites widget (updated for your data)
|
|
244
849
|
function generateSuitesWidget(suitesData) {
|
|
850
|
+
if (!suitesData || suitesData.length === 0) {
|
|
851
|
+
return `<div class="suites-widget"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
|
|
852
|
+
}
|
|
245
853
|
return `
|
|
246
854
|
<div class="suites-widget">
|
|
247
855
|
<div class="suites-header">
|
|
248
856
|
<h2>Test Suites</h2>
|
|
249
|
-
<
|
|
857
|
+
<span class="summary-badge">
|
|
250
858
|
${suitesData.length} suites • ${suitesData.reduce(
|
|
251
859
|
(sum, suite) => sum + suite.count,
|
|
252
860
|
0
|
|
253
861
|
)} tests
|
|
254
|
-
</
|
|
862
|
+
</span>
|
|
255
863
|
</div>
|
|
256
|
-
|
|
257
864
|
<div class="suites-grid">
|
|
258
865
|
${suitesData
|
|
259
866
|
.map(
|
|
260
867
|
(suite) => `
|
|
261
|
-
<div class="suite-card
|
|
262
|
-
<div class="suite-
|
|
263
|
-
<
|
|
264
|
-
.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
868
|
+
<div class="suite-card status-${suite.statusOverall}">
|
|
869
|
+
<div class="suite-card-header">
|
|
870
|
+
<h3 class="suite-name" title="${sanitizeHTML(
|
|
871
|
+
suite.name
|
|
872
|
+
)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
|
|
873
|
+
<span class="browser-tag">${sanitizeHTML(suite.browser)}</span>
|
|
874
|
+
</div>
|
|
875
|
+
<div class="suite-card-body">
|
|
876
|
+
<span class="test-count">${suite.count} test${
|
|
268
877
|
suite.count !== 1 ? "s" : ""
|
|
269
878
|
}</span>
|
|
879
|
+
<div class="suite-stats">
|
|
880
|
+
${
|
|
881
|
+
suite.passed > 0
|
|
882
|
+
? `<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>`
|
|
883
|
+
: ""
|
|
884
|
+
}
|
|
885
|
+
${
|
|
886
|
+
suite.failed > 0
|
|
887
|
+
? `<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>`
|
|
888
|
+
: ""
|
|
889
|
+
}
|
|
890
|
+
${
|
|
891
|
+
suite.skipped > 0
|
|
892
|
+
? `<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>`
|
|
893
|
+
: ""
|
|
894
|
+
}
|
|
895
|
+
</div>
|
|
270
896
|
</div>
|
|
271
|
-
|
|
272
|
-
</div>
|
|
273
|
-
`
|
|
897
|
+
</div>`
|
|
274
898
|
)
|
|
275
899
|
.join("")}
|
|
276
900
|
</div>
|
|
277
|
-
|
|
278
|
-
<style>
|
|
279
|
-
.suites-widget {
|
|
280
|
-
background: linear-gradient(to bottom right, #f0f4ff, #ffffff);
|
|
281
|
-
border-radius: 16px;
|
|
282
|
-
padding: 10px;
|
|
283
|
-
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
|
|
284
|
-
font-family: 'Segoe UI', Roboto, sans-serif;
|
|
285
|
-
height: 100%;
|
|
286
|
-
}
|
|
287
|
-
span.browser-name {
|
|
288
|
-
background-color: #265685;
|
|
289
|
-
font-size: 0.875rem;
|
|
290
|
-
color: #fff;
|
|
291
|
-
padding: 3px;
|
|
292
|
-
border-radius: 4px;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
.suites-header {
|
|
296
|
-
display: flex;
|
|
297
|
-
align-items: center;
|
|
298
|
-
gap: 16px;
|
|
299
|
-
margin-bottom: 24px;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
.suites-header h2 {
|
|
303
|
-
font-size: 20px;
|
|
304
|
-
font-weight: 600;
|
|
305
|
-
margin: 0;
|
|
306
|
-
color: #1a202c;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
.summary-badge {
|
|
310
|
-
background: #f8fafc;
|
|
311
|
-
color: #64748b;
|
|
312
|
-
padding: 4px 12px;
|
|
313
|
-
border-radius: 12px;
|
|
314
|
-
font-size: 14px;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
.suites-grid {
|
|
318
|
-
display: grid;
|
|
319
|
-
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
320
|
-
gap: 16px;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
.suite-card {
|
|
324
|
-
background: #e6e6e6;
|
|
325
|
-
border-radius: 12px;
|
|
326
|
-
padding: 18px;
|
|
327
|
-
border: 1px solid #f1f5f9;
|
|
328
|
-
transition: all 0.2s ease;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
.suite-card:hover {
|
|
332
|
-
transform: translateY(-2px);
|
|
333
|
-
box-shadow: 0 6px 12px rgba(0,0,0,0.08);
|
|
334
|
-
border-color: #e2e8f0;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
.suite-meta {
|
|
338
|
-
display: flex;
|
|
339
|
-
justify-content: space-between;
|
|
340
|
-
align-items: center;
|
|
341
|
-
margin-bottom: 12px;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
.browser-tag {
|
|
345
|
-
font-size: 12px;
|
|
346
|
-
font-weight: 600;
|
|
347
|
-
color: #64748b;
|
|
348
|
-
text-transform: uppercase;
|
|
349
|
-
letter-spacing: 0.5px;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
.status-indicator {
|
|
353
|
-
width: 12px;
|
|
354
|
-
height: 12px;
|
|
355
|
-
border-radius: 50%;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
.status-indicator.passed {
|
|
359
|
-
background: #2a9c68;
|
|
360
|
-
box-shadow: 0 0 0 3px #ecfdf5;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
.status-indicator.failed {
|
|
364
|
-
background: #ef4444;
|
|
365
|
-
box-shadow: 0 0 0 3px #fef2f2;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
.status-indicator.skipped {
|
|
369
|
-
background: #f59e0b;
|
|
370
|
-
box-shadow: 0 0 0 3px #fffbeb;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
.suite-card h3 {
|
|
374
|
-
font-size: 16px;
|
|
375
|
-
margin: 0 0 16px 0;
|
|
376
|
-
color: #1e293b;
|
|
377
|
-
white-space: nowrap;
|
|
378
|
-
overflow: hidden;
|
|
379
|
-
text-overflow: ellipsis;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
.test-visualization {
|
|
383
|
-
display: flex;
|
|
384
|
-
align-items: center;
|
|
385
|
-
gap: 12px;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
.test-dots {
|
|
389
|
-
padding: 4px;
|
|
390
|
-
display: flex;
|
|
391
|
-
flex-wrap: wrap;
|
|
392
|
-
gap: 6px;
|
|
393
|
-
flex-grow: 1;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
.test-dot {
|
|
397
|
-
width: 10px;
|
|
398
|
-
height: 10px;
|
|
399
|
-
border-radius: 50%;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
.test-dot.passed {
|
|
403
|
-
background: #2a9c68;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
.test-dot.failed {
|
|
407
|
-
background: #ef4444;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
.test-dot.skipped {
|
|
411
|
-
background: #f59e0b;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
.test-count {
|
|
415
|
-
font-size: 14px;
|
|
416
|
-
color: #64748b;
|
|
417
|
-
min-width: 60px;
|
|
418
|
-
text-align: right;
|
|
419
|
-
}
|
|
420
|
-
</style>
|
|
421
|
-
</div>
|
|
422
|
-
`;
|
|
901
|
+
</div>`;
|
|
423
902
|
}
|
|
424
903
|
|
|
425
|
-
|
|
426
|
-
function generateHTML(reportData) {
|
|
904
|
+
function generateHTML(reportData, trendData = null) {
|
|
427
905
|
const { run, results } = reportData;
|
|
428
|
-
const suitesData = getSuitesData(reportData.results);
|
|
906
|
+
const suitesData = getSuitesData(reportData.results || []);
|
|
429
907
|
const runSummary = run || {
|
|
430
908
|
totalTests: 0,
|
|
431
909
|
passed: 0,
|
|
432
910
|
failed: 0,
|
|
433
911
|
skipped: 0,
|
|
434
912
|
duration: 0,
|
|
435
|
-
timestamp: new Date(),
|
|
913
|
+
timestamp: new Date().toISOString(),
|
|
436
914
|
};
|
|
437
915
|
|
|
438
|
-
//
|
|
439
|
-
const passPercentage =
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
runSummary.totalTests > 0
|
|
445
|
-
? Math.round((runSummary.failed / runSummary.totalTests) * 100)
|
|
446
|
-
: 0;
|
|
447
|
-
const skipPercentage =
|
|
448
|
-
runSummary.totalTests > 0
|
|
449
|
-
? Math.round((runSummary.skipped / runSummary.totalTests) * 100)
|
|
450
|
-
: 0;
|
|
916
|
+
const totalTestsOr1 = runSummary.totalTests || 1; // Avoid division by zero
|
|
917
|
+
const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
|
|
918
|
+
const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
|
|
919
|
+
const skipPercentage = Math.round(
|
|
920
|
+
((runSummary.skipped || 0) / totalTestsOr1) * 100
|
|
921
|
+
);
|
|
451
922
|
const avgTestDuration =
|
|
452
923
|
runSummary.totalTests > 0
|
|
453
924
|
? formatDuration(runSummary.duration / runSummary.totalTests)
|
|
454
925
|
: "0.0s";
|
|
455
926
|
|
|
456
|
-
|
|
457
|
-
const generateTestCasesHTML = () => {
|
|
927
|
+
function generateTestCasesHTML() {
|
|
458
928
|
if (!results || results.length === 0) {
|
|
459
|
-
return '<div class="no-tests">No test results found
|
|
929
|
+
return '<div class="no-tests">No test results found in this run.</div>';
|
|
460
930
|
}
|
|
461
931
|
|
|
462
|
-
// Collect all unique tags and browsers
|
|
463
|
-
const allTags = new Set();
|
|
464
|
-
const allBrowsers = new Set();
|
|
465
|
-
|
|
466
|
-
results.forEach((test) => {
|
|
467
|
-
(test.tags || []).forEach((tag) => allTags.add(tag));
|
|
468
|
-
const browserMatch = test.name.match(/ > (\w+) > /);
|
|
469
|
-
if (browserMatch) allBrowsers.add(browserMatch[1]);
|
|
470
|
-
});
|
|
471
|
-
|
|
472
932
|
return results
|
|
473
933
|
.map((test, index) => {
|
|
474
934
|
const browser = test.browser || "unknown";
|
|
475
|
-
const
|
|
935
|
+
const testFileParts = test.name.split(" > ");
|
|
936
|
+
const testTitle =
|
|
937
|
+
testFileParts[testFileParts.length - 1] || "Unnamed Test";
|
|
476
938
|
|
|
477
|
-
// Generate steps HTML recursively
|
|
478
939
|
const generateStepsHTML = (steps, depth = 0) => {
|
|
479
|
-
if (!steps || steps.length === 0)
|
|
480
|
-
|
|
940
|
+
if (!steps || steps.length === 0)
|
|
941
|
+
return "<div class='no-steps'>No steps recorded for this test.</div>";
|
|
481
942
|
return steps
|
|
482
943
|
.map((step) => {
|
|
483
944
|
const hasNestedSteps = step.steps && step.steps.length > 0;
|
|
484
|
-
const isHook = step.
|
|
485
|
-
const stepClass = isHook
|
|
945
|
+
const isHook = step.hookType;
|
|
946
|
+
const stepClass = isHook
|
|
947
|
+
? `step-hook step-hook-${step.hookType}`
|
|
948
|
+
: "";
|
|
486
949
|
const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
|
|
487
950
|
|
|
488
951
|
return `
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
</div>
|
|
499
|
-
<div class="step-details">
|
|
500
|
-
${
|
|
501
|
-
step.codeLocation
|
|
502
|
-
? `<div><strong>Location:</strong> ${sanitizeHTML(
|
|
503
|
-
step.codeLocation
|
|
504
|
-
)}</div>`
|
|
505
|
-
: ""
|
|
506
|
-
}
|
|
507
|
-
${
|
|
508
|
-
step.errorMessage
|
|
509
|
-
? `
|
|
510
|
-
<div class="step-error">
|
|
511
|
-
<strong>Error:</strong> ${sanitizeHTML(step.errorMessage)}
|
|
512
|
-
${
|
|
513
|
-
step.stackTrace
|
|
514
|
-
? `<pre>${sanitizeHTML(step.stackTrace)}</pre>`
|
|
515
|
-
: ""
|
|
516
|
-
}
|
|
517
|
-
</div>
|
|
518
|
-
`
|
|
519
|
-
: ""
|
|
520
|
-
}
|
|
521
|
-
${
|
|
522
|
-
hasNestedSteps
|
|
523
|
-
? `
|
|
524
|
-
<div class="nested-steps">
|
|
525
|
-
${generateStepsHTML(step.steps, depth + 1)}
|
|
526
|
-
</div>
|
|
527
|
-
`
|
|
528
|
-
: ""
|
|
529
|
-
}
|
|
530
|
-
</div>
|
|
952
|
+
<div class="step-item" style="--depth: ${depth};">
|
|
953
|
+
<div class="step-header ${stepClass}" role="button" aria-expanded="false">
|
|
954
|
+
<span class="step-icon">${getStatusIcon(step.status)}</span>
|
|
955
|
+
<span class="step-title">${sanitizeHTML(
|
|
956
|
+
step.title
|
|
957
|
+
)}${hookIndicator}</span>
|
|
958
|
+
<span class="step-duration">${formatDuration(
|
|
959
|
+
step.duration
|
|
960
|
+
)}</span>
|
|
531
961
|
</div>
|
|
532
|
-
|
|
962
|
+
<div class="step-details" style="display: none;">
|
|
963
|
+
${
|
|
964
|
+
step.codeLocation
|
|
965
|
+
? `<div class="step-info"><strong>Location:</strong> ${sanitizeHTML(
|
|
966
|
+
step.codeLocation
|
|
967
|
+
)}</div>`
|
|
968
|
+
: ""
|
|
969
|
+
}
|
|
970
|
+
${
|
|
971
|
+
step.errorMessage
|
|
972
|
+
? `
|
|
973
|
+
<div class="step-error">
|
|
974
|
+
${
|
|
975
|
+
step.stackTrace
|
|
976
|
+
? `<pre class="stack-trace">${sanitizeHTML(
|
|
977
|
+
step.stackTrace
|
|
978
|
+
)}</pre>`
|
|
979
|
+
: ""
|
|
980
|
+
}
|
|
981
|
+
</div>`
|
|
982
|
+
: ""
|
|
983
|
+
}
|
|
984
|
+
${
|
|
985
|
+
hasNestedSteps
|
|
986
|
+
? `<div class="nested-steps">${generateStepsHTML(
|
|
987
|
+
step.steps,
|
|
988
|
+
depth + 1
|
|
989
|
+
)}</div>`
|
|
990
|
+
: ""
|
|
991
|
+
}
|
|
992
|
+
</div>
|
|
993
|
+
</div>`;
|
|
533
994
|
})
|
|
534
995
|
.join("");
|
|
535
996
|
};
|
|
536
997
|
|
|
537
998
|
return `
|
|
538
|
-
|
|
999
|
+
<div class="test-case" data-status="${
|
|
1000
|
+
test.status
|
|
1001
|
+
}" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
|
|
1002
|
+
.join(",")
|
|
1003
|
+
.toLowerCase()}">
|
|
1004
|
+
<div class="test-case-header" role="button" aria-expanded="false">
|
|
1005
|
+
<div class="test-case-summary">
|
|
1006
|
+
<span class="status-badge ${getStatusClass(test.status)}">${String(
|
|
539
1007
|
test.status
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
)}">${test.status.toUpperCase()}</span>
|
|
546
|
-
<span class="test-name">${sanitizeHTML(testName)}</span>
|
|
547
|
-
<span class="test-browser">(${browser})</span>
|
|
548
|
-
</div>
|
|
549
|
-
<div class="test-meta">
|
|
550
|
-
<span class="test-duration">${formatDuration(
|
|
551
|
-
test.duration
|
|
552
|
-
)}</span>
|
|
553
|
-
</div>
|
|
1008
|
+
).toUpperCase()}</span>
|
|
1009
|
+
<span class="test-case-title" title="${sanitizeHTML(
|
|
1010
|
+
test.name
|
|
1011
|
+
)}">${sanitizeHTML(testTitle)}</span>
|
|
1012
|
+
<span class="test-case-browser">(${sanitizeHTML(browser)})</span>
|
|
554
1013
|
</div>
|
|
555
|
-
<div class="
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1014
|
+
<div class="test-case-meta">
|
|
1015
|
+
${
|
|
1016
|
+
test.tags && test.tags.length > 0
|
|
1017
|
+
? test.tags
|
|
1018
|
+
.map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
|
|
1019
|
+
.join(" ")
|
|
1020
|
+
: ""
|
|
1021
|
+
}
|
|
1022
|
+
<span class="test-duration">${formatDuration(test.duration)}</span>
|
|
1023
|
+
</div>
|
|
1024
|
+
</div>
|
|
1025
|
+
<div class="test-case-content" style="display: none;">
|
|
1026
|
+
<p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
|
|
1027
|
+
${
|
|
1028
|
+
test.error
|
|
1029
|
+
? `<div class="test-error-summary">
|
|
1030
|
+
${formatPlaywrightError(test.error)}
|
|
1031
|
+
</div>`
|
|
1032
|
+
: ""
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
<h4>Steps</h4>
|
|
1036
|
+
<div class="steps-list">${generateStepsHTML(test.steps)}</div>
|
|
1037
|
+
|
|
1038
|
+
${
|
|
1039
|
+
test.stdout && test.stdout.length > 0
|
|
1040
|
+
? `
|
|
1041
|
+
<div class="console-output-section">
|
|
1042
|
+
<h4>Console Output (stdout)</h4>
|
|
1043
|
+
<pre class="console-log stdout-log" style="background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${test.stdout
|
|
1044
|
+
.map((line) => sanitizeHTML(line))
|
|
1045
|
+
.join("\n")}</pre>
|
|
1046
|
+
</div>`
|
|
1047
|
+
: ""
|
|
1048
|
+
}
|
|
1049
|
+
${
|
|
1050
|
+
test.stderr && test.stderr.length > 0
|
|
1051
|
+
? `
|
|
1052
|
+
<div class="console-output-section">
|
|
1053
|
+
<h4>Console Output (stderr)</h4>
|
|
1054
|
+
<pre class="console-log stderr-log" style="background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${test.stderr
|
|
1055
|
+
.map((line) => sanitizeHTML(line))
|
|
1056
|
+
.join("\n")}</pre>
|
|
1057
|
+
</div>`
|
|
1058
|
+
: ""
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
${
|
|
1062
|
+
test.screenshots && test.screenshots.length > 0
|
|
1063
|
+
? `
|
|
1064
|
+
<div class="attachments-section">
|
|
1065
|
+
<h4>Screenshots</h4>
|
|
1066
|
+
<div class="attachments-grid">
|
|
1067
|
+
${test.screenshots
|
|
1068
|
+
.map(
|
|
1069
|
+
(screenshot) => `
|
|
1070
|
+
<div class="attachment-item">
|
|
1071
|
+
<img src="${screenshot}" alt="Screenshot">
|
|
1072
|
+
<div class="attachment-info">
|
|
1073
|
+
<div class="trace-actions">
|
|
1074
|
+
<a href="${screenshot}" target="_blank">View Full Size</a></div>
|
|
1075
|
+
</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
`
|
|
1078
|
+
)
|
|
1079
|
+
.join("")}
|
|
574
1080
|
</div>
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1081
|
+
</div>
|
|
1082
|
+
`
|
|
1083
|
+
: ""
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
${
|
|
1087
|
+
test.videoPath
|
|
1088
|
+
? `
|
|
1089
|
+
<div class="attachments-section">
|
|
1090
|
+
<h4>Videos</h4>
|
|
1091
|
+
<div class="attachments-grid">
|
|
1092
|
+
${(() => {
|
|
1093
|
+
// Handle both string and array cases
|
|
1094
|
+
const videos = Array.isArray(test.videoPath)
|
|
1095
|
+
? test.videoPath
|
|
1096
|
+
: [test.videoPath];
|
|
1097
|
+
const mimeTypes = {
|
|
1098
|
+
mp4: "video/mp4",
|
|
1099
|
+
webm: "video/webm",
|
|
1100
|
+
ogg: "video/ogg",
|
|
1101
|
+
mov: "video/quicktime",
|
|
1102
|
+
avi: "video/x-msvideo",
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
return videos
|
|
1106
|
+
.map((video, index) => {
|
|
1107
|
+
const videoUrl =
|
|
1108
|
+
typeof video === "object" ? video.url || "" : video;
|
|
1109
|
+
const videoName =
|
|
1110
|
+
typeof video === "object"
|
|
1111
|
+
? video.name || `Video ${index + 1}`
|
|
1112
|
+
: `Video ${index + 1}`;
|
|
1113
|
+
const fileExtension = videoUrl
|
|
1114
|
+
.split(".")
|
|
1115
|
+
.pop()
|
|
1116
|
+
.toLowerCase();
|
|
1117
|
+
const mimeType = mimeTypes[fileExtension] || "video/mp4";
|
|
1118
|
+
|
|
1119
|
+
return `
|
|
585
1120
|
<div class="attachment-item">
|
|
586
|
-
<
|
|
1121
|
+
<video controls width="100%" height="auto" title="${videoName}">
|
|
1122
|
+
<source src="${videoUrl}" type="${mimeType}">
|
|
1123
|
+
Your browser does not support the video tag.
|
|
1124
|
+
</video>
|
|
587
1125
|
<div class="attachment-info">
|
|
588
|
-
<
|
|
1126
|
+
<div class="trace-actions">
|
|
1127
|
+
<a href="${videoUrl}" target="_blank" download="${videoName}.${fileExtension}">
|
|
1128
|
+
Download
|
|
1129
|
+
</a>
|
|
1130
|
+
</div>
|
|
589
1131
|
</div>
|
|
590
1132
|
</div>
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
1133
|
+
`;
|
|
1134
|
+
})
|
|
1135
|
+
.join("");
|
|
1136
|
+
})()}
|
|
1137
|
+
</div>
|
|
1138
|
+
</div>
|
|
1139
|
+
`
|
|
1140
|
+
: ""
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
${
|
|
1144
|
+
test.tracePath
|
|
1145
|
+
? `
|
|
1146
|
+
<div class="attachments-section">
|
|
1147
|
+
<h4>Trace Files</h4>
|
|
1148
|
+
<div class="attachments-grid">
|
|
1149
|
+
${(() => {
|
|
1150
|
+
// Handle both string and array cases
|
|
1151
|
+
const traces = Array.isArray(test.tracePath)
|
|
1152
|
+
? test.tracePath
|
|
1153
|
+
: [test.tracePath];
|
|
1154
|
+
|
|
1155
|
+
return traces
|
|
1156
|
+
.map((trace, index) => {
|
|
1157
|
+
const traceUrl =
|
|
1158
|
+
typeof trace === "object" ? trace.url || "" : trace;
|
|
1159
|
+
const traceName =
|
|
1160
|
+
typeof trace === "object"
|
|
1161
|
+
? trace.name || `Trace ${index + 1}`
|
|
1162
|
+
: `Trace ${index + 1}`;
|
|
1163
|
+
const traceFileName = traceUrl.split("/").pop();
|
|
1164
|
+
|
|
1165
|
+
return `
|
|
1166
|
+
<div class="attachment-item">
|
|
1167
|
+
<div class="trace-preview">
|
|
1168
|
+
<span class="trace-icon">📄</span>
|
|
1169
|
+
<span class="trace-name">${traceName}</span>
|
|
1170
|
+
</div>
|
|
1171
|
+
<div class="attachment-info">
|
|
1172
|
+
<div class="trace-actions">
|
|
1173
|
+
<a href="${traceUrl}" target="_blank" download="${traceFileName}" class="download-trace">
|
|
1174
|
+
Download
|
|
1175
|
+
</a>
|
|
606
1176
|
</div>
|
|
607
|
-
|
|
608
|
-
: ""
|
|
609
|
-
}
|
|
1177
|
+
</div>
|
|
610
1178
|
</div>
|
|
611
|
-
|
|
1179
|
+
`;
|
|
1180
|
+
})
|
|
1181
|
+
.join("");
|
|
1182
|
+
})()}
|
|
1183
|
+
</div>
|
|
1184
|
+
</div>
|
|
1185
|
+
`
|
|
1186
|
+
: ""
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
${
|
|
1190
|
+
test.codeSnippet
|
|
1191
|
+
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${sanitizeHTML(
|
|
1192
|
+
test.codeSnippet
|
|
1193
|
+
)}</code></pre></div>`
|
|
1194
|
+
: ""
|
|
1195
|
+
}
|
|
612
1196
|
</div>
|
|
613
|
-
|
|
1197
|
+
</div>`;
|
|
614
1198
|
})
|
|
615
1199
|
.join("");
|
|
616
|
-
}
|
|
1200
|
+
}
|
|
617
1201
|
|
|
618
|
-
// Generate HTML with optimized CSS and JS
|
|
619
1202
|
return `
|
|
620
1203
|
<!DOCTYPE html>
|
|
621
1204
|
<html lang="en">
|
|
622
1205
|
<head>
|
|
623
1206
|
<meta charset="UTF-8">
|
|
624
1207
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1208
|
+
<link rel="icon" type="image/png" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
|
|
1209
|
+
<link rel="apple-touch-icon" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
|
|
1210
|
+
<script src="https://code.highcharts.com/highcharts.js"></script>
|
|
625
1211
|
<title>Playwright Pulse Report</title>
|
|
626
1212
|
<style>
|
|
627
|
-
/* Base Styles */
|
|
628
1213
|
:root {
|
|
629
|
-
--primary-color: #3f51b5;
|
|
630
|
-
--secondary-color: #ff4081;
|
|
631
|
-
--
|
|
632
|
-
--
|
|
633
|
-
--
|
|
634
|
-
--
|
|
635
|
-
--
|
|
636
|
-
--
|
|
637
|
-
--
|
|
638
|
-
--
|
|
639
|
-
|
|
1214
|
+
--primary-color: #3f51b5; /* Indigo */
|
|
1215
|
+
--secondary-color: #ff4081; /* Pink */
|
|
1216
|
+
--accent-color: #673ab7; /* Deep Purple */
|
|
1217
|
+
--accent-color-alt: #FF9800; /* Orange for duration charts */
|
|
1218
|
+
--success-color: #4CAF50; /* Green */
|
|
1219
|
+
--danger-color: #F44336; /* Red */
|
|
1220
|
+
--warning-color: #FFC107; /* Amber */
|
|
1221
|
+
--info-color: #2196F3; /* Blue */
|
|
1222
|
+
--light-gray-color: #f5f5f5;
|
|
1223
|
+
--medium-gray-color: #e0e0e0;
|
|
1224
|
+
--dark-gray-color: #757575;
|
|
1225
|
+
--text-color: #333;
|
|
1226
|
+
--text-color-secondary: #555;
|
|
1227
|
+
--border-color: #ddd;
|
|
1228
|
+
--background-color: #f8f9fa;
|
|
1229
|
+
--card-background-color: #fff;
|
|
1230
|
+
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
1231
|
+
--border-radius: 8px;
|
|
1232
|
+
--box-shadow: 0 5px 15px rgba(0,0,0,0.08);
|
|
1233
|
+
--box-shadow-light: 0 3px 8px rgba(0,0,0,0.05);
|
|
1234
|
+
--box-shadow-inset: inset 0 1px 3px rgba(0,0,0,0.07);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/* General Highcharts styling */
|
|
1238
|
+
.highcharts-background { fill: transparent; }
|
|
1239
|
+
.highcharts-title, .highcharts-subtitle { font-family: var(--font-family); }
|
|
1240
|
+
.highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
|
|
1241
|
+
.highcharts-axis-title { fill: var(--text-color) !important; }
|
|
1242
|
+
.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; }
|
|
640
1243
|
|
|
641
1244
|
body {
|
|
642
|
-
font-family:
|
|
1245
|
+
font-family: var(--font-family);
|
|
643
1246
|
margin: 0;
|
|
644
|
-
|
|
645
|
-
background-color: #fafafa;
|
|
1247
|
+
background-color: var(--background-color);
|
|
646
1248
|
color: var(--text-color);
|
|
647
|
-
line-height: 1.
|
|
1249
|
+
line-height: 1.65;
|
|
1250
|
+
font-size: 16px;
|
|
648
1251
|
}
|
|
649
1252
|
|
|
650
1253
|
.container {
|
|
651
|
-
|
|
652
|
-
padding:
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1254
|
+
max-width: 1600px;
|
|
1255
|
+
padding: 30px;
|
|
1256
|
+
border-radius: var(--border-radius);
|
|
1257
|
+
box-shadow: var(--box-shadow);
|
|
1258
|
+
background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec);
|
|
656
1259
|
}
|
|
657
1260
|
|
|
658
|
-
/* Header Styles */
|
|
659
1261
|
.header {
|
|
660
1262
|
display: flex;
|
|
661
1263
|
justify-content: space-between;
|
|
662
1264
|
align-items: center;
|
|
663
1265
|
flex-wrap: wrap;
|
|
664
|
-
|
|
665
|
-
padding-bottom: 20px;
|
|
1266
|
+
padding-bottom: 25px;
|
|
666
1267
|
border-bottom: 1px solid var(--border-color);
|
|
1268
|
+
margin-bottom: 25px;
|
|
667
1269
|
}
|
|
1270
|
+
.header-title { display: flex; align-items: center; gap: 15px; }
|
|
1271
|
+
.header h1 { margin: 0; font-size: 1.85em; font-weight: 600; color: var(--primary-color); }
|
|
1272
|
+
#report-logo { height: 40px; width: 40px; border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.1);}
|
|
1273
|
+
.run-info { font-size: 0.9em; text-align: right; color: var(--text-color-secondary); line-height:1.5;}
|
|
1274
|
+
.run-info strong { color: var(--text-color); }
|
|
668
1275
|
|
|
669
|
-
.
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
align-items: center;
|
|
675
|
-
gap: 10px;
|
|
1276
|
+
.tabs { display: flex; border-bottom: 2px solid var(--border-color); margin-bottom: 30px; overflow-x: auto; }
|
|
1277
|
+
.tab-button {
|
|
1278
|
+
padding: 15px 25px; background: none; border: none; border-bottom: 3px solid transparent;
|
|
1279
|
+
cursor: pointer; font-size: 1.1em; font-weight: 600; color: black;
|
|
1280
|
+
transition: color 0.2s ease, border-color 0.2s ease; white-space: nowrap;
|
|
676
1281
|
}
|
|
677
|
-
|
|
678
|
-
.
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
1282
|
+
.tab-button:hover { color: var(--accent-color); }
|
|
1283
|
+
.tab-button.active { color: var(--primary-color); border-bottom-color: var(--primary-color); }
|
|
1284
|
+
.tab-content { display: none; animation: fadeIn 0.4s ease-out; }
|
|
1285
|
+
.tab-content.active { display: block; }
|
|
1286
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
1287
|
+
|
|
1288
|
+
.dashboard-grid {
|
|
1289
|
+
display: grid;
|
|
1290
|
+
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
|
1291
|
+
gap: 22px; margin-bottom: 35px;
|
|
683
1292
|
}
|
|
684
|
-
|
|
685
|
-
/* Tab Styles */
|
|
686
|
-
.tabs {
|
|
687
|
-
display: flex;
|
|
688
|
-
border-bottom: 1px solid var(--border-color);
|
|
689
|
-
margin-bottom: 20px;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
.tab-button {
|
|
693
|
-
padding: 10px 20px;
|
|
694
|
-
background: none;
|
|
695
|
-
border: none;
|
|
696
|
-
cursor: pointer;
|
|
697
|
-
font-size: 16px;
|
|
698
|
-
color: #666;
|
|
699
|
-
position: relative;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
.tab-button.active {
|
|
703
|
-
color: var(--primary-color);
|
|
704
|
-
font-weight: 500;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
.tab-button.active::after {
|
|
708
|
-
content: '';
|
|
709
|
-
position: absolute;
|
|
710
|
-
bottom: -1px;
|
|
711
|
-
left: 0;
|
|
712
|
-
right: 0;
|
|
713
|
-
height: 2px;
|
|
714
|
-
background: var(--primary-color);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
.tab-content {
|
|
718
|
-
display: none;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
.tab-content.active {
|
|
722
|
-
display: block;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/* Main dashboard grid layout */
|
|
726
|
-
.dashboard-grid {
|
|
727
|
-
display: grid;
|
|
728
|
-
grid-template-columns: 1fr;
|
|
729
|
-
gap: 20px;
|
|
730
|
-
padding: 16px 0;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
1293
|
.summary-card {
|
|
734
|
-
background:
|
|
735
|
-
border-radius:
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
.
|
|
744
|
-
|
|
745
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
746
|
-
background: #90e0e3; /* optional light background change */
|
|
747
|
-
}
|
|
1294
|
+
background-color: var(--card-background-color); border: 1px solid var(--border-color);
|
|
1295
|
+
border-radius: var(--border-radius); padding: 22px; text-align: center;
|
|
1296
|
+
box-shadow: var(--box-shadow-light); transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
1297
|
+
}
|
|
1298
|
+
.summary-card:hover { transform: translateY(-5px); box-shadow: var(--box-shadow); }
|
|
1299
|
+
.summary-card h3 { margin: 0 0 10px; font-size: 1.05em; font-weight: 500; color: var(--text-color-secondary); }
|
|
1300
|
+
.summary-card .value { font-size: 2.4em; font-weight: 600; margin-bottom: 8px; }
|
|
1301
|
+
.summary-card .trend-percentage { font-size: 1em; color: var(--dark-gray-color); }
|
|
1302
|
+
.status-passed .value, .stat-passed svg { color: var(--success-color); }
|
|
1303
|
+
.status-failed .value, .stat-failed svg { color: var(--danger-color); }
|
|
1304
|
+
.status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
|
|
748
1305
|
|
|
749
|
-
.
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
color: #666;
|
|
1306
|
+
.dashboard-bottom-row {
|
|
1307
|
+
display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
1308
|
+
gap: 28px; align-items: stretch;
|
|
753
1309
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
margin: 10px 0;
|
|
1310
|
+
.pie-chart-wrapper, .suites-widget, .trend-chart {
|
|
1311
|
+
background-color: var(--card-background-color); padding: 28px;
|
|
1312
|
+
border-radius: var(--border-radius); box-shadow: var(--box-shadow-light);
|
|
1313
|
+
display: flex; flex-direction: column;
|
|
759
1314
|
}
|
|
760
|
-
|
|
761
|
-
.
|
|
762
|
-
|
|
1315
|
+
|
|
1316
|
+
.pie-chart-wrapper h3, .suites-header h2, .trend-chart h3 {
|
|
1317
|
+
text-align: center; margin-top: 0; margin-bottom: 25px;
|
|
1318
|
+
font-size: 1.25em; font-weight: 600; color: var(--text-color);
|
|
763
1319
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1320
|
+
.trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { /* For Highcharts containers */
|
|
1321
|
+
flex-grow: 1;
|
|
1322
|
+
min-height: 250px; /* Ensure charts have some min height */
|
|
767
1323
|
}
|
|
768
1324
|
|
|
769
|
-
.
|
|
770
|
-
|
|
771
|
-
}
|
|
1325
|
+
.chart-tooltip { /* This class was for D3, Highcharts has its own tooltip styling via JS/SVG */
|
|
1326
|
+
/* Basic styling for Highcharts HTML tooltips can be done via .highcharts-tooltip span */
|
|
1327
|
+
}
|
|
1328
|
+
.status-badge-small-tooltip { padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; }
|
|
1329
|
+
.status-badge-small-tooltip.status-passed { background-color: var(--success-color); }
|
|
1330
|
+
.status-badge-small-tooltip.status-failed { background-color: var(--danger-color); }
|
|
1331
|
+
.status-badge-small-tooltip.status-skipped { background-color: var(--warning-color); }
|
|
1332
|
+
.status-badge-small-tooltip.status-unknown { background-color: var(--dark-gray-color); }
|
|
772
1333
|
|
|
773
|
-
.
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1334
|
+
.suites-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
|
1335
|
+
.summary-badge { background-color: var(--light-gray-color); color: var(--text-color-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
|
|
1336
|
+
.suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
|
|
1337
|
+
.suite-card {
|
|
1338
|
+
border: 1px solid var(--border-color); border-left-width: 5px;
|
|
1339
|
+
border-radius: calc(var(--border-radius) / 1.5); padding: 20px;
|
|
1340
|
+
background-color: var(--card-background-color); transition: box-shadow 0.2s ease, border-left-color 0.2s ease;
|
|
1341
|
+
}
|
|
1342
|
+
.suite-card:hover { box-shadow: var(--box-shadow); }
|
|
1343
|
+
.suite-card.status-passed { border-left-color: var(--success-color); }
|
|
1344
|
+
.suite-card.status-failed { border-left-color: var(--danger-color); }
|
|
1345
|
+
.suite-card.status-skipped { border-left-color: var(--warning-color); }
|
|
1346
|
+
.suite-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
|
|
1347
|
+
.suite-name { font-weight: 600; font-size: 1.05em; color: var(--text-color); margin-right: 10px; word-break: break-word;}
|
|
1348
|
+
.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;}
|
|
1349
|
+
.suite-card-body .test-count { font-size: 0.95em; color: var(--text-color-secondary); display: block; margin-bottom: 10px; }
|
|
1350
|
+
.suite-stats { display: flex; gap: 14px; font-size: 0.95em; align-items: center; }
|
|
1351
|
+
.suite-stats span { display: flex; align-items: center; gap: 6px; }
|
|
1352
|
+
.suite-stats svg { vertical-align: middle; font-size: 1.15em; }
|
|
1353
|
+
|
|
782
1354
|
.filters {
|
|
783
|
-
display: flex;
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
.filters
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1355
|
+
display: flex; flex-wrap: wrap; gap: 18px; margin-bottom: 28px;
|
|
1356
|
+
padding: 20px; background-color: var(--light-gray-color); border-radius: var(--border-radius);
|
|
1357
|
+
box-shadow: var(--box-shadow-inset); border-color: black; border-style: groove;
|
|
1358
|
+
}
|
|
1359
|
+
.filters input, .filters select, .filters button {
|
|
1360
|
+
padding: 11px 15px; border: 1px solid var(--border-color);
|
|
1361
|
+
border-radius: 6px; font-size: 1em;
|
|
1362
|
+
}
|
|
1363
|
+
.filters input { flex-grow: 1; min-width: 240px;}
|
|
1364
|
+
.filters select {min-width: 180px;}
|
|
1365
|
+
.filters button { background-color: var(--primary-color); color: white; cursor: pointer; transition: background-color 0.2s ease, box-shadow 0.2s ease; border: none; }
|
|
1366
|
+
.filters button:hover { background-color: var(--accent-color); box-shadow: 0 2px 5px rgba(0,0,0,0.15);}
|
|
1367
|
+
|
|
1368
|
+
.test-case {
|
|
1369
|
+
margin-bottom: 15px; border: 1px solid var(--border-color);
|
|
1370
|
+
border-radius: var(--border-radius); background-color: var(--card-background-color);
|
|
1371
|
+
box-shadow: var(--box-shadow-light); overflow: hidden;
|
|
1372
|
+
}
|
|
1373
|
+
.test-case-header {
|
|
1374
|
+
padding: 10px 15px; background-color: #fff; cursor: pointer;
|
|
1375
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
1376
|
+
border-bottom: 1px solid transparent;
|
|
1377
|
+
transition: background-color 0.2s ease;
|
|
1378
|
+
}
|
|
1379
|
+
.test-case-header:hover { background-color: #f4f6f8; }
|
|
1380
|
+
.test-case-header[aria-expanded="true"] { border-bottom-color: var(--border-color); background-color: #f9fafb; }
|
|
806
1381
|
|
|
807
|
-
.test-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
.suite-header {
|
|
815
|
-
padding: 12px 15px;
|
|
816
|
-
background: #f9f9f9;
|
|
817
|
-
cursor: pointer;
|
|
818
|
-
display: flex;
|
|
819
|
-
justify-content: space-between;
|
|
820
|
-
align-items: center;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
.suite-header:hover {
|
|
824
|
-
background: #f0f0f0;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
.suite-content {
|
|
828
|
-
display: none;
|
|
829
|
-
padding: 15px;
|
|
830
|
-
background: white;
|
|
831
|
-
}
|
|
1382
|
+
.test-case-summary { display: flex; align-items: center; gap: 14px; flex-grow: 1; flex-wrap: wrap;}
|
|
1383
|
+
.test-case-title { font-weight: 600; color: var(--text-color); font-size: 1em; }
|
|
1384
|
+
.test-case-browser { font-size: 0.9em; color: var(--text-color-secondary); }
|
|
1385
|
+
.test-case-meta { display: flex; align-items: center; gap: 12px; font-size: 0.9em; color: var(--text-color-secondary); flex-shrink: 0; }
|
|
1386
|
+
.test-duration { background-color: var(--light-gray-color); padding: 4px 10px; border-radius: 12px; font-size: 0.9em;}
|
|
832
1387
|
|
|
833
|
-
.
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
color: var(--dark-color);
|
|
1388
|
+
.status-badge {
|
|
1389
|
+
padding: 5px; border-radius: 6px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase;
|
|
1390
|
+
min-width: 70px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
837
1391
|
}
|
|
1392
|
+
.status-badge.status-passed { background-color: var(--success-color); }
|
|
1393
|
+
.status-badge.status-failed { background-color: var(--danger-color); }
|
|
1394
|
+
.status-badge.status-skipped { background-color: var(--warning-color); }
|
|
1395
|
+
.status-badge.status-unknown { background-color: var(--dark-gray-color); }
|
|
838
1396
|
|
|
839
|
-
.
|
|
840
|
-
margin: 15px 0;
|
|
841
|
-
padding: 0;
|
|
842
|
-
list-style: none;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
.step-item {
|
|
846
|
-
margin-bottom: 8px;
|
|
847
|
-
}
|
|
1397
|
+
.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; }
|
|
848
1398
|
|
|
1399
|
+
.test-case-content { display: none; padding: 20px; border-top: 1px solid var(--border-color); background-color: #fcfdff; }
|
|
1400
|
+
.test-case-content h4 { margin-top: 22px; margin-bottom: 14px; font-size: 1.15em; color: var(--primary-color); }
|
|
1401
|
+
.test-case-content p { margin-bottom: 10px; font-size: 1em; }
|
|
1402
|
+
.test-error-summary { margin-bottom: 20px; padding: 14px; background-color: rgba(244,67,54,0.05); border: 1px solid rgba(244,67,54,0.2); border-left: 4px solid var(--danger-color); border-radius: 4px; }
|
|
1403
|
+
.test-error-summary h4 { color: var(--danger-color); margin-top:0;}
|
|
1404
|
+
.test-error-summary pre { white-space: pre-wrap; word-break: break-all; color: var(--danger-color); font-size: 0.95em;}
|
|
1405
|
+
|
|
1406
|
+
.steps-list { margin: 18px 0; }
|
|
1407
|
+
.step-item { margin-bottom: 8px; padding-left: calc(var(--depth, 0) * 28px); }
|
|
849
1408
|
.step-header {
|
|
850
|
-
display: flex;
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
.step-
|
|
858
|
-
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
.step-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
.
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
.step-duration {
|
|
872
|
-
color: #666;
|
|
873
|
-
font-size: 12px;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
.step-details {
|
|
877
|
-
display: none;
|
|
878
|
-
padding: 10px;
|
|
879
|
-
margin-top: 5px;
|
|
880
|
-
background: #f9f9f9;
|
|
881
|
-
border-radius: 4px;
|
|
882
|
-
font-size: 14px;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
.step-error {
|
|
886
|
-
color: var(--danger-color);
|
|
887
|
-
margin-top: 8px;
|
|
888
|
-
padding: 8px;
|
|
889
|
-
background: rgba(244, 67, 54, 0.1);
|
|
890
|
-
border-radius: 4px;
|
|
891
|
-
font-size: 13px;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
.step-hook {
|
|
895
|
-
background: rgba(33, 150, 243, 0.1);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
.nested-steps {
|
|
899
|
-
display: none;
|
|
900
|
-
padding-left: 20px;
|
|
901
|
-
border-left: 2px solid #eee;
|
|
902
|
-
margin-top: 8px;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
.attachments-grid {
|
|
906
|
-
display: grid;
|
|
907
|
-
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
908
|
-
gap: 15px;
|
|
909
|
-
margin-top: 15px;
|
|
910
|
-
}
|
|
911
|
-
|
|
1409
|
+
display: flex; align-items: center; cursor: pointer;
|
|
1410
|
+
padding: 10px 14px; border-radius: 6px; background-color: #fff;
|
|
1411
|
+
border: 1px solid var(--light-gray-color);
|
|
1412
|
+
transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
|
1413
|
+
}
|
|
1414
|
+
.step-header:hover { background-color: #f0f2f5; border-color: var(--medium-gray-color); box-shadow: var(--box-shadow-inset); }
|
|
1415
|
+
.step-icon { margin-right: 12px; width: 20px; text-align: center; font-size: 1.1em; }
|
|
1416
|
+
.step-title { flex: 1; font-size: 1em; }
|
|
1417
|
+
.step-duration { color: var(--dark-gray-color); font-size: 0.9em; }
|
|
1418
|
+
.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); }
|
|
1419
|
+
.step-info { margin-bottom: 8px; }
|
|
1420
|
+
.step-error { color: var(--danger-color); margin-top: 12px; padding: 14px; background: rgba(244,67,54,0.05); border-radius: 4px; font-size: 0.95em; border-left: 3px solid var(--danger-color); }
|
|
1421
|
+
.step-error pre.stack-trace { margin-top: 10px; padding: 12px; background-color: rgba(0,0,0,0.03); border-radius: 4px; font-size:0.9em; max-height: 280px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; }
|
|
1422
|
+
.step-hook { background-color: rgba(33,150,243,0.04); border-left: 3px solid var(--info-color) !important; }
|
|
1423
|
+
.step-hook .step-title { font-style: italic; color: var(--info-color)}
|
|
1424
|
+
.nested-steps { margin-top: 12px; }
|
|
1425
|
+
|
|
1426
|
+
.attachments-section { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--light-gray-color); }
|
|
1427
|
+
.attachments-section h4 { margin-top: 0; margin-bottom: 20px; font-size: 1.1em; color: var(--text-color); }
|
|
1428
|
+
.attachments-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 22px; }
|
|
912
1429
|
.attachment-item {
|
|
913
|
-
border: 1px solid #
|
|
914
|
-
|
|
915
|
-
|
|
1430
|
+
border: 1px solid var(--border-color); border-radius: var(--border-radius); background-color: #fff;
|
|
1431
|
+
box-shadow: var(--box-shadow-light); overflow: hidden; display: flex; flex-direction: column;
|
|
1432
|
+
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
|
|
916
1433
|
}
|
|
917
|
-
|
|
1434
|
+
.attachment-item:hover { transform: translateY(-4px); box-shadow: var(--box-shadow); }
|
|
918
1435
|
.attachment-item img {
|
|
919
|
-
width: 100%;
|
|
920
|
-
|
|
921
|
-
display: block;
|
|
1436
|
+
width: 100%; height: 180px; object-fit: cover; display: block;
|
|
1437
|
+
border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease;
|
|
922
1438
|
}
|
|
923
|
-
|
|
924
|
-
.
|
|
925
|
-
|
|
926
|
-
background:
|
|
927
|
-
padding: 2px 6px;
|
|
928
|
-
border-radius: 4px;
|
|
929
|
-
font-size: 12px;
|
|
930
|
-
margin-right: 5px;
|
|
931
|
-
}
|
|
932
|
-
.status-badge {
|
|
933
|
-
padding: 3px 8px;
|
|
934
|
-
border-radius: 4px;
|
|
935
|
-
font-size: 12px;
|
|
936
|
-
font-weight: bold;
|
|
937
|
-
color: white;
|
|
938
|
-
text-transform: uppercase;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
span.status-passed {
|
|
942
|
-
background-color: #4CAF50 !important; /* Bright green */
|
|
943
|
-
color: white;
|
|
944
|
-
border-radius: 4px;
|
|
945
|
-
padding: 4px;
|
|
946
|
-
font-size: 0.75rem;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
span.status-failed {
|
|
950
|
-
background-color: #F44336 !important; /* Bright red */
|
|
951
|
-
color: white;
|
|
952
|
-
border-radius: 4px;
|
|
953
|
-
padding: 4px;
|
|
954
|
-
font-size: 0.75rem;
|
|
1439
|
+
.attachment-item a:hover img { opacity: 0.85; }
|
|
1440
|
+
.attachment-caption {
|
|
1441
|
+
padding: 12px 15px; font-size: 0.9em; text-align: center;
|
|
1442
|
+
color: var(--text-color-secondary); word-break: break-word; background-color: var(--light-gray-color);
|
|
955
1443
|
}
|
|
1444
|
+
.video-item a, .trace-item a { display: block; margin-bottom: 8px; color: var(--primary-color); text-decoration: none; font-weight: 500; }
|
|
1445
|
+
.video-item a:hover, .trace-item a:hover { text-decoration: underline; }
|
|
1446
|
+
.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;}
|
|
956
1447
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1448
|
+
.trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
|
|
1449
|
+
/* Removed D3 specific .chart-axis, .main-chart-title, .chart-line.* rules */
|
|
1450
|
+
/* Highcharts styles its elements with classes like .highcharts-axis, .highcharts-title etc. */
|
|
1451
|
+
|
|
1452
|
+
.test-history-container h2.tab-main-title { font-size: 1.6em; margin-bottom: 18px; color: var(--primary-color); border-bottom: 1px solid var(--border-color); padding-bottom: 12px;}
|
|
1453
|
+
.test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
|
|
1454
|
+
.test-history-card {
|
|
1455
|
+
background: var(--card-background-color); border: 1px solid var(--border-color); border-radius: var(--border-radius);
|
|
1456
|
+
padding: 22px; box-shadow: var(--box-shadow-light); display: flex; flex-direction: column;
|
|
1457
|
+
}
|
|
1458
|
+
.test-history-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 14px; border-bottom: 1px solid var(--light-gray-color); }
|
|
1459
|
+
.test-history-header h3 { margin: 0; font-size: 1.15em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1460
|
+
.test-history-header p { font-weight: 500 }
|
|
1461
|
+
.test-history-trend { margin-bottom: 20px; min-height: 110px; }
|
|
1462
|
+
.test-history-trend div[id^="testHistoryChart-"] { /* Highcharts container for history */
|
|
1463
|
+
display: block; margin: 0 auto; max-width:100%; height: 100px; width: 320px; /* Match JS config */
|
|
1464
|
+
}
|
|
1465
|
+
/* .test-history-trend .small-axis text {font-size: 11px;} Removed D3 specific */
|
|
1466
|
+
.test-history-details-collapsible summary { cursor: pointer; font-size: 1em; color: var(--primary-color); margin-bottom: 10px; font-weight:500; }
|
|
1467
|
+
.test-history-details-collapsible summary:hover {text-decoration: underline;}
|
|
1468
|
+
.test-history-details table { width: 100%; border-collapse: collapse; font-size: 0.95em; }
|
|
1469
|
+
.test-history-details th, .test-history-details td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--light-gray-color); }
|
|
1470
|
+
.test-history-details th { background-color: var(--light-gray-color); font-weight: 600; }
|
|
1471
|
+
.status-badge-small {
|
|
1472
|
+
padding: 3px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 600;
|
|
1473
|
+
color: white; text-transform: uppercase; display: inline-block;
|
|
1474
|
+
}
|
|
1475
|
+
.status-badge-small.status-passed { background-color: var(--success-color); }
|
|
1476
|
+
.status-badge-small.status-failed { background-color: var(--danger-color); }
|
|
1477
|
+
.status-badge-small.status-skipped { background-color: var(--warning-color); }
|
|
1478
|
+
.status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
|
|
1479
|
+
|
|
1480
|
+
.no-data, .no-tests, .no-steps, .no-data-chart {
|
|
1481
|
+
padding: 28px; text-align: center; color: var(--dark-gray-color); font-style: italic; font-size:1.1em;
|
|
1482
|
+
background-color: var(--light-gray-color); border-radius: var(--border-radius); margin: 18px 0;
|
|
1483
|
+
border: 1px dashed var(--medium-gray-color);
|
|
1484
|
+
}
|
|
1485
|
+
.no-data-chart {font-size: 0.95em; padding: 18px;}
|
|
1486
|
+
|
|
1487
|
+
#test-ai iframe { border: 1px solid var(--border-color); width: 100%; height: 85vh; border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); }
|
|
1488
|
+
#test-ai p {margin-bottom: 18px; font-size: 1em; color: var(--text-color-secondary);}
|
|
1489
|
+
pre .stdout-log { background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2; }
|
|
1490
|
+
pre .stderr-log { background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2; }
|
|
1491
|
+
|
|
1492
|
+
.trace-preview {
|
|
1493
|
+
padding: 1rem;
|
|
1494
|
+
text-align: center;
|
|
1495
|
+
background: #f5f5f5;
|
|
1496
|
+
border-bottom: 1px solid #e1e1e1;
|
|
1497
|
+
}
|
|
964
1498
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
margin: 20px 0;
|
|
971
|
-
}
|
|
1499
|
+
.trace-icon {
|
|
1500
|
+
font-size: 2rem;
|
|
1501
|
+
display: block;
|
|
1502
|
+
margin-bottom: 0.5rem;
|
|
1503
|
+
}
|
|
972
1504
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1505
|
+
.trace-name {
|
|
1506
|
+
word-break: break-word;
|
|
1507
|
+
font-size: 0.9rem;
|
|
1508
|
+
}
|
|
976
1509
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
}
|
|
1510
|
+
.trace-actions {
|
|
1511
|
+
display: flex;
|
|
1512
|
+
gap: 0.5rem;
|
|
1513
|
+
}
|
|
982
1514
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1515
|
+
.trace-actions a {
|
|
1516
|
+
flex: 1;
|
|
1517
|
+
text-align: center;
|
|
1518
|
+
padding: 0.25rem 0.5rem;
|
|
1519
|
+
font-size: 0.85rem;
|
|
1520
|
+
border-radius: 4px;
|
|
1521
|
+
text-decoration: none;
|
|
1522
|
+
}
|
|
987
1523
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
gap: 15px;
|
|
993
|
-
margin-top: 15px;
|
|
994
|
-
}
|
|
1524
|
+
.view-trace {
|
|
1525
|
+
background: #3182ce;
|
|
1526
|
+
color: white;
|
|
1527
|
+
}
|
|
995
1528
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
gap: 5px;
|
|
1000
|
-
font-size: 14px;
|
|
1001
|
-
}
|
|
1529
|
+
.view-trace:hover {
|
|
1530
|
+
background: #2c5282;
|
|
1531
|
+
}
|
|
1002
1532
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
display: inline-block;
|
|
1008
|
-
}
|
|
1533
|
+
.download-trace {
|
|
1534
|
+
background: #e2e8f0;
|
|
1535
|
+
color: #2d3748;
|
|
1536
|
+
}
|
|
1009
1537
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
.test-name {
|
|
1014
|
-
font-weight: 600;
|
|
1015
|
-
}
|
|
1016
|
-
.summary-cards {
|
|
1017
|
-
display: grid;
|
|
1018
|
-
gap: 20px;
|
|
1019
|
-
}
|
|
1538
|
+
.download-trace:hover {
|
|
1539
|
+
background: #cbd5e0;
|
|
1540
|
+
}
|
|
1020
1541
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
}
|
|
1027
|
-
/* Responsive Styles */
|
|
1028
|
-
/* Mobile (up to 480px) and Tablet (481px to 768px) Responsive Styles */
|
|
1542
|
+
.filters button.clear-filters-btn {
|
|
1543
|
+
background-color: var(--medium-gray-color); /* Or any other suitable color */
|
|
1544
|
+
color: var(--text-color);
|
|
1545
|
+
/* Add other styling as per your .filters button style if needed */
|
|
1546
|
+
}
|
|
1029
1547
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
align-items: flex-start;
|
|
1057
|
-
gap:
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
/* Dashboard Grid adjustments */
|
|
1078
|
-
.dashboard-grid {
|
|
1079
|
-
grid-template-columns: 1fr;
|
|
1080
|
-
gap: 15px;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
.summary-card {
|
|
1084
|
-
padding: 15px;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
.summary-card .value {
|
|
1088
|
-
font-size: 24px;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
.pie-chart-container {
|
|
1092
|
-
padding: 15px;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
.pie-chart-container svg {
|
|
1096
|
-
width: 300px;
|
|
1097
|
-
height: 300px;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
/* Test Suites Widget adjustments */
|
|
1101
|
-
.suites-widget {
|
|
1102
|
-
padding: 8px;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
.suites-header {
|
|
1106
|
-
flex-direction: column;
|
|
1107
|
-
align-items: flex-start;
|
|
1108
|
-
gap: 10px;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
.suites-grid {
|
|
1112
|
-
grid-template-columns: 1fr;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
/* Test Run Summary adjustments */
|
|
1116
|
-
.filters {
|
|
1117
|
-
flex-direction: column;
|
|
1118
|
-
gap: 8px;
|
|
1119
|
-
padding: 0.75rem;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
.filters input,
|
|
1123
|
-
.filters select {
|
|
1124
|
-
width: 100%;
|
|
1125
|
-
padding: 8px;
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
.filters button {
|
|
1129
|
-
width: 100%;
|
|
1130
|
-
margin-top: 5px;
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
.test-suite {
|
|
1134
|
-
margin-bottom: 10px;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
.suite-header {
|
|
1138
|
-
padding: 10px;
|
|
1139
|
-
flex-wrap: wrap;
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
.test-name {
|
|
1143
|
-
display: block;
|
|
1144
|
-
width: 100%;
|
|
1145
|
-
margin-top: 5px;
|
|
1146
|
-
font-weight: 600;
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
.test-meta {
|
|
1150
|
-
margin-top: 5px;
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
.suite-content {
|
|
1154
|
-
padding: 10px;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
.steps-list {
|
|
1158
|
-
margin: 10px 0;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
.step-header {
|
|
1162
|
-
padding: 6px;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
.step-icon {
|
|
1166
|
-
font-size: 14px;
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
.step-title {
|
|
1170
|
-
font-size: 14px;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
.step-duration {
|
|
1174
|
-
font-size: 11px;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
.attachments-grid {
|
|
1178
|
-
grid-template-columns: repeat(2, 1fr);
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
/* Specific adjustments for mobile only (up to 480px) */
|
|
1182
|
-
@media (max-width: 480px) {
|
|
1183
|
-
.header h1 {
|
|
1184
|
-
font-size: 20px;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
.summary-card .value {
|
|
1188
|
-
font-size: 22px;
|
|
1189
|
-
}
|
|
1190
|
-
.pie-chart-container {
|
|
1191
|
-
grid-column: span 1;
|
|
1192
|
-
}
|
|
1193
|
-
.pie-chart-container svg {
|
|
1194
|
-
width: 300px;
|
|
1195
|
-
height: 300px;
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
.attachments-grid {
|
|
1199
|
-
grid-template-columns: 1fr;
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
.step-item {
|
|
1203
|
-
padding-left: 0 !important;
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
.nested-steps {
|
|
1207
|
-
padding-left: 10px;
|
|
1548
|
+
.filters button.clear-filters-btn:hover {
|
|
1549
|
+
background-color: var(--dark-gray-color); /* Darker on hover */
|
|
1550
|
+
color: #fff;
|
|
1551
|
+
}
|
|
1552
|
+
@media (max-width: 1200px) {
|
|
1553
|
+
.trend-charts-row { grid-template-columns: 1fr; }
|
|
1554
|
+
}
|
|
1555
|
+
@media (max-width: 992px) {
|
|
1556
|
+
.dashboard-bottom-row { grid-template-columns: 1fr; }
|
|
1557
|
+
.pie-chart-wrapper div[id^="pieChart-"] { max-width: 350px; margin: 0 auto; }
|
|
1558
|
+
.filters input { min-width: 180px; }
|
|
1559
|
+
.filters select { min-width: 150px; }
|
|
1560
|
+
}
|
|
1561
|
+
@media (max-width: 768px) {
|
|
1562
|
+
body { font-size: 15px; }
|
|
1563
|
+
.container { margin: 10px; padding: 20px; }
|
|
1564
|
+
.header { flex-direction: column; align-items: flex-start; gap: 15px; }
|
|
1565
|
+
.header h1 { font-size: 1.6em; }
|
|
1566
|
+
.run-info { text-align: left; font-size:0.9em; }
|
|
1567
|
+
.tabs { margin-bottom: 25px;}
|
|
1568
|
+
.tab-button { padding: 12px 20px; font-size: 1.05em;}
|
|
1569
|
+
.dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;}
|
|
1570
|
+
.summary-card .value {font-size: 2em;}
|
|
1571
|
+
.summary-card h3 {font-size: 0.95em;}
|
|
1572
|
+
.filters { flex-direction: column; padding: 18px; gap: 12px;}
|
|
1573
|
+
.filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;}
|
|
1574
|
+
.test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; }
|
|
1575
|
+
.test-case-summary {gap: 10px;}
|
|
1576
|
+
.test-case-title {font-size: 1.05em;}
|
|
1577
|
+
.test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;}
|
|
1578
|
+
.attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;}
|
|
1579
|
+
.test-history-grid {grid-template-columns: 1fr;}
|
|
1580
|
+
.pie-chart-wrapper {min-height: auto;}
|
|
1581
|
+
}
|
|
1582
|
+
@media (max-width: 480px) {
|
|
1583
|
+
body {font-size: 14px;}
|
|
1584
|
+
.container {padding: 15px;}
|
|
1585
|
+
.header h1 {font-size: 1.4em;}
|
|
1586
|
+
#report-logo { height: 35px; width: 35px; }
|
|
1587
|
+
.tab-button {padding: 10px 15px; font-size: 1em;}
|
|
1588
|
+
.summary-card .value {font-size: 1.8em;}
|
|
1589
|
+
.attachments-grid {grid-template-columns: 1fr;}
|
|
1590
|
+
.step-item {padding-left: calc(var(--depth, 0) * 18px);}
|
|
1591
|
+
.test-case-content, .step-details {padding: 15px;}
|
|
1592
|
+
.trend-charts-row {gap: 20px;}
|
|
1593
|
+
.trend-chart {padding: 20px;}
|
|
1208
1594
|
}
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
1595
|
</style>
|
|
1212
1596
|
</head>
|
|
1213
1597
|
<body>
|
|
1214
1598
|
<div class="container">
|
|
1215
1599
|
<header class="header">
|
|
1216
|
-
<div
|
|
1217
|
-
<img id="report-logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+
|
|
1218
|
-
<h1>
|
|
1219
|
-
Playwright Pulse Report
|
|
1220
|
-
</h1>
|
|
1600
|
+
<div class="header-title">
|
|
1601
|
+
<img id="report-logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMNCA3bDggNSA4LTUtOC01eiIgZmlsbD0iIzNmNTFiNSIvPjxwYXRoIGQ9Ik0xMiA2TDQgMTFsOCA1IDgtNS04LTV6IiBmaWxsPSIjNDI4NWY0Ii8+PHBhdGggZD0iTTEyIDEwbC04IDUgOCA1IDgtNS04LTV6IiBmaWxsPSIjM2Q1NWI0Ii8+PC9zdmc+" alt="Report Logo">
|
|
1602
|
+
<h1>Playwright Pulse Report</h1>
|
|
1221
1603
|
</div>
|
|
1222
1604
|
<div class="run-info">
|
|
1223
1605
|
<strong>Run Date:</strong> ${formatDate(
|
|
@@ -1232,328 +1614,532 @@ function generateHTML(reportData) {
|
|
|
1232
1614
|
<div class="tabs">
|
|
1233
1615
|
<button class="tab-button active" data-tab="dashboard">Dashboard</button>
|
|
1234
1616
|
<button class="tab-button" data-tab="test-runs">Test Run Summary</button>
|
|
1617
|
+
<button class="tab-button" data-tab="test-history">Test History</button>
|
|
1235
1618
|
<button class="tab-button" data-tab="test-ai">AI Analysis</button>
|
|
1236
1619
|
</div>
|
|
1237
1620
|
|
|
1238
1621
|
<div id="dashboard" class="tab-content active">
|
|
1239
1622
|
<div class="dashboard-grid">
|
|
1240
1623
|
<div class="summary-card">
|
|
1241
|
-
<h3>Total Tests</h3
|
|
1242
|
-
|
|
1624
|
+
<h3>Total Tests</h3><div class="value">${
|
|
1625
|
+
runSummary.totalTests
|
|
1626
|
+
}</div>
|
|
1243
1627
|
</div>
|
|
1244
1628
|
<div class="summary-card status-passed">
|
|
1245
|
-
<h3>Passed</h3>
|
|
1246
|
-
<div class="
|
|
1247
|
-
<div class="trend">${passPercentage}%</div>
|
|
1629
|
+
<h3>Passed</h3><div class="value">${runSummary.passed}</div>
|
|
1630
|
+
<div class="trend-percentage">${passPercentage}%</div>
|
|
1248
1631
|
</div>
|
|
1249
1632
|
<div class="summary-card status-failed">
|
|
1250
|
-
<h3>Failed</h3>
|
|
1251
|
-
<div class="
|
|
1252
|
-
<div class="trend">${failPercentage}%</div>
|
|
1633
|
+
<h3>Failed</h3><div class="value">${runSummary.failed}</div>
|
|
1634
|
+
<div class="trend-percentage">${failPercentage}%</div>
|
|
1253
1635
|
</div>
|
|
1254
1636
|
<div class="summary-card status-skipped">
|
|
1255
|
-
<h3>Skipped</h3
|
|
1256
|
-
|
|
1257
|
-
|
|
1637
|
+
<h3>Skipped</h3><div class="value">${
|
|
1638
|
+
runSummary.skipped || 0
|
|
1639
|
+
}</div>
|
|
1640
|
+
<div class="trend-percentage">${skipPercentage}%</div>
|
|
1258
1641
|
</div>
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
${generateSuitesWidget(suitesData)}
|
|
1267
|
-
<div class="summary-cards">
|
|
1268
|
-
<div class="summary-card avg-time">
|
|
1269
|
-
<h3>Avg. Time</h3>
|
|
1270
|
-
<div class="value">${avgTestDuration}</div>
|
|
1271
|
-
</div>
|
|
1272
|
-
<div class="summary-card avg-time">
|
|
1273
|
-
<h3>Total Time</h3>
|
|
1274
|
-
<div class="value">${formatDuration(
|
|
1275
|
-
runSummary.duration
|
|
1276
|
-
)}</div>
|
|
1277
|
-
</div>
|
|
1278
|
-
</div>
|
|
1642
|
+
<div class="summary-card">
|
|
1643
|
+
<h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div>
|
|
1644
|
+
</div>
|
|
1645
|
+
<div class="summary-card">
|
|
1646
|
+
<h3>Run Duration</h3><div class="value">${formatDuration(
|
|
1647
|
+
runSummary.duration
|
|
1648
|
+
)}</div>
|
|
1279
1649
|
</div>
|
|
1280
1650
|
</div>
|
|
1651
|
+
<div class="dashboard-bottom-row">
|
|
1652
|
+
${generatePieChart(
|
|
1653
|
+
// Changed from generatePieChartD3
|
|
1654
|
+
[
|
|
1655
|
+
{ label: "Passed", value: runSummary.passed },
|
|
1656
|
+
{ label: "Failed", value: runSummary.failed },
|
|
1657
|
+
{ label: "Skipped", value: runSummary.skipped || 0 },
|
|
1658
|
+
],
|
|
1659
|
+
400, // Default width
|
|
1660
|
+
390 // Default height (adjusted for legend + title)
|
|
1661
|
+
)}
|
|
1662
|
+
${generateSuitesWidget(suitesData)}
|
|
1663
|
+
</div>
|
|
1281
1664
|
</div>
|
|
1282
1665
|
|
|
1283
1666
|
<div id="test-runs" class="tab-content">
|
|
1284
1667
|
<div class="filters">
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1668
|
+
<input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
|
|
1669
|
+
<select id="filter-status">
|
|
1670
|
+
<option value="">All Statuses</option>
|
|
1671
|
+
<option value="passed">Passed</option>
|
|
1672
|
+
<option value="failed">Failed</option>
|
|
1673
|
+
<option value="skipped">Skipped</option>
|
|
1674
|
+
</select>
|
|
1675
|
+
<select id="filter-browser">
|
|
1676
|
+
<option value="">All Browsers</option>
|
|
1677
|
+
{/* Dynamically generated options will be here */}
|
|
1678
|
+
${Array.from(
|
|
1679
|
+
new Set((results || []).map((test) => test.browser || "unknown"))
|
|
1680
|
+
)
|
|
1681
|
+
.map(
|
|
1682
|
+
(browser) =>
|
|
1683
|
+
`<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
|
|
1684
|
+
browser
|
|
1685
|
+
)}</option>`
|
|
1686
|
+
)
|
|
1687
|
+
.join("")}
|
|
1688
|
+
</select>
|
|
1689
|
+
<button id="expand-all-tests">Expand All</button>
|
|
1690
|
+
<button id="collapse-all-tests">Collapse All</button>
|
|
1691
|
+
<button id="clear-run-summary-filters" class="clear-filters-btn">Clear Filters</button>
|
|
1692
|
+
</div>
|
|
1693
|
+
<div class="test-cases-list">
|
|
1308
1694
|
${generateTestCasesHTML()}
|
|
1309
1695
|
</div>
|
|
1310
1696
|
</div>
|
|
1697
|
+
|
|
1698
|
+
<div id="test-history" class="tab-content">
|
|
1699
|
+
<h2 class="tab-main-title">Execution Trends</h2>
|
|
1700
|
+
<div class="trend-charts-row">
|
|
1701
|
+
<div class="trend-chart">
|
|
1702
|
+
<h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
|
|
1703
|
+
${
|
|
1704
|
+
trendData && trendData.overall && trendData.overall.length > 0
|
|
1705
|
+
? generateTestTrendsChart(trendData)
|
|
1706
|
+
: '<div class="no-data">Overall trend data not available for test counts.</div>'
|
|
1707
|
+
}
|
|
1708
|
+
</div>
|
|
1709
|
+
<div class="trend-chart">
|
|
1710
|
+
<h3 class="chart-title-header">Execution Duration Trends</h3>
|
|
1711
|
+
${
|
|
1712
|
+
trendData && trendData.overall && trendData.overall.length > 0
|
|
1713
|
+
? generateDurationTrendChart(trendData)
|
|
1714
|
+
: '<div class="no-data">Overall trend data not available for durations.</div>'
|
|
1715
|
+
}
|
|
1716
|
+
</div>
|
|
1717
|
+
</div>
|
|
1718
|
+
<h2 class="tab-main-title">Individual Test History</h2>
|
|
1719
|
+
${
|
|
1720
|
+
trendData &&
|
|
1721
|
+
trendData.testRuns &&
|
|
1722
|
+
Object.keys(trendData.testRuns).length > 0
|
|
1723
|
+
? generateTestHistoryContent(trendData)
|
|
1724
|
+
: '<div class="no-data">Individual test history data not available.</div>'
|
|
1725
|
+
}
|
|
1726
|
+
</div>
|
|
1727
|
+
|
|
1311
1728
|
<div id="test-ai" class="tab-content">
|
|
1312
|
-
|
|
1729
|
+
<iframe
|
|
1313
1730
|
src="https://ai-test-analyser.netlify.app/"
|
|
1314
1731
|
width="100%"
|
|
1315
|
-
height="
|
|
1316
|
-
|
|
1732
|
+
height="100%"
|
|
1733
|
+
frameborder="0"
|
|
1734
|
+
allowfullscreen
|
|
1735
|
+
style="border: none; height: 100vh;">
|
|
1317
1736
|
</iframe>
|
|
1318
1737
|
</div>
|
|
1738
|
+
<footer style="
|
|
1739
|
+
padding: 0.5rem;
|
|
1740
|
+
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
|
1741
|
+
text-align: center;
|
|
1742
|
+
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
1743
|
+
">
|
|
1744
|
+
<div style="
|
|
1745
|
+
display: inline-flex;
|
|
1746
|
+
align-items: center;
|
|
1747
|
+
gap: 0.5rem;
|
|
1748
|
+
color: #333;
|
|
1749
|
+
font-size: 0.9rem;
|
|
1750
|
+
font-weight: 600;
|
|
1751
|
+
letter-spacing: 0.5px;
|
|
1752
|
+
">
|
|
1753
|
+
<img width="48" height="48" src="https://img.icons8.com/emoji/48/index-pointing-at-the-viewer-light-skin-tone-emoji.png" alt="index-pointing-at-the-viewer-light-skin-tone-emoji"/>
|
|
1754
|
+
<span>Created by</span>
|
|
1755
|
+
<a href="https://github.com/Arghajit47"
|
|
1756
|
+
target="_blank"
|
|
1757
|
+
rel="noopener noreferrer"
|
|
1758
|
+
style="
|
|
1759
|
+
color: #7737BF;
|
|
1760
|
+
font-weight: 700;
|
|
1761
|
+
font-style: italic;
|
|
1762
|
+
text-decoration: none;
|
|
1763
|
+
transition: all 0.2s ease;
|
|
1764
|
+
"
|
|
1765
|
+
onmouseover="this.style.color='#BF5C37'"
|
|
1766
|
+
onmouseout="this.style.color='#7737BF'">
|
|
1767
|
+
Arghajit Singha
|
|
1768
|
+
</a>
|
|
1769
|
+
</div>
|
|
1770
|
+
<div style="
|
|
1771
|
+
margin-top: 0.5rem;
|
|
1772
|
+
font-size: 0.75rem;
|
|
1773
|
+
color: #666;
|
|
1774
|
+
">
|
|
1775
|
+
Crafted with precision
|
|
1776
|
+
</div>
|
|
1777
|
+
</footer>
|
|
1319
1778
|
</div>
|
|
1320
1779
|
|
|
1321
|
-
<script>
|
|
1322
|
-
// Tab switching functionality
|
|
1323
|
-
document.querySelectorAll('.tab-button').forEach(button => {
|
|
1324
|
-
button.addEventListener('click', () => {
|
|
1325
|
-
// Remove active class from all buttons and contents
|
|
1326
|
-
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
|
|
1327
|
-
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
|
1328
|
-
|
|
1329
|
-
// Add active class to clicked button and corresponding content
|
|
1330
|
-
const tabId = button.getAttribute('data-tab');
|
|
1331
|
-
button.classList.add('active');
|
|
1332
|
-
document.getElementById(tabId).classList.add('active');
|
|
1333
|
-
});
|
|
1334
|
-
});
|
|
1335
|
-
|
|
1336
|
-
// Test filtering functionality
|
|
1337
|
-
function setupFilters() {
|
|
1338
|
-
const nameFilter = document.getElementById('filter-name');
|
|
1339
|
-
const statusFilter = document.getElementById('filter-status');
|
|
1340
|
-
const browserFilter = document.getElementById('filter-browser');
|
|
1341
|
-
|
|
1342
|
-
const filterTests = () => {
|
|
1343
|
-
const nameValue = nameFilter.value.toLowerCase();
|
|
1344
|
-
const statusValue = statusFilter.value;
|
|
1345
|
-
const browserValue = browserFilter.value;
|
|
1346
|
-
|
|
1347
|
-
document.querySelectorAll('.test-suite').forEach(suite => {
|
|
1348
|
-
const name = suite.querySelector('.test-name').textContent.toLowerCase();
|
|
1349
|
-
const status = suite.getAttribute('data-status');
|
|
1350
|
-
const browser = suite.getAttribute('data-browser');
|
|
1351
|
-
|
|
1352
|
-
const nameMatch = name.includes(nameValue);
|
|
1353
|
-
const statusMatch = !statusValue || status === statusValue;
|
|
1354
|
-
const browserMatch = !browserValue || browser === browserValue;
|
|
1355
|
-
|
|
1356
|
-
if (nameMatch && statusMatch && browserMatch) {
|
|
1357
|
-
suite.style.display = 'block';
|
|
1358
|
-
} else {
|
|
1359
|
-
suite.style.display = 'none';
|
|
1360
|
-
}
|
|
1361
|
-
});
|
|
1362
|
-
};
|
|
1363
|
-
|
|
1364
|
-
nameFilter.addEventListener('input', filterTests);
|
|
1365
|
-
statusFilter.addEventListener('change', filterTests);
|
|
1366
|
-
browserFilter.addEventListener('change', filterTests);
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
// Test expansion functionality
|
|
1370
|
-
function toggleTestDetails(header) {
|
|
1371
|
-
const content = header.nextElementSibling;
|
|
1372
|
-
content.style.display = content.style.display === 'block' ? 'none' : 'block';
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
// Step expansion functionality
|
|
1376
|
-
function toggleStepDetails(header) {
|
|
1377
|
-
const details = header.nextElementSibling;
|
|
1378
|
-
details.style.display = details.style.display === 'block' ? 'none' : 'block';
|
|
1379
|
-
|
|
1380
|
-
// Toggle nested steps if they exist
|
|
1381
|
-
const nestedSteps = header.parentElement.querySelector('.nested-steps');
|
|
1382
|
-
if (nestedSteps) {
|
|
1383
|
-
nestedSteps.style.display = nestedSteps.style.display === 'block' ? 'none' : 'block';
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
// Expand all tests
|
|
1388
|
-
function expandAllTests() {
|
|
1389
|
-
document.querySelectorAll('.suite-content').forEach(el => {
|
|
1390
|
-
el.style.display = 'block';
|
|
1391
|
-
});
|
|
1392
|
-
document.querySelectorAll('.step-details').forEach(el => {
|
|
1393
|
-
el.style.display = 'block';
|
|
1394
|
-
});
|
|
1395
|
-
document.querySelectorAll('.nested-steps').forEach(el => {
|
|
1396
|
-
el.style.display = 'block';
|
|
1397
|
-
});
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
// Collapse all tests
|
|
1401
|
-
function collapseAllTests() {
|
|
1402
|
-
document.querySelectorAll('.suite-content').forEach(el => {
|
|
1403
|
-
el.style.display = 'none';
|
|
1404
|
-
});
|
|
1405
|
-
document.querySelectorAll('.step-details').forEach(el => {
|
|
1406
|
-
el.style.display = 'none';
|
|
1407
|
-
});
|
|
1408
|
-
document.querySelectorAll('.nested-steps').forEach(el => {
|
|
1409
|
-
el.style.display = 'none';
|
|
1410
|
-
});
|
|
1411
|
-
}
|
|
1412
1780
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
toggleStepDetails(this);
|
|
1421
|
-
});
|
|
1422
|
-
});
|
|
1423
|
-
|
|
1424
|
-
// Make test headers clickable
|
|
1425
|
-
document.querySelectorAll('.suite-header').forEach(header => {
|
|
1426
|
-
header.addEventListener('click', function() {
|
|
1427
|
-
toggleTestDetails(this);
|
|
1428
|
-
});
|
|
1429
|
-
});
|
|
1430
|
-
});
|
|
1431
|
-
|
|
1432
|
-
// Enhanced expand/collapse functionality
|
|
1433
|
-
function toggleTestDetails(header) {
|
|
1434
|
-
const content = header.nextElementSibling;
|
|
1435
|
-
const isExpanded = content.style.display === 'block';
|
|
1436
|
-
content.style.display = isExpanded ? 'none' : 'block';
|
|
1437
|
-
header.setAttribute('aria-expanded', !isExpanded);
|
|
1781
|
+
<script>
|
|
1782
|
+
// Ensure formatDuration is globally available
|
|
1783
|
+
if (typeof formatDuration === 'undefined') {
|
|
1784
|
+
function formatDuration(ms) {
|
|
1785
|
+
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
1786
|
+
return (ms / 1000).toFixed(1) + "s";
|
|
1787
|
+
}
|
|
1438
1788
|
}
|
|
1439
1789
|
|
|
1440
|
-
function
|
|
1441
|
-
|
|
1442
|
-
|
|
1790
|
+
function initializeReportInteractivity() {
|
|
1791
|
+
const tabButtons = document.querySelectorAll('.tab-button');
|
|
1792
|
+
const tabContents = document.querySelectorAll('.tab-content');
|
|
1793
|
+
tabButtons.forEach(button => {
|
|
1794
|
+
button.addEventListener('click', () => {
|
|
1795
|
+
tabButtons.forEach(btn => btn.classList.remove('active'));
|
|
1796
|
+
tabContents.forEach(content => content.classList.remove('active'));
|
|
1797
|
+
button.classList.add('active');
|
|
1798
|
+
const tabId = button.getAttribute('data-tab');
|
|
1799
|
+
const activeContent = document.getElementById(tabId);
|
|
1800
|
+
if (activeContent) activeContent.classList.add('active');
|
|
1801
|
+
});
|
|
1802
|
+
});
|
|
1443
1803
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1804
|
+
// --- Test Run Summary Filters ---
|
|
1805
|
+
const nameFilter = document.getElementById('filter-name');
|
|
1806
|
+
const statusFilter = document.getElementById('filter-status');
|
|
1807
|
+
const browserFilter = document.getElementById('filter-browser');
|
|
1808
|
+
const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters'); // Get the new button
|
|
1809
|
+
|
|
1810
|
+
function filterTestCases() {
|
|
1811
|
+
const nameValue = nameFilter ? nameFilter.value.toLowerCase() : "";
|
|
1812
|
+
const statusValue = statusFilter ? statusFilter.value : "";
|
|
1813
|
+
const browserValue = browserFilter ? browserFilter.value : "";
|
|
1814
|
+
|
|
1815
|
+
document.querySelectorAll('#test-runs .test-case').forEach(testCaseElement => {
|
|
1816
|
+
const titleElement = testCaseElement.querySelector('.test-case-title');
|
|
1817
|
+
const fullTestName = titleElement ? titleElement.getAttribute('title').toLowerCase() : "";
|
|
1818
|
+
const status = testCaseElement.getAttribute('data-status');
|
|
1819
|
+
const browser = testCaseElement.getAttribute('data-browser');
|
|
1820
|
+
|
|
1821
|
+
const nameMatch = fullTestName.includes(nameValue);
|
|
1822
|
+
const statusMatch = !statusValue || status === statusValue;
|
|
1823
|
+
const browserMatch = !browserValue || browser === browserValue;
|
|
1824
|
+
|
|
1825
|
+
testCaseElement.style.display = (nameMatch && statusMatch && browserMatch) ? '' : 'none';
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
if(nameFilter) nameFilter.addEventListener('input', filterTestCases);
|
|
1829
|
+
if(statusFilter) statusFilter.addEventListener('change', filterTestCases);
|
|
1830
|
+
if(browserFilter) browserFilter.addEventListener('change', filterTestCases);
|
|
1831
|
+
|
|
1832
|
+
// Event listener for clearing Test Run Summary filters
|
|
1833
|
+
if (clearRunSummaryFiltersBtn) {
|
|
1834
|
+
clearRunSummaryFiltersBtn.addEventListener('click', () => {
|
|
1835
|
+
if (nameFilter) nameFilter.value = '';
|
|
1836
|
+
if (statusFilter) statusFilter.value = '';
|
|
1837
|
+
if (browserFilter) browserFilter.value = '';
|
|
1838
|
+
filterTestCases(); // Re-apply filters (which will show all)
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// --- Test History Filters ---
|
|
1843
|
+
const historyNameFilter = document.getElementById('history-filter-name');
|
|
1844
|
+
const historyStatusFilter = document.getElementById('history-filter-status');
|
|
1845
|
+
const clearHistoryFiltersBtn = document.getElementById('clear-history-filters'); // Get the new button
|
|
1846
|
+
|
|
1847
|
+
|
|
1848
|
+
function filterTestHistoryCards() {
|
|
1849
|
+
const nameValue = historyNameFilter ? historyNameFilter.value.toLowerCase() : "";
|
|
1850
|
+
const statusValue = historyStatusFilter ? historyStatusFilter.value : "";
|
|
1851
|
+
|
|
1852
|
+
document.querySelectorAll('.test-history-card').forEach(card => {
|
|
1853
|
+
const testTitle = card.getAttribute('data-test-name').toLowerCase();
|
|
1854
|
+
const latestStatus = card.getAttribute('data-latest-status');
|
|
1855
|
+
|
|
1856
|
+
const nameMatch = testTitle.includes(nameValue);
|
|
1857
|
+
const statusMatch = !statusValue || latestStatus === statusValue;
|
|
1858
|
+
|
|
1859
|
+
card.style.display = (nameMatch && statusMatch) ? '' : 'none';
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
if(historyNameFilter) historyNameFilter.addEventListener('input', filterTestHistoryCards);
|
|
1863
|
+
if(historyStatusFilter) historyStatusFilter.addEventListener('change', filterTestHistoryCards);
|
|
1864
|
+
|
|
1865
|
+
// Event listener for clearing Test History filters
|
|
1866
|
+
if (clearHistoryFiltersBtn) {
|
|
1867
|
+
clearHistoryFiltersBtn.addEventListener('click', () => {
|
|
1868
|
+
if (historyNameFilter) historyNameFilter.value = '';
|
|
1869
|
+
if (historyStatusFilter) historyStatusFilter.value = '';
|
|
1870
|
+
filterTestHistoryCards(); // Re-apply filters (which will show all)
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// --- Expand/Collapse and Toggle Details Logic (remains the same) ---
|
|
1875
|
+
function toggleElementDetails(headerElement, contentSelector) {
|
|
1876
|
+
let contentElement;
|
|
1877
|
+
if (headerElement.classList.contains('test-case-header')) {
|
|
1878
|
+
contentElement = headerElement.parentElement.querySelector('.test-case-content');
|
|
1879
|
+
} else if (headerElement.classList.contains('step-header')) {
|
|
1880
|
+
contentElement = headerElement.nextElementSibling;
|
|
1881
|
+
if (!contentElement || !contentElement.matches(contentSelector || '.step-details')) {
|
|
1882
|
+
contentElement = null;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1447
1885
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1886
|
+
if (contentElement) {
|
|
1887
|
+
const isExpanded = contentElement.style.display === 'block';
|
|
1888
|
+
contentElement.style.display = isExpanded ? 'none' : 'block';
|
|
1889
|
+
headerElement.setAttribute('aria-expanded', String(!isExpanded));
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
document.querySelectorAll('#test-runs .test-case-header').forEach(header => {
|
|
1894
|
+
header.addEventListener('click', () => toggleElementDetails(header));
|
|
1895
|
+
});
|
|
1896
|
+
document.querySelectorAll('#test-runs .step-header').forEach(header => {
|
|
1897
|
+
header.addEventListener('click', () => toggleElementDetails(header, '.step-details'));
|
|
1898
|
+
});
|
|
1455
1899
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
el.style.display = 'block';
|
|
1459
|
-
});
|
|
1460
|
-
document.querySelectorAll('.step-details').forEach(el => {
|
|
1461
|
-
el.style.display = 'block';
|
|
1462
|
-
});
|
|
1463
|
-
document.querySelectorAll('.nested-steps').forEach(el => {
|
|
1464
|
-
el.style.display = 'block';
|
|
1465
|
-
});
|
|
1466
|
-
document.querySelectorAll('[aria-expanded]').forEach(el => {
|
|
1467
|
-
el.setAttribute('aria-expanded', 'true');
|
|
1468
|
-
});
|
|
1469
|
-
}
|
|
1900
|
+
const expandAllBtn = document.getElementById('expand-all-tests');
|
|
1901
|
+
const collapseAllBtn = document.getElementById('collapse-all-tests');
|
|
1470
1902
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
});
|
|
1481
|
-
document.querySelectorAll('[aria-expanded]').forEach(el => {
|
|
1482
|
-
el.setAttribute('aria-expanded', 'false');
|
|
1483
|
-
});
|
|
1903
|
+
function setAllTestRunDetailsVisibility(displayMode, ariaState) {
|
|
1904
|
+
document.querySelectorAll('#test-runs .test-case-content').forEach(el => el.style.display = displayMode);
|
|
1905
|
+
document.querySelectorAll('#test-runs .step-details').forEach(el => el.style.display = displayMode);
|
|
1906
|
+
document.querySelectorAll('#test-runs .test-case-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
|
|
1907
|
+
document.querySelectorAll('#test-runs .step-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
|
|
1911
|
+
if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
|
|
1484
1912
|
}
|
|
1913
|
+
document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
|
|
1914
|
+
</script>
|
|
1915
|
+
</body>
|
|
1916
|
+
</html>
|
|
1917
|
+
`;
|
|
1918
|
+
}
|
|
1485
1919
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
header.setAttribute('role', 'button');
|
|
1492
|
-
header.setAttribute('aria-expanded', 'false');
|
|
1920
|
+
async function runScript(scriptPath) {
|
|
1921
|
+
return new Promise((resolve, reject) => {
|
|
1922
|
+
console.log(chalk.blue(`Executing script: ${scriptPath}...`));
|
|
1923
|
+
const process = fork(scriptPath, [], {
|
|
1924
|
+
stdio: "inherit",
|
|
1493
1925
|
});
|
|
1494
1926
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
header.setAttribute('role', 'button');
|
|
1499
|
-
header.setAttribute('aria-expanded', 'false');
|
|
1927
|
+
process.on("error", (err) => {
|
|
1928
|
+
console.error(chalk.red(`Failed to start script: ${scriptPath}`), err);
|
|
1929
|
+
reject(err);
|
|
1500
1930
|
});
|
|
1501
1931
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
`;
|
|
1932
|
+
process.on("exit", (code) => {
|
|
1933
|
+
if (code === 0) {
|
|
1934
|
+
console.log(chalk.green(`Script ${scriptPath} finished successfully.`));
|
|
1935
|
+
resolve();
|
|
1936
|
+
} else {
|
|
1937
|
+
const errorMessage = `Script ${scriptPath} exited with code ${code}.`;
|
|
1938
|
+
console.error(chalk.red(errorMessage));
|
|
1939
|
+
reject(new Error(errorMessage));
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
});
|
|
1514
1943
|
}
|
|
1515
1944
|
|
|
1516
|
-
|
|
1945
|
+
async function main() {
|
|
1946
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
1947
|
+
const __dirname = path.dirname(__filename);
|
|
1517
1948
|
|
|
1518
|
-
//
|
|
1949
|
+
// Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
|
|
1950
|
+
const archiveRunScriptPath = path.resolve(
|
|
1951
|
+
__dirname,
|
|
1952
|
+
"generate-trend.mjs" // Keeping the filename as per your request
|
|
1953
|
+
);
|
|
1519
1954
|
|
|
1520
|
-
// Main execution function
|
|
1521
|
-
async function main() {
|
|
1522
1955
|
const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
1523
|
-
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE);
|
|
1956
|
+
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
|
|
1524
1957
|
const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
|
|
1525
1958
|
|
|
1526
|
-
|
|
1959
|
+
const historyDir = path.join(outputDir, "history"); // Directory for historical JSON files
|
|
1960
|
+
const HISTORY_FILE_PREFIX = "trend-"; // Match prefix used in archiving script
|
|
1961
|
+
const MAX_HISTORY_FILES_TO_LOAD_FOR_REPORT = 15; // How many historical runs to show in the report
|
|
1527
1962
|
|
|
1528
|
-
|
|
1963
|
+
console.log(chalk.blue(`Starting static HTML report generation...`));
|
|
1964
|
+
console.log(chalk.blue(`Output directory set to: ${outputDir}`));
|
|
1965
|
+
|
|
1966
|
+
// Step 1: Ensure current run data is archived to the history folder
|
|
1967
|
+
try {
|
|
1968
|
+
await runScript(archiveRunScriptPath); // This script now handles JSON history
|
|
1969
|
+
console.log(
|
|
1970
|
+
chalk.green("Current run data archiving to history completed.")
|
|
1971
|
+
);
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
console.error(
|
|
1974
|
+
chalk.red(
|
|
1975
|
+
"Failed to archive current run data. Report might use stale or incomplete historical trends."
|
|
1976
|
+
),
|
|
1977
|
+
error
|
|
1978
|
+
);
|
|
1979
|
+
// You might decide to proceed or exit depending on the importance of historical data
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// Step 2: Load current run's data (for non-trend sections of the report)
|
|
1983
|
+
let currentRunReportData; // Data for the run being reported
|
|
1529
1984
|
try {
|
|
1530
1985
|
const jsonData = await fs.readFile(reportJsonPath, "utf-8");
|
|
1531
|
-
|
|
1986
|
+
currentRunReportData = JSON.parse(jsonData);
|
|
1532
1987
|
if (
|
|
1533
|
-
!
|
|
1534
|
-
typeof
|
|
1535
|
-
!
|
|
1988
|
+
!currentRunReportData ||
|
|
1989
|
+
typeof currentRunReportData !== "object" ||
|
|
1990
|
+
!currentRunReportData.results
|
|
1536
1991
|
) {
|
|
1537
|
-
throw new Error(
|
|
1992
|
+
throw new Error(
|
|
1993
|
+
"Invalid report JSON structure. 'results' field is missing or invalid."
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
if (!Array.isArray(currentRunReportData.results)) {
|
|
1997
|
+
currentRunReportData.results = [];
|
|
1998
|
+
console.warn(
|
|
1999
|
+
chalk.yellow(
|
|
2000
|
+
"Warning: 'results' field in current run JSON was not an array. Treated as empty."
|
|
2001
|
+
)
|
|
2002
|
+
);
|
|
1538
2003
|
}
|
|
1539
2004
|
} catch (error) {
|
|
1540
|
-
console.error(
|
|
1541
|
-
|
|
2005
|
+
console.error(
|
|
2006
|
+
chalk.red(
|
|
2007
|
+
`Critical Error: Could not read or parse main report JSON at ${reportJsonPath}: ${error.message}`
|
|
2008
|
+
)
|
|
2009
|
+
);
|
|
2010
|
+
process.exit(1); // Exit if the main report for the current run is missing/invalid
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// Step 3: Load historical data for trends
|
|
2014
|
+
let historicalRuns = []; // Array of past PlaywrightPulseReport objects
|
|
2015
|
+
try {
|
|
2016
|
+
await fs.access(historyDir); // Check if history directory exists
|
|
2017
|
+
const allHistoryFiles = await fs.readdir(historyDir);
|
|
2018
|
+
|
|
2019
|
+
const jsonHistoryFiles = allHistoryFiles
|
|
2020
|
+
.filter(
|
|
2021
|
+
(file) => file.startsWith(HISTORY_FILE_PREFIX) && file.endsWith(".json")
|
|
2022
|
+
)
|
|
2023
|
+
.map((file) => {
|
|
2024
|
+
const timestampPart = file
|
|
2025
|
+
.replace(HISTORY_FILE_PREFIX, "")
|
|
2026
|
+
.replace(".json", "");
|
|
2027
|
+
return {
|
|
2028
|
+
name: file,
|
|
2029
|
+
path: path.join(historyDir, file),
|
|
2030
|
+
timestamp: parseInt(timestampPart, 10),
|
|
2031
|
+
};
|
|
2032
|
+
})
|
|
2033
|
+
.filter((file) => !isNaN(file.timestamp))
|
|
2034
|
+
.sort((a, b) => b.timestamp - a.timestamp); // Sort newest first to easily pick the latest N
|
|
2035
|
+
|
|
2036
|
+
const filesToLoadForTrend = jsonHistoryFiles.slice(
|
|
2037
|
+
0,
|
|
2038
|
+
MAX_HISTORY_FILES_TO_LOAD_FOR_REPORT
|
|
2039
|
+
);
|
|
2040
|
+
|
|
2041
|
+
for (const fileMeta of filesToLoadForTrend) {
|
|
2042
|
+
try {
|
|
2043
|
+
const fileContent = await fs.readFile(fileMeta.path, "utf-8");
|
|
2044
|
+
const runJsonData = JSON.parse(fileContent); // Each file IS a PlaywrightPulseReport
|
|
2045
|
+
historicalRuns.push(runJsonData);
|
|
2046
|
+
} catch (fileReadError) {
|
|
2047
|
+
console.warn(
|
|
2048
|
+
chalk.yellow(
|
|
2049
|
+
`Could not read/parse history file ${fileMeta.name}: ${fileReadError.message}`
|
|
2050
|
+
)
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
// Reverse to have oldest first for chart data series (if charts expect chronological)
|
|
2055
|
+
historicalRuns.reverse();
|
|
2056
|
+
console.log(
|
|
2057
|
+
chalk.green(
|
|
2058
|
+
`Loaded ${historicalRuns.length} historical run(s) for trend analysis.`
|
|
2059
|
+
)
|
|
2060
|
+
);
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
if (error.code === "ENOENT") {
|
|
2063
|
+
console.warn(
|
|
2064
|
+
chalk.yellow(
|
|
2065
|
+
`History directory '${historyDir}' not found. No historical trends will be displayed.`
|
|
2066
|
+
)
|
|
2067
|
+
);
|
|
2068
|
+
} else {
|
|
2069
|
+
console.warn(
|
|
2070
|
+
chalk.yellow(
|
|
2071
|
+
`Error loading historical data from '${historyDir}': ${error.message}`
|
|
2072
|
+
)
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
1542
2075
|
}
|
|
1543
2076
|
|
|
2077
|
+
// Step 4: Prepare trendData object in the format expected by chart functions
|
|
2078
|
+
const trendData = {
|
|
2079
|
+
overall: [], // For overall run summaries (passed, failed, skipped, duration over time)
|
|
2080
|
+
testRuns: {}, // For individual test history (key: "test run <run_timestamp_ms>", value: array of test result summaries)
|
|
2081
|
+
};
|
|
2082
|
+
|
|
2083
|
+
if (historicalRuns.length > 0) {
|
|
2084
|
+
historicalRuns.forEach((histRunReport) => {
|
|
2085
|
+
// histRunReport is a full PlaywrightPulseReport object from a past run
|
|
2086
|
+
if (histRunReport.run) {
|
|
2087
|
+
// Ensure timestamp is a Date object for correct sorting/comparison later if needed by charts
|
|
2088
|
+
const runTimestamp = new Date(histRunReport.run.timestamp);
|
|
2089
|
+
trendData.overall.push({
|
|
2090
|
+
runId: runTimestamp.getTime(), // Use timestamp as a unique ID for this context
|
|
2091
|
+
timestamp: runTimestamp,
|
|
2092
|
+
duration: histRunReport.run.duration,
|
|
2093
|
+
totalTests: histRunReport.run.totalTests,
|
|
2094
|
+
passed: histRunReport.run.passed,
|
|
2095
|
+
failed: histRunReport.run.failed,
|
|
2096
|
+
skipped: histRunReport.run.skipped || 0,
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
// For generateTestHistoryContent
|
|
2100
|
+
if (histRunReport.results && Array.isArray(histRunReport.results)) {
|
|
2101
|
+
const runKeyForTestHistory = `test run ${runTimestamp.getTime()}`; // Use timestamp to key test runs
|
|
2102
|
+
trendData.testRuns[runKeyForTestHistory] = histRunReport.results.map(
|
|
2103
|
+
(test) => ({
|
|
2104
|
+
testName: test.name, // Full test name path
|
|
2105
|
+
duration: test.duration,
|
|
2106
|
+
status: test.status,
|
|
2107
|
+
timestamp: new Date(test.startTime), // Assuming test.startTime exists and is what you need
|
|
2108
|
+
})
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
});
|
|
2113
|
+
// Ensure trendData.overall is sorted by timestamp if not already
|
|
2114
|
+
trendData.overall.sort(
|
|
2115
|
+
(a, b) => a.timestamp.getTime() - b.timestamp.getTime()
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// Step 5: Generate and write HTML
|
|
1544
2120
|
try {
|
|
1545
|
-
|
|
2121
|
+
// currentRunReportData is for the main content (test list, summary cards of *this* run)
|
|
2122
|
+
// trendData is for the historical charts and test history section
|
|
2123
|
+
const htmlContent = generateHTML(currentRunReportData, trendData);
|
|
1546
2124
|
await fs.writeFile(reportHtmlPath, htmlContent, "utf-8");
|
|
1547
2125
|
console.log(
|
|
1548
|
-
chalk.green(
|
|
2126
|
+
chalk.green.bold(
|
|
2127
|
+
`🎉 Pulse report generated successfully at: ${reportHtmlPath}`
|
|
2128
|
+
)
|
|
1549
2129
|
);
|
|
1550
|
-
console.log(chalk.
|
|
1551
|
-
console.log(chalk.blue(`open ${reportHtmlPath}`));
|
|
2130
|
+
console.log(chalk.gray(`(You can open this file in your browser)`));
|
|
1552
2131
|
} catch (error) {
|
|
1553
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
2132
|
+
console.error(chalk.red(`Error generating HTML report: ${error.message}`));
|
|
2133
|
+
console.error(chalk.red(error.stack)); // Log full stack for HTML generation errors
|
|
1554
2134
|
process.exit(1);
|
|
1555
2135
|
}
|
|
1556
2136
|
}
|
|
1557
2137
|
|
|
1558
|
-
//
|
|
1559
|
-
main()
|
|
2138
|
+
// Make sure main() is called at the end of your script
|
|
2139
|
+
main().catch((err) => {
|
|
2140
|
+
console.error(
|
|
2141
|
+
chalk.red.bold(`Unhandled error during script execution: ${err.message}`)
|
|
2142
|
+
);
|
|
2143
|
+
console.error(err.stack);
|
|
2144
|
+
process.exit(1);
|
|
2145
|
+
});
|