@arghajit/dummy 0.3.7 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/config-reader.mjs +100 -52
- package/scripts/generate-report.mjs +67 -56
- package/scripts/generate-static-report.mjs +110 -79
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arghajit/dummy",
|
|
3
3
|
"author": "Arghajit Singha",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.9",
|
|
5
5
|
"description": "A Playwright reporter and dashboard for visualizing test results.",
|
|
6
6
|
"homepage": "https://playwright-pulse-report.netlify.app/",
|
|
7
7
|
"keywords": [
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
import * as fs from "fs";
|
|
3
2
|
import * as path from "path";
|
|
4
3
|
import { pathToFileURL } from "url";
|
|
@@ -24,9 +23,61 @@ async function findPlaywrightConfig() {
|
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
async function extractOutputDirFromConfig(configPath) {
|
|
26
|
+
let fileContent = "";
|
|
27
27
|
try {
|
|
28
|
-
|
|
28
|
+
fileContent = fs.readFileSync(configPath, "utf-8");
|
|
29
|
+
} catch (e) {
|
|
30
|
+
// If we can't read the file, we can't parse or import it.
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 1. Strategy: Text Parsing (Safe & Fast)
|
|
35
|
+
// We try to read the file as text first. This finds the outputDir without
|
|
36
|
+
// triggering any Node.js warnings or errors.
|
|
37
|
+
try {
|
|
38
|
+
// Regex matches: outputDir: "value" or outputDir: 'value'
|
|
39
|
+
const match = fileContent.match(/outputDir:\s*["']([^"']+)["']/);
|
|
40
|
+
|
|
41
|
+
if (match && match[1]) {
|
|
42
|
+
return path.resolve(process.cwd(), match[1]);
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// Ignore text reading errors
|
|
46
|
+
}
|
|
29
47
|
|
|
48
|
+
// 2. Safety Check: Detect ESM in CJS to Prevent Node Warnings
|
|
49
|
+
// The warning "To load an ES module..." happens when we try to import()
|
|
50
|
+
// a .js file containing ESM syntax (import/export) in a CJS package.
|
|
51
|
+
// We explicitly check for this and ABORT the import if found.
|
|
52
|
+
if (configPath.endsWith(".js")) {
|
|
53
|
+
let isModulePackage = false;
|
|
54
|
+
try {
|
|
55
|
+
const pkgPath = path.resolve(process.cwd(), "package.json");
|
|
56
|
+
if (fs.existsSync(pkgPath)) {
|
|
57
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
58
|
+
isModulePackage = pkg.type === "module";
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {}
|
|
61
|
+
|
|
62
|
+
if (!isModulePackage) {
|
|
63
|
+
// Heuristic: Check for ESM syntax (import/export at start of lines)
|
|
64
|
+
const hasEsmSyntax =
|
|
65
|
+
/^\s*import\s+/m.test(fileContent) ||
|
|
66
|
+
/^\s*export\s+/m.test(fileContent);
|
|
67
|
+
|
|
68
|
+
if (hasEsmSyntax) {
|
|
69
|
+
// We found ESM syntax in a .js file within a CJS project.
|
|
70
|
+
// Attempting to import this WILL trigger the Node.js warning.
|
|
71
|
+
// Since regex failed to find outputDir, and we can't import safely, we abort now.
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3. Strategy: Dynamic Import
|
|
78
|
+
// If we passed the safety check, we try to import the config.
|
|
79
|
+
try {
|
|
80
|
+
let config;
|
|
30
81
|
const configDir = dirname(configPath);
|
|
31
82
|
const originalDirname = global.__dirname;
|
|
32
83
|
const originalFilename = global.__filename;
|
|
@@ -39,73 +90,70 @@ async function extractOutputDirFromConfig(configPath) {
|
|
|
39
90
|
try {
|
|
40
91
|
const { register } = await import("node:module");
|
|
41
92
|
const { pathToFileURL } = await import("node:url");
|
|
42
|
-
|
|
43
93
|
register("ts-node/esm", pathToFileURL("./"));
|
|
44
|
-
|
|
45
94
|
config = await import(pathToFileURL(configPath).href);
|
|
46
95
|
} catch (tsError) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
}
|
|
96
|
+
const tsNode = await import("ts-node");
|
|
97
|
+
tsNode.register({
|
|
98
|
+
transpileOnly: true,
|
|
99
|
+
compilerOptions: { module: "commonjs" },
|
|
100
|
+
});
|
|
101
|
+
config = require(configPath);
|
|
60
102
|
}
|
|
61
103
|
} else {
|
|
104
|
+
// Try dynamic import for JS/MJS
|
|
62
105
|
config = await import(pathToFileURL(configPath).href);
|
|
63
106
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
delete global.__dirname;
|
|
69
|
-
}
|
|
70
|
-
if (originalFilename !== undefined) {
|
|
71
|
-
global.__filename = originalFilename;
|
|
72
|
-
} else {
|
|
73
|
-
delete global.__filename;
|
|
107
|
+
|
|
108
|
+
// Handle Default Export
|
|
109
|
+
if (config && config.default) {
|
|
110
|
+
config = config.default;
|
|
74
111
|
}
|
|
75
|
-
}
|
|
76
112
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
if (config) {
|
|
114
|
+
// Check for Reporter Config
|
|
115
|
+
if (config.reporter) {
|
|
116
|
+
const reporters = Array.isArray(config.reporter)
|
|
117
|
+
? config.reporter
|
|
118
|
+
: [config.reporter];
|
|
119
|
+
|
|
120
|
+
for (const reporter of reporters) {
|
|
121
|
+
const reporterName = Array.isArray(reporter)
|
|
122
|
+
? reporter[0]
|
|
123
|
+
: reporter;
|
|
124
|
+
const reporterOptions = Array.isArray(reporter)
|
|
125
|
+
? reporter[1]
|
|
126
|
+
: null;
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
typeof reporterName === "string" &&
|
|
130
|
+
(reporterName.includes("playwright-pulse-report") ||
|
|
131
|
+
reporterName.includes("@arghajit/playwright-pulse-report") ||
|
|
132
|
+
reporterName.includes("@arghajit/dummy"))
|
|
133
|
+
) {
|
|
134
|
+
if (reporterOptions && reporterOptions.outputDir) {
|
|
135
|
+
return path.resolve(process.cwd(), reporterOptions.outputDir);
|
|
136
|
+
}
|
|
97
137
|
}
|
|
98
138
|
}
|
|
99
139
|
}
|
|
140
|
+
|
|
141
|
+
// Check for Global outputDir
|
|
142
|
+
if (config.outputDir) {
|
|
143
|
+
return path.resolve(process.cwd(), config.outputDir);
|
|
144
|
+
}
|
|
100
145
|
}
|
|
146
|
+
} finally {
|
|
147
|
+
// Clean up globals
|
|
148
|
+
global.__dirname = originalDirname;
|
|
149
|
+
global.__filename = originalFilename;
|
|
101
150
|
}
|
|
102
|
-
|
|
103
|
-
console.log("No matching reporter config found with outputDir");
|
|
104
|
-
return null;
|
|
105
151
|
} catch (error) {
|
|
106
|
-
|
|
152
|
+
// SILENT CATCH: Do NOT log anything here.
|
|
107
153
|
return null;
|
|
108
154
|
}
|
|
155
|
+
|
|
156
|
+
return null;
|
|
109
157
|
}
|
|
110
158
|
|
|
111
159
|
export async function getOutputDir(customOutputDirFromArgs = null) {
|
|
@@ -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,28 +1643,28 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1644
1643
|
</div>
|
|
1645
1644
|
`;
|
|
1646
1645
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
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
|
+
*/
|
|
1649
1653
|
function generateSpecDurationChart(results) {
|
|
1650
1654
|
if (!results || results.length === 0)
|
|
1651
1655
|
return '<div class="no-data">No results available.</div>';
|
|
1652
1656
|
|
|
1653
1657
|
const specDurations = {};
|
|
1654
1658
|
results.forEach((test) => {
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
fileName = test.location.file.split(path.sep).pop();
|
|
1658
|
-
} else {
|
|
1659
|
-
const parts = test.name.split(" > ");
|
|
1660
|
-
fileName = parts[0];
|
|
1661
|
-
}
|
|
1659
|
+
// Use the dedicated 'spec_file' key
|
|
1660
|
+
const fileName = test.spec_file || "Unknown File";
|
|
1662
1661
|
|
|
1663
1662
|
if (!specDurations[fileName]) specDurations[fileName] = 0;
|
|
1664
1663
|
specDurations[fileName] += test.duration;
|
|
1665
1664
|
});
|
|
1666
1665
|
|
|
1667
1666
|
const categories = Object.keys(specDurations);
|
|
1668
|
-
//
|
|
1667
|
+
// We map 'name' here, which we will use in the tooltip later
|
|
1669
1668
|
const data = categories.map((cat) => ({
|
|
1670
1669
|
y: specDurations[cat],
|
|
1671
1670
|
name: cat,
|
|
@@ -1694,24 +1693,21 @@ function generateSpecDurationChart(results) {
|
|
|
1694
1693
|
try {
|
|
1695
1694
|
chartContainer.innerHTML = '';
|
|
1696
1695
|
Highcharts.chart('${chartId}', {
|
|
1697
|
-
chart: { type: '
|
|
1696
|
+
chart: { type: 'area', height: 350, backgroundColor: 'transparent' },
|
|
1698
1697
|
title: { text: null },
|
|
1699
1698
|
xAxis: {
|
|
1700
1699
|
categories: ${categoriesStr},
|
|
1700
|
+
visible: false, // 1. HIDE THE X-AXIS
|
|
1701
1701
|
title: { text: null },
|
|
1702
|
-
crosshair: true
|
|
1703
|
-
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' } }
|
|
1702
|
+
crosshair: true
|
|
1704
1703
|
},
|
|
1705
1704
|
yAxis: {
|
|
1706
1705
|
min: 0,
|
|
1707
1706
|
title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
|
|
1708
1707
|
labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
|
|
1709
1708
|
},
|
|
1710
|
-
legend: {
|
|
1711
|
-
|
|
1712
|
-
series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}},
|
|
1713
|
-
line: { lineWidth: 2.5 }
|
|
1714
|
-
},
|
|
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 }},
|
|
1715
1711
|
tooltip: {
|
|
1716
1712
|
shared: true,
|
|
1717
1713
|
useHTML: true,
|
|
@@ -1719,19 +1715,22 @@ function generateSpecDurationChart(results) {
|
|
|
1719
1715
|
borderColor: 'rgba(10,10,10,0.92)',
|
|
1720
1716
|
style: { color: '#f5f5f5' },
|
|
1721
1717
|
formatter: function() {
|
|
1722
|
-
// 'this.x' is the File Name (category)
|
|
1723
1718
|
const point = this.points ? this.points[0].point : this.point;
|
|
1724
1719
|
const color = point.color || point.series.color;
|
|
1725
1720
|
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
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
|
+
}
|
|
1729
1725
|
},
|
|
1730
1726
|
series: [{
|
|
1731
1727
|
name: 'Duration',
|
|
1732
1728
|
data: ${dataStr},
|
|
1733
|
-
color: 'var(--accent-color-alt)',
|
|
1734
|
-
|
|
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
|
|
1735
1734
|
}],
|
|
1736
1735
|
credits: { enabled: false }
|
|
1737
1736
|
});
|
|
@@ -1741,31 +1740,33 @@ function generateSpecDurationChart(results) {
|
|
|
1741
1740
|
</script>
|
|
1742
1741
|
`;
|
|
1743
1742
|
}
|
|
1744
|
-
|
|
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
|
+
*/
|
|
1745
1749
|
function generateDescribeDurationChart(results) {
|
|
1746
1750
|
if (!results || results.length === 0)
|
|
1747
1751
|
return '<div class="no-data">No results available.</div>';
|
|
1748
1752
|
|
|
1749
|
-
// We need to group by (File + Describe) to handle same-named describes in different files
|
|
1750
1753
|
const describeMap = new Map();
|
|
1751
1754
|
let foundAnyDescribe = false;
|
|
1752
1755
|
|
|
1753
1756
|
results.forEach((test) => {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
fileName = parts[0];
|
|
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;
|
|
1764
1766
|
}
|
|
1765
1767
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
// Create a unique key for the map
|
|
1768
|
+
foundAnyDescribe = true;
|
|
1769
|
+
const fileName = test.spec_file || "Unknown File";
|
|
1769
1770
|
const key = fileName + "::" + describeName;
|
|
1770
1771
|
|
|
1771
1772
|
if (!describeMap.has(key)) {
|
|
@@ -1780,16 +1781,17 @@ function generateDescribeDurationChart(results) {
|
|
|
1780
1781
|
});
|
|
1781
1782
|
|
|
1782
1783
|
if (!foundAnyDescribe) {
|
|
1783
|
-
return '<div class="no-data">No test describe
|
|
1784
|
+
return '<div class="no-data">No valid test describe blocks found.</div>';
|
|
1784
1785
|
}
|
|
1785
1786
|
|
|
1786
1787
|
const categories = [];
|
|
1787
1788
|
const data = [];
|
|
1788
1789
|
|
|
1789
1790
|
for (const [key, val] of describeMap.entries()) {
|
|
1790
|
-
categories.push(val.describe);
|
|
1791
|
+
categories.push(val.describe);
|
|
1791
1792
|
data.push({
|
|
1792
1793
|
y: val.duration,
|
|
1794
|
+
name: val.describe,
|
|
1793
1795
|
custom: {
|
|
1794
1796
|
fileName: val.file,
|
|
1795
1797
|
describeName: val.describe,
|
|
@@ -1817,13 +1819,17 @@ function generateDescribeDurationChart(results) {
|
|
|
1817
1819
|
try {
|
|
1818
1820
|
chartContainer.innerHTML = '';
|
|
1819
1821
|
Highcharts.chart('${chartId}', {
|
|
1820
|
-
chart: {
|
|
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
|
+
},
|
|
1821
1827
|
title: { text: null },
|
|
1822
1828
|
xAxis: {
|
|
1823
1829
|
categories: ${categoriesStr},
|
|
1830
|
+
visible: false, // Hidden as requested
|
|
1824
1831
|
title: { text: null },
|
|
1825
|
-
crosshair: true
|
|
1826
|
-
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' } }
|
|
1832
|
+
crosshair: true
|
|
1827
1833
|
},
|
|
1828
1834
|
yAxis: {
|
|
1829
1835
|
min: 0,
|
|
@@ -1832,8 +1838,12 @@ function generateDescribeDurationChart(results) {
|
|
|
1832
1838
|
},
|
|
1833
1839
|
legend: { enabled: false },
|
|
1834
1840
|
plotOptions: {
|
|
1835
|
-
series: {
|
|
1836
|
-
|
|
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
|
|
1837
1847
|
},
|
|
1838
1848
|
tooltip: {
|
|
1839
1849
|
shared: true,
|
|
@@ -1842,24 +1852,20 @@ function generateDescribeDurationChart(results) {
|
|
|
1842
1852
|
borderColor: 'rgba(10,10,10,0.92)',
|
|
1843
1853
|
style: { color: '#f5f5f5' },
|
|
1844
1854
|
formatter: function() {
|
|
1845
|
-
// Retrieve custom data stored in the point
|
|
1846
1855
|
const point = this.points ? this.points[0].point : this.point;
|
|
1847
|
-
|
|
1848
|
-
// Safety check for your custom data
|
|
1849
1856
|
const file = (point.custom && point.custom.fileName) ? point.custom.fileName : 'Unknown';
|
|
1850
|
-
const desc =
|
|
1857
|
+
const desc = point.name || 'Unknown';
|
|
1851
1858
|
const color = point.color || point.series.color;
|
|
1852
1859
|
|
|
1853
1860
|
return '<span style="color:' + color + '">●</span> <b>Describe: ' + desc + '</b><br/>' +
|
|
1854
1861
|
'<span style="opacity: 0.8; font-size: 0.9em; color: #ddd;">File: ' + file + '</span><br/>' +
|
|
1855
1862
|
'Duration: <b>' + formatDuration(point.y) + '</b>';
|
|
1856
|
-
|
|
1863
|
+
}
|
|
1857
1864
|
},
|
|
1858
1865
|
series: [{
|
|
1859
1866
|
name: 'Duration',
|
|
1860
1867
|
data: ${dataStr},
|
|
1861
|
-
color: 'var(--accent-color-alt)',
|
|
1862
|
-
marker: { symbol: 'circle' }
|
|
1868
|
+
color: 'var(--accent-color-alt)',
|
|
1863
1869
|
}],
|
|
1864
1870
|
credits: { enabled: false }
|
|
1865
1871
|
});
|
|
@@ -1869,7 +1875,12 @@ function generateDescribeDurationChart(results) {
|
|
|
1869
1875
|
</script>
|
|
1870
1876
|
`;
|
|
1871
1877
|
}
|
|
1872
|
-
|
|
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
|
+
*/
|
|
1873
1884
|
function generateHTML(reportData, trendData = null) {
|
|
1874
1885
|
const { run, results } = reportData;
|
|
1875
1886
|
const suitesData = getSuitesData(reportData.results || []);
|
|
@@ -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,35 +1780,39 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1781
1780
|
</div>
|
|
1782
1781
|
`;
|
|
1783
1782
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
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
|
+
*/
|
|
1786
1790
|
function generateSpecDurationChart(results) {
|
|
1787
|
-
if (!results || results.length === 0)
|
|
1791
|
+
if (!results || results.length === 0)
|
|
1792
|
+
return '<div class="no-data">No results available.</div>';
|
|
1788
1793
|
|
|
1789
1794
|
const specDurations = {};
|
|
1790
|
-
results.forEach(test => {
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
} else {
|
|
1795
|
-
const parts = test.name.split(' > ');
|
|
1796
|
-
fileName = parts[0];
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1795
|
+
results.forEach((test) => {
|
|
1796
|
+
// Use the dedicated 'spec_file' key
|
|
1797
|
+
const fileName = test.spec_file || "Unknown File";
|
|
1798
|
+
|
|
1799
1799
|
if (!specDurations[fileName]) specDurations[fileName] = 0;
|
|
1800
1800
|
specDurations[fileName] += test.duration;
|
|
1801
1801
|
});
|
|
1802
1802
|
|
|
1803
1803
|
const categories = Object.keys(specDurations);
|
|
1804
|
-
//
|
|
1805
|
-
const data = categories.map(cat => ({
|
|
1806
|
-
|
|
1807
|
-
|
|
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
1808
|
}));
|
|
1809
1809
|
|
|
1810
|
-
if (categories.length === 0)
|
|
1810
|
+
if (categories.length === 0)
|
|
1811
|
+
return '<div class="no-data">No spec data found.</div>';
|
|
1811
1812
|
|
|
1812
|
-
const chartId = `specDurChart-${Date.now()}-${Math.random()
|
|
1813
|
+
const chartId = `specDurChart-${Date.now()}-${Math.random()
|
|
1814
|
+
.toString(36)
|
|
1815
|
+
.substring(2, 7)}`;
|
|
1813
1816
|
const renderFunctionName = `renderSpecDurChart_${chartId.replace(/-/g, "_")}`;
|
|
1814
1817
|
|
|
1815
1818
|
const categoriesStr = JSON.stringify(categories);
|
|
@@ -1827,36 +1830,44 @@ function generateSpecDurationChart(results) {
|
|
|
1827
1830
|
try {
|
|
1828
1831
|
chartContainer.innerHTML = '';
|
|
1829
1832
|
Highcharts.chart('${chartId}', {
|
|
1830
|
-
chart: { type: '
|
|
1833
|
+
chart: { type: 'area', height: 350, backgroundColor: 'transparent' },
|
|
1831
1834
|
title: { text: null },
|
|
1832
1835
|
xAxis: {
|
|
1833
1836
|
categories: ${categoriesStr},
|
|
1837
|
+
visible: false, // 1. HIDE THE X-AXIS
|
|
1834
1838
|
title: { text: null },
|
|
1835
|
-
crosshair: true
|
|
1836
|
-
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' } }
|
|
1839
|
+
crosshair: true
|
|
1837
1840
|
},
|
|
1838
1841
|
yAxis: {
|
|
1839
1842
|
min: 0,
|
|
1840
1843
|
title: { text: 'Total Duration', style: { color: 'var(--text-color)' } },
|
|
1841
1844
|
labels: { formatter: function() { return formatDuration(this.value); }, style: { color: 'var(--text-color-secondary)' } }
|
|
1842
1845
|
},
|
|
1843
|
-
legend: {
|
|
1844
|
-
|
|
1845
|
-
series: { marker: { radius: 4, states: { hover: { radius: 6 }}}, states: { hover: { halo: { size: 5, opacity: 0.1 }}}},
|
|
1846
|
-
line: { lineWidth: 2.5 }
|
|
1847
|
-
},
|
|
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
1848
|
tooltip: {
|
|
1849
|
-
shared: true,
|
|
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' },
|
|
1850
1854
|
formatter: function() {
|
|
1851
|
-
|
|
1852
|
-
|
|
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>';
|
|
1853
1861
|
}
|
|
1854
1862
|
},
|
|
1855
1863
|
series: [{
|
|
1856
1864
|
name: 'Duration',
|
|
1857
1865
|
data: ${dataStr},
|
|
1858
|
-
color: 'var(--accent-color-alt)',
|
|
1859
|
-
|
|
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
|
|
1860
1871
|
}],
|
|
1861
1872
|
credits: { enabled: false }
|
|
1862
1873
|
});
|
|
@@ -1866,60 +1877,70 @@ function generateSpecDurationChart(results) {
|
|
|
1866
1877
|
</script>
|
|
1867
1878
|
`;
|
|
1868
1879
|
}
|
|
1869
|
-
|
|
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
|
+
*/
|
|
1870
1886
|
function generateDescribeDurationChart(results) {
|
|
1871
|
-
if (!results || results.length === 0)
|
|
1887
|
+
if (!results || results.length === 0)
|
|
1888
|
+
return '<div class="no-data">No results available.</div>';
|
|
1872
1889
|
|
|
1873
|
-
// We need to group by (File + Describe) to handle same-named describes in different files
|
|
1874
1890
|
const describeMap = new Map();
|
|
1875
1891
|
let foundAnyDescribe = false;
|
|
1876
1892
|
|
|
1877
|
-
results.forEach(test => {
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
fileName = parts[0];
|
|
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;
|
|
1888
1903
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1904
|
+
|
|
1905
|
+
foundAnyDescribe = true;
|
|
1906
|
+
const fileName = test.spec_file || "Unknown File";
|
|
1907
|
+
const key = fileName + "::" + describeName;
|
|
1908
|
+
|
|
1895
1909
|
if (!describeMap.has(key)) {
|
|
1896
|
-
|
|
1910
|
+
describeMap.set(key, {
|
|
1911
|
+
duration: 0,
|
|
1912
|
+
file: fileName,
|
|
1913
|
+
describe: describeName,
|
|
1914
|
+
});
|
|
1897
1915
|
}
|
|
1898
1916
|
describeMap.get(key).duration += test.duration;
|
|
1899
1917
|
}
|
|
1900
1918
|
});
|
|
1901
1919
|
|
|
1902
1920
|
if (!foundAnyDescribe) {
|
|
1903
|
-
return '<div class="no-data">No test describe
|
|
1921
|
+
return '<div class="no-data">No valid test describe blocks found.</div>';
|
|
1904
1922
|
}
|
|
1905
1923
|
|
|
1906
1924
|
const categories = [];
|
|
1907
1925
|
const data = [];
|
|
1908
1926
|
|
|
1909
1927
|
for (const [key, val] of describeMap.entries()) {
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
}
|
|
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
|
+
});
|
|
1918
1937
|
}
|
|
1919
1938
|
|
|
1920
|
-
const chartId = `descDurChart-${Date.now()}-${Math.random()
|
|
1939
|
+
const chartId = `descDurChart-${Date.now()}-${Math.random()
|
|
1940
|
+
.toString(36)
|
|
1941
|
+
.substring(2, 7)}`;
|
|
1921
1942
|
const renderFunctionName = `renderDescDurChart_${chartId.replace(/-/g, "_")}`;
|
|
1922
|
-
|
|
1943
|
+
|
|
1923
1944
|
const categoriesStr = JSON.stringify(categories);
|
|
1924
1945
|
const dataStr = JSON.stringify(data);
|
|
1925
1946
|
|
|
@@ -1935,13 +1956,17 @@ function generateDescribeDurationChart(results) {
|
|
|
1935
1956
|
try {
|
|
1936
1957
|
chartContainer.innerHTML = '';
|
|
1937
1958
|
Highcharts.chart('${chartId}', {
|
|
1938
|
-
chart: {
|
|
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
|
+
},
|
|
1939
1964
|
title: { text: null },
|
|
1940
1965
|
xAxis: {
|
|
1941
1966
|
categories: ${categoriesStr},
|
|
1967
|
+
visible: false, // Hidden as requested
|
|
1942
1968
|
title: { text: null },
|
|
1943
|
-
crosshair: true
|
|
1944
|
-
labels: { style: { color: 'var(--text-color-secondary)', fontSize: '12px' } }
|
|
1969
|
+
crosshair: true
|
|
1945
1970
|
},
|
|
1946
1971
|
yAxis: {
|
|
1947
1972
|
min: 0,
|
|
@@ -1950,27 +1975,34 @@ function generateDescribeDurationChart(results) {
|
|
|
1950
1975
|
},
|
|
1951
1976
|
legend: { enabled: false },
|
|
1952
1977
|
plotOptions: {
|
|
1953
|
-
series: {
|
|
1954
|
-
|
|
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
|
|
1955
1984
|
},
|
|
1956
1985
|
tooltip: {
|
|
1957
|
-
shared: true,
|
|
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' },
|
|
1958
1991
|
formatter: function() {
|
|
1959
|
-
// Retrieve custom data stored in the point
|
|
1960
1992
|
const point = this.points ? this.points[0].point : this.point;
|
|
1961
1993
|
const file = (point.custom && point.custom.fileName) ? point.custom.fileName : 'Unknown';
|
|
1962
|
-
const desc =
|
|
1994
|
+
const desc = point.name || 'Unknown';
|
|
1995
|
+
const color = point.color || point.series.color;
|
|
1963
1996
|
|
|
1964
|
-
return '<
|
|
1965
|
-
|
|
1966
|
-
|
|
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>';
|
|
1967
2000
|
}
|
|
1968
2001
|
},
|
|
1969
2002
|
series: [{
|
|
1970
2003
|
name: 'Duration',
|
|
1971
2004
|
data: ${dataStr},
|
|
1972
|
-
color: 'var(--accent-color-alt)',
|
|
1973
|
-
marker: { symbol: 'circle' }
|
|
2005
|
+
color: 'var(--accent-color-alt)',
|
|
1974
2006
|
}],
|
|
1975
2007
|
credits: { enabled: false }
|
|
1976
2008
|
});
|
|
@@ -1980,7 +2012,6 @@ function generateDescribeDurationChart(results) {
|
|
|
1980
2012
|
</script>
|
|
1981
2013
|
`;
|
|
1982
2014
|
}
|
|
1983
|
-
// --- END NEW CHARTS HELPER FUNCTIONS ---
|
|
1984
2015
|
/**
|
|
1985
2016
|
* Generates the HTML report.
|
|
1986
2017
|
* @param {object} reportData - The data for the report.
|