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