@arghajit/dummy 0.3.6 → 0.3.8

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.
@@ -197,7 +197,7 @@ class PlaywrightPulseReporter {
197
197
  };
198
198
  }
199
199
  async onTestEnd(test, result) {
200
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
200
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
201
201
  const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
202
202
  const browserDetails = this.getBrowserDetails(test);
203
203
  const testStatus = convertStatus(result.status, test);
@@ -224,6 +224,16 @@ class PlaywrightPulseReporter {
224
224
  catch (e) {
225
225
  console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
226
226
  }
227
+ // 1. Get Spec File Name
228
+ const specFileName = ((_e = test.location) === null || _e === void 0 ? void 0 : _e.file)
229
+ ? path.basename(test.location.file)
230
+ : "n/a";
231
+ // 2. Get Describe Block Name
232
+ // Check if the immediate parent is a 'describe' block
233
+ let describeBlockName = "n/a";
234
+ if (((_f = test.parent) === null || _f === void 0 ? void 0 : _f.type) === "describe") {
235
+ describeBlockName = test.parent.title;
236
+ }
227
237
  const stdoutMessages = result.stdout.map((item) => typeof item === "string" ? item : item.toString());
228
238
  const stderrMessages = result.stderr.map((item) => typeof item === "string" ? item : item.toString());
229
239
  const maxWorkers = this.config.workers;
@@ -241,18 +251,20 @@ class PlaywrightPulseReporter {
241
251
  const pulseResult = {
242
252
  id: test.id,
243
253
  runId: "TBD",
254
+ describe: describeBlockName,
255
+ spec_file: specFileName,
244
256
  name: test.titlePath().join(" > "),
245
- suiteName: (project === null || project === void 0 ? void 0 : project.name) || ((_e = this.config.projects[0]) === null || _e === void 0 ? void 0 : _e.name) || "Default Suite",
257
+ suiteName: (project === null || project === void 0 ? void 0 : project.name) || ((_g = this.config.projects[0]) === null || _g === void 0 ? void 0 : _g.name) || "Default Suite",
246
258
  status: testStatus,
247
259
  duration: result.duration,
248
260
  startTime: startTime,
249
261
  endTime: endTime,
250
262
  browser: browserDetails,
251
263
  retries: result.retry,
252
- steps: ((_f = result.steps) === null || _f === void 0 ? void 0 : _f.length) ? await processAllSteps(result.steps) : [],
253
- errorMessage: (_g = result.error) === null || _g === void 0 ? void 0 : _g.message,
254
- stackTrace: (_h = result.error) === null || _h === void 0 ? void 0 : _h.stack,
255
- snippet: (_j = result.error) === null || _j === void 0 ? void 0 : _j.snippet,
264
+ steps: ((_h = result.steps) === null || _h === void 0 ? void 0 : _h.length) ? await processAllSteps(result.steps) : [],
265
+ errorMessage: (_j = result.error) === null || _j === void 0 ? void 0 : _j.message,
266
+ stackTrace: (_k = result.error) === null || _k === void 0 ? void 0 : _k.stack,
267
+ snippet: (_l = result.error) === null || _l === void 0 ? void 0 : _l.snippet,
256
268
  codeSnippet: codeSnippet,
257
269
  tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
258
270
  screenshots: [],
@@ -261,7 +273,7 @@ class PlaywrightPulseReporter {
261
273
  attachments: [],
262
274
  stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
263
275
  stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
264
- annotations: ((_k = test.annotations) === null || _k === void 0 ? void 0 : _k.length) > 0 ? test.annotations : undefined,
276
+ annotations: ((_m = test.annotations) === null || _m === void 0 ? void 0 : _m.length) > 0 ? test.annotations : undefined,
265
277
  ...testSpecificData,
266
278
  };
267
279
  for (const [index, attachment] of result.attachments.entries()) {
@@ -278,16 +290,16 @@ class PlaywrightPulseReporter {
278
290
  await this._ensureDirExists(path.dirname(absoluteDestPath));
279
291
  await fs.copyFile(attachment.path, absoluteDestPath);
280
292
  if (attachment.contentType.startsWith("image/")) {
281
- (_l = pulseResult.screenshots) === null || _l === void 0 ? void 0 : _l.push(relativeDestPath);
293
+ (_o = pulseResult.screenshots) === null || _o === void 0 ? void 0 : _o.push(relativeDestPath);
282
294
  }
283
295
  else if (attachment.contentType.startsWith("video/")) {
284
- (_m = pulseResult.videoPath) === null || _m === void 0 ? void 0 : _m.push(relativeDestPath);
296
+ (_p = pulseResult.videoPath) === null || _p === void 0 ? void 0 : _p.push(relativeDestPath);
285
297
  }
286
298
  else if (attachment.name === "trace") {
287
299
  pulseResult.tracePath = relativeDestPath;
288
300
  }
289
301
  else {
290
- (_o = pulseResult.attachments) === null || _o === void 0 ? void 0 : _o.push({
302
+ (_q = pulseResult.attachments) === null || _q === void 0 ? void 0 : _q.push({
291
303
  name: attachment.name,
292
304
  path: relativeDestPath,
293
305
  contentType: attachment.contentType,
@@ -17,6 +17,8 @@ export interface TestStep {
17
17
  }
18
18
  export interface TestResult {
19
19
  id: string;
20
+ describe?: string;
21
+ spec_file?: string;
20
22
  name: string;
21
23
  status: TestStatus;
22
24
  duration: number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arghajit/dummy",
3
3
  "author": "Arghajit Singha",
4
- "version": "0.3.6",
4
+ "version": "0.3.8",
5
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
6
  "homepage": "https://playwright-pulse-report.netlify.app/",
7
7
  "keywords": [
@@ -30,12 +30,12 @@
30
30
  ],
31
31
  "license": "MIT",
32
32
  "bin": {
33
- "generate-pulse-report": "./scripts/generate-static-report.mjs",
34
- "generate-report": "./scripts/generate-report.mjs",
35
- "merge-pulse-report": "./scripts/merge-pulse-report.js",
36
- "send-email": "./scripts/sendReport.mjs",
37
- "generate-trend": "./scripts/generate-trend.mjs",
38
- "generate-email-report": "./scripts/generate-email-report.mjs"
33
+ "generate-pulse-report": "scripts/generate-static-report.mjs",
34
+ "generate-report": "scripts/generate-report.mjs",
35
+ "merge-pulse-report": "scripts/merge-pulse-report.js",
36
+ "send-email": "scripts/sendReport.mjs",
37
+ "generate-trend": "scripts/generate-trend.mjs",
38
+ "generate-email-report": "scripts/generate-email-report.mjs"
39
39
  },
40
40
  "exports": {
41
41
  ".": {
@@ -28,82 +28,95 @@ async function extractOutputDirFromConfig(configPath) {
28
28
  let config;
29
29
 
30
30
  const configDir = dirname(configPath);
31
- const originalDirname = global.__dirname;
32
- const originalFilename = global.__filename;
31
+ // const originalDirname = global.__dirname; // Not strictly needed in ESM context usually, but keeping if you rely on it elsewhere
32
+ // const originalFilename = global.__filename;
33
33
 
34
+ // 1. Try Loading via Import (Existing Logic)
34
35
  try {
35
- global.__dirname = configDir;
36
- global.__filename = configPath;
37
-
38
36
  if (configPath.endsWith(".ts")) {
39
37
  try {
40
38
  const { register } = await import("node:module");
41
39
  const { pathToFileURL } = await import("node:url");
42
-
43
40
  register("ts-node/esm", pathToFileURL("./"));
44
-
45
41
  config = await import(pathToFileURL(configPath).href);
46
42
  } catch (tsError) {
47
- try {
48
- const tsNode = await import("ts-node");
49
- tsNode.register({
50
- transpileOnly: true,
51
- compilerOptions: {
52
- module: "ESNext",
53
- },
54
- });
55
- config = await import(pathToFileURL(configPath).href);
56
- } catch (fallbackError) {
57
- console.error("Failed to load TypeScript config:", fallbackError);
58
- return null;
59
- }
43
+ const tsNode = await import("ts-node");
44
+ tsNode.register({
45
+ transpileOnly: true,
46
+ compilerOptions: { module: "commonjs" },
47
+ });
48
+ config = require(configPath);
60
49
  }
61
50
  } else {
51
+ // Try dynamic import for JS/MJS
62
52
  config = await import(pathToFileURL(configPath).href);
63
53
  }
64
- } finally {
65
- if (originalDirname !== undefined) {
66
- global.__dirname = originalDirname;
67
- } else {
68
- delete global.__dirname;
69
- }
70
- if (originalFilename !== undefined) {
71
- global.__filename = originalFilename;
72
- } else {
73
- delete global.__filename;
54
+
55
+ // Extract from default export or direct export
56
+ if (config && config.default) {
57
+ config = config.default;
74
58
  }
75
- }
76
59
 
77
- const playwrightConfig = config.default || config;
78
-
79
- if (playwrightConfig && Array.isArray(playwrightConfig.reporter)) {
80
- for (const reporterConfig of playwrightConfig.reporter) {
81
- if (Array.isArray(reporterConfig)) {
82
- const [reporterPath, options] = reporterConfig;
83
-
84
- if (
85
- typeof reporterPath === "string" &&
86
- (reporterPath.includes("playwright-pulse-report") ||
87
- reporterPath.includes("@arghajit/playwright-pulse-report") ||
88
- reporterPath.includes("@arghajit/dummy"))
89
- ) {
90
- if (options && options.outputDir) {
91
- const resolvedPath =
92
- typeof options.outputDir === "string"
93
- ? options.outputDir
94
- : options.outputDir;
95
- console.log(`Found outputDir in config: ${resolvedPath}`);
96
- return path.resolve(process.cwd(), resolvedPath);
60
+ if (config) {
61
+ // Check specific reporter config
62
+ if (config.reporter) {
63
+ const reporters = Array.isArray(config.reporter)
64
+ ? config.reporter
65
+ : [config.reporter];
66
+
67
+ for (const reporter of reporters) {
68
+ // reporter can be ["list"] or ["html", { outputFolder: '...' }]
69
+ const reporterName = Array.isArray(reporter)
70
+ ? reporter[0]
71
+ : reporter;
72
+ const reporterOptions = Array.isArray(reporter)
73
+ ? reporter[1]
74
+ : null;
75
+
76
+ if (
77
+ typeof reporterName === "string" &&
78
+ (reporterName.includes("playwright-pulse-report") ||
79
+ reporterName.includes("@arghajit/playwright-pulse-report") ||
80
+ reporterName.includes("@arghajit/dummy"))
81
+ ) {
82
+ if (reporterOptions && reporterOptions.outputDir) {
83
+ // Found it via Import!
84
+ return path.resolve(process.cwd(), reporterOptions.outputDir);
85
+ }
97
86
  }
98
87
  }
99
88
  }
89
+
90
+ // Check global outputDir
91
+ if (config.outputDir) {
92
+ return path.resolve(process.cwd(), config.outputDir);
93
+ }
94
+ }
95
+ } catch (importError) {
96
+ // Import failed (likely the SyntaxError you saw).
97
+ // We suppress this error and fall through to the text-parsing fallback below.
98
+ }
99
+
100
+ // 2. Fallback: Parse file as text (New Logic)
101
+ // This runs if import failed or if import worked but didn't have the specific config
102
+ try {
103
+ const fileContent = fs.readFileSync(configPath, "utf-8");
104
+
105
+ // Regex to find: outputDir: "some/path" or 'some/path' inside the reporter config or global
106
+ // This is a simple heuristic to avoid the "Cannot use import statement" error
107
+ const match = fileContent.match(/outputDir:\s*["']([^"']+)["']/);
108
+
109
+ if (match && match[1]) {
110
+ console.log(`Found outputDir via text parsing: ${match[1]}`);
111
+ return path.resolve(process.cwd(), match[1]);
100
112
  }
113
+ } catch (readError) {
114
+ // If reading fails, just return null silently
101
115
  }
102
116
 
103
- console.log("No matching reporter config found with outputDir");
104
117
  return null;
105
118
  } catch (error) {
106
- console.error("Error extracting outputDir from config:", error);
119
+ // Final safety net: Do not log the stack trace to avoid cluttering the console
107
120
  return null;
108
121
  }
109
122
  }
@@ -350,6 +350,7 @@ function generateTestTrendsChart(trendData) {
350
350
  </script>
351
351
  `;
352
352
  }
353
+ const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
353
354
  function generateDurationTrendChart(trendData) {
354
355
  if (!trendData || !trendData.overall || trendData.overall.length === 0) {
355
356
  return '<div class="no-data">No overall trend data available for durations.</div>';
@@ -363,8 +364,6 @@ function generateDurationTrendChart(trendData) {
363
364
  )}`;
364
365
  const runs = trendData.overall;
365
366
 
366
- const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
367
-
368
367
  const chartDataString = JSON.stringify(runs.map((run) => run.duration));
369
368
  const categoriesString = JSON.stringify(runs.map((run, i) => `Run ${i + 1}`));
370
369
  const runsForTooltip = runs.map((r) => ({
@@ -1644,6 +1643,244 @@ function generateAIFailureAnalyzerTab(results) {
1644
1643
  </div>
1645
1644
  `;
1646
1645
  }
1646
+ /**
1647
+ * Generates a area chart showing the total duration per spec file.
1648
+ * The chart is lazy-loaded and rendered with Highcharts when scrolled into view.
1649
+ *
1650
+ * @param {Array<object>} results - Array of test result objects.
1651
+ * @returns {string} HTML string containing the chart container and lazy-loading script.
1652
+ */
1653
+ function generateSpecDurationChart(results) {
1654
+ if (!results || results.length === 0)
1655
+ return '<div class="no-data">No results available.</div>';
1656
+
1657
+ const specDurations = {};
1658
+ results.forEach((test) => {
1659
+ // Use the dedicated 'spec_file' key
1660
+ const fileName = test.spec_file || "Unknown File";
1661
+
1662
+ if (!specDurations[fileName]) specDurations[fileName] = 0;
1663
+ specDurations[fileName] += test.duration;
1664
+ });
1665
+
1666
+ const categories = Object.keys(specDurations);
1667
+ // We map 'name' here, which we will use in the tooltip later
1668
+ const data = categories.map((cat) => ({
1669
+ y: specDurations[cat],
1670
+ name: cat,
1671
+ }));
1672
+
1673
+ if (categories.length === 0)
1674
+ return '<div class="no-data">No spec data found.</div>';
1675
+
1676
+ const chartId = `specDurChart-${Date.now()}-${Math.random()
1677
+ .toString(36)
1678
+ .substring(2, 7)}`;
1679
+ const renderFunctionName = `renderSpecDurChart_${chartId.replace(/-/g, "_")}`;
1680
+
1681
+ const categoriesStr = JSON.stringify(categories);
1682
+ const dataStr = JSON.stringify(data);
1683
+
1684
+ return `
1685
+ <div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
1686
+ <div class="no-data">Loading Spec Duration Chart...</div>
1687
+ </div>
1688
+ <script>
1689
+ window.${renderFunctionName} = function() {
1690
+ const chartContainer = document.getElementById('${chartId}');
1691
+ if (!chartContainer) return;
1692
+ if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
1693
+ try {
1694
+ chartContainer.innerHTML = '';
1695
+ Highcharts.chart('${chartId}', {
1696
+ chart: { type: 'area', height: 350, backgroundColor: 'transparent' },
1697
+ title: { text: null },
1698
+ xAxis: {
1699
+ categories: ${categoriesStr},
1700
+ visible: false, // 1. HIDE THE X-AXIS
1701
+ title: { text: null },
1702
+ crosshair: true
1703
+ },
1704
+ yAxis: {
1705
+ min: 0,
1706
+ title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
1707
+ labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
1708
+ },
1709
+ legend: { layout: 'horizontal', align: 'center', verticalAlign: 'bottom', itemStyle: { fontSize: '12px', color: 'var(--text-color)' }},
1710
+ plotOptions: { area: { lineWidth: 2.5, states: { hover: { lineWidthPlus: 0 } }, threshold: null }},
1711
+ tooltip: {
1712
+ shared: true,
1713
+ useHTML: true,
1714
+ backgroundColor: 'rgba(10,10,10,0.92)',
1715
+ borderColor: 'rgba(10,10,10,0.92)',
1716
+ style: { color: '#f5f5f5' },
1717
+ formatter: function() {
1718
+ const point = this.points ? this.points[0].point : this.point;
1719
+ const color = point.color || point.series.color;
1720
+
1721
+ // 2. FIX: Use 'point.name' instead of 'this.x' to get the actual filename
1722
+ return '<span style="color:' + color + '">●</span> <b>File: ' + point.name + '</b><br/>' +
1723
+ 'Duration: <b>' + formatDuration(this.y) + '</b>';
1724
+ }
1725
+ },
1726
+ series: [{
1727
+ name: 'Duration',
1728
+ data: ${dataStr},
1729
+ color: 'var(--accent-color-alt)',
1730
+ type: 'area',
1731
+ marker: { symbol: 'circle', enabled: true, radius: 4, states: { hover: { radius: 6, lineWidthPlus: 0 } } },
1732
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(${accentColorAltRGB}, 0.4)'], [1, 'rgba(${accentColorAltRGB}, 0.05)']] },
1733
+ lineWidth: 2.5
1734
+ }],
1735
+ credits: { enabled: false }
1736
+ });
1737
+ } catch (e) { console.error("Error rendering spec chart:", e); }
1738
+ }
1739
+ };
1740
+ </script>
1741
+ `;
1742
+ }
1743
+ /**
1744
+ * Generates a vertical bar chart showing the total duration of each test describe block.
1745
+ * Tests without a describe block or with "n/a" / empty describe names are ignored.
1746
+ * @param {Array<object>} results - Array of test result objects.
1747
+ * @returns {string} HTML string containing the chart container and lazy-loading script.
1748
+ */
1749
+ function generateDescribeDurationChart(results) {
1750
+ if (!results || results.length === 0)
1751
+ return '<div class="no-data">No results available.</div>';
1752
+
1753
+ const describeMap = new Map();
1754
+ let foundAnyDescribe = false;
1755
+
1756
+ results.forEach((test) => {
1757
+ if (test.describe) {
1758
+ const describeName = test.describe;
1759
+ // Filter out invalid describe blocks
1760
+ if (
1761
+ !describeName ||
1762
+ describeName.trim().toLowerCase() === "n/a" ||
1763
+ describeName.trim() === ""
1764
+ ) {
1765
+ return;
1766
+ }
1767
+
1768
+ foundAnyDescribe = true;
1769
+ const fileName = test.spec_file || "Unknown File";
1770
+ const key = fileName + "::" + describeName;
1771
+
1772
+ if (!describeMap.has(key)) {
1773
+ describeMap.set(key, {
1774
+ duration: 0,
1775
+ file: fileName,
1776
+ describe: describeName,
1777
+ });
1778
+ }
1779
+ describeMap.get(key).duration += test.duration;
1780
+ }
1781
+ });
1782
+
1783
+ if (!foundAnyDescribe) {
1784
+ return '<div class="no-data">No valid test describe blocks found.</div>';
1785
+ }
1786
+
1787
+ const categories = [];
1788
+ const data = [];
1789
+
1790
+ for (const [key, val] of describeMap.entries()) {
1791
+ categories.push(val.describe);
1792
+ data.push({
1793
+ y: val.duration,
1794
+ name: val.describe,
1795
+ custom: {
1796
+ fileName: val.file,
1797
+ describeName: val.describe,
1798
+ },
1799
+ });
1800
+ }
1801
+
1802
+ const chartId = `descDurChart-${Date.now()}-${Math.random()
1803
+ .toString(36)
1804
+ .substring(2, 7)}`;
1805
+ const renderFunctionName = `renderDescDurChart_${chartId.replace(/-/g, "_")}`;
1806
+
1807
+ const categoriesStr = JSON.stringify(categories);
1808
+ const dataStr = JSON.stringify(data);
1809
+
1810
+ return `
1811
+ <div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
1812
+ <div class="no-data">Loading Describe Duration Chart...</div>
1813
+ </div>
1814
+ <script>
1815
+ window.${renderFunctionName} = function() {
1816
+ const chartContainer = document.getElementById('${chartId}');
1817
+ if (!chartContainer) return;
1818
+ if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
1819
+ try {
1820
+ chartContainer.innerHTML = '';
1821
+ Highcharts.chart('${chartId}', {
1822
+ chart: {
1823
+ type: 'column', // 1. CHANGED: 'bar' -> 'column' for vertical bars
1824
+ height: 400, // 2. CHANGED: Fixed height works better for vertical charts
1825
+ backgroundColor: 'transparent'
1826
+ },
1827
+ title: { text: null },
1828
+ xAxis: {
1829
+ categories: ${categoriesStr},
1830
+ visible: false, // Hidden as requested
1831
+ title: { text: null },
1832
+ crosshair: true
1833
+ },
1834
+ yAxis: {
1835
+ min: 0,
1836
+ title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
1837
+ labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
1838
+ },
1839
+ legend: { enabled: false },
1840
+ plotOptions: {
1841
+ series: {
1842
+ borderRadius: 4,
1843
+ borderWidth: 0,
1844
+ states: { hover: { brightness: 0.1 }}
1845
+ },
1846
+ column: { pointPadding: 0.2, groupPadding: 0.1 } // Adjust spacing for columns
1847
+ },
1848
+ tooltip: {
1849
+ shared: true,
1850
+ useHTML: true,
1851
+ backgroundColor: 'rgba(10,10,10,0.92)',
1852
+ borderColor: 'rgba(10,10,10,0.92)',
1853
+ style: { color: '#f5f5f5' },
1854
+ formatter: function() {
1855
+ const point = this.points ? this.points[0].point : this.point;
1856
+ const file = (point.custom && point.custom.fileName) ? point.custom.fileName : 'Unknown';
1857
+ const desc = point.name || 'Unknown';
1858
+ const color = point.color || point.series.color;
1859
+
1860
+ return '<span style="color:' + color + '">●</span> <b>Describe: ' + desc + '</b><br/>' +
1861
+ '<span style="opacity: 0.8; font-size: 0.9em; color: #ddd;">File: ' + file + '</span><br/>' +
1862
+ 'Duration: <b>' + formatDuration(point.y) + '</b>';
1863
+ }
1864
+ },
1865
+ series: [{
1866
+ name: 'Duration',
1867
+ data: ${dataStr},
1868
+ color: 'var(--accent-color-alt)',
1869
+ }],
1870
+ credits: { enabled: false }
1871
+ });
1872
+ } catch (e) { console.error("Error rendering describe chart:", e); }
1873
+ }
1874
+ };
1875
+ </script>
1876
+ `;
1877
+ }
1878
+ /**
1879
+ * Generates the HTML content for the report.
1880
+ * @param {object} reportData - The report data object containing run and results.
1881
+ * @param {object} trendData - Optional trend data object for additional trends.
1882
+ * @returns {string} HTML string for the report.
1883
+ */
1647
1884
  function generateHTML(reportData, trendData = null) {
1648
1885
  const { run, results } = reportData;
1649
1886
  const suitesData = getSuitesData(reportData.results || []);
@@ -2572,6 +2809,16 @@ function generateHTML(reportData, trendData = null) {
2572
2809
  }
2573
2810
  </div>
2574
2811
  </div>
2812
+ <div class="trend-charts-row">
2813
+ <div class="trend-chart">
2814
+ <h3 class="chart-title-header">Duration by Spec files</h3>
2815
+ ${generateSpecDurationChart(results)}
2816
+ </div>
2817
+ <div class="trend-chart">
2818
+ <h3 class="chart-title-header">Duration by Test Describe</h3>
2819
+ ${generateDescribeDurationChart(results)}
2820
+ </div>
2821
+ </div>
2575
2822
  <h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
2576
2823
  <div class="trend-charts-row">
2577
2824
  <div class="trend-chart">
@@ -3282,4 +3529,4 @@ main().catch((err) => {
3282
3529
  );
3283
3530
  console.error(err.stack);
3284
3531
  process.exit(1);
3285
- });
3532
+ });
@@ -394,6 +394,7 @@ function generateTestTrendsChart(trendData) {
394
394
  </script>
395
395
  `;
396
396
  }
397
+ const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
397
398
  /**
398
399
  * Generates HTML and JavaScript for a Highcharts area chart to display test duration trends.
399
400
  * @param {object} trendData Data for duration trends.
@@ -413,8 +414,6 @@ function generateDurationTrendChart(trendData) {
413
414
  )}`;
414
415
  const runs = trendData.overall;
415
416
 
416
- const accentColorAltRGB = "255, 152, 0"; // Assuming var(--accent-color-alt) is Orange #FF9800
417
-
418
417
  const chartDataString = JSON.stringify(runs.map((run) => run.duration));
419
418
  const categoriesString = JSON.stringify(runs.map((run, i) => `Run ${i + 1}`));
420
419
  const runsForTooltip = runs.map((r) => ({
@@ -1781,6 +1780,238 @@ function generateAIFailureAnalyzerTab(results) {
1781
1780
  </div>
1782
1781
  `;
1783
1782
  }
1783
+ /**
1784
+ * Generates a area chart showing the total duration per spec file.
1785
+ * The chart is lazy-loaded and rendered with Highcharts when scrolled into view.
1786
+ *
1787
+ * @param {Array<object>} results - Array of test result objects.
1788
+ * @returns {string} HTML string containing the chart container and lazy-loading script.
1789
+ */
1790
+ function generateSpecDurationChart(results) {
1791
+ if (!results || results.length === 0)
1792
+ return '<div class="no-data">No results available.</div>';
1793
+
1794
+ const specDurations = {};
1795
+ results.forEach((test) => {
1796
+ // Use the dedicated 'spec_file' key
1797
+ const fileName = test.spec_file || "Unknown File";
1798
+
1799
+ if (!specDurations[fileName]) specDurations[fileName] = 0;
1800
+ specDurations[fileName] += test.duration;
1801
+ });
1802
+
1803
+ const categories = Object.keys(specDurations);
1804
+ // We map 'name' here, which we will use in the tooltip later
1805
+ const data = categories.map((cat) => ({
1806
+ y: specDurations[cat],
1807
+ name: cat,
1808
+ }));
1809
+
1810
+ if (categories.length === 0)
1811
+ return '<div class="no-data">No spec data found.</div>';
1812
+
1813
+ const chartId = `specDurChart-${Date.now()}-${Math.random()
1814
+ .toString(36)
1815
+ .substring(2, 7)}`;
1816
+ const renderFunctionName = `renderSpecDurChart_${chartId.replace(/-/g, "_")}`;
1817
+
1818
+ const categoriesStr = JSON.stringify(categories);
1819
+ const dataStr = JSON.stringify(data);
1820
+
1821
+ return `
1822
+ <div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
1823
+ <div class="no-data">Loading Spec Duration Chart...</div>
1824
+ </div>
1825
+ <script>
1826
+ window.${renderFunctionName} = function() {
1827
+ const chartContainer = document.getElementById('${chartId}');
1828
+ if (!chartContainer) return;
1829
+ if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
1830
+ try {
1831
+ chartContainer.innerHTML = '';
1832
+ Highcharts.chart('${chartId}', {
1833
+ chart: { type: 'area', height: 350, backgroundColor: 'transparent' },
1834
+ title: { text: null },
1835
+ xAxis: {
1836
+ categories: ${categoriesStr},
1837
+ visible: false, // 1. HIDE THE X-AXIS
1838
+ title: { text: null },
1839
+ crosshair: true
1840
+ },
1841
+ yAxis: {
1842
+ min: 0,
1843
+ title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
1844
+ labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
1845
+ },
1846
+ legend: { layout: 'horizontal', align: 'center', verticalAlign: 'bottom', itemStyle: { fontSize: '12px', color: 'var(--text-color)' }},
1847
+ plotOptions: { area: { lineWidth: 2.5, states: { hover: { lineWidthPlus: 0 } }, threshold: null }},
1848
+ tooltip: {
1849
+ shared: true,
1850
+ useHTML: true,
1851
+ backgroundColor: 'rgba(10,10,10,0.92)',
1852
+ borderColor: 'rgba(10,10,10,0.92)',
1853
+ style: { color: '#f5f5f5' },
1854
+ formatter: function() {
1855
+ const point = this.points ? this.points[0].point : this.point;
1856
+ const color = point.color || point.series.color;
1857
+
1858
+ // 2. FIX: Use 'point.name' instead of 'this.x' to get the actual filename
1859
+ return '<span style="color:' + color + '">●</span> <b>File: ' + point.name + '</b><br/>' +
1860
+ 'Duration: <b>' + formatDuration(this.y) + '</b>';
1861
+ }
1862
+ },
1863
+ series: [{
1864
+ name: 'Duration',
1865
+ data: ${dataStr},
1866
+ color: 'var(--accent-color-alt)',
1867
+ type: 'area',
1868
+ marker: { symbol: 'circle', enabled: true, radius: 4, states: { hover: { radius: 6, lineWidthPlus: 0 } } },
1869
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(${accentColorAltRGB}, 0.4)'], [1, 'rgba(${accentColorAltRGB}, 0.05)']] },
1870
+ lineWidth: 2.5
1871
+ }],
1872
+ credits: { enabled: false }
1873
+ });
1874
+ } catch (e) { console.error("Error rendering spec chart:", e); }
1875
+ }
1876
+ };
1877
+ </script>
1878
+ `;
1879
+ }
1880
+ /**
1881
+ * Generates a vertical bar chart showing the total duration of each test describe block.
1882
+ * Tests without a describe block or with "n/a" / empty describe names are ignored.
1883
+ * @param {Array<object>} results - Array of test result objects.
1884
+ * @returns {string} HTML string containing the chart container and lazy-loading script.
1885
+ */
1886
+ function generateDescribeDurationChart(results) {
1887
+ if (!results || results.length === 0)
1888
+ return '<div class="no-data">No results available.</div>';
1889
+
1890
+ const describeMap = new Map();
1891
+ let foundAnyDescribe = false;
1892
+
1893
+ results.forEach((test) => {
1894
+ if (test.describe) {
1895
+ const describeName = test.describe;
1896
+ // Filter out invalid describe blocks
1897
+ if (
1898
+ !describeName ||
1899
+ describeName.trim().toLowerCase() === "n/a" ||
1900
+ describeName.trim() === ""
1901
+ ) {
1902
+ return;
1903
+ }
1904
+
1905
+ foundAnyDescribe = true;
1906
+ const fileName = test.spec_file || "Unknown File";
1907
+ const key = fileName + "::" + describeName;
1908
+
1909
+ if (!describeMap.has(key)) {
1910
+ describeMap.set(key, {
1911
+ duration: 0,
1912
+ file: fileName,
1913
+ describe: describeName,
1914
+ });
1915
+ }
1916
+ describeMap.get(key).duration += test.duration;
1917
+ }
1918
+ });
1919
+
1920
+ if (!foundAnyDescribe) {
1921
+ return '<div class="no-data">No valid test describe blocks found.</div>';
1922
+ }
1923
+
1924
+ const categories = [];
1925
+ const data = [];
1926
+
1927
+ for (const [key, val] of describeMap.entries()) {
1928
+ categories.push(val.describe);
1929
+ data.push({
1930
+ y: val.duration,
1931
+ name: val.describe,
1932
+ custom: {
1933
+ fileName: val.file,
1934
+ describeName: val.describe,
1935
+ },
1936
+ });
1937
+ }
1938
+
1939
+ const chartId = `descDurChart-${Date.now()}-${Math.random()
1940
+ .toString(36)
1941
+ .substring(2, 7)}`;
1942
+ const renderFunctionName = `renderDescDurChart_${chartId.replace(/-/g, "_")}`;
1943
+
1944
+ const categoriesStr = JSON.stringify(categories);
1945
+ const dataStr = JSON.stringify(data);
1946
+
1947
+ return `
1948
+ <div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
1949
+ <div class="no-data">Loading Describe Duration Chart...</div>
1950
+ </div>
1951
+ <script>
1952
+ window.${renderFunctionName} = function() {
1953
+ const chartContainer = document.getElementById('${chartId}');
1954
+ if (!chartContainer) return;
1955
+ if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
1956
+ try {
1957
+ chartContainer.innerHTML = '';
1958
+ Highcharts.chart('${chartId}', {
1959
+ chart: {
1960
+ type: 'column', // 1. CHANGED: 'bar' -> 'column' for vertical bars
1961
+ height: 400, // 2. CHANGED: Fixed height works better for vertical charts
1962
+ backgroundColor: 'transparent'
1963
+ },
1964
+ title: { text: null },
1965
+ xAxis: {
1966
+ categories: ${categoriesStr},
1967
+ visible: false, // Hidden as requested
1968
+ title: { text: null },
1969
+ crosshair: true
1970
+ },
1971
+ yAxis: {
1972
+ min: 0,
1973
+ title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
1974
+ labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
1975
+ },
1976
+ legend: { enabled: false },
1977
+ plotOptions: {
1978
+ series: {
1979
+ borderRadius: 4,
1980
+ borderWidth: 0,
1981
+ states: { hover: { brightness: 0.1 }}
1982
+ },
1983
+ column: { pointPadding: 0.2, groupPadding: 0.1 } // Adjust spacing for columns
1984
+ },
1985
+ tooltip: {
1986
+ shared: true,
1987
+ useHTML: true,
1988
+ backgroundColor: 'rgba(10,10,10,0.92)',
1989
+ borderColor: 'rgba(10,10,10,0.92)',
1990
+ style: { color: '#f5f5f5' },
1991
+ formatter: function() {
1992
+ const point = this.points ? this.points[0].point : this.point;
1993
+ const file = (point.custom && point.custom.fileName) ? point.custom.fileName : 'Unknown';
1994
+ const desc = point.name || 'Unknown';
1995
+ const color = point.color || point.series.color;
1996
+
1997
+ return '<span style="color:' + color + '">●</span> <b>Describe: ' + desc + '</b><br/>' +
1998
+ '<span style="opacity: 0.8; font-size: 0.9em; color: #ddd;">File: ' + file + '</span><br/>' +
1999
+ 'Duration: <b>' + formatDuration(point.y) + '</b>';
2000
+ }
2001
+ },
2002
+ series: [{
2003
+ name: 'Duration',
2004
+ data: ${dataStr},
2005
+ color: 'var(--accent-color-alt)',
2006
+ }],
2007
+ credits: { enabled: false }
2008
+ });
2009
+ } catch (e) { console.error("Error rendering describe chart:", e); }
2010
+ }
2011
+ };
2012
+ </script>
2013
+ `;
2014
+ }
1784
2015
  /**
1785
2016
  * Generates the HTML report.
1786
2017
  * @param {object} reportData - The data for the report.
@@ -2574,6 +2805,16 @@ aspect-ratio: 16 / 9;
2574
2805
  }
2575
2806
  </div>
2576
2807
  </div>
2808
+ <div class="trend-charts-row">
2809
+ <div class="trend-chart">
2810
+ <h3 class="chart-title-header">Duration by Spec files</h3>
2811
+ ${generateSpecDurationChart(results)}
2812
+ </div>
2813
+ <div class="trend-chart">
2814
+ <h3 class="chart-title-header">Duration by Test Describe</h3>
2815
+ ${generateDescribeDurationChart(results)}
2816
+ </div>
2817
+ </div>
2577
2818
  <h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
2578
2819
  <div class="trend-charts-row">
2579
2820
  <div class="trend-chart">