@arghajit/playwright-pulse-report 0.1.6 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // Using Node.js syntax compatible with `.mjs`
2
+
3
3
  import * as fs from "fs/promises";
4
4
  import path from "path";
5
- import * as d3 from "d3";
6
- import { JSDOM } from "jsdom";
5
+ import { fork } from "child_process"; // Add this
6
+ import { fileURLToPath } from "url"; // Add this for resolving path in ESM
7
+
7
8
  // Use dynamic import for chalk as it's ESM only
8
9
  let chalk;
9
10
  try {
@@ -27,33 +28,745 @@ const DEFAULT_HTML_FILE = "playwright-pulse-static-report.html";
27
28
 
28
29
  // Helper functions
29
30
  function sanitizeHTML(str) {
31
+ // User's provided version (note: this doesn't escape HTML special chars correctly)
30
32
  if (str === null || str === undefined) return "";
31
33
  return String(str)
32
- .replace(/&/g, "&")
33
- .replace(/</g, "&lt;")
34
- .replace(/>/g, "&gt;")
35
- .replace(/"/g, "&quot;")
36
- .replace(/'/g, "&#039;");
34
+ .replace(/&/g, "&")
35
+ .replace(/</g, "<")
36
+ .replace(/>/g, ">")
37
+ .replace(/"/g, `"`)
38
+ .replace(/'/g, "'");
39
+ }
40
+ function capitalize(str) {
41
+ if (!str) return ""; // Handle empty string
42
+ return str[0].toUpperCase() + str.slice(1).toLowerCase();
43
+ }
44
+
45
+ function formatPlaywrightError(error) {
46
+ // Get the error message and clean ANSI codes
47
+ const rawMessage = error.stack || error.message || error.toString();
48
+ const cleanMessage = rawMessage.replace(/\x1B\[[0-9;]*[mGKH]/g, "");
49
+
50
+ // Parse error components
51
+ const timeoutMatch = cleanMessage.match(/Timed out (\d+)ms waiting for/);
52
+ const assertionMatch = cleanMessage.match(/expect\((.*?)\)\.(.*?)\(/);
53
+ const expectedMatch = cleanMessage.match(
54
+ /Expected (?:pattern|string|regexp|value): (.*?)(?:\n|Call log|$)/s
55
+ );
56
+ const actualMatch = cleanMessage.match(
57
+ /Received (?:string|value): (.*?)(?:\n|Call log|$)/s
58
+ );
59
+
60
+ // HTML escape function
61
+ const escapeHtml = (str) => {
62
+ if (!str) return "";
63
+ return str.replace(
64
+ /[&<>'"]/g,
65
+ (tag) =>
66
+ ({
67
+ "&": "&amp;",
68
+ "<": "&lt;",
69
+ ">": "&gt;",
70
+ "'": "&#39;",
71
+ '"': "&quot;",
72
+ }[tag])
73
+ );
74
+ };
75
+
76
+ // Build HTML output
77
+ let html = `<div class="playwright-error">
78
+ <div class="error-header">Test Error</div>`;
79
+
80
+ if (timeoutMatch) {
81
+ html += `<div class="error-timeout">⏱ Timeout: ${escapeHtml(
82
+ timeoutMatch[1]
83
+ )}ms</div>`;
84
+ }
85
+
86
+ if (assertionMatch) {
87
+ html += `<div class="error-assertion">🔍 Assertion: expect(${escapeHtml(
88
+ assertionMatch[1]
89
+ )}).${escapeHtml(assertionMatch[2])}()</div>`;
90
+ }
91
+
92
+ if (expectedMatch) {
93
+ html += `<div class="error-expected">✅ Expected: ${escapeHtml(
94
+ expectedMatch[1]
95
+ )}</div>`;
96
+ }
97
+
98
+ if (actualMatch) {
99
+ html += `<div class="error-actual">❌ Actual: ${escapeHtml(
100
+ actualMatch[1]
101
+ )}</div>`;
102
+ }
103
+
104
+ // Add call log if present
105
+ const callLogStart = cleanMessage.indexOf("Call log:");
106
+ if (callLogStart !== -1) {
107
+ const callLogEnd =
108
+ cleanMessage.indexOf("\n\n", callLogStart) || cleanMessage.length;
109
+ const callLogSection = cleanMessage
110
+ .slice(callLogStart + 9, callLogEnd)
111
+ .trim();
112
+
113
+ html += `<div class="error-call-log">
114
+ <div class="call-log-header">📜 Call Log:</div>
115
+ <ul class="call-log-items">${callLogSection
116
+ .split("\n")
117
+ .map((line) => line.trim())
118
+ .filter((line) => line)
119
+ .map((line) => `<li>${escapeHtml(line.replace(/^-\s*/, ""))}</li>`)
120
+ .join("")}</ul>
121
+ </div>`;
122
+ }
123
+
124
+ // Add stack trace if present
125
+ const stackTraceMatch = cleanMessage.match(/\n\s*at\s.*/gs);
126
+ if (stackTraceMatch) {
127
+ html += `<div class="error-stack-trace">
128
+ <div class="stack-trace-header">🔎 Stack Trace:</div>
129
+ <ul class="stack-trace-items">${stackTraceMatch[0]
130
+ .trim()
131
+ .split("\n")
132
+ .map((line) => line.trim())
133
+ .filter((line) => line)
134
+ .map((line) => `<li>${escapeHtml(line)}</li>`)
135
+ .join("")}</ul>
136
+ </div>`;
137
+ }
138
+
139
+ html += `</div>`;
140
+
141
+ return html;
37
142
  }
38
143
 
144
+ // User-provided formatDuration function
39
145
  function formatDuration(ms) {
40
146
  if (ms === undefined || ms === null || ms < 0) return "0.0s";
41
147
  return (ms / 1000).toFixed(1) + "s";
42
148
  }
43
149
 
150
+ function generateTestTrendsChart(trendData) {
151
+ if (!trendData || !trendData.overall || trendData.overall.length === 0) {
152
+ return '<div class="no-data">No overall trend data available for test counts.</div>';
153
+ }
154
+
155
+ const chartId = `testTrendsChart-${Date.now()}-${Math.random()
156
+ .toString(36)
157
+ .substring(2, 7)}`;
158
+ const runs = trendData.overall;
159
+
160
+ const series = [
161
+ {
162
+ name: "Total",
163
+ data: runs.map((r) => r.totalTests),
164
+ color: "var(--primary-color)", // Blue
165
+ marker: { symbol: "circle" },
166
+ },
167
+ {
168
+ name: "Passed",
169
+ data: runs.map((r) => r.passed),
170
+ color: "var(--success-color)", // Green
171
+ marker: { symbol: "circle" },
172
+ },
173
+ {
174
+ name: "Failed",
175
+ data: runs.map((r) => r.failed),
176
+ color: "var(--danger-color)", // Red
177
+ marker: { symbol: "circle" },
178
+ },
179
+ {
180
+ name: "Skipped",
181
+ data: runs.map((r) => r.skipped || 0),
182
+ color: "var(--warning-color)", // Yellow
183
+ marker: { symbol: "circle" },
184
+ },
185
+ ];
186
+
187
+ // Data needed by the tooltip formatter, stringified to be embedded in the client-side script
188
+ const runsForTooltip = runs.map((r) => ({
189
+ runId: r.runId,
190
+ timestamp: r.timestamp,
191
+ duration: r.duration,
192
+ }));
193
+
194
+ const optionsObjectString = `
195
+ {
196
+ chart: { type: "line", height: 350, backgroundColor: "transparent" },
197
+ title: { text: null },
198
+ xAxis: {
199
+ categories: ${JSON.stringify(runs.map((run, i) => `Run ${i + 1}`))},
200
+ crosshair: true,
201
+ labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}
202
+ },
203
+ yAxis: {
204
+ title: { text: "Test Count", style: { color: 'var(--text-color)'} },
205
+ min: 0,
206
+ labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' }}
207
+ },
208
+ legend: {
209
+ layout: "horizontal", align: "center", verticalAlign: "bottom",
210
+ itemStyle: { fontSize: "12px", color: 'var(--text-color)' }
211
+ },
212
+ plotOptions: {
213
+ series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}},
214
+ line: { lineWidth: 2.5 } // fillOpacity was 0.1, but for line charts, area fill is usually separate (area chart type)
215
+ },
216
+ tooltip: {
217
+ shared: true, useHTML: true,
218
+ backgroundColor: 'rgba(10,10,10,0.92)',
219
+ borderColor: 'rgba(10,10,10,0.92)',
220
+ style: { color: '#f5f5f5' },
221
+ formatter: function () {
222
+ const runsData = ${JSON.stringify(runsForTooltip)};
223
+ const pointIndex = this.points[0].point.x; // Get index from point
224
+ const run = runsData[pointIndex];
225
+ let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' +
226
+ 'Date: ' + new Date(run.timestamp).toLocaleString() + '<br><br>';
227
+ this.points.forEach(point => {
228
+ tooltip += '<span style="color:' + point.color + '">●</span> ' + point.series.name + ': <b>' + point.y + '</b><br>';
229
+ });
230
+ tooltip += '<br>Duration: ' + formatDuration(run.duration);
231
+ return tooltip;
232
+ }
233
+ },
234
+ series: ${JSON.stringify(series)},
235
+ credits: { enabled: false }
236
+ }
237
+ `;
238
+
239
+ return `
240
+ <div id="${chartId}" class="trend-chart-container"></div>
241
+ <script>
242
+ document.addEventListener('DOMContentLoaded', function() {
243
+ if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
244
+ try {
245
+ const chartOptions = ${optionsObjectString};
246
+ Highcharts.chart('${chartId}', chartOptions);
247
+ } catch (e) {
248
+ console.error("Error rendering chart ${chartId}:", e);
249
+ document.getElementById('${chartId}').innerHTML = '<div class="no-data">Error rendering test trends chart.</div>';
250
+ }
251
+ } else {
252
+ document.getElementById('${chartId}').innerHTML = '<div class="no-data">Charting library not available.</div>';
253
+ }
254
+ });
255
+ </script>
256
+ `;
257
+ }
258
+
259
+ function generateDurationTrendChart(trendData) {
260
+ if (!trendData || !trendData.overall || trendData.overall.length === 0) {
261
+ return '<div class="no-data">No overall trend data available for durations.</div>';
262
+ }
263
+ const chartId = `durationTrendChart-${Date.now()}-${Math.random()
264
+ .toString(36)
265
+ .substring(2, 7)}`;
266
+ const runs = trendData.overall;
267
+
268
+ // Assuming var(--accent-color-alt) is Orange #FF9800
269
+ const accentColorAltRGB = "255, 152, 0";
270
+
271
+ const seriesString = `[{
272
+ name: 'Duration',
273
+ data: ${JSON.stringify(runs.map((run) => run.duration))},
274
+ color: 'var(--accent-color-alt)',
275
+ type: 'area',
276
+ marker: {
277
+ symbol: 'circle', enabled: true, radius: 4,
278
+ states: { hover: { radius: 6, lineWidthPlus: 0 } }
279
+ },
280
+ fillColor: {
281
+ linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
282
+ stops: [
283
+ [0, 'rgba(${accentColorAltRGB}, 0.4)'],
284
+ [1, 'rgba(${accentColorAltRGB}, 0.05)']
285
+ ]
286
+ },
287
+ lineWidth: 2.5
288
+ }]`;
289
+
290
+ const runsForTooltip = runs.map((r) => ({
291
+ runId: r.runId,
292
+ timestamp: r.timestamp,
293
+ duration: r.duration,
294
+ totalTests: r.totalTests,
295
+ }));
296
+
297
+ const optionsObjectString = `
298
+ {
299
+ chart: { type: 'area', height: 350, backgroundColor: 'transparent' },
300
+ title: { text: null },
301
+ xAxis: {
302
+ categories: ${JSON.stringify(runs.map((run, i) => `Run ${i + 1}`))},
303
+ crosshair: true,
304
+ labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' } }
305
+ },
306
+ yAxis: {
307
+ title: { text: 'Duration', style: { color: 'var(--text-color)' } },
308
+ labels: {
309
+ formatter: function() { return formatDuration(this.value); },
310
+ style: { color: 'var(--text-color-secondary)', fontSize: '12px' }
311
+ },
312
+ min: 0
313
+ },
314
+ legend: {
315
+ layout: 'horizontal', align: 'center', verticalAlign: 'bottom',
316
+ itemStyle: { fontSize: '12px', color: 'var(--text-color)' }
317
+ },
318
+ plotOptions: {
319
+ area: {
320
+ lineWidth: 2.5,
321
+ states: { hover: { lineWidthPlus: 0 } },
322
+ threshold: null
323
+ }
324
+ },
325
+ tooltip: {
326
+ shared: true, useHTML: true,
327
+ backgroundColor: 'rgba(10,10,10,0.92)',
328
+ borderColor: 'rgba(10,10,10,0.92)',
329
+ style: { color: '#f5f5f5' },
330
+ formatter: function () {
331
+ const runsData = ${JSON.stringify(runsForTooltip)};
332
+ const pointIndex = this.points[0].point.x;
333
+ const run = runsData[pointIndex];
334
+ let tooltip = '<strong>Run ' + (run.runId || pointIndex + 1) + '</strong><br>' +
335
+ 'Date: ' + new Date(run.timestamp).toLocaleString() + '<br>';
336
+ this.points.forEach(point => {
337
+ tooltip += '<span style="color:' + point.series.color + '">●</span> ' +
338
+ point.series.name + ': <b>' + formatDuration(point.y) + '</b><br>';
339
+ });
340
+ tooltip += '<br>Tests: ' + run.totalTests;
341
+ return tooltip;
342
+ }
343
+ },
344
+ series: ${seriesString},
345
+ credits: { enabled: false }
346
+ }
347
+ `;
348
+
349
+ return `
350
+ <div id="${chartId}" class="trend-chart-container"></div>
351
+ <script>
352
+ document.addEventListener('DOMContentLoaded', function() {
353
+ if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
354
+ try {
355
+ const chartOptions = ${optionsObjectString};
356
+ Highcharts.chart('${chartId}', chartOptions);
357
+ } catch (e) {
358
+ console.error("Error rendering chart ${chartId}:", e);
359
+ document.getElementById('${chartId}').innerHTML = '<div class="no-data">Error rendering duration trend chart.</div>';
360
+ }
361
+ } else {
362
+ document.getElementById('${chartId}').innerHTML = '<div class="no-data">Charting library not available.</div>';
363
+ }
364
+ });
365
+ </script>
366
+ `;
367
+ }
368
+
44
369
  function formatDate(dateStrOrDate) {
45
370
  if (!dateStrOrDate) return "N/A";
46
371
  try {
47
372
  const date = new Date(dateStrOrDate);
48
373
  if (isNaN(date.getTime())) return "Invalid Date";
49
- return date.toLocaleString();
374
+ return (
375
+ date.toLocaleDateString(undefined, {
376
+ year: "2-digit",
377
+ month: "2-digit",
378
+ day: "2-digit",
379
+ }) +
380
+ " " +
381
+ date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
382
+ );
50
383
  } catch (e) {
51
- return "Invalid Date";
384
+ return "Invalid Date Format";
385
+ }
386
+ }
387
+
388
+ function generateTestHistoryChart(history) {
389
+ if (!history || history.length === 0)
390
+ return '<div class="no-data-chart">No data for chart</div>';
391
+
392
+ const validHistory = history.filter(
393
+ (h) => h && typeof h.duration === "number" && h.duration >= 0
394
+ );
395
+ if (validHistory.length === 0)
396
+ return '<div class="no-data-chart">No valid data for chart</div>';
397
+
398
+ const chartId = `testHistoryChart-${Date.now()}-${Math.random()
399
+ .toString(36)
400
+ .substring(2, 7)}`;
401
+
402
+ const seriesDataPoints = validHistory.map((run) => {
403
+ let color;
404
+ switch (String(run.status).toLowerCase()) {
405
+ case "passed":
406
+ color = "var(--success-color)";
407
+ break;
408
+ case "failed":
409
+ color = "var(--danger-color)";
410
+ break;
411
+ case "skipped":
412
+ color = "var(--warning-color)";
413
+ break;
414
+ default:
415
+ color = "var(--dark-gray-color)";
416
+ }
417
+ return {
418
+ y: run.duration,
419
+ marker: {
420
+ fillColor: color,
421
+ symbol: "circle",
422
+ radius: 3.5,
423
+ states: { hover: { radius: 5 } },
424
+ },
425
+ status: run.status,
426
+ runId: run.runId,
427
+ };
428
+ });
429
+
430
+ // Assuming var(--accent-color) is Deep Purple #673ab7 -> RGB 103, 58, 183
431
+ const accentColorRGB = "103, 58, 183";
432
+
433
+ const optionsObjectString = `
434
+ {
435
+ chart: { type: 'area', height: 100, width: 320, backgroundColor: 'transparent', spacing: [10,10,15,35] },
436
+ title: { text: null },
437
+ xAxis: {
438
+ categories: ${JSON.stringify(
439
+ validHistory.map((_, i) => `R${i + 1}`)
440
+ )},
441
+ labels: { style: { fontSize: '10px', color: 'var(--text-color-secondary)' } }
442
+ },
443
+ yAxis: {
444
+ title: { text: null },
445
+ labels: {
446
+ formatter: function() { return formatDuration(this.value); },
447
+ style: { fontSize: '10px', color: 'var(--text-color-secondary)' },
448
+ align: 'left', x: -35, y: 3
449
+ },
450
+ min: 0,
451
+ gridLineWidth: 0,
452
+ tickAmount: 4
453
+ },
454
+ legend: { enabled: false },
455
+ plotOptions: {
456
+ area: {
457
+ lineWidth: 2,
458
+ lineColor: 'var(--accent-color)',
459
+ fillColor: {
460
+ linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
461
+ stops: [
462
+ [0, 'rgba(${accentColorRGB}, 0.4)'],
463
+ [1, 'rgba(${accentColorRGB}, 0)']
464
+ ]
465
+ },
466
+ marker: { enabled: true },
467
+ threshold: null
468
+ }
469
+ },
470
+ tooltip: {
471
+ useHTML: true,
472
+ backgroundColor: 'rgba(10,10,10,0.92)',
473
+ borderColor: 'rgba(10,10,10,0.92)',
474
+ style: { color: '#f5f5f5', padding: '8px' },
475
+ formatter: function() {
476
+ const pointData = this.point;
477
+ let statusBadgeHtml = '<span style="padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; background-color: ';
478
+ switch(String(pointData.status).toLowerCase()) {
479
+ case 'passed': statusBadgeHtml += 'var(--success-color)'; break;
480
+ case 'failed': statusBadgeHtml += 'var(--danger-color)'; break;
481
+ case 'skipped': statusBadgeHtml += 'var(--warning-color)'; break;
482
+ default: statusBadgeHtml += 'var(--dark-gray-color)';
483
+ }
484
+ statusBadgeHtml += ';">' + String(pointData.status).toUpperCase() + '</span>';
485
+
486
+ return '<strong>Run ' + (pointData.runId || (this.point.index + 1)) + '</strong><br>' +
487
+ 'Status: ' + statusBadgeHtml + '<br>' +
488
+ 'Duration: ' + formatDuration(pointData.y);
489
+ }
490
+ },
491
+ series: [{
492
+ data: ${JSON.stringify(seriesDataPoints)},
493
+ showInLegend: false
494
+ }],
495
+ credits: { enabled: false }
496
+ }
497
+ `;
498
+ return `
499
+ <div id="${chartId}" style="width: 320px; height: 100px;"></div>
500
+ <script>
501
+ document.addEventListener('DOMContentLoaded', function() {
502
+ if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
503
+ try {
504
+ const chartOptions = ${optionsObjectString};
505
+ Highcharts.chart('${chartId}', chartOptions);
506
+ } catch (e) {
507
+ console.error("Error rendering chart ${chartId}:", e);
508
+ document.getElementById('${chartId}').innerHTML = '<div class="no-data-chart">Error rendering history chart.</div>';
509
+ }
510
+ } else {
511
+ document.getElementById('${chartId}').innerHTML = '<div class="no-data-chart">Charting library not available.</div>';
512
+ }
513
+ });
514
+ </script>
515
+ `;
516
+ }
517
+
518
+ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
519
+ const total = data.reduce((sum, d) => sum + d.value, 0);
520
+ if (total === 0) {
521
+ return '<div class="pie-chart-wrapper"><h3>Test Distribution</h3><div class="no-data">No data for Test Distribution chart.</div></div>';
522
+ }
523
+ const passedEntry = data.find((d) => d.label === "Passed");
524
+ const passedPercentage = Math.round(
525
+ ((passedEntry ? passedEntry.value : 0) / total) * 100
526
+ );
527
+
528
+ const chartId = `pieChart-${Date.now()}-${Math.random()
529
+ .toString(36)
530
+ .substring(2, 7)}`;
531
+
532
+ const seriesData = [
533
+ {
534
+ name: "Tests", // Changed from 'Test Distribution' for tooltip clarity
535
+ data: data
536
+ .filter((d) => d.value > 0)
537
+ .map((d) => {
538
+ let color;
539
+ switch (d.label) {
540
+ case "Passed":
541
+ color = "var(--success-color)";
542
+ break;
543
+ case "Failed":
544
+ color = "var(--danger-color)";
545
+ break;
546
+ case "Skipped":
547
+ color = "var(--warning-color)";
548
+ break;
549
+ default:
550
+ color = "#CCCCCC"; // A neutral default color
551
+ }
552
+ return { name: d.label, y: d.value, color: color };
553
+ }),
554
+ size: "100%",
555
+ innerSize: "55%",
556
+ dataLabels: { enabled: false },
557
+ showInLegend: true,
558
+ },
559
+ ];
560
+
561
+ // Approximate font size for center text, can be adjusted or made dynamic with more client-side JS
562
+ const centerTitleFontSize =
563
+ Math.max(12, Math.min(chartWidth, chartHeight) / 12) + "px";
564
+ const centerSubtitleFontSize =
565
+ Math.max(10, Math.min(chartWidth, chartHeight) / 18) + "px";
566
+
567
+ const optionsObjectString = `
568
+ {
569
+ chart: {
570
+ type: 'pie',
571
+ width: ${chartWidth},
572
+ height: ${
573
+ chartHeight - 40
574
+ }, // Adjusted height to make space for legend if chartHeight is for the whole wrapper
575
+ backgroundColor: 'transparent',
576
+ plotShadow: false,
577
+ spacingBottom: 40 // Ensure space for legend
578
+ },
579
+ title: {
580
+ text: '${passedPercentage}%',
581
+ align: 'center',
582
+ verticalAlign: 'middle',
583
+ y: 5,
584
+ style: { fontSize: '${centerTitleFontSize}', fontWeight: 'bold', color: 'var(--primary-color)' }
585
+ },
586
+ subtitle: {
587
+ text: 'Passed',
588
+ align: 'center',
589
+ verticalAlign: 'middle',
590
+ y: 25,
591
+ style: { fontSize: '${centerSubtitleFontSize}', color: 'var(--text-color-secondary)' }
592
+ },
593
+ tooltip: {
594
+ pointFormat: '{series.name}: <b>{point.percentage:.1f}%</b> ({point.y})',
595
+ backgroundColor: 'rgba(10,10,10,0.92)',
596
+ borderColor: 'rgba(10,10,10,0.92)',
597
+ style: { color: '#f5f5f5' }
598
+ },
599
+ legend: {
600
+ layout: 'horizontal',
601
+ align: 'center',
602
+ verticalAlign: 'bottom',
603
+ itemStyle: { color: 'var(--text-color)', fontWeight: 'normal', fontSize: '12px' }
604
+ },
605
+ plotOptions: {
606
+ pie: {
607
+ allowPointSelect: true,
608
+ cursor: 'pointer',
609
+ borderWidth: 3,
610
+ borderColor: 'var(--card-background-color)', // Match D3 style
611
+ states: {
612
+ hover: {
613
+ // Using default Highcharts halo which is generally good
614
+ }
615
+ }
616
+ }
617
+ },
618
+ series: ${JSON.stringify(seriesData)},
619
+ credits: { enabled: false }
620
+ }
621
+ `;
622
+
623
+ return `
624
+ <div class="pie-chart-wrapper" style="align-items: center">
625
+ <div style="display: flex; align-items: start; width: 100%;"><h3>Test Distribution</h3></div>
626
+ <div id="${chartId}" style="width: ${chartWidth}px; height: ${
627
+ chartHeight - 40
628
+ }px;"></div>
629
+ <script>
630
+ document.addEventListener('DOMContentLoaded', function() {
631
+ if (typeof Highcharts !== 'undefined') {
632
+ try {
633
+ const chartOptions = ${optionsObjectString};
634
+ Highcharts.chart('${chartId}', chartOptions);
635
+ } catch (e) {
636
+ console.error("Error rendering chart ${chartId}:", e);
637
+ document.getElementById('${chartId}').innerHTML = '<div class="no-data">Error rendering pie chart.</div>';
638
+ }
639
+ } else {
640
+ document.getElementById('${chartId}').innerHTML = '<div class="no-data">Charting library not available.</div>';
641
+ }
642
+ });
643
+ </script>
644
+ </div>
645
+ `;
646
+ }
647
+
648
+ function generateTestHistoryContent(trendData) {
649
+ if (
650
+ !trendData ||
651
+ !trendData.testRuns ||
652
+ Object.keys(trendData.testRuns).length === 0
653
+ ) {
654
+ return '<div class="no-data">No historical test data available.</div>';
655
+ }
656
+
657
+ const allTestNamesAndPaths = new Map();
658
+ Object.values(trendData.testRuns).forEach((run) => {
659
+ if (Array.isArray(run)) {
660
+ run.forEach((test) => {
661
+ if (test && test.testName && !allTestNamesAndPaths.has(test.testName)) {
662
+ const parts = test.testName.split(" > ");
663
+ const title = parts[parts.length - 1];
664
+ allTestNamesAndPaths.set(test.testName, title);
665
+ }
666
+ });
667
+ }
668
+ });
669
+
670
+ if (allTestNamesAndPaths.size === 0) {
671
+ return '<div class="no-data">No historical test data found after processing.</div>';
52
672
  }
673
+
674
+ const testHistory = Array.from(allTestNamesAndPaths.entries())
675
+ .map(([fullTestName, testTitle]) => {
676
+ const history = [];
677
+ (trendData.overall || []).forEach((overallRun, index) => {
678
+ const runKey = overallRun.runId
679
+ ? `test run ${overallRun.runId}`
680
+ : `test run ${index + 1}`;
681
+ const testRunForThisOverallRun = trendData.testRuns[runKey]?.find(
682
+ (t) => t && t.testName === fullTestName
683
+ );
684
+ if (testRunForThisOverallRun) {
685
+ history.push({
686
+ runId: overallRun.runId || index + 1,
687
+ status: testRunForThisOverallRun.status || "unknown",
688
+ duration: testRunForThisOverallRun.duration || 0,
689
+ timestamp:
690
+ testRunForThisOverallRun.timestamp ||
691
+ overallRun.timestamp ||
692
+ new Date(),
693
+ });
694
+ }
695
+ });
696
+ return { fullTestName, testTitle, history };
697
+ })
698
+ .filter((item) => item.history.length > 0);
699
+
700
+ return `
701
+ <div class="test-history-container">
702
+ <div class="filters" style="border-color: black; border-style: groove;">
703
+ <input type="text" id="history-filter-name" placeholder="Search by test title..." style="border-color: black; border-style: outset;">
704
+ <select id="history-filter-status">
705
+ <option value="">All Statuses</option>
706
+ <option value="passed">Passed</option>
707
+ <option value="failed">Failed</option>
708
+ <option value="skipped">Skipped</option>
709
+ </select>
710
+ <button id="clear-history-filters" class="clear-filters-btn">Clear Filters</button>
711
+ </div>
712
+
713
+ <div class="test-history-grid">
714
+ ${testHistory
715
+ .map((test) => {
716
+ const latestRun =
717
+ test.history.length > 0
718
+ ? test.history[test.history.length - 1]
719
+ : { status: "unknown" };
720
+ return `
721
+ <div class="test-history-card" data-test-name="${sanitizeHTML(
722
+ test.testTitle.toLowerCase()
723
+ )}" data-latest-status="${latestRun.status}">
724
+ <div class="test-history-header">
725
+ <p title="${sanitizeHTML(test.testTitle)}">${capitalize(
726
+ sanitizeHTML(test.testTitle)
727
+ )}</p>
728
+ <span class="status-badge ${getStatusClass(latestRun.status)}">
729
+ ${String(latestRun.status).toUpperCase()}
730
+ </span>
731
+ </div>
732
+ <div class="test-history-trend">
733
+ ${generateTestHistoryChart(test.history)}
734
+ </div>
735
+ <details class="test-history-details-collapsible">
736
+ <summary>Show Run Details (${test.history.length})</summary>
737
+ <div class="test-history-details">
738
+ <table>
739
+ <thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Date</th></tr></thead>
740
+ <tbody>
741
+ ${test.history
742
+ .slice()
743
+ .reverse()
744
+ .map(
745
+ (run) => `
746
+ <tr>
747
+ <td>${run.runId}</td>
748
+ <td><span class="status-badge-small ${getStatusClass(
749
+ run.status
750
+ )}">${String(run.status).toUpperCase()}</span></td>
751
+ <td>${formatDuration(run.duration)}</td>
752
+ <td>${formatDate(run.timestamp)}</td>
753
+ </tr>`
754
+ )
755
+ .join("")}
756
+ </tbody>
757
+ </table>
758
+ </div>
759
+ </details>
760
+ </div>`;
761
+ })
762
+ .join("")}
763
+ </div>
764
+ </div>
765
+ `;
53
766
  }
54
767
 
55
768
  function getStatusClass(status) {
56
- switch (status) {
769
+ switch (String(status).toLowerCase()) {
57
770
  case "passed":
58
771
  return "status-passed";
59
772
  case "failed":
@@ -61,12 +774,12 @@ function getStatusClass(status) {
61
774
  case "skipped":
62
775
  return "status-skipped";
63
776
  default:
64
- return "";
777
+ return "status-unknown";
65
778
  }
66
779
  }
67
780
 
68
781
  function getStatusIcon(status) {
69
- switch (status) {
782
+ switch (String(status).toLowerCase()) {
70
783
  case "passed":
71
784
  return "✅";
72
785
  case "failed":
@@ -78,1146 +791,815 @@ function getStatusIcon(status) {
78
791
  }
79
792
  }
80
793
 
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
794
  function getSuitesData(results) {
221
795
  const suitesMap = new Map();
796
+ if (!results || results.length === 0) return [];
222
797
 
223
798
  results.forEach((test) => {
224
- const browser = test.browser; // Extract browser (chromium/firefox/webkit)
225
- const suiteName = test.suiteName;
799
+ const browser = test.browser || "unknown";
800
+ const suiteParts = test.name.split(" > ");
801
+ let suiteNameCandidate = "Default Suite";
802
+ if (suiteParts.length > 2) {
803
+ suiteNameCandidate = suiteParts[1];
804
+ } else if (suiteParts.length > 1) {
805
+ suiteNameCandidate = suiteParts[0]
806
+ .split(path.sep)
807
+ .pop()
808
+ .replace(/\.(spec|test)\.(ts|js|mjs|cjs)$/, "");
809
+ } else {
810
+ suiteNameCandidate = test.name
811
+ .split(path.sep)
812
+ .pop()
813
+ .replace(/\.(spec|test)\.(ts|js|mjs|cjs)$/, "");
814
+ }
815
+ const suiteName = suiteNameCandidate;
226
816
  const key = `${suiteName}|${browser}`;
227
817
 
228
818
  if (!suitesMap.has(key)) {
229
819
  suitesMap.set(key, {
230
- id: test.id,
231
- name: `${suiteName} (${browser})`,
232
- status: test.status,
233
- count: 0,
820
+ id: test.id || key,
821
+ name: suiteName,
234
822
  browser: browser,
823
+ passed: 0,
824
+ failed: 0,
825
+ skipped: 0,
826
+ count: 0,
827
+ statusOverall: "passed",
235
828
  });
236
829
  }
237
- suitesMap.get(key).count++;
238
- });
830
+ const suite = suitesMap.get(key);
831
+ suite.count++;
832
+ const currentStatus = String(test.status).toLowerCase();
833
+ if (currentStatus && suite[currentStatus] !== undefined) {
834
+ suite[currentStatus]++;
835
+ }
239
836
 
837
+ if (currentStatus === "failed") {
838
+ suite.statusOverall = "failed";
839
+ } else if (
840
+ currentStatus === "skipped" &&
841
+ suite.statusOverall !== "failed"
842
+ ) {
843
+ suite.statusOverall = "skipped";
844
+ }
845
+ });
240
846
  return Array.from(suitesMap.values());
241
847
  }
242
848
 
243
- // Generate suites widget (updated for your data)
244
849
  function generateSuitesWidget(suitesData) {
850
+ if (!suitesData || suitesData.length === 0) {
851
+ return `<div class="suites-widget"><div class="suites-header"><h2>Test Suites</h2></div><div class="no-data">No suite data available.</div></div>`;
852
+ }
245
853
  return `
246
854
  <div class="suites-widget">
247
855
  <div class="suites-header">
248
856
  <h2>Test Suites</h2>
249
- <div class="summary-badge">
857
+ <span class="summary-badge">
250
858
  ${suitesData.length} suites • ${suitesData.reduce(
251
859
  (sum, suite) => sum + suite.count,
252
860
  0
253
861
  )} tests
254
- </div>
862
+ </span>
255
863
  </div>
256
-
257
864
  <div class="suites-grid">
258
865
  ${suitesData
259
866
  .map(
260
867
  (suite) => `
261
- <div class="suite-card ${suite.status}">
262
- <div class="suite-meta">
263
- <span class="browser-tag">${suite.name
264
- .split("(")
265
- .pop()
266
- .replace(")", "")}</span>
267
- <span class="test-count">${suite.count} test${
868
+ <div class="suite-card status-${suite.statusOverall}">
869
+ <div class="suite-card-header">
870
+ <h3 class="suite-name" title="${sanitizeHTML(
871
+ suite.name
872
+ )} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
873
+ <span class="browser-tag">${sanitizeHTML(suite.browser)}</span>
874
+ </div>
875
+ <div class="suite-card-body">
876
+ <span class="test-count">${suite.count} test${
268
877
  suite.count !== 1 ? "s" : ""
269
878
  }</span>
879
+ <div class="suite-stats">
880
+ ${
881
+ suite.passed > 0
882
+ ? `<span class="stat-passed" title="Passed"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/></svg> ${suite.passed}</span>`
883
+ : ""
884
+ }
885
+ ${
886
+ suite.failed > 0
887
+ ? `<span class="stat-failed" title="Failed"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/></svg> ${suite.failed}</span>`
888
+ : ""
889
+ }
890
+ ${
891
+ suite.skipped > 0
892
+ ? `<span class="stat-skipped" title="Skipped"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-exclamation-triangle-fill" viewBox="0 0 16 16"><path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/></svg> ${suite.skipped}</span>`
893
+ : ""
894
+ }
895
+ </div>
270
896
  </div>
271
- <span class="browser-name">${suite.browser}</span>
272
- </div>
273
- `
897
+ </div>`
274
898
  )
275
899
  .join("")}
276
900
  </div>
277
-
278
- <style>
279
- .suites-widget {
280
- background: linear-gradient(to bottom right, #f0f4ff, #ffffff);
281
- border-radius: 16px;
282
- padding: 10px;
283
- box-shadow: 0 4px 20px rgba(0,0,0,0.05);
284
- font-family: 'Segoe UI', Roboto, sans-serif;
285
- height: 100%;
286
- }
287
- span.browser-name {
288
- background-color: #265685;
289
- font-size: 0.875rem;
290
- color: #fff;
291
- padding: 3px;
292
- border-radius: 4px;
293
- }
294
-
295
- .suites-header {
296
- display: flex;
297
- align-items: center;
298
- gap: 16px;
299
- margin-bottom: 24px;
300
- }
301
-
302
- .suites-header h2 {
303
- font-size: 20px;
304
- font-weight: 600;
305
- margin: 0;
306
- color: #1a202c;
307
- }
308
-
309
- .summary-badge {
310
- background: #f8fafc;
311
- color: #64748b;
312
- padding: 4px 12px;
313
- border-radius: 12px;
314
- font-size: 14px;
315
- }
316
-
317
- .suites-grid {
318
- display: grid;
319
- grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
320
- gap: 16px;
321
- }
322
-
323
- .suite-card {
324
- background: #e6e6e6;
325
- border-radius: 12px;
326
- padding: 18px;
327
- border: 1px solid #f1f5f9;
328
- transition: all 0.2s ease;
329
- }
330
-
331
- .suite-card:hover {
332
- transform: translateY(-2px);
333
- box-shadow: 0 6px 12px rgba(0,0,0,0.08);
334
- border-color: #e2e8f0;
335
- }
336
-
337
- .suite-meta {
338
- display: flex;
339
- justify-content: space-between;
340
- align-items: center;
341
- margin-bottom: 12px;
342
- }
343
-
344
- .browser-tag {
345
- font-size: 12px;
346
- font-weight: 600;
347
- color: #64748b;
348
- text-transform: uppercase;
349
- letter-spacing: 0.5px;
350
- }
351
-
352
- .status-indicator {
353
- width: 12px;
354
- height: 12px;
355
- border-radius: 50%;
356
- }
357
-
358
- .status-indicator.passed {
359
- background: #2a9c68;
360
- box-shadow: 0 0 0 3px #ecfdf5;
361
- }
362
-
363
- .status-indicator.failed {
364
- background: #ef4444;
365
- box-shadow: 0 0 0 3px #fef2f2;
366
- }
367
-
368
- .status-indicator.skipped {
369
- background: #f59e0b;
370
- box-shadow: 0 0 0 3px #fffbeb;
371
- }
372
-
373
- .suite-card h3 {
374
- font-size: 16px;
375
- margin: 0 0 16px 0;
376
- color: #1e293b;
377
- white-space: nowrap;
378
- overflow: hidden;
379
- text-overflow: ellipsis;
380
- }
381
-
382
- .test-visualization {
383
- display: flex;
384
- align-items: center;
385
- gap: 12px;
386
- }
387
-
388
- .test-dots {
389
- padding: 4px;
390
- display: flex;
391
- flex-wrap: wrap;
392
- gap: 6px;
393
- flex-grow: 1;
394
- }
395
-
396
- .test-dot {
397
- width: 10px;
398
- height: 10px;
399
- border-radius: 50%;
400
- }
401
-
402
- .test-dot.passed {
403
- background: #2a9c68;
404
- }
405
-
406
- .test-dot.failed {
407
- background: #ef4444;
408
- }
409
-
410
- .test-dot.skipped {
411
- background: #f59e0b;
412
- }
413
-
414
- .test-count {
415
- font-size: 14px;
416
- color: #64748b;
417
- min-width: 60px;
418
- text-align: right;
419
- }
420
- </style>
421
- </div>
422
- `;
901
+ </div>`;
423
902
  }
424
903
 
425
- // Enhanced HTML generation with properly integrated CSS and JS
426
- function generateHTML(reportData) {
904
+ function generateHTML(reportData, trendData = null) {
427
905
  const { run, results } = reportData;
428
- const suitesData = getSuitesData(reportData.results);
906
+ const suitesData = getSuitesData(reportData.results || []);
429
907
  const runSummary = run || {
430
908
  totalTests: 0,
431
909
  passed: 0,
432
910
  failed: 0,
433
911
  skipped: 0,
434
912
  duration: 0,
435
- timestamp: new Date(),
913
+ timestamp: new Date().toISOString(),
436
914
  };
437
915
 
438
- // Calculate additional metrics
439
- const passPercentage =
440
- runSummary.totalTests > 0
441
- ? Math.round((runSummary.passed / runSummary.totalTests) * 100)
442
- : 0;
443
- const failPercentage =
444
- runSummary.totalTests > 0
445
- ? Math.round((runSummary.failed / runSummary.totalTests) * 100)
446
- : 0;
447
- const skipPercentage =
448
- runSummary.totalTests > 0
449
- ? Math.round((runSummary.skipped / runSummary.totalTests) * 100)
450
- : 0;
916
+ const totalTestsOr1 = runSummary.totalTests || 1; // Avoid division by zero
917
+ const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
918
+ const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
919
+ const skipPercentage = Math.round(
920
+ ((runSummary.skipped || 0) / totalTestsOr1) * 100
921
+ );
451
922
  const avgTestDuration =
452
923
  runSummary.totalTests > 0
453
924
  ? formatDuration(runSummary.duration / runSummary.totalTests)
454
925
  : "0.0s";
455
926
 
456
- // Generate test cases HTML
457
- const generateTestCasesHTML = () => {
927
+ function generateTestCasesHTML() {
458
928
  if (!results || results.length === 0) {
459
- return '<div class="no-tests">No test results found</div>';
929
+ return '<div class="no-tests">No test results found in this run.</div>';
460
930
  }
461
931
 
462
- // Collect all unique tags and browsers
463
- const allTags = new Set();
464
- const allBrowsers = new Set();
465
-
466
- results.forEach((test) => {
467
- (test.tags || []).forEach((tag) => allTags.add(tag));
468
- const browserMatch = test.name.match(/ > (\w+) > /);
469
- if (browserMatch) allBrowsers.add(browserMatch[1]);
470
- });
471
-
472
932
  return results
473
933
  .map((test, index) => {
474
934
  const browser = test.browser || "unknown";
475
- const testName = test.name.split(" > ").pop() || test.name;
935
+ const testFileParts = test.name.split(" > ");
936
+ const testTitle =
937
+ testFileParts[testFileParts.length - 1] || "Unnamed Test";
476
938
 
477
- // Generate steps HTML recursively
478
939
  const generateStepsHTML = (steps, depth = 0) => {
479
- if (!steps || steps.length === 0) return "";
480
-
940
+ if (!steps || steps.length === 0)
941
+ return "<div class='no-steps'>No steps recorded for this test.</div>";
481
942
  return steps
482
943
  .map((step) => {
483
944
  const hasNestedSteps = step.steps && step.steps.length > 0;
484
- const isHook = step.isHook;
485
- const stepClass = isHook ? "step-hook" : "";
945
+ const isHook = step.hookType;
946
+ const stepClass = isHook
947
+ ? `step-hook step-hook-${step.hookType}`
948
+ : "";
486
949
  const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
487
950
 
488
951
  return `
489
- <div class="step-item" style="padding-left: ${depth * 20}px">
490
- <div class="step-header ${stepClass}" onclick="toggleStepDetails(this)">
491
- <span class="step-icon">${getStatusIcon(step.status)}</span>
492
- <span class="step-title">${sanitizeHTML(
493
- step.title
494
- )}${hookIndicator}</span>
495
- <span class="step-duration">${formatDuration(
496
- step.duration
497
- )}</span>
498
- </div>
499
- <div class="step-details">
500
- ${
501
- step.codeLocation
502
- ? `<div><strong>Location:</strong> ${sanitizeHTML(
503
- step.codeLocation
504
- )}</div>`
505
- : ""
506
- }
507
- ${
508
- step.errorMessage
509
- ? `
510
- <div class="step-error">
511
- <strong>Error:</strong> ${sanitizeHTML(step.errorMessage)}
512
- ${
513
- step.stackTrace
514
- ? `<pre>${sanitizeHTML(step.stackTrace)}</pre>`
515
- : ""
516
- }
517
- </div>
518
- `
519
- : ""
520
- }
521
- ${
522
- hasNestedSteps
523
- ? `
524
- <div class="nested-steps">
525
- ${generateStepsHTML(step.steps, depth + 1)}
526
- </div>
527
- `
528
- : ""
529
- }
530
- </div>
952
+ <div class="step-item" style="--depth: ${depth};">
953
+ <div class="step-header ${stepClass}" role="button" aria-expanded="false">
954
+ <span class="step-icon">${getStatusIcon(step.status)}</span>
955
+ <span class="step-title">${sanitizeHTML(
956
+ step.title
957
+ )}${hookIndicator}</span>
958
+ <span class="step-duration">${formatDuration(
959
+ step.duration
960
+ )}</span>
531
961
  </div>
532
- `;
962
+ <div class="step-details" style="display: none;">
963
+ ${
964
+ step.codeLocation
965
+ ? `<div class="step-info"><strong>Location:</strong> ${sanitizeHTML(
966
+ step.codeLocation
967
+ )}</div>`
968
+ : ""
969
+ }
970
+ ${
971
+ step.errorMessage
972
+ ? `
973
+ <div class="step-error">
974
+ ${
975
+ step.stackTrace
976
+ ? `<pre class="stack-trace">${sanitizeHTML(
977
+ step.stackTrace
978
+ )}</pre>`
979
+ : ""
980
+ }
981
+ </div>`
982
+ : ""
983
+ }
984
+ ${
985
+ hasNestedSteps
986
+ ? `<div class="nested-steps">${generateStepsHTML(
987
+ step.steps,
988
+ depth + 1
989
+ )}</div>`
990
+ : ""
991
+ }
992
+ </div>
993
+ </div>`;
533
994
  })
534
995
  .join("");
535
996
  };
536
997
 
537
998
  return `
538
- <div class="test-suite" data-status="${
999
+ <div class="test-case" data-status="${
1000
+ test.status
1001
+ }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
1002
+ .join(",")
1003
+ .toLowerCase()}">
1004
+ <div class="test-case-header" role="button" aria-expanded="false">
1005
+ <div class="test-case-summary">
1006
+ <span class="status-badge ${getStatusClass(test.status)}">${String(
539
1007
  test.status
540
- }" data-browser="${browser}" data-tags="${(test.tags || []).join(",")}">
541
- <div class="suite-header" onclick="toggleTestDetails(this)">
542
- <div>
543
- <span class="${getStatusClass(
544
- test.status
545
- )}">${test.status.toUpperCase()}</span>
546
- <span class="test-name">${sanitizeHTML(testName)}</span>
547
- <span class="test-browser">(${browser})</span>
548
- </div>
549
- <div class="test-meta">
550
- <span class="test-duration">${formatDuration(
551
- test.duration
552
- )}</span>
553
- </div>
1008
+ ).toUpperCase()}</span>
1009
+ <span class="test-case-title" title="${sanitizeHTML(
1010
+ test.name
1011
+ )}">${sanitizeHTML(testTitle)}</span>
1012
+ <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
554
1013
  </div>
555
- <div class="suite-content">
556
- <div class="test-details">
557
- <h3>Test Details</h3>
558
- <p><strong>Status:</strong> <span class="${getStatusClass(
559
- test.status
560
- )}">${test.status.toUpperCase()}</span></p>
561
- <p><strong>Browser:</strong> ${browser}</p>
562
- <p><strong>Duration:</strong> ${formatDuration(test.duration)}</p>
563
- ${
564
- test.tags && test.tags.length > 0
565
- ? `<p><strong>Tags:</strong> ${test.tags
566
- .map((t) => `<span class="tag">${t}</span>`)
567
- .join(" ")}</p>`
568
- : ""
569
- }
570
-
571
- <h3>Test Steps</h3>
572
- <div class="steps-list">
573
- ${generateStepsHTML(test.steps)}
1014
+ <div class="test-case-meta">
1015
+ ${
1016
+ test.tags && test.tags.length > 0
1017
+ ? test.tags
1018
+ .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
1019
+ .join(" ")
1020
+ : ""
1021
+ }
1022
+ <span class="test-duration">${formatDuration(test.duration)}</span>
1023
+ </div>
1024
+ </div>
1025
+ <div class="test-case-content" style="display: none;">
1026
+ <p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
1027
+ ${
1028
+ test.error
1029
+ ? `<div class="test-error-summary">
1030
+ ${formatPlaywrightError(test.error)}
1031
+ </div>`
1032
+ : ""
1033
+ }
1034
+
1035
+ <h4>Steps</h4>
1036
+ <div class="steps-list">${generateStepsHTML(test.steps)}</div>
1037
+
1038
+ ${
1039
+ test.stdout && test.stdout.length > 0
1040
+ ? `
1041
+ <div class="console-output-section">
1042
+ <h4>Console Output (stdout)</h4>
1043
+ <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
1044
+ .map((line) => sanitizeHTML(line))
1045
+ .join("\n")}</pre>
1046
+ </div>`
1047
+ : ""
1048
+ }
1049
+ ${
1050
+ test.stderr && test.stderr.length > 0
1051
+ ? `
1052
+ <div class="console-output-section">
1053
+ <h4>Console Output (stderr)</h4>
1054
+ <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
1055
+ .map((line) => sanitizeHTML(line))
1056
+ .join("\n")}</pre>
1057
+ </div>`
1058
+ : ""
1059
+ }
1060
+
1061
+ ${
1062
+ test.screenshots && test.screenshots.length > 0
1063
+ ? `
1064
+ <div class="attachments-section">
1065
+ <h4>Screenshots</h4>
1066
+ <div class="attachments-grid">
1067
+ ${test.screenshots
1068
+ .map(
1069
+ (screenshot) => `
1070
+ <div class="attachment-item">
1071
+ <img src="${screenshot}" alt="Screenshot">
1072
+ <div class="attachment-info">
1073
+ <div class="trace-actions">
1074
+ <a href="${screenshot}" target="_blank">View Full Size</a></div>
1075
+ </div>
1076
+ </div>
1077
+ `
1078
+ )
1079
+ .join("")}
574
1080
  </div>
575
-
576
- ${
577
- test.screenshots && test.screenshots.length > 0
578
- ? `
579
- <div class="attachments-section">
580
- <h4>Screenshots</h4>
581
- <div class="attachments-grid">
582
- ${test.screenshots
583
- .map(
584
- (screenshot) => `
1081
+ </div>
1082
+ `
1083
+ : ""
1084
+ }
1085
+
1086
+ ${
1087
+ test.videoPath
1088
+ ? `
1089
+ <div class="attachments-section">
1090
+ <h4>Videos</h4>
1091
+ <div class="attachments-grid">
1092
+ ${(() => {
1093
+ // Handle both string and array cases
1094
+ const videos = Array.isArray(test.videoPath)
1095
+ ? test.videoPath
1096
+ : [test.videoPath];
1097
+ const mimeTypes = {
1098
+ mp4: "video/mp4",
1099
+ webm: "video/webm",
1100
+ ogg: "video/ogg",
1101
+ mov: "video/quicktime",
1102
+ avi: "video/x-msvideo",
1103
+ };
1104
+
1105
+ return videos
1106
+ .map((video, index) => {
1107
+ const videoUrl =
1108
+ typeof video === "object" ? video.url || "" : video;
1109
+ const videoName =
1110
+ typeof video === "object"
1111
+ ? video.name || `Video ${index + 1}`
1112
+ : `Video ${index + 1}`;
1113
+ const fileExtension = videoUrl
1114
+ .split(".")
1115
+ .pop()
1116
+ .toLowerCase();
1117
+ const mimeType = mimeTypes[fileExtension] || "video/mp4";
1118
+
1119
+ return `
585
1120
  <div class="attachment-item">
586
- <img src="${screenshot}" alt="Screenshot">
1121
+ <video controls width="100%" height="auto" title="${videoName}">
1122
+ <source src="${videoUrl}" type="${mimeType}">
1123
+ Your browser does not support the video tag.
1124
+ </video>
587
1125
  <div class="attachment-info">
588
- <a href="${screenshot}" target="_blank">View Full Size</a>
1126
+ <div class="trace-actions">
1127
+ <a href="${videoUrl}" target="_blank" download="${videoName}.${fileExtension}">
1128
+ Download
1129
+ </a>
1130
+ </div>
589
1131
  </div>
590
1132
  </div>
591
- `
592
- )
593
- .join("")}
594
- </div>
595
- </div>
596
- `
597
- : ""
598
- }
599
-
600
- ${
601
- test.codeSnippet
602
- ? `
603
- <div class="code-section">
604
- <h4>Code Snippet</h4>
605
- <pre>${sanitizeHTML(test.codeSnippet)}</pre>
1133
+ `;
1134
+ })
1135
+ .join("");
1136
+ })()}
1137
+ </div>
1138
+ </div>
1139
+ `
1140
+ : ""
1141
+ }
1142
+
1143
+ ${
1144
+ test.tracePath
1145
+ ? `
1146
+ <div class="attachments-section">
1147
+ <h4>Trace Files</h4>
1148
+ <div class="attachments-grid">
1149
+ ${(() => {
1150
+ // Handle both string and array cases
1151
+ const traces = Array.isArray(test.tracePath)
1152
+ ? test.tracePath
1153
+ : [test.tracePath];
1154
+
1155
+ return traces
1156
+ .map((trace, index) => {
1157
+ const traceUrl =
1158
+ typeof trace === "object" ? trace.url || "" : trace;
1159
+ const traceName =
1160
+ typeof trace === "object"
1161
+ ? trace.name || `Trace ${index + 1}`
1162
+ : `Trace ${index + 1}`;
1163
+ const traceFileName = traceUrl.split("/").pop();
1164
+
1165
+ return `
1166
+ <div class="attachment-item">
1167
+ <div class="trace-preview">
1168
+ <span class="trace-icon">📄</span>
1169
+ <span class="trace-name">${traceName}</span>
1170
+ </div>
1171
+ <div class="attachment-info">
1172
+ <div class="trace-actions">
1173
+ <a href="${traceUrl}" target="_blank" download="${traceFileName}" class="download-trace">
1174
+ Download
1175
+ </a>
606
1176
  </div>
607
- `
608
- : ""
609
- }
1177
+ </div>
610
1178
  </div>
611
- </div>
1179
+ `;
1180
+ })
1181
+ .join("");
1182
+ })()}
1183
+ </div>
1184
+ </div>
1185
+ `
1186
+ : ""
1187
+ }
1188
+
1189
+ ${
1190
+ test.codeSnippet
1191
+ ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${sanitizeHTML(
1192
+ test.codeSnippet
1193
+ )}</code></pre></div>`
1194
+ : ""
1195
+ }
612
1196
  </div>
613
- `;
1197
+ </div>`;
614
1198
  })
615
1199
  .join("");
616
- };
1200
+ }
617
1201
 
618
- // Generate HTML with optimized CSS and JS
619
1202
  return `
620
1203
  <!DOCTYPE html>
621
1204
  <html lang="en">
622
1205
  <head>
623
1206
  <meta charset="UTF-8">
624
1207
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1208
+ <link rel="icon" type="image/png" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
1209
+ <link rel="apple-touch-icon" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
1210
+ <script src="https://code.highcharts.com/highcharts.js"></script>
625
1211
  <title>Playwright Pulse Report</title>
626
1212
  <style>
627
- /* Base Styles */
628
1213
  :root {
629
- --primary-color: #3f51b5;
630
- --secondary-color: #ff4081;
631
- --success-color: #4CAF50;
632
- --danger-color: #F44336;
633
- --warning-color: #FFC107;
634
- --info-color: #2196F3;
635
- --light-color: #f5f5f5;
636
- --dark-color: #212121;
637
- --text-color: #424242;
638
- --border-color: #e0e0e0;
639
- }
1214
+ --primary-color: #3f51b5; /* Indigo */
1215
+ --secondary-color: #ff4081; /* Pink */
1216
+ --accent-color: #673ab7; /* Deep Purple */
1217
+ --accent-color-alt: #FF9800; /* Orange for duration charts */
1218
+ --success-color: #4CAF50; /* Green */
1219
+ --danger-color: #F44336; /* Red */
1220
+ --warning-color: #FFC107; /* Amber */
1221
+ --info-color: #2196F3; /* Blue */
1222
+ --light-gray-color: #f5f5f5;
1223
+ --medium-gray-color: #e0e0e0;
1224
+ --dark-gray-color: #757575;
1225
+ --text-color: #333;
1226
+ --text-color-secondary: #555;
1227
+ --border-color: #ddd;
1228
+ --background-color: #f8f9fa;
1229
+ --card-background-color: #fff;
1230
+ --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
1231
+ --border-radius: 8px;
1232
+ --box-shadow: 0 5px 15px rgba(0,0,0,0.08);
1233
+ --box-shadow-light: 0 3px 8px rgba(0,0,0,0.05);
1234
+ --box-shadow-inset: inset 0 1px 3px rgba(0,0,0,0.07);
1235
+ }
1236
+
1237
+ /* General Highcharts styling */
1238
+ .highcharts-background { fill: transparent; }
1239
+ .highcharts-title, .highcharts-subtitle { font-family: var(--font-family); }
1240
+ .highcharts-axis-labels text, .highcharts-legend-item text { fill: var(--text-color-secondary) !important; font-size: 12px !important; }
1241
+ .highcharts-axis-title { fill: var(--text-color) !important; }
1242
+ .highcharts-tooltip > span { background-color: rgba(10,10,10,0.92) !important; border-color: rgba(10,10,10,0.92) !important; color: #f5f5f5 !important; padding: 10px !important; border-radius: 6px !important; }
640
1243
 
641
1244
  body {
642
- font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', sans-serif;
1245
+ font-family: var(--font-family);
643
1246
  margin: 0;
644
- padding: 0;
645
- background-color: #fafafa;
1247
+ background-color: var(--background-color);
646
1248
  color: var(--text-color);
647
- line-height: 1.6;
1249
+ line-height: 1.65;
1250
+ font-size: 16px;
648
1251
  }
649
1252
 
650
1253
  .container {
651
- margin: 20px auto;
652
- padding: 20px;
653
- background-color: #fff;
654
- border-radius: 8px;
655
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
1254
+ max-width: 1600px;
1255
+ padding: 30px;
1256
+ border-radius: var(--border-radius);
1257
+ box-shadow: var(--box-shadow);
1258
+ background: repeating-linear-gradient(#f1f8e9, #f9fbe7, #fce4ec);
656
1259
  }
657
1260
 
658
- /* Header Styles */
659
1261
  .header {
660
1262
  display: flex;
661
1263
  justify-content: space-between;
662
1264
  align-items: center;
663
1265
  flex-wrap: wrap;
664
- margin-bottom: 20px;
665
- padding-bottom: 20px;
1266
+ padding-bottom: 25px;
666
1267
  border-bottom: 1px solid var(--border-color);
1268
+ margin-bottom: 25px;
667
1269
  }
1270
+ .header-title { display: flex; align-items: center; gap: 15px; }
1271
+ .header h1 { margin: 0; font-size: 1.85em; font-weight: 600; color: var(--primary-color); }
1272
+ #report-logo { height: 40px; width: 40px; border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.1);}
1273
+ .run-info { font-size: 0.9em; text-align: right; color: var(--text-color-secondary); line-height:1.5;}
1274
+ .run-info strong { color: var(--text-color); }
668
1275
 
669
- .header h1 {
670
- margin: 0;
671
- font-size: 24px;
672
- color: var(--primary-color);
673
- display: flex;
674
- align-items: center;
675
- gap: 10px;
1276
+ .tabs { display: flex; border-bottom: 2px solid var(--border-color); margin-bottom: 30px; overflow-x: auto; }
1277
+ .tab-button {
1278
+ padding: 15px 25px; background: none; border: none; border-bottom: 3px solid transparent;
1279
+ cursor: pointer; font-size: 1.1em; font-weight: 600; color: black;
1280
+ transition: color 0.2s ease, border-color 0.2s ease; white-space: nowrap;
676
1281
  }
677
-
678
- .run-info {
679
- background: #f5f5f5;
680
- padding: 10px 15px;
681
- border-radius: 6px;
682
- font-size: 14px;
1282
+ .tab-button:hover { color: var(--accent-color); }
1283
+ .tab-button.active { color: var(--primary-color); border-bottom-color: var(--primary-color); }
1284
+ .tab-content { display: none; animation: fadeIn 0.4s ease-out; }
1285
+ .tab-content.active { display: block; }
1286
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
1287
+
1288
+ .dashboard-grid {
1289
+ display: grid;
1290
+ grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
1291
+ gap: 22px; margin-bottom: 35px;
683
1292
  }
684
-
685
- /* Tab Styles */
686
- .tabs {
687
- display: flex;
688
- border-bottom: 1px solid var(--border-color);
689
- margin-bottom: 20px;
690
- }
691
-
692
- .tab-button {
693
- padding: 10px 20px;
694
- background: none;
695
- border: none;
696
- cursor: pointer;
697
- font-size: 16px;
698
- color: #666;
699
- position: relative;
700
- }
701
-
702
- .tab-button.active {
703
- color: var(--primary-color);
704
- font-weight: 500;
705
- }
706
-
707
- .tab-button.active::after {
708
- content: '';
709
- position: absolute;
710
- bottom: -1px;
711
- left: 0;
712
- right: 0;
713
- height: 2px;
714
- background: var(--primary-color);
715
- }
716
-
717
- .tab-content {
718
- display: none;
719
- }
720
-
721
- .tab-content.active {
722
- display: block;
723
- }
724
-
725
- /* Main dashboard grid layout */
726
- .dashboard-grid {
727
- display: grid;
728
- grid-template-columns: 1fr;
729
- gap: 20px;
730
- padding: 16px 0;
731
- }
732
-
733
1293
  .summary-card {
734
- background: linear-gradient(to bottom right, #f0f4ff, #ffffff);
735
- border-radius: 8px;
736
- padding: 20px;
737
- box-shadow: 0 2px 5px rgba(0,0,0,0.05);
738
- text-align: center;
739
- transition: transform 0.2s ease, box-shadow 0.2s ease;
740
- cursor: pointer;
741
- }
742
-
743
- .summary-card:hover {
744
- transform: translateY(-4px);
745
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
746
- background: #90e0e3; /* optional light background change */
747
- }
1294
+ background-color: var(--card-background-color); border: 1px solid var(--border-color);
1295
+ border-radius: var(--border-radius); padding: 22px; text-align: center;
1296
+ box-shadow: var(--box-shadow-light); transition: transform 0.2s ease, box-shadow 0.2s ease;
1297
+ }
1298
+ .summary-card:hover { transform: translateY(-5px); box-shadow: var(--box-shadow); }
1299
+ .summary-card h3 { margin: 0 0 10px; font-size: 1.05em; font-weight: 500; color: var(--text-color-secondary); }
1300
+ .summary-card .value { font-size: 2.4em; font-weight: 600; margin-bottom: 8px; }
1301
+ .summary-card .trend-percentage { font-size: 1em; color: var(--dark-gray-color); }
1302
+ .status-passed .value, .stat-passed svg { color: var(--success-color); }
1303
+ .status-failed .value, .stat-failed svg { color: var(--danger-color); }
1304
+ .status-skipped .value, .stat-skipped svg { color: var(--warning-color); }
748
1305
 
749
- .summary-card h3 {
750
- margin: 0 0 10px;
751
- font-size: 16px;
752
- color: #666;
1306
+ .dashboard-bottom-row {
1307
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
1308
+ gap: 28px; align-items: stretch;
753
1309
  }
754
-
755
- .summary-card .value {
756
- font-size: 28px;
757
- font-weight: 600;
758
- margin: 10px 0;
1310
+ .pie-chart-wrapper, .suites-widget, .trend-chart {
1311
+ background-color: var(--card-background-color); padding: 28px;
1312
+ border-radius: var(--border-radius); box-shadow: var(--box-shadow-light);
1313
+ display: flex; flex-direction: column;
759
1314
  }
760
-
761
- .status-passed .value {
762
- color: var(--success-color);
1315
+
1316
+ .pie-chart-wrapper h3, .suites-header h2, .trend-chart h3 {
1317
+ text-align: center; margin-top: 0; margin-bottom: 25px;
1318
+ font-size: 1.25em; font-weight: 600; color: var(--text-color);
763
1319
  }
764
-
765
- .status-failed .value {
766
- color: var(--danger-color);
1320
+ .trend-chart-container, .pie-chart-wrapper div[id^="pieChart-"] { /* For Highcharts containers */
1321
+ flex-grow: 1;
1322
+ min-height: 250px; /* Ensure charts have some min height */
767
1323
  }
768
1324
 
769
- .status-skipped .value {
770
- color: var(--warning-color);
771
- }
1325
+ .chart-tooltip { /* This class was for D3, Highcharts has its own tooltip styling via JS/SVG */
1326
+ /* Basic styling for Highcharts HTML tooltips can be done via .highcharts-tooltip span */
1327
+ }
1328
+ .status-badge-small-tooltip { padding: 2px 5px; border-radius: 3px; font-size: 0.9em; font-weight: 600; color: white; text-transform: uppercase; }
1329
+ .status-badge-small-tooltip.status-passed { background-color: var(--success-color); }
1330
+ .status-badge-small-tooltip.status-failed { background-color: var(--danger-color); }
1331
+ .status-badge-small-tooltip.status-skipped { background-color: var(--warning-color); }
1332
+ .status-badge-small-tooltip.status-unknown { background-color: var(--dark-gray-color); }
772
1333
 
773
- .pie-chart-container {
774
- grid-column: span 2;
775
- background: linear-gradient(to bottom right, #f0f4ff, #ffffff);
776
- border-radius: 8px;
777
- padding: 20px;
778
- box-shadow: 0 2px 5px rgba(0,0,0,0.05);
779
- }
780
-
781
- /* Test Run Summary Styles */
1334
+ .suites-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
1335
+ .summary-badge { background-color: var(--light-gray-color); color: var(--text-color-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
1336
+ .suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
1337
+ .suite-card {
1338
+ border: 1px solid var(--border-color); border-left-width: 5px;
1339
+ border-radius: calc(var(--border-radius) / 1.5); padding: 20px;
1340
+ background-color: var(--card-background-color); transition: box-shadow 0.2s ease, border-left-color 0.2s ease;
1341
+ }
1342
+ .suite-card:hover { box-shadow: var(--box-shadow); }
1343
+ .suite-card.status-passed { border-left-color: var(--success-color); }
1344
+ .suite-card.status-failed { border-left-color: var(--danger-color); }
1345
+ .suite-card.status-skipped { border-left-color: var(--warning-color); }
1346
+ .suite-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
1347
+ .suite-name { font-weight: 600; font-size: 1.05em; color: var(--text-color); margin-right: 10px; word-break: break-word;}
1348
+ .browser-tag { font-size: 0.8em; background-color: var(--medium-gray-color); color: var(--text-color-secondary); padding: 3px 8px; border-radius: 4px; white-space: nowrap;}
1349
+ .suite-card-body .test-count { font-size: 0.95em; color: var(--text-color-secondary); display: block; margin-bottom: 10px; }
1350
+ .suite-stats { display: flex; gap: 14px; font-size: 0.95em; align-items: center; }
1351
+ .suite-stats span { display: flex; align-items: center; gap: 6px; }
1352
+ .suite-stats svg { vertical-align: middle; font-size: 1.15em; }
1353
+
782
1354
  .filters {
783
- display: flex;
784
- gap: 10px;
785
- margin-bottom: 20px;
786
- flex-wrap: wrap;
787
- padding: 0.75rem;
788
- }
789
-
790
- .filters input,
791
- .filters select {
792
- padding: 8px 12px;
793
- border: 1px solid #ddd;
794
- border-radius: 4px;
795
- font-size: 14px;
796
- }
797
-
798
- .filters button {
799
- padding: 8px 16px;
800
- background: var(--primary-color);
801
- color: white;
802
- border: none;
803
- border-radius: 4px;
804
- cursor: pointer;
805
- }
1355
+ display: flex; flex-wrap: wrap; gap: 18px; margin-bottom: 28px;
1356
+ padding: 20px; background-color: var(--light-gray-color); border-radius: var(--border-radius);
1357
+ box-shadow: var(--box-shadow-inset); border-color: black; border-style: groove;
1358
+ }
1359
+ .filters input, .filters select, .filters button {
1360
+ padding: 11px 15px; border: 1px solid var(--border-color);
1361
+ border-radius: 6px; font-size: 1em;
1362
+ }
1363
+ .filters input { flex-grow: 1; min-width: 240px;}
1364
+ .filters select {min-width: 180px;}
1365
+ .filters button { background-color: var(--primary-color); color: white; cursor: pointer; transition: background-color 0.2s ease, box-shadow 0.2s ease; border: none; }
1366
+ .filters button:hover { background-color: var(--accent-color); box-shadow: 0 2px 5px rgba(0,0,0,0.15);}
1367
+
1368
+ .test-case {
1369
+ margin-bottom: 15px; border: 1px solid var(--border-color);
1370
+ border-radius: var(--border-radius); background-color: var(--card-background-color);
1371
+ box-shadow: var(--box-shadow-light); overflow: hidden;
1372
+ }
1373
+ .test-case-header {
1374
+ padding: 10px 15px; background-color: #fff; cursor: pointer;
1375
+ display: flex; justify-content: space-between; align-items: center;
1376
+ border-bottom: 1px solid transparent;
1377
+ transition: background-color 0.2s ease;
1378
+ }
1379
+ .test-case-header:hover { background-color: #f4f6f8; }
1380
+ .test-case-header[aria-expanded="true"] { border-bottom-color: var(--border-color); background-color: #f9fafb; }
806
1381
 
807
- .test-suite {
808
- margin-bottom: 15px;
809
- border: 1px solid #eee;
810
- border-radius: 6px;
811
- overflow: hidden;
812
- }
813
-
814
- .suite-header {
815
- padding: 12px 15px;
816
- background: #f9f9f9;
817
- cursor: pointer;
818
- display: flex;
819
- justify-content: space-between;
820
- align-items: center;
821
- }
822
-
823
- .suite-header:hover {
824
- background: #f0f0f0;
825
- }
826
-
827
- .suite-content {
828
- display: none;
829
- padding: 15px;
830
- background: white;
831
- }
1382
+ .test-case-summary { display: flex; align-items: center; gap: 14px; flex-grow: 1; flex-wrap: wrap;}
1383
+ .test-case-title { font-weight: 600; color: var(--text-color); font-size: 1em; }
1384
+ .test-case-browser { font-size: 0.9em; color: var(--text-color-secondary); }
1385
+ .test-case-meta { display: flex; align-items: center; gap: 12px; font-size: 0.9em; color: var(--text-color-secondary); flex-shrink: 0; }
1386
+ .test-duration { background-color: var(--light-gray-color); padding: 4px 10px; border-radius: 12px; font-size: 0.9em;}
832
1387
 
833
- .test-details h3 {
834
- margin-top: 0;
835
- font-size: 18px;
836
- color: var(--dark-color);
1388
+ .status-badge {
1389
+ padding: 5px; border-radius: 6px; font-size: 0.8em; font-weight: 600; color: white; text-transform: uppercase;
1390
+ min-width: 70px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
837
1391
  }
1392
+ .status-badge.status-passed { background-color: var(--success-color); }
1393
+ .status-badge.status-failed { background-color: var(--danger-color); }
1394
+ .status-badge.status-skipped { background-color: var(--warning-color); }
1395
+ .status-badge.status-unknown { background-color: var(--dark-gray-color); }
838
1396
 
839
- .steps-list {
840
- margin: 15px 0;
841
- padding: 0;
842
- list-style: none;
843
- }
844
-
845
- .step-item {
846
- margin-bottom: 8px;
847
- }
1397
+ .tag { display: inline-block; background: linear-gradient( #fff, #333, #000); color: #fff; padding: 3px 10px; border-radius: 12px; font-size: 0.85em; margin-right: 6px; font-weight: 400; }
848
1398
 
1399
+ .test-case-content { display: none; padding: 20px; border-top: 1px solid var(--border-color); background-color: #fcfdff; }
1400
+ .test-case-content h4 { margin-top: 22px; margin-bottom: 14px; font-size: 1.15em; color: var(--primary-color); }
1401
+ .test-case-content p { margin-bottom: 10px; font-size: 1em; }
1402
+ .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; }
1403
+ .test-error-summary h4 { color: var(--danger-color); margin-top:0;}
1404
+ .test-error-summary pre { white-space: pre-wrap; word-break: break-all; color: var(--danger-color); font-size: 0.95em;}
1405
+
1406
+ .steps-list { margin: 18px 0; }
1407
+ .step-item { margin-bottom: 8px; padding-left: calc(var(--depth, 0) * 28px); }
849
1408
  .step-header {
850
- display: flex;
851
- align-items: center;
852
- cursor: pointer;
853
- padding: 8px;
854
- border-radius: 4px;
855
- }
856
-
857
- .step-header:hover {
858
- background: #f5f5f5;
859
- }
860
-
861
- .step-icon {
862
- margin-right: 8px;
863
- width: 20px;
864
- text-align: center;
865
- }
866
-
867
- .step-title {
868
- flex: 1;
869
- }
870
-
871
- .step-duration {
872
- color: #666;
873
- font-size: 12px;
874
- }
875
-
876
- .step-details {
877
- display: none;
878
- padding: 10px;
879
- margin-top: 5px;
880
- background: #f9f9f9;
881
- border-radius: 4px;
882
- font-size: 14px;
883
- }
884
-
885
- .step-error {
886
- color: var(--danger-color);
887
- margin-top: 8px;
888
- padding: 8px;
889
- background: rgba(244, 67, 54, 0.1);
890
- border-radius: 4px;
891
- font-size: 13px;
892
- }
893
-
894
- .step-hook {
895
- background: rgba(33, 150, 243, 0.1);
896
- }
897
-
898
- .nested-steps {
899
- display: none;
900
- padding-left: 20px;
901
- border-left: 2px solid #eee;
902
- margin-top: 8px;
903
- }
904
-
905
- .attachments-grid {
906
- display: grid;
907
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
908
- gap: 15px;
909
- margin-top: 15px;
910
- }
911
-
1409
+ display: flex; align-items: center; cursor: pointer;
1410
+ padding: 10px 14px; border-radius: 6px; background-color: #fff;
1411
+ border: 1px solid var(--light-gray-color);
1412
+ transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
1413
+ }
1414
+ .step-header:hover { background-color: #f0f2f5; border-color: var(--medium-gray-color); box-shadow: var(--box-shadow-inset); }
1415
+ .step-icon { margin-right: 12px; width: 20px; text-align: center; font-size: 1.1em; }
1416
+ .step-title { flex: 1; font-size: 1em; }
1417
+ .step-duration { color: var(--dark-gray-color); font-size: 0.9em; }
1418
+ .step-details { display: none; padding: 14px; margin-top: 8px; background: #fdfdfd; border-radius: 6px; font-size: 0.95em; border: 1px solid var(--light-gray-color); }
1419
+ .step-info { margin-bottom: 8px; }
1420
+ .step-error { color: var(--danger-color); margin-top: 12px; padding: 14px; background: rgba(244,67,54,0.05); border-radius: 4px; font-size: 0.95em; border-left: 3px solid var(--danger-color); }
1421
+ .step-error pre.stack-trace { margin-top: 10px; padding: 12px; background-color: rgba(0,0,0,0.03); border-radius: 4px; font-size:0.9em; max-height: 280px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; }
1422
+ .step-hook { background-color: rgba(33,150,243,0.04); border-left: 3px solid var(--info-color) !important; }
1423
+ .step-hook .step-title { font-style: italic; color: var(--info-color)}
1424
+ .nested-steps { margin-top: 12px; }
1425
+
1426
+ .attachments-section { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--light-gray-color); }
1427
+ .attachments-section h4 { margin-top: 0; margin-bottom: 20px; font-size: 1.1em; color: var(--text-color); }
1428
+ .attachments-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 22px; }
912
1429
  .attachment-item {
913
- border: 1px solid #eee;
914
- border-radius: 4px;
915
- overflow: hidden;
1430
+ border: 1px solid var(--border-color); border-radius: var(--border-radius); background-color: #fff;
1431
+ box-shadow: var(--box-shadow-light); overflow: hidden; display: flex; flex-direction: column;
1432
+ transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
916
1433
  }
917
-
1434
+ .attachment-item:hover { transform: translateY(-4px); box-shadow: var(--box-shadow); }
918
1435
  .attachment-item img {
919
- width: 100%;
920
- height: auto;
921
- display: block;
1436
+ width: 100%; height: 180px; object-fit: cover; display: block;
1437
+ border-bottom: 1px solid var(--border-color); transition: opacity 0.3s ease;
922
1438
  }
923
-
924
- .tag {
925
- display: inline-block;
926
- background: #e0e0e0;
927
- padding: 2px 6px;
928
- border-radius: 4px;
929
- font-size: 12px;
930
- margin-right: 5px;
931
- }
932
- .status-badge {
933
- padding: 3px 8px;
934
- border-radius: 4px;
935
- font-size: 12px;
936
- font-weight: bold;
937
- color: white;
938
- text-transform: uppercase;
939
- }
940
-
941
- span.status-passed {
942
- background-color: #4CAF50 !important; /* Bright green */
943
- color: white;
944
- border-radius: 4px;
945
- padding: 4px;
946
- font-size: 0.75rem;
947
- }
948
-
949
- span.status-failed {
950
- background-color: #F44336 !important; /* Bright red */
951
- color: white;
952
- border-radius: 4px;
953
- padding: 4px;
954
- font-size: 0.75rem;
1439
+ .attachment-item a:hover img { opacity: 0.85; }
1440
+ .attachment-caption {
1441
+ padding: 12px 15px; font-size: 0.9em; text-align: center;
1442
+ color: var(--text-color-secondary); word-break: break-word; background-color: var(--light-gray-color);
955
1443
  }
1444
+ .video-item a, .trace-item a { display: block; margin-bottom: 8px; color: var(--primary-color); text-decoration: none; font-weight: 500; }
1445
+ .video-item a:hover, .trace-item a:hover { text-decoration: underline; }
1446
+ .code-section pre { background-color: #2d2d2d; color: #f0f0f0; padding: 20px; border-radius: 6px; overflow-x: auto; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 0.95em; line-height:1.6;}
956
1447
 
957
- span.status-skipped {
958
- background-color: #FFC107 !important; /* Deep yellow */
959
- color: white;
960
- border-radius: 4px;
961
- padding: 4px;
962
- font-size: 0.75rem;
963
- }
1448
+ .trend-charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 28px; margin-bottom: 35px; }
1449
+ /* Removed D3 specific .chart-axis, .main-chart-title, .chart-line.* rules */
1450
+ /* Highcharts styles its elements with classes like .highcharts-axis, .highcharts-title etc. */
1451
+
1452
+ .test-history-container h2.tab-main-title { font-size: 1.6em; margin-bottom: 18px; color: var(--primary-color); border-bottom: 1px solid var(--border-color); padding-bottom: 12px;}
1453
+ .test-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 22px; margin-top: 22px; }
1454
+ .test-history-card {
1455
+ background: var(--card-background-color); border: 1px solid var(--border-color); border-radius: var(--border-radius);
1456
+ padding: 22px; box-shadow: var(--box-shadow-light); display: flex; flex-direction: column;
1457
+ }
1458
+ .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); }
1459
+ .test-history-header h3 { margin: 0; font-size: 1.15em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1460
+ .test-history-header p { font-weight: 500 }
1461
+ .test-history-trend { margin-bottom: 20px; min-height: 110px; }
1462
+ .test-history-trend div[id^="testHistoryChart-"] { /* Highcharts container for history */
1463
+ display: block; margin: 0 auto; max-width:100%; height: 100px; width: 320px; /* Match JS config */
1464
+ }
1465
+ /* .test-history-trend .small-axis text {font-size: 11px;} Removed D3 specific */
1466
+ .test-history-details-collapsible summary { cursor: pointer; font-size: 1em; color: var(--primary-color); margin-bottom: 10px; font-weight:500; }
1467
+ .test-history-details-collapsible summary:hover {text-decoration: underline;}
1468
+ .test-history-details table { width: 100%; border-collapse: collapse; font-size: 0.95em; }
1469
+ .test-history-details th, .test-history-details td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--light-gray-color); }
1470
+ .test-history-details th { background-color: var(--light-gray-color); font-weight: 600; }
1471
+ .status-badge-small {
1472
+ padding: 3px 7px; border-radius: 4px; font-size: 0.8em; font-weight: 600;
1473
+ color: white; text-transform: uppercase; display: inline-block;
1474
+ }
1475
+ .status-badge-small.status-passed { background-color: var(--success-color); }
1476
+ .status-badge-small.status-failed { background-color: var(--danger-color); }
1477
+ .status-badge-small.status-skipped { background-color: var(--warning-color); }
1478
+ .status-badge-small.status-unknown { background-color: var(--dark-gray-color); }
1479
+
1480
+ .no-data, .no-tests, .no-steps, .no-data-chart {
1481
+ padding: 28px; text-align: center; color: var(--dark-gray-color); font-style: italic; font-size:1.1em;
1482
+ background-color: var(--light-gray-color); border-radius: var(--border-radius); margin: 18px 0;
1483
+ border: 1px dashed var(--medium-gray-color);
1484
+ }
1485
+ .no-data-chart {font-size: 0.95em; padding: 18px;}
1486
+
1487
+ #test-ai iframe { border: 1px solid var(--border-color); width: 100%; height: 85vh; border-radius: var(--border-radius); box-shadow: var(--box-shadow-light); }
1488
+ #test-ai p {margin-bottom: 18px; font-size: 1em; color: var(--text-color-secondary);}
1489
+ pre .stdout-log { background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2; }
1490
+ pre .stderr-log { background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2; }
1491
+
1492
+ .trace-preview {
1493
+ padding: 1rem;
1494
+ text-align: center;
1495
+ background: #f5f5f5;
1496
+ border-bottom: 1px solid #e1e1e1;
1497
+ }
964
1498
 
965
- /* Enhanced Pie Chart Styles */
966
- .pie-chart-container {
967
- display: flex;
968
- flex-direction: column;
969
- align-items: center;
970
- margin: 20px 0;
971
- }
1499
+ .trace-icon {
1500
+ font-size: 2rem;
1501
+ display: block;
1502
+ margin-bottom: 0.5rem;
1503
+ }
972
1504
 
973
- .pie-chart-svg {
974
- margin: 0 auto;
975
- }
1505
+ .trace-name {
1506
+ word-break: break-word;
1507
+ font-size: 0.9rem;
1508
+ }
976
1509
 
977
- .pie-chart-total {
978
- font-size: 18px;
979
- font-weight: bold;
980
- fill: #333;
981
- }
1510
+ .trace-actions {
1511
+ display: flex;
1512
+ gap: 0.5rem;
1513
+ }
982
1514
 
983
- .pie-chart-label {
984
- font-size: 12px;
985
- fill: #666;
986
- }
1515
+ .trace-actions a {
1516
+ flex: 1;
1517
+ text-align: center;
1518
+ padding: 0.25rem 0.5rem;
1519
+ font-size: 0.85rem;
1520
+ border-radius: 4px;
1521
+ text-decoration: none;
1522
+ }
987
1523
 
988
- .pie-chart-legend {
989
- display: flex;
990
- flex-wrap: wrap;
991
- justify-content: center;
992
- gap: 15px;
993
- margin-top: 15px;
994
- }
1524
+ .view-trace {
1525
+ background: #3182ce;
1526
+ color: white;
1527
+ }
995
1528
 
996
- .legend-item {
997
- display: flex;
998
- align-items: center;
999
- gap: 5px;
1000
- font-size: 14px;
1001
- }
1529
+ .view-trace:hover {
1530
+ background: #2c5282;
1531
+ }
1002
1532
 
1003
- .legend-color {
1004
- width: 12px;
1005
- height: 12px;
1006
- border-radius: 50%;
1007
- display: inline-block;
1008
- }
1533
+ .download-trace {
1534
+ background: #e2e8f0;
1535
+ color: #2d3748;
1536
+ }
1009
1537
 
1010
- .legend-value {
1011
- font-weight: 500;
1012
- }
1013
- .test-name {
1014
- font-weight: 600;
1015
- }
1016
- .summary-cards {
1017
- display: grid;
1018
- gap: 20px;
1019
- }
1538
+ .download-trace:hover {
1539
+ background: #cbd5e0;
1540
+ }
1020
1541
 
1021
- /* Below summary cards: chart and test suites */
1022
- .dashboard-bottom {
1023
- display: flex;
1024
- flex-direction: column;
1025
- gap: 24px;
1026
- }
1027
- /* Responsive Styles */
1028
- /* Mobile (up to 480px) and Tablet (481px to 768px) Responsive Styles */
1542
+ .filters button.clear-filters-btn {
1543
+ background-color: var(--medium-gray-color); /* Or any other suitable color */
1544
+ color: var(--text-color);
1545
+ /* Add other styling as per your .filters button style if needed */
1546
+ }
1029
1547
 
1030
- @media (min-width: 768px) {
1031
- .dashboard-grid {
1032
- grid-template-columns: repeat(4, 1fr); /* Four summary cards side-by-side */
1033
- }
1034
- .dashboard-bottom {
1035
- flex-direction: row;
1036
- }
1037
- .test-distribution {
1038
- flex: 1;
1039
- }
1040
- .test-suites {
1041
- flex: 2; /* dynamically expand */
1042
- min-width: 300px;
1043
- padding: 0.75rem;
1044
- }
1045
- }
1046
- @media (max-width: 768px) {
1047
- /* Base container adjustments */
1048
- .container {
1049
- padding: 15px;
1050
- margin: 10px auto;
1051
- }
1052
-
1053
- /* Header adjustments */
1054
- .header {
1055
- flex-direction: column;
1056
- align-items: flex-start;
1057
- gap: 15px;
1058
- padding-bottom: 15px;
1059
- }
1060
-
1061
- .run-info {
1062
- font-size: 13px;
1063
- }
1064
-
1065
- /* Tab adjustments */
1066
- .tabs {
1067
- overflow-x: auto;
1068
- white-space: nowrap;
1069
- padding-bottom: 5px;
1070
- }
1071
-
1072
- .tab-button {
1073
- padding: 8px 15px;
1074
- font-size: 14px;
1075
- }
1076
-
1077
- /* Dashboard Grid adjustments */
1078
- .dashboard-grid {
1079
- grid-template-columns: 1fr;
1080
- gap: 15px;
1081
- }
1082
-
1083
- .summary-card {
1084
- padding: 15px;
1085
- }
1086
-
1087
- .summary-card .value {
1088
- font-size: 24px;
1089
- }
1090
-
1091
- .pie-chart-container {
1092
- padding: 15px;
1093
- }
1094
-
1095
- .pie-chart-container svg {
1096
- width: 300px;
1097
- height: 300px;
1098
- }
1099
-
1100
- /* Test Suites Widget adjustments */
1101
- .suites-widget {
1102
- padding: 8px;
1103
- }
1104
-
1105
- .suites-header {
1106
- flex-direction: column;
1107
- align-items: flex-start;
1108
- gap: 10px;
1109
- }
1110
-
1111
- .suites-grid {
1112
- grid-template-columns: 1fr;
1113
- }
1114
-
1115
- /* Test Run Summary adjustments */
1116
- .filters {
1117
- flex-direction: column;
1118
- gap: 8px;
1119
- padding: 0.75rem;
1120
- }
1121
-
1122
- .filters input,
1123
- .filters select {
1124
- width: 100%;
1125
- padding: 8px;
1126
- }
1127
-
1128
- .filters button {
1129
- width: 100%;
1130
- margin-top: 5px;
1131
- }
1132
-
1133
- .test-suite {
1134
- margin-bottom: 10px;
1135
- }
1136
-
1137
- .suite-header {
1138
- padding: 10px;
1139
- flex-wrap: wrap;
1140
- }
1141
-
1142
- .test-name {
1143
- display: block;
1144
- width: 100%;
1145
- margin-top: 5px;
1146
- font-weight: 600;
1147
- }
1148
-
1149
- .test-meta {
1150
- margin-top: 5px;
1151
- }
1152
-
1153
- .suite-content {
1154
- padding: 10px;
1155
- }
1156
-
1157
- .steps-list {
1158
- margin: 10px 0;
1159
- }
1160
-
1161
- .step-header {
1162
- padding: 6px;
1163
- }
1164
-
1165
- .step-icon {
1166
- font-size: 14px;
1167
- }
1168
-
1169
- .step-title {
1170
- font-size: 14px;
1171
- }
1172
-
1173
- .step-duration {
1174
- font-size: 11px;
1175
- }
1176
-
1177
- .attachments-grid {
1178
- grid-template-columns: repeat(2, 1fr);
1179
- }
1180
-
1181
- /* Specific adjustments for mobile only (up to 480px) */
1182
- @media (max-width: 480px) {
1183
- .header h1 {
1184
- font-size: 20px;
1185
- }
1186
-
1187
- .summary-card .value {
1188
- font-size: 22px;
1189
- }
1190
- .pie-chart-container {
1191
- grid-column: span 1;
1192
- }
1193
- .pie-chart-container svg {
1194
- width: 300px;
1195
- height: 300px;
1196
- }
1197
-
1198
- .attachments-grid {
1199
- grid-template-columns: 1fr;
1200
- }
1201
-
1202
- .step-item {
1203
- padding-left: 0 !important;
1204
- }
1205
-
1206
- .nested-steps {
1207
- padding-left: 10px;
1548
+ .filters button.clear-filters-btn:hover {
1549
+ background-color: var(--dark-gray-color); /* Darker on hover */
1550
+ color: #fff;
1551
+ }
1552
+ @media (max-width: 1200px) {
1553
+ .trend-charts-row { grid-template-columns: 1fr; }
1554
+ }
1555
+ @media (max-width: 992px) {
1556
+ .dashboard-bottom-row { grid-template-columns: 1fr; }
1557
+ .pie-chart-wrapper div[id^="pieChart-"] { max-width: 350px; margin: 0 auto; }
1558
+ .filters input { min-width: 180px; }
1559
+ .filters select { min-width: 150px; }
1560
+ }
1561
+ @media (max-width: 768px) {
1562
+ body { font-size: 15px; }
1563
+ .container { margin: 10px; padding: 20px; }
1564
+ .header { flex-direction: column; align-items: flex-start; gap: 15px; }
1565
+ .header h1 { font-size: 1.6em; }
1566
+ .run-info { text-align: left; font-size:0.9em; }
1567
+ .tabs { margin-bottom: 25px;}
1568
+ .tab-button { padding: 12px 20px; font-size: 1.05em;}
1569
+ .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;}
1570
+ .summary-card .value {font-size: 2em;}
1571
+ .summary-card h3 {font-size: 0.95em;}
1572
+ .filters { flex-direction: column; padding: 18px; gap: 12px;}
1573
+ .filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;}
1574
+ .test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; }
1575
+ .test-case-summary {gap: 10px;}
1576
+ .test-case-title {font-size: 1.05em;}
1577
+ .test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;}
1578
+ .attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;}
1579
+ .test-history-grid {grid-template-columns: 1fr;}
1580
+ .pie-chart-wrapper {min-height: auto;}
1581
+ }
1582
+ @media (max-width: 480px) {
1583
+ body {font-size: 14px;}
1584
+ .container {padding: 15px;}
1585
+ .header h1 {font-size: 1.4em;}
1586
+ #report-logo { height: 35px; width: 35px; }
1587
+ .tab-button {padding: 10px 15px; font-size: 1em;}
1588
+ .summary-card .value {font-size: 1.8em;}
1589
+ .attachments-grid {grid-template-columns: 1fr;}
1590
+ .step-item {padding-left: calc(var(--depth, 0) * 18px);}
1591
+ .test-case-content, .step-details {padding: 15px;}
1592
+ .trend-charts-row {gap: 20px;}
1593
+ .trend-chart {padding: 20px;}
1208
1594
  }
1209
- }
1210
- }
1211
1595
  </style>
1212
1596
  </head>
1213
1597
  <body>
1214
1598
  <div class="container">
1215
1599
  <header class="header">
1216
- <div style="display: flex; align-items: center; gap: 15px;">
1217
- <img id="report-logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMNCA3bDggNSA4LTUtOC01eiIgZmlsbD0iIzNmNTEiLz48cGF0aCBkPSJNMTIgNkw0IDExbDggNSA4LTUtOC01eiIgZmlsbD0iIzQyODVmNCIvPjxwYXRoIGQ9Ik0xMiAxMGwtOCA1IDggNSA4LTUtOC01eiIgZmlsbD0iIzNkNTViNCIvPjwvc3ZnPg==" alt="Logo" style="height: 40px;">
1218
- <h1>
1219
- Playwright Pulse Report
1220
- </h1>
1600
+ <div class="header-title">
1601
+ <img id="report-logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMNCA3bDggNSA4LTUtOC01eiIgZmlsbD0iIzNmNTFiNSIvPjxwYXRoIGQ9Ik0xMiA2TDQgMTFsOCA1IDgtNS04LTV6IiBmaWxsPSIjNDI4NWY0Ii8+PHBhdGggZD0iTTEyIDEwbC04IDUgOCA1IDgtNS04LTV6IiBmaWxsPSIjM2Q1NWI0Ii8+PC9zdmc+" alt="Report Logo">
1602
+ <h1>Playwright Pulse Report</h1>
1221
1603
  </div>
1222
1604
  <div class="run-info">
1223
1605
  <strong>Run Date:</strong> ${formatDate(
@@ -1232,328 +1614,532 @@ function generateHTML(reportData) {
1232
1614
  <div class="tabs">
1233
1615
  <button class="tab-button active" data-tab="dashboard">Dashboard</button>
1234
1616
  <button class="tab-button" data-tab="test-runs">Test Run Summary</button>
1617
+ <button class="tab-button" data-tab="test-history">Test History</button>
1235
1618
  <button class="tab-button" data-tab="test-ai">AI Analysis</button>
1236
1619
  </div>
1237
1620
 
1238
1621
  <div id="dashboard" class="tab-content active">
1239
1622
  <div class="dashboard-grid">
1240
1623
  <div class="summary-card">
1241
- <h3>Total Tests</h3>
1242
- <div class="value">${runSummary.totalTests}</div>
1624
+ <h3>Total Tests</h3><div class="value">${
1625
+ runSummary.totalTests
1626
+ }</div>
1243
1627
  </div>
1244
1628
  <div class="summary-card status-passed">
1245
- <h3>Passed</h3>
1246
- <div class="value">${runSummary.passed}</div>
1247
- <div class="trend">${passPercentage}%</div>
1629
+ <h3>Passed</h3><div class="value">${runSummary.passed}</div>
1630
+ <div class="trend-percentage">${passPercentage}%</div>
1248
1631
  </div>
1249
1632
  <div class="summary-card status-failed">
1250
- <h3>Failed</h3>
1251
- <div class="value">${runSummary.failed}</div>
1252
- <div class="trend">${failPercentage}%</div>
1633
+ <h3>Failed</h3><div class="value">${runSummary.failed}</div>
1634
+ <div class="trend-percentage">${failPercentage}%</div>
1253
1635
  </div>
1254
1636
  <div class="summary-card status-skipped">
1255
- <h3>Skipped</h3>
1256
- <div class="value">${runSummary.skipped}</div>
1257
- <div class="trend">${skipPercentage}%</div>
1637
+ <h3>Skipped</h3><div class="value">${
1638
+ runSummary.skipped || 0
1639
+ }</div>
1640
+ <div class="trend-percentage">${skipPercentage}%</div>
1258
1641
  </div>
1259
- </div>
1260
- <div class="dashboard-bottom">
1261
- ${generatePieChartD3([
1262
- { label: "Passed", value: runSummary.passed },
1263
- { label: "Failed", value: runSummary.failed },
1264
- { label: "Skipped", value: runSummary.skipped },
1265
- ])}
1266
- ${generateSuitesWidget(suitesData)}
1267
- <div class="summary-cards">
1268
- <div class="summary-card avg-time">
1269
- <h3>Avg. Time</h3>
1270
- <div class="value">${avgTestDuration}</div>
1271
- </div>
1272
- <div class="summary-card avg-time">
1273
- <h3>Total Time</h3>
1274
- <div class="value">${formatDuration(
1275
- runSummary.duration
1276
- )}</div>
1277
- </div>
1278
- </div>
1642
+ <div class="summary-card">
1643
+ <h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div>
1644
+ </div>
1645
+ <div class="summary-card">
1646
+ <h3>Run Duration</h3><div class="value">${formatDuration(
1647
+ runSummary.duration
1648
+ )}</div>
1279
1649
  </div>
1280
1650
  </div>
1651
+ <div class="dashboard-bottom-row">
1652
+ ${generatePieChart(
1653
+ // Changed from generatePieChartD3
1654
+ [
1655
+ { label: "Passed", value: runSummary.passed },
1656
+ { label: "Failed", value: runSummary.failed },
1657
+ { label: "Skipped", value: runSummary.skipped || 0 },
1658
+ ],
1659
+ 400, // Default width
1660
+ 390 // Default height (adjusted for legend + title)
1661
+ )}
1662
+ ${generateSuitesWidget(suitesData)}
1663
+ </div>
1281
1664
  </div>
1282
1665
 
1283
1666
  <div id="test-runs" class="tab-content">
1284
1667
  <div class="filters">
1285
- <input type="text" id="filter-name" placeholder="Search by test name...">
1286
- <select id="filter-status">
1287
- <option value="">All Statuses</option>
1288
- <option value="passed">Passed</option>
1289
- <option value="failed">Failed</option>
1290
- <option value="skipped">Skipped</option>
1291
- </select>
1292
- <select id="filter-browser">
1293
- <option value="">All Browsers</option>
1294
- ${Array.from(
1295
- new Set(results.map((test) => test.browser || "unknown"))
1296
- )
1297
- .map(
1298
- (browser) => `
1299
- <option value="${browser}">${browser}</option>
1300
- `
1301
- )
1302
- .join("")}
1303
- </select>
1304
- <button onclick="expandAllTests()">Expand All</button>
1305
- <button onclick="collapseAllTests()">Collapse All</button>
1306
- </div>
1307
- <div class="test-suites">
1668
+ <input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
1669
+ <select id="filter-status">
1670
+ <option value="">All Statuses</option>
1671
+ <option value="passed">Passed</option>
1672
+ <option value="failed">Failed</option>
1673
+ <option value="skipped">Skipped</option>
1674
+ </select>
1675
+ <select id="filter-browser">
1676
+ <option value="">All Browsers</option>
1677
+ {/* Dynamically generated options will be here */}
1678
+ ${Array.from(
1679
+ new Set((results || []).map((test) => test.browser || "unknown"))
1680
+ )
1681
+ .map(
1682
+ (browser) =>
1683
+ `<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
1684
+ browser
1685
+ )}</option>`
1686
+ )
1687
+ .join("")}
1688
+ </select>
1689
+ <button id="expand-all-tests">Expand All</button>
1690
+ <button id="collapse-all-tests">Collapse All</button>
1691
+ <button id="clear-run-summary-filters" class="clear-filters-btn">Clear Filters</button>
1692
+ </div>
1693
+ <div class="test-cases-list">
1308
1694
  ${generateTestCasesHTML()}
1309
1695
  </div>
1310
1696
  </div>
1697
+
1698
+ <div id="test-history" class="tab-content">
1699
+ <h2 class="tab-main-title">Execution Trends</h2>
1700
+ <div class="trend-charts-row">
1701
+ <div class="trend-chart">
1702
+ <h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
1703
+ ${
1704
+ trendData && trendData.overall && trendData.overall.length > 0
1705
+ ? generateTestTrendsChart(trendData)
1706
+ : '<div class="no-data">Overall trend data not available for test counts.</div>'
1707
+ }
1708
+ </div>
1709
+ <div class="trend-chart">
1710
+ <h3 class="chart-title-header">Execution Duration Trends</h3>
1711
+ ${
1712
+ trendData && trendData.overall && trendData.overall.length > 0
1713
+ ? generateDurationTrendChart(trendData)
1714
+ : '<div class="no-data">Overall trend data not available for durations.</div>'
1715
+ }
1716
+ </div>
1717
+ </div>
1718
+ <h2 class="tab-main-title">Individual Test History</h2>
1719
+ ${
1720
+ trendData &&
1721
+ trendData.testRuns &&
1722
+ Object.keys(trendData.testRuns).length > 0
1723
+ ? generateTestHistoryContent(trendData)
1724
+ : '<div class="no-data">Individual test history data not available.</div>'
1725
+ }
1726
+ </div>
1727
+
1311
1728
  <div id="test-ai" class="tab-content">
1312
- <iframe
1729
+ <iframe
1313
1730
  src="https://ai-test-analyser.netlify.app/"
1314
1731
  width="100%"
1315
- height="1200px"
1316
- style="border: none;">
1732
+ height="100%"
1733
+ frameborder="0"
1734
+ allowfullscreen
1735
+ style="border: none; height: 100vh;">
1317
1736
  </iframe>
1318
1737
  </div>
1738
+ <footer style="
1739
+ padding: 0.5rem;
1740
+ box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
1741
+ text-align: center;
1742
+ font-family: 'Segoe UI', system-ui, sans-serif;
1743
+ ">
1744
+ <div style="
1745
+ display: inline-flex;
1746
+ align-items: center;
1747
+ gap: 0.5rem;
1748
+ color: #333;
1749
+ font-size: 0.9rem;
1750
+ font-weight: 600;
1751
+ letter-spacing: 0.5px;
1752
+ ">
1753
+ <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"/>
1754
+ <span>Created by</span>
1755
+ <a href="https://github.com/Arghajit47"
1756
+ target="_blank"
1757
+ rel="noopener noreferrer"
1758
+ style="
1759
+ color: #7737BF;
1760
+ font-weight: 700;
1761
+ font-style: italic;
1762
+ text-decoration: none;
1763
+ transition: all 0.2s ease;
1764
+ "
1765
+ onmouseover="this.style.color='#BF5C37'"
1766
+ onmouseout="this.style.color='#7737BF'">
1767
+ Arghajit Singha
1768
+ </a>
1769
+ </div>
1770
+ <div style="
1771
+ margin-top: 0.5rem;
1772
+ font-size: 0.75rem;
1773
+ color: #666;
1774
+ ">
1775
+ Crafted with precision
1776
+ </div>
1777
+ </footer>
1319
1778
  </div>
1320
1779
 
1321
- <script>
1322
- // Tab switching functionality
1323
- document.querySelectorAll('.tab-button').forEach(button => {
1324
- button.addEventListener('click', () => {
1325
- // Remove active class from all buttons and contents
1326
- document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
1327
- document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
1328
-
1329
- // Add active class to clicked button and corresponding content
1330
- const tabId = button.getAttribute('data-tab');
1331
- button.classList.add('active');
1332
- document.getElementById(tabId).classList.add('active');
1333
- });
1334
- });
1335
-
1336
- // Test filtering functionality
1337
- function setupFilters() {
1338
- const nameFilter = document.getElementById('filter-name');
1339
- const statusFilter = document.getElementById('filter-status');
1340
- const browserFilter = document.getElementById('filter-browser');
1341
-
1342
- const filterTests = () => {
1343
- const nameValue = nameFilter.value.toLowerCase();
1344
- const statusValue = statusFilter.value;
1345
- const browserValue = browserFilter.value;
1346
-
1347
- document.querySelectorAll('.test-suite').forEach(suite => {
1348
- const name = suite.querySelector('.test-name').textContent.toLowerCase();
1349
- const status = suite.getAttribute('data-status');
1350
- const browser = suite.getAttribute('data-browser');
1351
-
1352
- const nameMatch = name.includes(nameValue);
1353
- const statusMatch = !statusValue || status === statusValue;
1354
- const browserMatch = !browserValue || browser === browserValue;
1355
-
1356
- if (nameMatch && statusMatch && browserMatch) {
1357
- suite.style.display = 'block';
1358
- } else {
1359
- suite.style.display = 'none';
1360
- }
1361
- });
1362
- };
1363
-
1364
- nameFilter.addEventListener('input', filterTests);
1365
- statusFilter.addEventListener('change', filterTests);
1366
- browserFilter.addEventListener('change', filterTests);
1367
- }
1368
-
1369
- // Test expansion functionality
1370
- function toggleTestDetails(header) {
1371
- const content = header.nextElementSibling;
1372
- content.style.display = content.style.display === 'block' ? 'none' : 'block';
1373
- }
1374
-
1375
- // Step expansion functionality
1376
- function toggleStepDetails(header) {
1377
- const details = header.nextElementSibling;
1378
- details.style.display = details.style.display === 'block' ? 'none' : 'block';
1379
-
1380
- // Toggle nested steps if they exist
1381
- const nestedSteps = header.parentElement.querySelector('.nested-steps');
1382
- if (nestedSteps) {
1383
- nestedSteps.style.display = nestedSteps.style.display === 'block' ? 'none' : 'block';
1384
- }
1385
- }
1386
-
1387
- // Expand all tests
1388
- function expandAllTests() {
1389
- document.querySelectorAll('.suite-content').forEach(el => {
1390
- el.style.display = 'block';
1391
- });
1392
- document.querySelectorAll('.step-details').forEach(el => {
1393
- el.style.display = 'block';
1394
- });
1395
- document.querySelectorAll('.nested-steps').forEach(el => {
1396
- el.style.display = 'block';
1397
- });
1398
- }
1399
-
1400
- // Collapse all tests
1401
- function collapseAllTests() {
1402
- document.querySelectorAll('.suite-content').forEach(el => {
1403
- el.style.display = 'none';
1404
- });
1405
- document.querySelectorAll('.step-details').forEach(el => {
1406
- el.style.display = 'none';
1407
- });
1408
- document.querySelectorAll('.nested-steps').forEach(el => {
1409
- el.style.display = 'none';
1410
- });
1411
- }
1412
1780
 
1413
- // Initialize everything when DOM is loaded
1414
- document.addEventListener('DOMContentLoaded', () => {
1415
- setupFilters();
1416
-
1417
- // Make step headers clickable
1418
- document.querySelectorAll('.step-header').forEach(header => {
1419
- header.addEventListener('click', function() {
1420
- toggleStepDetails(this);
1421
- });
1422
- });
1423
-
1424
- // Make test headers clickable
1425
- document.querySelectorAll('.suite-header').forEach(header => {
1426
- header.addEventListener('click', function() {
1427
- toggleTestDetails(this);
1428
- });
1429
- });
1430
- });
1431
-
1432
- // Enhanced expand/collapse functionality
1433
- function toggleTestDetails(header) {
1434
- const content = header.nextElementSibling;
1435
- const isExpanded = content.style.display === 'block';
1436
- content.style.display = isExpanded ? 'none' : 'block';
1437
- header.setAttribute('aria-expanded', !isExpanded);
1781
+ <script>
1782
+ // Ensure formatDuration is globally available
1783
+ if (typeof formatDuration === 'undefined') {
1784
+ function formatDuration(ms) {
1785
+ if (ms === undefined || ms === null || ms < 0) return "0.0s";
1786
+ return (ms / 1000).toFixed(1) + "s";
1787
+ }
1438
1788
  }
1439
1789
 
1440
- function toggleStepDetails(header) {
1441
- const details = header.nextElementSibling;
1442
- const nestedSteps = header.parentElement.querySelector('.nested-steps');
1790
+ function initializeReportInteractivity() {
1791
+ const tabButtons = document.querySelectorAll('.tab-button');
1792
+ const tabContents = document.querySelectorAll('.tab-content');
1793
+ tabButtons.forEach(button => {
1794
+ button.addEventListener('click', () => {
1795
+ tabButtons.forEach(btn => btn.classList.remove('active'));
1796
+ tabContents.forEach(content => content.classList.remove('active'));
1797
+ button.classList.add('active');
1798
+ const tabId = button.getAttribute('data-tab');
1799
+ const activeContent = document.getElementById(tabId);
1800
+ if (activeContent) activeContent.classList.add('active');
1801
+ });
1802
+ });
1443
1803
 
1444
- // Toggle main step details
1445
- const isExpanded = details.style.display === 'block';
1446
- details.style.display = isExpanded ? 'none' : 'block';
1804
+ // --- Test Run Summary Filters ---
1805
+ const nameFilter = document.getElementById('filter-name');
1806
+ const statusFilter = document.getElementById('filter-status');
1807
+ const browserFilter = document.getElementById('filter-browser');
1808
+ const clearRunSummaryFiltersBtn = document.getElementById('clear-run-summary-filters'); // Get the new button
1809
+
1810
+ function filterTestCases() {
1811
+ const nameValue = nameFilter ? nameFilter.value.toLowerCase() : "";
1812
+ const statusValue = statusFilter ? statusFilter.value : "";
1813
+ const browserValue = browserFilter ? browserFilter.value : "";
1814
+
1815
+ document.querySelectorAll('#test-runs .test-case').forEach(testCaseElement => {
1816
+ const titleElement = testCaseElement.querySelector('.test-case-title');
1817
+ const fullTestName = titleElement ? titleElement.getAttribute('title').toLowerCase() : "";
1818
+ const status = testCaseElement.getAttribute('data-status');
1819
+ const browser = testCaseElement.getAttribute('data-browser');
1820
+
1821
+ const nameMatch = fullTestName.includes(nameValue);
1822
+ const statusMatch = !statusValue || status === statusValue;
1823
+ const browserMatch = !browserValue || browser === browserValue;
1824
+
1825
+ testCaseElement.style.display = (nameMatch && statusMatch && browserMatch) ? '' : 'none';
1826
+ });
1827
+ }
1828
+ if(nameFilter) nameFilter.addEventListener('input', filterTestCases);
1829
+ if(statusFilter) statusFilter.addEventListener('change', filterTestCases);
1830
+ if(browserFilter) browserFilter.addEventListener('change', filterTestCases);
1831
+
1832
+ // Event listener for clearing Test Run Summary filters
1833
+ if (clearRunSummaryFiltersBtn) {
1834
+ clearRunSummaryFiltersBtn.addEventListener('click', () => {
1835
+ if (nameFilter) nameFilter.value = '';
1836
+ if (statusFilter) statusFilter.value = '';
1837
+ if (browserFilter) browserFilter.value = '';
1838
+ filterTestCases(); // Re-apply filters (which will show all)
1839
+ });
1840
+ }
1841
+
1842
+ // --- Test History Filters ---
1843
+ const historyNameFilter = document.getElementById('history-filter-name');
1844
+ const historyStatusFilter = document.getElementById('history-filter-status');
1845
+ const clearHistoryFiltersBtn = document.getElementById('clear-history-filters'); // Get the new button
1846
+
1847
+
1848
+ function filterTestHistoryCards() {
1849
+ const nameValue = historyNameFilter ? historyNameFilter.value.toLowerCase() : "";
1850
+ const statusValue = historyStatusFilter ? historyStatusFilter.value : "";
1851
+
1852
+ document.querySelectorAll('.test-history-card').forEach(card => {
1853
+ const testTitle = card.getAttribute('data-test-name').toLowerCase();
1854
+ const latestStatus = card.getAttribute('data-latest-status');
1855
+
1856
+ const nameMatch = testTitle.includes(nameValue);
1857
+ const statusMatch = !statusValue || latestStatus === statusValue;
1858
+
1859
+ card.style.display = (nameMatch && statusMatch) ? '' : 'none';
1860
+ });
1861
+ }
1862
+ if(historyNameFilter) historyNameFilter.addEventListener('input', filterTestHistoryCards);
1863
+ if(historyStatusFilter) historyStatusFilter.addEventListener('change', filterTestHistoryCards);
1864
+
1865
+ // Event listener for clearing Test History filters
1866
+ if (clearHistoryFiltersBtn) {
1867
+ clearHistoryFiltersBtn.addEventListener('click', () => {
1868
+ if (historyNameFilter) historyNameFilter.value = '';
1869
+ if (historyStatusFilter) historyStatusFilter.value = '';
1870
+ filterTestHistoryCards(); // Re-apply filters (which will show all)
1871
+ });
1872
+ }
1873
+
1874
+ // --- Expand/Collapse and Toggle Details Logic (remains the same) ---
1875
+ function toggleElementDetails(headerElement, contentSelector) {
1876
+ let contentElement;
1877
+ if (headerElement.classList.contains('test-case-header')) {
1878
+ contentElement = headerElement.parentElement.querySelector('.test-case-content');
1879
+ } else if (headerElement.classList.contains('step-header')) {
1880
+ contentElement = headerElement.nextElementSibling;
1881
+ if (!contentElement || !contentElement.matches(contentSelector || '.step-details')) {
1882
+ contentElement = null;
1883
+ }
1884
+ }
1447
1885
 
1448
- // Toggle nested steps if they exist
1449
- if (nestedSteps) {
1450
- nestedSteps.style.display = isExpanded ? 'none' : 'block';
1451
- }
1452
-
1453
- header.setAttribute('aria-expanded', !isExpanded);
1454
- }
1886
+ if (contentElement) {
1887
+ const isExpanded = contentElement.style.display === 'block';
1888
+ contentElement.style.display = isExpanded ? 'none' : 'block';
1889
+ headerElement.setAttribute('aria-expanded', String(!isExpanded));
1890
+ }
1891
+ }
1892
+
1893
+ document.querySelectorAll('#test-runs .test-case-header').forEach(header => {
1894
+ header.addEventListener('click', () => toggleElementDetails(header));
1895
+ });
1896
+ document.querySelectorAll('#test-runs .step-header').forEach(header => {
1897
+ header.addEventListener('click', () => toggleElementDetails(header, '.step-details'));
1898
+ });
1455
1899
 
1456
- function expandAllTests() {
1457
- document.querySelectorAll('.suite-content').forEach(el => {
1458
- el.style.display = 'block';
1459
- });
1460
- document.querySelectorAll('.step-details').forEach(el => {
1461
- el.style.display = 'block';
1462
- });
1463
- document.querySelectorAll('.nested-steps').forEach(el => {
1464
- el.style.display = 'block';
1465
- });
1466
- document.querySelectorAll('[aria-expanded]').forEach(el => {
1467
- el.setAttribute('aria-expanded', 'true');
1468
- });
1469
- }
1900
+ const expandAllBtn = document.getElementById('expand-all-tests');
1901
+ const collapseAllBtn = document.getElementById('collapse-all-tests');
1470
1902
 
1471
- function collapseAllTests() {
1472
- document.querySelectorAll('.suite-content').forEach(el => {
1473
- el.style.display = 'none';
1474
- });
1475
- document.querySelectorAll('.step-details').forEach(el => {
1476
- el.style.display = 'none';
1477
- });
1478
- document.querySelectorAll('.nested-steps').forEach(el => {
1479
- el.style.display = 'none';
1480
- });
1481
- document.querySelectorAll('[aria-expanded]').forEach(el => {
1482
- el.setAttribute('aria-expanded', 'false');
1483
- });
1903
+ function setAllTestRunDetailsVisibility(displayMode, ariaState) {
1904
+ document.querySelectorAll('#test-runs .test-case-content').forEach(el => el.style.display = displayMode);
1905
+ document.querySelectorAll('#test-runs .step-details').forEach(el => el.style.display = displayMode);
1906
+ document.querySelectorAll('#test-runs .test-case-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
1907
+ document.querySelectorAll('#test-runs .step-header[aria-expanded]').forEach(el => el.setAttribute('aria-expanded', ariaState));
1908
+ }
1909
+
1910
+ if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
1911
+ if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
1484
1912
  }
1913
+ document.addEventListener('DOMContentLoaded', initializeReportInteractivity);
1914
+ </script>
1915
+ </body>
1916
+ </html>
1917
+ `;
1918
+ }
1485
1919
 
1486
- // Initialize all interactive elements
1487
- function initializeInteractiveElements() {
1488
- // Test headers
1489
- document.querySelectorAll('.suite-header').forEach(header => {
1490
- header.addEventListener('click', () => toggleTestDetails(header));
1491
- header.setAttribute('role', 'button');
1492
- header.setAttribute('aria-expanded', 'false');
1920
+ async function runScript(scriptPath) {
1921
+ return new Promise((resolve, reject) => {
1922
+ console.log(chalk.blue(`Executing script: ${scriptPath}...`));
1923
+ const process = fork(scriptPath, [], {
1924
+ stdio: "inherit",
1493
1925
  });
1494
1926
 
1495
- // Step headers
1496
- document.querySelectorAll('.step-header').forEach(header => {
1497
- header.addEventListener('click', () => toggleStepDetails(header));
1498
- header.setAttribute('role', 'button');
1499
- header.setAttribute('aria-expanded', 'false');
1927
+ process.on("error", (err) => {
1928
+ console.error(chalk.red(`Failed to start script: ${scriptPath}`), err);
1929
+ reject(err);
1500
1930
  });
1501
1931
 
1502
- // Filter buttons
1503
- document.getElementById('filter-name').addEventListener('input', filterTests);
1504
- document.getElementById('filter-status').addEventListener('change', filterTests);
1505
- document.getElementById('filter-browser').addEventListener('change', filterTests);
1506
- }
1507
-
1508
- // Initialize when DOM is loaded
1509
- document.addEventListener('DOMContentLoaded', initializeInteractiveElements);
1510
- </script>
1511
- </body>
1512
- </html>
1513
- `;
1932
+ process.on("exit", (code) => {
1933
+ if (code === 0) {
1934
+ console.log(chalk.green(`Script ${scriptPath} finished successfully.`));
1935
+ resolve();
1936
+ } else {
1937
+ const errorMessage = `Script ${scriptPath} exited with code ${code}.`;
1938
+ console.error(chalk.red(errorMessage));
1939
+ reject(new Error(errorMessage));
1940
+ }
1941
+ });
1942
+ });
1514
1943
  }
1515
1944
 
1516
- // [Keep the rest of the file unchanged...]
1945
+ async function main() {
1946
+ const __filename = fileURLToPath(import.meta.url);
1947
+ const __dirname = path.dirname(__filename);
1517
1948
 
1518
- // [Rest of the file remains the same...]
1949
+ // Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
1950
+ const archiveRunScriptPath = path.resolve(
1951
+ __dirname,
1952
+ "generate-trend.mjs" // Keeping the filename as per your request
1953
+ );
1519
1954
 
1520
- // Main execution function
1521
- async function main() {
1522
1955
  const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
1523
- const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE);
1956
+ const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
1524
1957
  const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
1525
1958
 
1526
- console.log(chalk.blue(`Generating enhanced static report in: ${outputDir}`));
1959
+ const historyDir = path.join(outputDir, "history"); // Directory for historical JSON files
1960
+ const HISTORY_FILE_PREFIX = "trend-"; // Match prefix used in archiving script
1961
+ const MAX_HISTORY_FILES_TO_LOAD_FOR_REPORT = 15; // How many historical runs to show in the report
1527
1962
 
1528
- let reportData;
1963
+ console.log(chalk.blue(`Starting static HTML report generation...`));
1964
+ console.log(chalk.blue(`Output directory set to: ${outputDir}`));
1965
+
1966
+ // Step 1: Ensure current run data is archived to the history folder
1967
+ try {
1968
+ await runScript(archiveRunScriptPath); // This script now handles JSON history
1969
+ console.log(
1970
+ chalk.green("Current run data archiving to history completed.")
1971
+ );
1972
+ } catch (error) {
1973
+ console.error(
1974
+ chalk.red(
1975
+ "Failed to archive current run data. Report might use stale or incomplete historical trends."
1976
+ ),
1977
+ error
1978
+ );
1979
+ // You might decide to proceed or exit depending on the importance of historical data
1980
+ }
1981
+
1982
+ // Step 2: Load current run's data (for non-trend sections of the report)
1983
+ let currentRunReportData; // Data for the run being reported
1529
1984
  try {
1530
1985
  const jsonData = await fs.readFile(reportJsonPath, "utf-8");
1531
- reportData = JSON.parse(jsonData);
1986
+ currentRunReportData = JSON.parse(jsonData);
1532
1987
  if (
1533
- !reportData ||
1534
- typeof reportData !== "object" ||
1535
- !Array.isArray(reportData.results)
1988
+ !currentRunReportData ||
1989
+ typeof currentRunReportData !== "object" ||
1990
+ !currentRunReportData.results
1536
1991
  ) {
1537
- throw new Error("Invalid report JSON structure.");
1992
+ throw new Error(
1993
+ "Invalid report JSON structure. 'results' field is missing or invalid."
1994
+ );
1995
+ }
1996
+ if (!Array.isArray(currentRunReportData.results)) {
1997
+ currentRunReportData.results = [];
1998
+ console.warn(
1999
+ chalk.yellow(
2000
+ "Warning: 'results' field in current run JSON was not an array. Treated as empty."
2001
+ )
2002
+ );
1538
2003
  }
1539
2004
  } catch (error) {
1540
- console.error(chalk.red(`Error: ${error.message}`));
1541
- process.exit(1);
2005
+ console.error(
2006
+ chalk.red(
2007
+ `Critical Error: Could not read or parse main report JSON at ${reportJsonPath}: ${error.message}`
2008
+ )
2009
+ );
2010
+ process.exit(1); // Exit if the main report for the current run is missing/invalid
2011
+ }
2012
+
2013
+ // Step 3: Load historical data for trends
2014
+ let historicalRuns = []; // Array of past PlaywrightPulseReport objects
2015
+ try {
2016
+ await fs.access(historyDir); // Check if history directory exists
2017
+ const allHistoryFiles = await fs.readdir(historyDir);
2018
+
2019
+ const jsonHistoryFiles = allHistoryFiles
2020
+ .filter(
2021
+ (file) => file.startsWith(HISTORY_FILE_PREFIX) && file.endsWith(".json")
2022
+ )
2023
+ .map((file) => {
2024
+ const timestampPart = file
2025
+ .replace(HISTORY_FILE_PREFIX, "")
2026
+ .replace(".json", "");
2027
+ return {
2028
+ name: file,
2029
+ path: path.join(historyDir, file),
2030
+ timestamp: parseInt(timestampPart, 10),
2031
+ };
2032
+ })
2033
+ .filter((file) => !isNaN(file.timestamp))
2034
+ .sort((a, b) => b.timestamp - a.timestamp); // Sort newest first to easily pick the latest N
2035
+
2036
+ const filesToLoadForTrend = jsonHistoryFiles.slice(
2037
+ 0,
2038
+ MAX_HISTORY_FILES_TO_LOAD_FOR_REPORT
2039
+ );
2040
+
2041
+ for (const fileMeta of filesToLoadForTrend) {
2042
+ try {
2043
+ const fileContent = await fs.readFile(fileMeta.path, "utf-8");
2044
+ const runJsonData = JSON.parse(fileContent); // Each file IS a PlaywrightPulseReport
2045
+ historicalRuns.push(runJsonData);
2046
+ } catch (fileReadError) {
2047
+ console.warn(
2048
+ chalk.yellow(
2049
+ `Could not read/parse history file ${fileMeta.name}: ${fileReadError.message}`
2050
+ )
2051
+ );
2052
+ }
2053
+ }
2054
+ // Reverse to have oldest first for chart data series (if charts expect chronological)
2055
+ historicalRuns.reverse();
2056
+ console.log(
2057
+ chalk.green(
2058
+ `Loaded ${historicalRuns.length} historical run(s) for trend analysis.`
2059
+ )
2060
+ );
2061
+ } catch (error) {
2062
+ if (error.code === "ENOENT") {
2063
+ console.warn(
2064
+ chalk.yellow(
2065
+ `History directory '${historyDir}' not found. No historical trends will be displayed.`
2066
+ )
2067
+ );
2068
+ } else {
2069
+ console.warn(
2070
+ chalk.yellow(
2071
+ `Error loading historical data from '${historyDir}': ${error.message}`
2072
+ )
2073
+ );
2074
+ }
1542
2075
  }
1543
2076
 
2077
+ // Step 4: Prepare trendData object in the format expected by chart functions
2078
+ const trendData = {
2079
+ overall: [], // For overall run summaries (passed, failed, skipped, duration over time)
2080
+ testRuns: {}, // For individual test history (key: "test run <run_timestamp_ms>", value: array of test result summaries)
2081
+ };
2082
+
2083
+ if (historicalRuns.length > 0) {
2084
+ historicalRuns.forEach((histRunReport) => {
2085
+ // histRunReport is a full PlaywrightPulseReport object from a past run
2086
+ if (histRunReport.run) {
2087
+ // Ensure timestamp is a Date object for correct sorting/comparison later if needed by charts
2088
+ const runTimestamp = new Date(histRunReport.run.timestamp);
2089
+ trendData.overall.push({
2090
+ runId: runTimestamp.getTime(), // Use timestamp as a unique ID for this context
2091
+ timestamp: runTimestamp,
2092
+ duration: histRunReport.run.duration,
2093
+ totalTests: histRunReport.run.totalTests,
2094
+ passed: histRunReport.run.passed,
2095
+ failed: histRunReport.run.failed,
2096
+ skipped: histRunReport.run.skipped || 0,
2097
+ });
2098
+
2099
+ // For generateTestHistoryContent
2100
+ if (histRunReport.results && Array.isArray(histRunReport.results)) {
2101
+ const runKeyForTestHistory = `test run ${runTimestamp.getTime()}`; // Use timestamp to key test runs
2102
+ trendData.testRuns[runKeyForTestHistory] = histRunReport.results.map(
2103
+ (test) => ({
2104
+ testName: test.name, // Full test name path
2105
+ duration: test.duration,
2106
+ status: test.status,
2107
+ timestamp: new Date(test.startTime), // Assuming test.startTime exists and is what you need
2108
+ })
2109
+ );
2110
+ }
2111
+ }
2112
+ });
2113
+ // Ensure trendData.overall is sorted by timestamp if not already
2114
+ trendData.overall.sort(
2115
+ (a, b) => a.timestamp.getTime() - b.timestamp.getTime()
2116
+ );
2117
+ }
2118
+
2119
+ // Step 5: Generate and write HTML
1544
2120
  try {
1545
- const htmlContent = generateHTML(reportData);
2121
+ // currentRunReportData is for the main content (test list, summary cards of *this* run)
2122
+ // trendData is for the historical charts and test history section
2123
+ const htmlContent = generateHTML(currentRunReportData, trendData);
1546
2124
  await fs.writeFile(reportHtmlPath, htmlContent, "utf-8");
1547
2125
  console.log(
1548
- chalk.green(`Report generated successfully at: ${reportHtmlPath}`)
2126
+ chalk.green.bold(
2127
+ `🎉 Pulse report generated successfully at: ${reportHtmlPath}`
2128
+ )
1549
2129
  );
1550
- console.log(chalk.blue(`You can open it in your browser with:`));
1551
- console.log(chalk.blue(`open ${reportHtmlPath}`));
2130
+ console.log(chalk.gray(`(You can open this file in your browser)`));
1552
2131
  } catch (error) {
1553
- console.error(chalk.red(`Error: ${error.message}`));
2132
+ console.error(chalk.red(`Error generating HTML report: ${error.message}`));
2133
+ console.error(chalk.red(error.stack)); // Log full stack for HTML generation errors
1554
2134
  process.exit(1);
1555
2135
  }
1556
2136
  }
1557
2137
 
1558
- // Run the main function
1559
- main();
2138
+ // Make sure main() is called at the end of your script
2139
+ main().catch((err) => {
2140
+ console.error(
2141
+ chalk.red.bold(`Unhandled error during script execution: ${err.message}`)
2142
+ );
2143
+ console.error(err.stack);
2144
+ process.exit(1);
2145
+ });