@arghajit/dummy 0.1.0-beta-12 → 0.1.0-beta-14
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 +32 -18
- package/dist/reporter/playwright-pulse-reporter.d.ts +1 -0
- package/dist/reporter/playwright-pulse-reporter.js +35 -0
- package/dist/types/index.d.ts +17 -0
- package/package.json +10 -7
- package/scripts/generate-email-report.mjs +576 -0
- package/scripts/generate-static-report.mjs +603 -991
- package/scripts/{sendReport.js → sendReport.mjs} +138 -71
|
@@ -5,6 +5,7 @@ import { readFileSync, existsSync as fsExistsSync } from "fs"; // ADD THIS LINE
|
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { fork } from "child_process"; // Add this
|
|
7
7
|
import { fileURLToPath } from "url"; // Add this for resolving path in ESM
|
|
8
|
+
import prettyAnsi from "pretty-ansi";
|
|
8
9
|
|
|
9
10
|
// Use dynamic import for chalk as it's ESM only
|
|
10
11
|
let chalk;
|
|
@@ -21,12 +22,10 @@ try {
|
|
|
21
22
|
gray: (text) => text,
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
|
-
|
|
25
25
|
// Default configuration
|
|
26
26
|
const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
27
27
|
const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
|
|
28
28
|
const DEFAULT_HTML_FILE = "playwright-pulse-static-report.html";
|
|
29
|
-
|
|
30
29
|
// Helper functions
|
|
31
30
|
function sanitizeHTML(str) {
|
|
32
31
|
if (str === null || str === undefined) return "";
|
|
@@ -45,112 +44,111 @@ function capitalize(str) {
|
|
|
45
44
|
if (!str) return ""; // Handle empty string
|
|
46
45
|
return str[0].toUpperCase() + str.slice(1).toLowerCase();
|
|
47
46
|
}
|
|
48
|
-
|
|
49
47
|
function formatPlaywrightError(error) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
48
|
+
const commandOutput = prettyAnsi(error || error.message);
|
|
49
|
+
return convertPlaywrightErrorToHTML(commandOutput);
|
|
50
|
+
}
|
|
51
|
+
function convertPlaywrightErrorToHTML(str) {
|
|
52
|
+
return (
|
|
53
|
+
str
|
|
54
|
+
// Convert leading spaces to and tabs to
|
|
55
|
+
.replace(/^(\s+)/gm, (match) =>
|
|
56
|
+
match.replace(/ /g, " ").replace(/\t/g, " ")
|
|
57
|
+
)
|
|
58
|
+
// Color and style replacements
|
|
59
|
+
.replace(/<red>/g, '<span style="color: red;">')
|
|
60
|
+
.replace(/<green>/g, '<span style="color: green;">')
|
|
61
|
+
.replace(/<dim>/g, '<span style="opacity: 0.6;">')
|
|
62
|
+
.replace(/<intensity>/g, '<span style="font-weight: bold;">') // Changed to apply bold
|
|
63
|
+
.replace(/<\/color>/g, "</span>")
|
|
64
|
+
.replace(/<\/intensity>/g, "</span>")
|
|
65
|
+
// Convert newlines to <br> after processing other replacements
|
|
66
|
+
.replace(/\n/g, "<br>")
|
|
62
67
|
);
|
|
68
|
+
}
|
|
69
|
+
function formatDuration(ms, options = {}) {
|
|
70
|
+
const {
|
|
71
|
+
precision = 1,
|
|
72
|
+
invalidInputReturn = "N/A",
|
|
73
|
+
defaultForNullUndefinedNegative = null,
|
|
74
|
+
} = options;
|
|
75
|
+
|
|
76
|
+
const validPrecision = Math.max(0, Math.floor(precision));
|
|
77
|
+
const zeroWithPrecision = (0).toFixed(validPrecision) + "s";
|
|
78
|
+
const resolvedNullUndefNegReturn =
|
|
79
|
+
defaultForNullUndefinedNegative === null
|
|
80
|
+
? zeroWithPrecision
|
|
81
|
+
: defaultForNullUndefinedNegative;
|
|
82
|
+
|
|
83
|
+
if (ms === undefined || ms === null) {
|
|
84
|
+
return resolvedNullUndefNegReturn;
|
|
85
|
+
}
|
|
63
86
|
|
|
64
|
-
|
|
65
|
-
const escapeHtml = (str) => {
|
|
66
|
-
if (!str) return "";
|
|
67
|
-
return str.replace(
|
|
68
|
-
/[&<>'"]/g,
|
|
69
|
-
(tag) =>
|
|
70
|
-
({
|
|
71
|
-
"&": "&",
|
|
72
|
-
"<": "<",
|
|
73
|
-
">": ">",
|
|
74
|
-
"'": "'",
|
|
75
|
-
'"': """,
|
|
76
|
-
}[tag])
|
|
77
|
-
);
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// Build HTML output
|
|
81
|
-
let html = `<div class="playwright-error">
|
|
82
|
-
<div class="error-header">Test Error</div>`;
|
|
87
|
+
const numMs = Number(ms);
|
|
83
88
|
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
timeoutMatch[1]
|
|
87
|
-
)}ms</div>`;
|
|
89
|
+
if (Number.isNaN(numMs) || !Number.isFinite(numMs)) {
|
|
90
|
+
return invalidInputReturn;
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
assertionMatch[1]
|
|
93
|
-
)}).${escapeHtml(assertionMatch[2])}()</div>`;
|
|
93
|
+
if (numMs < 0) {
|
|
94
|
+
return resolvedNullUndefNegReturn;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
expectedMatch[1]
|
|
99
|
-
)}</div>`;
|
|
97
|
+
if (numMs === 0) {
|
|
98
|
+
return zeroWithPrecision;
|
|
100
99
|
}
|
|
101
100
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
101
|
+
const MS_PER_SECOND = 1000;
|
|
102
|
+
const SECONDS_PER_MINUTE = 60;
|
|
103
|
+
const MINUTES_PER_HOUR = 60;
|
|
104
|
+
const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
|
|
107
105
|
|
|
108
|
-
|
|
109
|
-
const callLogStart = cleanMessage.indexOf("Call log:");
|
|
110
|
-
if (callLogStart !== -1) {
|
|
111
|
-
const callLogEnd =
|
|
112
|
-
cleanMessage.indexOf("\n\n", callLogStart) || cleanMessage.length;
|
|
113
|
-
const callLogSection = cleanMessage
|
|
114
|
-
.slice(callLogStart + 9, callLogEnd)
|
|
115
|
-
.trim();
|
|
106
|
+
const totalRawSeconds = numMs / MS_PER_SECOND;
|
|
116
107
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
108
|
+
// Decision: Are we going to display hours or minutes?
|
|
109
|
+
// This happens if the duration is inherently >= 1 minute OR
|
|
110
|
+
// if it's < 1 minute but ceiling the seconds makes it >= 1 minute.
|
|
111
|
+
if (
|
|
112
|
+
totalRawSeconds < SECONDS_PER_MINUTE &&
|
|
113
|
+
Math.ceil(totalRawSeconds) < SECONDS_PER_MINUTE
|
|
114
|
+
) {
|
|
115
|
+
// Strictly seconds-only display, use precision.
|
|
116
|
+
return `${totalRawSeconds.toFixed(validPrecision)}s`;
|
|
117
|
+
} else {
|
|
118
|
+
// Display will include minutes and/or hours, or seconds round up to a minute.
|
|
119
|
+
// Seconds part should be an integer (ceiling).
|
|
120
|
+
// Round the total milliseconds UP to the nearest full second.
|
|
121
|
+
const totalMsRoundedUpToSecond =
|
|
122
|
+
Math.ceil(numMs / MS_PER_SECOND) * MS_PER_SECOND;
|
|
127
123
|
|
|
128
|
-
|
|
129
|
-
const stackTraceMatch = cleanMessage.match(/\n\s*at\s.*/gs);
|
|
130
|
-
if (stackTraceMatch) {
|
|
131
|
-
html += `<div class="error-stack-trace">
|
|
132
|
-
<div class="stack-trace-header">🔎 Stack Trace:</div>
|
|
133
|
-
<ul class="stack-trace-items">${stackTraceMatch[0]
|
|
134
|
-
.trim()
|
|
135
|
-
.split("\n")
|
|
136
|
-
.map((line) => line.trim())
|
|
137
|
-
.filter((line) => line)
|
|
138
|
-
.map((line) => `<li>${escapeHtml(line)}</li>`)
|
|
139
|
-
.join("")}</ul>
|
|
140
|
-
</div>`;
|
|
141
|
-
}
|
|
124
|
+
let remainingMs = totalMsRoundedUpToSecond;
|
|
142
125
|
|
|
143
|
-
|
|
126
|
+
const h = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_HOUR));
|
|
127
|
+
remainingMs %= MS_PER_SECOND * SECONDS_PER_HOUR;
|
|
144
128
|
|
|
145
|
-
|
|
146
|
-
|
|
129
|
+
const m = Math.floor(remainingMs / (MS_PER_SECOND * SECONDS_PER_MINUTE));
|
|
130
|
+
remainingMs %= MS_PER_SECOND * SECONDS_PER_MINUTE;
|
|
147
131
|
|
|
148
|
-
//
|
|
149
|
-
function formatDuration(ms) {
|
|
150
|
-
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
151
|
-
return (ms / 1000).toFixed(1) + "s";
|
|
152
|
-
}
|
|
132
|
+
const s = Math.floor(remainingMs / MS_PER_SECOND); // This will be an integer
|
|
153
133
|
|
|
134
|
+
const parts = [];
|
|
135
|
+
if (h > 0) {
|
|
136
|
+
parts.push(`${h}h`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Show minutes if:
|
|
140
|
+
// - hours are present (e.g., "1h 0m 5s")
|
|
141
|
+
// - OR minutes themselves are > 0 (e.g., "5m 10s")
|
|
142
|
+
// - OR the original duration was >= 1 minute (ensures "1m 0s" for 60000ms)
|
|
143
|
+
if (h > 0 || m > 0 || numMs >= MS_PER_SECOND * SECONDS_PER_MINUTE) {
|
|
144
|
+
parts.push(`${m}m`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
parts.push(`${s}s`);
|
|
148
|
+
|
|
149
|
+
return parts.join(" ");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
154
152
|
function generateTestTrendsChart(trendData) {
|
|
155
153
|
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
156
154
|
return '<div class="no-data">No overall trend data available for test counts.</div>';
|
|
@@ -159,107 +157,93 @@ function generateTestTrendsChart(trendData) {
|
|
|
159
157
|
const chartId = `testTrendsChart-${Date.now()}-${Math.random()
|
|
160
158
|
.toString(36)
|
|
161
159
|
.substring(2, 7)}`;
|
|
160
|
+
const renderFunctionName = `renderTestTrendsChart_${chartId.replace(
|
|
161
|
+
/-/g,
|
|
162
|
+
"_"
|
|
163
|
+
)}`;
|
|
162
164
|
const runs = trendData.overall;
|
|
163
165
|
|
|
164
166
|
const series = [
|
|
165
167
|
{
|
|
166
168
|
name: "Total",
|
|
167
169
|
data: runs.map((r) => r.totalTests),
|
|
168
|
-
color: "var(--primary-color)",
|
|
170
|
+
color: "var(--primary-color)",
|
|
169
171
|
marker: { symbol: "circle" },
|
|
170
172
|
},
|
|
171
173
|
{
|
|
172
174
|
name: "Passed",
|
|
173
175
|
data: runs.map((r) => r.passed),
|
|
174
|
-
color: "var(--success-color)",
|
|
176
|
+
color: "var(--success-color)",
|
|
175
177
|
marker: { symbol: "circle" },
|
|
176
178
|
},
|
|
177
179
|
{
|
|
178
180
|
name: "Failed",
|
|
179
181
|
data: runs.map((r) => r.failed),
|
|
180
|
-
color: "var(--danger-color)",
|
|
182
|
+
color: "var(--danger-color)",
|
|
181
183
|
marker: { symbol: "circle" },
|
|
182
184
|
},
|
|
183
185
|
{
|
|
184
186
|
name: "Skipped",
|
|
185
187
|
data: runs.map((r) => r.skipped || 0),
|
|
186
|
-
color: "var(--warning-color)",
|
|
188
|
+
color: "var(--warning-color)",
|
|
187
189
|
marker: { symbol: "circle" },
|
|
188
190
|
},
|
|
189
191
|
];
|
|
190
|
-
|
|
191
|
-
// Data needed by the tooltip formatter, stringified to be embedded in the client-side script
|
|
192
192
|
const runsForTooltip = runs.map((r) => ({
|
|
193
193
|
runId: r.runId,
|
|
194
194
|
timestamp: r.timestamp,
|
|
195
195
|
duration: r.duration,
|
|
196
196
|
}));
|
|
197
197
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
title: { text: null },
|
|
202
|
-
xAxis: {
|
|
203
|
-
categories: ${JSON.stringify(runs.map((run, i) => `Run ${i + 1}`))},
|
|
204
|
-
crosshair: true,
|
|
205
|
-
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}
|
|
206
|
-
},
|
|
207
|
-
yAxis: {
|
|
208
|
-
title: { text: "Test Count", style: { color: 'var(--text-color)'} },
|
|
209
|
-
min: 0,
|
|
210
|
-
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}
|
|
211
|
-
},
|
|
212
|
-
legend: {
|
|
213
|
-
layout: "horizontal", align: "center", verticalAlign: "bottom",
|
|
214
|
-
itemStyle: { fontSize: "12px", color: 'var(--text-color)' }
|
|
215
|
-
},
|
|
216
|
-
plotOptions: {
|
|
217
|
-
series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}},
|
|
218
|
-
line: { lineWidth: 2.5 } // fillOpacity was 0.1, but for line charts, area fill is usually separate (area chart type)
|
|
219
|
-
},
|
|
220
|
-
tooltip: {
|
|
221
|
-
shared: true, useHTML: true,
|
|
222
|
-
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
223
|
-
borderColor: 'rgba(10,10,10,0.92)',
|
|
224
|
-
style: { color: '#f5f5f5' },
|
|
225
|
-
formatter: function () {
|
|
226
|
-
const runsData = ${JSON.stringify(runsForTooltip)};
|
|
227
|
-
const pointIndex = this.points[0].point.x; // Get index from point
|
|
228
|
-
const run = runsData[pointIndex];
|
|
229
|
-
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' +
|
|
230
|
-
'Date: ' + new Date(run.timestamp).toLocaleString() + '<br><br>';
|
|
231
|
-
this.points.forEach(point => {
|
|
232
|
-
tooltip += '<span style="color:' + point.color + '">●</span> ' + point.series.name + ': <b>' + point.y + '</b><br>';
|
|
233
|
-
});
|
|
234
|
-
tooltip += '<br>Duration: ' + formatDuration(run.duration);
|
|
235
|
-
return tooltip;
|
|
236
|
-
}
|
|
237
|
-
},
|
|
238
|
-
series: ${JSON.stringify(series)},
|
|
239
|
-
credits: { enabled: false }
|
|
240
|
-
}
|
|
241
|
-
`;
|
|
198
|
+
const categoriesString = JSON.stringify(runs.map((run, i) => `Run ${i + 1}`));
|
|
199
|
+
const seriesString = JSON.stringify(series);
|
|
200
|
+
const runsForTooltipString = JSON.stringify(runsForTooltip);
|
|
242
201
|
|
|
243
202
|
return `
|
|
244
|
-
<div id="${chartId}" class="trend-chart-container"
|
|
203
|
+
<div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
|
|
204
|
+
<div class="no-data">Loading Test Volume Trends...</div>
|
|
205
|
+
</div>
|
|
245
206
|
<script>
|
|
246
|
-
|
|
207
|
+
window.${renderFunctionName} = function() {
|
|
208
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
209
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found for lazy loading."); return; }
|
|
247
210
|
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
248
211
|
try {
|
|
249
|
-
|
|
212
|
+
chartContainer.innerHTML = ''; // Clear placeholder
|
|
213
|
+
const chartOptions = {
|
|
214
|
+
chart: { type: "line", height: 350, backgroundColor: "transparent" },
|
|
215
|
+
title: { text: null },
|
|
216
|
+
xAxis: { categories: ${categoriesString}, crosshair: true, labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}},
|
|
217
|
+
yAxis: { title: { text: "Test Count", style: { color: 'var(--text-color)'} }, min: 0, labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}},
|
|
218
|
+
legend: { layout: "horizontal", align: "center", verticalAlign: "bottom", itemStyle: { fontSize: "12px", color: 'var(--text-color)' }},
|
|
219
|
+
plotOptions: { series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}}, line: { lineWidth: 2.5 }},
|
|
220
|
+
tooltip: {
|
|
221
|
+
shared: true, useHTML: true, backgroundColor: 'rgba(10,10,10,0.92)', borderColor: 'rgba(10,10,10,0.92)', style: { color: '#f5f5f5' },
|
|
222
|
+
formatter: function () {
|
|
223
|
+
const runsData = ${runsForTooltipString};
|
|
224
|
+
const pointIndex = this.points[0].point.x;
|
|
225
|
+
const run = runsData[pointIndex];
|
|
226
|
+
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' + 'Date: ' + new Date(run.timestamp).toLocaleString() + '<br><br>';
|
|
227
|
+
this.points.forEach(point => { tooltip += '<span style="color:' + point.color + '">●</span> ' + point.series.name + ': <b>' + point.y + '</b><br>'; });
|
|
228
|
+
tooltip += '<br>Duration: ' + formatDuration(run.duration);
|
|
229
|
+
return tooltip;
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
series: ${seriesString},
|
|
233
|
+
credits: { enabled: false }
|
|
234
|
+
};
|
|
250
235
|
Highcharts.chart('${chartId}', chartOptions);
|
|
251
236
|
} catch (e) {
|
|
252
|
-
console.error("Error rendering chart ${chartId}:", e);
|
|
253
|
-
|
|
237
|
+
console.error("Error rendering chart ${chartId} (lazy):", e);
|
|
238
|
+
chartContainer.innerHTML = '<div class="no-data">Error rendering test trends chart.</div>';
|
|
254
239
|
}
|
|
255
240
|
} else {
|
|
256
|
-
|
|
241
|
+
chartContainer.innerHTML = '<div class="no-data">Charting library not available for test trends.</div>';
|
|
257
242
|
}
|
|
258
|
-
}
|
|
243
|
+
};
|
|
259
244
|
</script>
|
|
260
245
|
`;
|
|
261
246
|
}
|
|
262
|
-
|
|
263
247
|
function generateDurationTrendChart(trendData) {
|
|
264
248
|
if (!trendData || !trendData.overall || trendData.overall.length === 0) {
|
|
265
249
|
return '<div class="no-data">No overall trend data available for durations.</div>';
|
|
@@ -267,109 +251,83 @@ function generateDurationTrendChart(trendData) {
|
|
|
267
251
|
const chartId = `durationTrendChart-${Date.now()}-${Math.random()
|
|
268
252
|
.toString(36)
|
|
269
253
|
.substring(2, 7)}`;
|
|
254
|
+
const renderFunctionName = `renderDurationTrendChart_${chartId.replace(
|
|
255
|
+
/-/g,
|
|
256
|
+
"_"
|
|
257
|
+
)}`;
|
|
270
258
|
const runs = trendData.overall;
|
|
271
259
|
|
|
272
|
-
// Assuming var(--accent-color-alt) is Orange #FF9800
|
|
273
|
-
const accentColorAltRGB = "255, 152, 0";
|
|
274
|
-
|
|
275
|
-
const seriesString = `[{
|
|
276
|
-
name: 'Duration',
|
|
277
|
-
data: ${JSON.stringify(runs.map((run) => run.duration))},
|
|
278
|
-
color: 'var(--accent-color-alt)',
|
|
279
|
-
type: 'area',
|
|
280
|
-
marker: {
|
|
281
|
-
symbol: 'circle', enabled: true, radius: 4,
|
|
282
|
-
states: { hover: { radius: 6, lineWidthPlus: 0 } }
|
|
283
|
-
},
|
|
284
|
-
fillColor: {
|
|
285
|
-
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
286
|
-
stops: [
|
|
287
|
-
[0, 'rgba(${accentColorAltRGB}, 0.4)'],
|
|
288
|
-
[1, 'rgba(${accentColorAltRGB}, 0.05)']
|
|
289
|
-
]
|
|
290
|
-
},
|
|
291
|
-
lineWidth: 2.5
|
|
292
|
-
}]`;
|
|
260
|
+
const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
|
|
293
261
|
|
|
262
|
+
const chartDataString = JSON.stringify(runs.map((run) => run.duration));
|
|
263
|
+
const categoriesString = JSON.stringify(runs.map((run, i) => `Run ${i + 1}`));
|
|
294
264
|
const runsForTooltip = runs.map((r) => ({
|
|
295
265
|
runId: r.runId,
|
|
296
266
|
timestamp: r.timestamp,
|
|
297
267
|
duration: r.duration,
|
|
298
268
|
totalTests: r.totalTests,
|
|
299
269
|
}));
|
|
270
|
+
const runsForTooltipString = JSON.stringify(runsForTooltip);
|
|
300
271
|
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
yAxis: {
|
|
311
|
-
title: { text: 'Duration', style: { color: 'var(--text-color)' } },
|
|
312
|
-
labels: {
|
|
313
|
-
formatter: function() { return formatDuration(this.value); },
|
|
314
|
-
style: { color: 'var(--text-color-secondary)', fontSize: '12px' }
|
|
315
|
-
},
|
|
316
|
-
min: 0
|
|
317
|
-
},
|
|
318
|
-
legend: {
|
|
319
|
-
layout: 'horizontal', align: 'center', verticalAlign: 'bottom',
|
|
320
|
-
itemStyle: { fontSize: '12px', color: 'var(--text-color)' }
|
|
321
|
-
},
|
|
322
|
-
plotOptions: {
|
|
323
|
-
area: {
|
|
324
|
-
lineWidth: 2.5,
|
|
325
|
-
states: { hover: { lineWidthPlus: 0 } },
|
|
326
|
-
threshold: null
|
|
327
|
-
}
|
|
328
|
-
},
|
|
329
|
-
tooltip: {
|
|
330
|
-
shared: true, useHTML: true,
|
|
331
|
-
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
332
|
-
borderColor: 'rgba(10,10,10,0.92)',
|
|
333
|
-
style: { color: '#f5f5f5' },
|
|
334
|
-
formatter: function () {
|
|
335
|
-
const runsData = ${JSON.stringify(runsForTooltip)};
|
|
336
|
-
const pointIndex = this.points[0].point.x;
|
|
337
|
-
const run = runsData[pointIndex];
|
|
338
|
-
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' +
|
|
339
|
-
'Date: ' + new Date(run.timestamp).toLocaleString() + '<br>';
|
|
340
|
-
this.points.forEach(point => {
|
|
341
|
-
tooltip += '<span style="color:' + point.series.color + '">●</span> ' +
|
|
342
|
-
point.series.name + ': <b>' + formatDuration(point.y) + '</b><br>';
|
|
343
|
-
});
|
|
344
|
-
tooltip += '<br>Tests: ' + run.totalTests;
|
|
345
|
-
return tooltip;
|
|
346
|
-
}
|
|
347
|
-
},
|
|
348
|
-
series: ${seriesString},
|
|
349
|
-
credits: { enabled: false }
|
|
350
|
-
}
|
|
351
|
-
`;
|
|
272
|
+
const seriesStringForRender = `[{
|
|
273
|
+
name: 'Duration',
|
|
274
|
+
data: ${chartDataString},
|
|
275
|
+
color: 'var(--accent-color-alt)',
|
|
276
|
+
type: 'area',
|
|
277
|
+
marker: { symbol: 'circle', enabled: true, radius: 4, states: { hover: { radius: 6, lineWidthPlus: 0 } } },
|
|
278
|
+
fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(${accentColorAltRGB}, 0.4)'], [1, 'rgba(${accentColorAltRGB}, 0.05)']] },
|
|
279
|
+
lineWidth: 2.5
|
|
280
|
+
}]`;
|
|
352
281
|
|
|
353
282
|
return `
|
|
354
|
-
<div id="${chartId}" class="trend-chart-container"
|
|
283
|
+
<div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
|
|
284
|
+
<div class="no-data">Loading Duration Trends...</div>
|
|
285
|
+
</div>
|
|
355
286
|
<script>
|
|
356
|
-
|
|
287
|
+
window.${renderFunctionName} = function() {
|
|
288
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
289
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found for lazy loading."); return; }
|
|
357
290
|
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
358
291
|
try {
|
|
359
|
-
|
|
292
|
+
chartContainer.innerHTML = ''; // Clear placeholder
|
|
293
|
+
const chartOptions = {
|
|
294
|
+
chart: { type: 'area', height: 350, backgroundColor: 'transparent' },
|
|
295
|
+
title: { text: null },
|
|
296
|
+
xAxis: { categories: ${categoriesString}, crosshair: true, labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}},
|
|
297
|
+
yAxis: {
|
|
298
|
+
title: { text: 'Duration', style: { color: 'var(--text-color)' } },
|
|
299
|
+
labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)', fontSize: '12px' }},
|
|
300
|
+
min: 0
|
|
301
|
+
},
|
|
302
|
+
legend: { layout: 'horizontal', align: 'center', verticalAlign: 'bottom', itemStyle: { fontSize: '12px', color: 'var(--text-color)' }},
|
|
303
|
+
plotOptions: { area: { lineWidth: 2.5, states: { hover: { lineWidthPlus: 0 } }, threshold: null }},
|
|
304
|
+
tooltip: {
|
|
305
|
+
shared: true, useHTML: true, backgroundColor: 'rgba(10,10,10,0.92)', borderColor: 'rgba(10,10,10,0.92)', style: { color: '#f5f5f5' },
|
|
306
|
+
formatter: function () {
|
|
307
|
+
const runsData = ${runsForTooltipString};
|
|
308
|
+
const pointIndex = this.points[0].point.x;
|
|
309
|
+
const run = runsData[pointIndex];
|
|
310
|
+
let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' + 'Date: ' + new Date(run.timestamp).toLocaleString() + '<br>';
|
|
311
|
+
this.points.forEach(point => { tooltip += '<span style="color:' + point.series.color + '">●</span> ' + point.series.name + ': <b>' + formatDuration(point.y) + '</b><br>'; });
|
|
312
|
+
tooltip += '<br>Tests: ' + run.totalTests;
|
|
313
|
+
return tooltip;
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
series: ${seriesStringForRender}, // This is already a string representation of an array
|
|
317
|
+
credits: { enabled: false }
|
|
318
|
+
};
|
|
360
319
|
Highcharts.chart('${chartId}', chartOptions);
|
|
361
320
|
} catch (e) {
|
|
362
|
-
console.error("Error rendering chart ${chartId}:", e);
|
|
363
|
-
|
|
321
|
+
console.error("Error rendering chart ${chartId} (lazy):", e);
|
|
322
|
+
chartContainer.innerHTML = '<div class="no-data">Error rendering duration trend chart.</div>';
|
|
364
323
|
}
|
|
365
324
|
} else {
|
|
366
|
-
|
|
325
|
+
chartContainer.innerHTML = '<div class="no-data">Charting library not available for duration trends.</div>';
|
|
367
326
|
}
|
|
368
|
-
}
|
|
327
|
+
};
|
|
369
328
|
</script>
|
|
370
329
|
`;
|
|
371
330
|
}
|
|
372
|
-
|
|
373
331
|
function formatDate(dateStrOrDate) {
|
|
374
332
|
if (!dateStrOrDate) return "N/A";
|
|
375
333
|
try {
|
|
@@ -388,11 +346,9 @@ function formatDate(dateStrOrDate) {
|
|
|
388
346
|
return "Invalid Date Format";
|
|
389
347
|
}
|
|
390
348
|
}
|
|
391
|
-
|
|
392
349
|
function generateTestHistoryChart(history) {
|
|
393
350
|
if (!history || history.length === 0)
|
|
394
351
|
return '<div class="no-data-chart">No data for chart</div>';
|
|
395
|
-
|
|
396
352
|
const validHistory = history.filter(
|
|
397
353
|
(h) => h && typeof h.duration === "number" && h.duration >= 0
|
|
398
354
|
);
|
|
@@ -402,6 +358,10 @@ function generateTestHistoryChart(history) {
|
|
|
402
358
|
const chartId = `testHistoryChart-${Date.now()}-${Math.random()
|
|
403
359
|
.toString(36)
|
|
404
360
|
.substring(2, 7)}`;
|
|
361
|
+
const renderFunctionName = `renderTestHistoryChart_${chartId.replace(
|
|
362
|
+
/-/g,
|
|
363
|
+
"_"
|
|
364
|
+
)}`;
|
|
405
365
|
|
|
406
366
|
const seriesDataPoints = validHistory.map((run) => {
|
|
407
367
|
let color;
|
|
@@ -431,94 +391,71 @@ function generateTestHistoryChart(history) {
|
|
|
431
391
|
};
|
|
432
392
|
});
|
|
433
393
|
|
|
434
|
-
// Assuming var(--accent-color) is Deep Purple #673ab7
|
|
435
|
-
const accentColorRGB = "103, 58, 183";
|
|
394
|
+
const accentColorRGB = "103, 58, 183"; // Assuming var(--accent-color) is Deep Purple #673ab7
|
|
436
395
|
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
xAxis: {
|
|
442
|
-
categories: ${JSON.stringify(
|
|
443
|
-
validHistory.map((_, i) => `R${i + 1}`)
|
|
444
|
-
)},
|
|
445
|
-
labels: { style: { fontSize: '10px', color: 'var(--text-color-secondary)' } }
|
|
446
|
-
},
|
|
447
|
-
yAxis: {
|
|
448
|
-
title: { text: null },
|
|
449
|
-
labels: {
|
|
450
|
-
formatter: function() { return formatDuration(this.value); },
|
|
451
|
-
style: { fontSize: '10px', color: 'var(--text-color-secondary)' },
|
|
452
|
-
align: 'left', x: -35, y: 3
|
|
453
|
-
},
|
|
454
|
-
min: 0,
|
|
455
|
-
gridLineWidth: 0,
|
|
456
|
-
tickAmount: 4
|
|
457
|
-
},
|
|
458
|
-
legend: { enabled: false },
|
|
459
|
-
plotOptions: {
|
|
460
|
-
area: {
|
|
461
|
-
lineWidth: 2,
|
|
462
|
-
lineColor: 'var(--accent-color)',
|
|
463
|
-
fillColor: {
|
|
464
|
-
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
465
|
-
stops: [
|
|
466
|
-
[0, 'rgba(${accentColorRGB}, 0.4)'],
|
|
467
|
-
[1, 'rgba(${accentColorRGB}, 0)']
|
|
468
|
-
]
|
|
469
|
-
},
|
|
470
|
-
marker: { enabled: true },
|
|
471
|
-
threshold: null
|
|
472
|
-
}
|
|
473
|
-
},
|
|
474
|
-
tooltip: {
|
|
475
|
-
useHTML: true,
|
|
476
|
-
backgroundColor: 'rgba(10,10,10,0.92)',
|
|
477
|
-
borderColor: 'rgba(10,10,10,0.92)',
|
|
478
|
-
style: { color: '#f5f5f5', padding: '8px' },
|
|
479
|
-
formatter: function() {
|
|
480
|
-
const pointData = this.point;
|
|
481
|
-
let statusBadgeHtml = '<span style="padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; background-color: ';
|
|
482
|
-
switch(String(pointData.status).toLowerCase()) {
|
|
483
|
-
case 'passed': statusBadgeHtml += 'var(--success-color)'; break;
|
|
484
|
-
case 'failed': statusBadgeHtml += 'var(--danger-color)'; break;
|
|
485
|
-
case 'skipped': statusBadgeHtml += 'var(--warning-color)'; break;
|
|
486
|
-
default: statusBadgeHtml += 'var(--dark-gray-color)';
|
|
487
|
-
}
|
|
488
|
-
statusBadgeHtml += ';">' + String(pointData.status).toUpperCase() + '</span>';
|
|
396
|
+
const categoriesString = JSON.stringify(
|
|
397
|
+
validHistory.map((_, i) => `R${i + 1}`)
|
|
398
|
+
);
|
|
399
|
+
const seriesDataPointsString = JSON.stringify(seriesDataPoints);
|
|
489
400
|
|
|
490
|
-
return '<strong>Run ' + (pointData.runId || (this.point.index + 1)) + '</strong><br>' +
|
|
491
|
-
'Status: ' + statusBadgeHtml + '<br>' +
|
|
492
|
-
'Duration: ' + formatDuration(pointData.y);
|
|
493
|
-
}
|
|
494
|
-
},
|
|
495
|
-
series: [{
|
|
496
|
-
data: ${JSON.stringify(seriesDataPoints)},
|
|
497
|
-
showInLegend: false
|
|
498
|
-
}],
|
|
499
|
-
credits: { enabled: false }
|
|
500
|
-
}
|
|
501
|
-
`;
|
|
502
401
|
return `
|
|
503
|
-
<div id="${chartId}" style="width: 320px; height: 100px;"
|
|
402
|
+
<div id="${chartId}" style="width: 320px; height: 100px;" class="lazy-load-chart" data-render-function-name="${renderFunctionName}">
|
|
403
|
+
<div class="no-data-chart">Loading History...</div>
|
|
404
|
+
</div>
|
|
504
405
|
<script>
|
|
505
|
-
|
|
406
|
+
window.${renderFunctionName} = function() {
|
|
407
|
+
const chartContainer = document.getElementById('${chartId}');
|
|
408
|
+
if (!chartContainer) { console.error("Chart container ${chartId} not found for lazy loading."); return; }
|
|
506
409
|
if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
|
|
507
410
|
try {
|
|
508
|
-
|
|
411
|
+
chartContainer.innerHTML = ''; // Clear placeholder
|
|
412
|
+
const chartOptions = {
|
|
413
|
+
chart: { type: 'area', height: 100, width: 320, backgroundColor: 'transparent', spacing: [10,10,15,35] },
|
|
414
|
+
title: { text: null },
|
|
415
|
+
xAxis: { categories: ${categoriesString}, labels: { style: { fontSize: '10px', color: 'var(--text-color-secondary)' }}},
|
|
416
|
+
yAxis: {
|
|
417
|
+
title: { text: null },
|
|
418
|
+
labels: { formatter: function() { return formatDuration(this.value); }, style: { fontSize: '10px', color: 'var(--text-color-secondary)' }, align: 'left', x: -35, y: 3 },
|
|
419
|
+
min: 0, gridLineWidth: 0, tickAmount: 4
|
|
420
|
+
},
|
|
421
|
+
legend: { enabled: false },
|
|
422
|
+
plotOptions: {
|
|
423
|
+
area: {
|
|
424
|
+
lineWidth: 2, lineColor: 'var(--accent-color)',
|
|
425
|
+
fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(${accentColorRGB}, 0.4)'],[1, 'rgba(${accentColorRGB}, 0)']]},
|
|
426
|
+
marker: { enabled: true }, threshold: null
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
tooltip: {
|
|
430
|
+
useHTML: true, backgroundColor: 'rgba(10,10,10,0.92)', borderColor: 'rgba(10,10,10,0.92)', style: { color: '#f5f5f5', padding: '8px' },
|
|
431
|
+
formatter: function() {
|
|
432
|
+
const pointData = this.point;
|
|
433
|
+
let statusBadgeHtml = '<span style="padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; background-color: ';
|
|
434
|
+
switch(String(pointData.status).toLowerCase()) {
|
|
435
|
+
case 'passed': statusBadgeHtml += 'var(--success-color)'; break;
|
|
436
|
+
case 'failed': statusBadgeHtml += 'var(--danger-color)'; break;
|
|
437
|
+
case 'skipped': statusBadgeHtml += 'var(--warning-color)'; break;
|
|
438
|
+
default: statusBadgeHtml += 'var(--dark-gray-color)';
|
|
439
|
+
}
|
|
440
|
+
statusBadgeHtml += ';">' + String(pointData.status).toUpperCase() + '</span>';
|
|
441
|
+
return '<strong>Run ' + (pointData.runId || (this.point.index + 1)) + '</strong><br>' + 'Status: ' + statusBadgeHtml + '<br>' + 'Duration: ' + formatDuration(pointData.y);
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
series: [{ data: ${seriesDataPointsString}, showInLegend: false }],
|
|
445
|
+
credits: { enabled: false }
|
|
446
|
+
};
|
|
509
447
|
Highcharts.chart('${chartId}', chartOptions);
|
|
510
448
|
} catch (e) {
|
|
511
|
-
console.error("Error rendering chart ${chartId}:", e);
|
|
512
|
-
|
|
449
|
+
console.error("Error rendering chart ${chartId} (lazy):", e);
|
|
450
|
+
chartContainer.innerHTML = '<div class="no-data-chart">Error rendering history chart.</div>';
|
|
513
451
|
}
|
|
514
452
|
} else {
|
|
515
|
-
|
|
453
|
+
chartContainer.innerHTML = '<div class="no-data-chart">Charting library not available for history.</div>';
|
|
516
454
|
}
|
|
517
|
-
}
|
|
455
|
+
};
|
|
518
456
|
</script>
|
|
519
457
|
`;
|
|
520
458
|
}
|
|
521
|
-
|
|
522
459
|
function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
523
460
|
const total = data.reduce((sum, d) => sum + d.value, 0);
|
|
524
461
|
if (total === 0) {
|
|
@@ -625,7 +562,7 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
625
562
|
`;
|
|
626
563
|
|
|
627
564
|
return `
|
|
628
|
-
<div class="pie-chart-wrapper" style="align-items: center">
|
|
565
|
+
<div class="pie-chart-wrapper" style="align-items: center; max-height: 450px">
|
|
629
566
|
<div style="display: flex; align-items: start; width: 100%;"><h3>Test Distribution</h3></div>
|
|
630
567
|
<div id="${chartId}" style="width: ${chartWidth}px; height: ${
|
|
631
568
|
chartHeight - 40
|
|
@@ -648,7 +585,6 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
648
585
|
</div>
|
|
649
586
|
`;
|
|
650
587
|
}
|
|
651
|
-
|
|
652
588
|
function generateTestHistoryContent(trendData) {
|
|
653
589
|
if (
|
|
654
590
|
!trendData ||
|
|
@@ -734,7 +670,7 @@ function generateTestHistoryContent(trendData) {
|
|
|
734
670
|
</span>
|
|
735
671
|
</div>
|
|
736
672
|
<div class="test-history-trend">
|
|
737
|
-
${generateTestHistoryChart(test.history)}
|
|
673
|
+
${generateTestHistoryChart(test.history)}
|
|
738
674
|
</div>
|
|
739
675
|
<details class="test-history-details-collapsible">
|
|
740
676
|
<summary>Show Run Details (${test.history.length})</summary>
|
|
@@ -768,7 +704,6 @@ function generateTestHistoryContent(trendData) {
|
|
|
768
704
|
</div>
|
|
769
705
|
`;
|
|
770
706
|
}
|
|
771
|
-
|
|
772
707
|
function getStatusClass(status) {
|
|
773
708
|
switch (String(status).toLowerCase()) {
|
|
774
709
|
case "passed":
|
|
@@ -781,7 +716,6 @@ function getStatusClass(status) {
|
|
|
781
716
|
return "status-unknown";
|
|
782
717
|
}
|
|
783
718
|
}
|
|
784
|
-
|
|
785
719
|
function getStatusIcon(status) {
|
|
786
720
|
switch (String(status).toLowerCase()) {
|
|
787
721
|
case "passed":
|
|
@@ -794,7 +728,6 @@ function getStatusIcon(status) {
|
|
|
794
728
|
return "❓";
|
|
795
729
|
}
|
|
796
730
|
}
|
|
797
|
-
|
|
798
731
|
function getSuitesData(results) {
|
|
799
732
|
const suitesMap = new Map();
|
|
800
733
|
if (!results || results.length === 0) return [];
|
|
@@ -837,19 +770,12 @@ function getSuitesData(results) {
|
|
|
837
770
|
if (currentStatus && suite[currentStatus] !== undefined) {
|
|
838
771
|
suite[currentStatus]++;
|
|
839
772
|
}
|
|
840
|
-
|
|
841
|
-
if (currentStatus === "failed")
|
|
842
|
-
suite.statusOverall = "failed";
|
|
843
|
-
} else if (
|
|
844
|
-
currentStatus === "skipped" &&
|
|
845
|
-
suite.statusOverall !== "failed"
|
|
846
|
-
) {
|
|
773
|
+
if (currentStatus === "failed") suite.statusOverall = "failed";
|
|
774
|
+
else if (currentStatus === "skipped" && suite.statusOverall !== "failed")
|
|
847
775
|
suite.statusOverall = "skipped";
|
|
848
|
-
}
|
|
849
776
|
});
|
|
850
777
|
return Array.from(suitesMap.values());
|
|
851
778
|
}
|
|
852
|
-
|
|
853
779
|
function generateSuitesWidget(suitesData) {
|
|
854
780
|
if (!suitesData || suitesData.length === 0) {
|
|
855
781
|
return `<div class="suites-widget"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
|
|
@@ -858,12 +784,12 @@ function generateSuitesWidget(suitesData) {
|
|
|
858
784
|
<div class="suites-widget">
|
|
859
785
|
<div class="suites-header">
|
|
860
786
|
<h2>Test Suites</h2>
|
|
861
|
-
<span class="summary-badge"
|
|
862
|
-
|
|
787
|
+
<span class="summary-badge">${
|
|
788
|
+
suitesData.length
|
|
789
|
+
} suites • ${suitesData.reduce(
|
|
863
790
|
(sum, suite) => sum + suite.count,
|
|
864
791
|
0
|
|
865
|
-
)} tests
|
|
866
|
-
</span>
|
|
792
|
+
)} tests</span>
|
|
867
793
|
</div>
|
|
868
794
|
<div class="suites-grid">
|
|
869
795
|
${suitesData
|
|
@@ -875,10 +801,9 @@ function generateSuitesWidget(suitesData) {
|
|
|
875
801
|
suite.name
|
|
876
802
|
)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
|
|
877
803
|
</div>
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
</div>
|
|
804
|
+
<div>🖥️ <span class="browser-tag">${sanitizeHTML(
|
|
805
|
+
suite.browser
|
|
806
|
+
)}</span></div>
|
|
882
807
|
<div class="suite-card-body">
|
|
883
808
|
<span class="test-count">${suite.count} test${
|
|
884
809
|
suite.count !== 1 ? "s" : ""
|
|
@@ -907,7 +832,6 @@ function generateSuitesWidget(suitesData) {
|
|
|
907
832
|
</div>
|
|
908
833
|
</div>`;
|
|
909
834
|
}
|
|
910
|
-
|
|
911
835
|
function generateHTML(reportData, trendData = null) {
|
|
912
836
|
const { run, results } = reportData;
|
|
913
837
|
const suitesData = getSuitesData(reportData.results || []);
|
|
@@ -919,8 +843,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
919
843
|
duration: 0,
|
|
920
844
|
timestamp: new Date().toISOString(),
|
|
921
845
|
};
|
|
922
|
-
|
|
923
|
-
const totalTestsOr1 = runSummary.totalTests || 1; // Avoid division by zero
|
|
846
|
+
const totalTestsOr1 = runSummary.totalTests || 1;
|
|
924
847
|
const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
|
|
925
848
|
const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
|
|
926
849
|
const skipPercentage = Math.round(
|
|
@@ -930,19 +853,15 @@ function generateHTML(reportData, trendData = null) {
|
|
|
930
853
|
runSummary.totalTests > 0
|
|
931
854
|
? formatDuration(runSummary.duration / runSummary.totalTests)
|
|
932
855
|
: "0.0s";
|
|
933
|
-
|
|
934
856
|
function generateTestCasesHTML() {
|
|
935
|
-
if (!results || results.length === 0)
|
|
857
|
+
if (!results || results.length === 0)
|
|
936
858
|
return '<div class="no-tests">No test results found in this run.</div>';
|
|
937
|
-
}
|
|
938
|
-
|
|
939
859
|
return results
|
|
940
860
|
.map((test, index) => {
|
|
941
861
|
const browser = test.browser || "unknown";
|
|
942
862
|
const testFileParts = test.name.split(" > ");
|
|
943
863
|
const testTitle =
|
|
944
864
|
testFileParts[testFileParts.length - 1] || "Unnamed Test";
|
|
945
|
-
|
|
946
865
|
const generateStepsHTML = (steps, depth = 0) => {
|
|
947
866
|
if (!steps || steps.length === 0)
|
|
948
867
|
return "<div class='no-steps'>No steps recorded for this test.</div>";
|
|
@@ -954,7 +873,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
954
873
|
? `step-hook step-hook-${step.hookType}`
|
|
955
874
|
: "";
|
|
956
875
|
const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
|
|
957
|
-
|
|
958
876
|
return `
|
|
959
877
|
<div class="step-item" style="--depth: ${depth};">
|
|
960
878
|
<div class="step-header ${stepClass}" role="button" aria-expanded="false">
|
|
@@ -976,16 +894,34 @@ function generateHTML(reportData, trendData = null) {
|
|
|
976
894
|
}
|
|
977
895
|
${
|
|
978
896
|
step.errorMessage
|
|
979
|
-
?
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
897
|
+
? `<div class="step-error">
|
|
898
|
+
${
|
|
899
|
+
step.stackTrace
|
|
900
|
+
? `<div class="stack-trace">${formatPlaywrightError(
|
|
901
|
+
step.stackTrace
|
|
902
|
+
)}</div>`
|
|
903
|
+
: ""
|
|
904
|
+
}
|
|
905
|
+
<button
|
|
906
|
+
class="copy-error-btn"
|
|
907
|
+
onclick="copyErrorToClipboard(this)"
|
|
908
|
+
style="
|
|
909
|
+
margin-top: 8px;
|
|
910
|
+
padding: 4px 8px;
|
|
911
|
+
background: #f0f0f0;
|
|
912
|
+
border: 2px solid #ccc;
|
|
913
|
+
border-radius: 4px;
|
|
914
|
+
cursor: pointer;
|
|
915
|
+
font-size: 12px;
|
|
916
|
+
border-color: #8B0000;
|
|
917
|
+
color: #8B0000;
|
|
918
|
+
"
|
|
919
|
+
onmouseover="this.style.background='#e0e0e0'"
|
|
920
|
+
onmouseout="this.style.background='#f0f0f0'"
|
|
921
|
+
>
|
|
922
|
+
Copy Error Prompt
|
|
923
|
+
</button>
|
|
924
|
+
</div>`
|
|
989
925
|
: ""
|
|
990
926
|
}
|
|
991
927
|
${
|
|
@@ -1002,6 +938,16 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1002
938
|
.join("");
|
|
1003
939
|
};
|
|
1004
940
|
|
|
941
|
+
// Local escapeHTML for screenshot rendering part, ensuring it uses proper entities
|
|
942
|
+
const escapeHTMLForScreenshots = (str) => {
|
|
943
|
+
if (str === null || str === undefined) return "";
|
|
944
|
+
return String(str).replace(
|
|
945
|
+
/[&<>"']/g,
|
|
946
|
+
(match) =>
|
|
947
|
+
({ "&": "&", "<": "<", ">": ">", '"': '"', "'": "'" }[match] ||
|
|
948
|
+
match)
|
|
949
|
+
);
|
|
950
|
+
};
|
|
1005
951
|
return `
|
|
1006
952
|
<div class="test-case" data-status="${
|
|
1007
953
|
test.status
|
|
@@ -1032,107 +978,93 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1032
978
|
<div class="test-case-content" style="display: none;">
|
|
1033
979
|
<p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
|
|
1034
980
|
${
|
|
1035
|
-
test.
|
|
1036
|
-
? `<div class="test-error-summary"
|
|
1037
|
-
|
|
1038
|
-
|
|
981
|
+
test.errorMessage
|
|
982
|
+
? `<div class="test-error-summary">${formatPlaywrightError(
|
|
983
|
+
test.errorMessage
|
|
984
|
+
)}
|
|
985
|
+
<button
|
|
986
|
+
class="copy-error-btn"
|
|
987
|
+
onclick="copyErrorToClipboard(this)"
|
|
988
|
+
style="
|
|
989
|
+
margin-top: 8px;
|
|
990
|
+
padding: 4px 8px;
|
|
991
|
+
background: #f0f0f0;
|
|
992
|
+
border: 2px solid #ccc;
|
|
993
|
+
border-radius: 4px;
|
|
994
|
+
cursor: pointer;
|
|
995
|
+
font-size: 12px;
|
|
996
|
+
border-color: #8B0000;
|
|
997
|
+
color: #8B0000;
|
|
998
|
+
"
|
|
999
|
+
onmouseover="this.style.background='#e0e0e0'"
|
|
1000
|
+
onmouseout="this.style.background='#f0f0f0'"
|
|
1001
|
+
>
|
|
1002
|
+
Copy Error Prompt
|
|
1003
|
+
</button>
|
|
1004
|
+
</div>`
|
|
1039
1005
|
: ""
|
|
1040
1006
|
}
|
|
1041
|
-
|
|
1042
1007
|
<h4>Steps</h4>
|
|
1043
1008
|
<div class="steps-list">${generateStepsHTML(test.steps)}</div>
|
|
1044
|
-
|
|
1045
1009
|
${
|
|
1046
1010
|
test.stdout && test.stdout.length > 0
|
|
1047
|
-
?
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
<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
|
|
1051
|
-
.map((line) => sanitizeHTML(line))
|
|
1052
|
-
.join("\n")}</pre>
|
|
1053
|
-
</div>`
|
|
1011
|
+
? `<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
|
|
1012
|
+
.map((line) => sanitizeHTML(line))
|
|
1013
|
+
.join("\n")}</pre></div>`
|
|
1054
1014
|
: ""
|
|
1055
1015
|
}
|
|
1056
1016
|
${
|
|
1057
1017
|
test.stderr && test.stderr.length > 0
|
|
1058
|
-
?
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
<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
|
|
1062
|
-
.map((line) => sanitizeHTML(line))
|
|
1063
|
-
.join("\n")}</pre>
|
|
1064
|
-
</div>`
|
|
1018
|
+
? `<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
|
|
1019
|
+
.map((line) => sanitizeHTML(line))
|
|
1020
|
+
.join("\n")}</pre></div>`
|
|
1065
1021
|
: ""
|
|
1066
1022
|
}
|
|
1067
|
-
|
|
1068
1023
|
${(() => {
|
|
1024
|
+
// Screenshots
|
|
1069
1025
|
if (!test.screenshots || test.screenshots.length === 0) return "";
|
|
1070
|
-
|
|
1071
|
-
// Define base output directory to resolve relative screenshot paths
|
|
1072
|
-
// This assumes screenshot paths in your JSON are relative to DEFAULT_OUTPUT_DIR
|
|
1073
1026
|
const baseOutputDir = path.resolve(
|
|
1074
1027
|
process.cwd(),
|
|
1075
1028
|
DEFAULT_OUTPUT_DIR
|
|
1076
1029
|
);
|
|
1077
1030
|
|
|
1078
|
-
// Helper to escape HTML special characters (safer than the global sanitizeHTML)
|
|
1079
|
-
const escapeHTML = (str) => {
|
|
1080
|
-
if (str === null || str === undefined) return "";
|
|
1081
|
-
return String(str).replace(/[&<>"']/g, (match) => {
|
|
1082
|
-
const replacements = {
|
|
1083
|
-
"&": "&",
|
|
1084
|
-
"<": "<",
|
|
1085
|
-
">": ">",
|
|
1086
|
-
'"': '"',
|
|
1087
|
-
"'": "'",
|
|
1088
|
-
};
|
|
1089
|
-
return replacements[match] || match;
|
|
1090
|
-
});
|
|
1091
|
-
};
|
|
1092
|
-
|
|
1093
1031
|
const renderScreenshot = (screenshotPathOrData, index) => {
|
|
1094
1032
|
let base64ImageData = "";
|
|
1095
1033
|
const uniqueSuffix = `${Date.now()}-${index}-${Math.random()
|
|
1096
1034
|
.toString(36)
|
|
1097
1035
|
.substring(2, 7)}`;
|
|
1098
|
-
|
|
1099
1036
|
try {
|
|
1100
1037
|
if (
|
|
1101
1038
|
typeof screenshotPathOrData === "string" &&
|
|
1102
1039
|
!screenshotPathOrData.startsWith("data:image")
|
|
1103
1040
|
) {
|
|
1104
|
-
// It's likely a file path, try to read and convert
|
|
1105
1041
|
const imagePath = path.resolve(
|
|
1106
1042
|
baseOutputDir,
|
|
1107
1043
|
screenshotPathOrData
|
|
1108
1044
|
);
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
base64ImageData = imageBuffer.toString("base64");
|
|
1114
|
-
} else {
|
|
1045
|
+
if (fsExistsSync(imagePath))
|
|
1046
|
+
base64ImageData =
|
|
1047
|
+
readFileSync(imagePath).toString("base64");
|
|
1048
|
+
else {
|
|
1115
1049
|
console.warn(
|
|
1116
1050
|
chalk.yellow(
|
|
1117
1051
|
`[Reporter] Screenshot file not found: ${imagePath}`
|
|
1118
1052
|
)
|
|
1119
1053
|
);
|
|
1120
|
-
return `<div class="attachment-item error" style="padding:10px; color:red;">Screenshot not found: ${
|
|
1054
|
+
return `<div class="attachment-item error" style="padding:10px; color:red;">Screenshot not found: ${escapeHTMLForScreenshots(
|
|
1121
1055
|
screenshotPathOrData
|
|
1122
1056
|
)}</div>`;
|
|
1123
1057
|
}
|
|
1124
1058
|
} else if (
|
|
1125
1059
|
typeof screenshotPathOrData === "string" &&
|
|
1126
1060
|
screenshotPathOrData.startsWith("data:image/png;base64,")
|
|
1127
|
-
)
|
|
1128
|
-
// It's already a data URI, extract base64 part
|
|
1061
|
+
)
|
|
1129
1062
|
base64ImageData = screenshotPathOrData.substring(
|
|
1130
1063
|
"data:image/png;base64,".length
|
|
1131
1064
|
);
|
|
1132
|
-
|
|
1133
|
-
// Assume it's raw Base64 data if it's a string but not a known path or full data URI
|
|
1065
|
+
else if (typeof screenshotPathOrData === "string")
|
|
1134
1066
|
base64ImageData = screenshotPathOrData;
|
|
1135
|
-
|
|
1067
|
+
else {
|
|
1136
1068
|
console.warn(
|
|
1137
1069
|
chalk.yellow(
|
|
1138
1070
|
`[Reporter] Invalid screenshot data type for item at index ${index}.`
|
|
@@ -1140,76 +1072,46 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1140
1072
|
);
|
|
1141
1073
|
return `<div class="attachment-item error" style="padding:10px; color:red;">Invalid screenshot data</div>`;
|
|
1142
1074
|
}
|
|
1143
|
-
|
|
1144
1075
|
if (!base64ImageData) {
|
|
1145
|
-
// This case should ideally be caught above, but as a fallback:
|
|
1146
1076
|
console.warn(
|
|
1147
1077
|
chalk.yellow(
|
|
1148
|
-
`[Reporter] Could not obtain base64 data for screenshot: ${
|
|
1078
|
+
`[Reporter] Could not obtain base64 data for screenshot: ${escapeHTMLForScreenshots(
|
|
1149
1079
|
String(screenshotPathOrData)
|
|
1150
1080
|
)}`
|
|
1151
1081
|
)
|
|
1152
1082
|
);
|
|
1153
|
-
return `<div class="attachment-item error" style="padding:10px; color:red;">Error loading screenshot: ${
|
|
1083
|
+
return `<div class="attachment-item error" style="padding:10px; color:red;">Error loading screenshot: ${escapeHTMLForScreenshots(
|
|
1154
1084
|
String(screenshotPathOrData)
|
|
1155
1085
|
)}</div>`;
|
|
1156
1086
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
loading="lazy"
|
|
1163
|
-
onerror="this.alt='Error displaying embedded image'; this.style.display='none'; this.parentElement.innerHTML='<p style=\\'color:red;padding:10px;\\'>Error displaying screenshot ${
|
|
1164
|
-
index + 1
|
|
1165
|
-
}.</p>';">
|
|
1166
|
-
<div class="attachment-info">
|
|
1167
|
-
<div class="trace-actions">
|
|
1168
|
-
<a href="data:image/png;base64,${base64ImageData}"
|
|
1169
|
-
target="_blank"
|
|
1170
|
-
class="view-full">
|
|
1171
|
-
View Full Image
|
|
1172
|
-
</a>
|
|
1173
|
-
<a href="data:image/png;base64,${base64ImageData}"
|
|
1174
|
-
target="_blank"
|
|
1175
|
-
download="screenshot-${uniqueSuffix}.png">
|
|
1176
|
-
Download
|
|
1177
|
-
</a>
|
|
1178
|
-
</div>
|
|
1179
|
-
</div>
|
|
1180
|
-
</div>`;
|
|
1087
|
+
return `<div class="attachment-item"><img src="data:image/png;base64,${base64ImageData}" alt="Screenshot ${
|
|
1088
|
+
index + 1
|
|
1089
|
+
}" 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 ${
|
|
1090
|
+
index + 1
|
|
1091
|
+
}.</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>`;
|
|
1181
1092
|
} catch (e) {
|
|
1182
1093
|
console.error(
|
|
1183
1094
|
chalk.red(
|
|
1184
|
-
`[Reporter] Error processing screenshot ${
|
|
1095
|
+
`[Reporter] Error processing screenshot ${escapeHTMLForScreenshots(
|
|
1185
1096
|
String(screenshotPathOrData)
|
|
1186
1097
|
)}: ${e.message}`
|
|
1187
1098
|
)
|
|
1188
1099
|
);
|
|
1189
|
-
return `<div class="attachment-item error" style="padding:10px; color:red;">Failed to load screenshot: ${
|
|
1100
|
+
return `<div class="attachment-item error" style="padding:10px; color:red;">Failed to load screenshot: ${escapeHTMLForScreenshots(
|
|
1190
1101
|
String(screenshotPathOrData)
|
|
1191
1102
|
)}</div>`;
|
|
1192
1103
|
}
|
|
1193
|
-
};
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
${test.screenshots.map(renderScreenshot).join("")}
|
|
1200
|
-
</div>
|
|
1201
|
-
</div>
|
|
1202
|
-
`;
|
|
1104
|
+
};
|
|
1105
|
+
return `<div class="attachments-section"><h4>Screenshots (${
|
|
1106
|
+
test.screenshots.length
|
|
1107
|
+
})</h4><div class="attachments-grid">${test.screenshots
|
|
1108
|
+
.map(renderScreenshot)
|
|
1109
|
+
.join("")}</div></div>`;
|
|
1203
1110
|
})()}
|
|
1204
|
-
|
|
1205
1111
|
${
|
|
1206
1112
|
test.videoPath
|
|
1207
|
-
?
|
|
1208
|
-
|
|
1209
|
-
<h4>Videos</h4>
|
|
1210
|
-
<div class="attachments-grid">
|
|
1211
|
-
${(() => {
|
|
1212
|
-
// Handle both string and array cases
|
|
1113
|
+
? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${(() => {
|
|
1114
|
+
// Videos
|
|
1213
1115
|
const videos = Array.isArray(test.videoPath)
|
|
1214
1116
|
? test.videoPath
|
|
1215
1117
|
: [test.videoPath];
|
|
@@ -1220,7 +1122,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1220
1122
|
mov: "video/quicktime",
|
|
1221
1123
|
avi: "video/x-msvideo",
|
|
1222
1124
|
};
|
|
1223
|
-
|
|
1224
1125
|
return videos
|
|
1225
1126
|
.map((video, index) => {
|
|
1226
1127
|
const videoUrl =
|
|
@@ -1229,82 +1130,53 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1229
1130
|
typeof video === "object"
|
|
1230
1131
|
? video.name || `Video ${index + 1}`
|
|
1231
1132
|
: `Video ${index + 1}`;
|
|
1232
|
-
const fileExtension = videoUrl
|
|
1133
|
+
const fileExtension = String(videoUrl)
|
|
1233
1134
|
.split(".")
|
|
1234
1135
|
.pop()
|
|
1235
1136
|
.toLowerCase();
|
|
1236
1137
|
const mimeType = mimeTypes[fileExtension] || "video/mp4";
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
<a href="${videoUrl}" target="_blank" download="${videoName}.${fileExtension}">
|
|
1247
|
-
Download
|
|
1248
|
-
</a>
|
|
1249
|
-
</div>
|
|
1250
|
-
</div>
|
|
1251
|
-
</div>
|
|
1252
|
-
`;
|
|
1138
|
+
return `<div class="attachment-item"><video controls width="100%" height="auto" title="${sanitizeHTML(
|
|
1139
|
+
videoName
|
|
1140
|
+
)}"><source src="${sanitizeHTML(
|
|
1141
|
+
videoUrl
|
|
1142
|
+
)}" type="${mimeType}">Your browser does not support the video tag.</video><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
|
|
1143
|
+
videoUrl
|
|
1144
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
1145
|
+
videoName
|
|
1146
|
+
)}.${fileExtension}">Download</a></div></div></div>`;
|
|
1253
1147
|
})
|
|
1254
1148
|
.join("");
|
|
1255
|
-
})()}
|
|
1256
|
-
</div>
|
|
1257
|
-
</div>
|
|
1258
|
-
`
|
|
1149
|
+
})()}</div></div>`
|
|
1259
1150
|
: ""
|
|
1260
1151
|
}
|
|
1261
|
-
|
|
1262
1152
|
${
|
|
1263
1153
|
test.tracePath
|
|
1264
|
-
?
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
<span class="trace-name">${traceName}</span>
|
|
1289
|
-
</div>
|
|
1290
|
-
<div class="attachment-info">
|
|
1291
|
-
<div class="trace-actions">
|
|
1292
|
-
<a href="${traceUrl}" target="_blank" download="${traceFileName}" class="download-trace">
|
|
1293
|
-
Download
|
|
1294
|
-
</a>
|
|
1295
|
-
</div>
|
|
1296
|
-
</div>
|
|
1297
|
-
</div>
|
|
1298
|
-
`;
|
|
1299
|
-
})
|
|
1300
|
-
.join("");
|
|
1301
|
-
})()}
|
|
1302
|
-
</div>
|
|
1303
|
-
</div>
|
|
1304
|
-
`
|
|
1154
|
+
? `<div class="attachments-section"><h4>Trace Files</h4><div class="attachments-grid">${(() => {
|
|
1155
|
+
// Traces
|
|
1156
|
+
const traces = Array.isArray(test.tracePath)
|
|
1157
|
+
? test.tracePath
|
|
1158
|
+
: [test.tracePath];
|
|
1159
|
+
return traces
|
|
1160
|
+
.map((trace, index) => {
|
|
1161
|
+
const traceUrl =
|
|
1162
|
+
typeof trace === "object" ? trace.url || "" : trace;
|
|
1163
|
+
const traceName =
|
|
1164
|
+
typeof trace === "object"
|
|
1165
|
+
? trace.name || `Trace ${index + 1}`
|
|
1166
|
+
: `Trace ${index + 1}`;
|
|
1167
|
+
const traceFileName = String(traceUrl).split("/").pop();
|
|
1168
|
+
return `<div class="attachment-item"><div class="trace-preview"><span class="trace-icon">📄</span><span class="trace-name">${sanitizeHTML(
|
|
1169
|
+
traceName
|
|
1170
|
+
)}</span></div><div class="attachment-info"><div class="trace-actions"><a href="${sanitizeHTML(
|
|
1171
|
+
traceUrl
|
|
1172
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
1173
|
+
traceFileName
|
|
1174
|
+
)}" class="download-trace">Download</a></div></div></div>`;
|
|
1175
|
+
})
|
|
1176
|
+
.join("");
|
|
1177
|
+
})()}</div></div>`
|
|
1305
1178
|
: ""
|
|
1306
1179
|
}
|
|
1307
|
-
|
|
1308
1180
|
${
|
|
1309
1181
|
test.codeSnippet
|
|
1310
1182
|
? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${sanitizeHTML(
|
|
@@ -1317,7 +1189,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1317
1189
|
})
|
|
1318
1190
|
.join("");
|
|
1319
1191
|
}
|
|
1320
|
-
|
|
1321
1192
|
return `
|
|
1322
1193
|
<!DOCTYPE html>
|
|
1323
1194
|
<html lang="en">
|
|
@@ -1329,30 +1200,17 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1329
1200
|
<script src="https://code.highcharts.com/highcharts.js"></script>
|
|
1330
1201
|
<title>Playwright Pulse Report</title>
|
|
1331
1202
|
<style>
|
|
1332
|
-
:root {
|
|
1333
|
-
--primary-color: #3f51b5;
|
|
1334
|
-
--
|
|
1335
|
-
--
|
|
1336
|
-
--
|
|
1337
|
-
--
|
|
1338
|
-
--
|
|
1339
|
-
--warning-color: #FFC107; /* Amber */
|
|
1340
|
-
--info-color: #2196F3; /* Blue */
|
|
1341
|
-
--light-gray-color: #f5f5f5;
|
|
1342
|
-
--medium-gray-color: #e0e0e0;
|
|
1343
|
-
--dark-gray-color: #757575;
|
|
1344
|
-
--text-color: #333;
|
|
1345
|
-
--text-color-secondary: #555;
|
|
1346
|
-
--border-color: #ddd;
|
|
1347
|
-
--background-color: #f8f9fa;
|
|
1348
|
-
--card-background-color: #fff;
|
|
1349
|
-
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
1350
|
-
--border-radius: 8px;
|
|
1351
|
-
--box-shadow: 0 5px 15px rgba(0,0,0,0.08);
|
|
1352
|
-
--box-shadow-light: 0 3px 8px rgba(0,0,0,0.05);
|
|
1353
|
-
--box-shadow-inset: inset 0 1px 3px rgba(0,0,0,0.07);
|
|
1203
|
+
:root {
|
|
1204
|
+
--primary-color: #3f51b5; --secondary-color: #ff4081; --accent-color: #673ab7; --accent-color-alt: #FF9800;
|
|
1205
|
+
--success-color: #4CAF50; --danger-color: #F44336; --warning-color: #FFC107; --info-color: #2196F3;
|
|
1206
|
+
--light-gray-color: #f5f5f5; --medium-gray-color: #e0e0e0; --dark-gray-color: #757575;
|
|
1207
|
+
--text-color: #333; --text-color-secondary: #555; --border-color: #ddd; --background-color: #f8f9fa;
|
|
1208
|
+
--card-background-color: #fff; --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
1209
|
+
--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);
|
|
1354
1210
|
}
|
|
1355
|
-
|
|
1211
|
+
.trend-chart-container, .test-history-trend div[id^="testHistoryChart-"] { min-height: 100px; }
|
|
1212
|
+
.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); }
|
|
1213
|
+
|
|
1356
1214
|
/* General Highcharts styling */
|
|
1357
1215
|
.highcharts-background { fill: transparent; }
|
|
1358
1216
|
.highcharts-title, .highcharts-subtitle { font-family: var(--font-family); }
|
|
@@ -1360,60 +1218,23 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1360
1218
|
.highcharts-axis-title { fill: var(--text-color) !important; }
|
|
1361
1219
|
.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; }
|
|
1362
1220
|
|
|
1363
|
-
body {
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
background-color: var(--background-color);
|
|
1367
|
-
color: var(--text-color);
|
|
1368
|
-
line-height: 1.65;
|
|
1369
|
-
font-size: 16px;
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
.container {
|
|
1373
|
-
max-width: 1600px;
|
|
1374
|
-
padding: 30px;
|
|
1375
|
-
border-radius: var(--border-radius);
|
|
1376
|
-
box-shadow: var(--box-shadow);
|
|
1377
|
-
background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec);
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
.header {
|
|
1381
|
-
display: flex;
|
|
1382
|
-
justify-content: space-between;
|
|
1383
|
-
align-items: center;
|
|
1384
|
-
flex-wrap: wrap;
|
|
1385
|
-
padding-bottom: 25px;
|
|
1386
|
-
border-bottom: 1px solid var(--border-color);
|
|
1387
|
-
margin-bottom: 25px;
|
|
1388
|
-
}
|
|
1221
|
+
body { font-family: var(--font-family); margin: 0; background-color: var(--background-color); color: var(--text-color); line-height: 1.65; font-size: 16px; }
|
|
1222
|
+
.container { padding: 30px; border-radius: var(--border-radius); box-shadow: var(--box-shadow); background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec); }
|
|
1223
|
+
.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; }
|
|
1389
1224
|
.header-title { display: flex; align-items: center; gap: 15px; }
|
|
1390
1225
|
.header h1 { margin: 0; font-size: 1.85em; font-weight: 600; color: var(--primary-color); }
|
|
1391
1226
|
#report-logo { height: 40px; width: 40px; border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.1);}
|
|
1392
1227
|
.run-info { font-size: 0.9em; text-align: right; color: var(--text-color-secondary); line-height:1.5;}
|
|
1393
1228
|
.run-info strong { color: var(--text-color); }
|
|
1394
|
-
|
|
1395
1229
|
.tabs { display: flex; border-bottom: 2px solid var(--border-color); margin-bottom: 30px; overflow-x: auto; }
|
|
1396
|
-
.tab-button {
|
|
1397
|
-
padding: 15px 25px; background: none; border: none; border-bottom: 3px solid transparent;
|
|
1398
|
-
cursor: pointer; font-size: 1.1em; font-weight: 600; color: black;
|
|
1399
|
-
transition: color 0.2s ease, border-color 0.2s ease; white-space: nowrap;
|
|
1400
|
-
}
|
|
1230
|
+
.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; }
|
|
1401
1231
|
.tab-button:hover { color: var(--accent-color); }
|
|
1402
1232
|
.tab-button.active { color: var(--primary-color); border-bottom-color: var(--primary-color); }
|
|
1403
1233
|
.tab-content { display: none; animation: fadeIn 0.4s ease-out; }
|
|
1404
1234
|
.tab-content.active { display: block; }
|
|
1405
1235
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
1406
|
-
|
|
1407
|
-
.
|
|
1408
|
-
display: grid;
|
|
1409
|
-
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
|
1410
|
-
gap: 22px; margin-bottom: 35px;
|
|
1411
|
-
}
|
|
1412
|
-
.summary-card {
|
|
1413
|
-
background-color: var(--card-background-color); border: 1px solid var(--border-color);
|
|
1414
|
-
border-radius: var(--border-radius); padding: 22px; text-align: center;
|
|
1415
|
-
box-shadow: var(--box-shadow-light); transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
1416
|
-
}
|
|
1236
|
+
.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 22px; margin-bottom: 35px; }
|
|
1237
|
+
.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; }
|
|
1417
1238
|
.summary-card:hover { transform: translateY(-5px); box-shadow: var(--box-shadow); }
|
|
1418
1239
|
.summary-card h3 { margin: 0 0 10px; font-size: 1.05em; font-weight: 500; color: var(--text-color-secondary); }
|
|
1419
1240
|
.summary-card .value { font-size: 2.4em; font-weight: 600; margin-bottom: 8px; }
|
|
@@ -1421,43 +1242,19 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1421
1242
|
.status-passed .value, .stat-passed svg { color: var(--success-color); }
|
|
1422
1243
|
.status-failed .value, .stat-failed svg { color: var(--danger-color); }
|
|
1423
1244
|
.status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
|
|
1424
|
-
|
|
1425
|
-
.
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
}
|
|
1429
|
-
.pie-chart-wrapper, .suites-widget, .trend-chart {
|
|
1430
|
-
background-color: var(--card-background-color); padding: 28px;
|
|
1431
|
-
border-radius: var(--border-radius); box-shadow: var(--box-shadow-light);
|
|
1432
|
-
display: flex; flex-direction: column;
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
.pie-chart-wrapper h3, .suites-header h2, .trend-chart h3 {
|
|
1436
|
-
text-align: center; margin-top: 0; margin-bottom: 25px;
|
|
1437
|
-
font-size: 1.25em; font-weight: 600; color: var(--text-color);
|
|
1438
|
-
}
|
|
1439
|
-
.trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { /* For Highcharts containers */
|
|
1440
|
-
flex-grow: 1;
|
|
1441
|
-
min-height: 250px; /* Ensure charts have some min height */
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
.chart-tooltip { /* This class was for D3, Highcharts has its own tooltip styling via JS/SVG */
|
|
1445
|
-
/* Basic styling for Highcharts HTML tooltips can be done via .highcharts-tooltip span */
|
|
1446
|
-
}
|
|
1245
|
+
.dashboard-bottom-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 28px; align-items: stretch; }
|
|
1246
|
+
.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; }
|
|
1247
|
+
.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); }
|
|
1248
|
+
.trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { flex-grow: 1; min-height: 250px; }
|
|
1447
1249
|
.status-badge-small-tooltip { padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; }
|
|
1448
1250
|
.status-badge-small-tooltip.status-passed { background-color: var(--success-color); }
|
|
1449
1251
|
.status-badge-small-tooltip.status-failed { background-color: var(--danger-color); }
|
|
1450
1252
|
.status-badge-small-tooltip.status-skipped { background-color: var(--warning-color); }
|
|
1451
1253
|
.status-badge-small-tooltip.status-unknown { background-color: var(--dark-gray-color); }
|
|
1452
|
-
|
|
1453
1254
|
.suites-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
|
1454
1255
|
.summary-badge { background-color: var(--light-gray-color); color: var(--text-color-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
|
|
1455
1256
|
.suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
|
|
1456
|
-
.suite-card {
|
|
1457
|
-
border: 1px solid var(--border-color); border-left-width: 5px;
|
|
1458
|
-
border-radius: calc(var(--border-radius) / 1.5); padding: 20px;
|
|
1459
|
-
background-color: var(--card-background-color); transition: box-shadow 0.2s ease, border-left-color 0.2s ease;
|
|
1460
|
-
}
|
|
1257
|
+
.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; }
|
|
1461
1258
|
.suite-card:hover { box-shadow: var(--box-shadow); }
|
|
1462
1259
|
.suite-card.status-passed { border-left-color: var(--success-color); }
|
|
1463
1260
|
.suite-card.status-failed { border-left-color: var(--danger-color); }
|
|
@@ -1469,67 +1266,36 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1469
1266
|
.suite-stats { display: flex; gap: 14px; font-size: 0.95em; align-items: center; }
|
|
1470
1267
|
.suite-stats span { display: flex; align-items: center; gap: 6px; }
|
|
1471
1268
|
.suite-stats svg { vertical-align: middle; font-size: 1.15em; }
|
|
1472
|
-
|
|
1473
|
-
.filters {
|
|
1474
|
-
display: flex; flex-wrap: wrap; gap: 18px; margin-bottom: 28px;
|
|
1475
|
-
padding: 20px; background-color: var(--light-gray-color); border-radius: var(--border-radius);
|
|
1476
|
-
box-shadow: var(--box-shadow-inset); border-color: black; border-style: groove;
|
|
1477
|
-
}
|
|
1478
|
-
.filters input, .filters select, .filters button {
|
|
1479
|
-
padding: 11px 15px; border: 1px solid var(--border-color);
|
|
1480
|
-
border-radius: 6px; font-size: 1em;
|
|
1481
|
-
}
|
|
1269
|
+
.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; }
|
|
1270
|
+
.filters input, .filters select, .filters button { padding: 11px 15px; border: 1px solid var(--border-color); border-radius: 6px; font-size: 1em; }
|
|
1482
1271
|
.filters input { flex-grow: 1; min-width: 240px;}
|
|
1483
1272
|
.filters select {min-width: 180px;}
|
|
1484
1273
|
.filters button { background-color: var(--primary-color); color: white; cursor: pointer; transition: background-color 0.2s ease, box-shadow 0.2s ease; border: none; }
|
|
1485
1274
|
.filters button:hover { background-color: var(--accent-color); box-shadow: 0 2px 5px rgba(0,0,0,0.15);}
|
|
1486
|
-
|
|
1487
|
-
.test-case {
|
|
1488
|
-
margin-bottom: 15px; border: 1px solid var(--border-color);
|
|
1489
|
-
border-radius: var(--border-radius); background-color: var(--card-background-color);
|
|
1490
|
-
box-shadow: var(--box-shadow-light); overflow: hidden;
|
|
1491
|
-
}
|
|
1492
|
-
.test-case-header {
|
|
1493
|
-
padding: 10px 15px; background-color: #fff; cursor: pointer;
|
|
1494
|
-
display: flex; justify-content: space-between; align-items: center;
|
|
1495
|
-
border-bottom: 1px solid transparent;
|
|
1496
|
-
transition: background-color 0.2s ease;
|
|
1497
|
-
}
|
|
1275
|
+
.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; }
|
|
1276
|
+
.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; }
|
|
1498
1277
|
.test-case-header:hover { background-color: #f4f6f8; }
|
|
1499
1278
|
.test-case-header[aria-expanded="true"] { border-bottom-color: var(--border-color); background-color: #f9fafb; }
|
|
1500
|
-
|
|
1501
1279
|
.test-case-summary { display: flex; align-items: center; gap: 14px; flex-grow: 1; flex-wrap: wrap;}
|
|
1502
1280
|
.test-case-title { font-weight: 600; color: var(--text-color); font-size: 1em; }
|
|
1503
1281
|
.test-case-browser { font-size: 0.9em; color: var(--text-color-secondary); }
|
|
1504
1282
|
.test-case-meta { display: flex; align-items: center; gap: 12px; font-size: 0.9em; color: var(--text-color-secondary); flex-shrink: 0; }
|
|
1505
1283
|
.test-duration { background-color: var(--light-gray-color); padding: 4px 10px; border-radius: 12px; font-size: 0.9em;}
|
|
1506
|
-
|
|
1507
|
-
.status-badge {
|
|
1508
|
-
padding: 5px; border-radius: 6px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase;
|
|
1509
|
-
min-width: 70px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
1510
|
-
}
|
|
1284
|
+
.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); }
|
|
1511
1285
|
.status-badge.status-passed { background-color: var(--success-color); }
|
|
1512
1286
|
.status-badge.status-failed { background-color: var(--danger-color); }
|
|
1513
1287
|
.status-badge.status-skipped { background-color: var(--warning-color); }
|
|
1514
1288
|
.status-badge.status-unknown { background-color: var(--dark-gray-color); }
|
|
1515
|
-
|
|
1516
1289
|
.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; }
|
|
1517
|
-
|
|
1518
1290
|
.test-case-content { display: none; padding: 20px; border-top: 1px solid var(--border-color); background-color: #fcfdff; }
|
|
1519
1291
|
.test-case-content h4 { margin-top: 22px; margin-bottom: 14px; font-size: 1.15em; color: var(--primary-color); }
|
|
1520
1292
|
.test-case-content p { margin-bottom: 10px; font-size: 1em; }
|
|
1521
1293
|
.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; }
|
|
1522
1294
|
.test-error-summary h4 { color: var(--danger-color); margin-top:0;}
|
|
1523
1295
|
.test-error-summary pre { white-space: pre-wrap; word-break: break-all; color: var(--danger-color); font-size: 0.95em;}
|
|
1524
|
-
|
|
1525
1296
|
.steps-list { margin: 18px 0; }
|
|
1526
1297
|
.step-item { margin-bottom: 8px; padding-left: calc(var(--depth, 0) * 28px); }
|
|
1527
|
-
.step-header {
|
|
1528
|
-
display: flex; align-items: center; cursor: pointer;
|
|
1529
|
-
padding: 10px 14px; border-radius: 6px; background-color: #fff;
|
|
1530
|
-
border: 1px solid var(--light-gray-color);
|
|
1531
|
-
transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
|
1532
|
-
}
|
|
1298
|
+
.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; }
|
|
1533
1299
|
.step-header:hover { background-color: #f0f2f5; border-color: var(--medium-gray-color); box-shadow: var(--box-shadow-inset); }
|
|
1534
1300
|
.step-icon { margin-right: 12px; width: 20px; text-align: center; font-size: 1.1em; }
|
|
1535
1301
|
.step-title { flex: 1; font-size: 1em; }
|
|
@@ -1541,178 +1307,55 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1541
1307
|
.step-hook { background-color: rgba(33,150,243,0.04); border-left: 3px solid var(--info-color) !important; }
|
|
1542
1308
|
.step-hook .step-title { font-style: italic; color: var(--info-color)}
|
|
1543
1309
|
.nested-steps { margin-top: 12px; }
|
|
1544
|
-
|
|
1545
1310
|
.attachments-section { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--light-gray-color); }
|
|
1546
1311
|
.attachments-section h4 { margin-top: 0; margin-bottom: 20px; font-size: 1.1em; color: var(--text-color); }
|
|
1547
1312
|
.attachments-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 22px; }
|
|
1548
|
-
.attachment-item {
|
|
1549
|
-
border: 1px solid var(--border-color); border-radius: var(--border-radius); background-color: #fff;
|
|
1550
|
-
box-shadow: var(--box-shadow-light); overflow: hidden; display: flex; flex-direction: column;
|
|
1551
|
-
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
|
|
1552
|
-
}
|
|
1313
|
+
.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; }
|
|
1553
1314
|
.attachment-item:hover { transform: translateY(-4px); box-shadow: var(--box-shadow); }
|
|
1554
|
-
.attachment-item img {
|
|
1555
|
-
width: 100%; height: 180px; object-fit: cover; display: block;
|
|
1556
|
-
border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease;
|
|
1557
|
-
}
|
|
1315
|
+
.attachment-item img { width: 100%; height: 180px; object-fit: cover; display: block; border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease; }
|
|
1558
1316
|
.attachment-item a:hover img { opacity: 0.85; }
|
|
1559
|
-
.attachment-caption {
|
|
1560
|
-
padding: 12px 15px; font-size: 0.9em; text-align: center;
|
|
1561
|
-
color: var(--text-color-secondary); word-break: break-word; background-color: var(--light-gray-color);
|
|
1562
|
-
}
|
|
1317
|
+
.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); }
|
|
1563
1318
|
.video-item a, .trace-item a { display: block; margin-bottom: 8px; color: var(--primary-color); text-decoration: none; font-weight: 500; }
|
|
1564
1319
|
.video-item a:hover, .trace-item a:hover { text-decoration: underline; }
|
|
1565
1320
|
.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;}
|
|
1566
|
-
|
|
1567
1321
|
.trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
|
|
1568
|
-
/* Removed D3 specific .chart-axis, .main-chart-title, .chart-line.* rules */
|
|
1569
|
-
/* Highcharts styles its elements with classes like .highcharts-axis, .highcharts-title etc. */
|
|
1570
|
-
|
|
1571
1322
|
.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;}
|
|
1572
1323
|
.test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
|
|
1573
|
-
.test-history-card {
|
|
1574
|
-
background: var(--card-background-color); border: 1px solid var(--border-color); border-radius: var(--border-radius);
|
|
1575
|
-
padding: 22px; box-shadow: var(--box-shadow-light); display: flex; flex-direction: column;
|
|
1576
|
-
}
|
|
1324
|
+
.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; }
|
|
1577
1325
|
.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); }
|
|
1578
|
-
.test-history-header h3 { margin: 0; font-size: 1.15em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1579
|
-
.test-history-header p { font-weight: 500 }
|
|
1326
|
+
.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 */
|
|
1327
|
+
.test-history-header p { font-weight: 500 } /* Added this */
|
|
1580
1328
|
.test-history-trend { margin-bottom: 20px; min-height: 110px; }
|
|
1581
|
-
.test-history-trend div[id^="testHistoryChart-"] {
|
|
1582
|
-
display: block; margin: 0 auto; max-width:100%; height: 100px; width: 320px; /* Match JS config */
|
|
1583
|
-
}
|
|
1584
|
-
/* .test-history-trend .small-axis text {font-size: 11px;} Removed D3 specific */
|
|
1329
|
+
.test-history-trend div[id^="testHistoryChart-"] { display: block; margin: 0 auto; max-width:100%; height: 100px; width: 320px; }
|
|
1585
1330
|
.test-history-details-collapsible summary { cursor: pointer; font-size: 1em; color: var(--primary-color); margin-bottom: 10px; font-weight:500; }
|
|
1586
1331
|
.test-history-details-collapsible summary:hover {text-decoration: underline;}
|
|
1587
1332
|
.test-history-details table { width: 100%; border-collapse: collapse; font-size: 0.95em; }
|
|
1588
1333
|
.test-history-details th, .test-history-details td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--light-gray-color); }
|
|
1589
1334
|
.test-history-details th { background-color: var(--light-gray-color); font-weight: 600; }
|
|
1590
|
-
.status-badge-small {
|
|
1591
|
-
padding: 3px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 600;
|
|
1592
|
-
color: white; text-transform: uppercase; display: inline-block;
|
|
1593
|
-
}
|
|
1335
|
+
.status-badge-small { padding: 3px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase; display: inline-block; }
|
|
1594
1336
|
.status-badge-small.status-passed { background-color: var(--success-color); }
|
|
1595
1337
|
.status-badge-small.status-failed { background-color: var(--danger-color); }
|
|
1596
1338
|
.status-badge-small.status-skipped { background-color: var(--warning-color); }
|
|
1597
1339
|
.status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
|
|
1598
|
-
|
|
1599
|
-
.no-data, .no-tests, .no-steps, .no-data-chart {
|
|
1600
|
-
padding: 28px; text-align: center; color: var(--dark-gray-color); font-style: italic; font-size:1.1em;
|
|
1601
|
-
background-color: var(--light-gray-color); border-radius: var(--border-radius); margin: 18px 0;
|
|
1602
|
-
border: 1px dashed var(--medium-gray-color);
|
|
1603
|
-
}
|
|
1340
|
+
.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); }
|
|
1604
1341
|
.no-data-chart {font-size: 0.95em; padding: 18px;}
|
|
1605
|
-
|
|
1606
1342
|
#test-ai iframe { border: 1px solid var(--border-color); width: 100%; height: 85vh; border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); }
|
|
1607
1343
|
#test-ai p {margin-bottom: 18px; font-size: 1em; color: var(--text-color-secondary);}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
.trace-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
.
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
.trace-name {
|
|
1625
|
-
word-break: break-word;
|
|
1626
|
-
font-size: 0.9rem;
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
.trace-actions {
|
|
1630
|
-
display: flex;
|
|
1631
|
-
gap: 0.5rem;
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
.trace-actions a {
|
|
1635
|
-
flex: 1;
|
|
1636
|
-
text-align: center;
|
|
1637
|
-
padding: 0.25rem 0.5rem;
|
|
1638
|
-
font-size: 0.85rem;
|
|
1639
|
-
border-radius: 4px;
|
|
1640
|
-
text-decoration: none;
|
|
1641
|
-
background: cornflowerblue;
|
|
1642
|
-
color: aliceblue;
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
.view-trace {
|
|
1646
|
-
background: #3182ce;
|
|
1647
|
-
color: white;
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
.view-trace:hover {
|
|
1651
|
-
background: #2c5282;
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
.download-trace {
|
|
1655
|
-
background: #e2e8f0;
|
|
1656
|
-
color: #2d3748;
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
.download-trace:hover {
|
|
1660
|
-
background: #cbd5e0;
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
.filters button.clear-filters-btn {
|
|
1664
|
-
background-color: var(--medium-gray-color); /* Or any other suitable color */
|
|
1665
|
-
color: var(--text-color);
|
|
1666
|
-
/* Add other styling as per your .filters button style if needed */
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
.filters button.clear-filters-btn:hover {
|
|
1670
|
-
background-color: var(--dark-gray-color); /* Darker on hover */
|
|
1671
|
-
color: #fff;
|
|
1672
|
-
}
|
|
1673
|
-
@media (max-width: 1200px) {
|
|
1674
|
-
.trend-charts-row { grid-template-columns: 1fr; }
|
|
1675
|
-
}
|
|
1676
|
-
@media (max-width: 992px) {
|
|
1677
|
-
.dashboard-bottom-row { grid-template-columns: 1fr; }
|
|
1678
|
-
.pie-chart-wrapper div[id^="pieChart-"] { max-width: 350px; margin: 0 auto; }
|
|
1679
|
-
.filters input { min-width: 180px; }
|
|
1680
|
-
.filters select { min-width: 150px; }
|
|
1681
|
-
}
|
|
1682
|
-
@media (max-width: 768px) {
|
|
1683
|
-
body { font-size: 15px; }
|
|
1684
|
-
.container { margin: 10px; padding: 20px; }
|
|
1685
|
-
.header { flex-direction: column; align-items: flex-start; gap: 15px; }
|
|
1686
|
-
.header h1 { font-size: 1.6em; }
|
|
1687
|
-
.run-info { text-align: left; font-size:0.9em; }
|
|
1688
|
-
.tabs { margin-bottom: 25px;}
|
|
1689
|
-
.tab-button { padding: 12px 20px; font-size: 1.05em;}
|
|
1690
|
-
.dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;}
|
|
1691
|
-
.summary-card .value {font-size: 2em;}
|
|
1692
|
-
.summary-card h3 {font-size: 0.95em;}
|
|
1693
|
-
.filters { flex-direction: column; padding: 18px; gap: 12px;}
|
|
1694
|
-
.filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;}
|
|
1695
|
-
.test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; }
|
|
1696
|
-
.test-case-summary {gap: 10px;}
|
|
1697
|
-
.test-case-title {font-size: 1.05em;}
|
|
1698
|
-
.test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;}
|
|
1699
|
-
.attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;}
|
|
1700
|
-
.test-history-grid {grid-template-columns: 1fr;}
|
|
1701
|
-
.pie-chart-wrapper {min-height: auto;}
|
|
1702
|
-
}
|
|
1703
|
-
@media (max-width: 480px) {
|
|
1704
|
-
body {font-size: 14px;}
|
|
1705
|
-
.container {padding: 15px;}
|
|
1706
|
-
.header h1 {font-size: 1.4em;}
|
|
1707
|
-
#report-logo { height: 35px; width: 35px; }
|
|
1708
|
-
.tab-button {padding: 10px 15px; font-size: 1em;}
|
|
1709
|
-
.summary-card .value {font-size: 1.8em;}
|
|
1710
|
-
.attachments-grid {grid-template-columns: 1fr;}
|
|
1711
|
-
.step-item {padding-left: calc(var(--depth, 0) * 18px);}
|
|
1712
|
-
.test-case-content, .step-details {padding: 15px;}
|
|
1713
|
-
.trend-charts-row {gap: 20px;}
|
|
1714
|
-
.trend-chart {padding: 20px;}
|
|
1715
|
-
}
|
|
1344
|
+
.trace-preview { padding: 1rem; text-align: center; background: #f5f5f5; border-bottom: 1px solid #e1e1e1; }
|
|
1345
|
+
.trace-icon { font-size: 2rem; display: block; margin-bottom: 0.5rem; }
|
|
1346
|
+
.trace-name { word-break: break-word; font-size: 0.9rem; }
|
|
1347
|
+
.trace-actions { display: flex; gap: 0.5rem; }
|
|
1348
|
+
.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; }
|
|
1349
|
+
.view-trace { background: #3182ce; color: white; }
|
|
1350
|
+
.view-trace:hover { background: #2c5282; }
|
|
1351
|
+
.download-trace { background: #e2e8f0; color: #2d3748; }
|
|
1352
|
+
.download-trace:hover { background: #cbd5e0; }
|
|
1353
|
+
.filters button.clear-filters-btn { background-color: var(--medium-gray-color); color: var(--text-color); }
|
|
1354
|
+
.filters button.clear-filters-btn:hover { background-color: var(--dark-gray-color); color: #fff; }
|
|
1355
|
+
@media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
|
|
1356
|
+
@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; } }
|
|
1357
|
+
@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;} }
|
|
1358
|
+
@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;} }
|
|
1716
1359
|
</style>
|
|
1717
1360
|
</head>
|
|
1718
1361
|
<body>
|
|
@@ -1722,113 +1365,81 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1722
1365
|
<img id="report-logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMNCA3bDggNSA4LTUtOC01eiIgZmlsbD0iIzNmNTFiNSIvPjxwYXRoIGQ9Ik0xMiA2TDQgMTFsOCA1IDgtNS04LTV6IiBmaWxsPSIjNDI4NWY0Ii8+PHBhdGggZD0iTTEyIDEwbC04IDUgOCA1IDgtNS04LTV6IiBmaWxsPSIjM2Q1NWI0Ii8+PC9zdmc+" alt="Report Logo">
|
|
1723
1366
|
<h1>Playwright Pulse Report</h1>
|
|
1724
1367
|
</div>
|
|
1725
|
-
<div class="run-info">
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
runSummary.duration
|
|
1731
|
-
)}
|
|
1732
|
-
</div>
|
|
1368
|
+
<div class="run-info"><strong>Run Date:</strong> ${formatDate(
|
|
1369
|
+
runSummary.timestamp
|
|
1370
|
+
)}<br><strong>Total Duration:</strong> ${formatDuration(
|
|
1371
|
+
runSummary.duration
|
|
1372
|
+
)}</div>
|
|
1733
1373
|
</header>
|
|
1734
|
-
|
|
1735
1374
|
<div class="tabs">
|
|
1736
1375
|
<button class="tab-button active" data-tab="dashboard">Dashboard</button>
|
|
1737
1376
|
<button class="tab-button" data-tab="test-runs">Test Run Summary</button>
|
|
1738
1377
|
<button class="tab-button" data-tab="test-history">Test History</button>
|
|
1739
1378
|
<button class="tab-button" data-tab="test-ai">AI Analysis</button>
|
|
1740
1379
|
</div>
|
|
1741
|
-
|
|
1742
1380
|
<div id="dashboard" class="tab-content active">
|
|
1743
1381
|
<div class="dashboard-grid">
|
|
1744
|
-
<div class="summary-card">
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
</div>
|
|
1753
|
-
<div class="summary-card status-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
</div>
|
|
1757
|
-
<div class="summary-card
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
}</div>
|
|
1761
|
-
<div class="trend-percentage">${skipPercentage}%</div>
|
|
1762
|
-
</div>
|
|
1763
|
-
<div class="summary-card">
|
|
1764
|
-
<h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div>
|
|
1765
|
-
</div>
|
|
1766
|
-
<div class="summary-card">
|
|
1767
|
-
<h3>Run Duration</h3><div class="value">${formatDuration(
|
|
1768
|
-
runSummary.duration
|
|
1769
|
-
)}</div>
|
|
1770
|
-
</div>
|
|
1382
|
+
<div class="summary-card"><h3>Total Tests</h3><div class="value">${
|
|
1383
|
+
runSummary.totalTests
|
|
1384
|
+
}</div></div>
|
|
1385
|
+
<div class="summary-card status-passed"><h3>Passed</h3><div class="value">${
|
|
1386
|
+
runSummary.passed
|
|
1387
|
+
}</div><div class="trend-percentage">${passPercentage}%</div></div>
|
|
1388
|
+
<div class="summary-card status-failed"><h3>Failed</h3><div class="value">${
|
|
1389
|
+
runSummary.failed
|
|
1390
|
+
}</div><div class="trend-percentage">${failPercentage}%</div></div>
|
|
1391
|
+
<div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
|
|
1392
|
+
runSummary.skipped || 0
|
|
1393
|
+
}</div><div class="trend-percentage">${skipPercentage}%</div></div>
|
|
1394
|
+
<div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
|
|
1395
|
+
<div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
|
|
1396
|
+
runSummary.duration
|
|
1397
|
+
)}</div></div>
|
|
1771
1398
|
</div>
|
|
1772
1399
|
<div class="dashboard-bottom-row">
|
|
1773
1400
|
${generatePieChart(
|
|
1774
|
-
// Changed from generatePieChartD3
|
|
1775
1401
|
[
|
|
1776
1402
|
{ label: "Passed", value: runSummary.passed },
|
|
1777
1403
|
{ label: "Failed", value: runSummary.failed },
|
|
1778
1404
|
{ label: "Skipped", value: runSummary.skipped || 0 },
|
|
1779
1405
|
],
|
|
1780
|
-
400,
|
|
1781
|
-
390
|
|
1406
|
+
400,
|
|
1407
|
+
390
|
|
1782
1408
|
)}
|
|
1783
1409
|
${generateSuitesWidget(suitesData)}
|
|
1784
1410
|
</div>
|
|
1785
1411
|
</div>
|
|
1786
|
-
|
|
1787
1412
|
<div id="test-runs" class="tab-content">
|
|
1788
1413
|
<div class="filters">
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
`<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
|
|
1805
|
-
browser
|
|
1806
|
-
)}</option>`
|
|
1807
|
-
)
|
|
1808
|
-
.join("")}
|
|
1809
|
-
</select>
|
|
1810
|
-
<button id="expand-all-tests">Expand All</button>
|
|
1811
|
-
<button id="collapse-all-tests">Collapse All</button>
|
|
1812
|
-
<button id="clear-run-summary-filters" class="clear-filters-btn">Clear Filters</button>
|
|
1813
|
-
</div>
|
|
1814
|
-
<div class="test-cases-list">
|
|
1815
|
-
${generateTestCasesHTML()}
|
|
1414
|
+
<input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
|
|
1415
|
+
<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>
|
|
1416
|
+
<select id="filter-browser"><option value="">All Browsers</option>${Array.from(
|
|
1417
|
+
new Set(
|
|
1418
|
+
(results || []).map((test) => test.browser || "unknown")
|
|
1419
|
+
)
|
|
1420
|
+
)
|
|
1421
|
+
.map(
|
|
1422
|
+
(browser) =>
|
|
1423
|
+
`<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
|
|
1424
|
+
browser
|
|
1425
|
+
)}</option>`
|
|
1426
|
+
)
|
|
1427
|
+
.join("")}</select>
|
|
1428
|
+
<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>
|
|
1816
1429
|
</div>
|
|
1430
|
+
<div class="test-cases-list">${generateTestCasesHTML()}</div>
|
|
1817
1431
|
</div>
|
|
1818
|
-
|
|
1819
1432
|
<div id="test-history" class="tab-content">
|
|
1820
1433
|
<h2 class="tab-main-title">Execution Trends</h2>
|
|
1821
1434
|
<div class="trend-charts-row">
|
|
1822
|
-
<div class="trend-chart">
|
|
1823
|
-
<h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
|
|
1435
|
+
<div class="trend-chart"><h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
|
|
1824
1436
|
${
|
|
1825
1437
|
trendData && trendData.overall && trendData.overall.length > 0
|
|
1826
1438
|
? generateTestTrendsChart(trendData)
|
|
1827
1439
|
: '<div class="no-data">Overall trend data not available for test counts.</div>'
|
|
1828
1440
|
}
|
|
1829
1441
|
</div>
|
|
1830
|
-
<div class="trend-chart">
|
|
1831
|
-
<h3 class="chart-title-header">Execution Duration Trends</h3>
|
|
1442
|
+
<div class="trend-chart"><h3 class="chart-title-header">Execution Duration Trends</h3>
|
|
1832
1443
|
${
|
|
1833
1444
|
trendData && trendData.overall && trendData.overall.length > 0
|
|
1834
1445
|
? generateDurationTrendChart(trendData)
|
|
@@ -1845,69 +1456,26 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1845
1456
|
: '<div class="no-data">Individual test history data not available.</div>'
|
|
1846
1457
|
}
|
|
1847
1458
|
</div>
|
|
1848
|
-
|
|
1849
1459
|
<div id="test-ai" class="tab-content">
|
|
1850
|
-
<iframe
|
|
1851
|
-
src="https://ai-test-analyser.netlify.app/"
|
|
1852
|
-
width="100%"
|
|
1853
|
-
height="100%"
|
|
1854
|
-
frameborder="0"
|
|
1855
|
-
allowfullscreen
|
|
1856
|
-
style="border: none; height: 100vh;">
|
|
1857
|
-
</iframe>
|
|
1460
|
+
<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>
|
|
1858
1461
|
</div>
|
|
1859
|
-
<footer style="
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
align-items: center;
|
|
1868
|
-
gap: 0.5rem;
|
|
1869
|
-
color: #333;
|
|
1870
|
-
font-size: 0.9rem;
|
|
1871
|
-
font-weight: 600;
|
|
1872
|
-
letter-spacing: 0.5px;
|
|
1873
|
-
">
|
|
1874
|
-
<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"/>
|
|
1875
|
-
<span>Created by</span>
|
|
1876
|
-
<a href="https://github.com/Arghajit47"
|
|
1877
|
-
target="_blank"
|
|
1878
|
-
rel="noopener noreferrer"
|
|
1879
|
-
style="
|
|
1880
|
-
color: #7737BF;
|
|
1881
|
-
font-weight: 700;
|
|
1882
|
-
font-style: italic;
|
|
1883
|
-
text-decoration: none;
|
|
1884
|
-
transition: all 0.2s ease;
|
|
1885
|
-
"
|
|
1886
|
-
onmouseover="this.style.color='#BF5C37'"
|
|
1887
|
-
onmouseout="this.style.color='#7737BF'">
|
|
1888
|
-
Arghajit Singha
|
|
1889
|
-
</a>
|
|
1890
|
-
</div>
|
|
1891
|
-
<div style="
|
|
1892
|
-
margin-top: 0.5rem;
|
|
1893
|
-
font-size: 0.75rem;
|
|
1894
|
-
color: #666;
|
|
1895
|
-
">
|
|
1896
|
-
Crafted with precision
|
|
1897
|
-
</div>
|
|
1898
|
-
</footer>
|
|
1462
|
+
<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;">
|
|
1463
|
+
<div style="display: inline-flex; align-items: center; gap: 0.5rem; color: #333; font-size: 0.9rem; font-weight: 600; letter-spacing: 0.5px;">
|
|
1464
|
+
<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"/>
|
|
1465
|
+
<span>Created by</span>
|
|
1466
|
+
<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>
|
|
1467
|
+
</div>
|
|
1468
|
+
<div style="margin-top: 0.5rem; font-size: 0.75rem; color: #666;">Crafted with precision</div>
|
|
1469
|
+
</footer>
|
|
1899
1470
|
</div>
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
1471
|
<script>
|
|
1903
1472
|
// Ensure formatDuration is globally available
|
|
1904
|
-
if (typeof formatDuration === 'undefined') {
|
|
1905
|
-
function formatDuration(ms) {
|
|
1906
|
-
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
1907
|
-
return (ms / 1000).toFixed(1) + "s";
|
|
1473
|
+
if (typeof formatDuration === 'undefined') {
|
|
1474
|
+
function formatDuration(ms) {
|
|
1475
|
+
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
1476
|
+
return (ms / 1000).toFixed(1) + "s";
|
|
1908
1477
|
}
|
|
1909
1478
|
}
|
|
1910
|
-
|
|
1911
1479
|
function initializeReportInteractivity() {
|
|
1912
1480
|
const tabButtons = document.querySelectorAll('.tab-button');
|
|
1913
1481
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
@@ -1918,82 +1486,64 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1918
1486
|
button.classList.add('active');
|
|
1919
1487
|
const tabId = button.getAttribute('data-tab');
|
|
1920
1488
|
const activeContent = document.getElementById(tabId);
|
|
1921
|
-
if (activeContent)
|
|
1489
|
+
if (activeContent) {
|
|
1490
|
+
activeContent.classList.add('active');
|
|
1491
|
+
// Check if IntersectionObserver is already handling elements in this tab
|
|
1492
|
+
// For simplicity, we assume if an element is observed, it will be handled when it becomes visible.
|
|
1493
|
+
// If IntersectionObserver is not supported, already-visible elements would have been loaded by fallback.
|
|
1494
|
+
}
|
|
1922
1495
|
});
|
|
1923
1496
|
});
|
|
1924
|
-
|
|
1925
1497
|
// --- Test Run Summary Filters ---
|
|
1926
1498
|
const nameFilter = document.getElementById('filter-name');
|
|
1927
1499
|
const statusFilter = document.getElementById('filter-status');
|
|
1928
1500
|
const browserFilter = document.getElementById('filter-browser');
|
|
1929
|
-
const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters');
|
|
1930
|
-
|
|
1931
|
-
function filterTestCases() {
|
|
1501
|
+
const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters');
|
|
1502
|
+
function filterTestCases() {
|
|
1932
1503
|
const nameValue = nameFilter ? nameFilter.value.toLowerCase() : "";
|
|
1933
1504
|
const statusValue = statusFilter ? statusFilter.value : "";
|
|
1934
1505
|
const browserValue = browserFilter ? browserFilter.value : "";
|
|
1935
|
-
|
|
1936
1506
|
document.querySelectorAll('#test-runs .test-case').forEach(testCaseElement => {
|
|
1937
1507
|
const titleElement = testCaseElement.querySelector('.test-case-title');
|
|
1938
1508
|
const fullTestName = titleElement ? titleElement.getAttribute('title').toLowerCase() : "";
|
|
1939
1509
|
const status = testCaseElement.getAttribute('data-status');
|
|
1940
1510
|
const browser = testCaseElement.getAttribute('data-browser');
|
|
1941
|
-
|
|
1942
1511
|
const nameMatch = fullTestName.includes(nameValue);
|
|
1943
1512
|
const statusMatch = !statusValue || status === statusValue;
|
|
1944
1513
|
const browserMatch = !browserValue || browser === browserValue;
|
|
1945
|
-
|
|
1946
1514
|
testCaseElement.style.display = (nameMatch && statusMatch && browserMatch) ? '' : 'none';
|
|
1947
1515
|
});
|
|
1948
1516
|
}
|
|
1949
1517
|
if(nameFilter) nameFilter.addEventListener('input', filterTestCases);
|
|
1950
1518
|
if(statusFilter) statusFilter.addEventListener('change', filterTestCases);
|
|
1951
1519
|
if(browserFilter) browserFilter.addEventListener('change', filterTestCases);
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
if (nameFilter) nameFilter.value = '';
|
|
1957
|
-
if (statusFilter) statusFilter.value = '';
|
|
1958
|
-
if (browserFilter) browserFilter.value = '';
|
|
1959
|
-
filterTestCases(); // Re-apply filters (which will show all)
|
|
1960
|
-
});
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1520
|
+
if(clearRunSummaryFiltersBtn) clearRunSummaryFiltersBtn.addEventListener('click', () => {
|
|
1521
|
+
if(nameFilter) nameFilter.value = ''; if(statusFilter) statusFilter.value = ''; if(browserFilter) browserFilter.value = '';
|
|
1522
|
+
filterTestCases();
|
|
1523
|
+
});
|
|
1963
1524
|
// --- Test History Filters ---
|
|
1964
1525
|
const historyNameFilter = document.getElementById('history-filter-name');
|
|
1965
1526
|
const historyStatusFilter = document.getElementById('history-filter-status');
|
|
1966
|
-
const clearHistoryFiltersBtn = document.getElementById('clear-history-filters');
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
function filterTestHistoryCards() {
|
|
1527
|
+
const clearHistoryFiltersBtn = document.getElementById('clear-history-filters');
|
|
1528
|
+
function filterTestHistoryCards() {
|
|
1970
1529
|
const nameValue = historyNameFilter ? historyNameFilter.value.toLowerCase() : "";
|
|
1971
1530
|
const statusValue = historyStatusFilter ? historyStatusFilter.value : "";
|
|
1972
|
-
|
|
1973
1531
|
document.querySelectorAll('.test-history-card').forEach(card => {
|
|
1974
1532
|
const testTitle = card.getAttribute('data-test-name').toLowerCase();
|
|
1975
1533
|
const latestStatus = card.getAttribute('data-latest-status');
|
|
1976
|
-
|
|
1977
1534
|
const nameMatch = testTitle.includes(nameValue);
|
|
1978
1535
|
const statusMatch = !statusValue || latestStatus === statusValue;
|
|
1979
|
-
|
|
1980
1536
|
card.style.display = (nameMatch && statusMatch) ? '' : 'none';
|
|
1981
1537
|
});
|
|
1982
1538
|
}
|
|
1983
1539
|
if(historyNameFilter) historyNameFilter.addEventListener('input', filterTestHistoryCards);
|
|
1984
1540
|
if(historyStatusFilter) historyStatusFilter.addEventListener('change', filterTestHistoryCards);
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
filterTestHistoryCards(); // Re-apply filters (which will show all)
|
|
1992
|
-
});
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
// --- Expand/Collapse and Toggle Details Logic (remains the same) ---
|
|
1996
|
-
function toggleElementDetails(headerElement, contentSelector) {
|
|
1541
|
+
if(clearHistoryFiltersBtn) clearHistoryFiltersBtn.addEventListener('click', () => {
|
|
1542
|
+
if(historyNameFilter) historyNameFilter.value = ''; if(historyStatusFilter) historyStatusFilter.value = '';
|
|
1543
|
+
filterTestHistoryCards();
|
|
1544
|
+
});
|
|
1545
|
+
// --- Expand/Collapse and Toggle Details Logic ---
|
|
1546
|
+
function toggleElementDetails(headerElement, contentSelector) {
|
|
1997
1547
|
let contentElement;
|
|
1998
1548
|
if (headerElement.classList.contains('test-case-header')) {
|
|
1999
1549
|
contentElement = headerElement.parentElement.querySelector('.test-case-content');
|
|
@@ -2003,41 +1553,114 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2003
1553
|
contentElement = null;
|
|
2004
1554
|
}
|
|
2005
1555
|
}
|
|
2006
|
-
|
|
2007
1556
|
if (contentElement) {
|
|
2008
1557
|
const isExpanded = contentElement.style.display === 'block';
|
|
2009
1558
|
contentElement.style.display = isExpanded ? 'none' : 'block';
|
|
2010
1559
|
headerElement.setAttribute('aria-expanded', String(!isExpanded));
|
|
2011
1560
|
}
|
|
2012
1561
|
}
|
|
2013
|
-
|
|
2014
1562
|
document.querySelectorAll('#test-runs .test-case-header').forEach(header => {
|
|
2015
1563
|
header.addEventListener('click', () => toggleElementDetails(header));
|
|
2016
1564
|
});
|
|
2017
1565
|
document.querySelectorAll('#test-runs .step-header').forEach(header => {
|
|
2018
1566
|
header.addEventListener('click', () => toggleElementDetails(header, '.step-details'));
|
|
2019
1567
|
});
|
|
2020
|
-
|
|
2021
1568
|
const expandAllBtn = document.getElementById('expand-all-tests');
|
|
2022
1569
|
const collapseAllBtn = document.getElementById('collapse-all-tests');
|
|
2023
|
-
|
|
2024
1570
|
function setAllTestRunDetailsVisibility(displayMode, ariaState) {
|
|
2025
1571
|
document.querySelectorAll('#test-runs .test-case-content').forEach(el => el.style.display = displayMode);
|
|
2026
1572
|
document.querySelectorAll('#test-runs .step-details').forEach(el => el.style.display = displayMode);
|
|
2027
1573
|
document.querySelectorAll('#test-runs .test-case-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
|
|
2028
1574
|
document.querySelectorAll('#test-runs .step-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
|
|
2029
1575
|
}
|
|
2030
|
-
|
|
2031
1576
|
if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
|
|
2032
1577
|
if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
|
|
1578
|
+
// --- Intersection Observer for Lazy Loading ---
|
|
1579
|
+
const lazyLoadElements = document.querySelectorAll('.lazy-load-chart, .lazy-load-iframe');
|
|
1580
|
+
if ('IntersectionObserver' in window) {
|
|
1581
|
+
let lazyObserver = new IntersectionObserver((entries, observer) => {
|
|
1582
|
+
entries.forEach(entry => {
|
|
1583
|
+
if (entry.isIntersecting) {
|
|
1584
|
+
const element = entry.target;
|
|
1585
|
+
if (element.classList.contains('lazy-load-iframe')) {
|
|
1586
|
+
if (element.dataset.src) {
|
|
1587
|
+
element.src = element.dataset.src;
|
|
1588
|
+
element.removeAttribute('data-src'); // Optional: remove data-src after loading
|
|
1589
|
+
console.log('Lazy loaded iframe:', element.title || 'Untitled Iframe');
|
|
1590
|
+
}
|
|
1591
|
+
} else if (element.classList.contains('lazy-load-chart')) {
|
|
1592
|
+
const renderFunctionName = element.dataset.renderFunctionName;
|
|
1593
|
+
if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
|
|
1594
|
+
try {
|
|
1595
|
+
console.log('Lazy loading chart with function:', renderFunctionName);
|
|
1596
|
+
window[renderFunctionName](); // Call the render function
|
|
1597
|
+
} catch (e) {
|
|
1598
|
+
console.error(\`Error lazy-loading chart \${element.id} using \${renderFunctionName}:\`, e);
|
|
1599
|
+
element.innerHTML = '<div class="no-data-chart">Error lazy-loading chart.</div>';
|
|
1600
|
+
}
|
|
1601
|
+
} else {
|
|
1602
|
+
console.warn(\`Render function \${renderFunctionName} not found or not a function for chart:\`, element.id);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
observer.unobserve(element); // Important: stop observing once loaded
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
}, {
|
|
1609
|
+
rootMargin: "0px 0px 200px 0px" // Start loading when element is 200px from viewport bottom
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
lazyLoadElements.forEach(el => {
|
|
1613
|
+
lazyObserver.observe(el);
|
|
1614
|
+
});
|
|
1615
|
+
} else { // Fallback for browsers without IntersectionObserver
|
|
1616
|
+
console.warn("IntersectionObserver not supported. Loading all items immediately.");
|
|
1617
|
+
lazyLoadElements.forEach(element => {
|
|
1618
|
+
if (element.classList.contains('lazy-load-iframe')) {
|
|
1619
|
+
if (element.dataset.src) {
|
|
1620
|
+
element.src = element.dataset.src;
|
|
1621
|
+
element.removeAttribute('data-src');
|
|
1622
|
+
}
|
|
1623
|
+
} else if (element.classList.contains('lazy-load-chart')) {
|
|
1624
|
+
const renderFunctionName = element.dataset.renderFunctionName;
|
|
1625
|
+
if (renderFunctionName && typeof window[renderFunctionName] === 'function') {
|
|
1626
|
+
try {
|
|
1627
|
+
window[renderFunctionName]();
|
|
1628
|
+
} catch (e) {
|
|
1629
|
+
console.error(\`Error loading chart (fallback) \${element.id} using \${renderFunctionName}:\`, e);
|
|
1630
|
+
element.innerHTML = '<div class="no-data-chart">Error loading chart (fallback).</div>';
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
2033
1636
|
}
|
|
2034
1637
|
document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
|
|
1638
|
+
|
|
1639
|
+
function copyErrorToClipboard(button) {
|
|
1640
|
+
const errorContainer = button.closest('.step-error');
|
|
1641
|
+
const errorText = errorContainer.querySelector('.stack-trace').textContent;
|
|
1642
|
+
const textarea = document.createElement('textarea');
|
|
1643
|
+
textarea.value = errorText;
|
|
1644
|
+
document.body.appendChild(textarea);
|
|
1645
|
+
textarea.select();
|
|
1646
|
+
try {
|
|
1647
|
+
const successful = document.execCommand('copy');
|
|
1648
|
+
const originalText = button.textContent;
|
|
1649
|
+
button.textContent = successful ? 'Copied!' : 'Failed to copy';
|
|
1650
|
+
setTimeout(() => {
|
|
1651
|
+
button.textContent = originalText;
|
|
1652
|
+
}, 2000);
|
|
1653
|
+
} catch (err) {
|
|
1654
|
+
console.error('Failed to copy: ', err);
|
|
1655
|
+
button.textContent = 'Failed to copy';
|
|
1656
|
+
}
|
|
1657
|
+
document.body.removeChild(textarea);
|
|
1658
|
+
}
|
|
2035
1659
|
</script>
|
|
2036
1660
|
</body>
|
|
2037
1661
|
</html>
|
|
2038
1662
|
`;
|
|
2039
1663
|
}
|
|
2040
|
-
|
|
2041
1664
|
async function runScript(scriptPath) {
|
|
2042
1665
|
return new Promise((resolve, reject) => {
|
|
2043
1666
|
console.log(chalk.blue(`Executing script: ${scriptPath}...`));
|
|
@@ -2062,7 +1685,6 @@ async function runScript(scriptPath) {
|
|
|
2062
1685
|
});
|
|
2063
1686
|
});
|
|
2064
1687
|
}
|
|
2065
|
-
|
|
2066
1688
|
async function main() {
|
|
2067
1689
|
const __filename = fileURLToPath(import.meta.url);
|
|
2068
1690
|
const __dirname = path.dirname(__filename);
|
|
@@ -2097,11 +1719,10 @@ async function main() {
|
|
|
2097
1719
|
),
|
|
2098
1720
|
error
|
|
2099
1721
|
);
|
|
2100
|
-
// You might decide to proceed or exit depending on the importance of historical data
|
|
2101
1722
|
}
|
|
2102
1723
|
|
|
2103
1724
|
// Step 2: Load current run's data (for non-trend sections of the report)
|
|
2104
|
-
let currentRunReportData;
|
|
1725
|
+
let currentRunReportData;
|
|
2105
1726
|
try {
|
|
2106
1727
|
const jsonData = await fs.readFile(reportJsonPath, "utf-8");
|
|
2107
1728
|
currentRunReportData = JSON.parse(jsonData);
|
|
@@ -2128,13 +1749,13 @@ async function main() {
|
|
|
2128
1749
|
`Critical Error: Could not read or parse main report JSON at ${reportJsonPath}: ${error.message}`
|
|
2129
1750
|
)
|
|
2130
1751
|
);
|
|
2131
|
-
process.exit(1);
|
|
1752
|
+
process.exit(1);
|
|
2132
1753
|
}
|
|
2133
1754
|
|
|
2134
1755
|
// Step 3: Load historical data for trends
|
|
2135
|
-
let historicalRuns = [];
|
|
1756
|
+
let historicalRuns = [];
|
|
2136
1757
|
try {
|
|
2137
|
-
await fs.access(historyDir);
|
|
1758
|
+
await fs.access(historyDir);
|
|
2138
1759
|
const allHistoryFiles = await fs.readdir(historyDir);
|
|
2139
1760
|
|
|
2140
1761
|
const jsonHistoryFiles = allHistoryFiles
|
|
@@ -2152,7 +1773,7 @@ async function main() {
|
|
|
2152
1773
|
};
|
|
2153
1774
|
})
|
|
2154
1775
|
.filter((file) => !isNaN(file.timestamp))
|
|
2155
|
-
.sort((a, b) => b.timestamp - a.timestamp);
|
|
1776
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
2156
1777
|
|
|
2157
1778
|
const filesToLoadForTrend = jsonHistoryFiles.slice(
|
|
2158
1779
|
0,
|
|
@@ -2162,7 +1783,7 @@ async function main() {
|
|
|
2162
1783
|
for (const fileMeta of filesToLoadForTrend) {
|
|
2163
1784
|
try {
|
|
2164
1785
|
const fileContent = await fs.readFile(fileMeta.path, "utf-8");
|
|
2165
|
-
const runJsonData = JSON.parse(fileContent);
|
|
1786
|
+
const runJsonData = JSON.parse(fileContent);
|
|
2166
1787
|
historicalRuns.push(runJsonData);
|
|
2167
1788
|
} catch (fileReadError) {
|
|
2168
1789
|
console.warn(
|
|
@@ -2172,8 +1793,7 @@ async function main() {
|
|
|
2172
1793
|
);
|
|
2173
1794
|
}
|
|
2174
1795
|
}
|
|
2175
|
-
//
|
|
2176
|
-
historicalRuns.reverse();
|
|
1796
|
+
historicalRuns.reverse(); // Oldest first for charts
|
|
2177
1797
|
console.log(
|
|
2178
1798
|
chalk.green(
|
|
2179
1799
|
`Loaded ${historicalRuns.length} historical run(s) for trend analysis.`
|
|
@@ -2195,20 +1815,18 @@ async function main() {
|
|
|
2195
1815
|
}
|
|
2196
1816
|
}
|
|
2197
1817
|
|
|
2198
|
-
// Step 4: Prepare trendData object
|
|
1818
|
+
// Step 4: Prepare trendData object
|
|
2199
1819
|
const trendData = {
|
|
2200
|
-
overall: [],
|
|
2201
|
-
testRuns: {},
|
|
1820
|
+
overall: [],
|
|
1821
|
+
testRuns: {},
|
|
2202
1822
|
};
|
|
2203
1823
|
|
|
2204
1824
|
if (historicalRuns.length > 0) {
|
|
2205
1825
|
historicalRuns.forEach((histRunReport) => {
|
|
2206
|
-
// histRunReport is a full PlaywrightPulseReport object from a past run
|
|
2207
1826
|
if (histRunReport.run) {
|
|
2208
|
-
// Ensure timestamp is a Date object for correct sorting/comparison later if needed by charts
|
|
2209
1827
|
const runTimestamp = new Date(histRunReport.run.timestamp);
|
|
2210
1828
|
trendData.overall.push({
|
|
2211
|
-
runId: runTimestamp.getTime(),
|
|
1829
|
+
runId: runTimestamp.getTime(),
|
|
2212
1830
|
timestamp: runTimestamp,
|
|
2213
1831
|
duration: histRunReport.run.duration,
|
|
2214
1832
|
totalTests: histRunReport.run.totalTests,
|
|
@@ -2217,21 +1835,19 @@ async function main() {
|
|
|
2217
1835
|
skipped: histRunReport.run.skipped || 0,
|
|
2218
1836
|
});
|
|
2219
1837
|
|
|
2220
|
-
// For generateTestHistoryContent
|
|
2221
1838
|
if (histRunReport.results && Array.isArray(histRunReport.results)) {
|
|
2222
|
-
const runKeyForTestHistory = `test run ${runTimestamp.getTime()}`;
|
|
1839
|
+
const runKeyForTestHistory = `test run ${runTimestamp.getTime()}`;
|
|
2223
1840
|
trendData.testRuns[runKeyForTestHistory] = histRunReport.results.map(
|
|
2224
1841
|
(test) => ({
|
|
2225
|
-
testName: test.name,
|
|
1842
|
+
testName: test.name,
|
|
2226
1843
|
duration: test.duration,
|
|
2227
1844
|
status: test.status,
|
|
2228
|
-
timestamp: new Date(test.startTime),
|
|
1845
|
+
timestamp: new Date(test.startTime),
|
|
2229
1846
|
})
|
|
2230
1847
|
);
|
|
2231
1848
|
}
|
|
2232
1849
|
}
|
|
2233
1850
|
});
|
|
2234
|
-
// Ensure trendData.overall is sorted by timestamp if not already
|
|
2235
1851
|
trendData.overall.sort(
|
|
2236
1852
|
(a, b) => a.timestamp.getTime() - b.timestamp.getTime()
|
|
2237
1853
|
);
|
|
@@ -2239,8 +1855,6 @@ async function main() {
|
|
|
2239
1855
|
|
|
2240
1856
|
// Step 5: Generate and write HTML
|
|
2241
1857
|
try {
|
|
2242
|
-
// currentRunReportData is for the main content (test list, summary cards of *this* run)
|
|
2243
|
-
// trendData is for the historical charts and test history section
|
|
2244
1858
|
const htmlContent = generateHTML(currentRunReportData, trendData);
|
|
2245
1859
|
await fs.writeFile(reportHtmlPath, htmlContent, "utf-8");
|
|
2246
1860
|
console.log(
|
|
@@ -2251,12 +1865,10 @@ async function main() {
|
|
|
2251
1865
|
console.log(chalk.gray(`(You can open this file in your browser)`));
|
|
2252
1866
|
} catch (error) {
|
|
2253
1867
|
console.error(chalk.red(`Error generating HTML report: ${error.message}`));
|
|
2254
|
-
console.error(chalk.red(error.stack));
|
|
1868
|
+
console.error(chalk.red(error.stack));
|
|
2255
1869
|
process.exit(1);
|
|
2256
1870
|
}
|
|
2257
1871
|
}
|
|
2258
|
-
|
|
2259
|
-
// Make sure main() is called at the end of your script
|
|
2260
1872
|
main().catch((err) => {
|
|
2261
1873
|
console.error(
|
|
2262
1874
|
chalk.red.bold(`Unhandled error during script execution: ${err.message}`)
|