ortoni-report 4.0.0 → 4.0.1-beta.3
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/dist/chunk-4RZ5C7KY.mjs +744 -0
- package/dist/chunk-HOOC3MDY.mjs +632 -0
- package/dist/chunk-ISCRDMPY.mjs +693 -0
- package/dist/chunk-P72FKFLZ.mjs +692 -0
- package/dist/chunk-S45BZGXX.mjs +744 -0
- package/dist/chunk-ZG4JPYLC.mjs +692 -0
- package/dist/chunk-ZSPTPISU.mjs +692 -0
- package/dist/cli.js +911 -0
- package/dist/cli.mjs +206 -0
- package/dist/index.html +2 -2
- package/dist/ortoni-report.d.mts +8 -0
- package/dist/ortoni-report.d.ts +8 -0
- package/dist/ortoni-report.js +226 -82
- package/dist/ortoni-report.mjs +64 -576
- package/package.json +11 -4
- package/dist/chunk-A6HCKATU.mjs +0 -76
- package/dist/cli/cli.js +0 -106
- package/dist/cli/cli.mjs +0 -23
- /package/dist/{cli/cli.d.mts → cli.d.mts} +0 -0
- /package/dist/{cli/cli.d.ts → cli.d.ts} +0 -0
package/dist/ortoni-report.mjs
CHANGED
|
@@ -1,235 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DatabaseManager,
|
|
3
|
+
FileManager,
|
|
4
|
+
HTMLGenerator,
|
|
2
5
|
__publicField,
|
|
3
|
-
|
|
6
|
+
ensureHtmlExtension,
|
|
7
|
+
escapeHtml,
|
|
8
|
+
extractSuites,
|
|
9
|
+
normalizeFilePath,
|
|
4
10
|
startReportServer
|
|
5
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-4RZ5C7KY.mjs";
|
|
6
12
|
|
|
7
|
-
// src/helpers/
|
|
8
|
-
import fs from "fs";
|
|
9
|
-
import path from "path";
|
|
10
|
-
var FileManager = class {
|
|
11
|
-
constructor(folderPath) {
|
|
12
|
-
this.folderPath = folderPath;
|
|
13
|
-
}
|
|
14
|
-
ensureReportDirectory() {
|
|
15
|
-
const ortoniDataFolder = path.join(this.folderPath, "ortoni-data");
|
|
16
|
-
if (!fs.existsSync(this.folderPath)) {
|
|
17
|
-
fs.mkdirSync(this.folderPath, { recursive: true });
|
|
18
|
-
} else {
|
|
19
|
-
if (fs.existsSync(ortoniDataFolder)) {
|
|
20
|
-
fs.rmSync(ortoniDataFolder, { recursive: true, force: true });
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
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);
|
|
31
|
-
const outputPath = path.join(process.cwd(), this.folderPath, filename);
|
|
32
|
-
fs.writeFileSync(outputPath, html);
|
|
33
|
-
return outputPath;
|
|
34
|
-
}
|
|
35
|
-
copyTraceViewerAssets(skip) {
|
|
36
|
-
if (skip) return;
|
|
37
|
-
const traceViewerFolder = path.join(
|
|
38
|
-
__require.resolve("playwright-core"),
|
|
39
|
-
"..",
|
|
40
|
-
"lib",
|
|
41
|
-
"vite",
|
|
42
|
-
"traceViewer"
|
|
43
|
-
);
|
|
44
|
-
const traceViewerTargetFolder = path.join(this.folderPath, "trace");
|
|
45
|
-
const traceViewerAssetsTargetFolder = path.join(
|
|
46
|
-
traceViewerTargetFolder,
|
|
47
|
-
"assets"
|
|
48
|
-
);
|
|
49
|
-
fs.mkdirSync(traceViewerAssetsTargetFolder, { recursive: true });
|
|
50
|
-
for (const file of fs.readdirSync(traceViewerFolder)) {
|
|
51
|
-
if (file.endsWith(".map") || file.includes("watch") || file.includes("assets"))
|
|
52
|
-
continue;
|
|
53
|
-
fs.copyFileSync(
|
|
54
|
-
path.join(traceViewerFolder, file),
|
|
55
|
-
path.join(traceViewerTargetFolder, file)
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
const assetsFolder = path.join(traceViewerFolder, "assets");
|
|
59
|
-
for (const file of fs.readdirSync(assetsFolder)) {
|
|
60
|
-
if (file.endsWith(".map") || file.includes("xtermModule")) continue;
|
|
61
|
-
fs.copyFileSync(
|
|
62
|
-
path.join(assetsFolder, file),
|
|
63
|
-
path.join(traceViewerAssetsTargetFolder, file)
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// src/utils/groupProjects.ts
|
|
70
|
-
function groupResults(config, results) {
|
|
71
|
-
if (config.showProject) {
|
|
72
|
-
const groupedResults = results.reduce((acc, result, index) => {
|
|
73
|
-
const testId = `${result.filePath}:${result.projectName}:${result.title}`;
|
|
74
|
-
const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
|
|
75
|
-
const { filePath, suite, projectName } = result;
|
|
76
|
-
acc[filePath] = acc[filePath] || {};
|
|
77
|
-
acc[filePath][suite] = acc[filePath][suite] || {};
|
|
78
|
-
acc[filePath][suite][projectName] = acc[filePath][suite][projectName] || [];
|
|
79
|
-
acc[filePath][suite][projectName].push({ ...result, index, testId, key });
|
|
80
|
-
return acc;
|
|
81
|
-
}, {});
|
|
82
|
-
return groupedResults;
|
|
83
|
-
} else {
|
|
84
|
-
const groupedResults = results.reduce((acc, result, index) => {
|
|
85
|
-
const testId = `${result.filePath}:${result.projectName}:${result.title}`;
|
|
86
|
-
const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
|
|
87
|
-
const { filePath, suite } = result;
|
|
88
|
-
acc[filePath] = acc[filePath] || {};
|
|
89
|
-
acc[filePath][suite] = acc[filePath][suite] || [];
|
|
90
|
-
acc[filePath][suite].push({ ...result, index, testId, key });
|
|
91
|
-
return acc;
|
|
92
|
-
}, {});
|
|
93
|
-
return groupedResults;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// src/helpers/HTMLGenerator.ts
|
|
98
|
-
var HTMLGenerator = class {
|
|
99
|
-
constructor(ortoniConfig, dbManager) {
|
|
100
|
-
this.ortoniConfig = ortoniConfig;
|
|
101
|
-
this.dbManager = dbManager;
|
|
102
|
-
}
|
|
103
|
-
async generateFinalReport(filteredResults, totalDuration, results, projectSet) {
|
|
104
|
-
const data = await this.prepareReportData(
|
|
105
|
-
filteredResults,
|
|
106
|
-
totalDuration,
|
|
107
|
-
results,
|
|
108
|
-
projectSet
|
|
109
|
-
);
|
|
110
|
-
return data;
|
|
111
|
-
}
|
|
112
|
-
async getReportData() {
|
|
113
|
-
return {
|
|
114
|
-
summary: await this.dbManager.getSummaryData(),
|
|
115
|
-
trends: await this.dbManager.getTrends(),
|
|
116
|
-
flakyTests: await this.dbManager.getFlakyTests(),
|
|
117
|
-
slowTests: await this.dbManager.getSlowTests()
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
async prepareReportData(filteredResults, totalDuration, results, projectSet) {
|
|
121
|
-
const totalTests = filteredResults.length;
|
|
122
|
-
const passedTests = results.filter((r) => r.status === "passed").length;
|
|
123
|
-
const flakyTests = results.filter((r) => r.status === "flaky").length;
|
|
124
|
-
const failed = filteredResults.filter(
|
|
125
|
-
(r) => r.status === "failed" || r.status === "timedOut"
|
|
126
|
-
).length;
|
|
127
|
-
const successRate = ((passedTests + flakyTests) / totalTests * 100).toFixed(2);
|
|
128
|
-
const allTags = /* @__PURE__ */ new Set();
|
|
129
|
-
results.forEach(
|
|
130
|
-
(result) => result.testTags.forEach((tag) => allTags.add(tag))
|
|
131
|
-
);
|
|
132
|
-
const projectResults = this.calculateProjectResults(
|
|
133
|
-
filteredResults,
|
|
134
|
-
results,
|
|
135
|
-
projectSet
|
|
136
|
-
);
|
|
137
|
-
const lastRunDate = (/* @__PURE__ */ new Date()).toLocaleString();
|
|
138
|
-
const testHistories = await Promise.all(
|
|
139
|
-
results.map(async (result) => {
|
|
140
|
-
const testId = `${result.filePath}:${result.projectName}:${result.title}`;
|
|
141
|
-
const history = await this.dbManager.getTestHistory(testId);
|
|
142
|
-
return {
|
|
143
|
-
testId,
|
|
144
|
-
history
|
|
145
|
-
};
|
|
146
|
-
})
|
|
147
|
-
);
|
|
148
|
-
return {
|
|
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
|
-
}
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
calculateProjectResults(filteredResults, results, projectSet) {
|
|
190
|
-
return Array.from(projectSet).map((projectName) => {
|
|
191
|
-
const projectTests = filteredResults.filter(
|
|
192
|
-
(r) => r.projectName === projectName
|
|
193
|
-
);
|
|
194
|
-
const allProjectTests = results.filter(
|
|
195
|
-
(r) => r.projectName === projectName
|
|
196
|
-
);
|
|
197
|
-
return {
|
|
198
|
-
projectName,
|
|
199
|
-
passedTests: projectTests.filter((r) => r.status === "passed").length,
|
|
200
|
-
failedTests: projectTests.filter(
|
|
201
|
-
(r) => r.status === "failed" || r.status === "timedOut"
|
|
202
|
-
).length,
|
|
203
|
-
skippedTests: allProjectTests.filter((r) => r.status === "skipped").length,
|
|
204
|
-
retryTests: allProjectTests.filter((r) => r.retryAttemptCount).length,
|
|
205
|
-
flakyTests: allProjectTests.filter((r) => r.status === "flaky").length,
|
|
206
|
-
totalTests: projectTests.length
|
|
207
|
-
};
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
extractProjectStats(projectResults) {
|
|
211
|
-
return {
|
|
212
|
-
projectNames: projectResults.map((result) => result.projectName),
|
|
213
|
-
totalTests: projectResults.map((result) => result.totalTests),
|
|
214
|
-
passedTests: projectResults.map((result) => result.passedTests),
|
|
215
|
-
failedTests: projectResults.map((result) => result.failedTests),
|
|
216
|
-
skippedTests: projectResults.map((result) => result.skippedTests),
|
|
217
|
-
retryTests: projectResults.map((result) => result.retryTests),
|
|
218
|
-
flakyTests: projectResults.map((result) => result.flakyTests)
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
// src/helpers/resultProcessor .ts
|
|
13
|
+
// src/helpers/resultProcessor.ts
|
|
224
14
|
import AnsiToHtml from "ansi-to-html";
|
|
225
|
-
import
|
|
15
|
+
import path2 from "path";
|
|
226
16
|
|
|
227
17
|
// src/utils/attachFiles.ts
|
|
228
|
-
import
|
|
229
|
-
import
|
|
18
|
+
import path from "path";
|
|
19
|
+
import fs2 from "fs";
|
|
230
20
|
|
|
231
21
|
// src/helpers/markdownConverter.ts
|
|
232
|
-
import
|
|
22
|
+
import fs from "fs";
|
|
233
23
|
|
|
234
24
|
// node_modules/marked/lib/marked.esm.js
|
|
235
25
|
function M() {
|
|
@@ -1355,27 +1145,27 @@ var Qt = b.lex;
|
|
|
1355
1145
|
|
|
1356
1146
|
// src/helpers/markdownConverter.ts
|
|
1357
1147
|
function convertMarkdownToHtml(markdownPath, htmlOutputPath) {
|
|
1358
|
-
const hasMarkdown =
|
|
1359
|
-
const markdownContent = hasMarkdown ?
|
|
1148
|
+
const hasMarkdown = fs.existsSync(markdownPath);
|
|
1149
|
+
const markdownContent = hasMarkdown ? fs.readFileSync(markdownPath, "utf-8") : "";
|
|
1360
1150
|
const markdownHtml = hasMarkdown ? k(markdownContent) : "";
|
|
1361
1151
|
const drawerHtml = `${markdownHtml || ""}`;
|
|
1362
|
-
|
|
1152
|
+
fs.writeFileSync(htmlOutputPath, drawerHtml.trim(), "utf-8");
|
|
1363
1153
|
if (hasMarkdown) {
|
|
1364
|
-
|
|
1154
|
+
fs.unlinkSync(markdownPath);
|
|
1365
1155
|
}
|
|
1366
1156
|
}
|
|
1367
1157
|
|
|
1368
1158
|
// src/utils/attachFiles.ts
|
|
1369
1159
|
function attachFiles(subFolder, result, testResult, config, steps, errors) {
|
|
1370
1160
|
const folderPath = config.folderPath || "ortoni-report";
|
|
1371
|
-
const attachmentsFolder =
|
|
1161
|
+
const attachmentsFolder = path.join(
|
|
1372
1162
|
folderPath,
|
|
1373
1163
|
"ortoni-data",
|
|
1374
1164
|
"attachments",
|
|
1375
1165
|
subFolder
|
|
1376
1166
|
);
|
|
1377
|
-
if (!
|
|
1378
|
-
|
|
1167
|
+
if (!fs2.existsSync(attachmentsFolder)) {
|
|
1168
|
+
fs2.mkdirSync(attachmentsFolder, { recursive: true });
|
|
1379
1169
|
}
|
|
1380
1170
|
if (!result.attachments) return;
|
|
1381
1171
|
const { base64Image } = config;
|
|
@@ -1384,14 +1174,14 @@ function attachFiles(subFolder, result, testResult, config, steps, errors) {
|
|
|
1384
1174
|
result.attachments.forEach((attachment) => {
|
|
1385
1175
|
const { contentType, name, path: attachmentPath, body } = attachment;
|
|
1386
1176
|
if (!attachmentPath && !body) return;
|
|
1387
|
-
const fileName = attachmentPath ?
|
|
1388
|
-
const relativePath =
|
|
1177
|
+
const fileName = attachmentPath ? path.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
|
|
1178
|
+
const relativePath = path.join(
|
|
1389
1179
|
"ortoni-data",
|
|
1390
1180
|
"attachments",
|
|
1391
1181
|
subFolder,
|
|
1392
1182
|
fileName
|
|
1393
1183
|
);
|
|
1394
|
-
const fullPath =
|
|
1184
|
+
const fullPath = path.join(attachmentsFolder, fileName);
|
|
1395
1185
|
if (contentType === "image/png") {
|
|
1396
1186
|
handleImage(
|
|
1397
1187
|
attachmentPath,
|
|
@@ -1434,13 +1224,13 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
|
|
|
1434
1224
|
let screenshotPath = "";
|
|
1435
1225
|
if (attachmentPath) {
|
|
1436
1226
|
try {
|
|
1437
|
-
const screenshotContent =
|
|
1227
|
+
const screenshotContent = fs2.readFileSync(
|
|
1438
1228
|
attachmentPath,
|
|
1439
1229
|
base64Image ? "base64" : void 0
|
|
1440
1230
|
);
|
|
1441
1231
|
screenshotPath = base64Image ? `data:image/png;base64,${screenshotContent}` : relativePath;
|
|
1442
1232
|
if (!base64Image) {
|
|
1443
|
-
|
|
1233
|
+
fs2.copyFileSync(attachmentPath, fullPath);
|
|
1444
1234
|
}
|
|
1445
1235
|
} catch (error) {
|
|
1446
1236
|
console.error(
|
|
@@ -1457,7 +1247,7 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
|
|
|
1457
1247
|
}
|
|
1458
1248
|
function handleAttachment(attachmentPath, fullPath, relativePath, resultKey, testResult, steps, errors) {
|
|
1459
1249
|
if (attachmentPath) {
|
|
1460
|
-
|
|
1250
|
+
fs2.copyFileSync(attachmentPath, fullPath);
|
|
1461
1251
|
if (resultKey === "videoPath") {
|
|
1462
1252
|
testResult[resultKey]?.push(relativePath);
|
|
1463
1253
|
} else if (resultKey === "tracePath") {
|
|
@@ -1482,62 +1272,7 @@ function getFileExtension(contentType) {
|
|
|
1482
1272
|
return extensions[contentType] || "unknown";
|
|
1483
1273
|
}
|
|
1484
1274
|
|
|
1485
|
-
// src/
|
|
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
|
-
|
|
1540
|
-
// src/helpers/resultProcessor .ts
|
|
1275
|
+
// src/helpers/resultProcessor.ts
|
|
1541
1276
|
var TestResultProcessor = class {
|
|
1542
1277
|
constructor(projectRoot) {
|
|
1543
1278
|
this.ansiToHtml = new AnsiToHtml({ fg: "var(--snippet-color)" });
|
|
@@ -1593,7 +1328,7 @@ var TestResultProcessor = class {
|
|
|
1593
1328
|
}
|
|
1594
1329
|
processSteps(steps) {
|
|
1595
1330
|
return steps.map((step) => {
|
|
1596
|
-
const stepLocation = step.location ? `${
|
|
1331
|
+
const stepLocation = step.location ? `${path2.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
|
|
1597
1332
|
return {
|
|
1598
1333
|
snippet: this.ansiToHtml.toHtml(escapeHtml(step.error?.snippet || "")),
|
|
1599
1334
|
title: step.title,
|
|
@@ -1622,282 +1357,8 @@ var ServerManager = class {
|
|
|
1622
1357
|
}
|
|
1623
1358
|
};
|
|
1624
1359
|
|
|
1625
|
-
// src/helpers/databaseManager.ts
|
|
1626
|
-
import { open } from "sqlite";
|
|
1627
|
-
import sqlite3 from "sqlite3";
|
|
1628
|
-
var DatabaseManager = class {
|
|
1629
|
-
constructor() {
|
|
1630
|
-
this.db = null;
|
|
1631
|
-
}
|
|
1632
|
-
async initialize(dbPath) {
|
|
1633
|
-
try {
|
|
1634
|
-
this.db = await open({
|
|
1635
|
-
filename: dbPath,
|
|
1636
|
-
driver: sqlite3.Database
|
|
1637
|
-
});
|
|
1638
|
-
await this.createTables();
|
|
1639
|
-
await this.createIndexes();
|
|
1640
|
-
} catch (error) {
|
|
1641
|
-
console.error("OrtoniReport: Error initializing database:", error);
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
async createTables() {
|
|
1645
|
-
if (!this.db) {
|
|
1646
|
-
console.error("OrtoniReport: Database not initialized");
|
|
1647
|
-
return;
|
|
1648
|
-
}
|
|
1649
|
-
try {
|
|
1650
|
-
await this.db.exec(`
|
|
1651
|
-
CREATE TABLE IF NOT EXISTS test_runs (
|
|
1652
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1653
|
-
run_date TEXT
|
|
1654
|
-
);
|
|
1655
|
-
|
|
1656
|
-
CREATE TABLE IF NOT EXISTS test_results (
|
|
1657
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1658
|
-
run_id INTEGER,
|
|
1659
|
-
test_id TEXT,
|
|
1660
|
-
status TEXT,
|
|
1661
|
-
duration INTEGER, -- store duration as raw ms
|
|
1662
|
-
error_message TEXT,
|
|
1663
|
-
FOREIGN KEY (run_id) REFERENCES test_runs (id)
|
|
1664
|
-
);
|
|
1665
|
-
`);
|
|
1666
|
-
} catch (error) {
|
|
1667
|
-
console.error("OrtoniReport: Error creating tables:", error);
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
async createIndexes() {
|
|
1671
|
-
if (!this.db) {
|
|
1672
|
-
console.error("OrtoniReport: Database not initialized");
|
|
1673
|
-
return;
|
|
1674
|
-
}
|
|
1675
|
-
try {
|
|
1676
|
-
await this.db.exec(`
|
|
1677
|
-
CREATE INDEX IF NOT EXISTS idx_test_id ON test_results (test_id);
|
|
1678
|
-
CREATE INDEX IF NOT EXISTS idx_run_id ON test_results (run_id);
|
|
1679
|
-
`);
|
|
1680
|
-
} catch (error) {
|
|
1681
|
-
console.error("OrtoniReport: Error creating indexes:", error);
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
async saveTestRun() {
|
|
1685
|
-
if (!this.db) {
|
|
1686
|
-
console.error("OrtoniReport: Database not initialized");
|
|
1687
|
-
return null;
|
|
1688
|
-
}
|
|
1689
|
-
try {
|
|
1690
|
-
const runDate = (/* @__PURE__ */ new Date()).toISOString();
|
|
1691
|
-
const { lastID } = await this.db.run(
|
|
1692
|
-
`
|
|
1693
|
-
INSERT INTO test_runs (run_date)
|
|
1694
|
-
VALUES (?)
|
|
1695
|
-
`,
|
|
1696
|
-
[runDate]
|
|
1697
|
-
);
|
|
1698
|
-
return lastID;
|
|
1699
|
-
} catch (error) {
|
|
1700
|
-
console.error("OrtoniReport: Error saving test run:", error);
|
|
1701
|
-
return null;
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
async saveTestResults(runId, results) {
|
|
1705
|
-
if (!this.db) {
|
|
1706
|
-
console.error("OrtoniReport: Database not initialized");
|
|
1707
|
-
return;
|
|
1708
|
-
}
|
|
1709
|
-
try {
|
|
1710
|
-
await this.db.exec("BEGIN TRANSACTION;");
|
|
1711
|
-
const stmt = await this.db.prepare(`
|
|
1712
|
-
INSERT INTO test_results (run_id, test_id, status, duration, error_message)
|
|
1713
|
-
VALUES (?, ?, ?, ?, ?)
|
|
1714
|
-
`);
|
|
1715
|
-
for (const result of results) {
|
|
1716
|
-
await stmt.run([
|
|
1717
|
-
runId,
|
|
1718
|
-
`${result.filePath}:${result.projectName}:${result.title}`,
|
|
1719
|
-
result.status,
|
|
1720
|
-
result.duration,
|
|
1721
|
-
// store raw ms
|
|
1722
|
-
result.errors.join("\n")
|
|
1723
|
-
]);
|
|
1724
|
-
}
|
|
1725
|
-
await stmt.finalize();
|
|
1726
|
-
await this.db.exec("COMMIT;");
|
|
1727
|
-
} catch (error) {
|
|
1728
|
-
await this.db.exec("ROLLBACK;");
|
|
1729
|
-
console.error("OrtoniReport: Error saving test results:", error);
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
async getTestHistory(testId, limit = 10) {
|
|
1733
|
-
if (!this.db) {
|
|
1734
|
-
console.error("OrtoniReport: Database not initialized");
|
|
1735
|
-
return [];
|
|
1736
|
-
}
|
|
1737
|
-
try {
|
|
1738
|
-
const results = await this.db.all(
|
|
1739
|
-
`
|
|
1740
|
-
SELECT tr.status, tr.duration, tr.error_message, trun.run_date
|
|
1741
|
-
FROM test_results tr
|
|
1742
|
-
JOIN test_runs trun ON tr.run_id = trun.id
|
|
1743
|
-
WHERE tr.test_id = ?
|
|
1744
|
-
ORDER BY trun.run_date DESC
|
|
1745
|
-
LIMIT ?
|
|
1746
|
-
`,
|
|
1747
|
-
[testId, limit]
|
|
1748
|
-
);
|
|
1749
|
-
return results.map((result) => ({
|
|
1750
|
-
...result,
|
|
1751
|
-
run_date: formatDateLocal(result.run_date)
|
|
1752
|
-
}));
|
|
1753
|
-
} catch (error) {
|
|
1754
|
-
console.error("OrtoniReport: Error getting test history:", error);
|
|
1755
|
-
return [];
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
async close() {
|
|
1759
|
-
if (this.db) {
|
|
1760
|
-
try {
|
|
1761
|
-
await this.db.close();
|
|
1762
|
-
} catch (error) {
|
|
1763
|
-
console.error("OrtoniReport: Error closing database:", error);
|
|
1764
|
-
} finally {
|
|
1765
|
-
this.db = null;
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
async getSummaryData() {
|
|
1770
|
-
if (!this.db) {
|
|
1771
|
-
console.error("OrtoniReport: Database not initialized");
|
|
1772
|
-
return {
|
|
1773
|
-
totalRuns: 0,
|
|
1774
|
-
totalTests: 0,
|
|
1775
|
-
passed: 0,
|
|
1776
|
-
failed: 0,
|
|
1777
|
-
passRate: 0,
|
|
1778
|
-
avgDuration: 0
|
|
1779
|
-
};
|
|
1780
|
-
}
|
|
1781
|
-
try {
|
|
1782
|
-
const summary = await this.db.get(`
|
|
1783
|
-
SELECT
|
|
1784
|
-
(SELECT COUNT(*) FROM test_runs) as totalRuns,
|
|
1785
|
-
(SELECT COUNT(*) FROM test_results) as totalTests,
|
|
1786
|
-
(SELECT COUNT(*) FROM test_results WHERE status = 'passed') as passed,
|
|
1787
|
-
(SELECT COUNT(*) FROM test_results WHERE status = 'failed') as failed,
|
|
1788
|
-
(SELECT AVG(duration) FROM test_results) as avgDuration
|
|
1789
|
-
`);
|
|
1790
|
-
const passRate = summary.totalTests ? (summary.passed / summary.totalTests * 100).toFixed(2) : 0;
|
|
1791
|
-
return {
|
|
1792
|
-
totalRuns: summary.totalRuns,
|
|
1793
|
-
totalTests: summary.totalTests,
|
|
1794
|
-
passed: summary.passed,
|
|
1795
|
-
failed: summary.failed,
|
|
1796
|
-
passRate: parseFloat(passRate.toString()),
|
|
1797
|
-
avgDuration: Math.round(summary.avgDuration || 0)
|
|
1798
|
-
// raw ms avg
|
|
1799
|
-
};
|
|
1800
|
-
} catch (error) {
|
|
1801
|
-
console.error("OrtoniReport: Error getting summary data:", error);
|
|
1802
|
-
return {
|
|
1803
|
-
totalRuns: 0,
|
|
1804
|
-
totalTests: 0,
|
|
1805
|
-
passed: 0,
|
|
1806
|
-
failed: 0,
|
|
1807
|
-
passRate: 0,
|
|
1808
|
-
avgDuration: 0
|
|
1809
|
-
};
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
async getTrends(limit = 100) {
|
|
1813
|
-
if (!this.db) {
|
|
1814
|
-
console.error("OrtoniReport: Database not initialized");
|
|
1815
|
-
return [];
|
|
1816
|
-
}
|
|
1817
|
-
try {
|
|
1818
|
-
const rows = await this.db.all(
|
|
1819
|
-
`
|
|
1820
|
-
SELECT trun.run_date,
|
|
1821
|
-
SUM(CASE WHEN tr.status = 'passed' THEN 1 ELSE 0 END) AS passed,
|
|
1822
|
-
SUM(CASE WHEN tr.status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
|
1823
|
-
AVG(tr.duration) AS avg_duration
|
|
1824
|
-
FROM test_results tr
|
|
1825
|
-
JOIN test_runs trun ON tr.run_id = trun.id
|
|
1826
|
-
GROUP BY trun.run_date
|
|
1827
|
-
ORDER BY trun.run_date DESC
|
|
1828
|
-
LIMIT ?
|
|
1829
|
-
`,
|
|
1830
|
-
[limit]
|
|
1831
|
-
);
|
|
1832
|
-
return rows.reverse().map((row) => ({
|
|
1833
|
-
...row,
|
|
1834
|
-
run_date: formatDateLocal(row.run_date),
|
|
1835
|
-
avg_duration: Math.round(row.avg_duration || 0)
|
|
1836
|
-
// raw ms avg
|
|
1837
|
-
}));
|
|
1838
|
-
} catch (error) {
|
|
1839
|
-
console.error("OrtoniReport: Error getting trends data:", error);
|
|
1840
|
-
return [];
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
async getFlakyTests(limit = 10) {
|
|
1844
|
-
if (!this.db) {
|
|
1845
|
-
console.error("OrtoniReport: Database not initialized");
|
|
1846
|
-
return [];
|
|
1847
|
-
}
|
|
1848
|
-
try {
|
|
1849
|
-
return await this.db.all(
|
|
1850
|
-
`
|
|
1851
|
-
SELECT
|
|
1852
|
-
test_id,
|
|
1853
|
-
COUNT(*) AS total,
|
|
1854
|
-
SUM(CASE WHEN status = 'flaky' THEN 1 ELSE 0 END) AS flaky,
|
|
1855
|
-
AVG(duration) AS avg_duration
|
|
1856
|
-
FROM test_results
|
|
1857
|
-
GROUP BY test_id
|
|
1858
|
-
HAVING flaky > 0
|
|
1859
|
-
ORDER BY flaky DESC
|
|
1860
|
-
LIMIT ?
|
|
1861
|
-
`,
|
|
1862
|
-
[limit]
|
|
1863
|
-
);
|
|
1864
|
-
} catch (error) {
|
|
1865
|
-
console.error("OrtoniReport: Error getting flaky tests:", error);
|
|
1866
|
-
return [];
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
async getSlowTests(limit = 10) {
|
|
1870
|
-
if (!this.db) {
|
|
1871
|
-
console.error("OrtoniReport: Database not initialized");
|
|
1872
|
-
return [];
|
|
1873
|
-
}
|
|
1874
|
-
try {
|
|
1875
|
-
const rows = await this.db.all(
|
|
1876
|
-
`
|
|
1877
|
-
SELECT
|
|
1878
|
-
test_id,
|
|
1879
|
-
AVG(duration) AS avg_duration
|
|
1880
|
-
FROM test_results
|
|
1881
|
-
GROUP BY test_id
|
|
1882
|
-
ORDER BY avg_duration DESC
|
|
1883
|
-
LIMIT ?
|
|
1884
|
-
`,
|
|
1885
|
-
[limit]
|
|
1886
|
-
);
|
|
1887
|
-
return rows.map((row) => ({
|
|
1888
|
-
test_id: row.test_id,
|
|
1889
|
-
avg_duration: Math.round(row.avg_duration || 0)
|
|
1890
|
-
// raw ms avg
|
|
1891
|
-
}));
|
|
1892
|
-
} catch (error) {
|
|
1893
|
-
console.error("OrtoniReport: Error getting slow tests:", error);
|
|
1894
|
-
return [];
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
};
|
|
1898
|
-
|
|
1899
1360
|
// src/ortoni-report.ts
|
|
1900
|
-
import
|
|
1361
|
+
import path3 from "path";
|
|
1901
1362
|
var OrtoniReport = class {
|
|
1902
1363
|
constructor(ortoniConfig = {}) {
|
|
1903
1364
|
this.ortoniConfig = ortoniConfig;
|
|
@@ -1928,8 +1389,9 @@ var OrtoniReport = class {
|
|
|
1928
1389
|
this.testResultProcessor = new TestResultProcessor(config.rootDir);
|
|
1929
1390
|
this.fileManager.ensureReportDirectory();
|
|
1930
1391
|
await this.dbManager.initialize(
|
|
1931
|
-
|
|
1392
|
+
path3.join(this.folderPath, "ortoni-data-history.sqlite")
|
|
1932
1393
|
);
|
|
1394
|
+
this.shardConfig = config?.shard;
|
|
1933
1395
|
}
|
|
1934
1396
|
onStdOut(chunk, _test, _result) {
|
|
1935
1397
|
if (this.reportsCount == 1 && this.showConsoleLogs) {
|
|
@@ -1946,7 +1408,7 @@ var OrtoniReport = class {
|
|
|
1946
1408
|
);
|
|
1947
1409
|
this.results.push(testResult);
|
|
1948
1410
|
} catch (error) {
|
|
1949
|
-
console.error("
|
|
1411
|
+
console.error("Ortoni Report: Error processing test end:", error);
|
|
1950
1412
|
}
|
|
1951
1413
|
}
|
|
1952
1414
|
printsToStdio() {
|
|
@@ -1965,6 +1427,32 @@ var OrtoniReport = class {
|
|
|
1965
1427
|
(r) => r.status !== "skipped"
|
|
1966
1428
|
);
|
|
1967
1429
|
const totalDuration = result.duration;
|
|
1430
|
+
if (this.shardConfig && this.shardConfig.total > 1) {
|
|
1431
|
+
const shard = this.shardConfig;
|
|
1432
|
+
const shardFile = `ortoni-shard-${shard.current}-of-${shard.total}.json`;
|
|
1433
|
+
const shardData = {
|
|
1434
|
+
status: result.status,
|
|
1435
|
+
duration: totalDuration,
|
|
1436
|
+
results: this.results,
|
|
1437
|
+
projectSet: Array.from(this.projectSet),
|
|
1438
|
+
userConfig: {
|
|
1439
|
+
projectName: this.ortoniConfig.projectName,
|
|
1440
|
+
authorName: this.ortoniConfig.authorName,
|
|
1441
|
+
type: this.ortoniConfig.testType,
|
|
1442
|
+
title: this.ortoniConfig.title
|
|
1443
|
+
},
|
|
1444
|
+
userMeta: {
|
|
1445
|
+
meta: this.ortoniConfig.meta
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
const shardFilePath = this.fileManager.writeRawFile(
|
|
1449
|
+
shardFile,
|
|
1450
|
+
shardData
|
|
1451
|
+
);
|
|
1452
|
+
console.info(`Ortoni Report: Shard data written to ${shardFilePath}`);
|
|
1453
|
+
this.shouldGenerateReport = false;
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1968
1456
|
const runId = await this.dbManager.saveTestRun();
|
|
1969
1457
|
if (runId !== null) {
|
|
1970
1458
|
await this.dbManager.saveTestResults(runId, this.results);
|
|
@@ -1974,21 +1462,21 @@ var OrtoniReport = class {
|
|
|
1974
1462
|
this.results,
|
|
1975
1463
|
this.projectSet
|
|
1976
1464
|
);
|
|
1977
|
-
this.outputPath = this.fileManager.writeReportFile(
|
|
1465
|
+
this.outputPath = await this.fileManager.writeReportFile(
|
|
1978
1466
|
this.outputFilename,
|
|
1979
1467
|
finalReportData
|
|
1980
1468
|
);
|
|
1981
1469
|
} else {
|
|
1982
|
-
console.error("
|
|
1470
|
+
console.error("Ortoni Report: Error saving test run to database");
|
|
1983
1471
|
}
|
|
1984
1472
|
} else {
|
|
1985
1473
|
console.error(
|
|
1986
|
-
"
|
|
1474
|
+
"Ortoni Report: Report generation skipped due to error in Playwright worker!"
|
|
1987
1475
|
);
|
|
1988
1476
|
}
|
|
1989
1477
|
} catch (error) {
|
|
1990
1478
|
this.shouldGenerateReport = false;
|
|
1991
|
-
console.error("
|
|
1479
|
+
console.error("Ortoni Report: Error generating report:", error);
|
|
1992
1480
|
}
|
|
1993
1481
|
}
|
|
1994
1482
|
async onExit() {
|
|
@@ -1996,7 +1484,7 @@ var OrtoniReport = class {
|
|
|
1996
1484
|
await this.dbManager.close();
|
|
1997
1485
|
if (this.shouldGenerateReport) {
|
|
1998
1486
|
this.fileManager.copyTraceViewerAssets(this.skipTraceViewer);
|
|
1999
|
-
console.info(`Ortoni
|
|
1487
|
+
console.info(`Ortoni Report generated at ${this.outputPath}`);
|
|
2000
1488
|
this.serverManager.startServer(
|
|
2001
1489
|
this.folderPath,
|
|
2002
1490
|
this.outputFilename,
|
|
@@ -2006,7 +1494,7 @@ var OrtoniReport = class {
|
|
|
2006
1494
|
});
|
|
2007
1495
|
}
|
|
2008
1496
|
} catch (error) {
|
|
2009
|
-
console.error("
|
|
1497
|
+
console.error("Ortoni Report: Error in onExit:", error);
|
|
2010
1498
|
}
|
|
2011
1499
|
}
|
|
2012
1500
|
};
|