@arghajit/dummy 0.1.0-beta-13 → 0.1.0-beta-14

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