ortoni-report 3.0.4 → 4.0.0
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/changelog.md +37 -0
- package/dist/index.html +21 -0
- package/dist/ortoni-report.d.mts +2 -12
- package/dist/ortoni-report.d.ts +2 -12
- package/dist/ortoni-report.js +162 -283
- package/dist/ortoni-report.mjs +160 -281
- package/package.json +4 -5
- package/readme.md +26 -33
- package/dist/chunk-AY2PKDHU.mjs +0 -69
- package/dist/chunk-OOALU4XG.mjs +0 -72
- package/dist/chunk-ZSIRUQUA.mjs +0 -68
- package/dist/style/main.css +0 -80
- package/dist/views/analytics.hbs +0 -103
- package/dist/views/head.hbs +0 -11
- package/dist/views/main.hbs +0 -1285
- package/dist/views/project.hbs +0 -238
- package/dist/views/sidebar.hbs +0 -244
- package/dist/views/summaryCard.hbs +0 -15
- package/dist/views/testIcons.hbs +0 -13
- package/dist/views/testPanel.hbs +0 -45
- package/dist/views/testStatus.hbs +0 -9
- package/dist/views/userInfo.hbs +0 -260
package/dist/ortoni-report.mjs
CHANGED
|
@@ -21,17 +21,17 @@ var FileManager = class {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
writeReportFile(filename,
|
|
24
|
+
writeReportFile(filename, data) {
|
|
25
|
+
const templatePath = path.resolve(__dirname, "index.html");
|
|
26
|
+
let html = fs.readFileSync(templatePath, "utf-8");
|
|
27
|
+
const reportJSON = JSON.stringify({
|
|
28
|
+
data
|
|
29
|
+
});
|
|
30
|
+
html = html.replace("__ORTONI_TEST_REPORTDATA__", reportJSON);
|
|
25
31
|
const outputPath = path.join(process.cwd(), this.folderPath, filename);
|
|
26
|
-
fs.writeFileSync(outputPath,
|
|
32
|
+
fs.writeFileSync(outputPath, html);
|
|
27
33
|
return outputPath;
|
|
28
34
|
}
|
|
29
|
-
readCssContent() {
|
|
30
|
-
return fs.readFileSync(
|
|
31
|
-
path.resolve(__dirname, "style", "main.css"),
|
|
32
|
-
"utf-8"
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
35
|
copyTraceViewerAssets(skip) {
|
|
36
36
|
if (skip) return;
|
|
37
37
|
const traceViewerFolder = path.join(
|
|
@@ -66,157 +66,48 @@ var FileManager = class {
|
|
|
66
66
|
}
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
-
// src/helpers/HTMLGenerator.ts
|
|
70
|
-
import path3 from "path";
|
|
71
|
-
|
|
72
69
|
// src/utils/groupProjects.ts
|
|
73
70
|
function groupResults(config, results) {
|
|
74
71
|
if (config.showProject) {
|
|
75
72
|
const groupedResults = results.reduce((acc, result, index) => {
|
|
76
73
|
const testId = `${result.filePath}:${result.projectName}:${result.title}`;
|
|
74
|
+
const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
|
|
77
75
|
const { filePath, suite, projectName } = result;
|
|
78
76
|
acc[filePath] = acc[filePath] || {};
|
|
79
77
|
acc[filePath][suite] = acc[filePath][suite] || {};
|
|
80
78
|
acc[filePath][suite][projectName] = acc[filePath][suite][projectName] || [];
|
|
81
|
-
acc[filePath][suite][projectName].push({ ...result, index, testId });
|
|
79
|
+
acc[filePath][suite][projectName].push({ ...result, index, testId, key });
|
|
82
80
|
return acc;
|
|
83
81
|
}, {});
|
|
84
82
|
return groupedResults;
|
|
85
83
|
} else {
|
|
86
84
|
const groupedResults = results.reduce((acc, result, index) => {
|
|
87
85
|
const testId = `${result.filePath}:${result.projectName}:${result.title}`;
|
|
86
|
+
const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
|
|
88
87
|
const { filePath, suite } = result;
|
|
89
88
|
acc[filePath] = acc[filePath] || {};
|
|
90
89
|
acc[filePath][suite] = acc[filePath][suite] || [];
|
|
91
|
-
acc[filePath][suite].push({ ...result, index, testId });
|
|
90
|
+
acc[filePath][suite].push({ ...result, index, testId, key });
|
|
92
91
|
return acc;
|
|
93
92
|
}, {});
|
|
94
93
|
return groupedResults;
|
|
95
94
|
}
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
// src/utils/utils.ts
|
|
99
|
-
import path2 from "path";
|
|
100
|
-
function msToTime(duration) {
|
|
101
|
-
const milliseconds = Math.floor(duration % 1e3);
|
|
102
|
-
const seconds = Math.floor(duration / 1e3 % 60);
|
|
103
|
-
const minutes = Math.floor(duration / (1e3 * 60) % 60);
|
|
104
|
-
const hours = Math.floor(duration / (1e3 * 60 * 60) % 24);
|
|
105
|
-
let result = "";
|
|
106
|
-
if (hours > 0) {
|
|
107
|
-
result += `${hours}h:`;
|
|
108
|
-
}
|
|
109
|
-
if (minutes > 0 || hours > 0) {
|
|
110
|
-
result += `${minutes < 10 ? "0" + minutes : minutes}m:`;
|
|
111
|
-
}
|
|
112
|
-
if (seconds > 0 || minutes > 0 || hours > 0) {
|
|
113
|
-
result += `${seconds < 10 ? "0" + seconds : seconds}s`;
|
|
114
|
-
}
|
|
115
|
-
if (milliseconds > 0 && !(seconds > 0 || minutes > 0 || hours > 0)) {
|
|
116
|
-
result += `${milliseconds}ms`;
|
|
117
|
-
} else if (milliseconds > 0) {
|
|
118
|
-
result += `:${milliseconds < 100 ? "0" + milliseconds : milliseconds}ms`;
|
|
119
|
-
}
|
|
120
|
-
return result;
|
|
121
|
-
}
|
|
122
|
-
function normalizeFilePath(filePath) {
|
|
123
|
-
const normalizedPath = path2.normalize(filePath);
|
|
124
|
-
return path2.basename(normalizedPath);
|
|
125
|
-
}
|
|
126
|
-
function formatDate(date) {
|
|
127
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
128
|
-
const month = date.toLocaleString("default", { month: "short" });
|
|
129
|
-
const year = date.getFullYear();
|
|
130
|
-
const time = date.toLocaleTimeString();
|
|
131
|
-
return `${day}-${month}-${year} ${time}`;
|
|
132
|
-
}
|
|
133
|
-
function safeStringify(obj, indent = 2) {
|
|
134
|
-
const cache = /* @__PURE__ */ new Set();
|
|
135
|
-
const json = JSON.stringify(
|
|
136
|
-
obj,
|
|
137
|
-
(key, value) => {
|
|
138
|
-
if (typeof value === "object" && value !== null) {
|
|
139
|
-
if (cache.has(value)) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
cache.add(value);
|
|
143
|
-
}
|
|
144
|
-
return value;
|
|
145
|
-
},
|
|
146
|
-
indent
|
|
147
|
-
);
|
|
148
|
-
cache.clear();
|
|
149
|
-
return json;
|
|
150
|
-
}
|
|
151
|
-
function ensureHtmlExtension(filename) {
|
|
152
|
-
const ext = path2.extname(filename);
|
|
153
|
-
if (ext && ext.toLowerCase() === ".html") {
|
|
154
|
-
return filename;
|
|
155
|
-
}
|
|
156
|
-
return `${filename}.html`;
|
|
157
|
-
}
|
|
158
|
-
function escapeHtml(unsafe) {
|
|
159
|
-
if (typeof unsafe !== "string") {
|
|
160
|
-
return String(unsafe);
|
|
161
|
-
}
|
|
162
|
-
return unsafe.replace(/[&<"']/g, function(match) {
|
|
163
|
-
const escapeMap = {
|
|
164
|
-
"&": "&",
|
|
165
|
-
"<": "<",
|
|
166
|
-
">": ">",
|
|
167
|
-
'"': """,
|
|
168
|
-
"'": "'"
|
|
169
|
-
};
|
|
170
|
-
return escapeMap[match] || match;
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
function formatDateUTC(date) {
|
|
174
|
-
return date.toISOString();
|
|
175
|
-
}
|
|
176
|
-
function formatDateLocal(isoString) {
|
|
177
|
-
const date = new Date(isoString);
|
|
178
|
-
const options = {
|
|
179
|
-
year: "numeric",
|
|
180
|
-
month: "short",
|
|
181
|
-
day: "2-digit",
|
|
182
|
-
hour: "2-digit",
|
|
183
|
-
minute: "2-digit",
|
|
184
|
-
hour12: true,
|
|
185
|
-
timeZoneName: "short"
|
|
186
|
-
};
|
|
187
|
-
return new Intl.DateTimeFormat(void 0, options).format(date);
|
|
188
|
-
}
|
|
189
|
-
function formatDateNoTimezone(isoString) {
|
|
190
|
-
const date = new Date(isoString);
|
|
191
|
-
return date.toLocaleString("en-US", {
|
|
192
|
-
dateStyle: "medium",
|
|
193
|
-
timeStyle: "short"
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
97
|
// src/helpers/HTMLGenerator.ts
|
|
198
|
-
import fs2 from "fs";
|
|
199
|
-
import Handlebars from "handlebars";
|
|
200
98
|
var HTMLGenerator = class {
|
|
201
99
|
constructor(ortoniConfig, dbManager) {
|
|
202
100
|
this.ortoniConfig = ortoniConfig;
|
|
203
|
-
this.registerHandlebarsHelpers();
|
|
204
|
-
this.registerPartials();
|
|
205
101
|
this.dbManager = dbManager;
|
|
206
102
|
}
|
|
207
|
-
async
|
|
103
|
+
async generateFinalReport(filteredResults, totalDuration, results, projectSet) {
|
|
208
104
|
const data = await this.prepareReportData(
|
|
209
105
|
filteredResults,
|
|
210
106
|
totalDuration,
|
|
211
107
|
results,
|
|
212
108
|
projectSet
|
|
213
109
|
);
|
|
214
|
-
|
|
215
|
-
path3.resolve(__dirname, "views", "main.hbs"),
|
|
216
|
-
"utf-8"
|
|
217
|
-
);
|
|
218
|
-
const template = Handlebars.compile(templateSource);
|
|
219
|
-
return template({ ...data, inlineCss: cssContent });
|
|
110
|
+
return data;
|
|
220
111
|
}
|
|
221
112
|
async getReportData() {
|
|
222
113
|
return {
|
|
@@ -226,18 +117,6 @@ var HTMLGenerator = class {
|
|
|
226
117
|
slowTests: await this.dbManager.getSlowTests()
|
|
227
118
|
};
|
|
228
119
|
}
|
|
229
|
-
async chartTrendData() {
|
|
230
|
-
return {
|
|
231
|
-
labels: (await this.getReportData()).trends.map(
|
|
232
|
-
(t) => formatDateNoTimezone(t.run_date)
|
|
233
|
-
),
|
|
234
|
-
passed: (await this.getReportData()).trends.map((t) => t.passed),
|
|
235
|
-
failed: (await this.getReportData()).trends.map((t) => t.failed),
|
|
236
|
-
avgDuration: (await this.getReportData()).trends.map(
|
|
237
|
-
(t) => t.avg_duration
|
|
238
|
-
)
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
120
|
async prepareReportData(filteredResults, totalDuration, results, projectSet) {
|
|
242
121
|
const totalTests = filteredResults.length;
|
|
243
122
|
const passedTests = results.filter((r) => r.status === "passed").length;
|
|
@@ -255,8 +134,7 @@ var HTMLGenerator = class {
|
|
|
255
134
|
results,
|
|
256
135
|
projectSet
|
|
257
136
|
);
|
|
258
|
-
const
|
|
259
|
-
const localRunDate = formatDateLocal(utcRunDate);
|
|
137
|
+
const lastRunDate = (/* @__PURE__ */ new Date()).toLocaleString();
|
|
260
138
|
const testHistories = await Promise.all(
|
|
261
139
|
results.map(async (result) => {
|
|
262
140
|
const testId = `${result.filePath}:${result.projectName}:${result.title}`;
|
|
@@ -268,34 +146,44 @@ var HTMLGenerator = class {
|
|
|
268
146
|
})
|
|
269
147
|
);
|
|
270
148
|
return {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
149
|
+
summary: {
|
|
150
|
+
overAllResult: {
|
|
151
|
+
pass: passedTests,
|
|
152
|
+
fail: failed,
|
|
153
|
+
skip: results.filter((r) => r.status === "skipped").length,
|
|
154
|
+
retry: results.filter((r) => r.retryAttemptCount).length,
|
|
155
|
+
flaky: flakyTests,
|
|
156
|
+
total: filteredResults.length
|
|
157
|
+
},
|
|
158
|
+
successRate,
|
|
159
|
+
lastRunDate,
|
|
160
|
+
totalDuration,
|
|
161
|
+
stats: this.extractProjectStats(projectResults)
|
|
162
|
+
},
|
|
163
|
+
testResult: {
|
|
164
|
+
tests: groupResults(this.ortoniConfig, results),
|
|
165
|
+
testHistories,
|
|
166
|
+
allTags: Array.from(allTags),
|
|
167
|
+
set: projectSet
|
|
168
|
+
},
|
|
169
|
+
userConfig: {
|
|
170
|
+
projectName: this.ortoniConfig.projectName,
|
|
171
|
+
authorName: this.ortoniConfig.authorName,
|
|
172
|
+
type: this.ortoniConfig.testType,
|
|
173
|
+
title: this.ortoniConfig.title
|
|
174
|
+
},
|
|
175
|
+
userMeta: {
|
|
176
|
+
meta: this.ortoniConfig.meta
|
|
177
|
+
},
|
|
178
|
+
preferences: {
|
|
179
|
+
theme: this.ortoniConfig.preferredTheme,
|
|
180
|
+
logo: this.ortoniConfig.logo || void 0,
|
|
181
|
+
showProject: this.ortoniConfig.showProject || false
|
|
182
|
+
},
|
|
183
|
+
analytics: {
|
|
184
|
+
reportData: await this.getReportData()
|
|
185
|
+
// chartTrendData: await this.chartTrendData(),
|
|
186
|
+
}
|
|
299
187
|
};
|
|
300
188
|
}
|
|
301
189
|
calculateProjectResults(filteredResults, results, projectSet) {
|
|
@@ -313,7 +201,7 @@ var HTMLGenerator = class {
|
|
|
313
201
|
(r) => r.status === "failed" || r.status === "timedOut"
|
|
314
202
|
).length,
|
|
315
203
|
skippedTests: allProjectTests.filter((r) => r.status === "skipped").length,
|
|
316
|
-
retryTests: allProjectTests.filter((r) => r.
|
|
204
|
+
retryTests: allProjectTests.filter((r) => r.retryAttemptCount).length,
|
|
317
205
|
flakyTests: allProjectTests.filter((r) => r.status === "flaky").length,
|
|
318
206
|
totalTests: projectTests.length
|
|
319
207
|
};
|
|
@@ -330,59 +218,18 @@ var HTMLGenerator = class {
|
|
|
330
218
|
flakyTests: projectResults.map((result) => result.flakyTests)
|
|
331
219
|
};
|
|
332
220
|
}
|
|
333
|
-
registerHandlebarsHelpers() {
|
|
334
|
-
Handlebars.registerHelper("joinWithSpace", (array) => array.join(" "));
|
|
335
|
-
Handlebars.registerHelper("json", (context) => safeStringify(context));
|
|
336
|
-
Handlebars.registerHelper(
|
|
337
|
-
"eq",
|
|
338
|
-
(actualStatus, expectedStatus) => actualStatus === expectedStatus
|
|
339
|
-
);
|
|
340
|
-
Handlebars.registerHelper(
|
|
341
|
-
"includes",
|
|
342
|
-
(actualStatus, expectedStatus) => actualStatus.includes(expectedStatus)
|
|
343
|
-
);
|
|
344
|
-
Handlebars.registerHelper("gr", (count) => count > 0);
|
|
345
|
-
Handlebars.registerHelper("or", function(a3, b2) {
|
|
346
|
-
return a3 || b2;
|
|
347
|
-
});
|
|
348
|
-
Handlebars.registerHelper("concat", function(...args) {
|
|
349
|
-
args.pop();
|
|
350
|
-
return args.join("");
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
registerPartials() {
|
|
354
|
-
[
|
|
355
|
-
"head",
|
|
356
|
-
"sidebar",
|
|
357
|
-
"testPanel",
|
|
358
|
-
"summaryCard",
|
|
359
|
-
"userInfo",
|
|
360
|
-
"project",
|
|
361
|
-
"testStatus",
|
|
362
|
-
"testIcons",
|
|
363
|
-
"analytics"
|
|
364
|
-
].forEach((partialName) => {
|
|
365
|
-
Handlebars.registerPartial(
|
|
366
|
-
partialName,
|
|
367
|
-
fs2.readFileSync(
|
|
368
|
-
path3.resolve(__dirname, "views", `${partialName}.hbs`),
|
|
369
|
-
"utf-8"
|
|
370
|
-
)
|
|
371
|
-
);
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
221
|
};
|
|
375
222
|
|
|
376
223
|
// src/helpers/resultProcessor .ts
|
|
377
224
|
import AnsiToHtml from "ansi-to-html";
|
|
378
|
-
import
|
|
225
|
+
import path4 from "path";
|
|
379
226
|
|
|
380
227
|
// src/utils/attachFiles.ts
|
|
381
|
-
import
|
|
382
|
-
import
|
|
228
|
+
import path2 from "path";
|
|
229
|
+
import fs3 from "fs";
|
|
383
230
|
|
|
384
231
|
// src/helpers/markdownConverter.ts
|
|
385
|
-
import
|
|
232
|
+
import fs2 from "fs";
|
|
386
233
|
|
|
387
234
|
// node_modules/marked/lib/marked.esm.js
|
|
388
235
|
function M() {
|
|
@@ -1507,77 +1354,44 @@ var Ft = T.parse;
|
|
|
1507
1354
|
var Qt = b.lex;
|
|
1508
1355
|
|
|
1509
1356
|
// src/helpers/markdownConverter.ts
|
|
1510
|
-
function convertMarkdownToHtml(markdownPath, htmlOutputPath
|
|
1511
|
-
const hasMarkdown =
|
|
1512
|
-
const markdownContent = hasMarkdown ?
|
|
1357
|
+
function convertMarkdownToHtml(markdownPath, htmlOutputPath) {
|
|
1358
|
+
const hasMarkdown = fs2.existsSync(markdownPath);
|
|
1359
|
+
const markdownContent = hasMarkdown ? fs2.readFileSync(markdownPath, "utf-8") : "";
|
|
1513
1360
|
const markdownHtml = hasMarkdown ? k(markdownContent) : "";
|
|
1514
|
-
const
|
|
1515
|
-
|
|
1516
|
-
<div>
|
|
1517
|
-
<pre><code>${step.snippet}</code></pre>
|
|
1518
|
-
${step.location ? `<p><em>Location: ${escapeHtml(step.location)}</em></p>` : ""}
|
|
1519
|
-
</div>`
|
|
1520
|
-
).join("\n");
|
|
1521
|
-
const errorHtml = resultError.map((error) => `<pre><code>${error}</code></pre>`).join("\n");
|
|
1522
|
-
const fullHtml = `
|
|
1523
|
-
<!DOCTYPE html>
|
|
1524
|
-
<html lang="en">
|
|
1525
|
-
<head>
|
|
1526
|
-
<meta charset="UTF-8" />
|
|
1527
|
-
<title>Ortoni Error Report</title>
|
|
1528
|
-
<style>
|
|
1529
|
-
body { font-family: sans-serif; padding: 2rem; line-height: 1.6; max-width: 900px; margin: auto; }
|
|
1530
|
-
code, pre { background: #f4f4f4; padding: 0.5rem; border-radius: 5px; display: block; overflow-x: auto; }
|
|
1531
|
-
h1, h2, h3 { color: #444; }
|
|
1532
|
-
hr { margin: 2em 0; }
|
|
1533
|
-
</style>
|
|
1534
|
-
</head>
|
|
1535
|
-
<body>
|
|
1536
|
-
<h1>Instructions</h1>
|
|
1537
|
-
<ul>
|
|
1538
|
-
<li>Following Playwright test failed.</li>
|
|
1539
|
-
<li>Explain why, be concise, respect Playwright best practices.</li>
|
|
1540
|
-
<li>Provide a snippet of code with the fix, if possible.</li>
|
|
1541
|
-
</ul>
|
|
1542
|
-
<h1>Error Details</h1>
|
|
1543
|
-
${errorHtml || "<p>No errors found.</p>"}
|
|
1544
|
-
${stepsHtml || "<p>No step data available.</p>"}
|
|
1545
|
-
${markdownHtml ? `${markdownHtml}` : ""}
|
|
1546
|
-
</body>
|
|
1547
|
-
</html>
|
|
1548
|
-
`;
|
|
1549
|
-
fs3.writeFileSync(htmlOutputPath, fullHtml, "utf-8");
|
|
1361
|
+
const drawerHtml = `${markdownHtml || ""}`;
|
|
1362
|
+
fs2.writeFileSync(htmlOutputPath, drawerHtml.trim(), "utf-8");
|
|
1550
1363
|
if (hasMarkdown) {
|
|
1551
|
-
|
|
1364
|
+
fs2.unlinkSync(markdownPath);
|
|
1552
1365
|
}
|
|
1553
1366
|
}
|
|
1554
1367
|
|
|
1555
1368
|
// src/utils/attachFiles.ts
|
|
1556
1369
|
function attachFiles(subFolder, result, testResult, config, steps, errors) {
|
|
1557
1370
|
const folderPath = config.folderPath || "ortoni-report";
|
|
1558
|
-
const attachmentsFolder =
|
|
1371
|
+
const attachmentsFolder = path2.join(
|
|
1559
1372
|
folderPath,
|
|
1560
1373
|
"ortoni-data",
|
|
1561
1374
|
"attachments",
|
|
1562
1375
|
subFolder
|
|
1563
1376
|
);
|
|
1564
|
-
if (!
|
|
1565
|
-
|
|
1377
|
+
if (!fs3.existsSync(attachmentsFolder)) {
|
|
1378
|
+
fs3.mkdirSync(attachmentsFolder, { recursive: true });
|
|
1566
1379
|
}
|
|
1567
1380
|
if (!result.attachments) return;
|
|
1568
1381
|
const { base64Image } = config;
|
|
1569
1382
|
testResult.screenshots = [];
|
|
1383
|
+
testResult.videoPath = [];
|
|
1570
1384
|
result.attachments.forEach((attachment) => {
|
|
1571
1385
|
const { contentType, name, path: attachmentPath, body } = attachment;
|
|
1572
1386
|
if (!attachmentPath && !body) return;
|
|
1573
|
-
const fileName = attachmentPath ?
|
|
1574
|
-
const relativePath =
|
|
1387
|
+
const fileName = attachmentPath ? path2.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
|
|
1388
|
+
const relativePath = path2.join(
|
|
1575
1389
|
"ortoni-data",
|
|
1576
1390
|
"attachments",
|
|
1577
1391
|
subFolder,
|
|
1578
1392
|
fileName
|
|
1579
1393
|
);
|
|
1580
|
-
const fullPath =
|
|
1394
|
+
const fullPath = path2.join(attachmentsFolder, fileName);
|
|
1581
1395
|
if (contentType === "image/png") {
|
|
1582
1396
|
handleImage(
|
|
1583
1397
|
attachmentPath,
|
|
@@ -1620,13 +1434,13 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
|
|
|
1620
1434
|
let screenshotPath = "";
|
|
1621
1435
|
if (attachmentPath) {
|
|
1622
1436
|
try {
|
|
1623
|
-
const screenshotContent =
|
|
1437
|
+
const screenshotContent = fs3.readFileSync(
|
|
1624
1438
|
attachmentPath,
|
|
1625
1439
|
base64Image ? "base64" : void 0
|
|
1626
1440
|
);
|
|
1627
1441
|
screenshotPath = base64Image ? `data:image/png;base64,${screenshotContent}` : relativePath;
|
|
1628
1442
|
if (!base64Image) {
|
|
1629
|
-
|
|
1443
|
+
fs3.copyFileSync(attachmentPath, fullPath);
|
|
1630
1444
|
}
|
|
1631
1445
|
} catch (error) {
|
|
1632
1446
|
console.error(
|
|
@@ -1643,13 +1457,17 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
|
|
|
1643
1457
|
}
|
|
1644
1458
|
function handleAttachment(attachmentPath, fullPath, relativePath, resultKey, testResult, steps, errors) {
|
|
1645
1459
|
if (attachmentPath) {
|
|
1646
|
-
|
|
1647
|
-
|
|
1460
|
+
fs3.copyFileSync(attachmentPath, fullPath);
|
|
1461
|
+
if (resultKey === "videoPath") {
|
|
1462
|
+
testResult[resultKey]?.push(relativePath);
|
|
1463
|
+
} else if (resultKey === "tracePath") {
|
|
1464
|
+
testResult[resultKey] = relativePath;
|
|
1465
|
+
}
|
|
1648
1466
|
}
|
|
1649
1467
|
if (resultKey === "markdownPath" && errors) {
|
|
1650
1468
|
const htmlPath = fullPath.replace(/\.md$/, ".html");
|
|
1651
1469
|
const htmlRelativePath = relativePath.replace(/\.md$/, ".html");
|
|
1652
|
-
convertMarkdownToHtml(fullPath, htmlPath
|
|
1470
|
+
convertMarkdownToHtml(fullPath, htmlPath);
|
|
1653
1471
|
testResult[resultKey] = htmlRelativePath;
|
|
1654
1472
|
return;
|
|
1655
1473
|
}
|
|
@@ -1664,6 +1482,61 @@ function getFileExtension(contentType) {
|
|
|
1664
1482
|
return extensions[contentType] || "unknown";
|
|
1665
1483
|
}
|
|
1666
1484
|
|
|
1485
|
+
// src/utils/utils.ts
|
|
1486
|
+
import path3 from "path";
|
|
1487
|
+
function normalizeFilePath(filePath) {
|
|
1488
|
+
const normalizedPath = path3.normalize(filePath);
|
|
1489
|
+
return path3.basename(normalizedPath);
|
|
1490
|
+
}
|
|
1491
|
+
function ensureHtmlExtension(filename) {
|
|
1492
|
+
const ext = path3.extname(filename);
|
|
1493
|
+
if (ext && ext.toLowerCase() === ".html") {
|
|
1494
|
+
return filename;
|
|
1495
|
+
}
|
|
1496
|
+
return `${filename}.html`;
|
|
1497
|
+
}
|
|
1498
|
+
function escapeHtml(unsafe) {
|
|
1499
|
+
if (typeof unsafe !== "string") {
|
|
1500
|
+
return String(unsafe);
|
|
1501
|
+
}
|
|
1502
|
+
return unsafe.replace(/[&<"']/g, function(match) {
|
|
1503
|
+
const escapeMap = {
|
|
1504
|
+
"&": "&",
|
|
1505
|
+
"<": "<",
|
|
1506
|
+
">": ">",
|
|
1507
|
+
'"': """,
|
|
1508
|
+
"'": "'"
|
|
1509
|
+
};
|
|
1510
|
+
return escapeMap[match] || match;
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
function formatDateLocal(dateInput) {
|
|
1514
|
+
const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
|
|
1515
|
+
const options = {
|
|
1516
|
+
year: "numeric",
|
|
1517
|
+
month: "short",
|
|
1518
|
+
day: "2-digit",
|
|
1519
|
+
hour: "2-digit",
|
|
1520
|
+
minute: "2-digit",
|
|
1521
|
+
hour12: true,
|
|
1522
|
+
timeZoneName: "short"
|
|
1523
|
+
// or "Asia/Kolkata"
|
|
1524
|
+
};
|
|
1525
|
+
return new Intl.DateTimeFormat(void 0, options).format(date);
|
|
1526
|
+
}
|
|
1527
|
+
function extractSuites(titlePath) {
|
|
1528
|
+
const tagPattern = /@[\w]+/g;
|
|
1529
|
+
const suiteParts = titlePath.slice(3, titlePath.length - 1).map((p) => p.replace(tagPattern, "").trim());
|
|
1530
|
+
return {
|
|
1531
|
+
hierarchy: suiteParts.join(" > "),
|
|
1532
|
+
// full hierarchy
|
|
1533
|
+
topLevelSuite: suiteParts[0] ?? "",
|
|
1534
|
+
// first suite
|
|
1535
|
+
parentSuite: suiteParts[suiteParts.length - 1] ?? ""
|
|
1536
|
+
// last suite
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1667
1540
|
// src/helpers/resultProcessor .ts
|
|
1668
1541
|
var TestResultProcessor = class {
|
|
1669
1542
|
constructor(projectRoot) {
|
|
@@ -1679,19 +1552,21 @@ var TestResultProcessor = class {
|
|
|
1679
1552
|
const tagPattern = /@[\w]+/g;
|
|
1680
1553
|
const title = test.title.replace(tagPattern, "").trim();
|
|
1681
1554
|
const suite = test.titlePath()[3].replace(tagPattern, "").trim();
|
|
1555
|
+
const suiteAndTitle = extractSuites(test.titlePath());
|
|
1556
|
+
const suiteHierarchy = suiteAndTitle.hierarchy;
|
|
1682
1557
|
const testResult = {
|
|
1683
|
-
|
|
1558
|
+
suiteHierarchy,
|
|
1559
|
+
key: test.id,
|
|
1684
1560
|
annotations: test.annotations,
|
|
1685
1561
|
testTags: test.tags,
|
|
1686
1562
|
location: `${filePath}:${location.line}:${location.column}`,
|
|
1687
|
-
|
|
1688
|
-
isRetry: result.retry,
|
|
1563
|
+
retryAttemptCount: result.retry,
|
|
1689
1564
|
projectName,
|
|
1690
1565
|
suite,
|
|
1691
1566
|
title,
|
|
1692
1567
|
status,
|
|
1693
1568
|
flaky: test.outcome(),
|
|
1694
|
-
duration:
|
|
1569
|
+
duration: result.duration,
|
|
1695
1570
|
errors: result.errors.map(
|
|
1696
1571
|
(e) => this.ansiToHtml.toHtml(escapeHtml(e.stack || e.toString()))
|
|
1697
1572
|
),
|
|
@@ -1703,7 +1578,8 @@ var TestResultProcessor = class {
|
|
|
1703
1578
|
),
|
|
1704
1579
|
filePath,
|
|
1705
1580
|
filters: projectSet,
|
|
1706
|
-
base64Image: ortoniConfig.base64Image
|
|
1581
|
+
base64Image: ortoniConfig.base64Image,
|
|
1582
|
+
testId: `${filePath}:${projectName}:${title}`
|
|
1707
1583
|
};
|
|
1708
1584
|
attachFiles(
|
|
1709
1585
|
test.id,
|
|
@@ -1717,7 +1593,7 @@ var TestResultProcessor = class {
|
|
|
1717
1593
|
}
|
|
1718
1594
|
processSteps(steps) {
|
|
1719
1595
|
return steps.map((step) => {
|
|
1720
|
-
const stepLocation = step.location ? `${
|
|
1596
|
+
const stepLocation = step.location ? `${path4.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
|
|
1721
1597
|
return {
|
|
1722
1598
|
snippet: this.ansiToHtml.toHtml(escapeHtml(step.error?.snippet || "")),
|
|
1723
1599
|
title: step.title,
|
|
@@ -1782,7 +1658,7 @@ var DatabaseManager = class {
|
|
|
1782
1658
|
run_id INTEGER,
|
|
1783
1659
|
test_id TEXT,
|
|
1784
1660
|
status TEXT,
|
|
1785
|
-
duration
|
|
1661
|
+
duration INTEGER, -- store duration as raw ms
|
|
1786
1662
|
error_message TEXT,
|
|
1787
1663
|
FOREIGN KEY (run_id) REFERENCES test_runs (id)
|
|
1788
1664
|
);
|
|
@@ -1842,6 +1718,7 @@ var DatabaseManager = class {
|
|
|
1842
1718
|
`${result.filePath}:${result.projectName}:${result.title}`,
|
|
1843
1719
|
result.status,
|
|
1844
1720
|
result.duration,
|
|
1721
|
+
// store raw ms
|
|
1845
1722
|
result.errors.join("\n")
|
|
1846
1723
|
]);
|
|
1847
1724
|
}
|
|
@@ -1908,7 +1785,7 @@ var DatabaseManager = class {
|
|
|
1908
1785
|
(SELECT COUNT(*) FROM test_results) as totalTests,
|
|
1909
1786
|
(SELECT COUNT(*) FROM test_results WHERE status = 'passed') as passed,
|
|
1910
1787
|
(SELECT COUNT(*) FROM test_results WHERE status = 'failed') as failed,
|
|
1911
|
-
(SELECT AVG(
|
|
1788
|
+
(SELECT AVG(duration) FROM test_results) as avgDuration
|
|
1912
1789
|
`);
|
|
1913
1790
|
const passRate = summary.totalTests ? (summary.passed / summary.totalTests * 100).toFixed(2) : 0;
|
|
1914
1791
|
return {
|
|
@@ -1918,6 +1795,7 @@ var DatabaseManager = class {
|
|
|
1918
1795
|
failed: summary.failed,
|
|
1919
1796
|
passRate: parseFloat(passRate.toString()),
|
|
1920
1797
|
avgDuration: Math.round(summary.avgDuration || 0)
|
|
1798
|
+
// raw ms avg
|
|
1921
1799
|
};
|
|
1922
1800
|
} catch (error) {
|
|
1923
1801
|
console.error("OrtoniReport: Error getting summary data:", error);
|
|
@@ -1942,7 +1820,7 @@ var DatabaseManager = class {
|
|
|
1942
1820
|
SELECT trun.run_date,
|
|
1943
1821
|
SUM(CASE WHEN tr.status = 'passed' THEN 1 ELSE 0 END) AS passed,
|
|
1944
1822
|
SUM(CASE WHEN tr.status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
|
1945
|
-
AVG(
|
|
1823
|
+
AVG(tr.duration) AS avg_duration
|
|
1946
1824
|
FROM test_results tr
|
|
1947
1825
|
JOIN test_runs trun ON tr.run_id = trun.id
|
|
1948
1826
|
GROUP BY trun.run_date
|
|
@@ -1955,6 +1833,7 @@ var DatabaseManager = class {
|
|
|
1955
1833
|
...row,
|
|
1956
1834
|
run_date: formatDateLocal(row.run_date),
|
|
1957
1835
|
avg_duration: Math.round(row.avg_duration || 0)
|
|
1836
|
+
// raw ms avg
|
|
1958
1837
|
}));
|
|
1959
1838
|
} catch (error) {
|
|
1960
1839
|
console.error("OrtoniReport: Error getting trends data:", error);
|
|
@@ -1972,7 +1851,8 @@ var DatabaseManager = class {
|
|
|
1972
1851
|
SELECT
|
|
1973
1852
|
test_id,
|
|
1974
1853
|
COUNT(*) AS total,
|
|
1975
|
-
SUM(CASE WHEN status = 'flaky' THEN 1 ELSE 0 END) AS flaky
|
|
1854
|
+
SUM(CASE WHEN status = 'flaky' THEN 1 ELSE 0 END) AS flaky,
|
|
1855
|
+
AVG(duration) AS avg_duration
|
|
1976
1856
|
FROM test_results
|
|
1977
1857
|
GROUP BY test_id
|
|
1978
1858
|
HAVING flaky > 0
|
|
@@ -1996,7 +1876,7 @@ var DatabaseManager = class {
|
|
|
1996
1876
|
`
|
|
1997
1877
|
SELECT
|
|
1998
1878
|
test_id,
|
|
1999
|
-
AVG(
|
|
1879
|
+
AVG(duration) AS avg_duration
|
|
2000
1880
|
FROM test_results
|
|
2001
1881
|
GROUP BY test_id
|
|
2002
1882
|
ORDER BY avg_duration DESC
|
|
@@ -2007,6 +1887,7 @@ var DatabaseManager = class {
|
|
|
2007
1887
|
return rows.map((row) => ({
|
|
2008
1888
|
test_id: row.test_id,
|
|
2009
1889
|
avg_duration: Math.round(row.avg_duration || 0)
|
|
1890
|
+
// raw ms avg
|
|
2010
1891
|
}));
|
|
2011
1892
|
} catch (error) {
|
|
2012
1893
|
console.error("OrtoniReport: Error getting slow tests:", error);
|
|
@@ -2016,7 +1897,7 @@ var DatabaseManager = class {
|
|
|
2016
1897
|
};
|
|
2017
1898
|
|
|
2018
1899
|
// src/ortoni-report.ts
|
|
2019
|
-
import
|
|
1900
|
+
import path5 from "path";
|
|
2020
1901
|
var OrtoniReport = class {
|
|
2021
1902
|
constructor(ortoniConfig = {}) {
|
|
2022
1903
|
this.ortoniConfig = ortoniConfig;
|
|
@@ -2047,7 +1928,7 @@ var OrtoniReport = class {
|
|
|
2047
1928
|
this.testResultProcessor = new TestResultProcessor(config.rootDir);
|
|
2048
1929
|
this.fileManager.ensureReportDirectory();
|
|
2049
1930
|
await this.dbManager.initialize(
|
|
2050
|
-
|
|
1931
|
+
path5.join(this.folderPath, "ortoni-data-history.sqlite")
|
|
2051
1932
|
);
|
|
2052
1933
|
}
|
|
2053
1934
|
onStdOut(chunk, _test, _result) {
|
|
@@ -2081,23 +1962,21 @@ var OrtoniReport = class {
|
|
|
2081
1962
|
this.overAllStatus = result.status;
|
|
2082
1963
|
if (this.shouldGenerateReport) {
|
|
2083
1964
|
const filteredResults = this.results.filter(
|
|
2084
|
-
(r) => r.status !== "skipped"
|
|
1965
|
+
(r) => r.status !== "skipped"
|
|
2085
1966
|
);
|
|
2086
|
-
const totalDuration =
|
|
2087
|
-
const cssContent = this.fileManager.readCssContent();
|
|
1967
|
+
const totalDuration = result.duration;
|
|
2088
1968
|
const runId = await this.dbManager.saveTestRun();
|
|
2089
1969
|
if (runId !== null) {
|
|
2090
1970
|
await this.dbManager.saveTestResults(runId, this.results);
|
|
2091
|
-
const
|
|
1971
|
+
const finalReportData = await this.htmlGenerator.generateFinalReport(
|
|
2092
1972
|
filteredResults,
|
|
2093
1973
|
totalDuration,
|
|
2094
|
-
cssContent,
|
|
2095
1974
|
this.results,
|
|
2096
1975
|
this.projectSet
|
|
2097
1976
|
);
|
|
2098
1977
|
this.outputPath = this.fileManager.writeReportFile(
|
|
2099
1978
|
this.outputFilename,
|
|
2100
|
-
|
|
1979
|
+
finalReportData
|
|
2101
1980
|
);
|
|
2102
1981
|
} else {
|
|
2103
1982
|
console.error("OrtoniReport: Error saving test run to database");
|