ortoni-report 3.0.5 → 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 +30 -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 -314
- package/dist/ortoni-report.mjs +160 -312
- 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 -1295
- 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: "shortOffset"
|
|
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,108 +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
|
-
#copyBtn {
|
|
1534
|
-
background-color: #007acc;
|
|
1535
|
-
color: white;
|
|
1536
|
-
border: none;
|
|
1537
|
-
padding: 0.5rem 1rem;
|
|
1538
|
-
margin-bottom: 1rem;
|
|
1539
|
-
border-radius: 5px;
|
|
1540
|
-
cursor: pointer;
|
|
1541
|
-
}
|
|
1542
|
-
#copyBtn:hover {
|
|
1543
|
-
background-color: #005fa3;
|
|
1544
|
-
}
|
|
1545
|
-
</style>
|
|
1546
|
-
</head>
|
|
1547
|
-
<body>
|
|
1548
|
-
<button id="copyBtn">\u{1F4CB} Copy All</button>
|
|
1549
|
-
<script>
|
|
1550
|
-
document.getElementById("copyBtn").addEventListener("click", () => {
|
|
1551
|
-
const content = document.getElementById("markdownContent").innerText;
|
|
1552
|
-
navigator.clipboard.writeText(content).then(() => {
|
|
1553
|
-
// change button text to indicate success
|
|
1554
|
-
const button = document.getElementById("copyBtn");
|
|
1555
|
-
button.textContent = "\u2705 Copied!";
|
|
1556
|
-
setTimeout(() => {
|
|
1557
|
-
button.textContent = "\u{1F4CB} Copy All"
|
|
1558
|
-
}, 2000);
|
|
1559
|
-
}).catch(err => {
|
|
1560
|
-
console.error("Failed to copy text: ", err);
|
|
1561
|
-
alert("Failed to copy text. Please try manually.");
|
|
1562
|
-
});
|
|
1563
|
-
});
|
|
1564
|
-
</script>
|
|
1565
|
-
<div id="markdownContent">
|
|
1566
|
-
<h1>Instructions</h1>
|
|
1567
|
-
<ul>
|
|
1568
|
-
<li>Following Playwright test failed.</li>
|
|
1569
|
-
<li>Explain why, be concise, respect Playwright best practices.</li>
|
|
1570
|
-
<li>Provide a snippet of code with the fix, if possible.</li>
|
|
1571
|
-
</ul>
|
|
1572
|
-
<h1>Error Details</h1>
|
|
1573
|
-
${errorHtml || "<p>No errors found.</p>"}
|
|
1574
|
-
${stepsHtml || "<p>No step data available.</p>"}
|
|
1575
|
-
${markdownHtml || ""}
|
|
1576
|
-
</div>
|
|
1577
|
-
</body>
|
|
1578
|
-
</html>
|
|
1579
|
-
`;
|
|
1580
|
-
fs3.writeFileSync(htmlOutputPath, fullHtml, "utf-8");
|
|
1361
|
+
const drawerHtml = `${markdownHtml || ""}`;
|
|
1362
|
+
fs2.writeFileSync(htmlOutputPath, drawerHtml.trim(), "utf-8");
|
|
1581
1363
|
if (hasMarkdown) {
|
|
1582
|
-
|
|
1364
|
+
fs2.unlinkSync(markdownPath);
|
|
1583
1365
|
}
|
|
1584
1366
|
}
|
|
1585
1367
|
|
|
1586
1368
|
// src/utils/attachFiles.ts
|
|
1587
1369
|
function attachFiles(subFolder, result, testResult, config, steps, errors) {
|
|
1588
1370
|
const folderPath = config.folderPath || "ortoni-report";
|
|
1589
|
-
const attachmentsFolder =
|
|
1371
|
+
const attachmentsFolder = path2.join(
|
|
1590
1372
|
folderPath,
|
|
1591
1373
|
"ortoni-data",
|
|
1592
1374
|
"attachments",
|
|
1593
1375
|
subFolder
|
|
1594
1376
|
);
|
|
1595
|
-
if (!
|
|
1596
|
-
|
|
1377
|
+
if (!fs3.existsSync(attachmentsFolder)) {
|
|
1378
|
+
fs3.mkdirSync(attachmentsFolder, { recursive: true });
|
|
1597
1379
|
}
|
|
1598
1380
|
if (!result.attachments) return;
|
|
1599
1381
|
const { base64Image } = config;
|
|
1600
1382
|
testResult.screenshots = [];
|
|
1383
|
+
testResult.videoPath = [];
|
|
1601
1384
|
result.attachments.forEach((attachment) => {
|
|
1602
1385
|
const { contentType, name, path: attachmentPath, body } = attachment;
|
|
1603
1386
|
if (!attachmentPath && !body) return;
|
|
1604
|
-
const fileName = attachmentPath ?
|
|
1605
|
-
const relativePath =
|
|
1387
|
+
const fileName = attachmentPath ? path2.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
|
|
1388
|
+
const relativePath = path2.join(
|
|
1606
1389
|
"ortoni-data",
|
|
1607
1390
|
"attachments",
|
|
1608
1391
|
subFolder,
|
|
1609
1392
|
fileName
|
|
1610
1393
|
);
|
|
1611
|
-
const fullPath =
|
|
1394
|
+
const fullPath = path2.join(attachmentsFolder, fileName);
|
|
1612
1395
|
if (contentType === "image/png") {
|
|
1613
1396
|
handleImage(
|
|
1614
1397
|
attachmentPath,
|
|
@@ -1651,13 +1434,13 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
|
|
|
1651
1434
|
let screenshotPath = "";
|
|
1652
1435
|
if (attachmentPath) {
|
|
1653
1436
|
try {
|
|
1654
|
-
const screenshotContent =
|
|
1437
|
+
const screenshotContent = fs3.readFileSync(
|
|
1655
1438
|
attachmentPath,
|
|
1656
1439
|
base64Image ? "base64" : void 0
|
|
1657
1440
|
);
|
|
1658
1441
|
screenshotPath = base64Image ? `data:image/png;base64,${screenshotContent}` : relativePath;
|
|
1659
1442
|
if (!base64Image) {
|
|
1660
|
-
|
|
1443
|
+
fs3.copyFileSync(attachmentPath, fullPath);
|
|
1661
1444
|
}
|
|
1662
1445
|
} catch (error) {
|
|
1663
1446
|
console.error(
|
|
@@ -1674,13 +1457,17 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
|
|
|
1674
1457
|
}
|
|
1675
1458
|
function handleAttachment(attachmentPath, fullPath, relativePath, resultKey, testResult, steps, errors) {
|
|
1676
1459
|
if (attachmentPath) {
|
|
1677
|
-
|
|
1678
|
-
|
|
1460
|
+
fs3.copyFileSync(attachmentPath, fullPath);
|
|
1461
|
+
if (resultKey === "videoPath") {
|
|
1462
|
+
testResult[resultKey]?.push(relativePath);
|
|
1463
|
+
} else if (resultKey === "tracePath") {
|
|
1464
|
+
testResult[resultKey] = relativePath;
|
|
1465
|
+
}
|
|
1679
1466
|
}
|
|
1680
1467
|
if (resultKey === "markdownPath" && errors) {
|
|
1681
1468
|
const htmlPath = fullPath.replace(/\.md$/, ".html");
|
|
1682
1469
|
const htmlRelativePath = relativePath.replace(/\.md$/, ".html");
|
|
1683
|
-
convertMarkdownToHtml(fullPath, htmlPath
|
|
1470
|
+
convertMarkdownToHtml(fullPath, htmlPath);
|
|
1684
1471
|
testResult[resultKey] = htmlRelativePath;
|
|
1685
1472
|
return;
|
|
1686
1473
|
}
|
|
@@ -1695,6 +1482,61 @@ function getFileExtension(contentType) {
|
|
|
1695
1482
|
return extensions[contentType] || "unknown";
|
|
1696
1483
|
}
|
|
1697
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
|
+
|
|
1698
1540
|
// src/helpers/resultProcessor .ts
|
|
1699
1541
|
var TestResultProcessor = class {
|
|
1700
1542
|
constructor(projectRoot) {
|
|
@@ -1710,19 +1552,21 @@ var TestResultProcessor = class {
|
|
|
1710
1552
|
const tagPattern = /@[\w]+/g;
|
|
1711
1553
|
const title = test.title.replace(tagPattern, "").trim();
|
|
1712
1554
|
const suite = test.titlePath()[3].replace(tagPattern, "").trim();
|
|
1555
|
+
const suiteAndTitle = extractSuites(test.titlePath());
|
|
1556
|
+
const suiteHierarchy = suiteAndTitle.hierarchy;
|
|
1713
1557
|
const testResult = {
|
|
1714
|
-
|
|
1558
|
+
suiteHierarchy,
|
|
1559
|
+
key: test.id,
|
|
1715
1560
|
annotations: test.annotations,
|
|
1716
1561
|
testTags: test.tags,
|
|
1717
1562
|
location: `${filePath}:${location.line}:${location.column}`,
|
|
1718
|
-
|
|
1719
|
-
isRetry: result.retry,
|
|
1563
|
+
retryAttemptCount: result.retry,
|
|
1720
1564
|
projectName,
|
|
1721
1565
|
suite,
|
|
1722
1566
|
title,
|
|
1723
1567
|
status,
|
|
1724
1568
|
flaky: test.outcome(),
|
|
1725
|
-
duration:
|
|
1569
|
+
duration: result.duration,
|
|
1726
1570
|
errors: result.errors.map(
|
|
1727
1571
|
(e) => this.ansiToHtml.toHtml(escapeHtml(e.stack || e.toString()))
|
|
1728
1572
|
),
|
|
@@ -1734,7 +1578,8 @@ var TestResultProcessor = class {
|
|
|
1734
1578
|
),
|
|
1735
1579
|
filePath,
|
|
1736
1580
|
filters: projectSet,
|
|
1737
|
-
base64Image: ortoniConfig.base64Image
|
|
1581
|
+
base64Image: ortoniConfig.base64Image,
|
|
1582
|
+
testId: `${filePath}:${projectName}:${title}`
|
|
1738
1583
|
};
|
|
1739
1584
|
attachFiles(
|
|
1740
1585
|
test.id,
|
|
@@ -1748,7 +1593,7 @@ var TestResultProcessor = class {
|
|
|
1748
1593
|
}
|
|
1749
1594
|
processSteps(steps) {
|
|
1750
1595
|
return steps.map((step) => {
|
|
1751
|
-
const stepLocation = step.location ? `${
|
|
1596
|
+
const stepLocation = step.location ? `${path4.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
|
|
1752
1597
|
return {
|
|
1753
1598
|
snippet: this.ansiToHtml.toHtml(escapeHtml(step.error?.snippet || "")),
|
|
1754
1599
|
title: step.title,
|
|
@@ -1813,7 +1658,7 @@ var DatabaseManager = class {
|
|
|
1813
1658
|
run_id INTEGER,
|
|
1814
1659
|
test_id TEXT,
|
|
1815
1660
|
status TEXT,
|
|
1816
|
-
duration
|
|
1661
|
+
duration INTEGER, -- store duration as raw ms
|
|
1817
1662
|
error_message TEXT,
|
|
1818
1663
|
FOREIGN KEY (run_id) REFERENCES test_runs (id)
|
|
1819
1664
|
);
|
|
@@ -1873,6 +1718,7 @@ var DatabaseManager = class {
|
|
|
1873
1718
|
`${result.filePath}:${result.projectName}:${result.title}`,
|
|
1874
1719
|
result.status,
|
|
1875
1720
|
result.duration,
|
|
1721
|
+
// store raw ms
|
|
1876
1722
|
result.errors.join("\n")
|
|
1877
1723
|
]);
|
|
1878
1724
|
}
|
|
@@ -1939,7 +1785,7 @@ var DatabaseManager = class {
|
|
|
1939
1785
|
(SELECT COUNT(*) FROM test_results) as totalTests,
|
|
1940
1786
|
(SELECT COUNT(*) FROM test_results WHERE status = 'passed') as passed,
|
|
1941
1787
|
(SELECT COUNT(*) FROM test_results WHERE status = 'failed') as failed,
|
|
1942
|
-
(SELECT AVG(
|
|
1788
|
+
(SELECT AVG(duration) FROM test_results) as avgDuration
|
|
1943
1789
|
`);
|
|
1944
1790
|
const passRate = summary.totalTests ? (summary.passed / summary.totalTests * 100).toFixed(2) : 0;
|
|
1945
1791
|
return {
|
|
@@ -1949,6 +1795,7 @@ var DatabaseManager = class {
|
|
|
1949
1795
|
failed: summary.failed,
|
|
1950
1796
|
passRate: parseFloat(passRate.toString()),
|
|
1951
1797
|
avgDuration: Math.round(summary.avgDuration || 0)
|
|
1798
|
+
// raw ms avg
|
|
1952
1799
|
};
|
|
1953
1800
|
} catch (error) {
|
|
1954
1801
|
console.error("OrtoniReport: Error getting summary data:", error);
|
|
@@ -1973,7 +1820,7 @@ var DatabaseManager = class {
|
|
|
1973
1820
|
SELECT trun.run_date,
|
|
1974
1821
|
SUM(CASE WHEN tr.status = 'passed' THEN 1 ELSE 0 END) AS passed,
|
|
1975
1822
|
SUM(CASE WHEN tr.status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
|
1976
|
-
AVG(
|
|
1823
|
+
AVG(tr.duration) AS avg_duration
|
|
1977
1824
|
FROM test_results tr
|
|
1978
1825
|
JOIN test_runs trun ON tr.run_id = trun.id
|
|
1979
1826
|
GROUP BY trun.run_date
|
|
@@ -1986,6 +1833,7 @@ var DatabaseManager = class {
|
|
|
1986
1833
|
...row,
|
|
1987
1834
|
run_date: formatDateLocal(row.run_date),
|
|
1988
1835
|
avg_duration: Math.round(row.avg_duration || 0)
|
|
1836
|
+
// raw ms avg
|
|
1989
1837
|
}));
|
|
1990
1838
|
} catch (error) {
|
|
1991
1839
|
console.error("OrtoniReport: Error getting trends data:", error);
|
|
@@ -2003,7 +1851,8 @@ var DatabaseManager = class {
|
|
|
2003
1851
|
SELECT
|
|
2004
1852
|
test_id,
|
|
2005
1853
|
COUNT(*) AS total,
|
|
2006
|
-
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
|
|
2007
1856
|
FROM test_results
|
|
2008
1857
|
GROUP BY test_id
|
|
2009
1858
|
HAVING flaky > 0
|
|
@@ -2027,7 +1876,7 @@ var DatabaseManager = class {
|
|
|
2027
1876
|
`
|
|
2028
1877
|
SELECT
|
|
2029
1878
|
test_id,
|
|
2030
|
-
AVG(
|
|
1879
|
+
AVG(duration) AS avg_duration
|
|
2031
1880
|
FROM test_results
|
|
2032
1881
|
GROUP BY test_id
|
|
2033
1882
|
ORDER BY avg_duration DESC
|
|
@@ -2038,6 +1887,7 @@ var DatabaseManager = class {
|
|
|
2038
1887
|
return rows.map((row) => ({
|
|
2039
1888
|
test_id: row.test_id,
|
|
2040
1889
|
avg_duration: Math.round(row.avg_duration || 0)
|
|
1890
|
+
// raw ms avg
|
|
2041
1891
|
}));
|
|
2042
1892
|
} catch (error) {
|
|
2043
1893
|
console.error("OrtoniReport: Error getting slow tests:", error);
|
|
@@ -2047,7 +1897,7 @@ var DatabaseManager = class {
|
|
|
2047
1897
|
};
|
|
2048
1898
|
|
|
2049
1899
|
// src/ortoni-report.ts
|
|
2050
|
-
import
|
|
1900
|
+
import path5 from "path";
|
|
2051
1901
|
var OrtoniReport = class {
|
|
2052
1902
|
constructor(ortoniConfig = {}) {
|
|
2053
1903
|
this.ortoniConfig = ortoniConfig;
|
|
@@ -2078,7 +1928,7 @@ var OrtoniReport = class {
|
|
|
2078
1928
|
this.testResultProcessor = new TestResultProcessor(config.rootDir);
|
|
2079
1929
|
this.fileManager.ensureReportDirectory();
|
|
2080
1930
|
await this.dbManager.initialize(
|
|
2081
|
-
|
|
1931
|
+
path5.join(this.folderPath, "ortoni-data-history.sqlite")
|
|
2082
1932
|
);
|
|
2083
1933
|
}
|
|
2084
1934
|
onStdOut(chunk, _test, _result) {
|
|
@@ -2112,23 +1962,21 @@ var OrtoniReport = class {
|
|
|
2112
1962
|
this.overAllStatus = result.status;
|
|
2113
1963
|
if (this.shouldGenerateReport) {
|
|
2114
1964
|
const filteredResults = this.results.filter(
|
|
2115
|
-
(r) => r.status !== "skipped"
|
|
1965
|
+
(r) => r.status !== "skipped"
|
|
2116
1966
|
);
|
|
2117
|
-
const totalDuration =
|
|
2118
|
-
const cssContent = this.fileManager.readCssContent();
|
|
1967
|
+
const totalDuration = result.duration;
|
|
2119
1968
|
const runId = await this.dbManager.saveTestRun();
|
|
2120
1969
|
if (runId !== null) {
|
|
2121
1970
|
await this.dbManager.saveTestResults(runId, this.results);
|
|
2122
|
-
const
|
|
1971
|
+
const finalReportData = await this.htmlGenerator.generateFinalReport(
|
|
2123
1972
|
filteredResults,
|
|
2124
1973
|
totalDuration,
|
|
2125
|
-
cssContent,
|
|
2126
1974
|
this.results,
|
|
2127
1975
|
this.projectSet
|
|
2128
1976
|
);
|
|
2129
1977
|
this.outputPath = this.fileManager.writeReportFile(
|
|
2130
1978
|
this.outputFilename,
|
|
2131
|
-
|
|
1979
|
+
finalReportData
|
|
2132
1980
|
);
|
|
2133
1981
|
} else {
|
|
2134
1982
|
console.error("OrtoniReport: Error saving test run to database");
|