@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) || ((
|
|
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: ((
|
|
253
|
-
errorMessage: (
|
|
254
|
-
stackTrace: (
|
|
255
|
-
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: ((
|
|
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
|
-
(
|
|
293
|
+
(_o = pulseResult.screenshots) === null || _o === void 0 ? void 0 : _o.push(relativeDestPath);
|
|
282
294
|
}
|
|
283
295
|
else if (attachment.contentType.startsWith("video/")) {
|
|
284
|
-
(
|
|
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
|
-
(
|
|
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,
|
package/dist/types/index.d.ts
CHANGED
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.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": "
|
|
34
|
-
"generate-report": "
|
|
35
|
-
"merge-pulse-report": "
|
|
36
|
-
"send-email": "
|
|
37
|
-
"generate-trend": "
|
|
38
|
-
"generate-email-report": "
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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">
|