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.js
CHANGED
|
@@ -54,17 +54,17 @@ var FileManager = class {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
writeReportFile(filename,
|
|
57
|
+
writeReportFile(filename, data) {
|
|
58
|
+
const templatePath = import_path.default.resolve(__dirname, "index.html");
|
|
59
|
+
let html = import_fs.default.readFileSync(templatePath, "utf-8");
|
|
60
|
+
const reportJSON = JSON.stringify({
|
|
61
|
+
data
|
|
62
|
+
});
|
|
63
|
+
html = html.replace("__ORTONI_TEST_REPORTDATA__", reportJSON);
|
|
58
64
|
const outputPath = import_path.default.join(process.cwd(), this.folderPath, filename);
|
|
59
|
-
import_fs.default.writeFileSync(outputPath,
|
|
65
|
+
import_fs.default.writeFileSync(outputPath, html);
|
|
60
66
|
return outputPath;
|
|
61
67
|
}
|
|
62
|
-
readCssContent() {
|
|
63
|
-
return import_fs.default.readFileSync(
|
|
64
|
-
import_path.default.resolve(__dirname, "style", "main.css"),
|
|
65
|
-
"utf-8"
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
68
|
copyTraceViewerAssets(skip) {
|
|
69
69
|
if (skip) return;
|
|
70
70
|
const traceViewerFolder = import_path.default.join(
|
|
@@ -99,157 +99,48 @@ var FileManager = class {
|
|
|
99
99
|
}
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
-
// src/helpers/HTMLGenerator.ts
|
|
103
|
-
var import_path3 = __toESM(require("path"));
|
|
104
|
-
|
|
105
102
|
// src/utils/groupProjects.ts
|
|
106
103
|
function groupResults(config, results) {
|
|
107
104
|
if (config.showProject) {
|
|
108
105
|
const groupedResults = results.reduce((acc, result, index) => {
|
|
109
106
|
const testId = `${result.filePath}:${result.projectName}:${result.title}`;
|
|
107
|
+
const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
|
|
110
108
|
const { filePath, suite, projectName } = result;
|
|
111
109
|
acc[filePath] = acc[filePath] || {};
|
|
112
110
|
acc[filePath][suite] = acc[filePath][suite] || {};
|
|
113
111
|
acc[filePath][suite][projectName] = acc[filePath][suite][projectName] || [];
|
|
114
|
-
acc[filePath][suite][projectName].push({ ...result, index, testId });
|
|
112
|
+
acc[filePath][suite][projectName].push({ ...result, index, testId, key });
|
|
115
113
|
return acc;
|
|
116
114
|
}, {});
|
|
117
115
|
return groupedResults;
|
|
118
116
|
} else {
|
|
119
117
|
const groupedResults = results.reduce((acc, result, index) => {
|
|
120
118
|
const testId = `${result.filePath}:${result.projectName}:${result.title}`;
|
|
119
|
+
const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
|
|
121
120
|
const { filePath, suite } = result;
|
|
122
121
|
acc[filePath] = acc[filePath] || {};
|
|
123
122
|
acc[filePath][suite] = acc[filePath][suite] || [];
|
|
124
|
-
acc[filePath][suite].push({ ...result, index, testId });
|
|
123
|
+
acc[filePath][suite].push({ ...result, index, testId, key });
|
|
125
124
|
return acc;
|
|
126
125
|
}, {});
|
|
127
126
|
return groupedResults;
|
|
128
127
|
}
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
// src/utils/utils.ts
|
|
132
|
-
var import_path2 = __toESM(require("path"));
|
|
133
|
-
function msToTime(duration) {
|
|
134
|
-
const milliseconds = Math.floor(duration % 1e3);
|
|
135
|
-
const seconds = Math.floor(duration / 1e3 % 60);
|
|
136
|
-
const minutes = Math.floor(duration / (1e3 * 60) % 60);
|
|
137
|
-
const hours = Math.floor(duration / (1e3 * 60 * 60) % 24);
|
|
138
|
-
let result = "";
|
|
139
|
-
if (hours > 0) {
|
|
140
|
-
result += `${hours}h:`;
|
|
141
|
-
}
|
|
142
|
-
if (minutes > 0 || hours > 0) {
|
|
143
|
-
result += `${minutes < 10 ? "0" + minutes : minutes}m:`;
|
|
144
|
-
}
|
|
145
|
-
if (seconds > 0 || minutes > 0 || hours > 0) {
|
|
146
|
-
result += `${seconds < 10 ? "0" + seconds : seconds}s`;
|
|
147
|
-
}
|
|
148
|
-
if (milliseconds > 0 && !(seconds > 0 || minutes > 0 || hours > 0)) {
|
|
149
|
-
result += `${milliseconds}ms`;
|
|
150
|
-
} else if (milliseconds > 0) {
|
|
151
|
-
result += `:${milliseconds < 100 ? "0" + milliseconds : milliseconds}ms`;
|
|
152
|
-
}
|
|
153
|
-
return result;
|
|
154
|
-
}
|
|
155
|
-
function normalizeFilePath(filePath) {
|
|
156
|
-
const normalizedPath = import_path2.default.normalize(filePath);
|
|
157
|
-
return import_path2.default.basename(normalizedPath);
|
|
158
|
-
}
|
|
159
|
-
function formatDate(date) {
|
|
160
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
161
|
-
const month = date.toLocaleString("default", { month: "short" });
|
|
162
|
-
const year = date.getFullYear();
|
|
163
|
-
const time = date.toLocaleTimeString();
|
|
164
|
-
return `${day}-${month}-${year} ${time}`;
|
|
165
|
-
}
|
|
166
|
-
function safeStringify(obj, indent = 2) {
|
|
167
|
-
const cache = /* @__PURE__ */ new Set();
|
|
168
|
-
const json = JSON.stringify(
|
|
169
|
-
obj,
|
|
170
|
-
(key, value) => {
|
|
171
|
-
if (typeof value === "object" && value !== null) {
|
|
172
|
-
if (cache.has(value)) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
cache.add(value);
|
|
176
|
-
}
|
|
177
|
-
return value;
|
|
178
|
-
},
|
|
179
|
-
indent
|
|
180
|
-
);
|
|
181
|
-
cache.clear();
|
|
182
|
-
return json;
|
|
183
|
-
}
|
|
184
|
-
function ensureHtmlExtension(filename) {
|
|
185
|
-
const ext = import_path2.default.extname(filename);
|
|
186
|
-
if (ext && ext.toLowerCase() === ".html") {
|
|
187
|
-
return filename;
|
|
188
|
-
}
|
|
189
|
-
return `${filename}.html`;
|
|
190
|
-
}
|
|
191
|
-
function escapeHtml(unsafe) {
|
|
192
|
-
if (typeof unsafe !== "string") {
|
|
193
|
-
return String(unsafe);
|
|
194
|
-
}
|
|
195
|
-
return unsafe.replace(/[&<"']/g, function(match) {
|
|
196
|
-
const escapeMap = {
|
|
197
|
-
"&": "&",
|
|
198
|
-
"<": "<",
|
|
199
|
-
">": ">",
|
|
200
|
-
'"': """,
|
|
201
|
-
"'": "'"
|
|
202
|
-
};
|
|
203
|
-
return escapeMap[match] || match;
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
function formatDateUTC(date) {
|
|
207
|
-
return date.toISOString();
|
|
208
|
-
}
|
|
209
|
-
function formatDateLocal(isoString) {
|
|
210
|
-
const date = new Date(isoString);
|
|
211
|
-
const options = {
|
|
212
|
-
year: "numeric",
|
|
213
|
-
month: "short",
|
|
214
|
-
day: "2-digit",
|
|
215
|
-
hour: "2-digit",
|
|
216
|
-
minute: "2-digit",
|
|
217
|
-
hour12: true,
|
|
218
|
-
timeZoneName: "short"
|
|
219
|
-
};
|
|
220
|
-
return new Intl.DateTimeFormat(void 0, options).format(date);
|
|
221
|
-
}
|
|
222
|
-
function formatDateNoTimezone(isoString) {
|
|
223
|
-
const date = new Date(isoString);
|
|
224
|
-
return date.toLocaleString("en-US", {
|
|
225
|
-
dateStyle: "medium",
|
|
226
|
-
timeStyle: "short"
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
130
|
// src/helpers/HTMLGenerator.ts
|
|
231
|
-
var import_fs2 = __toESM(require("fs"));
|
|
232
|
-
var import_handlebars = __toESM(require("handlebars"));
|
|
233
131
|
var HTMLGenerator = class {
|
|
234
132
|
constructor(ortoniConfig, dbManager) {
|
|
235
133
|
this.ortoniConfig = ortoniConfig;
|
|
236
|
-
this.registerHandlebarsHelpers();
|
|
237
|
-
this.registerPartials();
|
|
238
134
|
this.dbManager = dbManager;
|
|
239
135
|
}
|
|
240
|
-
async
|
|
136
|
+
async generateFinalReport(filteredResults, totalDuration, results, projectSet) {
|
|
241
137
|
const data = await this.prepareReportData(
|
|
242
138
|
filteredResults,
|
|
243
139
|
totalDuration,
|
|
244
140
|
results,
|
|
245
141
|
projectSet
|
|
246
142
|
);
|
|
247
|
-
|
|
248
|
-
import_path3.default.resolve(__dirname, "views", "main.hbs"),
|
|
249
|
-
"utf-8"
|
|
250
|
-
);
|
|
251
|
-
const template = import_handlebars.default.compile(templateSource);
|
|
252
|
-
return template({ ...data, inlineCss: cssContent });
|
|
143
|
+
return data;
|
|
253
144
|
}
|
|
254
145
|
async getReportData() {
|
|
255
146
|
return {
|
|
@@ -259,18 +150,6 @@ var HTMLGenerator = class {
|
|
|
259
150
|
slowTests: await this.dbManager.getSlowTests()
|
|
260
151
|
};
|
|
261
152
|
}
|
|
262
|
-
async chartTrendData() {
|
|
263
|
-
return {
|
|
264
|
-
labels: (await this.getReportData()).trends.map(
|
|
265
|
-
(t) => formatDateNoTimezone(t.run_date)
|
|
266
|
-
),
|
|
267
|
-
passed: (await this.getReportData()).trends.map((t) => t.passed),
|
|
268
|
-
failed: (await this.getReportData()).trends.map((t) => t.failed),
|
|
269
|
-
avgDuration: (await this.getReportData()).trends.map(
|
|
270
|
-
(t) => t.avg_duration
|
|
271
|
-
)
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
153
|
async prepareReportData(filteredResults, totalDuration, results, projectSet) {
|
|
275
154
|
const totalTests = filteredResults.length;
|
|
276
155
|
const passedTests = results.filter((r) => r.status === "passed").length;
|
|
@@ -288,8 +167,7 @@ var HTMLGenerator = class {
|
|
|
288
167
|
results,
|
|
289
168
|
projectSet
|
|
290
169
|
);
|
|
291
|
-
const
|
|
292
|
-
const localRunDate = formatDateLocal(utcRunDate);
|
|
170
|
+
const lastRunDate = (/* @__PURE__ */ new Date()).toLocaleString();
|
|
293
171
|
const testHistories = await Promise.all(
|
|
294
172
|
results.map(async (result) => {
|
|
295
173
|
const testId = `${result.filePath}:${result.projectName}:${result.title}`;
|
|
@@ -301,34 +179,44 @@ var HTMLGenerator = class {
|
|
|
301
179
|
})
|
|
302
180
|
);
|
|
303
181
|
return {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
182
|
+
summary: {
|
|
183
|
+
overAllResult: {
|
|
184
|
+
pass: passedTests,
|
|
185
|
+
fail: failed,
|
|
186
|
+
skip: results.filter((r) => r.status === "skipped").length,
|
|
187
|
+
retry: results.filter((r) => r.retryAttemptCount).length,
|
|
188
|
+
flaky: flakyTests,
|
|
189
|
+
total: filteredResults.length
|
|
190
|
+
},
|
|
191
|
+
successRate,
|
|
192
|
+
lastRunDate,
|
|
193
|
+
totalDuration,
|
|
194
|
+
stats: this.extractProjectStats(projectResults)
|
|
195
|
+
},
|
|
196
|
+
testResult: {
|
|
197
|
+
tests: groupResults(this.ortoniConfig, results),
|
|
198
|
+
testHistories,
|
|
199
|
+
allTags: Array.from(allTags),
|
|
200
|
+
set: projectSet
|
|
201
|
+
},
|
|
202
|
+
userConfig: {
|
|
203
|
+
projectName: this.ortoniConfig.projectName,
|
|
204
|
+
authorName: this.ortoniConfig.authorName,
|
|
205
|
+
type: this.ortoniConfig.testType,
|
|
206
|
+
title: this.ortoniConfig.title
|
|
207
|
+
},
|
|
208
|
+
userMeta: {
|
|
209
|
+
meta: this.ortoniConfig.meta
|
|
210
|
+
},
|
|
211
|
+
preferences: {
|
|
212
|
+
theme: this.ortoniConfig.preferredTheme,
|
|
213
|
+
logo: this.ortoniConfig.logo || void 0,
|
|
214
|
+
showProject: this.ortoniConfig.showProject || false
|
|
215
|
+
},
|
|
216
|
+
analytics: {
|
|
217
|
+
reportData: await this.getReportData()
|
|
218
|
+
// chartTrendData: await this.chartTrendData(),
|
|
219
|
+
}
|
|
332
220
|
};
|
|
333
221
|
}
|
|
334
222
|
calculateProjectResults(filteredResults, results, projectSet) {
|
|
@@ -346,7 +234,7 @@ var HTMLGenerator = class {
|
|
|
346
234
|
(r) => r.status === "failed" || r.status === "timedOut"
|
|
347
235
|
).length,
|
|
348
236
|
skippedTests: allProjectTests.filter((r) => r.status === "skipped").length,
|
|
349
|
-
retryTests: allProjectTests.filter((r) => r.
|
|
237
|
+
retryTests: allProjectTests.filter((r) => r.retryAttemptCount).length,
|
|
350
238
|
flakyTests: allProjectTests.filter((r) => r.status === "flaky").length,
|
|
351
239
|
totalTests: projectTests.length
|
|
352
240
|
};
|
|
@@ -363,59 +251,18 @@ var HTMLGenerator = class {
|
|
|
363
251
|
flakyTests: projectResults.map((result) => result.flakyTests)
|
|
364
252
|
};
|
|
365
253
|
}
|
|
366
|
-
registerHandlebarsHelpers() {
|
|
367
|
-
import_handlebars.default.registerHelper("joinWithSpace", (array) => array.join(" "));
|
|
368
|
-
import_handlebars.default.registerHelper("json", (context) => safeStringify(context));
|
|
369
|
-
import_handlebars.default.registerHelper(
|
|
370
|
-
"eq",
|
|
371
|
-
(actualStatus, expectedStatus) => actualStatus === expectedStatus
|
|
372
|
-
);
|
|
373
|
-
import_handlebars.default.registerHelper(
|
|
374
|
-
"includes",
|
|
375
|
-
(actualStatus, expectedStatus) => actualStatus.includes(expectedStatus)
|
|
376
|
-
);
|
|
377
|
-
import_handlebars.default.registerHelper("gr", (count) => count > 0);
|
|
378
|
-
import_handlebars.default.registerHelper("or", function(a3, b2) {
|
|
379
|
-
return a3 || b2;
|
|
380
|
-
});
|
|
381
|
-
import_handlebars.default.registerHelper("concat", function(...args) {
|
|
382
|
-
args.pop();
|
|
383
|
-
return args.join("");
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
registerPartials() {
|
|
387
|
-
[
|
|
388
|
-
"head",
|
|
389
|
-
"sidebar",
|
|
390
|
-
"testPanel",
|
|
391
|
-
"summaryCard",
|
|
392
|
-
"userInfo",
|
|
393
|
-
"project",
|
|
394
|
-
"testStatus",
|
|
395
|
-
"testIcons",
|
|
396
|
-
"analytics"
|
|
397
|
-
].forEach((partialName) => {
|
|
398
|
-
import_handlebars.default.registerPartial(
|
|
399
|
-
partialName,
|
|
400
|
-
import_fs2.default.readFileSync(
|
|
401
|
-
import_path3.default.resolve(__dirname, "views", `${partialName}.hbs`),
|
|
402
|
-
"utf-8"
|
|
403
|
-
)
|
|
404
|
-
);
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
254
|
};
|
|
408
255
|
|
|
409
256
|
// src/helpers/resultProcessor .ts
|
|
410
257
|
var import_ansi_to_html = __toESM(require("ansi-to-html"));
|
|
411
|
-
var
|
|
258
|
+
var import_path4 = __toESM(require("path"));
|
|
412
259
|
|
|
413
260
|
// src/utils/attachFiles.ts
|
|
414
|
-
var
|
|
415
|
-
var
|
|
261
|
+
var import_path2 = __toESM(require("path"));
|
|
262
|
+
var import_fs3 = __toESM(require("fs"));
|
|
416
263
|
|
|
417
264
|
// src/helpers/markdownConverter.ts
|
|
418
|
-
var
|
|
265
|
+
var import_fs2 = __toESM(require("fs"));
|
|
419
266
|
|
|
420
267
|
// node_modules/marked/lib/marked.esm.js
|
|
421
268
|
function M() {
|
|
@@ -1540,77 +1387,44 @@ var Ft = T.parse;
|
|
|
1540
1387
|
var Qt = b.lex;
|
|
1541
1388
|
|
|
1542
1389
|
// src/helpers/markdownConverter.ts
|
|
1543
|
-
function convertMarkdownToHtml(markdownPath, htmlOutputPath
|
|
1544
|
-
const hasMarkdown =
|
|
1545
|
-
const markdownContent = hasMarkdown ?
|
|
1390
|
+
function convertMarkdownToHtml(markdownPath, htmlOutputPath) {
|
|
1391
|
+
const hasMarkdown = import_fs2.default.existsSync(markdownPath);
|
|
1392
|
+
const markdownContent = hasMarkdown ? import_fs2.default.readFileSync(markdownPath, "utf-8") : "";
|
|
1546
1393
|
const markdownHtml = hasMarkdown ? k(markdownContent) : "";
|
|
1547
|
-
const
|
|
1548
|
-
|
|
1549
|
-
<div>
|
|
1550
|
-
<pre><code>${step.snippet}</code></pre>
|
|
1551
|
-
${step.location ? `<p><em>Location: ${escapeHtml(step.location)}</em></p>` : ""}
|
|
1552
|
-
</div>`
|
|
1553
|
-
).join("\n");
|
|
1554
|
-
const errorHtml = resultError.map((error) => `<pre><code>${error}</code></pre>`).join("\n");
|
|
1555
|
-
const fullHtml = `
|
|
1556
|
-
<!DOCTYPE html>
|
|
1557
|
-
<html lang="en">
|
|
1558
|
-
<head>
|
|
1559
|
-
<meta charset="UTF-8" />
|
|
1560
|
-
<title>Ortoni Error Report</title>
|
|
1561
|
-
<style>
|
|
1562
|
-
body { font-family: sans-serif; padding: 2rem; line-height: 1.6; max-width: 900px; margin: auto; }
|
|
1563
|
-
code, pre { background: #f4f4f4; padding: 0.5rem; border-radius: 5px; display: block; overflow-x: auto; }
|
|
1564
|
-
h1, h2, h3 { color: #444; }
|
|
1565
|
-
hr { margin: 2em 0; }
|
|
1566
|
-
</style>
|
|
1567
|
-
</head>
|
|
1568
|
-
<body>
|
|
1569
|
-
<h1>Instructions</h1>
|
|
1570
|
-
<ul>
|
|
1571
|
-
<li>Following Playwright test failed.</li>
|
|
1572
|
-
<li>Explain why, be concise, respect Playwright best practices.</li>
|
|
1573
|
-
<li>Provide a snippet of code with the fix, if possible.</li>
|
|
1574
|
-
</ul>
|
|
1575
|
-
<h1>Error Details</h1>
|
|
1576
|
-
${errorHtml || "<p>No errors found.</p>"}
|
|
1577
|
-
${stepsHtml || "<p>No step data available.</p>"}
|
|
1578
|
-
${markdownHtml ? `${markdownHtml}` : ""}
|
|
1579
|
-
</body>
|
|
1580
|
-
</html>
|
|
1581
|
-
`;
|
|
1582
|
-
import_fs3.default.writeFileSync(htmlOutputPath, fullHtml, "utf-8");
|
|
1394
|
+
const drawerHtml = `${markdownHtml || ""}`;
|
|
1395
|
+
import_fs2.default.writeFileSync(htmlOutputPath, drawerHtml.trim(), "utf-8");
|
|
1583
1396
|
if (hasMarkdown) {
|
|
1584
|
-
|
|
1397
|
+
import_fs2.default.unlinkSync(markdownPath);
|
|
1585
1398
|
}
|
|
1586
1399
|
}
|
|
1587
1400
|
|
|
1588
1401
|
// src/utils/attachFiles.ts
|
|
1589
1402
|
function attachFiles(subFolder, result, testResult, config, steps, errors) {
|
|
1590
1403
|
const folderPath = config.folderPath || "ortoni-report";
|
|
1591
|
-
const attachmentsFolder =
|
|
1404
|
+
const attachmentsFolder = import_path2.default.join(
|
|
1592
1405
|
folderPath,
|
|
1593
1406
|
"ortoni-data",
|
|
1594
1407
|
"attachments",
|
|
1595
1408
|
subFolder
|
|
1596
1409
|
);
|
|
1597
|
-
if (!
|
|
1598
|
-
|
|
1410
|
+
if (!import_fs3.default.existsSync(attachmentsFolder)) {
|
|
1411
|
+
import_fs3.default.mkdirSync(attachmentsFolder, { recursive: true });
|
|
1599
1412
|
}
|
|
1600
1413
|
if (!result.attachments) return;
|
|
1601
1414
|
const { base64Image } = config;
|
|
1602
1415
|
testResult.screenshots = [];
|
|
1416
|
+
testResult.videoPath = [];
|
|
1603
1417
|
result.attachments.forEach((attachment) => {
|
|
1604
1418
|
const { contentType, name, path: attachmentPath, body } = attachment;
|
|
1605
1419
|
if (!attachmentPath && !body) return;
|
|
1606
|
-
const fileName = attachmentPath ?
|
|
1607
|
-
const relativePath =
|
|
1420
|
+
const fileName = attachmentPath ? import_path2.default.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
|
|
1421
|
+
const relativePath = import_path2.default.join(
|
|
1608
1422
|
"ortoni-data",
|
|
1609
1423
|
"attachments",
|
|
1610
1424
|
subFolder,
|
|
1611
1425
|
fileName
|
|
1612
1426
|
);
|
|
1613
|
-
const fullPath =
|
|
1427
|
+
const fullPath = import_path2.default.join(attachmentsFolder, fileName);
|
|
1614
1428
|
if (contentType === "image/png") {
|
|
1615
1429
|
handleImage(
|
|
1616
1430
|
attachmentPath,
|
|
@@ -1653,13 +1467,13 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
|
|
|
1653
1467
|
let screenshotPath = "";
|
|
1654
1468
|
if (attachmentPath) {
|
|
1655
1469
|
try {
|
|
1656
|
-
const screenshotContent =
|
|
1470
|
+
const screenshotContent = import_fs3.default.readFileSync(
|
|
1657
1471
|
attachmentPath,
|
|
1658
1472
|
base64Image ? "base64" : void 0
|
|
1659
1473
|
);
|
|
1660
1474
|
screenshotPath = base64Image ? `data:image/png;base64,${screenshotContent}` : relativePath;
|
|
1661
1475
|
if (!base64Image) {
|
|
1662
|
-
|
|
1476
|
+
import_fs3.default.copyFileSync(attachmentPath, fullPath);
|
|
1663
1477
|
}
|
|
1664
1478
|
} catch (error) {
|
|
1665
1479
|
console.error(
|
|
@@ -1676,13 +1490,17 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
|
|
|
1676
1490
|
}
|
|
1677
1491
|
function handleAttachment(attachmentPath, fullPath, relativePath, resultKey, testResult, steps, errors) {
|
|
1678
1492
|
if (attachmentPath) {
|
|
1679
|
-
|
|
1680
|
-
|
|
1493
|
+
import_fs3.default.copyFileSync(attachmentPath, fullPath);
|
|
1494
|
+
if (resultKey === "videoPath") {
|
|
1495
|
+
testResult[resultKey]?.push(relativePath);
|
|
1496
|
+
} else if (resultKey === "tracePath") {
|
|
1497
|
+
testResult[resultKey] = relativePath;
|
|
1498
|
+
}
|
|
1681
1499
|
}
|
|
1682
1500
|
if (resultKey === "markdownPath" && errors) {
|
|
1683
1501
|
const htmlPath = fullPath.replace(/\.md$/, ".html");
|
|
1684
1502
|
const htmlRelativePath = relativePath.replace(/\.md$/, ".html");
|
|
1685
|
-
convertMarkdownToHtml(fullPath, htmlPath
|
|
1503
|
+
convertMarkdownToHtml(fullPath, htmlPath);
|
|
1686
1504
|
testResult[resultKey] = htmlRelativePath;
|
|
1687
1505
|
return;
|
|
1688
1506
|
}
|
|
@@ -1697,6 +1515,61 @@ function getFileExtension(contentType) {
|
|
|
1697
1515
|
return extensions[contentType] || "unknown";
|
|
1698
1516
|
}
|
|
1699
1517
|
|
|
1518
|
+
// src/utils/utils.ts
|
|
1519
|
+
var import_path3 = __toESM(require("path"));
|
|
1520
|
+
function normalizeFilePath(filePath) {
|
|
1521
|
+
const normalizedPath = import_path3.default.normalize(filePath);
|
|
1522
|
+
return import_path3.default.basename(normalizedPath);
|
|
1523
|
+
}
|
|
1524
|
+
function ensureHtmlExtension(filename) {
|
|
1525
|
+
const ext = import_path3.default.extname(filename);
|
|
1526
|
+
if (ext && ext.toLowerCase() === ".html") {
|
|
1527
|
+
return filename;
|
|
1528
|
+
}
|
|
1529
|
+
return `${filename}.html`;
|
|
1530
|
+
}
|
|
1531
|
+
function escapeHtml(unsafe) {
|
|
1532
|
+
if (typeof unsafe !== "string") {
|
|
1533
|
+
return String(unsafe);
|
|
1534
|
+
}
|
|
1535
|
+
return unsafe.replace(/[&<"']/g, function(match) {
|
|
1536
|
+
const escapeMap = {
|
|
1537
|
+
"&": "&",
|
|
1538
|
+
"<": "<",
|
|
1539
|
+
">": ">",
|
|
1540
|
+
'"': """,
|
|
1541
|
+
"'": "'"
|
|
1542
|
+
};
|
|
1543
|
+
return escapeMap[match] || match;
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
function formatDateLocal(dateInput) {
|
|
1547
|
+
const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
|
|
1548
|
+
const options = {
|
|
1549
|
+
year: "numeric",
|
|
1550
|
+
month: "short",
|
|
1551
|
+
day: "2-digit",
|
|
1552
|
+
hour: "2-digit",
|
|
1553
|
+
minute: "2-digit",
|
|
1554
|
+
hour12: true,
|
|
1555
|
+
timeZoneName: "short"
|
|
1556
|
+
// or "Asia/Kolkata"
|
|
1557
|
+
};
|
|
1558
|
+
return new Intl.DateTimeFormat(void 0, options).format(date);
|
|
1559
|
+
}
|
|
1560
|
+
function extractSuites(titlePath) {
|
|
1561
|
+
const tagPattern = /@[\w]+/g;
|
|
1562
|
+
const suiteParts = titlePath.slice(3, titlePath.length - 1).map((p) => p.replace(tagPattern, "").trim());
|
|
1563
|
+
return {
|
|
1564
|
+
hierarchy: suiteParts.join(" > "),
|
|
1565
|
+
// full hierarchy
|
|
1566
|
+
topLevelSuite: suiteParts[0] ?? "",
|
|
1567
|
+
// first suite
|
|
1568
|
+
parentSuite: suiteParts[suiteParts.length - 1] ?? ""
|
|
1569
|
+
// last suite
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1700
1573
|
// src/helpers/resultProcessor .ts
|
|
1701
1574
|
var TestResultProcessor = class {
|
|
1702
1575
|
constructor(projectRoot) {
|
|
@@ -1712,19 +1585,21 @@ var TestResultProcessor = class {
|
|
|
1712
1585
|
const tagPattern = /@[\w]+/g;
|
|
1713
1586
|
const title = test.title.replace(tagPattern, "").trim();
|
|
1714
1587
|
const suite = test.titlePath()[3].replace(tagPattern, "").trim();
|
|
1588
|
+
const suiteAndTitle = extractSuites(test.titlePath());
|
|
1589
|
+
const suiteHierarchy = suiteAndTitle.hierarchy;
|
|
1715
1590
|
const testResult = {
|
|
1716
|
-
|
|
1591
|
+
suiteHierarchy,
|
|
1592
|
+
key: test.id,
|
|
1717
1593
|
annotations: test.annotations,
|
|
1718
1594
|
testTags: test.tags,
|
|
1719
1595
|
location: `${filePath}:${location.line}:${location.column}`,
|
|
1720
|
-
|
|
1721
|
-
isRetry: result.retry,
|
|
1596
|
+
retryAttemptCount: result.retry,
|
|
1722
1597
|
projectName,
|
|
1723
1598
|
suite,
|
|
1724
1599
|
title,
|
|
1725
1600
|
status,
|
|
1726
1601
|
flaky: test.outcome(),
|
|
1727
|
-
duration:
|
|
1602
|
+
duration: result.duration,
|
|
1728
1603
|
errors: result.errors.map(
|
|
1729
1604
|
(e) => this.ansiToHtml.toHtml(escapeHtml(e.stack || e.toString()))
|
|
1730
1605
|
),
|
|
@@ -1736,7 +1611,8 @@ var TestResultProcessor = class {
|
|
|
1736
1611
|
),
|
|
1737
1612
|
filePath,
|
|
1738
1613
|
filters: projectSet,
|
|
1739
|
-
base64Image: ortoniConfig.base64Image
|
|
1614
|
+
base64Image: ortoniConfig.base64Image,
|
|
1615
|
+
testId: `${filePath}:${projectName}:${title}`
|
|
1740
1616
|
};
|
|
1741
1617
|
attachFiles(
|
|
1742
1618
|
test.id,
|
|
@@ -1750,7 +1626,7 @@ var TestResultProcessor = class {
|
|
|
1750
1626
|
}
|
|
1751
1627
|
processSteps(steps) {
|
|
1752
1628
|
return steps.map((step) => {
|
|
1753
|
-
const stepLocation = step.location ? `${
|
|
1629
|
+
const stepLocation = step.location ? `${import_path4.default.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
|
|
1754
1630
|
return {
|
|
1755
1631
|
snippet: this.ansiToHtml.toHtml(escapeHtml(step.error?.snippet || "")),
|
|
1756
1632
|
title: step.title,
|
|
@@ -1762,14 +1638,14 @@ var TestResultProcessor = class {
|
|
|
1762
1638
|
|
|
1763
1639
|
// src/utils/expressServer.ts
|
|
1764
1640
|
var import_express = __toESM(require("express"));
|
|
1765
|
-
var
|
|
1641
|
+
var import_path5 = __toESM(require("path"));
|
|
1766
1642
|
var import_child_process = require("child_process");
|
|
1767
1643
|
function startReportServer(reportFolder, reportFilename, port = 2004, open2) {
|
|
1768
1644
|
const app = (0, import_express.default)();
|
|
1769
1645
|
app.use(import_express.default.static(reportFolder));
|
|
1770
1646
|
app.get("/", (_req, res) => {
|
|
1771
1647
|
try {
|
|
1772
|
-
res.sendFile(
|
|
1648
|
+
res.sendFile(import_path5.default.resolve(reportFolder, reportFilename));
|
|
1773
1649
|
} catch (error) {
|
|
1774
1650
|
console.error("Ortoni-Report: Error sending report file:", error);
|
|
1775
1651
|
res.status(500).send("Error loading report");
|
|
@@ -1876,7 +1752,7 @@ var DatabaseManager = class {
|
|
|
1876
1752
|
run_id INTEGER,
|
|
1877
1753
|
test_id TEXT,
|
|
1878
1754
|
status TEXT,
|
|
1879
|
-
duration
|
|
1755
|
+
duration INTEGER, -- store duration as raw ms
|
|
1880
1756
|
error_message TEXT,
|
|
1881
1757
|
FOREIGN KEY (run_id) REFERENCES test_runs (id)
|
|
1882
1758
|
);
|
|
@@ -1936,6 +1812,7 @@ var DatabaseManager = class {
|
|
|
1936
1812
|
`${result.filePath}:${result.projectName}:${result.title}`,
|
|
1937
1813
|
result.status,
|
|
1938
1814
|
result.duration,
|
|
1815
|
+
// store raw ms
|
|
1939
1816
|
result.errors.join("\n")
|
|
1940
1817
|
]);
|
|
1941
1818
|
}
|
|
@@ -2002,7 +1879,7 @@ var DatabaseManager = class {
|
|
|
2002
1879
|
(SELECT COUNT(*) FROM test_results) as totalTests,
|
|
2003
1880
|
(SELECT COUNT(*) FROM test_results WHERE status = 'passed') as passed,
|
|
2004
1881
|
(SELECT COUNT(*) FROM test_results WHERE status = 'failed') as failed,
|
|
2005
|
-
(SELECT AVG(
|
|
1882
|
+
(SELECT AVG(duration) FROM test_results) as avgDuration
|
|
2006
1883
|
`);
|
|
2007
1884
|
const passRate = summary.totalTests ? (summary.passed / summary.totalTests * 100).toFixed(2) : 0;
|
|
2008
1885
|
return {
|
|
@@ -2012,6 +1889,7 @@ var DatabaseManager = class {
|
|
|
2012
1889
|
failed: summary.failed,
|
|
2013
1890
|
passRate: parseFloat(passRate.toString()),
|
|
2014
1891
|
avgDuration: Math.round(summary.avgDuration || 0)
|
|
1892
|
+
// raw ms avg
|
|
2015
1893
|
};
|
|
2016
1894
|
} catch (error) {
|
|
2017
1895
|
console.error("OrtoniReport: Error getting summary data:", error);
|
|
@@ -2036,7 +1914,7 @@ var DatabaseManager = class {
|
|
|
2036
1914
|
SELECT trun.run_date,
|
|
2037
1915
|
SUM(CASE WHEN tr.status = 'passed' THEN 1 ELSE 0 END) AS passed,
|
|
2038
1916
|
SUM(CASE WHEN tr.status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
|
2039
|
-
AVG(
|
|
1917
|
+
AVG(tr.duration) AS avg_duration
|
|
2040
1918
|
FROM test_results tr
|
|
2041
1919
|
JOIN test_runs trun ON tr.run_id = trun.id
|
|
2042
1920
|
GROUP BY trun.run_date
|
|
@@ -2049,6 +1927,7 @@ var DatabaseManager = class {
|
|
|
2049
1927
|
...row,
|
|
2050
1928
|
run_date: formatDateLocal(row.run_date),
|
|
2051
1929
|
avg_duration: Math.round(row.avg_duration || 0)
|
|
1930
|
+
// raw ms avg
|
|
2052
1931
|
}));
|
|
2053
1932
|
} catch (error) {
|
|
2054
1933
|
console.error("OrtoniReport: Error getting trends data:", error);
|
|
@@ -2066,7 +1945,8 @@ var DatabaseManager = class {
|
|
|
2066
1945
|
SELECT
|
|
2067
1946
|
test_id,
|
|
2068
1947
|
COUNT(*) AS total,
|
|
2069
|
-
SUM(CASE WHEN status = 'flaky' THEN 1 ELSE 0 END) AS flaky
|
|
1948
|
+
SUM(CASE WHEN status = 'flaky' THEN 1 ELSE 0 END) AS flaky,
|
|
1949
|
+
AVG(duration) AS avg_duration
|
|
2070
1950
|
FROM test_results
|
|
2071
1951
|
GROUP BY test_id
|
|
2072
1952
|
HAVING flaky > 0
|
|
@@ -2090,7 +1970,7 @@ var DatabaseManager = class {
|
|
|
2090
1970
|
`
|
|
2091
1971
|
SELECT
|
|
2092
1972
|
test_id,
|
|
2093
|
-
AVG(
|
|
1973
|
+
AVG(duration) AS avg_duration
|
|
2094
1974
|
FROM test_results
|
|
2095
1975
|
GROUP BY test_id
|
|
2096
1976
|
ORDER BY avg_duration DESC
|
|
@@ -2101,6 +1981,7 @@ var DatabaseManager = class {
|
|
|
2101
1981
|
return rows.map((row) => ({
|
|
2102
1982
|
test_id: row.test_id,
|
|
2103
1983
|
avg_duration: Math.round(row.avg_duration || 0)
|
|
1984
|
+
// raw ms avg
|
|
2104
1985
|
}));
|
|
2105
1986
|
} catch (error) {
|
|
2106
1987
|
console.error("OrtoniReport: Error getting slow tests:", error);
|
|
@@ -2110,7 +1991,7 @@ var DatabaseManager = class {
|
|
|
2110
1991
|
};
|
|
2111
1992
|
|
|
2112
1993
|
// src/ortoni-report.ts
|
|
2113
|
-
var
|
|
1994
|
+
var import_path6 = __toESM(require("path"));
|
|
2114
1995
|
var OrtoniReport = class {
|
|
2115
1996
|
constructor(ortoniConfig = {}) {
|
|
2116
1997
|
this.ortoniConfig = ortoniConfig;
|
|
@@ -2141,7 +2022,7 @@ var OrtoniReport = class {
|
|
|
2141
2022
|
this.testResultProcessor = new TestResultProcessor(config.rootDir);
|
|
2142
2023
|
this.fileManager.ensureReportDirectory();
|
|
2143
2024
|
await this.dbManager.initialize(
|
|
2144
|
-
|
|
2025
|
+
import_path6.default.join(this.folderPath, "ortoni-data-history.sqlite")
|
|
2145
2026
|
);
|
|
2146
2027
|
}
|
|
2147
2028
|
onStdOut(chunk, _test, _result) {
|
|
@@ -2175,23 +2056,21 @@ var OrtoniReport = class {
|
|
|
2175
2056
|
this.overAllStatus = result.status;
|
|
2176
2057
|
if (this.shouldGenerateReport) {
|
|
2177
2058
|
const filteredResults = this.results.filter(
|
|
2178
|
-
(r) => r.status !== "skipped"
|
|
2059
|
+
(r) => r.status !== "skipped"
|
|
2179
2060
|
);
|
|
2180
|
-
const totalDuration =
|
|
2181
|
-
const cssContent = this.fileManager.readCssContent();
|
|
2061
|
+
const totalDuration = result.duration;
|
|
2182
2062
|
const runId = await this.dbManager.saveTestRun();
|
|
2183
2063
|
if (runId !== null) {
|
|
2184
2064
|
await this.dbManager.saveTestResults(runId, this.results);
|
|
2185
|
-
const
|
|
2065
|
+
const finalReportData = await this.htmlGenerator.generateFinalReport(
|
|
2186
2066
|
filteredResults,
|
|
2187
2067
|
totalDuration,
|
|
2188
|
-
cssContent,
|
|
2189
2068
|
this.results,
|
|
2190
2069
|
this.projectSet
|
|
2191
2070
|
);
|
|
2192
2071
|
this.outputPath = this.fileManager.writeReportFile(
|
|
2193
2072
|
this.outputFilename,
|
|
2194
|
-
|
|
2073
|
+
finalReportData
|
|
2195
2074
|
);
|
|
2196
2075
|
} else {
|
|
2197
2076
|
console.error("OrtoniReport: Error saving test run to database");
|