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.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: "shortOffset"
|
|
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,108 +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
|
-
#copyBtn {
|
|
1567
|
-
background-color: #007acc;
|
|
1568
|
-
color: white;
|
|
1569
|
-
border: none;
|
|
1570
|
-
padding: 0.5rem 1rem;
|
|
1571
|
-
margin-bottom: 1rem;
|
|
1572
|
-
border-radius: 5px;
|
|
1573
|
-
cursor: pointer;
|
|
1574
|
-
}
|
|
1575
|
-
#copyBtn:hover {
|
|
1576
|
-
background-color: #005fa3;
|
|
1577
|
-
}
|
|
1578
|
-
</style>
|
|
1579
|
-
</head>
|
|
1580
|
-
<body>
|
|
1581
|
-
<button id="copyBtn">\u{1F4CB} Copy All</button>
|
|
1582
|
-
<script>
|
|
1583
|
-
document.getElementById("copyBtn").addEventListener("click", () => {
|
|
1584
|
-
const content = document.getElementById("markdownContent").innerText;
|
|
1585
|
-
navigator.clipboard.writeText(content).then(() => {
|
|
1586
|
-
// change button text to indicate success
|
|
1587
|
-
const button = document.getElementById("copyBtn");
|
|
1588
|
-
button.textContent = "\u2705 Copied!";
|
|
1589
|
-
setTimeout(() => {
|
|
1590
|
-
button.textContent = "\u{1F4CB} Copy All"
|
|
1591
|
-
}, 2000);
|
|
1592
|
-
}).catch(err => {
|
|
1593
|
-
console.error("Failed to copy text: ", err);
|
|
1594
|
-
alert("Failed to copy text. Please try manually.");
|
|
1595
|
-
});
|
|
1596
|
-
});
|
|
1597
|
-
</script>
|
|
1598
|
-
<div id="markdownContent">
|
|
1599
|
-
<h1>Instructions</h1>
|
|
1600
|
-
<ul>
|
|
1601
|
-
<li>Following Playwright test failed.</li>
|
|
1602
|
-
<li>Explain why, be concise, respect Playwright best practices.</li>
|
|
1603
|
-
<li>Provide a snippet of code with the fix, if possible.</li>
|
|
1604
|
-
</ul>
|
|
1605
|
-
<h1>Error Details</h1>
|
|
1606
|
-
${errorHtml || "<p>No errors found.</p>"}
|
|
1607
|
-
${stepsHtml || "<p>No step data available.</p>"}
|
|
1608
|
-
${markdownHtml || ""}
|
|
1609
|
-
</div>
|
|
1610
|
-
</body>
|
|
1611
|
-
</html>
|
|
1612
|
-
`;
|
|
1613
|
-
import_fs3.default.writeFileSync(htmlOutputPath, fullHtml, "utf-8");
|
|
1394
|
+
const drawerHtml = `${markdownHtml || ""}`;
|
|
1395
|
+
import_fs2.default.writeFileSync(htmlOutputPath, drawerHtml.trim(), "utf-8");
|
|
1614
1396
|
if (hasMarkdown) {
|
|
1615
|
-
|
|
1397
|
+
import_fs2.default.unlinkSync(markdownPath);
|
|
1616
1398
|
}
|
|
1617
1399
|
}
|
|
1618
1400
|
|
|
1619
1401
|
// src/utils/attachFiles.ts
|
|
1620
1402
|
function attachFiles(subFolder, result, testResult, config, steps, errors) {
|
|
1621
1403
|
const folderPath = config.folderPath || "ortoni-report";
|
|
1622
|
-
const attachmentsFolder =
|
|
1404
|
+
const attachmentsFolder = import_path2.default.join(
|
|
1623
1405
|
folderPath,
|
|
1624
1406
|
"ortoni-data",
|
|
1625
1407
|
"attachments",
|
|
1626
1408
|
subFolder
|
|
1627
1409
|
);
|
|
1628
|
-
if (!
|
|
1629
|
-
|
|
1410
|
+
if (!import_fs3.default.existsSync(attachmentsFolder)) {
|
|
1411
|
+
import_fs3.default.mkdirSync(attachmentsFolder, { recursive: true });
|
|
1630
1412
|
}
|
|
1631
1413
|
if (!result.attachments) return;
|
|
1632
1414
|
const { base64Image } = config;
|
|
1633
1415
|
testResult.screenshots = [];
|
|
1416
|
+
testResult.videoPath = [];
|
|
1634
1417
|
result.attachments.forEach((attachment) => {
|
|
1635
1418
|
const { contentType, name, path: attachmentPath, body } = attachment;
|
|
1636
1419
|
if (!attachmentPath && !body) return;
|
|
1637
|
-
const fileName = attachmentPath ?
|
|
1638
|
-
const relativePath =
|
|
1420
|
+
const fileName = attachmentPath ? import_path2.default.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
|
|
1421
|
+
const relativePath = import_path2.default.join(
|
|
1639
1422
|
"ortoni-data",
|
|
1640
1423
|
"attachments",
|
|
1641
1424
|
subFolder,
|
|
1642
1425
|
fileName
|
|
1643
1426
|
);
|
|
1644
|
-
const fullPath =
|
|
1427
|
+
const fullPath = import_path2.default.join(attachmentsFolder, fileName);
|
|
1645
1428
|
if (contentType === "image/png") {
|
|
1646
1429
|
handleImage(
|
|
1647
1430
|
attachmentPath,
|
|
@@ -1684,13 +1467,13 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
|
|
|
1684
1467
|
let screenshotPath = "";
|
|
1685
1468
|
if (attachmentPath) {
|
|
1686
1469
|
try {
|
|
1687
|
-
const screenshotContent =
|
|
1470
|
+
const screenshotContent = import_fs3.default.readFileSync(
|
|
1688
1471
|
attachmentPath,
|
|
1689
1472
|
base64Image ? "base64" : void 0
|
|
1690
1473
|
);
|
|
1691
1474
|
screenshotPath = base64Image ? `data:image/png;base64,${screenshotContent}` : relativePath;
|
|
1692
1475
|
if (!base64Image) {
|
|
1693
|
-
|
|
1476
|
+
import_fs3.default.copyFileSync(attachmentPath, fullPath);
|
|
1694
1477
|
}
|
|
1695
1478
|
} catch (error) {
|
|
1696
1479
|
console.error(
|
|
@@ -1707,13 +1490,17 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
|
|
|
1707
1490
|
}
|
|
1708
1491
|
function handleAttachment(attachmentPath, fullPath, relativePath, resultKey, testResult, steps, errors) {
|
|
1709
1492
|
if (attachmentPath) {
|
|
1710
|
-
|
|
1711
|
-
|
|
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
|
+
}
|
|
1712
1499
|
}
|
|
1713
1500
|
if (resultKey === "markdownPath" && errors) {
|
|
1714
1501
|
const htmlPath = fullPath.replace(/\.md$/, ".html");
|
|
1715
1502
|
const htmlRelativePath = relativePath.replace(/\.md$/, ".html");
|
|
1716
|
-
convertMarkdownToHtml(fullPath, htmlPath
|
|
1503
|
+
convertMarkdownToHtml(fullPath, htmlPath);
|
|
1717
1504
|
testResult[resultKey] = htmlRelativePath;
|
|
1718
1505
|
return;
|
|
1719
1506
|
}
|
|
@@ -1728,6 +1515,61 @@ function getFileExtension(contentType) {
|
|
|
1728
1515
|
return extensions[contentType] || "unknown";
|
|
1729
1516
|
}
|
|
1730
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
|
+
|
|
1731
1573
|
// src/helpers/resultProcessor .ts
|
|
1732
1574
|
var TestResultProcessor = class {
|
|
1733
1575
|
constructor(projectRoot) {
|
|
@@ -1743,19 +1585,21 @@ var TestResultProcessor = class {
|
|
|
1743
1585
|
const tagPattern = /@[\w]+/g;
|
|
1744
1586
|
const title = test.title.replace(tagPattern, "").trim();
|
|
1745
1587
|
const suite = test.titlePath()[3].replace(tagPattern, "").trim();
|
|
1588
|
+
const suiteAndTitle = extractSuites(test.titlePath());
|
|
1589
|
+
const suiteHierarchy = suiteAndTitle.hierarchy;
|
|
1746
1590
|
const testResult = {
|
|
1747
|
-
|
|
1591
|
+
suiteHierarchy,
|
|
1592
|
+
key: test.id,
|
|
1748
1593
|
annotations: test.annotations,
|
|
1749
1594
|
testTags: test.tags,
|
|
1750
1595
|
location: `${filePath}:${location.line}:${location.column}`,
|
|
1751
|
-
|
|
1752
|
-
isRetry: result.retry,
|
|
1596
|
+
retryAttemptCount: result.retry,
|
|
1753
1597
|
projectName,
|
|
1754
1598
|
suite,
|
|
1755
1599
|
title,
|
|
1756
1600
|
status,
|
|
1757
1601
|
flaky: test.outcome(),
|
|
1758
|
-
duration:
|
|
1602
|
+
duration: result.duration,
|
|
1759
1603
|
errors: result.errors.map(
|
|
1760
1604
|
(e) => this.ansiToHtml.toHtml(escapeHtml(e.stack || e.toString()))
|
|
1761
1605
|
),
|
|
@@ -1767,7 +1611,8 @@ var TestResultProcessor = class {
|
|
|
1767
1611
|
),
|
|
1768
1612
|
filePath,
|
|
1769
1613
|
filters: projectSet,
|
|
1770
|
-
base64Image: ortoniConfig.base64Image
|
|
1614
|
+
base64Image: ortoniConfig.base64Image,
|
|
1615
|
+
testId: `${filePath}:${projectName}:${title}`
|
|
1771
1616
|
};
|
|
1772
1617
|
attachFiles(
|
|
1773
1618
|
test.id,
|
|
@@ -1781,7 +1626,7 @@ var TestResultProcessor = class {
|
|
|
1781
1626
|
}
|
|
1782
1627
|
processSteps(steps) {
|
|
1783
1628
|
return steps.map((step) => {
|
|
1784
|
-
const stepLocation = step.location ? `${
|
|
1629
|
+
const stepLocation = step.location ? `${import_path4.default.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
|
|
1785
1630
|
return {
|
|
1786
1631
|
snippet: this.ansiToHtml.toHtml(escapeHtml(step.error?.snippet || "")),
|
|
1787
1632
|
title: step.title,
|
|
@@ -1793,14 +1638,14 @@ var TestResultProcessor = class {
|
|
|
1793
1638
|
|
|
1794
1639
|
// src/utils/expressServer.ts
|
|
1795
1640
|
var import_express = __toESM(require("express"));
|
|
1796
|
-
var
|
|
1641
|
+
var import_path5 = __toESM(require("path"));
|
|
1797
1642
|
var import_child_process = require("child_process");
|
|
1798
1643
|
function startReportServer(reportFolder, reportFilename, port = 2004, open2) {
|
|
1799
1644
|
const app = (0, import_express.default)();
|
|
1800
1645
|
app.use(import_express.default.static(reportFolder));
|
|
1801
1646
|
app.get("/", (_req, res) => {
|
|
1802
1647
|
try {
|
|
1803
|
-
res.sendFile(
|
|
1648
|
+
res.sendFile(import_path5.default.resolve(reportFolder, reportFilename));
|
|
1804
1649
|
} catch (error) {
|
|
1805
1650
|
console.error("Ortoni-Report: Error sending report file:", error);
|
|
1806
1651
|
res.status(500).send("Error loading report");
|
|
@@ -1907,7 +1752,7 @@ var DatabaseManager = class {
|
|
|
1907
1752
|
run_id INTEGER,
|
|
1908
1753
|
test_id TEXT,
|
|
1909
1754
|
status TEXT,
|
|
1910
|
-
duration
|
|
1755
|
+
duration INTEGER, -- store duration as raw ms
|
|
1911
1756
|
error_message TEXT,
|
|
1912
1757
|
FOREIGN KEY (run_id) REFERENCES test_runs (id)
|
|
1913
1758
|
);
|
|
@@ -1967,6 +1812,7 @@ var DatabaseManager = class {
|
|
|
1967
1812
|
`${result.filePath}:${result.projectName}:${result.title}`,
|
|
1968
1813
|
result.status,
|
|
1969
1814
|
result.duration,
|
|
1815
|
+
// store raw ms
|
|
1970
1816
|
result.errors.join("\n")
|
|
1971
1817
|
]);
|
|
1972
1818
|
}
|
|
@@ -2033,7 +1879,7 @@ var DatabaseManager = class {
|
|
|
2033
1879
|
(SELECT COUNT(*) FROM test_results) as totalTests,
|
|
2034
1880
|
(SELECT COUNT(*) FROM test_results WHERE status = 'passed') as passed,
|
|
2035
1881
|
(SELECT COUNT(*) FROM test_results WHERE status = 'failed') as failed,
|
|
2036
|
-
(SELECT AVG(
|
|
1882
|
+
(SELECT AVG(duration) FROM test_results) as avgDuration
|
|
2037
1883
|
`);
|
|
2038
1884
|
const passRate = summary.totalTests ? (summary.passed / summary.totalTests * 100).toFixed(2) : 0;
|
|
2039
1885
|
return {
|
|
@@ -2043,6 +1889,7 @@ var DatabaseManager = class {
|
|
|
2043
1889
|
failed: summary.failed,
|
|
2044
1890
|
passRate: parseFloat(passRate.toString()),
|
|
2045
1891
|
avgDuration: Math.round(summary.avgDuration || 0)
|
|
1892
|
+
// raw ms avg
|
|
2046
1893
|
};
|
|
2047
1894
|
} catch (error) {
|
|
2048
1895
|
console.error("OrtoniReport: Error getting summary data:", error);
|
|
@@ -2067,7 +1914,7 @@ var DatabaseManager = class {
|
|
|
2067
1914
|
SELECT trun.run_date,
|
|
2068
1915
|
SUM(CASE WHEN tr.status = 'passed' THEN 1 ELSE 0 END) AS passed,
|
|
2069
1916
|
SUM(CASE WHEN tr.status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
|
2070
|
-
AVG(
|
|
1917
|
+
AVG(tr.duration) AS avg_duration
|
|
2071
1918
|
FROM test_results tr
|
|
2072
1919
|
JOIN test_runs trun ON tr.run_id = trun.id
|
|
2073
1920
|
GROUP BY trun.run_date
|
|
@@ -2080,6 +1927,7 @@ var DatabaseManager = class {
|
|
|
2080
1927
|
...row,
|
|
2081
1928
|
run_date: formatDateLocal(row.run_date),
|
|
2082
1929
|
avg_duration: Math.round(row.avg_duration || 0)
|
|
1930
|
+
// raw ms avg
|
|
2083
1931
|
}));
|
|
2084
1932
|
} catch (error) {
|
|
2085
1933
|
console.error("OrtoniReport: Error getting trends data:", error);
|
|
@@ -2097,7 +1945,8 @@ var DatabaseManager = class {
|
|
|
2097
1945
|
SELECT
|
|
2098
1946
|
test_id,
|
|
2099
1947
|
COUNT(*) AS total,
|
|
2100
|
-
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
|
|
2101
1950
|
FROM test_results
|
|
2102
1951
|
GROUP BY test_id
|
|
2103
1952
|
HAVING flaky > 0
|
|
@@ -2121,7 +1970,7 @@ var DatabaseManager = class {
|
|
|
2121
1970
|
`
|
|
2122
1971
|
SELECT
|
|
2123
1972
|
test_id,
|
|
2124
|
-
AVG(
|
|
1973
|
+
AVG(duration) AS avg_duration
|
|
2125
1974
|
FROM test_results
|
|
2126
1975
|
GROUP BY test_id
|
|
2127
1976
|
ORDER BY avg_duration DESC
|
|
@@ -2132,6 +1981,7 @@ var DatabaseManager = class {
|
|
|
2132
1981
|
return rows.map((row) => ({
|
|
2133
1982
|
test_id: row.test_id,
|
|
2134
1983
|
avg_duration: Math.round(row.avg_duration || 0)
|
|
1984
|
+
// raw ms avg
|
|
2135
1985
|
}));
|
|
2136
1986
|
} catch (error) {
|
|
2137
1987
|
console.error("OrtoniReport: Error getting slow tests:", error);
|
|
@@ -2141,7 +1991,7 @@ var DatabaseManager = class {
|
|
|
2141
1991
|
};
|
|
2142
1992
|
|
|
2143
1993
|
// src/ortoni-report.ts
|
|
2144
|
-
var
|
|
1994
|
+
var import_path6 = __toESM(require("path"));
|
|
2145
1995
|
var OrtoniReport = class {
|
|
2146
1996
|
constructor(ortoniConfig = {}) {
|
|
2147
1997
|
this.ortoniConfig = ortoniConfig;
|
|
@@ -2172,7 +2022,7 @@ var OrtoniReport = class {
|
|
|
2172
2022
|
this.testResultProcessor = new TestResultProcessor(config.rootDir);
|
|
2173
2023
|
this.fileManager.ensureReportDirectory();
|
|
2174
2024
|
await this.dbManager.initialize(
|
|
2175
|
-
|
|
2025
|
+
import_path6.default.join(this.folderPath, "ortoni-data-history.sqlite")
|
|
2176
2026
|
);
|
|
2177
2027
|
}
|
|
2178
2028
|
onStdOut(chunk, _test, _result) {
|
|
@@ -2206,23 +2056,21 @@ var OrtoniReport = class {
|
|
|
2206
2056
|
this.overAllStatus = result.status;
|
|
2207
2057
|
if (this.shouldGenerateReport) {
|
|
2208
2058
|
const filteredResults = this.results.filter(
|
|
2209
|
-
(r) => r.status !== "skipped"
|
|
2059
|
+
(r) => r.status !== "skipped"
|
|
2210
2060
|
);
|
|
2211
|
-
const totalDuration =
|
|
2212
|
-
const cssContent = this.fileManager.readCssContent();
|
|
2061
|
+
const totalDuration = result.duration;
|
|
2213
2062
|
const runId = await this.dbManager.saveTestRun();
|
|
2214
2063
|
if (runId !== null) {
|
|
2215
2064
|
await this.dbManager.saveTestResults(runId, this.results);
|
|
2216
|
-
const
|
|
2065
|
+
const finalReportData = await this.htmlGenerator.generateFinalReport(
|
|
2217
2066
|
filteredResults,
|
|
2218
2067
|
totalDuration,
|
|
2219
|
-
cssContent,
|
|
2220
2068
|
this.results,
|
|
2221
2069
|
this.projectSet
|
|
2222
2070
|
);
|
|
2223
2071
|
this.outputPath = this.fileManager.writeReportFile(
|
|
2224
2072
|
this.outputFilename,
|
|
2225
|
-
|
|
2073
|
+
finalReportData
|
|
2226
2074
|
);
|
|
2227
2075
|
} else {
|
|
2228
2076
|
console.error("OrtoniReport: Error saving test run to database");
|