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