@arghajit/dummy 0.3.6 → 0.3.7

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.7",
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
  ".": {
@@ -1644,6 +1644,232 @@ function generateAIFailureAnalyzerTab(results) {
1644
1644
  </div>
1645
1645
  `;
1646
1646
  }
1647
+ // --- NEW CHARTS HELPER FUNCTIONS (Style: Line Chart + Enhanced Tooltips) ---
1648
+
1649
+ function generateSpecDurationChart(results) {
1650
+ if (!results || results.length === 0)
1651
+ return '<div class="no-data">No results available.</div>';
1652
+
1653
+ const specDurations = {};
1654
+ results.forEach((test) => {
1655
+ let fileName = "Unknown";
1656
+ if (test.location && test.location.file) {
1657
+ fileName = test.location.file.split(path.sep).pop();
1658
+ } else {
1659
+ const parts = test.name.split(" > ");
1660
+ fileName = parts[0];
1661
+ }
1662
+
1663
+ if (!specDurations[fileName]) specDurations[fileName] = 0;
1664
+ specDurations[fileName] += test.duration;
1665
+ });
1666
+
1667
+ const categories = Object.keys(specDurations);
1668
+ // Map data to objects to pass metadata if needed, though categories usually handle this.
1669
+ const data = categories.map((cat) => ({
1670
+ y: specDurations[cat],
1671
+ name: cat,
1672
+ }));
1673
+
1674
+ if (categories.length === 0)
1675
+ return '<div class="no-data">No spec data found.</div>';
1676
+
1677
+ const chartId = `specDurChart-${Date.now()}-${Math.random()
1678
+ .toString(36)
1679
+ .substring(2, 7)}`;
1680
+ const renderFunctionName = `renderSpecDurChart_${chartId.replace(/-/g, "_")}`;
1681
+
1682
+ const categoriesStr = JSON.stringify(categories);
1683
+ const dataStr = JSON.stringify(data);
1684
+
1685
+ return `
1686
+ <div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
1687
+ <div class="no-data">Loading Spec Duration Chart...</div>
1688
+ </div>
1689
+ <script>
1690
+ window.${renderFunctionName} = function() {
1691
+ const chartContainer = document.getElementById('${chartId}');
1692
+ if (!chartContainer) return;
1693
+ if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
1694
+ try {
1695
+ chartContainer.innerHTML = '';
1696
+ Highcharts.chart('${chartId}', {
1697
+ chart: { type: 'line', height: 350, backgroundColor: 'transparent' },
1698
+ title: { text: null },
1699
+ xAxis: {
1700
+ categories: ${categoriesStr},
1701
+ title: { text: null },
1702
+ crosshair: true,
1703
+ labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' } }
1704
+ },
1705
+ yAxis: {
1706
+ min: 0,
1707
+ title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
1708
+ labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
1709
+ },
1710
+ legend: { enabled: false },
1711
+ plotOptions: {
1712
+ series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}},
1713
+ line: { lineWidth: 2.5 }
1714
+ },
1715
+ tooltip: {
1716
+ shared: true,
1717
+ useHTML: true,
1718
+ backgroundColor: 'rgba(10,10,10,0.92)',
1719
+ borderColor: 'rgba(10,10,10,0.92)',
1720
+ style: { color: '#f5f5f5' },
1721
+ formatter: function() {
1722
+ // 'this.x' is the File Name (category)
1723
+ const point = this.points ? this.points[0].point : this.point;
1724
+ const color = point.color || point.series.color;
1725
+
1726
+ return '<span style="color:' + color + '">●</span> <b>File: ' + this.x + '</b><br/>' +
1727
+ 'Duration: <b>' + formatDuration(this.y) + '</b>';
1728
+ }
1729
+ },
1730
+ series: [{
1731
+ name: 'Duration',
1732
+ data: ${dataStr},
1733
+ color: 'var(--accent-color-alt)', // Orange theme
1734
+ marker: { symbol: 'circle' }
1735
+ }],
1736
+ credits: { enabled: false }
1737
+ });
1738
+ } catch (e) { console.error("Error rendering spec chart:", e); }
1739
+ }
1740
+ };
1741
+ </script>
1742
+ `;
1743
+ }
1744
+
1745
+ function generateDescribeDurationChart(results) {
1746
+ if (!results || results.length === 0)
1747
+ return '<div class="no-data">No results available.</div>';
1748
+
1749
+ // We need to group by (File + Describe) to handle same-named describes in different files
1750
+ const describeMap = new Map();
1751
+ let foundAnyDescribe = false;
1752
+
1753
+ results.forEach((test) => {
1754
+ const parts = test.name.split(" > ");
1755
+ if (parts.length > 2) {
1756
+ foundAnyDescribe = true;
1757
+
1758
+ // Extract Filename
1759
+ let fileName = "Unknown";
1760
+ if (test.location && test.location.file) {
1761
+ fileName = test.location.file.split(path.sep).pop();
1762
+ } else {
1763
+ fileName = parts[0];
1764
+ }
1765
+
1766
+ const describeName = parts.slice(1, parts.length - 1).join(" > ");
1767
+
1768
+ // Create a unique key for the map
1769
+ const key = fileName + "::" + describeName;
1770
+
1771
+ if (!describeMap.has(key)) {
1772
+ describeMap.set(key, {
1773
+ duration: 0,
1774
+ file: fileName,
1775
+ describe: describeName,
1776
+ });
1777
+ }
1778
+ describeMap.get(key).duration += test.duration;
1779
+ }
1780
+ });
1781
+
1782
+ if (!foundAnyDescribe) {
1783
+ return '<div class="no-data">No test describe block found through out the executed test suite</div>';
1784
+ }
1785
+
1786
+ const categories = [];
1787
+ const data = [];
1788
+
1789
+ for (const [key, val] of describeMap.entries()) {
1790
+ categories.push(val.describe); // X-Axis label
1791
+ data.push({
1792
+ y: val.duration,
1793
+ custom: {
1794
+ fileName: val.file,
1795
+ describeName: val.describe,
1796
+ },
1797
+ });
1798
+ }
1799
+
1800
+ const chartId = `descDurChart-${Date.now()}-${Math.random()
1801
+ .toString(36)
1802
+ .substring(2, 7)}`;
1803
+ const renderFunctionName = `renderDescDurChart_${chartId.replace(/-/g, "_")}`;
1804
+
1805
+ const categoriesStr = JSON.stringify(categories);
1806
+ const dataStr = JSON.stringify(data);
1807
+
1808
+ return `
1809
+ <div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
1810
+ <div class="no-data">Loading Describe Duration Chart...</div>
1811
+ </div>
1812
+ <script>
1813
+ window.${renderFunctionName} = function() {
1814
+ const chartContainer = document.getElementById('${chartId}');
1815
+ if (!chartContainer) return;
1816
+ if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
1817
+ try {
1818
+ chartContainer.innerHTML = '';
1819
+ Highcharts.chart('${chartId}', {
1820
+ chart: { type: 'line', height: 350, backgroundColor: 'transparent' },
1821
+ title: { text: null },
1822
+ xAxis: {
1823
+ categories: ${categoriesStr},
1824
+ title: { text: null },
1825
+ crosshair: true,
1826
+ labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' } }
1827
+ },
1828
+ yAxis: {
1829
+ min: 0,
1830
+ title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
1831
+ labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
1832
+ },
1833
+ legend: { enabled: false },
1834
+ plotOptions: {
1835
+ series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}},
1836
+ line: { lineWidth: 2.5 }
1837
+ },
1838
+ tooltip: {
1839
+ shared: true,
1840
+ useHTML: true,
1841
+ backgroundColor: 'rgba(10,10,10,0.92)',
1842
+ borderColor: 'rgba(10,10,10,0.92)',
1843
+ style: { color: '#f5f5f5' },
1844
+ formatter: function() {
1845
+ // Retrieve custom data stored in the point
1846
+ const point = this.points ? this.points[0].point : this.point;
1847
+
1848
+ // Safety check for your custom data
1849
+ const file = (point.custom && point.custom.fileName) ? point.custom.fileName : 'Unknown';
1850
+ const desc = (point.custom && point.custom.describeName) ? point.custom.describeName : this.x;
1851
+ const color = point.color || point.series.color;
1852
+
1853
+ return '<span style="color:' + color + '">●</span> <b>Describe: ' + desc + '</b><br/>' +
1854
+ '<span style="opacity: 0.8; font-size: 0.9em; color: #ddd;">File: ' + file + '</span><br/>' +
1855
+ 'Duration: <b>' + formatDuration(point.y) + '</b>';
1856
+ }
1857
+ },
1858
+ series: [{
1859
+ name: 'Duration',
1860
+ data: ${dataStr},
1861
+ color: 'var(--accent-color-alt)', // Orange theme
1862
+ marker: { symbol: 'circle' }
1863
+ }],
1864
+ credits: { enabled: false }
1865
+ });
1866
+ } catch (e) { console.error("Error rendering describe chart:", e); }
1867
+ }
1868
+ };
1869
+ </script>
1870
+ `;
1871
+ }
1872
+ // --- END NEW CHARTS HELPER FUNCTIONS ---
1647
1873
  function generateHTML(reportData, trendData = null) {
1648
1874
  const { run, results } = reportData;
1649
1875
  const suitesData = getSuitesData(reportData.results || []);
@@ -2572,6 +2798,16 @@ function generateHTML(reportData, trendData = null) {
2572
2798
  }
2573
2799
  </div>
2574
2800
  </div>
2801
+ <div class="trend-charts-row">
2802
+ <div class="trend-chart">
2803
+ <h3 class="chart-title-header">Duration by Spec files</h3>
2804
+ ${generateSpecDurationChart(results)}
2805
+ </div>
2806
+ <div class="trend-chart">
2807
+ <h3 class="chart-title-header">Duration by Test Describe</h3>
2808
+ ${generateDescribeDurationChart(results)}
2809
+ </div>
2810
+ </div>
2575
2811
  <h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
2576
2812
  <div class="trend-charts-row">
2577
2813
  <div class="trend-chart">
@@ -3282,4 +3518,4 @@ main().catch((err) => {
3282
3518
  );
3283
3519
  console.error(err.stack);
3284
3520
  process.exit(1);
3285
- });
3521
+ });
@@ -1781,6 +1781,206 @@ function generateAIFailureAnalyzerTab(results) {
1781
1781
  </div>
1782
1782
  `;
1783
1783
  }
1784
+ // --- NEW CHARTS HELPER FUNCTIONS (Style: Line Chart + Enhanced Tooltips) ---
1785
+
1786
+ function generateSpecDurationChart(results) {
1787
+ if (!results || results.length === 0) return '<div class="no-data">No results available.</div>';
1788
+
1789
+ const specDurations = {};
1790
+ results.forEach(test => {
1791
+ let fileName = 'Unknown';
1792
+ if (test.location && test.location.file) {
1793
+ fileName = test.location.file.split(path.sep).pop();
1794
+ } else {
1795
+ const parts = test.name.split(' > ');
1796
+ fileName = parts[0];
1797
+ }
1798
+
1799
+ if (!specDurations[fileName]) specDurations[fileName] = 0;
1800
+ specDurations[fileName] += test.duration;
1801
+ });
1802
+
1803
+ const categories = Object.keys(specDurations);
1804
+ // Map data to objects to pass metadata if needed, though categories usually handle this.
1805
+ const data = categories.map(cat => ({
1806
+ y: specDurations[cat],
1807
+ name: cat
1808
+ }));
1809
+
1810
+ if (categories.length === 0) return '<div class="no-data">No spec data found.</div>';
1811
+
1812
+ const chartId = `specDurChart-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
1813
+ const renderFunctionName = `renderSpecDurChart_${chartId.replace(/-/g, "_")}`;
1814
+
1815
+ const categoriesStr = JSON.stringify(categories);
1816
+ const dataStr = JSON.stringify(data);
1817
+
1818
+ return `
1819
+ <div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
1820
+ <div class="no-data">Loading Spec Duration Chart...</div>
1821
+ </div>
1822
+ <script>
1823
+ window.${renderFunctionName} = function() {
1824
+ const chartContainer = document.getElementById('${chartId}');
1825
+ if (!chartContainer) return;
1826
+ if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
1827
+ try {
1828
+ chartContainer.innerHTML = '';
1829
+ Highcharts.chart('${chartId}', {
1830
+ chart: { type: 'line', height: 350, backgroundColor: 'transparent' },
1831
+ title: { text: null },
1832
+ xAxis: {
1833
+ categories: ${categoriesStr},
1834
+ title: { text: null },
1835
+ crosshair: true,
1836
+ labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' } }
1837
+ },
1838
+ yAxis: {
1839
+ min: 0,
1840
+ title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
1841
+ labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
1842
+ },
1843
+ legend: { enabled: false },
1844
+ plotOptions: {
1845
+ series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}},
1846
+ line: { lineWidth: 2.5 }
1847
+ },
1848
+ tooltip: {
1849
+ shared: true, useHTML: true, backgroundColor: 'rgba(10,10,10,0.92)', borderColor: 'rgba(10,10,10,0.92)', style: { color: '#f5f5f5' },
1850
+ formatter: function() {
1851
+ // Use 'this.x' which contains the category name (File Name)
1852
+ return '<b>File: ' + this.x + '</b><br/>Duration: ' + formatDuration(this.y);
1853
+ }
1854
+ },
1855
+ series: [{
1856
+ name: 'Duration',
1857
+ data: ${dataStr},
1858
+ color: 'var(--accent-color-alt)', // Orange theme
1859
+ marker: { symbol: 'circle' }
1860
+ }],
1861
+ credits: { enabled: false }
1862
+ });
1863
+ } catch (e) { console.error("Error rendering spec chart:", e); }
1864
+ }
1865
+ };
1866
+ </script>
1867
+ `;
1868
+ }
1869
+
1870
+ function generateDescribeDurationChart(results) {
1871
+ if (!results || results.length === 0) return '<div class="no-data">No results available.</div>';
1872
+
1873
+ // We need to group by (File + Describe) to handle same-named describes in different files
1874
+ const describeMap = new Map();
1875
+ let foundAnyDescribe = false;
1876
+
1877
+ results.forEach(test => {
1878
+ const parts = test.name.split(' > ');
1879
+ if (parts.length > 2) {
1880
+ foundAnyDescribe = true;
1881
+
1882
+ // Extract Filename
1883
+ let fileName = 'Unknown';
1884
+ if (test.location && test.location.file) {
1885
+ fileName = test.location.file.split(path.sep).pop();
1886
+ } else {
1887
+ fileName = parts[0];
1888
+ }
1889
+
1890
+ const describeName = parts.slice(1, parts.length - 1).join(' > ');
1891
+
1892
+ // Create a unique key for the map
1893
+ const key = fileName + '::' + describeName;
1894
+
1895
+ if (!describeMap.has(key)) {
1896
+ describeMap.set(key, { duration: 0, file: fileName, describe: describeName });
1897
+ }
1898
+ describeMap.get(key).duration += test.duration;
1899
+ }
1900
+ });
1901
+
1902
+ if (!foundAnyDescribe) {
1903
+ return '<div class="no-data">No test describe block found through out the executed test suite</div>';
1904
+ }
1905
+
1906
+ const categories = [];
1907
+ const data = [];
1908
+
1909
+ for (const [key, val] of describeMap.entries()) {
1910
+ categories.push(val.describe); // X-Axis label
1911
+ data.push({
1912
+ y: val.duration,
1913
+ custom: {
1914
+ fileName: val.file,
1915
+ describeName: val.describe
1916
+ }
1917
+ });
1918
+ }
1919
+
1920
+ const chartId = `descDurChart-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
1921
+ const renderFunctionName = `renderDescDurChart_${chartId.replace(/-/g, "_")}`;
1922
+
1923
+ const categoriesStr = JSON.stringify(categories);
1924
+ const dataStr = JSON.stringify(data);
1925
+
1926
+ return `
1927
+ <div id="${chartId}" class="trend-chart-container lazy-load-chart" data-render-function-name="${renderFunctionName}">
1928
+ <div class="no-data">Loading Describe Duration Chart...</div>
1929
+ </div>
1930
+ <script>
1931
+ window.${renderFunctionName} = function() {
1932
+ const chartContainer = document.getElementById('${chartId}');
1933
+ if (!chartContainer) return;
1934
+ if (typeof Highcharts !== 'undefined' && typeof formatDuration !== 'undefined') {
1935
+ try {
1936
+ chartContainer.innerHTML = '';
1937
+ Highcharts.chart('${chartId}', {
1938
+ chart: { type: 'line', height: 350, backgroundColor: 'transparent' },
1939
+ title: { text: null },
1940
+ xAxis: {
1941
+ categories: ${categoriesStr},
1942
+ title: { text: null },
1943
+ crosshair: true,
1944
+ labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' } }
1945
+ },
1946
+ yAxis: {
1947
+ min: 0,
1948
+ title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
1949
+ labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
1950
+ },
1951
+ legend: { enabled: false },
1952
+ plotOptions: {
1953
+ series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}},
1954
+ line: { lineWidth: 2.5 }
1955
+ },
1956
+ tooltip: {
1957
+ shared: true, useHTML: true, backgroundColor: 'rgba(10,10,10,0.92)', borderColor: 'rgba(10,10,10,0.92)', style: { color: '#f5f5f5' },
1958
+ formatter: function() {
1959
+ // Retrieve custom data stored in the point
1960
+ const point = this.points ? this.points[0].point : this.point;
1961
+ const file = (point.custom && point.custom.fileName) ? point.custom.fileName : 'Unknown';
1962
+ const desc = (point.custom && point.custom.describeName) ? point.custom.describeName : this.x;
1963
+
1964
+ return '<b>File:</b> ' + file + '<br/>' +
1965
+ '<b>Describe:</b> ' + desc + '<br/>' +
1966
+ 'Duration: ' + formatDuration(point.y);
1967
+ }
1968
+ },
1969
+ series: [{
1970
+ name: 'Duration',
1971
+ data: ${dataStr},
1972
+ color: 'var(--accent-color-alt)', // Orange theme
1973
+ marker: { symbol: 'circle' }
1974
+ }],
1975
+ credits: { enabled: false }
1976
+ });
1977
+ } catch (e) { console.error("Error rendering describe chart:", e); }
1978
+ }
1979
+ };
1980
+ </script>
1981
+ `;
1982
+ }
1983
+ // --- END NEW CHARTS HELPER FUNCTIONS ---
1784
1984
  /**
1785
1985
  * Generates the HTML report.
1786
1986
  * @param {object} reportData - The data for the report.
@@ -2574,6 +2774,16 @@ aspect-ratio: 16 / 9;
2574
2774
  }
2575
2775
  </div>
2576
2776
  </div>
2777
+ <div class="trend-charts-row">
2778
+ <div class="trend-chart">
2779
+ <h3 class="chart-title-header">Duration by Spec files</h3>
2780
+ ${generateSpecDurationChart(results)}
2781
+ </div>
2782
+ <div class="trend-chart">
2783
+ <h3 class="chart-title-header">Duration by Test Describe</h3>
2784
+ ${generateDescribeDurationChart(results)}
2785
+ </div>
2786
+ </div>
2577
2787
  <h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
2578
2788
  <div class="trend-charts-row">
2579
2789
  <div class="trend-chart">