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