@arghajit/playwright-pulse-report 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/reporter/playwright-pulse-reporter.js +115 -107
- package/package.json +3 -3
- package/scripts/generate-static-report.mjs +945 -1058
- package/scripts/generate-trend.mjs +165 -0
- package/scripts/generate-trend-excel.mjs +0 -273
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import * as d3 from "d3";
|
|
6
|
-
import { JSDOM } from "jsdom";
|
|
7
|
-
import * as XLSX from "xlsx";
|
|
8
5
|
import { fork } from "child_process"; // Add this
|
|
9
6
|
import { fileURLToPath } from "url"; // Add this for resolving path in ESM
|
|
10
7
|
|
|
@@ -31,7 +28,7 @@ const DEFAULT_HTML_FILE = "playwright-pulse-static-report.html";
|
|
|
31
28
|
|
|
32
29
|
// Helper functions
|
|
33
30
|
function sanitizeHTML(str) {
|
|
34
|
-
//
|
|
31
|
+
// User's provided version (note: this doesn't escape HTML special chars correctly)
|
|
35
32
|
if (str === null || str === undefined) return "";
|
|
36
33
|
return String(str)
|
|
37
34
|
.replace(/&/g, "&")
|
|
@@ -45,494 +42,328 @@ function capitalize(str) {
|
|
|
45
42
|
return str[0].toUpperCase() + str.slice(1).toLowerCase();
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
45
|
+
function formatPlaywrightError(error) {
|
|
46
|
+
// Get the error message and clean ANSI codes
|
|
47
|
+
const rawMessage = error.stack || error.message || error.toString();
|
|
48
|
+
const cleanMessage = rawMessage.replace(/\x1B\[[0-9;]*[mGKH]/g, "");
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
// Parse error components
|
|
51
|
+
const timeoutMatch = cleanMessage.match(/Timed out (\d+)ms waiting for/);
|
|
52
|
+
const assertionMatch = cleanMessage.match(/expect\((.*?)\)\.(.*?)\(/);
|
|
53
|
+
const expectedMatch = cleanMessage.match(
|
|
54
|
+
/Expected (?:pattern|string|regexp|value): (.*?)(?:\n|Call log|$)/s
|
|
55
|
+
);
|
|
56
|
+
const actualMatch = cleanMessage.match(
|
|
57
|
+
/Received (?:string|value): (.*?)(?:\n|Call log|$)/s
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// HTML escape function
|
|
61
|
+
const escapeHtml = (str) => {
|
|
62
|
+
if (!str) return "";
|
|
63
|
+
return str.replace(
|
|
64
|
+
/[&<>'"]/g,
|
|
65
|
+
(tag) =>
|
|
66
|
+
({
|
|
67
|
+
"&": "&",
|
|
68
|
+
"<": "<",
|
|
69
|
+
">": ">",
|
|
70
|
+
"'": "'",
|
|
71
|
+
'"': """,
|
|
72
|
+
}[tag])
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Build HTML output
|
|
77
|
+
let html = `<div class="playwright-error">
|
|
78
|
+
<div class="error-header">Test Error</div>`;
|
|
79
|
+
|
|
80
|
+
if (timeoutMatch) {
|
|
81
|
+
html += `<div class="error-timeout">⏱ Timeout: ${escapeHtml(
|
|
82
|
+
timeoutMatch[1]
|
|
83
|
+
)}ms</div>`;
|
|
57
84
|
}
|
|
58
85
|
|
|
59
|
-
|
|
60
|
-
|
|
86
|
+
if (assertionMatch) {
|
|
87
|
+
html += `<div class="error-assertion">🔍 Assertion: expect(${escapeHtml(
|
|
88
|
+
assertionMatch[1]
|
|
89
|
+
)}).${escapeHtml(assertionMatch[2])}()</div>`;
|
|
90
|
+
}
|
|
61
91
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
92
|
+
if (expectedMatch) {
|
|
93
|
+
html += `<div class="error-expected">✅ Expected: ${escapeHtml(
|
|
94
|
+
expectedMatch[1]
|
|
95
|
+
)}</div>`;
|
|
96
|
+
}
|
|
66
97
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
height + margin.top + margin.bottom
|
|
73
|
-
}`
|
|
74
|
-
)
|
|
75
|
-
.attr("preserveAspectRatio", "xMidYMid meet");
|
|
98
|
+
if (actualMatch) {
|
|
99
|
+
html += `<div class="error-actual">❌ Actual: ${escapeHtml(
|
|
100
|
+
actualMatch[1]
|
|
101
|
+
)}</div>`;
|
|
102
|
+
}
|
|
76
103
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
// Add call log if present
|
|
105
|
+
const callLogStart = cleanMessage.indexOf("Call log:");
|
|
106
|
+
if (callLogStart !== -1) {
|
|
107
|
+
const callLogEnd =
|
|
108
|
+
cleanMessage.indexOf("\n\n", callLogStart) || cleanMessage.length;
|
|
109
|
+
const callLogSection = cleanMessage
|
|
110
|
+
.slice(callLogStart + 9, callLogEnd)
|
|
111
|
+
.trim();
|
|
112
|
+
|
|
113
|
+
html += `<div class="error-call-log">
|
|
114
|
+
<div class="call-log-header">📜 Call Log:</div>
|
|
115
|
+
<ul class="call-log-items">${callLogSection
|
|
116
|
+
.split("\n")
|
|
117
|
+
.map((line) => line.trim())
|
|
118
|
+
.filter((line) => line)
|
|
119
|
+
.map((line) => `<li>${escapeHtml(line.replace(/^-\s*/, ""))}</li>`)
|
|
120
|
+
.join("")}</ul>
|
|
121
|
+
</div>`;
|
|
122
|
+
}
|
|
80
123
|
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
124
|
+
// Add stack trace if present
|
|
125
|
+
const stackTraceMatch = cleanMessage.match(/\n\s*at\s.*/gs);
|
|
126
|
+
if (stackTraceMatch) {
|
|
127
|
+
html += `<div class="error-stack-trace">
|
|
128
|
+
<div class="stack-trace-header">🔎 Stack Trace:</div>
|
|
129
|
+
<ul class="stack-trace-items">${stackTraceMatch[0]
|
|
130
|
+
.trim()
|
|
131
|
+
.split("\n")
|
|
132
|
+
.map((line) => line.trim())
|
|
133
|
+
.filter((line) => line)
|
|
134
|
+
.map((line) => `<li>${escapeHtml(line)}</li>`)
|
|
135
|
+
.join("")}</ul>
|
|
136
|
+
</div>`;
|
|
137
|
+
}
|
|
86
138
|
|
|
87
|
-
|
|
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
|
-
];
|
|
139
|
+
html += `</div>`;
|
|
128
140
|
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
});
|
|
141
|
+
return html;
|
|
142
|
+
}
|
|
148
143
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
);
|
|
144
|
+
// User-provided formatDuration function
|
|
145
|
+
function formatDuration(ms) {
|
|
146
|
+
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
147
|
+
return (ms / 1000).toFixed(1) + "s";
|
|
148
|
+
}
|
|
182
149
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
});
|
|
150
|
+
function generateTestTrendsChart(trendData) {
|
|
151
|
+
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
152
|
+
return '<div class="no-data">No overall trend data available for test counts.</div>';
|
|
153
|
+
}
|
|
269
154
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
});
|
|
155
|
+
const chartId = `testTrendsChart-${Date.now()}-${Math.random()
|
|
156
|
+
.toString(36)
|
|
157
|
+
.substring(2, 7)}`;
|
|
158
|
+
const runs = trendData.overall;
|
|
285
159
|
|
|
286
|
-
|
|
287
|
-
const legendData = [
|
|
160
|
+
const series = [
|
|
288
161
|
{
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
162
|
+
name: "Total",
|
|
163
|
+
data: runs.map((r) => r.totalTests),
|
|
164
|
+
color: "var(--primary-color)", // Blue
|
|
165
|
+
marker: { symbol: "circle" },
|
|
292
166
|
},
|
|
293
167
|
{
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
168
|
+
name: "Passed",
|
|
169
|
+
data: runs.map((r) => r.passed),
|
|
170
|
+
color: "var(--success-color)", // Green
|
|
171
|
+
marker: { symbol: "circle" },
|
|
297
172
|
},
|
|
298
173
|
{
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
174
|
+
name: "Failed",
|
|
175
|
+
data: runs.map((r) => r.failed),
|
|
176
|
+
color: "var(--danger-color)", // Red
|
|
177
|
+
marker: { symbol: "circle" },
|
|
302
178
|
},
|
|
303
179
|
{
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
180
|
+
name: "Skipped",
|
|
181
|
+
data: runs.map((r) => r.skipped || 0),
|
|
182
|
+
color: "var(--warning-color)", // Yellow
|
|
183
|
+
marker: { symbol: "circle" },
|
|
307
184
|
},
|
|
308
185
|
];
|
|
309
186
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
.
|
|
313
|
-
.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
187
|
+
// Data needed by the tooltip formatter, stringified to be embedded in the client-side script
|
|
188
|
+
const runsForTooltip = runs.map((r) => ({
|
|
189
|
+
runId: r.runId,
|
|
190
|
+
timestamp: r.timestamp,
|
|
191
|
+
duration: r.duration,
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
const optionsObjectString = `
|
|
195
|
+
{
|
|
196
|
+
chart: { type: "line", height: 350, backgroundColor: "transparent" },
|
|
197
|
+
title: { text: null },
|
|
198
|
+
xAxis: {
|
|
199
|
+
categories: ${JSON.stringify(runs.map((run, i) => `Run ${i + 1}`))},
|
|
200
|
+
crosshair: true,
|
|
201
|
+
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}
|
|
202
|
+
},
|
|
203
|
+
yAxis: {
|
|
204
|
+
title: { text: "Test Count", style: { color: 'var(--text-color)'} },
|
|
205
|
+
min: 0,
|
|
206
|
+
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}
|
|
207
|
+
},
|
|
208
|
+
legend: {
|
|
209
|
+
layout: "horizontal", align: "center", verticalAlign: "bottom",
|
|
210
|
+
itemStyle: { fontSize: "12px", color: 'var(--text-color)' }
|
|
211
|
+
},
|
|
212
|
+
plotOptions: {
|
|
213
|
+
series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}},
|
|
214
|
+
line: { lineWidth: 2.5 } // fillOpacity was 0.1, but for line charts, area fill is usually separate (area chart type)
|
|
215
|
+
},
|
|
216
|
+
tooltip: {
|
|
217
|
+
shared: true, useHTML: true,
|
|
218
|
+
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
219
|
+
borderColor: 'rgba(10,10,10,0.92)',
|
|
220
|
+
style: { color: '#f5f5f5' },
|
|
221
|
+
formatter: function () {
|
|
222
|
+
const runsData = ${JSON.stringify(runsForTooltip)};
|
|
223
|
+
const pointIndex = this.points[0].point.x; // Get index from point
|
|
224
|
+
const run = runsData[pointIndex];
|
|
225
|
+
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' +
|
|
226
|
+
'Date: ' + new Date(run.timestamp).toLocaleString() + '<br><br>';
|
|
227
|
+
this.points.forEach(point => {
|
|
228
|
+
tooltip += '<span style="color:' + point.color + '">●</span> ' + point.series.name + ': <b>' + point.y + '</b><br>';
|
|
229
|
+
});
|
|
230
|
+
tooltip += '<br>Duration: ' + formatDuration(run.duration);
|
|
231
|
+
return tooltip;
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
series: ${JSON.stringify(series)},
|
|
235
|
+
credits: { enabled: false }
|
|
236
|
+
}
|
|
237
|
+
`;
|
|
341
238
|
|
|
342
|
-
return
|
|
239
|
+
return `
|
|
240
|
+
<div id="${chartId}" class="trend-chart-container"></div>
|
|
241
|
+
<script>
|
|
242
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
243
|
+
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
244
|
+
try {
|
|
245
|
+
const chartOptions = ${optionsObjectString};
|
|
246
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
console.error("Error rendering chart ${chartId}:", e);
|
|
249
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data">Error rendering test trends chart.</div>';
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data">Charting library not available.</div>';
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
</script>
|
|
256
|
+
`;
|
|
343
257
|
}
|
|
344
258
|
|
|
345
259
|
function generateDurationTrendChart(trendData) {
|
|
346
260
|
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
347
261
|
return '<div class="no-data">No overall trend data available for durations.</div>';
|
|
348
262
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
263
|
+
const chartId = `durationTrendChart-${Date.now()}-${Math.random()
|
|
264
|
+
.toString(36)
|
|
265
|
+
.substring(2, 7)}`;
|
|
372
266
|
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
267
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
268
|
+
// Assuming var(--accent-color-alt) is Orange #FF9800
|
|
269
|
+
const accentColorAltRGB = "255, 152, 0";
|
|
270
|
+
|
|
271
|
+
const seriesString = `[{
|
|
272
|
+
name: 'Duration',
|
|
273
|
+
data: ${JSON.stringify(runs.map((run) => run.duration))},
|
|
274
|
+
color: 'var(--accent-color-alt)',
|
|
275
|
+
type: 'area',
|
|
276
|
+
marker: {
|
|
277
|
+
symbol: 'circle', enabled: true, radius: 4,
|
|
278
|
+
states: { hover: { radius: 6, lineWidthPlus: 0 } }
|
|
279
|
+
},
|
|
280
|
+
fillColor: {
|
|
281
|
+
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
282
|
+
stops: [
|
|
283
|
+
[0, 'rgba(${accentColorAltRGB}, 0.4)'],
|
|
284
|
+
[1, 'rgba(${accentColorAltRGB}, 0.05)']
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
lineWidth: 2.5
|
|
288
|
+
}]`;
|
|
289
|
+
|
|
290
|
+
const runsForTooltip = runs.map((r) => ({
|
|
291
|
+
runId: r.runId,
|
|
292
|
+
timestamp: r.timestamp,
|
|
293
|
+
duration: r.duration,
|
|
294
|
+
totalTests: r.totalTests,
|
|
295
|
+
}));
|
|
296
|
+
|
|
297
|
+
const optionsObjectString = `
|
|
298
|
+
{
|
|
299
|
+
chart: { type: 'area', height: 350, backgroundColor: 'transparent' },
|
|
300
|
+
title: { text: null },
|
|
301
|
+
xAxis: {
|
|
302
|
+
categories: ${JSON.stringify(runs.map((run, i) => `Run ${i + 1}`))},
|
|
303
|
+
crosshair: true,
|
|
304
|
+
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' } }
|
|
305
|
+
},
|
|
306
|
+
yAxis: {
|
|
307
|
+
title: { text: 'Duration', style: { color: 'var(--text-color)' } },
|
|
308
|
+
labels: {
|
|
309
|
+
formatter: function() { return formatDuration(this.value); },
|
|
310
|
+
style: { color: 'var(--text-color-secondary)', fontSize: '12px' }
|
|
311
|
+
},
|
|
312
|
+
min: 0
|
|
313
|
+
},
|
|
314
|
+
legend: {
|
|
315
|
+
layout: 'horizontal', align: 'center', verticalAlign: 'bottom',
|
|
316
|
+
itemStyle: { fontSize: '12px', color: 'var(--text-color)' }
|
|
317
|
+
},
|
|
318
|
+
plotOptions: {
|
|
319
|
+
area: {
|
|
320
|
+
lineWidth: 2.5,
|
|
321
|
+
states: { hover: { lineWidthPlus: 0 } },
|
|
322
|
+
threshold: null
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
tooltip: {
|
|
326
|
+
shared: true, useHTML: true,
|
|
327
|
+
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
328
|
+
borderColor: 'rgba(10,10,10,0.92)',
|
|
329
|
+
style: { color: '#f5f5f5' },
|
|
330
|
+
formatter: function () {
|
|
331
|
+
const runsData = ${JSON.stringify(runsForTooltip)};
|
|
332
|
+
const pointIndex = this.points[0].point.x;
|
|
333
|
+
const run = runsData[pointIndex];
|
|
334
|
+
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' +
|
|
335
|
+
'Date: ' + new Date(run.timestamp).toLocaleString() + '<br>';
|
|
336
|
+
this.points.forEach(point => {
|
|
337
|
+
tooltip += '<span style="color:' + point.series.color + '">●</span> ' +
|
|
338
|
+
point.series.name + ': <b>' + formatDuration(point.y) + '</b><br>';
|
|
339
|
+
});
|
|
340
|
+
tooltip += '<br>Tests: ' + run.totalTests;
|
|
341
|
+
return tooltip;
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
series: ${seriesString},
|
|
345
|
+
credits: { enabled: false }
|
|
346
|
+
}
|
|
347
|
+
`;
|
|
500
348
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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>`;
|
|
349
|
+
return `
|
|
350
|
+
<div id="${chartId}" class="trend-chart-container"></div>
|
|
351
|
+
<script>
|
|
352
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
353
|
+
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
354
|
+
try {
|
|
355
|
+
const chartOptions = ${optionsObjectString};
|
|
356
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
357
|
+
} catch (e) {
|
|
358
|
+
console.error("Error rendering chart ${chartId}:", e);
|
|
359
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data">Error rendering duration trend chart.</div>';
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data">Charting library not available.</div>';
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
</script>
|
|
366
|
+
`;
|
|
536
367
|
}
|
|
537
368
|
|
|
538
369
|
function formatDate(dateStrOrDate) {
|
|
@@ -540,7 +371,6 @@ function formatDate(dateStrOrDate) {
|
|
|
540
371
|
try {
|
|
541
372
|
const date = new Date(dateStrOrDate);
|
|
542
373
|
if (isNaN(date.getTime())) return "Invalid Date";
|
|
543
|
-
// Using a more common and less verbose format
|
|
544
374
|
return (
|
|
545
375
|
date.toLocaleDateString(undefined, {
|
|
546
376
|
year: "2-digit",
|
|
@@ -559,331 +389,260 @@ function generateTestHistoryChart(history) {
|
|
|
559
389
|
if (!history || history.length === 0)
|
|
560
390
|
return '<div class="no-data-chart">No data for chart</div>';
|
|
561
391
|
|
|
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
392
|
const validHistory = history.filter(
|
|
582
393
|
(h) => h && typeof h.duration === "number" && h.duration >= 0
|
|
583
394
|
);
|
|
584
395
|
if (validHistory.length === 0)
|
|
585
396
|
return '<div class="no-data-chart">No valid data for chart</div>';
|
|
586
397
|
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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)");
|
|
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");
|
|
398
|
+
const chartId = `testHistoryChart-${Date.now()}-${Math.random()
|
|
399
|
+
.toString(36)
|
|
400
|
+
.substring(2, 7)}`;
|
|
401
|
+
|
|
402
|
+
const seriesDataPoints = validHistory.map((run) => {
|
|
403
|
+
let color;
|
|
404
|
+
switch (String(run.status).toLowerCase()) {
|
|
405
|
+
case "passed":
|
|
406
|
+
color = "var(--success-color)";
|
|
407
|
+
break;
|
|
408
|
+
case "failed":
|
|
409
|
+
color = "var(--danger-color)";
|
|
410
|
+
break;
|
|
411
|
+
case "skipped":
|
|
412
|
+
color = "var(--warning-color)";
|
|
413
|
+
break;
|
|
414
|
+
default:
|
|
415
|
+
color = "var(--dark-gray-color)";
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
y: run.duration,
|
|
419
|
+
marker: {
|
|
420
|
+
fillColor: color,
|
|
421
|
+
symbol: "circle",
|
|
422
|
+
radius: 3.5,
|
|
423
|
+
states: { hover: { radius: 5 } },
|
|
424
|
+
},
|
|
425
|
+
status: run.status,
|
|
426
|
+
runId: run.runId,
|
|
427
|
+
};
|
|
730
428
|
});
|
|
731
429
|
|
|
732
|
-
|
|
733
|
-
|
|
430
|
+
// Assuming var(--accent-color) is Deep Purple #673ab7 -> RGB 103, 58, 183
|
|
431
|
+
const accentColorRGB = "103, 58, 183";
|
|
432
|
+
|
|
433
|
+
const optionsObjectString = `
|
|
434
|
+
{
|
|
435
|
+
chart: { type: 'area', height: 100, width: 320, backgroundColor: 'transparent', spacing: [10,10,15,35] },
|
|
436
|
+
title: { text: null },
|
|
437
|
+
xAxis: {
|
|
438
|
+
categories: ${JSON.stringify(
|
|
439
|
+
validHistory.map((_, i) => `R${i + 1}`)
|
|
440
|
+
)},
|
|
441
|
+
labels: { style: { fontSize: '10px', color: 'var(--text-color-secondary)' } }
|
|
442
|
+
},
|
|
443
|
+
yAxis: {
|
|
444
|
+
title: { text: null },
|
|
445
|
+
labels: {
|
|
446
|
+
formatter: function() { return formatDuration(this.value); },
|
|
447
|
+
style: { fontSize: '10px', color: 'var(--text-color-secondary)' },
|
|
448
|
+
align: 'left', x: -35, y: 3
|
|
449
|
+
},
|
|
450
|
+
min: 0,
|
|
451
|
+
gridLineWidth: 0,
|
|
452
|
+
tickAmount: 4
|
|
453
|
+
},
|
|
454
|
+
legend: { enabled: false },
|
|
455
|
+
plotOptions: {
|
|
456
|
+
area: {
|
|
457
|
+
lineWidth: 2,
|
|
458
|
+
lineColor: 'var(--accent-color)',
|
|
459
|
+
fillColor: {
|
|
460
|
+
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
461
|
+
stops: [
|
|
462
|
+
[0, 'rgba(${accentColorRGB}, 0.4)'],
|
|
463
|
+
[1, 'rgba(${accentColorRGB}, 0)']
|
|
464
|
+
]
|
|
465
|
+
},
|
|
466
|
+
marker: { enabled: true },
|
|
467
|
+
threshold: null
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
tooltip: {
|
|
471
|
+
useHTML: true,
|
|
472
|
+
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
473
|
+
borderColor: 'rgba(10,10,10,0.92)',
|
|
474
|
+
style: { color: '#f5f5f5', padding: '8px' },
|
|
475
|
+
formatter: function() {
|
|
476
|
+
const pointData = this.point;
|
|
477
|
+
let statusBadgeHtml = '<span style="padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; background-color: ';
|
|
478
|
+
switch(String(pointData.status).toLowerCase()) {
|
|
479
|
+
case 'passed': statusBadgeHtml += 'var(--success-color)'; break;
|
|
480
|
+
case 'failed': statusBadgeHtml += 'var(--danger-color)'; break;
|
|
481
|
+
case 'skipped': statusBadgeHtml += 'var(--warning-color)'; break;
|
|
482
|
+
default: statusBadgeHtml += 'var(--dark-gray-color)';
|
|
483
|
+
}
|
|
484
|
+
statusBadgeHtml += ';">' + String(pointData.status).toUpperCase() + '</span>';
|
|
734
485
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
486
|
+
return '<strong>Run ' + (pointData.runId || (this.point.index + 1)) + '</strong><br>' +
|
|
487
|
+
'Status: ' + statusBadgeHtml + '<br>' +
|
|
488
|
+
'Duration: ' + formatDuration(pointData.y);
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
series: [{
|
|
492
|
+
data: ${JSON.stringify(seriesDataPoints)},
|
|
493
|
+
showInLegend: false
|
|
494
|
+
}],
|
|
495
|
+
credits: { enabled: false }
|
|
496
|
+
}
|
|
497
|
+
`;
|
|
498
|
+
return `
|
|
499
|
+
<div id="${chartId}" style="width: 320px; height: 100px;"></div>
|
|
500
|
+
<script>
|
|
501
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
502
|
+
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
503
|
+
try {
|
|
504
|
+
const chartOptions = ${optionsObjectString};
|
|
505
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
506
|
+
} catch (e) {
|
|
507
|
+
console.error("Error rendering chart ${chartId}:", e);
|
|
508
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data-chart">Error rendering history chart.</div>';
|
|
509
|
+
}
|
|
510
|
+
} else {
|
|
511
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data-chart">Charting library not available.</div>';
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
</script>
|
|
515
|
+
`;
|
|
516
|
+
}
|
|
738
517
|
|
|
518
|
+
function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
739
519
|
const total = data.reduce((sum, d) => sum + d.value, 0);
|
|
740
520
|
if (total === 0) {
|
|
741
|
-
return '<div class="no-data">No data for Test Distribution chart.</div>';
|
|
521
|
+
return '<div class="pie-chart-wrapper"><h3>Test Distribution</h3><div class="no-data">No data for Test Distribution chart.</div></div>';
|
|
742
522
|
}
|
|
523
|
+
const passedEntry = data.find((d) => d.label === "Passed");
|
|
743
524
|
const passedPercentage = Math.round(
|
|
744
|
-
((
|
|
525
|
+
((passedEntry ? passedEntry.value : 0) / total) * 100
|
|
745
526
|
);
|
|
746
527
|
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const effectiveChartHeight = chartHeight - legendAreaHeight - 10; // Space for legend below
|
|
751
|
-
|
|
752
|
-
const outerRadius = Math.min(chartWidth, effectiveChartHeight) / 2 - 10; // Adjusted radius for legend space
|
|
753
|
-
const innerRadius = outerRadius * 0.55;
|
|
754
|
-
|
|
755
|
-
const pie = d3
|
|
756
|
-
.pie()
|
|
757
|
-
.value((d) => d.value)
|
|
758
|
-
.sort(null);
|
|
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
|
-
};
|
|
769
|
-
const color = d3
|
|
770
|
-
.scaleOrdinal()
|
|
771
|
-
.domain(data.map((d) => d.label))
|
|
772
|
-
.range(data.map((d) => colorMap[d.label] || "#ccc"));
|
|
773
|
-
|
|
774
|
-
const svg = body
|
|
775
|
-
.append("svg")
|
|
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
|
|
782
|
-
.append("g")
|
|
783
|
-
.attr(
|
|
784
|
-
"transform",
|
|
785
|
-
`translate(${chartWidth / 2}, ${effectiveChartHeight / 2 + 5})`
|
|
786
|
-
); // Centered in available chart area
|
|
787
|
-
|
|
788
|
-
const tooltip = body
|
|
789
|
-
.append("div")
|
|
790
|
-
.attr("class", "chart-tooltip")
|
|
791
|
-
.style("opacity", 0);
|
|
792
|
-
|
|
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()
|
|
797
|
-
.append("path")
|
|
798
|
-
.attr("class", "arc-path")
|
|
799
|
-
.attr("d", arcGenerator)
|
|
800
|
-
.attr("fill", (d) => color(d.data.label))
|
|
801
|
-
.style("stroke", "var(--card-background-color)")
|
|
802
|
-
.style("stroke-width", 3)
|
|
803
|
-
.on("mouseover", function (event, d) {
|
|
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);
|
|
815
|
-
tooltip
|
|
816
|
-
.html(
|
|
817
|
-
`${d.data.label}: ${d.data.value} (${Math.round(
|
|
818
|
-
(d.data.value / total) * 100
|
|
819
|
-
)}%)`
|
|
820
|
-
)
|
|
821
|
-
.style("left", event.pageX + 15 + "px")
|
|
822
|
-
.style("top", event.pageY - 28 + "px");
|
|
823
|
-
})
|
|
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
|
-
});
|
|
528
|
+
const chartId = `pieChart-${Date.now()}-${Math.random()
|
|
529
|
+
.toString(36)
|
|
530
|
+
.substring(2, 7)}`;
|
|
828
531
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
-
});
|
|
532
|
+
const seriesData = [
|
|
533
|
+
{
|
|
534
|
+
name: "Tests", // Changed from 'Test Distribution' for tooltip clarity
|
|
535
|
+
data: data
|
|
536
|
+
.filter((d) => d.value > 0)
|
|
537
|
+
.map((d) => {
|
|
538
|
+
let color;
|
|
539
|
+
switch (d.label) {
|
|
540
|
+
case "Passed":
|
|
541
|
+
color = "var(--success-color)";
|
|
542
|
+
break;
|
|
543
|
+
case "Failed":
|
|
544
|
+
color = "var(--danger-color)";
|
|
545
|
+
break;
|
|
546
|
+
case "Skipped":
|
|
547
|
+
color = "var(--warning-color)";
|
|
548
|
+
break;
|
|
549
|
+
default:
|
|
550
|
+
color = "#CCCCCC"; // A neutral default color
|
|
551
|
+
}
|
|
552
|
+
return { name: d.label, y: d.value, color: color };
|
|
553
|
+
}),
|
|
554
|
+
size: "100%",
|
|
555
|
+
innerSize: "55%",
|
|
556
|
+
dataLabels: { enabled: false },
|
|
557
|
+
showInLegend: true,
|
|
558
|
+
},
|
|
559
|
+
];
|
|
864
560
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
.
|
|
868
|
-
|
|
869
|
-
.
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
561
|
+
// Approximate font size for center text, can be adjusted or made dynamic with more client-side JS
|
|
562
|
+
const centerTitleFontSize =
|
|
563
|
+
Math.max(12, Math.min(chartWidth, chartHeight) / 12) + "px";
|
|
564
|
+
const centerSubtitleFontSize =
|
|
565
|
+
Math.max(10, Math.min(chartWidth, chartHeight) / 18) + "px";
|
|
566
|
+
|
|
567
|
+
const optionsObjectString = `
|
|
568
|
+
{
|
|
569
|
+
chart: {
|
|
570
|
+
type: 'pie',
|
|
571
|
+
width: ${chartWidth},
|
|
572
|
+
height: ${
|
|
573
|
+
chartHeight - 40
|
|
574
|
+
}, // Adjusted height to make space for legend if chartHeight is for the whole wrapper
|
|
575
|
+
backgroundColor: 'transparent',
|
|
576
|
+
plotShadow: false,
|
|
577
|
+
spacingBottom: 40 // Ensure space for legend
|
|
578
|
+
},
|
|
579
|
+
title: {
|
|
580
|
+
text: '${passedPercentage}%',
|
|
581
|
+
align: 'center',
|
|
582
|
+
verticalAlign: 'middle',
|
|
583
|
+
y: 5,
|
|
584
|
+
style: { fontSize: '${centerTitleFontSize}', fontWeight: 'bold', color: 'var(--primary-color)' }
|
|
585
|
+
},
|
|
586
|
+
subtitle: {
|
|
587
|
+
text: 'Passed',
|
|
588
|
+
align: 'center',
|
|
589
|
+
verticalAlign: 'middle',
|
|
590
|
+
y: 25,
|
|
591
|
+
style: { fontSize: '${centerSubtitleFontSize}', color: 'var(--text-color-secondary)' }
|
|
592
|
+
},
|
|
593
|
+
tooltip: {
|
|
594
|
+
pointFormat: '{series.name}: <b>{point.percentage:.1f}%</b> ({point.y})',
|
|
595
|
+
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
596
|
+
borderColor: 'rgba(10,10,10,0.92)',
|
|
597
|
+
style: { color: '#f5f5f5' }
|
|
598
|
+
},
|
|
599
|
+
legend: {
|
|
600
|
+
layout: 'horizontal',
|
|
601
|
+
align: 'center',
|
|
602
|
+
verticalAlign: 'bottom',
|
|
603
|
+
itemStyle: { color: 'var(--text-color)', fontWeight: 'normal', fontSize: '12px' }
|
|
604
|
+
},
|
|
605
|
+
plotOptions: {
|
|
606
|
+
pie: {
|
|
607
|
+
allowPointSelect: true,
|
|
608
|
+
cursor: 'pointer',
|
|
609
|
+
borderWidth: 3,
|
|
610
|
+
borderColor: 'var(--card-background-color)', // Match D3 style
|
|
611
|
+
states: {
|
|
612
|
+
hover: {
|
|
613
|
+
// Using default Highcharts halo which is generally good
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
series: ${JSON.stringify(seriesData)},
|
|
619
|
+
credits: { enabled: false }
|
|
620
|
+
}
|
|
621
|
+
`;
|
|
881
622
|
|
|
882
623
|
return `
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
624
|
+
<div class="pie-chart-wrapper" style="align-items: center">
|
|
625
|
+
<div style="display: flex; align-items: start; width: 100%;"><h3>Test Distribution</h3></div>
|
|
626
|
+
<div id="${chartId}" style="width: ${chartWidth}px; height: ${
|
|
627
|
+
chartHeight - 40
|
|
628
|
+
}px;"></div>
|
|
629
|
+
<script>
|
|
630
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
631
|
+
if (typeof Highcharts !== 'undefined') {
|
|
632
|
+
try {
|
|
633
|
+
const chartOptions = ${optionsObjectString};
|
|
634
|
+
Highcharts.chart('${chartId}', chartOptions);
|
|
635
|
+
} catch (e) {
|
|
636
|
+
console.error("Error rendering chart ${chartId}:", e);
|
|
637
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data">Error rendering pie chart.</div>';
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
document.getElementById('${chartId}').innerHTML = '<div class="no-data">Charting library not available.</div>';
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
</script>
|
|
644
|
+
</div>
|
|
645
|
+
`;
|
|
887
646
|
}
|
|
888
647
|
|
|
889
648
|
function generateTestHistoryContent(trendData) {
|
|
@@ -895,7 +654,7 @@ function generateTestHistoryContent(trendData) {
|
|
|
895
654
|
return '<div class="no-data">No historical test data available.</div>';
|
|
896
655
|
}
|
|
897
656
|
|
|
898
|
-
const allTestNamesAndPaths = new Map();
|
|
657
|
+
const allTestNamesAndPaths = new Map();
|
|
899
658
|
Object.values(trendData.testRuns).forEach((run) => {
|
|
900
659
|
if (Array.isArray(run)) {
|
|
901
660
|
run.forEach((test) => {
|
|
@@ -941,14 +700,15 @@ function generateTestHistoryContent(trendData) {
|
|
|
941
700
|
return `
|
|
942
701
|
<div class="test-history-container">
|
|
943
702
|
<div class="filters" style="border-color: black; border-style: groove;">
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
703
|
+
<input type="text" id="history-filter-name" placeholder="Search by test title..." style="border-color: black; border-style: outset;">
|
|
704
|
+
<select id="history-filter-status">
|
|
705
|
+
<option value="">All Statuses</option>
|
|
706
|
+
<option value="passed">Passed</option>
|
|
707
|
+
<option value="failed">Failed</option>
|
|
708
|
+
<option value="skipped">Skipped</option>
|
|
709
|
+
</select>
|
|
710
|
+
<button id="clear-history-filters" class="clear-filters-btn">Clear Filters</button>
|
|
711
|
+
</div>
|
|
952
712
|
|
|
953
713
|
<div class="test-history-grid">
|
|
954
714
|
${testHistory
|
|
@@ -957,7 +717,6 @@ function generateTestHistoryContent(trendData) {
|
|
|
957
717
|
test.history.length > 0
|
|
958
718
|
? test.history[test.history.length - 1]
|
|
959
719
|
: { status: "unknown" };
|
|
960
|
-
// For data-test-name, use the title for filtering as per input placeholder
|
|
961
720
|
return `
|
|
962
721
|
<div class="test-history-card" data-test-name="${sanitizeHTML(
|
|
963
722
|
test.testTitle.toLowerCase()
|
|
@@ -967,7 +726,7 @@ function generateTestHistoryContent(trendData) {
|
|
|
967
726
|
sanitizeHTML(test.testTitle)
|
|
968
727
|
)}</p>
|
|
969
728
|
<span class="status-badge ${getStatusClass(latestRun.status)}">
|
|
970
|
-
${latestRun.status.toUpperCase()}
|
|
729
|
+
${String(latestRun.status).toUpperCase()}
|
|
971
730
|
</span>
|
|
972
731
|
</div>
|
|
973
732
|
<div class="test-history-trend">
|
|
@@ -988,7 +747,7 @@ function generateTestHistoryContent(trendData) {
|
|
|
988
747
|
<td>${run.runId}</td>
|
|
989
748
|
<td><span class="status-badge-small ${getStatusClass(
|
|
990
749
|
run.status
|
|
991
|
-
)}">${run.status.toUpperCase()}</span></td>
|
|
750
|
+
)}">${String(run.status).toUpperCase()}</span></td>
|
|
992
751
|
<td>${formatDuration(run.duration)}</td>
|
|
993
752
|
<td>${formatDate(run.timestamp)}</td>
|
|
994
753
|
</tr>`
|
|
@@ -1039,19 +798,15 @@ function getSuitesData(results) {
|
|
|
1039
798
|
results.forEach((test) => {
|
|
1040
799
|
const browser = test.browser || "unknown";
|
|
1041
800
|
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
801
|
let suiteNameCandidate = "Default Suite";
|
|
1044
802
|
if (suiteParts.length > 2) {
|
|
1045
|
-
// e.g. file > suite > test
|
|
1046
803
|
suiteNameCandidate = suiteParts[1];
|
|
1047
804
|
} else if (suiteParts.length > 1) {
|
|
1048
|
-
// e.g. file > test
|
|
1049
805
|
suiteNameCandidate = suiteParts[0]
|
|
1050
806
|
.split(path.sep)
|
|
1051
807
|
.pop()
|
|
1052
808
|
.replace(/\.(spec|test)\.(ts|js|mjs|cjs)$/, "");
|
|
1053
809
|
} else {
|
|
1054
|
-
// Just file name or malformed
|
|
1055
810
|
suiteNameCandidate = test.name
|
|
1056
811
|
.split(path.sep)
|
|
1057
812
|
.pop()
|
|
@@ -1158,7 +913,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1158
913
|
timestamp: new Date().toISOString(),
|
|
1159
914
|
};
|
|
1160
915
|
|
|
1161
|
-
const totalTestsOr1 = runSummary.totalTests || 1;
|
|
916
|
+
const totalTestsOr1 = runSummary.totalTests || 1; // Avoid division by zero
|
|
1162
917
|
const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
|
|
1163
918
|
const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
|
|
1164
919
|
const skipPercentage = Math.round(
|
|
@@ -1169,12 +924,8 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1169
924
|
? formatDuration(runSummary.duration / runSummary.totalTests)
|
|
1170
925
|
: "0.0s";
|
|
1171
926
|
|
|
1172
|
-
// Inside generate-static-report.mjs
|
|
1173
|
-
|
|
1174
927
|
function generateTestCasesHTML() {
|
|
1175
|
-
// Make sure this is within the scope where 'results' is defined
|
|
1176
928
|
if (!results || results.length === 0) {
|
|
1177
|
-
// Assuming 'results' is accessible here
|
|
1178
929
|
return '<div class="no-tests">No test results found in this run.</div>';
|
|
1179
930
|
}
|
|
1180
931
|
|
|
@@ -1220,7 +971,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1220
971
|
step.errorMessage
|
|
1221
972
|
? `
|
|
1222
973
|
<div class="step-error">
|
|
1223
|
-
<strong>Error:</strong> ${sanitizeHTML(step.errorMessage)}
|
|
1224
974
|
${
|
|
1225
975
|
step.stackTrace
|
|
1226
976
|
? `<pre class="stack-trace">${sanitizeHTML(
|
|
@@ -1275,17 +1025,16 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1275
1025
|
<div class="test-case-content" style="display: none;">
|
|
1276
1026
|
<p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
|
|
1277
1027
|
${
|
|
1278
|
-
test.error
|
|
1279
|
-
? `<div class="test-error-summary"
|
|
1280
|
-
|
|
1281
|
-
|
|
1028
|
+
test.error
|
|
1029
|
+
? `<div class="test-error-summary">
|
|
1030
|
+
${formatPlaywrightError(test.error)}
|
|
1031
|
+
</div>`
|
|
1282
1032
|
: ""
|
|
1283
1033
|
}
|
|
1284
1034
|
|
|
1285
1035
|
<h4>Steps</h4>
|
|
1286
1036
|
<div class="steps-list">${generateStepsHTML(test.steps)}</div>
|
|
1287
1037
|
|
|
1288
|
-
${/* NEW: stdout and stderr sections START */ ""}
|
|
1289
1038
|
${
|
|
1290
1039
|
test.stdout && test.stdout.length > 0
|
|
1291
1040
|
? `
|
|
@@ -1308,77 +1057,132 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1308
1057
|
</div>`
|
|
1309
1058
|
: ""
|
|
1310
1059
|
}
|
|
1311
|
-
${/* NEW: stdout and stderr sections END */ ""}
|
|
1312
1060
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1061
|
+
${
|
|
1062
|
+
test.screenshots && test.screenshots.length > 0
|
|
1063
|
+
? `
|
|
1316
1064
|
<div class="attachments-section">
|
|
1317
1065
|
<h4>Screenshots</h4>
|
|
1318
1066
|
<div class="attachments-grid">
|
|
1319
1067
|
${test.screenshots
|
|
1320
|
-
.map(
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
</a>
|
|
1332
|
-
<div class="attachment-caption">${screenshotName}</div>
|
|
1333
|
-
</div>`
|
|
1334
|
-
: "";
|
|
1335
|
-
})
|
|
1068
|
+
.map(
|
|
1069
|
+
(screenshot) => `
|
|
1070
|
+
<div class="attachment-item">
|
|
1071
|
+
<img src="${screenshot}" alt="Screenshot">
|
|
1072
|
+
<div class="attachment-info">
|
|
1073
|
+
<div class="trace-actions">
|
|
1074
|
+
<a href="${screenshot}" target="_blank">View Full Size</a></div>
|
|
1075
|
+
</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
`
|
|
1078
|
+
)
|
|
1336
1079
|
.join("")}
|
|
1337
1080
|
</div>
|
|
1338
|
-
</div
|
|
1339
|
-
|
|
1340
|
-
|
|
1081
|
+
</div>
|
|
1082
|
+
`
|
|
1083
|
+
: ""
|
|
1084
|
+
}
|
|
1341
1085
|
|
|
1342
1086
|
${
|
|
1343
|
-
test.
|
|
1087
|
+
test.videoPath
|
|
1344
1088
|
? `
|
|
1345
1089
|
<div class="attachments-section">
|
|
1346
1090
|
<h4>Videos</h4>
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1091
|
+
<div class="attachments-grid">
|
|
1092
|
+
${(() => {
|
|
1093
|
+
// Handle both string and array cases
|
|
1094
|
+
const videos = Array.isArray(test.videoPath)
|
|
1095
|
+
? test.videoPath
|
|
1096
|
+
: [test.videoPath];
|
|
1097
|
+
const mimeTypes = {
|
|
1098
|
+
mp4: "video/mp4",
|
|
1099
|
+
webm: "video/webm",
|
|
1100
|
+
ogg: "video/ogg",
|
|
1101
|
+
mov: "video/quicktime",
|
|
1102
|
+
avi: "video/x-msvideo",
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
return videos
|
|
1106
|
+
.map((video, index) => {
|
|
1107
|
+
const videoUrl =
|
|
1108
|
+
typeof video === "object" ? video.url || "" : video;
|
|
1109
|
+
const videoName =
|
|
1110
|
+
typeof video === "object"
|
|
1111
|
+
? video.name || `Video ${index + 1}`
|
|
1112
|
+
: `Video ${index + 1}`;
|
|
1113
|
+
const fileExtension = videoUrl
|
|
1114
|
+
.split(".")
|
|
1115
|
+
.pop()
|
|
1116
|
+
.toLowerCase();
|
|
1117
|
+
const mimeType = mimeTypes[fileExtension] || "video/mp4";
|
|
1118
|
+
|
|
1119
|
+
return `
|
|
1120
|
+
<div class="attachment-item">
|
|
1121
|
+
<video controls width="100%" height="auto" title="${videoName}">
|
|
1122
|
+
<source src="${videoUrl}" type="${mimeType}">
|
|
1123
|
+
Your browser does not support the video tag.
|
|
1124
|
+
</video>
|
|
1125
|
+
<div class="attachment-info">
|
|
1126
|
+
<div class="trace-actions">
|
|
1127
|
+
<a href="${videoUrl}" target="_blank" download="${videoName}.${fileExtension}">
|
|
1128
|
+
Download
|
|
1129
|
+
</a>
|
|
1130
|
+
</div>
|
|
1131
|
+
</div>
|
|
1132
|
+
</div>
|
|
1133
|
+
`;
|
|
1134
|
+
})
|
|
1135
|
+
.join("");
|
|
1136
|
+
})()}
|
|
1137
|
+
</div>
|
|
1138
|
+
</div>
|
|
1139
|
+
`
|
|
1360
1140
|
: ""
|
|
1361
1141
|
}
|
|
1362
|
-
|
|
1142
|
+
|
|
1363
1143
|
${
|
|
1364
|
-
test.
|
|
1144
|
+
test.tracePath
|
|
1365
1145
|
? `
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1146
|
+
<div class="attachments-section">
|
|
1147
|
+
<h4>Trace Files</h4>
|
|
1148
|
+
<div class="attachments-grid">
|
|
1149
|
+
${(() => {
|
|
1150
|
+
// Handle both string and array cases
|
|
1151
|
+
const traces = Array.isArray(test.tracePath)
|
|
1152
|
+
? test.tracePath
|
|
1153
|
+
: [test.tracePath];
|
|
1154
|
+
|
|
1155
|
+
return traces
|
|
1156
|
+
.map((trace, index) => {
|
|
1157
|
+
const traceUrl =
|
|
1158
|
+
typeof trace === "object" ? trace.url || "" : trace;
|
|
1159
|
+
const traceName =
|
|
1160
|
+
typeof trace === "object"
|
|
1161
|
+
? trace.name || `Trace ${index + 1}`
|
|
1162
|
+
: `Trace ${index + 1}`;
|
|
1163
|
+
const traceFileName = traceUrl.split("/").pop();
|
|
1164
|
+
|
|
1165
|
+
return `
|
|
1166
|
+
<div class="attachment-item">
|
|
1167
|
+
<div class="trace-preview">
|
|
1168
|
+
<span class="trace-icon">📄</span>
|
|
1169
|
+
<span class="trace-name">${traceName}</span>
|
|
1170
|
+
</div>
|
|
1171
|
+
<div class="attachment-info">
|
|
1172
|
+
<div class="trace-actions">
|
|
1173
|
+
<a href="${traceUrl}" target="_blank" download="${traceFileName}" class="download-trace">
|
|
1174
|
+
Download
|
|
1175
|
+
</a>
|
|
1176
|
+
</div>
|
|
1177
|
+
</div>
|
|
1178
|
+
</div>
|
|
1179
|
+
`;
|
|
1180
|
+
})
|
|
1181
|
+
.join("");
|
|
1182
|
+
})()}
|
|
1183
|
+
</div>
|
|
1184
|
+
</div>
|
|
1185
|
+
`
|
|
1382
1186
|
: ""
|
|
1383
1187
|
}
|
|
1384
1188
|
|
|
@@ -1403,6 +1207,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1403
1207
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1404
1208
|
<link rel="icon" type="image/png" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
|
|
1405
1209
|
<link rel="apple-touch-icon" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
|
|
1210
|
+
<script src="https://code.highcharts.com/highcharts.js"></script>
|
|
1406
1211
|
<title>Playwright Pulse Report</title>
|
|
1407
1212
|
<style>
|
|
1408
1213
|
:root {
|
|
@@ -1420,27 +1225,34 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1420
1225
|
--text-color: #333;
|
|
1421
1226
|
--text-color-secondary: #555;
|
|
1422
1227
|
--border-color: #ddd;
|
|
1423
|
-
--background-color: #f8f9fa;
|
|
1228
|
+
--background-color: #f8f9fa;
|
|
1424
1229
|
--card-background-color: #fff;
|
|
1425
1230
|
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
1426
1231
|
--border-radius: 8px;
|
|
1427
|
-
--box-shadow: 0 5px 15px rgba(0,0,0,0.08);
|
|
1232
|
+
--box-shadow: 0 5px 15px rgba(0,0,0,0.08);
|
|
1428
1233
|
--box-shadow-light: 0 3px 8px rgba(0,0,0,0.05);
|
|
1429
1234
|
--box-shadow-inset: inset 0 1px 3px rgba(0,0,0,0.07);
|
|
1430
1235
|
}
|
|
1236
|
+
|
|
1237
|
+
/* General Highcharts styling */
|
|
1238
|
+
.highcharts-background { fill: transparent; }
|
|
1239
|
+
.highcharts-title, .highcharts-subtitle { font-family: var(--font-family); }
|
|
1240
|
+
.highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
|
|
1241
|
+
.highcharts-axis-title { fill: var(--text-color) !important; }
|
|
1242
|
+
.highcharts-tooltip > span { background-color: rgba(10,10,10,0.92) !important; border-color: rgba(10,10,10,0.92) !important; color: #f5f5f5 !important; padding: 10px !important; border-radius: 6px !important; }
|
|
1431
1243
|
|
|
1432
1244
|
body {
|
|
1433
1245
|
font-family: var(--font-family);
|
|
1434
1246
|
margin: 0;
|
|
1435
1247
|
background-color: var(--background-color);
|
|
1436
1248
|
color: var(--text-color);
|
|
1437
|
-
line-height: 1.65;
|
|
1249
|
+
line-height: 1.65;
|
|
1438
1250
|
font-size: 16px;
|
|
1439
1251
|
}
|
|
1440
1252
|
|
|
1441
1253
|
.container {
|
|
1442
1254
|
max-width: 1600px;
|
|
1443
|
-
padding: 30px;
|
|
1255
|
+
padding: 30px;
|
|
1444
1256
|
border-radius: var(--border-radius);
|
|
1445
1257
|
box-shadow: var(--box-shadow);
|
|
1446
1258
|
background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec);
|
|
@@ -1492,27 +1304,27 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1492
1304
|
.status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
|
|
1493
1305
|
|
|
1494
1306
|
.dashboard-bottom-row {
|
|
1495
|
-
display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
1496
|
-
gap: 28px; align-items: stretch;
|
|
1307
|
+
display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
1308
|
+
gap: 28px; align-items: stretch;
|
|
1497
1309
|
}
|
|
1498
1310
|
.pie-chart-wrapper, .suites-widget, .trend-chart {
|
|
1499
|
-
background-color: var(--card-background-color); padding: 28px;
|
|
1311
|
+
background-color: var(--card-background-color); padding: 28px;
|
|
1500
1312
|
border-radius: var(--border-radius); box-shadow: var(--box-shadow-light);
|
|
1501
|
-
display: flex; flex-direction: column;
|
|
1313
|
+
display: flex; flex-direction: column;
|
|
1502
1314
|
}
|
|
1503
|
-
|
|
1315
|
+
|
|
1316
|
+
.pie-chart-wrapper h3, .suites-header h2, .trend-chart h3 {
|
|
1504
1317
|
text-align: center; margin-top: 0; margin-bottom: 25px;
|
|
1505
1318
|
font-size: 1.25em; font-weight: 600; color: var(--text-color);
|
|
1506
1319
|
}
|
|
1507
|
-
|
|
1320
|
+
.trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { /* For Highcharts containers */
|
|
1321
|
+
flex-grow: 1;
|
|
1322
|
+
min-height: 250px; /* Ensure charts have some min height */
|
|
1323
|
+
}
|
|
1508
1324
|
|
|
1509
|
-
.chart-tooltip {
|
|
1510
|
-
|
|
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;
|
|
1325
|
+
.chart-tooltip { /* This class was for D3, Highcharts has its own tooltip styling via JS/SVG */
|
|
1326
|
+
/* Basic styling for Highcharts HTML tooltips can be done via .highcharts-tooltip span */
|
|
1514
1327
|
}
|
|
1515
|
-
.chart-tooltip strong { color: #fff; font-weight: 600;}
|
|
1516
1328
|
.status-badge-small-tooltip { padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; }
|
|
1517
1329
|
.status-badge-small-tooltip.status-passed { background-color: var(--success-color); }
|
|
1518
1330
|
.status-badge-small-tooltip.status-failed { background-color: var(--danger-color); }
|
|
@@ -1564,7 +1376,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1564
1376
|
border-bottom: 1px solid transparent;
|
|
1565
1377
|
transition: background-color 0.2s ease;
|
|
1566
1378
|
}
|
|
1567
|
-
.test-case-header:hover { background-color: #f4f6f8; }
|
|
1379
|
+
.test-case-header:hover { background-color: #f4f6f8; }
|
|
1568
1380
|
.test-case-header[aria-expanded="true"] { border-bottom-color: var(--border-color); background-color: #f9fafb; }
|
|
1569
1381
|
|
|
1570
1382
|
.test-case-summary { display: flex; align-items: center; gap: 14px; flex-grow: 1; flex-wrap: wrap;}
|
|
@@ -1634,24 +1446,10 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1634
1446
|
.code-section pre { background-color: #2d2d2d; color: #f0f0f0; padding: 20px; border-radius: 6px; overflow-x: auto; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 0.95em; line-height:1.6;}
|
|
1635
1447
|
|
|
1636
1448
|
.trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
|
|
1637
|
-
|
|
1638
|
-
|
|
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;}
|
|
1449
|
+
/* Removed D3 specific .chart-axis, .main-chart-title, .chart-line.* rules */
|
|
1450
|
+
/* Highcharts styles its elements with classes like .highcharts-axis, .highcharts-title etc. */
|
|
1647
1451
|
|
|
1648
|
-
.
|
|
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;}
|
|
1452
|
+
.test-history-container h2.tab-main-title { font-size: 1.6em; margin-bottom: 18px; color: var(--primary-color); border-bottom: 1px solid var(--border-color); padding-bottom: 12px;}
|
|
1655
1453
|
.test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
|
|
1656
1454
|
.test-history-card {
|
|
1657
1455
|
background: var(--card-background-color); border: 1px solid var(--border-color); border-radius: var(--border-radius);
|
|
@@ -1661,8 +1459,10 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1661
1459
|
.test-history-header h3 { margin: 0; font-size: 1.15em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1662
1460
|
.test-history-header p { font-weight: 500 }
|
|
1663
1461
|
.test-history-trend { margin-bottom: 20px; min-height: 110px; }
|
|
1664
|
-
.test-history-trend
|
|
1665
|
-
|
|
1462
|
+
.test-history-trend div[id^="testHistoryChart-"] { /* Highcharts container for history */
|
|
1463
|
+
display: block; margin: 0 auto; max-width:100%; height: 100px; width: 320px; /* Match JS config */
|
|
1464
|
+
}
|
|
1465
|
+
/* .test-history-trend .small-axis text {font-size: 11px;} Removed D3 specific */
|
|
1666
1466
|
.test-history-details-collapsible summary { cursor: pointer; font-size: 1em; color: var(--primary-color); margin-bottom: 10px; font-weight:500; }
|
|
1667
1467
|
.test-history-details-collapsible summary:hover {text-decoration: underline;}
|
|
1668
1468
|
.test-history-details table { width: 100%; border-collapse: collapse; font-size: 0.95em; }
|
|
@@ -1677,7 +1477,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1677
1477
|
.status-badge-small.status-skipped { background-color: var(--warning-color); }
|
|
1678
1478
|
.status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
|
|
1679
1479
|
|
|
1680
|
-
|
|
1681
1480
|
.no-data, .no-tests, .no-steps, .no-data-chart {
|
|
1682
1481
|
padding: 28px; text-align: center; color: var(--dark-gray-color); font-style: italic; font-size:1.1em;
|
|
1683
1482
|
background-color: var(--light-gray-color); border-radius: var(--border-radius); margin: 18px 0;
|
|
@@ -1689,19 +1488,79 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1689
1488
|
#test-ai p {margin-bottom: 18px; font-size: 1em; color: var(--text-color-secondary);}
|
|
1690
1489
|
pre .stdout-log { background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2; }
|
|
1691
1490
|
pre .stderr-log { background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2; }
|
|
1692
|
-
|
|
1491
|
+
|
|
1492
|
+
.trace-preview {
|
|
1493
|
+
padding: 1rem;
|
|
1494
|
+
text-align: center;
|
|
1495
|
+
background: #f5f5f5;
|
|
1496
|
+
border-bottom: 1px solid #e1e1e1;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
.trace-icon {
|
|
1500
|
+
font-size: 2rem;
|
|
1501
|
+
display: block;
|
|
1502
|
+
margin-bottom: 0.5rem;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
.trace-name {
|
|
1506
|
+
word-break: break-word;
|
|
1507
|
+
font-size: 0.9rem;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
.trace-actions {
|
|
1511
|
+
display: flex;
|
|
1512
|
+
gap: 0.5rem;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
.trace-actions a {
|
|
1516
|
+
flex: 1;
|
|
1517
|
+
text-align: center;
|
|
1518
|
+
padding: 0.25rem 0.5rem;
|
|
1519
|
+
font-size: 0.85rem;
|
|
1520
|
+
border-radius: 4px;
|
|
1521
|
+
text-decoration: none;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
.view-trace {
|
|
1525
|
+
background: #3182ce;
|
|
1526
|
+
color: white;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
.view-trace:hover {
|
|
1530
|
+
background: #2c5282;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
.download-trace {
|
|
1534
|
+
background: #e2e8f0;
|
|
1535
|
+
color: #2d3748;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
.download-trace:hover {
|
|
1539
|
+
background: #cbd5e0;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
.filters button.clear-filters-btn {
|
|
1543
|
+
background-color: var(--medium-gray-color); /* Or any other suitable color */
|
|
1544
|
+
color: var(--text-color);
|
|
1545
|
+
/* Add other styling as per your .filters button style if needed */
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
.filters button.clear-filters-btn:hover {
|
|
1549
|
+
background-color: var(--dark-gray-color); /* Darker on hover */
|
|
1550
|
+
color: #fff;
|
|
1551
|
+
}
|
|
1693
1552
|
@media (max-width: 1200px) {
|
|
1694
|
-
.trend-charts-row { grid-template-columns: 1fr; }
|
|
1553
|
+
.trend-charts-row { grid-template-columns: 1fr; }
|
|
1695
1554
|
}
|
|
1696
1555
|
@media (max-width: 992px) {
|
|
1697
1556
|
.dashboard-bottom-row { grid-template-columns: 1fr; }
|
|
1698
|
-
.pie-chart-wrapper
|
|
1557
|
+
.pie-chart-wrapper div[id^="pieChart-"] { max-width: 350px; margin: 0 auto; }
|
|
1699
1558
|
.filters input { min-width: 180px; }
|
|
1700
1559
|
.filters select { min-width: 150px; }
|
|
1701
1560
|
}
|
|
1702
1561
|
@media (max-width: 768px) {
|
|
1703
1562
|
body { font-size: 15px; }
|
|
1704
|
-
.container { margin: 10px; padding: 20px; }
|
|
1563
|
+
.container { margin: 10px; padding: 20px; }
|
|
1705
1564
|
.header { flex-direction: column; align-items: flex-start; gap: 15px; }
|
|
1706
1565
|
.header h1 { font-size: 1.6em; }
|
|
1707
1566
|
.run-info { text-align: left; font-size:0.9em; }
|
|
@@ -1718,9 +1577,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1718
1577
|
.test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;}
|
|
1719
1578
|
.attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;}
|
|
1720
1579
|
.test-history-grid {grid-template-columns: 1fr;}
|
|
1721
|
-
.pie-chart-wrapper {min-height: auto;}
|
|
1722
|
-
.pie-chart-legend-d3 { transform: translate(calc(50% - 50px), calc(100% - 50px));} /* Adjust legend for mobile for pie */
|
|
1723
|
-
|
|
1580
|
+
.pie-chart-wrapper {min-height: auto;}
|
|
1724
1581
|
}
|
|
1725
1582
|
@media (max-width: 480px) {
|
|
1726
1583
|
body {font-size: 14px;}
|
|
@@ -1730,14 +1587,10 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1730
1587
|
.tab-button {padding: 10px 15px; font-size: 1em;}
|
|
1731
1588
|
.summary-card .value {font-size: 1.8em;}
|
|
1732
1589
|
.attachments-grid {grid-template-columns: 1fr;}
|
|
1733
|
-
.step-item {padding-left: calc(var(--depth, 0) * 18px);}
|
|
1590
|
+
.step-item {padding-left: calc(var(--depth, 0) * 18px);}
|
|
1734
1591
|
.test-case-content, .step-details {padding: 15px;}
|
|
1735
1592
|
.trend-charts-row {gap: 20px;}
|
|
1736
1593
|
.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);}
|
|
1741
1594
|
}
|
|
1742
1595
|
</style>
|
|
1743
1596
|
</head>
|
|
@@ -1796,14 +1649,15 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1796
1649
|
</div>
|
|
1797
1650
|
</div>
|
|
1798
1651
|
<div class="dashboard-bottom-row">
|
|
1799
|
-
${
|
|
1652
|
+
${generatePieChart(
|
|
1653
|
+
// Changed from generatePieChartD3
|
|
1800
1654
|
[
|
|
1801
1655
|
{ label: "Passed", value: runSummary.passed },
|
|
1802
1656
|
{ label: "Failed", value: runSummary.failed },
|
|
1803
1657
|
{ label: "Skipped", value: runSummary.skipped || 0 },
|
|
1804
1658
|
],
|
|
1805
|
-
400,
|
|
1806
|
-
|
|
1659
|
+
400, // Default width
|
|
1660
|
+
390 // Default height (adjusted for legend + title)
|
|
1807
1661
|
)}
|
|
1808
1662
|
${generateSuitesWidget(suitesData)}
|
|
1809
1663
|
</div>
|
|
@@ -1811,31 +1665,31 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1811
1665
|
|
|
1812
1666
|
<div id="test-runs" class="tab-content">
|
|
1813
1667
|
<div class="filters">
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1668
|
+
<input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
|
|
1669
|
+
<select id="filter-status">
|
|
1670
|
+
<option value="">All Statuses</option>
|
|
1671
|
+
<option value="passed">Passed</option>
|
|
1672
|
+
<option value="failed">Failed</option>
|
|
1673
|
+
<option value="skipped">Skipped</option>
|
|
1674
|
+
</select>
|
|
1675
|
+
<select id="filter-browser">
|
|
1676
|
+
<option value="">All Browsers</option>
|
|
1677
|
+
{/* Dynamically generated options will be here */}
|
|
1678
|
+
${Array.from(
|
|
1679
|
+
new Set((results || []).map((test) => test.browser || "unknown"))
|
|
1680
|
+
)
|
|
1681
|
+
.map(
|
|
1682
|
+
(browser) =>
|
|
1683
|
+
`<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
|
|
1684
|
+
browser
|
|
1685
|
+
)}</option>`
|
|
1686
|
+
)
|
|
1687
|
+
.join("")}
|
|
1688
|
+
</select>
|
|
1689
|
+
<button id="expand-all-tests">Expand All</button>
|
|
1690
|
+
<button id="collapse-all-tests">Collapse All</button>
|
|
1691
|
+
<button id="clear-run-summary-filters" class="clear-filters-btn">Clear Filters</button>
|
|
1692
|
+
</div>
|
|
1839
1693
|
<div class="test-cases-list">
|
|
1840
1694
|
${generateTestCasesHTML()}
|
|
1841
1695
|
</div>
|
|
@@ -1925,6 +1779,14 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1925
1779
|
|
|
1926
1780
|
|
|
1927
1781
|
<script>
|
|
1782
|
+
// Ensure formatDuration is globally available
|
|
1783
|
+
if (typeof formatDuration === 'undefined') {
|
|
1784
|
+
function formatDuration(ms) {
|
|
1785
|
+
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
1786
|
+
return (ms / 1000).toFixed(1) + "s";
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1928
1790
|
function initializeReportInteractivity() {
|
|
1929
1791
|
const tabButtons = document.querySelectorAll('.tab-button');
|
|
1930
1792
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
@@ -1939,9 +1801,11 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1939
1801
|
});
|
|
1940
1802
|
});
|
|
1941
1803
|
|
|
1804
|
+
// --- Test Run Summary Filters ---
|
|
1942
1805
|
const nameFilter = document.getElementById('filter-name');
|
|
1943
1806
|
const statusFilter = document.getElementById('filter-status');
|
|
1944
1807
|
const browserFilter = document.getElementById('filter-browser');
|
|
1808
|
+
const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters'); // Get the new button
|
|
1945
1809
|
|
|
1946
1810
|
function filterTestCases() {
|
|
1947
1811
|
const nameValue = nameFilter ? nameFilter.value.toLowerCase() : "";
|
|
@@ -1950,7 +1814,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1950
1814
|
|
|
1951
1815
|
document.querySelectorAll('#test-runs .test-case').forEach(testCaseElement => {
|
|
1952
1816
|
const titleElement = testCaseElement.querySelector('.test-case-title');
|
|
1953
|
-
// Use the 'title' attribute of .test-case-title for full path filtering
|
|
1954
1817
|
const fullTestName = titleElement ? titleElement.getAttribute('title').toLowerCase() : "";
|
|
1955
1818
|
const status = testCaseElement.getAttribute('data-status');
|
|
1956
1819
|
const browser = testCaseElement.getAttribute('data-browser');
|
|
@@ -1966,15 +1829,27 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1966
1829
|
if(statusFilter) statusFilter.addEventListener('change', filterTestCases);
|
|
1967
1830
|
if(browserFilter) browserFilter.addEventListener('change', filterTestCases);
|
|
1968
1831
|
|
|
1832
|
+
// Event listener for clearing Test Run Summary filters
|
|
1833
|
+
if (clearRunSummaryFiltersBtn) {
|
|
1834
|
+
clearRunSummaryFiltersBtn.addEventListener('click', () => {
|
|
1835
|
+
if (nameFilter) nameFilter.value = '';
|
|
1836
|
+
if (statusFilter) statusFilter.value = '';
|
|
1837
|
+
if (browserFilter) browserFilter.value = '';
|
|
1838
|
+
filterTestCases(); // Re-apply filters (which will show all)
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// --- Test History Filters ---
|
|
1969
1843
|
const historyNameFilter = document.getElementById('history-filter-name');
|
|
1970
1844
|
const historyStatusFilter = document.getElementById('history-filter-status');
|
|
1845
|
+
const clearHistoryFiltersBtn = document.getElementById('clear-history-filters'); // Get the new button
|
|
1846
|
+
|
|
1971
1847
|
|
|
1972
1848
|
function filterTestHistoryCards() {
|
|
1973
1849
|
const nameValue = historyNameFilter ? historyNameFilter.value.toLowerCase() : "";
|
|
1974
1850
|
const statusValue = historyStatusFilter ? historyStatusFilter.value : "";
|
|
1975
1851
|
|
|
1976
1852
|
document.querySelectorAll('.test-history-card').forEach(card => {
|
|
1977
|
-
// data-test-name now holds the test title (last part of full name)
|
|
1978
1853
|
const testTitle = card.getAttribute('data-test-name').toLowerCase();
|
|
1979
1854
|
const latestStatus = card.getAttribute('data-latest-status');
|
|
1980
1855
|
|
|
@@ -1987,15 +1862,22 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1987
1862
|
if(historyNameFilter) historyNameFilter.addEventListener('input', filterTestHistoryCards);
|
|
1988
1863
|
if(historyStatusFilter) historyStatusFilter.addEventListener('change', filterTestHistoryCards);
|
|
1989
1864
|
|
|
1865
|
+
// Event listener for clearing Test History filters
|
|
1866
|
+
if (clearHistoryFiltersBtn) {
|
|
1867
|
+
clearHistoryFiltersBtn.addEventListener('click', () => {
|
|
1868
|
+
if (historyNameFilter) historyNameFilter.value = '';
|
|
1869
|
+
if (historyStatusFilter) historyStatusFilter.value = '';
|
|
1870
|
+
filterTestHistoryCards(); // Re-apply filters (which will show all)
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// --- Expand/Collapse and Toggle Details Logic (remains the same) ---
|
|
1990
1875
|
function toggleElementDetails(headerElement, contentSelector) {
|
|
1991
1876
|
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
1877
|
if (headerElement.classList.contains('test-case-header')) {
|
|
1995
1878
|
contentElement = headerElement.parentElement.querySelector('.test-case-content');
|
|
1996
1879
|
} else if (headerElement.classList.contains('step-header')) {
|
|
1997
1880
|
contentElement = headerElement.nextElementSibling;
|
|
1998
|
-
// Verify it's the correct details div
|
|
1999
1881
|
if (!contentElement || !contentElement.matches(contentSelector || '.step-details')) {
|
|
2000
1882
|
contentElement = null;
|
|
2001
1883
|
}
|
|
@@ -2029,20 +1911,17 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2029
1911
|
if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
|
|
2030
1912
|
}
|
|
2031
1913
|
document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
|
|
2032
|
-
|
|
1914
|
+
</script>
|
|
2033
1915
|
</body>
|
|
2034
1916
|
</html>
|
|
2035
1917
|
`;
|
|
2036
1918
|
}
|
|
2037
1919
|
|
|
2038
|
-
// Add this helper function somewhere in generate-static-report.mjs,
|
|
2039
|
-
// possibly before your main() function.
|
|
2040
|
-
|
|
2041
1920
|
async function runScript(scriptPath) {
|
|
2042
1921
|
return new Promise((resolve, reject) => {
|
|
2043
1922
|
console.log(chalk.blue(`Executing script: ${scriptPath}...`));
|
|
2044
1923
|
const process = fork(scriptPath, [], {
|
|
2045
|
-
stdio: "inherit",
|
|
1924
|
+
stdio: "inherit",
|
|
2046
1925
|
});
|
|
2047
1926
|
|
|
2048
1927
|
process.on("error", (err) => {
|
|
@@ -2064,195 +1943,203 @@ async function runScript(scriptPath) {
|
|
|
2064
1943
|
}
|
|
2065
1944
|
|
|
2066
1945
|
async function main() {
|
|
2067
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
2068
|
-
const __dirname = path.dirname(__filename);
|
|
2069
|
-
|
|
1946
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
1947
|
+
const __dirname = path.dirname(__filename);
|
|
1948
|
+
|
|
1949
|
+
// Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
|
|
1950
|
+
const archiveRunScriptPath = path.resolve(
|
|
2070
1951
|
__dirname,
|
|
2071
|
-
"generate-trend
|
|
2072
|
-
);
|
|
1952
|
+
"generate-trend.mjs" // Keeping the filename as per your request
|
|
1953
|
+
);
|
|
1954
|
+
|
|
2073
1955
|
const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2074
|
-
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE);
|
|
1956
|
+
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
|
|
2075
1957
|
const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
|
|
2076
|
-
|
|
1958
|
+
|
|
1959
|
+
const historyDir = path.join(outputDir, "history"); // Directory for historical JSON files
|
|
1960
|
+
const HISTORY_FILE_PREFIX = "trend-"; // Match prefix used in archiving script
|
|
1961
|
+
const MAX_HISTORY_FILES_TO_LOAD_FOR_REPORT = 15; // How many historical runs to show in the report
|
|
2077
1962
|
|
|
2078
1963
|
console.log(chalk.blue(`Starting static HTML report generation...`));
|
|
2079
1964
|
console.log(chalk.blue(`Output directory set to: ${outputDir}`));
|
|
2080
1965
|
|
|
2081
|
-
//
|
|
1966
|
+
// Step 1: Ensure current run data is archived to the history folder
|
|
2082
1967
|
try {
|
|
2083
|
-
await runScript(
|
|
2084
|
-
console.log(
|
|
1968
|
+
await runScript(archiveRunScriptPath); // This script now handles JSON history
|
|
1969
|
+
console.log(
|
|
1970
|
+
chalk.green("Current run data archiving to history completed.")
|
|
1971
|
+
);
|
|
2085
1972
|
} catch (error) {
|
|
2086
1973
|
console.error(
|
|
2087
1974
|
chalk.red(
|
|
2088
|
-
"Failed to
|
|
1975
|
+
"Failed to archive current run data. Report might use stale or incomplete historical trends."
|
|
2089
1976
|
),
|
|
2090
1977
|
error
|
|
2091
1978
|
);
|
|
1979
|
+
// You might decide to proceed or exit depending on the importance of historical data
|
|
2092
1980
|
}
|
|
2093
1981
|
|
|
2094
|
-
|
|
1982
|
+
// Step 2: Load current run's data (for non-trend sections of the report)
|
|
1983
|
+
let currentRunReportData; // Data for the run being reported
|
|
2095
1984
|
try {
|
|
2096
1985
|
const jsonData = await fs.readFile(reportJsonPath, "utf-8");
|
|
2097
|
-
|
|
2098
|
-
if (
|
|
1986
|
+
currentRunReportData = JSON.parse(jsonData);
|
|
1987
|
+
if (
|
|
1988
|
+
!currentRunReportData ||
|
|
1989
|
+
typeof currentRunReportData !== "object" ||
|
|
1990
|
+
!currentRunReportData.results
|
|
1991
|
+
) {
|
|
2099
1992
|
throw new Error(
|
|
2100
1993
|
"Invalid report JSON structure. 'results' field is missing or invalid."
|
|
2101
1994
|
);
|
|
2102
1995
|
}
|
|
2103
|
-
if (!Array.isArray(
|
|
2104
|
-
|
|
1996
|
+
if (!Array.isArray(currentRunReportData.results)) {
|
|
1997
|
+
currentRunReportData.results = [];
|
|
2105
1998
|
console.warn(
|
|
2106
1999
|
chalk.yellow(
|
|
2107
|
-
"Warning: 'results' field in JSON was not an array. Treated as empty."
|
|
2000
|
+
"Warning: 'results' field in current run JSON was not an array. Treated as empty."
|
|
2108
2001
|
)
|
|
2109
2002
|
);
|
|
2110
2003
|
}
|
|
2111
2004
|
} catch (error) {
|
|
2112
2005
|
console.error(
|
|
2113
|
-
chalk.red(
|
|
2006
|
+
chalk.red(
|
|
2007
|
+
`Critical Error: Could not read or parse main report JSON at ${reportJsonPath}: ${error.message}`
|
|
2008
|
+
)
|
|
2114
2009
|
);
|
|
2115
|
-
process.exit(1);
|
|
2010
|
+
process.exit(1); // Exit if the main report for the current run is missing/invalid
|
|
2116
2011
|
}
|
|
2117
2012
|
|
|
2118
|
-
|
|
2013
|
+
// Step 3: Load historical data for trends
|
|
2014
|
+
let historicalRuns = []; // Array of past PlaywrightPulseReport objects
|
|
2119
2015
|
try {
|
|
2120
|
-
await fs.access(
|
|
2121
|
-
const
|
|
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
|
-
}
|
|
2016
|
+
await fs.access(historyDir); // Check if history directory exists
|
|
2017
|
+
const allHistoryFiles = await fs.readdir(historyDir);
|
|
2158
2018
|
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2019
|
+
const jsonHistoryFiles = allHistoryFiles
|
|
2020
|
+
.filter(
|
|
2021
|
+
(file) => file.startsWith(HISTORY_FILE_PREFIX) && file.endsWith(".json")
|
|
2022
|
+
)
|
|
2023
|
+
.map((file) => {
|
|
2024
|
+
const timestampPart = file
|
|
2025
|
+
.replace(HISTORY_FILE_PREFIX, "")
|
|
2026
|
+
.replace(".json", "");
|
|
2027
|
+
return {
|
|
2028
|
+
name: file,
|
|
2029
|
+
path: path.join(historyDir, file),
|
|
2030
|
+
timestamp: parseInt(timestampPart, 10),
|
|
2031
|
+
};
|
|
2032
|
+
})
|
|
2033
|
+
.filter((file) => !isNaN(file.timestamp))
|
|
2034
|
+
.sort((a, b) => b.timestamp - a.timestamp); // Sort newest first to easily pick the latest N
|
|
2171
2035
|
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
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
|
-
});
|
|
2036
|
+
const filesToLoadForTrend = jsonHistoryFiles.slice(
|
|
2037
|
+
0,
|
|
2038
|
+
MAX_HISTORY_FILES_TO_LOAD_FOR_REPORT
|
|
2039
|
+
);
|
|
2040
|
+
|
|
2041
|
+
for (const fileMeta of filesToLoadForTrend) {
|
|
2042
|
+
try {
|
|
2043
|
+
const fileContent = await fs.readFile(fileMeta.path, "utf-8");
|
|
2044
|
+
const runJsonData = JSON.parse(fileContent); // Each file IS a PlaywrightPulseReport
|
|
2045
|
+
historicalRuns.push(runJsonData);
|
|
2046
|
+
} catch (fileReadError) {
|
|
2047
|
+
console.warn(
|
|
2048
|
+
chalk.yellow(
|
|
2049
|
+
`Could not read/parse history file ${fileMeta.name}: ${fileReadError.message}`
|
|
2050
|
+
)
|
|
2051
|
+
);
|
|
2204
2052
|
}
|
|
2205
|
-
});
|
|
2206
|
-
if (
|
|
2207
|
-
trendData.overall.length > 0 ||
|
|
2208
|
-
Object.keys(trendData.testRuns).length > 0
|
|
2209
|
-
) {
|
|
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
|
-
);
|
|
2219
2053
|
}
|
|
2054
|
+
// Reverse to have oldest first for chart data series (if charts expect chronological)
|
|
2055
|
+
historicalRuns.reverse();
|
|
2056
|
+
console.log(
|
|
2057
|
+
chalk.green(
|
|
2058
|
+
`Loaded ${historicalRuns.length} historical run(s) for trend analysis.`
|
|
2059
|
+
)
|
|
2060
|
+
);
|
|
2220
2061
|
} catch (error) {
|
|
2221
2062
|
if (error.code === "ENOENT") {
|
|
2222
2063
|
console.warn(
|
|
2223
2064
|
chalk.yellow(
|
|
2224
|
-
`
|
|
2065
|
+
`History directory '${historyDir}' not found. No historical trends will be displayed.`
|
|
2225
2066
|
)
|
|
2226
2067
|
);
|
|
2227
2068
|
} else {
|
|
2228
2069
|
console.warn(
|
|
2229
2070
|
chalk.yellow(
|
|
2230
|
-
`
|
|
2071
|
+
`Error loading historical data from '${historyDir}': ${error.message}`
|
|
2231
2072
|
)
|
|
2232
2073
|
);
|
|
2233
2074
|
}
|
|
2234
2075
|
}
|
|
2235
2076
|
|
|
2077
|
+
// Step 4: Prepare trendData object in the format expected by chart functions
|
|
2078
|
+
const trendData = {
|
|
2079
|
+
overall: [], // For overall run summaries (passed, failed, skipped, duration over time)
|
|
2080
|
+
testRuns: {}, // For individual test history (key: "test run <run_timestamp_ms>", value: array of test result summaries)
|
|
2081
|
+
};
|
|
2082
|
+
|
|
2083
|
+
if (historicalRuns.length > 0) {
|
|
2084
|
+
historicalRuns.forEach((histRunReport) => {
|
|
2085
|
+
// histRunReport is a full PlaywrightPulseReport object from a past run
|
|
2086
|
+
if (histRunReport.run) {
|
|
2087
|
+
// Ensure timestamp is a Date object for correct sorting/comparison later if needed by charts
|
|
2088
|
+
const runTimestamp = new Date(histRunReport.run.timestamp);
|
|
2089
|
+
trendData.overall.push({
|
|
2090
|
+
runId: runTimestamp.getTime(), // Use timestamp as a unique ID for this context
|
|
2091
|
+
timestamp: runTimestamp,
|
|
2092
|
+
duration: histRunReport.run.duration,
|
|
2093
|
+
totalTests: histRunReport.run.totalTests,
|
|
2094
|
+
passed: histRunReport.run.passed,
|
|
2095
|
+
failed: histRunReport.run.failed,
|
|
2096
|
+
skipped: histRunReport.run.skipped || 0,
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
// For generateTestHistoryContent
|
|
2100
|
+
if (histRunReport.results && Array.isArray(histRunReport.results)) {
|
|
2101
|
+
const runKeyForTestHistory = `test run ${runTimestamp.getTime()}`; // Use timestamp to key test runs
|
|
2102
|
+
trendData.testRuns[runKeyForTestHistory] = histRunReport.results.map(
|
|
2103
|
+
(test) => ({
|
|
2104
|
+
testName: test.name, // Full test name path
|
|
2105
|
+
duration: test.duration,
|
|
2106
|
+
status: test.status,
|
|
2107
|
+
timestamp: new Date(test.startTime), // Assuming test.startTime exists and is what you need
|
|
2108
|
+
})
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
});
|
|
2113
|
+
// Ensure trendData.overall is sorted by timestamp if not already
|
|
2114
|
+
trendData.overall.sort(
|
|
2115
|
+
(a, b) => a.timestamp.getTime() - b.timestamp.getTime()
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// Step 5: Generate and write HTML
|
|
2236
2120
|
try {
|
|
2237
|
-
|
|
2121
|
+
// currentRunReportData is for the main content (test list, summary cards of *this* run)
|
|
2122
|
+
// trendData is for the historical charts and test history section
|
|
2123
|
+
const htmlContent = generateHTML(currentRunReportData, trendData);
|
|
2238
2124
|
await fs.writeFile(reportHtmlPath, htmlContent, "utf-8");
|
|
2239
2125
|
console.log(
|
|
2240
2126
|
chalk.green.bold(
|
|
2241
|
-
`🎉
|
|
2127
|
+
`🎉 Pulse report generated successfully at: ${reportHtmlPath}`
|
|
2242
2128
|
)
|
|
2243
2129
|
);
|
|
2244
|
-
console.log(chalk.gray(`
|
|
2130
|
+
console.log(chalk.gray(`(You can open this file in your browser)`));
|
|
2245
2131
|
} catch (error) {
|
|
2246
2132
|
console.error(chalk.red(`Error generating HTML report: ${error.message}`));
|
|
2247
|
-
console.error(chalk.red(error.stack));
|
|
2133
|
+
console.error(chalk.red(error.stack)); // Log full stack for HTML generation errors
|
|
2248
2134
|
process.exit(1);
|
|
2249
2135
|
}
|
|
2250
2136
|
}
|
|
2251
2137
|
|
|
2138
|
+
// Make sure main() is called at the end of your script
|
|
2252
2139
|
main().catch((err) => {
|
|
2253
2140
|
console.error(
|
|
2254
2141
|
chalk.red.bold(`Unhandled error during script execution: ${err.message}`)
|
|
2255
2142
|
);
|
|
2256
2143
|
console.error(err.stack);
|
|
2257
2144
|
process.exit(1);
|
|
2258
|
-
});
|
|
2145
|
+
});
|