@arghajit/playwright-pulse-report 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +192 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +26 -0
- package/dist/lib/report-types.d.ts +8 -0
- package/dist/lib/report-types.js +2 -0
- package/dist/playwright-pulse-reporter.d.ts +26 -0
- package/dist/playwright-pulse-reporter.js +304 -0
- package/dist/reporter/attachment-utils.d.ts +10 -0
- package/dist/reporter/attachment-utils.js +192 -0
- package/dist/reporter/index.d.ts +5 -0
- package/dist/reporter/index.js +9 -0
- package/dist/reporter/lib/report-types.d.ts +8 -0
- package/dist/reporter/lib/report-types.js +2 -0
- package/dist/reporter/playwright-pulse-reporter.d.ts +27 -0
- package/dist/reporter/playwright-pulse-reporter.js +419 -0
- package/dist/reporter/reporter/playwright-pulse-reporter.d.ts +1 -0
- package/dist/reporter/reporter/playwright-pulse-reporter.js +380 -0
- package/dist/reporter/tsconfig.reporter.tsbuildinfo +1 -0
- package/dist/reporter/types/index.d.ts +52 -0
- package/dist/reporter/types/index.js +2 -0
- package/dist/types/index.d.ts +61 -0
- package/dist/types/index.js +2 -0
- package/package.json +107 -0
- package/scripts/generate-static-report.mjs +1539 -0
|
@@ -0,0 +1,1539 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Using Node.js syntax compatible with `.mjs`
|
|
3
|
+
import * as fs from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import * as d3 from "d3";
|
|
6
|
+
import { JSDOM } from "jsdom";
|
|
7
|
+
// Use dynamic import for chalk as it's ESM only
|
|
8
|
+
let chalk;
|
|
9
|
+
try {
|
|
10
|
+
chalk = (await import("chalk")).default;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
console.warn("Chalk could not be imported. Using plain console logs.");
|
|
13
|
+
chalk = {
|
|
14
|
+
green: (text) => text,
|
|
15
|
+
red: (text) => text,
|
|
16
|
+
yellow: (text) => text,
|
|
17
|
+
blue: (text) => text,
|
|
18
|
+
bold: (text) => text,
|
|
19
|
+
gray: (text) => text,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Default configuration
|
|
24
|
+
const DEFAULT_OUTPUT_DIR = "pulse-report-output";
|
|
25
|
+
const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
|
|
26
|
+
const DEFAULT_HTML_FILE = "playwright-pulse-static-report.html";
|
|
27
|
+
|
|
28
|
+
// Helper functions
|
|
29
|
+
function sanitizeHTML(str) {
|
|
30
|
+
if (str === null || str === undefined) return "";
|
|
31
|
+
return String(str)
|
|
32
|
+
.replace(/&/g, "&")
|
|
33
|
+
.replace(/</g, "<")
|
|
34
|
+
.replace(/>/g, ">")
|
|
35
|
+
.replace(/"/g, """)
|
|
36
|
+
.replace(/'/g, "'");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatDuration(ms) {
|
|
40
|
+
if (ms === undefined || ms === null || ms < 0) return "0.0s";
|
|
41
|
+
return (ms / 1000).toFixed(1) + "s";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatDate(dateStrOrDate) {
|
|
45
|
+
if (!dateStrOrDate) return "N/A";
|
|
46
|
+
try {
|
|
47
|
+
const date = new Date(dateStrOrDate);
|
|
48
|
+
if (isNaN(date.getTime())) return "Invalid Date";
|
|
49
|
+
return date.toLocaleString();
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return "Invalid Date";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getStatusClass(status) {
|
|
56
|
+
switch (status) {
|
|
57
|
+
case "passed":
|
|
58
|
+
return "status-passed";
|
|
59
|
+
case "failed":
|
|
60
|
+
return "status-failed";
|
|
61
|
+
case "skipped":
|
|
62
|
+
return "status-skipped";
|
|
63
|
+
default:
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getStatusIcon(status) {
|
|
69
|
+
switch (status) {
|
|
70
|
+
case "passed":
|
|
71
|
+
return "✅";
|
|
72
|
+
case "failed":
|
|
73
|
+
return "❌";
|
|
74
|
+
case "skipped":
|
|
75
|
+
return "⏭️";
|
|
76
|
+
default:
|
|
77
|
+
return "❓";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function generatePieChartD3(data, width = 300, height = 300) {
|
|
82
|
+
const { document } = new JSDOM().window;
|
|
83
|
+
const body = d3.select(document.body);
|
|
84
|
+
|
|
85
|
+
// Calculate passed percentage
|
|
86
|
+
const total = data.reduce((sum, d) => sum + d.value, 0);
|
|
87
|
+
const passedPercentage =
|
|
88
|
+
total > 0
|
|
89
|
+
? Math.round(
|
|
90
|
+
((data.find((d) => d.label === "Passed")?.value || 0) / total) * 100
|
|
91
|
+
)
|
|
92
|
+
: 0;
|
|
93
|
+
|
|
94
|
+
// Chart dimensions
|
|
95
|
+
const radius = Math.min(width, height) / 2 - 50; // Reduced radius for legend space
|
|
96
|
+
const legendRectSize = 15;
|
|
97
|
+
const legendSpacing = 8;
|
|
98
|
+
|
|
99
|
+
// Pie generator
|
|
100
|
+
const pie = d3
|
|
101
|
+
.pie()
|
|
102
|
+
.value((d) => d.value)
|
|
103
|
+
.sort(null);
|
|
104
|
+
const arc = d3.arc().innerRadius(0).outerRadius(radius);
|
|
105
|
+
|
|
106
|
+
// Colors
|
|
107
|
+
const color = d3
|
|
108
|
+
.scaleOrdinal()
|
|
109
|
+
.domain(data.map((d) => d.label))
|
|
110
|
+
.range(["#4CAF50", "#F44336", "#FFC107"]);
|
|
111
|
+
|
|
112
|
+
// Create SVG with more width for legend
|
|
113
|
+
const svg = body
|
|
114
|
+
.append("svg")
|
|
115
|
+
.attr("width", width + 100) // Extra width for legend
|
|
116
|
+
.attr("height", height)
|
|
117
|
+
.append("g")
|
|
118
|
+
.attr("transform", `translate(${width / 2},${height / 2})`);
|
|
119
|
+
|
|
120
|
+
// Tooltip setup
|
|
121
|
+
const tooltip = body
|
|
122
|
+
.append("div")
|
|
123
|
+
.style("opacity", 0)
|
|
124
|
+
.style("position", "absolute")
|
|
125
|
+
.style("background", "white")
|
|
126
|
+
.style("padding", "5px 10px")
|
|
127
|
+
.style("border-radius", "4px")
|
|
128
|
+
.style("box-shadow", "0 2px 5px rgba(0,0,0,0.1)");
|
|
129
|
+
|
|
130
|
+
// Draw pie slices
|
|
131
|
+
const arcs = svg
|
|
132
|
+
.selectAll(".arc")
|
|
133
|
+
.data(pie(data))
|
|
134
|
+
.enter()
|
|
135
|
+
.append("g")
|
|
136
|
+
.attr("class", "arc");
|
|
137
|
+
|
|
138
|
+
arcs
|
|
139
|
+
.append("path")
|
|
140
|
+
.attr("d", arc)
|
|
141
|
+
.attr("fill", (d) => color(d.data.label))
|
|
142
|
+
.style("stroke", "#fff")
|
|
143
|
+
.style("stroke-width", 2)
|
|
144
|
+
.on("mouseover", function (event, d) {
|
|
145
|
+
tooltip.transition().style("opacity", 1);
|
|
146
|
+
tooltip
|
|
147
|
+
.html(
|
|
148
|
+
`${d.data.label}: ${d.data.value} (${Math.round(
|
|
149
|
+
(d.data.value / total) * 100
|
|
150
|
+
)}%)`
|
|
151
|
+
)
|
|
152
|
+
.style("left", event.pageX + 10 + "px")
|
|
153
|
+
.style("top", event.pageY - 28 + "px");
|
|
154
|
+
})
|
|
155
|
+
.on("mouseout", () => tooltip.transition().style("opacity", 0));
|
|
156
|
+
|
|
157
|
+
// Center percentage
|
|
158
|
+
svg
|
|
159
|
+
.append("text")
|
|
160
|
+
.attr("text-anchor", "middle")
|
|
161
|
+
.attr("dy", ".3em")
|
|
162
|
+
.style("font-size", "24px")
|
|
163
|
+
.style("font-weight", "bold")
|
|
164
|
+
.text(`${passedPercentage}%`);
|
|
165
|
+
|
|
166
|
+
// Legend - positioned to the right
|
|
167
|
+
const legend = svg
|
|
168
|
+
.selectAll(".legend")
|
|
169
|
+
.data(color.domain())
|
|
170
|
+
.enter()
|
|
171
|
+
.append("g")
|
|
172
|
+
.attr("class", "legend")
|
|
173
|
+
.attr(
|
|
174
|
+
"transform",
|
|
175
|
+
(d, i) =>
|
|
176
|
+
`translate(${radius + 20},${i * (legendRectSize + legendSpacing) - 40})`
|
|
177
|
+
); // Moved right
|
|
178
|
+
|
|
179
|
+
legend
|
|
180
|
+
.append("rect")
|
|
181
|
+
.attr("width", legendRectSize)
|
|
182
|
+
.attr("height", legendRectSize)
|
|
183
|
+
.style("fill", color)
|
|
184
|
+
.style("stroke", color);
|
|
185
|
+
|
|
186
|
+
legend
|
|
187
|
+
.append("text")
|
|
188
|
+
.attr("x", legendRectSize + 5)
|
|
189
|
+
.attr("y", legendRectSize - 2)
|
|
190
|
+
.text((d) => d)
|
|
191
|
+
.style("font-size", "12px")
|
|
192
|
+
.style("text-anchor", "start");
|
|
193
|
+
|
|
194
|
+
return `
|
|
195
|
+
<div class="pie-chart-container">
|
|
196
|
+
<h3>Test Distribution Chart</h3>
|
|
197
|
+
${body.html()}
|
|
198
|
+
<style>
|
|
199
|
+
.pie-chart-container {
|
|
200
|
+
display: flex;
|
|
201
|
+
justify-content: center;
|
|
202
|
+
margin: 20px 0;
|
|
203
|
+
}
|
|
204
|
+
.pie-chart-container svg {
|
|
205
|
+
display: block;
|
|
206
|
+
margin: 0 auto;
|
|
207
|
+
}
|
|
208
|
+
.pie-chart-container h3 {
|
|
209
|
+
text-align: center;
|
|
210
|
+
margin: 0 0 10px;
|
|
211
|
+
font-size: 16px;
|
|
212
|
+
color: var(--text-color);
|
|
213
|
+
}
|
|
214
|
+
</style>
|
|
215
|
+
</div>
|
|
216
|
+
`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Process the JSON data to extract suites information
|
|
220
|
+
function getSuitesData(results) {
|
|
221
|
+
const suitesMap = new Map();
|
|
222
|
+
|
|
223
|
+
results.forEach((test) => {
|
|
224
|
+
const browser = test.name.split(" > ")[1]; // Extract browser (chromium/firefox/webkit)
|
|
225
|
+
const suiteName = test.suiteName;
|
|
226
|
+
const key = `${suiteName}|${browser}`;
|
|
227
|
+
|
|
228
|
+
if (!suitesMap.has(key)) {
|
|
229
|
+
suitesMap.set(key, {
|
|
230
|
+
id: test.id,
|
|
231
|
+
name: `${suiteName} (${browser})`,
|
|
232
|
+
status: test.status,
|
|
233
|
+
count: 0,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
suitesMap.get(key).count++;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return Array.from(suitesMap.values());
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Generate suites widget (updated for your data)
|
|
243
|
+
function generateSuitesWidget(suitesData) {
|
|
244
|
+
return `
|
|
245
|
+
<div class="suites-widget">
|
|
246
|
+
<div class="suites-header">
|
|
247
|
+
<h2>Test Suites</h2>
|
|
248
|
+
<div class="summary-badge">
|
|
249
|
+
${suitesData.length} suites • ${suitesData.reduce(
|
|
250
|
+
(sum, suite) => sum + suite.count,
|
|
251
|
+
0
|
|
252
|
+
)} tests
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div class="suites-grid">
|
|
257
|
+
${suitesData
|
|
258
|
+
.map(
|
|
259
|
+
(suite) => `
|
|
260
|
+
<div class="suite-card ${suite.status}">
|
|
261
|
+
<div class="suite-meta">
|
|
262
|
+
<span class="browser-tag">${suite.name
|
|
263
|
+
.split("(")
|
|
264
|
+
.pop()
|
|
265
|
+
.replace(")", "")}</span>
|
|
266
|
+
<span class="test-count">${suite.count} test${
|
|
267
|
+
suite.count !== 1 ? "s" : ""
|
|
268
|
+
}</span>
|
|
269
|
+
</div>
|
|
270
|
+
<span class="browser-name">${suite.name
|
|
271
|
+
.split(" (")[1]
|
|
272
|
+
.replace(")", "")}</span>
|
|
273
|
+
</div>
|
|
274
|
+
`
|
|
275
|
+
)
|
|
276
|
+
.join("")}
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<style>
|
|
280
|
+
.suites-widget {
|
|
281
|
+
background: linear-gradient(to bottom right, #f0f4ff, #ffffff);
|
|
282
|
+
border-radius: 16px;
|
|
283
|
+
padding: 10px;
|
|
284
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
|
|
285
|
+
font-family: 'Segoe UI', Roboto, sans-serif;
|
|
286
|
+
height: 100%;
|
|
287
|
+
}
|
|
288
|
+
span.browser-name {
|
|
289
|
+
background-color: #265685;
|
|
290
|
+
font-size: 0.875rem;
|
|
291
|
+
color: #fff;
|
|
292
|
+
padding: 3px;
|
|
293
|
+
border-radius: 4px;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.suites-header {
|
|
297
|
+
display: flex;
|
|
298
|
+
align-items: center;
|
|
299
|
+
gap: 16px;
|
|
300
|
+
margin-bottom: 24px;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.suites-header h2 {
|
|
304
|
+
font-size: 20px;
|
|
305
|
+
font-weight: 600;
|
|
306
|
+
margin: 0;
|
|
307
|
+
color: #1a202c;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.summary-badge {
|
|
311
|
+
background: #f8fafc;
|
|
312
|
+
color: #64748b;
|
|
313
|
+
padding: 4px 12px;
|
|
314
|
+
border-radius: 12px;
|
|
315
|
+
font-size: 14px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.suites-grid {
|
|
319
|
+
display: grid;
|
|
320
|
+
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
321
|
+
gap: 16px;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.suite-card {
|
|
325
|
+
background: #e6e6e6;
|
|
326
|
+
border-radius: 12px;
|
|
327
|
+
padding: 18px;
|
|
328
|
+
border: 1px solid #f1f5f9;
|
|
329
|
+
transition: all 0.2s ease;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.suite-card:hover {
|
|
333
|
+
transform: translateY(-2px);
|
|
334
|
+
box-shadow: 0 6px 12px rgba(0,0,0,0.08);
|
|
335
|
+
border-color: #e2e8f0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.suite-meta {
|
|
339
|
+
display: flex;
|
|
340
|
+
justify-content: space-between;
|
|
341
|
+
align-items: center;
|
|
342
|
+
margin-bottom: 12px;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.browser-tag {
|
|
346
|
+
font-size: 12px;
|
|
347
|
+
font-weight: 600;
|
|
348
|
+
color: #64748b;
|
|
349
|
+
text-transform: uppercase;
|
|
350
|
+
letter-spacing: 0.5px;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.status-indicator {
|
|
354
|
+
width: 12px;
|
|
355
|
+
height: 12px;
|
|
356
|
+
border-radius: 50%;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.status-indicator.passed {
|
|
360
|
+
background: #2a9c68;
|
|
361
|
+
box-shadow: 0 0 0 3px #ecfdf5;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.status-indicator.failed {
|
|
365
|
+
background: #ef4444;
|
|
366
|
+
box-shadow: 0 0 0 3px #fef2f2;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.status-indicator.skipped {
|
|
370
|
+
background: #f59e0b;
|
|
371
|
+
box-shadow: 0 0 0 3px #fffbeb;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.suite-card h3 {
|
|
375
|
+
font-size: 16px;
|
|
376
|
+
margin: 0 0 16px 0;
|
|
377
|
+
color: #1e293b;
|
|
378
|
+
white-space: nowrap;
|
|
379
|
+
overflow: hidden;
|
|
380
|
+
text-overflow: ellipsis;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.test-visualization {
|
|
384
|
+
display: flex;
|
|
385
|
+
align-items: center;
|
|
386
|
+
gap: 12px;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.test-dots {
|
|
390
|
+
padding: 4px;
|
|
391
|
+
display: flex;
|
|
392
|
+
flex-wrap: wrap;
|
|
393
|
+
gap: 6px;
|
|
394
|
+
flex-grow: 1;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.test-dot {
|
|
398
|
+
width: 10px;
|
|
399
|
+
height: 10px;
|
|
400
|
+
border-radius: 50%;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.test-dot.passed {
|
|
404
|
+
background: #2a9c68;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.test-dot.failed {
|
|
408
|
+
background: #ef4444;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.test-dot.skipped {
|
|
412
|
+
background: #f59e0b;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.test-count {
|
|
416
|
+
font-size: 14px;
|
|
417
|
+
color: #64748b;
|
|
418
|
+
min-width: 60px;
|
|
419
|
+
text-align: right;
|
|
420
|
+
}
|
|
421
|
+
</style>
|
|
422
|
+
</div>
|
|
423
|
+
`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Enhanced HTML generation with properly integrated CSS and JS
|
|
427
|
+
function generateHTML(reportData) {
|
|
428
|
+
const { run, results } = reportData;
|
|
429
|
+
const suitesData = getSuitesData(reportData.results);
|
|
430
|
+
const runSummary = run || {
|
|
431
|
+
totalTests: 0,
|
|
432
|
+
passed: 0,
|
|
433
|
+
failed: 0,
|
|
434
|
+
skipped: 0,
|
|
435
|
+
duration: 0,
|
|
436
|
+
timestamp: new Date(),
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Calculate additional metrics
|
|
440
|
+
const passPercentage =
|
|
441
|
+
runSummary.totalTests > 0
|
|
442
|
+
? Math.round((runSummary.passed / runSummary.totalTests) * 100)
|
|
443
|
+
: 0;
|
|
444
|
+
const failPercentage =
|
|
445
|
+
runSummary.totalTests > 0
|
|
446
|
+
? Math.round((runSummary.failed / runSummary.totalTests) * 100)
|
|
447
|
+
: 0;
|
|
448
|
+
const skipPercentage =
|
|
449
|
+
runSummary.totalTests > 0
|
|
450
|
+
? Math.round((runSummary.skipped / runSummary.totalTests) * 100)
|
|
451
|
+
: 0;
|
|
452
|
+
const avgTestDuration =
|
|
453
|
+
runSummary.totalTests > 0
|
|
454
|
+
? formatDuration(runSummary.duration / runSummary.totalTests)
|
|
455
|
+
: "0.0s";
|
|
456
|
+
|
|
457
|
+
// Generate test cases HTML
|
|
458
|
+
const generateTestCasesHTML = () => {
|
|
459
|
+
if (!results || results.length === 0) {
|
|
460
|
+
return '<div class="no-tests">No test results found</div>';
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Collect all unique tags and browsers
|
|
464
|
+
const allTags = new Set();
|
|
465
|
+
const allBrowsers = new Set();
|
|
466
|
+
|
|
467
|
+
results.forEach((test) => {
|
|
468
|
+
(test.tags || []).forEach((tag) => allTags.add(tag));
|
|
469
|
+
const browserMatch = test.name.match(/ > (\w+) > /);
|
|
470
|
+
if (browserMatch) allBrowsers.add(browserMatch[1]);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
return results
|
|
474
|
+
.map((test, index) => {
|
|
475
|
+
const browserMatch = test.name.match(/ > (\w+) > /);
|
|
476
|
+
const browser = browserMatch ? browserMatch[1] : "unknown";
|
|
477
|
+
const testName = test.name.split(" > ").pop() || test.name;
|
|
478
|
+
|
|
479
|
+
// Generate steps HTML recursively
|
|
480
|
+
const generateStepsHTML = (steps, depth = 0) => {
|
|
481
|
+
if (!steps || steps.length === 0) return "";
|
|
482
|
+
|
|
483
|
+
return steps
|
|
484
|
+
.map((step) => {
|
|
485
|
+
const hasNestedSteps = step.steps && step.steps.length > 0;
|
|
486
|
+
const isHook = step.isHook;
|
|
487
|
+
const stepClass = isHook ? "step-hook" : "";
|
|
488
|
+
const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
|
|
489
|
+
|
|
490
|
+
return `
|
|
491
|
+
<div class="step-item" style="padding-left: ${depth * 20}px">
|
|
492
|
+
<div class="step-header ${stepClass}" onclick="toggleStepDetails(this)">
|
|
493
|
+
<span class="step-icon">${getStatusIcon(step.status)}</span>
|
|
494
|
+
<span class="step-title">${sanitizeHTML(
|
|
495
|
+
step.title
|
|
496
|
+
)}${hookIndicator}</span>
|
|
497
|
+
<span class="step-duration">${formatDuration(
|
|
498
|
+
step.duration
|
|
499
|
+
)}</span>
|
|
500
|
+
</div>
|
|
501
|
+
<div class="step-details">
|
|
502
|
+
${
|
|
503
|
+
step.codeLocation
|
|
504
|
+
? `<div><strong>Location:</strong> ${sanitizeHTML(
|
|
505
|
+
step.codeLocation
|
|
506
|
+
)}</div>`
|
|
507
|
+
: ""
|
|
508
|
+
}
|
|
509
|
+
${
|
|
510
|
+
step.errorMessage
|
|
511
|
+
? `
|
|
512
|
+
<div class="step-error">
|
|
513
|
+
<strong>Error:</strong> ${sanitizeHTML(step.errorMessage)}
|
|
514
|
+
${
|
|
515
|
+
step.stackTrace
|
|
516
|
+
? `<pre>${sanitizeHTML(step.stackTrace)}</pre>`
|
|
517
|
+
: ""
|
|
518
|
+
}
|
|
519
|
+
</div>
|
|
520
|
+
`
|
|
521
|
+
: ""
|
|
522
|
+
}
|
|
523
|
+
${
|
|
524
|
+
hasNestedSteps
|
|
525
|
+
? `
|
|
526
|
+
<div class="nested-steps">
|
|
527
|
+
${generateStepsHTML(step.steps, depth + 1)}
|
|
528
|
+
</div>
|
|
529
|
+
`
|
|
530
|
+
: ""
|
|
531
|
+
}
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
`;
|
|
535
|
+
})
|
|
536
|
+
.join("");
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
return `
|
|
540
|
+
<div class="test-suite" data-status="${
|
|
541
|
+
test.status
|
|
542
|
+
}" data-browser="${browser}" data-tags="${(test.tags || []).join(",")}">
|
|
543
|
+
<div class="suite-header" onclick="toggleTestDetails(this)">
|
|
544
|
+
<div>
|
|
545
|
+
<span class="${getStatusClass(
|
|
546
|
+
test.status
|
|
547
|
+
)}">${test.status.toUpperCase()}</span>
|
|
548
|
+
<span class="test-name">${sanitizeHTML(testName)}</span>
|
|
549
|
+
<span class="test-browser">(${browser})</span>
|
|
550
|
+
</div>
|
|
551
|
+
<div class="test-meta">
|
|
552
|
+
<span class="test-duration">${formatDuration(
|
|
553
|
+
test.duration
|
|
554
|
+
)}</span>
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
<div class="suite-content">
|
|
558
|
+
<div class="test-details">
|
|
559
|
+
<h3>Test Details</h3>
|
|
560
|
+
<p><strong>Status:</strong> <span class="${getStatusClass(
|
|
561
|
+
test.status
|
|
562
|
+
)}">${test.status.toUpperCase()}</span></p>
|
|
563
|
+
<p><strong>Browser:</strong> ${browser}</p>
|
|
564
|
+
<p><strong>Duration:</strong> ${formatDuration(test.duration)}</p>
|
|
565
|
+
${
|
|
566
|
+
test.tags && test.tags.length > 0
|
|
567
|
+
? `<p><strong>Tags:</strong> ${test.tags
|
|
568
|
+
.map((t) => `<span class="tag">${t}</span>`)
|
|
569
|
+
.join(" ")}</p>`
|
|
570
|
+
: ""
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
<h3>Test Steps</h3>
|
|
574
|
+
<div class="steps-list">
|
|
575
|
+
${generateStepsHTML(test.steps)}
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
${
|
|
579
|
+
test.screenshots && test.screenshots.length > 0
|
|
580
|
+
? `
|
|
581
|
+
<div class="attachments-section">
|
|
582
|
+
<h4>Screenshots</h4>
|
|
583
|
+
<div class="attachments-grid">
|
|
584
|
+
${test.screenshots
|
|
585
|
+
.map(
|
|
586
|
+
(screenshot) => `
|
|
587
|
+
<div class="attachment-item">
|
|
588
|
+
<img src="${screenshot}" alt="Screenshot">
|
|
589
|
+
<div class="attachment-info">
|
|
590
|
+
<a href="${screenshot}" target="_blank">View Full Size</a>
|
|
591
|
+
</div>
|
|
592
|
+
</div>
|
|
593
|
+
`
|
|
594
|
+
)
|
|
595
|
+
.join("")}
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
`
|
|
599
|
+
: ""
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
${
|
|
603
|
+
test.codeSnippet
|
|
604
|
+
? `
|
|
605
|
+
<div class="code-section">
|
|
606
|
+
<h4>Code Snippet</h4>
|
|
607
|
+
<pre>${sanitizeHTML(test.codeSnippet)}</pre>
|
|
608
|
+
</div>
|
|
609
|
+
`
|
|
610
|
+
: ""
|
|
611
|
+
}
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
</div>
|
|
615
|
+
`;
|
|
616
|
+
})
|
|
617
|
+
.join("");
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
// Generate HTML with optimized CSS and JS
|
|
621
|
+
return `
|
|
622
|
+
<!DOCTYPE html>
|
|
623
|
+
<html lang="en">
|
|
624
|
+
<head>
|
|
625
|
+
<meta charset="UTF-8">
|
|
626
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
627
|
+
<title>Playwright Pulse Report</title>
|
|
628
|
+
<style>
|
|
629
|
+
/* Base Styles */
|
|
630
|
+
:root {
|
|
631
|
+
--primary-color: #3f51b5;
|
|
632
|
+
--secondary-color: #ff4081;
|
|
633
|
+
--success-color: #4CAF50;
|
|
634
|
+
--danger-color: #F44336;
|
|
635
|
+
--warning-color: #FFC107;
|
|
636
|
+
--info-color: #2196F3;
|
|
637
|
+
--light-color: #f5f5f5;
|
|
638
|
+
--dark-color: #212121;
|
|
639
|
+
--text-color: #424242;
|
|
640
|
+
--border-color: #e0e0e0;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
body {
|
|
644
|
+
font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', sans-serif;
|
|
645
|
+
margin: 0;
|
|
646
|
+
padding: 0;
|
|
647
|
+
background-color: #fafafa;
|
|
648
|
+
color: var(--text-color);
|
|
649
|
+
line-height: 1.6;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.container {
|
|
653
|
+
margin: 20px auto;
|
|
654
|
+
padding: 20px;
|
|
655
|
+
background-color: #fff;
|
|
656
|
+
border-radius: 8px;
|
|
657
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/* Header Styles */
|
|
661
|
+
.header {
|
|
662
|
+
display: flex;
|
|
663
|
+
justify-content: space-between;
|
|
664
|
+
align-items: center;
|
|
665
|
+
flex-wrap: wrap;
|
|
666
|
+
margin-bottom: 20px;
|
|
667
|
+
padding-bottom: 20px;
|
|
668
|
+
border-bottom: 1px solid var(--border-color);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.header h1 {
|
|
672
|
+
margin: 0;
|
|
673
|
+
font-size: 24px;
|
|
674
|
+
color: var(--primary-color);
|
|
675
|
+
display: flex;
|
|
676
|
+
align-items: center;
|
|
677
|
+
gap: 10px;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.run-info {
|
|
681
|
+
background: #f5f5f5;
|
|
682
|
+
padding: 10px 15px;
|
|
683
|
+
border-radius: 6px;
|
|
684
|
+
font-size: 14px;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/* Tab Styles */
|
|
688
|
+
.tabs {
|
|
689
|
+
display: flex;
|
|
690
|
+
border-bottom: 1px solid var(--border-color);
|
|
691
|
+
margin-bottom: 20px;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.tab-button {
|
|
695
|
+
padding: 10px 20px;
|
|
696
|
+
background: none;
|
|
697
|
+
border: none;
|
|
698
|
+
cursor: pointer;
|
|
699
|
+
font-size: 16px;
|
|
700
|
+
color: #666;
|
|
701
|
+
position: relative;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.tab-button.active {
|
|
705
|
+
color: var(--primary-color);
|
|
706
|
+
font-weight: 500;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.tab-button.active::after {
|
|
710
|
+
content: '';
|
|
711
|
+
position: absolute;
|
|
712
|
+
bottom: -1px;
|
|
713
|
+
left: 0;
|
|
714
|
+
right: 0;
|
|
715
|
+
height: 2px;
|
|
716
|
+
background: var(--primary-color);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.tab-content {
|
|
720
|
+
display: none;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.tab-content.active {
|
|
724
|
+
display: block;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/* Main dashboard grid layout */
|
|
728
|
+
.dashboard-grid {
|
|
729
|
+
display: grid;
|
|
730
|
+
grid-template-columns: 1fr;
|
|
731
|
+
gap: 20px;
|
|
732
|
+
padding: 16px 0;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
.summary-card {
|
|
736
|
+
background: linear-gradient(to bottom right, #f0f4ff, #ffffff);
|
|
737
|
+
border-radius: 8px;
|
|
738
|
+
padding: 20px;
|
|
739
|
+
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
|
740
|
+
text-align: center;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.summary-card h3 {
|
|
744
|
+
margin: 0 0 10px;
|
|
745
|
+
font-size: 16px;
|
|
746
|
+
color: #666;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.summary-card .value {
|
|
750
|
+
font-size: 28px;
|
|
751
|
+
font-weight: 600;
|
|
752
|
+
margin: 10px 0;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.status-passed .value {
|
|
756
|
+
color: var(--success-color);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.status-failed .value {
|
|
760
|
+
color: var(--danger-color);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.status-skipped .value {
|
|
764
|
+
color: var(--warning-color);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.pie-chart-container {
|
|
768
|
+
grid-column: span 2;
|
|
769
|
+
background: linear-gradient(to bottom right, #f0f4ff, #ffffff);
|
|
770
|
+
border-radius: 8px;
|
|
771
|
+
padding: 20px;
|
|
772
|
+
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/* Test Run Summary Styles */
|
|
776
|
+
.filters {
|
|
777
|
+
display: flex;
|
|
778
|
+
gap: 10px;
|
|
779
|
+
margin-bottom: 20px;
|
|
780
|
+
flex-wrap: wrap;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.filters input,
|
|
784
|
+
.filters select {
|
|
785
|
+
padding: 8px 12px;
|
|
786
|
+
border: 1px solid #ddd;
|
|
787
|
+
border-radius: 4px;
|
|
788
|
+
font-size: 14px;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.filters button {
|
|
792
|
+
padding: 8px 16px;
|
|
793
|
+
background: var(--primary-color);
|
|
794
|
+
color: white;
|
|
795
|
+
border: none;
|
|
796
|
+
border-radius: 4px;
|
|
797
|
+
cursor: pointer;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.test-suite {
|
|
801
|
+
margin-bottom: 15px;
|
|
802
|
+
border: 1px solid #eee;
|
|
803
|
+
border-radius: 6px;
|
|
804
|
+
overflow: hidden;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.suite-header {
|
|
808
|
+
padding: 12px 15px;
|
|
809
|
+
background: #f9f9f9;
|
|
810
|
+
cursor: pointer;
|
|
811
|
+
display: flex;
|
|
812
|
+
justify-content: space-between;
|
|
813
|
+
align-items: center;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.suite-header:hover {
|
|
817
|
+
background: #f0f0f0;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
.suite-content {
|
|
821
|
+
display: none;
|
|
822
|
+
padding: 15px;
|
|
823
|
+
background: white;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.test-details h3 {
|
|
827
|
+
margin-top: 0;
|
|
828
|
+
font-size: 18px;
|
|
829
|
+
color: var(--dark-color);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.steps-list {
|
|
833
|
+
margin: 15px 0;
|
|
834
|
+
padding: 0;
|
|
835
|
+
list-style: none;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.step-item {
|
|
839
|
+
margin-bottom: 8px;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.step-header {
|
|
843
|
+
display: flex;
|
|
844
|
+
align-items: center;
|
|
845
|
+
cursor: pointer;
|
|
846
|
+
padding: 8px;
|
|
847
|
+
border-radius: 4px;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.step-header:hover {
|
|
851
|
+
background: #f5f5f5;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
.step-icon {
|
|
855
|
+
margin-right: 8px;
|
|
856
|
+
width: 20px;
|
|
857
|
+
text-align: center;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.step-title {
|
|
861
|
+
flex: 1;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.step-duration {
|
|
865
|
+
color: #666;
|
|
866
|
+
font-size: 12px;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.step-details {
|
|
870
|
+
display: none;
|
|
871
|
+
padding: 10px;
|
|
872
|
+
margin-top: 5px;
|
|
873
|
+
background: #f9f9f9;
|
|
874
|
+
border-radius: 4px;
|
|
875
|
+
font-size: 14px;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.step-error {
|
|
879
|
+
color: var(--danger-color);
|
|
880
|
+
margin-top: 8px;
|
|
881
|
+
padding: 8px;
|
|
882
|
+
background: rgba(244, 67, 54, 0.1);
|
|
883
|
+
border-radius: 4px;
|
|
884
|
+
font-size: 13px;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
.step-hook {
|
|
888
|
+
background: rgba(33, 150, 243, 0.1);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.nested-steps {
|
|
892
|
+
display: none;
|
|
893
|
+
padding-left: 20px;
|
|
894
|
+
border-left: 2px solid #eee;
|
|
895
|
+
margin-top: 8px;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.attachments-grid {
|
|
899
|
+
display: grid;
|
|
900
|
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
901
|
+
gap: 15px;
|
|
902
|
+
margin-top: 15px;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
.attachment-item {
|
|
906
|
+
border: 1px solid #eee;
|
|
907
|
+
border-radius: 4px;
|
|
908
|
+
overflow: hidden;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.attachment-item img {
|
|
912
|
+
width: 100%;
|
|
913
|
+
height: auto;
|
|
914
|
+
display: block;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
.tag {
|
|
918
|
+
display: inline-block;
|
|
919
|
+
background: #e0e0e0;
|
|
920
|
+
padding: 2px 6px;
|
|
921
|
+
border-radius: 4px;
|
|
922
|
+
font-size: 12px;
|
|
923
|
+
margin-right: 5px;
|
|
924
|
+
}
|
|
925
|
+
.status-badge {
|
|
926
|
+
padding: 3px 8px;
|
|
927
|
+
border-radius: 4px;
|
|
928
|
+
font-size: 12px;
|
|
929
|
+
font-weight: bold;
|
|
930
|
+
color: white;
|
|
931
|
+
text-transform: uppercase;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
span.status-passed {
|
|
935
|
+
background-color: #4CAF50 !important; /* Bright green */
|
|
936
|
+
color: white;
|
|
937
|
+
border-radius: 4px;
|
|
938
|
+
padding: 4px
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
span.status-failed {
|
|
942
|
+
background-color: #F44336 !important; /* Bright red */
|
|
943
|
+
color: white;
|
|
944
|
+
border-radius: 4px;
|
|
945
|
+
padding: 4px
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
span.status-skipped {
|
|
949
|
+
background-color: #FFC107 !important; /* Deep yellow */
|
|
950
|
+
color: white;
|
|
951
|
+
border-radius: 4px;
|
|
952
|
+
padding: 4px
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/* Enhanced Pie Chart Styles */
|
|
956
|
+
.pie-chart-container {
|
|
957
|
+
display: flex;
|
|
958
|
+
flex-direction: column;
|
|
959
|
+
align-items: center;
|
|
960
|
+
margin: 20px 0;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.pie-chart-svg {
|
|
964
|
+
margin: 0 auto;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
.pie-chart-total {
|
|
968
|
+
font-size: 18px;
|
|
969
|
+
font-weight: bold;
|
|
970
|
+
fill: #333;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
.pie-chart-label {
|
|
974
|
+
font-size: 12px;
|
|
975
|
+
fill: #666;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
.pie-chart-legend {
|
|
979
|
+
display: flex;
|
|
980
|
+
flex-wrap: wrap;
|
|
981
|
+
justify-content: center;
|
|
982
|
+
gap: 15px;
|
|
983
|
+
margin-top: 15px;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.legend-item {
|
|
987
|
+
display: flex;
|
|
988
|
+
align-items: center;
|
|
989
|
+
gap: 5px;
|
|
990
|
+
font-size: 14px;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
.legend-color {
|
|
994
|
+
width: 12px;
|
|
995
|
+
height: 12px;
|
|
996
|
+
border-radius: 50%;
|
|
997
|
+
display: inline-block;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
.legend-value {
|
|
1001
|
+
font-weight: 500;
|
|
1002
|
+
}
|
|
1003
|
+
.test-name {
|
|
1004
|
+
font-weight: 600;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/* Below summary cards: chart and test suites */
|
|
1008
|
+
.dashboard-bottom {
|
|
1009
|
+
display: flex;
|
|
1010
|
+
flex-direction: column;
|
|
1011
|
+
gap: 24px;
|
|
1012
|
+
}
|
|
1013
|
+
/* Responsive Styles */
|
|
1014
|
+
/* Mobile (up to 480px) and Tablet (481px to 768px) Responsive Styles */
|
|
1015
|
+
|
|
1016
|
+
@media (min-width: 768px) {
|
|
1017
|
+
.dashboard-grid {
|
|
1018
|
+
grid-template-columns: repeat(4, 1fr); /* Four summary cards side-by-side */
|
|
1019
|
+
}
|
|
1020
|
+
.dashboard-bottom {
|
|
1021
|
+
flex-direction: row;
|
|
1022
|
+
}
|
|
1023
|
+
.test-distribution {
|
|
1024
|
+
flex: 1;
|
|
1025
|
+
}
|
|
1026
|
+
.test-suites {
|
|
1027
|
+
flex: 2; /* dynamically expand */
|
|
1028
|
+
min-width: 300px;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
@media (max-width: 768px) {
|
|
1032
|
+
/* Base container adjustments */
|
|
1033
|
+
.container {
|
|
1034
|
+
padding: 15px;
|
|
1035
|
+
margin: 10px auto;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/* Header adjustments */
|
|
1039
|
+
.header {
|
|
1040
|
+
flex-direction: column;
|
|
1041
|
+
align-items: flex-start;
|
|
1042
|
+
gap: 15px;
|
|
1043
|
+
padding-bottom: 15px;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
.run-info {
|
|
1047
|
+
font-size: 13px;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/* Tab adjustments */
|
|
1051
|
+
.tabs {
|
|
1052
|
+
overflow-x: auto;
|
|
1053
|
+
white-space: nowrap;
|
|
1054
|
+
padding-bottom: 5px;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.tab-button {
|
|
1058
|
+
padding: 8px 15px;
|
|
1059
|
+
font-size: 14px;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/* Dashboard Grid adjustments */
|
|
1063
|
+
.dashboard-grid {
|
|
1064
|
+
grid-template-columns: 1fr;
|
|
1065
|
+
gap: 15px;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
.summary-card {
|
|
1069
|
+
padding: 15px;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
.summary-card .value {
|
|
1073
|
+
font-size: 24px;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
.pie-chart-container {
|
|
1077
|
+
padding: 15px;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
.pie-chart-container svg {
|
|
1081
|
+
width: 300px;
|
|
1082
|
+
height: 300px;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/* Test Suites Widget adjustments */
|
|
1086
|
+
.suites-widget {
|
|
1087
|
+
padding: 8px;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.suites-header {
|
|
1091
|
+
flex-direction: column;
|
|
1092
|
+
align-items: flex-start;
|
|
1093
|
+
gap: 10px;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
.suites-grid {
|
|
1097
|
+
grid-template-columns: 1fr;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/* Test Run Summary adjustments */
|
|
1101
|
+
.filters {
|
|
1102
|
+
flex-direction: column;
|
|
1103
|
+
gap: 8px;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
.filters input,
|
|
1107
|
+
.filters select {
|
|
1108
|
+
width: 100%;
|
|
1109
|
+
padding: 8px;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
.filters button {
|
|
1113
|
+
width: 100%;
|
|
1114
|
+
margin-top: 5px;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
.test-suite {
|
|
1118
|
+
margin-bottom: 10px;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
.suite-header {
|
|
1122
|
+
padding: 10px;
|
|
1123
|
+
flex-wrap: wrap;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.test-name {
|
|
1127
|
+
display: block;
|
|
1128
|
+
width: 100%;
|
|
1129
|
+
margin-top: 5px;
|
|
1130
|
+
font-weight: 600;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
.test-meta {
|
|
1134
|
+
margin-top: 5px;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.suite-content {
|
|
1138
|
+
padding: 10px;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
.steps-list {
|
|
1142
|
+
margin: 10px 0;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
.step-header {
|
|
1146
|
+
padding: 6px;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.step-icon {
|
|
1150
|
+
font-size: 14px;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
.step-title {
|
|
1154
|
+
font-size: 14px;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.step-duration {
|
|
1158
|
+
font-size: 11px;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.attachments-grid {
|
|
1162
|
+
grid-template-columns: repeat(2, 1fr);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/* Specific adjustments for mobile only (up to 480px) */
|
|
1166
|
+
@media (max-width: 480px) {
|
|
1167
|
+
.header h1 {
|
|
1168
|
+
font-size: 20px;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
.summary-card .value {
|
|
1172
|
+
font-size: 22px;
|
|
1173
|
+
}
|
|
1174
|
+
.pie-chart-container {
|
|
1175
|
+
grid-column: span 1;
|
|
1176
|
+
}
|
|
1177
|
+
.pie-chart-container svg {
|
|
1178
|
+
width: 300px;
|
|
1179
|
+
height: 300px;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
.attachments-grid {
|
|
1183
|
+
grid-template-columns: 1fr;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
.step-item {
|
|
1187
|
+
padding-left: 0 !important;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
.nested-steps {
|
|
1191
|
+
padding-left: 10px;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
</style>
|
|
1196
|
+
</head>
|
|
1197
|
+
<body>
|
|
1198
|
+
<div class="container">
|
|
1199
|
+
<header class="header">
|
|
1200
|
+
<div style="display: flex; align-items: center; gap: 15px;">
|
|
1201
|
+
<img id="report-logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMNCA3bDggNSA4LTUtOC01eiIgZmlsbD0iIzNmNTEiLz48cGF0aCBkPSJNMTIgNkw0IDExbDggNSA4LTUtOC01eiIgZmlsbD0iIzQyODVmNCIvPjxwYXRoIGQ9Ik0xMiAxMGwtOCA1IDggNSA4LTUtOC01eiIgZmlsbD0iIzNkNTViNCIvPjwvc3ZnPg==" alt="Logo" style="height: 40px;">
|
|
1202
|
+
<h1>
|
|
1203
|
+
Playwright Pulse Report
|
|
1204
|
+
</h1>
|
|
1205
|
+
</div>
|
|
1206
|
+
<div class="run-info">
|
|
1207
|
+
<strong>Run Date:</strong> ${formatDate(
|
|
1208
|
+
runSummary.timestamp
|
|
1209
|
+
)}<br>
|
|
1210
|
+
<strong>Total Duration:</strong> ${formatDuration(
|
|
1211
|
+
runSummary.duration
|
|
1212
|
+
)}
|
|
1213
|
+
</div>
|
|
1214
|
+
</header>
|
|
1215
|
+
|
|
1216
|
+
<div class="tabs">
|
|
1217
|
+
<button class="tab-button active" data-tab="dashboard">Dashboard</button>
|
|
1218
|
+
<button class="tab-button" data-tab="test-runs">Test Run Summary</button>
|
|
1219
|
+
</div>
|
|
1220
|
+
|
|
1221
|
+
<div id="dashboard" class="tab-content active">
|
|
1222
|
+
<div class="dashboard-grid">
|
|
1223
|
+
<div class="summary-card">
|
|
1224
|
+
<h3>Total Tests</h3>
|
|
1225
|
+
<div class="value">${runSummary.totalTests}</div>
|
|
1226
|
+
</div>
|
|
1227
|
+
<div class="summary-card status-passed">
|
|
1228
|
+
<h3>Passed</h3>
|
|
1229
|
+
<div class="value">${runSummary.passed}</div>
|
|
1230
|
+
<div class="trend">${passPercentage}%</div>
|
|
1231
|
+
</div>
|
|
1232
|
+
<div class="summary-card status-failed">
|
|
1233
|
+
<h3>Failed</h3>
|
|
1234
|
+
<div class="value">${runSummary.failed}</div>
|
|
1235
|
+
<div class="trend">${failPercentage}%</div>
|
|
1236
|
+
</div>
|
|
1237
|
+
<div class="summary-card status-skipped">
|
|
1238
|
+
<h3>Skipped</h3>
|
|
1239
|
+
<div class="value">${runSummary.skipped}</div>
|
|
1240
|
+
<div class="trend">${skipPercentage}%</div>
|
|
1241
|
+
</div>
|
|
1242
|
+
</div>
|
|
1243
|
+
<div class="dashboard-bottom">
|
|
1244
|
+
${generatePieChartD3([
|
|
1245
|
+
{ label: "Passed", value: runSummary.passed },
|
|
1246
|
+
{ label: "Failed", value: runSummary.failed },
|
|
1247
|
+
{ label: "Skipped", value: runSummary.skipped },
|
|
1248
|
+
])}
|
|
1249
|
+
${generateSuitesWidget(suitesData)}
|
|
1250
|
+
<div class="summary-cards">
|
|
1251
|
+
<div class="summary-card avg-time">
|
|
1252
|
+
<h3>Avg. Time</h3>
|
|
1253
|
+
<div class="value">${avgTestDuration}</div>
|
|
1254
|
+
</div>
|
|
1255
|
+
<div class="summary-card avg-time">
|
|
1256
|
+
<h3>Total Time</h3>
|
|
1257
|
+
<div class="value">${formatDuration(
|
|
1258
|
+
runSummary.duration
|
|
1259
|
+
)}</div>
|
|
1260
|
+
</div>
|
|
1261
|
+
</div>
|
|
1262
|
+
</div>
|
|
1263
|
+
</div>
|
|
1264
|
+
</div>
|
|
1265
|
+
|
|
1266
|
+
<div id="test-runs" class="tab-content">
|
|
1267
|
+
<div class="filters">
|
|
1268
|
+
<input type="text" id="filter-name" placeholder="Search by test name...">
|
|
1269
|
+
<select id="filter-status">
|
|
1270
|
+
<option value="">All Statuses</option>
|
|
1271
|
+
<option value="passed">Passed</option>
|
|
1272
|
+
<option value="failed">Failed</option>
|
|
1273
|
+
<option value="skipped">Skipped</option>
|
|
1274
|
+
</select>
|
|
1275
|
+
<select id="filter-browser">
|
|
1276
|
+
<option value="">All Browsers</option>
|
|
1277
|
+
${Array.from(
|
|
1278
|
+
new Set(
|
|
1279
|
+
results.map((test) => {
|
|
1280
|
+
const match = test.name.match(/ > (\w+) > /);
|
|
1281
|
+
return match ? match[1] : "unknown";
|
|
1282
|
+
})
|
|
1283
|
+
)
|
|
1284
|
+
)
|
|
1285
|
+
.map(
|
|
1286
|
+
(browser) => `
|
|
1287
|
+
<option value="${browser}">${browser}</option>
|
|
1288
|
+
`
|
|
1289
|
+
)
|
|
1290
|
+
.join("")}
|
|
1291
|
+
</select>
|
|
1292
|
+
<button onclick="expandAllTests()">Expand All</button>
|
|
1293
|
+
<button onclick="collapseAllTests()">Collapse All</button>
|
|
1294
|
+
</div>
|
|
1295
|
+
<div class="test-suites">
|
|
1296
|
+
${generateTestCasesHTML()}
|
|
1297
|
+
</div>
|
|
1298
|
+
</div>
|
|
1299
|
+
</div>
|
|
1300
|
+
|
|
1301
|
+
<script>
|
|
1302
|
+
// Tab switching functionality
|
|
1303
|
+
document.querySelectorAll('.tab-button').forEach(button => {
|
|
1304
|
+
button.addEventListener('click', () => {
|
|
1305
|
+
// Remove active class from all buttons and contents
|
|
1306
|
+
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
|
|
1307
|
+
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
|
1308
|
+
|
|
1309
|
+
// Add active class to clicked button and corresponding content
|
|
1310
|
+
const tabId = button.getAttribute('data-tab');
|
|
1311
|
+
button.classList.add('active');
|
|
1312
|
+
document.getElementById(tabId).classList.add('active');
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
// Test filtering functionality
|
|
1317
|
+
function setupFilters() {
|
|
1318
|
+
const nameFilter = document.getElementById('filter-name');
|
|
1319
|
+
const statusFilter = document.getElementById('filter-status');
|
|
1320
|
+
const browserFilter = document.getElementById('filter-browser');
|
|
1321
|
+
|
|
1322
|
+
const filterTests = () => {
|
|
1323
|
+
const nameValue = nameFilter.value.toLowerCase();
|
|
1324
|
+
const statusValue = statusFilter.value;
|
|
1325
|
+
const browserValue = browserFilter.value;
|
|
1326
|
+
|
|
1327
|
+
document.querySelectorAll('.test-suite').forEach(suite => {
|
|
1328
|
+
const name = suite.querySelector('.test-name').textContent.toLowerCase();
|
|
1329
|
+
const status = suite.getAttribute('data-status');
|
|
1330
|
+
const browser = suite.getAttribute('data-browser');
|
|
1331
|
+
|
|
1332
|
+
const nameMatch = name.includes(nameValue);
|
|
1333
|
+
const statusMatch = !statusValue || status === statusValue;
|
|
1334
|
+
const browserMatch = !browserValue || browser === browserValue;
|
|
1335
|
+
|
|
1336
|
+
if (nameMatch && statusMatch && browserMatch) {
|
|
1337
|
+
suite.style.display = 'block';
|
|
1338
|
+
} else {
|
|
1339
|
+
suite.style.display = 'none';
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
nameFilter.addEventListener('input', filterTests);
|
|
1345
|
+
statusFilter.addEventListener('change', filterTests);
|
|
1346
|
+
browserFilter.addEventListener('change', filterTests);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Test expansion functionality
|
|
1350
|
+
function toggleTestDetails(header) {
|
|
1351
|
+
const content = header.nextElementSibling;
|
|
1352
|
+
content.style.display = content.style.display === 'block' ? 'none' : 'block';
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Step expansion functionality
|
|
1356
|
+
function toggleStepDetails(header) {
|
|
1357
|
+
const details = header.nextElementSibling;
|
|
1358
|
+
details.style.display = details.style.display === 'block' ? 'none' : 'block';
|
|
1359
|
+
|
|
1360
|
+
// Toggle nested steps if they exist
|
|
1361
|
+
const nestedSteps = header.parentElement.querySelector('.nested-steps');
|
|
1362
|
+
if (nestedSteps) {
|
|
1363
|
+
nestedSteps.style.display = nestedSteps.style.display === 'block' ? 'none' : 'block';
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// Expand all tests
|
|
1368
|
+
function expandAllTests() {
|
|
1369
|
+
document.querySelectorAll('.suite-content').forEach(el => {
|
|
1370
|
+
el.style.display = 'block';
|
|
1371
|
+
});
|
|
1372
|
+
document.querySelectorAll('.step-details').forEach(el => {
|
|
1373
|
+
el.style.display = 'block';
|
|
1374
|
+
});
|
|
1375
|
+
document.querySelectorAll('.nested-steps').forEach(el => {
|
|
1376
|
+
el.style.display = 'block';
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// Collapse all tests
|
|
1381
|
+
function collapseAllTests() {
|
|
1382
|
+
document.querySelectorAll('.suite-content').forEach(el => {
|
|
1383
|
+
el.style.display = 'none';
|
|
1384
|
+
});
|
|
1385
|
+
document.querySelectorAll('.step-details').forEach(el => {
|
|
1386
|
+
el.style.display = 'none';
|
|
1387
|
+
});
|
|
1388
|
+
document.querySelectorAll('.nested-steps').forEach(el => {
|
|
1389
|
+
el.style.display = 'none';
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Initialize everything when DOM is loaded
|
|
1394
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1395
|
+
setupFilters();
|
|
1396
|
+
|
|
1397
|
+
// Make step headers clickable
|
|
1398
|
+
document.querySelectorAll('.step-header').forEach(header => {
|
|
1399
|
+
header.addEventListener('click', function() {
|
|
1400
|
+
toggleStepDetails(this);
|
|
1401
|
+
});
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
// Make test headers clickable
|
|
1405
|
+
document.querySelectorAll('.suite-header').forEach(header => {
|
|
1406
|
+
header.addEventListener('click', function() {
|
|
1407
|
+
toggleTestDetails(this);
|
|
1408
|
+
});
|
|
1409
|
+
});
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
// Enhanced expand/collapse functionality
|
|
1413
|
+
function toggleTestDetails(header) {
|
|
1414
|
+
const content = header.nextElementSibling;
|
|
1415
|
+
const isExpanded = content.style.display === 'block';
|
|
1416
|
+
content.style.display = isExpanded ? 'none' : 'block';
|
|
1417
|
+
header.setAttribute('aria-expanded', !isExpanded);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function toggleStepDetails(header) {
|
|
1421
|
+
const details = header.nextElementSibling;
|
|
1422
|
+
const nestedSteps = header.parentElement.querySelector('.nested-steps');
|
|
1423
|
+
|
|
1424
|
+
// Toggle main step details
|
|
1425
|
+
const isExpanded = details.style.display === 'block';
|
|
1426
|
+
details.style.display = isExpanded ? 'none' : 'block';
|
|
1427
|
+
|
|
1428
|
+
// Toggle nested steps if they exist
|
|
1429
|
+
if (nestedSteps) {
|
|
1430
|
+
nestedSteps.style.display = isExpanded ? 'none' : 'block';
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
header.setAttribute('aria-expanded', !isExpanded);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function expandAllTests() {
|
|
1437
|
+
document.querySelectorAll('.suite-content').forEach(el => {
|
|
1438
|
+
el.style.display = 'block';
|
|
1439
|
+
});
|
|
1440
|
+
document.querySelectorAll('.step-details').forEach(el => {
|
|
1441
|
+
el.style.display = 'block';
|
|
1442
|
+
});
|
|
1443
|
+
document.querySelectorAll('.nested-steps').forEach(el => {
|
|
1444
|
+
el.style.display = 'block';
|
|
1445
|
+
});
|
|
1446
|
+
document.querySelectorAll('[aria-expanded]').forEach(el => {
|
|
1447
|
+
el.setAttribute('aria-expanded', 'true');
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function collapseAllTests() {
|
|
1452
|
+
document.querySelectorAll('.suite-content').forEach(el => {
|
|
1453
|
+
el.style.display = 'none';
|
|
1454
|
+
});
|
|
1455
|
+
document.querySelectorAll('.step-details').forEach(el => {
|
|
1456
|
+
el.style.display = 'none';
|
|
1457
|
+
});
|
|
1458
|
+
document.querySelectorAll('.nested-steps').forEach(el => {
|
|
1459
|
+
el.style.display = 'none';
|
|
1460
|
+
});
|
|
1461
|
+
document.querySelectorAll('[aria-expanded]').forEach(el => {
|
|
1462
|
+
el.setAttribute('aria-expanded', 'false');
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// Initialize all interactive elements
|
|
1467
|
+
function initializeInteractiveElements() {
|
|
1468
|
+
// Test headers
|
|
1469
|
+
document.querySelectorAll('.suite-header').forEach(header => {
|
|
1470
|
+
header.addEventListener('click', () => toggleTestDetails(header));
|
|
1471
|
+
header.setAttribute('role', 'button');
|
|
1472
|
+
header.setAttribute('aria-expanded', 'false');
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
// Step headers
|
|
1476
|
+
document.querySelectorAll('.step-header').forEach(header => {
|
|
1477
|
+
header.addEventListener('click', () => toggleStepDetails(header));
|
|
1478
|
+
header.setAttribute('role', 'button');
|
|
1479
|
+
header.setAttribute('aria-expanded', 'false');
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
// Filter buttons
|
|
1483
|
+
document.getElementById('filter-name').addEventListener('input', filterTests);
|
|
1484
|
+
document.getElementById('filter-status').addEventListener('change', filterTests);
|
|
1485
|
+
document.getElementById('filter-browser').addEventListener('change', filterTests);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// Initialize when DOM is loaded
|
|
1489
|
+
document.addEventListener('DOMContentLoaded', initializeInteractiveElements);
|
|
1490
|
+
</script>
|
|
1491
|
+
</body>
|
|
1492
|
+
</html>
|
|
1493
|
+
`;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// [Keep the rest of the file unchanged...]
|
|
1497
|
+
|
|
1498
|
+
// [Rest of the file remains the same...]
|
|
1499
|
+
|
|
1500
|
+
// Main execution function
|
|
1501
|
+
async function main() {
|
|
1502
|
+
const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
1503
|
+
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE);
|
|
1504
|
+
const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
|
|
1505
|
+
|
|
1506
|
+
console.log(chalk.blue(`Generating enhanced static report in: ${outputDir}`));
|
|
1507
|
+
|
|
1508
|
+
let reportData;
|
|
1509
|
+
try {
|
|
1510
|
+
const jsonData = await fs.readFile(reportJsonPath, "utf-8");
|
|
1511
|
+
reportData = JSON.parse(jsonData);
|
|
1512
|
+
if (
|
|
1513
|
+
!reportData ||
|
|
1514
|
+
typeof reportData !== "object" ||
|
|
1515
|
+
!Array.isArray(reportData.results)
|
|
1516
|
+
) {
|
|
1517
|
+
throw new Error("Invalid report JSON structure.");
|
|
1518
|
+
}
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
1521
|
+
process.exit(1);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
try {
|
|
1525
|
+
const htmlContent = generateHTML(reportData);
|
|
1526
|
+
await fs.writeFile(reportHtmlPath, htmlContent, "utf-8");
|
|
1527
|
+
console.log(
|
|
1528
|
+
chalk.green(`Report generated successfully at: ${reportHtmlPath}`)
|
|
1529
|
+
);
|
|
1530
|
+
console.log(chalk.blue(`You can open it in your browser with:`));
|
|
1531
|
+
console.log(chalk.blue(`open ${reportHtmlPath}`));
|
|
1532
|
+
} catch (error) {
|
|
1533
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
1534
|
+
process.exit(1);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// Run the main function
|
|
1539
|
+
main();
|