ortoni-report 4.0.2-beta.1 → 4.0.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/changelog.md +27 -0
- package/dist/{chunk-INS3E7E6.mjs → chunk-4RZ5C7KY.mjs} +402 -296
- package/dist/{cli/cli.js → cli.js} +405 -190
- package/dist/cli.mjs +208 -0
- package/dist/helpers/HTMLGenerator.d.ts +89 -0
- package/dist/helpers/HTMLGenerator.js +163 -0
- package/dist/helpers/databaseManager.d.ts +35 -0
- package/dist/helpers/databaseManager.js +268 -0
- package/dist/helpers/fileManager.d.ts +8 -0
- package/dist/helpers/fileManager.js +60 -0
- package/dist/helpers/markdownConverter.d.ts +1 -0
- package/dist/helpers/markdownConverter.js +14 -0
- package/dist/helpers/resultProcessor.d.ts +10 -0
- package/dist/helpers/resultProcessor.js +60 -0
- package/dist/helpers/serverManager.d.ts +6 -0
- package/dist/helpers/serverManager.js +15 -0
- package/dist/helpers/templateLoader.d.ts +15 -0
- package/dist/helpers/templateLoader.js +88 -0
- package/dist/index.html +1 -1
- package/dist/mergeData.d.ts +13 -0
- package/dist/mergeData.js +182 -0
- package/dist/ortoni-report.d.mts +8 -1
- package/dist/ortoni-report.d.ts +8 -1
- package/dist/ortoni-report.js +211 -96
- package/dist/ortoni-report.mjs +25 -22
- package/dist/{ortoni-report.d.cts → types/reporterConfig.d.ts} +8 -33
- package/dist/types/reporterConfig.js +1 -0
- package/dist/types/testResults.d.ts +31 -0
- package/dist/types/testResults.js +1 -0
- package/dist/utils/attachFiles.d.ts +4 -0
- package/dist/utils/attachFiles.js +87 -0
- package/dist/utils/expressServer.d.ts +1 -0
- package/dist/utils/expressServer.js +61 -0
- package/dist/utils/groupProjects.d.ts +3 -0
- package/dist/utils/groupProjects.js +30 -0
- package/dist/utils/utils.d.ts +15 -0
- package/dist/utils/utils.js +84 -0
- package/package.json +11 -4
- package/readme.md +60 -75
- package/dist/chunk-45EJSEX2.mjs +0 -632
- package/dist/chunk-75EAJL2U.mjs +0 -632
- package/dist/chunk-A6HCKATU.mjs +0 -76
- package/dist/chunk-FGIYOFIC.mjs +0 -632
- package/dist/chunk-FHKWBHU6.mjs +0 -633
- package/dist/chunk-GLICR3VS.mjs +0 -637
- package/dist/chunk-HFO6XSKC.mjs +0 -633
- package/dist/chunk-HOZD6YIV.mjs +0 -634
- package/dist/chunk-IJO2YIFE.mjs +0 -637
- package/dist/chunk-JEIWNUQY.mjs +0 -632
- package/dist/chunk-JPLAGYR7.mjs +0 -632
- package/dist/chunk-NM6ULN2O.mjs +0 -632
- package/dist/chunk-OZS6QIJS.mjs +0 -638
- package/dist/chunk-P57227VN.mjs +0 -633
- package/dist/chunk-QMTRYN5N.js +0 -635
- package/dist/chunk-TI33PMMQ.mjs +0 -639
- package/dist/chunk-Z5NBP5TS.mjs +0 -635
- package/dist/cli/cli.cjs +0 -678
- package/dist/cli/cli.d.cts +0 -1
- package/dist/cli/cli.mjs +0 -103
- package/dist/ortoni-report.cjs +0 -2134
- /package/dist/{cli/cli.d.mts → cli.d.mts} +0 -0
- /package/dist/{cli/cli.d.ts → cli.d.ts} +0 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { open } from "sqlite";
|
|
2
|
+
import sqlite3 from "sqlite3";
|
|
3
|
+
import { formatDateLocal } from "../utils/utils";
|
|
4
|
+
export class DatabaseManager {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.db = null;
|
|
7
|
+
}
|
|
8
|
+
async initialize(dbPath) {
|
|
9
|
+
try {
|
|
10
|
+
this.db = await open({
|
|
11
|
+
filename: dbPath,
|
|
12
|
+
driver: sqlite3.Database,
|
|
13
|
+
});
|
|
14
|
+
await this.createTables();
|
|
15
|
+
await this.createIndexes();
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
console.error("OrtoniReport: Error initializing database:", error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async createTables() {
|
|
22
|
+
if (!this.db) {
|
|
23
|
+
console.error("OrtoniReport: Database not initialized");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
await this.db.exec(`
|
|
28
|
+
CREATE TABLE IF NOT EXISTS test_runs (
|
|
29
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
30
|
+
run_date TEXT
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS test_results (
|
|
34
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
35
|
+
run_id INTEGER,
|
|
36
|
+
test_id TEXT,
|
|
37
|
+
status TEXT,
|
|
38
|
+
duration INTEGER, -- store duration as raw ms
|
|
39
|
+
error_message TEXT,
|
|
40
|
+
FOREIGN KEY (run_id) REFERENCES test_runs (id)
|
|
41
|
+
);
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error("OrtoniReport: Error creating tables:", error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async createIndexes() {
|
|
49
|
+
if (!this.db) {
|
|
50
|
+
console.error("OrtoniReport: Database not initialized");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await this.db.exec(`
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_test_id ON test_results (test_id);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_run_id ON test_results (run_id);
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
console.error("OrtoniReport: Error creating indexes:", error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async saveTestRun() {
|
|
64
|
+
if (!this.db) {
|
|
65
|
+
console.error("OrtoniReport: Database not initialized");
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const runDate = new Date().toISOString();
|
|
70
|
+
const { lastID } = await this.db.run(`
|
|
71
|
+
INSERT INTO test_runs (run_date)
|
|
72
|
+
VALUES (?)
|
|
73
|
+
`, [runDate]);
|
|
74
|
+
return lastID;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error("OrtoniReport: Error saving test run:", error);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async saveTestResults(runId, results) {
|
|
82
|
+
if (!this.db) {
|
|
83
|
+
console.error("OrtoniReport: Database not initialized");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await this.db.exec("BEGIN TRANSACTION;");
|
|
88
|
+
const stmt = await this.db.prepare(`
|
|
89
|
+
INSERT INTO test_results (run_id, test_id, status, duration, error_message)
|
|
90
|
+
VALUES (?, ?, ?, ?, ?)
|
|
91
|
+
`);
|
|
92
|
+
for (const result of results) {
|
|
93
|
+
await stmt.run([
|
|
94
|
+
runId,
|
|
95
|
+
`${result.filePath}:${result.projectName}:${result.title}`,
|
|
96
|
+
result.status,
|
|
97
|
+
result.duration,
|
|
98
|
+
result.errors.join("\n"),
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
await stmt.finalize();
|
|
102
|
+
await this.db.exec("COMMIT;");
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
await this.db.exec("ROLLBACK;");
|
|
106
|
+
console.error("OrtoniReport: Error saving test results:", error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async getTestHistory(testId, limit = 10) {
|
|
110
|
+
if (!this.db) {
|
|
111
|
+
console.error("OrtoniReport: Database not initialized");
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const results = await this.db.all(`
|
|
116
|
+
SELECT tr.status, tr.duration, tr.error_message, trun.run_date
|
|
117
|
+
FROM test_results tr
|
|
118
|
+
JOIN test_runs trun ON tr.run_id = trun.id
|
|
119
|
+
WHERE tr.test_id = ?
|
|
120
|
+
ORDER BY trun.run_date DESC
|
|
121
|
+
LIMIT ?
|
|
122
|
+
`, [testId, limit]);
|
|
123
|
+
return results.map((result) => ({
|
|
124
|
+
...result,
|
|
125
|
+
run_date: formatDateLocal(result.run_date),
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error("OrtoniReport: Error getting test history:", error);
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async close() {
|
|
134
|
+
if (this.db) {
|
|
135
|
+
try {
|
|
136
|
+
await this.db.close();
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
console.error("OrtoniReport: Error closing database:", error);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
this.db = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async getSummaryData() {
|
|
147
|
+
if (!this.db) {
|
|
148
|
+
console.error("OrtoniReport: Database not initialized");
|
|
149
|
+
return {
|
|
150
|
+
totalRuns: 0,
|
|
151
|
+
totalTests: 0,
|
|
152
|
+
passed: 0,
|
|
153
|
+
failed: 0,
|
|
154
|
+
passRate: 0,
|
|
155
|
+
avgDuration: 0,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const summary = await this.db.get(`
|
|
160
|
+
SELECT
|
|
161
|
+
(SELECT COUNT(*) FROM test_runs) as totalRuns,
|
|
162
|
+
(SELECT COUNT(*) FROM test_results) as totalTests,
|
|
163
|
+
(SELECT COUNT(*) FROM test_results WHERE status = 'passed') as passed,
|
|
164
|
+
(SELECT COUNT(*) FROM test_results WHERE status = 'failed') as failed,
|
|
165
|
+
(SELECT AVG(duration) FROM test_results) as avgDuration
|
|
166
|
+
`);
|
|
167
|
+
const passRate = summary.totalTests
|
|
168
|
+
? ((summary.passed / summary.totalTests) * 100).toFixed(2)
|
|
169
|
+
: 0;
|
|
170
|
+
return {
|
|
171
|
+
totalRuns: summary.totalRuns,
|
|
172
|
+
totalTests: summary.totalTests,
|
|
173
|
+
passed: summary.passed,
|
|
174
|
+
failed: summary.failed,
|
|
175
|
+
passRate: parseFloat(passRate.toString()),
|
|
176
|
+
avgDuration: Math.round(summary.avgDuration || 0), // raw ms avg
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
console.error("OrtoniReport: Error getting summary data:", error);
|
|
181
|
+
return {
|
|
182
|
+
totalRuns: 0,
|
|
183
|
+
totalTests: 0,
|
|
184
|
+
passed: 0,
|
|
185
|
+
failed: 0,
|
|
186
|
+
passRate: 0,
|
|
187
|
+
avgDuration: 0,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async getTrends(limit = 100) {
|
|
192
|
+
if (!this.db) {
|
|
193
|
+
console.error("OrtoniReport: Database not initialized");
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const rows = await this.db.all(`
|
|
198
|
+
SELECT trun.run_date,
|
|
199
|
+
SUM(CASE WHEN tr.status = 'passed' THEN 1 ELSE 0 END) AS passed,
|
|
200
|
+
SUM(CASE WHEN tr.status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
|
201
|
+
AVG(tr.duration) AS avg_duration
|
|
202
|
+
FROM test_results tr
|
|
203
|
+
JOIN test_runs trun ON tr.run_id = trun.id
|
|
204
|
+
GROUP BY trun.run_date
|
|
205
|
+
ORDER BY trun.run_date DESC
|
|
206
|
+
LIMIT ?
|
|
207
|
+
`, [limit]);
|
|
208
|
+
return rows.reverse().map((row) => ({
|
|
209
|
+
...row,
|
|
210
|
+
run_date: formatDateLocal(row.run_date),
|
|
211
|
+
avg_duration: Math.round(row.avg_duration || 0), // raw ms avg
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
console.error("OrtoniReport: Error getting trends data:", error);
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async getFlakyTests(limit = 10) {
|
|
220
|
+
if (!this.db) {
|
|
221
|
+
console.error("OrtoniReport: Database not initialized");
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
return await this.db.all(`
|
|
226
|
+
SELECT
|
|
227
|
+
test_id,
|
|
228
|
+
COUNT(*) AS total,
|
|
229
|
+
SUM(CASE WHEN status = 'flaky' THEN 1 ELSE 0 END) AS flaky,
|
|
230
|
+
AVG(duration) AS avg_duration
|
|
231
|
+
FROM test_results
|
|
232
|
+
GROUP BY test_id
|
|
233
|
+
HAVING flaky > 0
|
|
234
|
+
ORDER BY flaky DESC
|
|
235
|
+
LIMIT ?
|
|
236
|
+
`, [limit]);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
console.error("OrtoniReport: Error getting flaky tests:", error);
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async getSlowTests(limit = 10) {
|
|
244
|
+
if (!this.db) {
|
|
245
|
+
console.error("OrtoniReport: Database not initialized");
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const rows = await this.db.all(`
|
|
250
|
+
SELECT
|
|
251
|
+
test_id,
|
|
252
|
+
AVG(duration) AS avg_duration
|
|
253
|
+
FROM test_results
|
|
254
|
+
GROUP BY test_id
|
|
255
|
+
ORDER BY avg_duration DESC
|
|
256
|
+
LIMIT ?
|
|
257
|
+
`, [limit]);
|
|
258
|
+
return rows.map((row) => ({
|
|
259
|
+
test_id: row.test_id,
|
|
260
|
+
avg_duration: Math.round(row.avg_duration || 0), // raw ms avg
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
console.error("OrtoniReport: Error getting slow tests:", error);
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class FileManager {
|
|
2
|
+
private folderPath;
|
|
3
|
+
constructor(folderPath: string);
|
|
4
|
+
ensureReportDirectory(): void;
|
|
5
|
+
writeReportFile(filename: string, data: unknown): Promise<string>;
|
|
6
|
+
writeRawFile(filename: string, data: unknown): string;
|
|
7
|
+
copyTraceViewerAssets(skip: boolean): void;
|
|
8
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { readBundledTemplate } from "./templateLoader";
|
|
4
|
+
export class FileManager {
|
|
5
|
+
constructor(folderPath) {
|
|
6
|
+
this.folderPath = folderPath;
|
|
7
|
+
}
|
|
8
|
+
ensureReportDirectory() {
|
|
9
|
+
const ortoniDataFolder = path.join(this.folderPath, "ortoni-data");
|
|
10
|
+
if (!fs.existsSync(this.folderPath)) {
|
|
11
|
+
fs.mkdirSync(this.folderPath, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
if (fs.existsSync(ortoniDataFolder)) {
|
|
15
|
+
fs.rmSync(ortoniDataFolder, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async writeReportFile(filename, data) {
|
|
20
|
+
let html = await readBundledTemplate();
|
|
21
|
+
// let html = fs.readFileSync(templatePath, "utf-8");
|
|
22
|
+
const reportJSON = JSON.stringify({
|
|
23
|
+
data,
|
|
24
|
+
});
|
|
25
|
+
html = html.replace("__ORTONI_TEST_REPORTDATA__", reportJSON);
|
|
26
|
+
const outputPath = path.join(process.cwd(), this.folderPath, filename);
|
|
27
|
+
fs.writeFileSync(outputPath, html);
|
|
28
|
+
return outputPath;
|
|
29
|
+
}
|
|
30
|
+
writeRawFile(filename, data) {
|
|
31
|
+
const outputPath = path.join(process.cwd(), this.folderPath, filename);
|
|
32
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
33
|
+
const content = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
34
|
+
fs.writeFileSync(outputPath, content, "utf-8");
|
|
35
|
+
return outputPath;
|
|
36
|
+
}
|
|
37
|
+
copyTraceViewerAssets(skip) {
|
|
38
|
+
if (skip)
|
|
39
|
+
return;
|
|
40
|
+
const traceViewerFolder = path.join(require.resolve("playwright-core"), "..", "lib", "vite", "traceViewer");
|
|
41
|
+
const traceViewerTargetFolder = path.join(this.folderPath, "trace");
|
|
42
|
+
const traceViewerAssetsTargetFolder = path.join(traceViewerTargetFolder, "assets");
|
|
43
|
+
fs.mkdirSync(traceViewerAssetsTargetFolder, { recursive: true });
|
|
44
|
+
// Copy main trace viewer files
|
|
45
|
+
for (const file of fs.readdirSync(traceViewerFolder)) {
|
|
46
|
+
if (file.endsWith(".map") ||
|
|
47
|
+
file.includes("watch") ||
|
|
48
|
+
file.includes("assets"))
|
|
49
|
+
continue;
|
|
50
|
+
fs.copyFileSync(path.join(traceViewerFolder, file), path.join(traceViewerTargetFolder, file));
|
|
51
|
+
}
|
|
52
|
+
// Copy assets
|
|
53
|
+
const assetsFolder = path.join(traceViewerFolder, "assets");
|
|
54
|
+
for (const file of fs.readdirSync(assetsFolder)) {
|
|
55
|
+
if (file.endsWith(".map") || file.includes("xtermModule"))
|
|
56
|
+
continue;
|
|
57
|
+
fs.copyFileSync(path.join(assetsFolder, file), path.join(traceViewerAssetsTargetFolder, file));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function convertMarkdownToHtml(markdownPath: string, htmlOutputPath: string): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
export function convertMarkdownToHtml(markdownPath, htmlOutputPath) {
|
|
4
|
+
const hasMarkdown = fs.existsSync(markdownPath);
|
|
5
|
+
const markdownContent = hasMarkdown
|
|
6
|
+
? fs.readFileSync(markdownPath, "utf-8")
|
|
7
|
+
: "";
|
|
8
|
+
const markdownHtml = hasMarkdown ? marked(markdownContent) : "";
|
|
9
|
+
const drawerHtml = `${markdownHtml || ""}`;
|
|
10
|
+
fs.writeFileSync(htmlOutputPath, drawerHtml.trim(), "utf-8");
|
|
11
|
+
if (hasMarkdown) {
|
|
12
|
+
fs.unlinkSync(markdownPath);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { TestCase, TestResult } from "@playwright/test/reporter";
|
|
2
|
+
import { TestResultData } from "../types/testResults";
|
|
3
|
+
import { OrtoniReportConfig } from "../types/reporterConfig";
|
|
4
|
+
export declare class TestResultProcessor {
|
|
5
|
+
private ansiToHtml;
|
|
6
|
+
private projectRoot;
|
|
7
|
+
constructor(projectRoot: string);
|
|
8
|
+
processTestResult(test: TestCase, result: TestResult, projectSet: Set<string>, ortoniConfig: OrtoniReportConfig): TestResultData;
|
|
9
|
+
private processSteps;
|
|
10
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import AnsiToHtml from "ansi-to-html";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { attachFiles } from "../utils/attachFiles";
|
|
4
|
+
import { normalizeFilePath, escapeHtml, extractSuites } from "../utils/utils";
|
|
5
|
+
export class TestResultProcessor {
|
|
6
|
+
constructor(projectRoot) {
|
|
7
|
+
this.ansiToHtml = new AnsiToHtml({ fg: "var(--snippet-color)" });
|
|
8
|
+
this.projectRoot = projectRoot;
|
|
9
|
+
}
|
|
10
|
+
processTestResult(test, result, projectSet, ortoniConfig) {
|
|
11
|
+
const status = test.outcome() === "flaky" ? "flaky" : result.status;
|
|
12
|
+
const projectName = test.titlePath()[1];
|
|
13
|
+
projectSet.add(projectName);
|
|
14
|
+
const location = test.location;
|
|
15
|
+
const filePath = normalizeFilePath(test.titlePath()[2]);
|
|
16
|
+
const tagPattern = /@[\w]+/g;
|
|
17
|
+
const title = test.title.replace(tagPattern, "").trim();
|
|
18
|
+
const suite = test.titlePath()[3].replace(tagPattern, "").trim();
|
|
19
|
+
const suiteAndTitle = extractSuites(test.titlePath());
|
|
20
|
+
const suiteHierarchy = suiteAndTitle.hierarchy;
|
|
21
|
+
const testResult = {
|
|
22
|
+
suiteHierarchy,
|
|
23
|
+
key: test.id,
|
|
24
|
+
annotations: test.annotations,
|
|
25
|
+
testTags: test.tags,
|
|
26
|
+
location: `${filePath}:${location.line}:${location.column}`,
|
|
27
|
+
retryAttemptCount: result.retry,
|
|
28
|
+
projectName: projectName,
|
|
29
|
+
suite,
|
|
30
|
+
title,
|
|
31
|
+
status,
|
|
32
|
+
flaky: test.outcome(),
|
|
33
|
+
duration: result.duration,
|
|
34
|
+
errors: result.errors.map((e) => this.ansiToHtml.toHtml(escapeHtml(e.stack || e.toString()))),
|
|
35
|
+
steps: this.processSteps(result.steps),
|
|
36
|
+
logs: this.ansiToHtml.toHtml(escapeHtml(result.stdout
|
|
37
|
+
.concat(result.stderr)
|
|
38
|
+
.map((log) => log)
|
|
39
|
+
.join("\n"))),
|
|
40
|
+
filePath: filePath,
|
|
41
|
+
filters: projectSet,
|
|
42
|
+
base64Image: ortoniConfig.base64Image,
|
|
43
|
+
testId: `${filePath}:${projectName}:${title}`,
|
|
44
|
+
};
|
|
45
|
+
attachFiles(path.join(test.id, `retry-${result.retry}`), result, testResult, ortoniConfig, testResult.steps, testResult.errors);
|
|
46
|
+
return testResult;
|
|
47
|
+
}
|
|
48
|
+
processSteps(steps) {
|
|
49
|
+
return steps.map((step) => {
|
|
50
|
+
const stepLocation = step.location
|
|
51
|
+
? `${path.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}`
|
|
52
|
+
: "";
|
|
53
|
+
return {
|
|
54
|
+
snippet: this.ansiToHtml.toHtml(escapeHtml(step.error?.snippet || "")),
|
|
55
|
+
title: step.title,
|
|
56
|
+
location: step.error ? stepLocation : "",
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { OrtoniReportConfig } from "../types/reporterConfig";
|
|
2
|
+
export declare class ServerManager {
|
|
3
|
+
private ortoniConfig;
|
|
4
|
+
constructor(ortoniConfig: OrtoniReportConfig);
|
|
5
|
+
startServer(folderPath: string, outputFilename: string, overAllStatus: string | undefined): Promise<void>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { startReportServer } from "../utils/expressServer";
|
|
2
|
+
export class ServerManager {
|
|
3
|
+
constructor(ortoniConfig) {
|
|
4
|
+
this.ortoniConfig = ortoniConfig;
|
|
5
|
+
}
|
|
6
|
+
async startServer(folderPath, outputFilename, overAllStatus) {
|
|
7
|
+
const openOption = this.ortoniConfig.open || "never";
|
|
8
|
+
const hasFailures = overAllStatus === "failed";
|
|
9
|
+
if (openOption === "always" ||
|
|
10
|
+
(openOption === "on-failure" && hasFailures)) {
|
|
11
|
+
startReportServer(folderPath, outputFilename, this.ortoniConfig.port, openOption);
|
|
12
|
+
await new Promise((_resolve) => { });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the bundled index.html template.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1) require.resolve('<pkgName>/dist/index.html') via require (synchronous; CJS fast path)
|
|
6
|
+
* 2) createRequire(resolve) via dynamic import('module') (async ESM fallback)
|
|
7
|
+
* 3) relative to this file: ../dist/index.html (dev/run-from-repo)
|
|
8
|
+
* 4) node_modules/<pkgName>/dist/index.html from process.cwd()
|
|
9
|
+
* 5) process.cwd()/dist/index.html
|
|
10
|
+
*
|
|
11
|
+
* Returns the template string. Throws a helpful Error if not found.
|
|
12
|
+
*
|
|
13
|
+
* NOTE: This function is async because dynamic import('module') is async in ESM.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readBundledTemplate(pkgName?: string): Promise<string>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/helpers/templateLoader.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
/**
|
|
5
|
+
* Read the bundled index.html template.
|
|
6
|
+
*
|
|
7
|
+
* Resolution order:
|
|
8
|
+
* 1) require.resolve('<pkgName>/dist/index.html') via require (synchronous; CJS fast path)
|
|
9
|
+
* 2) createRequire(resolve) via dynamic import('module') (async ESM fallback)
|
|
10
|
+
* 3) relative to this file: ../dist/index.html (dev/run-from-repo)
|
|
11
|
+
* 4) node_modules/<pkgName>/dist/index.html from process.cwd()
|
|
12
|
+
* 5) process.cwd()/dist/index.html
|
|
13
|
+
*
|
|
14
|
+
* Returns the template string. Throws a helpful Error if not found.
|
|
15
|
+
*
|
|
16
|
+
* NOTE: This function is async because dynamic import('module') is async in ESM.
|
|
17
|
+
*/
|
|
18
|
+
export async function readBundledTemplate(pkgName = "ortoni-report") {
|
|
19
|
+
const packagedRel = "dist/index.html";
|
|
20
|
+
// 1) Sync CJS fast-path: if `require` exists (typical for CJS consumers)
|
|
21
|
+
try {
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
23
|
+
if (typeof require === "function") {
|
|
24
|
+
// Use require.resolve to reliably find the file inside installed package
|
|
25
|
+
const resolved = require.resolve(`${pkgName}/${packagedRel}`);
|
|
26
|
+
if (fs.existsSync(resolved)) {
|
|
27
|
+
return fs.readFileSync(resolved, "utf-8");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// ignore and continue to async fallback
|
|
33
|
+
}
|
|
34
|
+
// 2) Async ESM-safe createRequire via dynamic import (no eval)
|
|
35
|
+
try {
|
|
36
|
+
// dynamic import of 'module' works in ESM environments
|
|
37
|
+
const moduleNS = await import("module");
|
|
38
|
+
if (moduleNS && typeof moduleNS.createRequire === "function") {
|
|
39
|
+
const createRequire = moduleNS.createRequire;
|
|
40
|
+
// createRequire needs a filename/URL; use this file (works for both ESM and CJS)
|
|
41
|
+
const req = createRequire(
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
typeof __filename !== "undefined" ? __filename : import.meta.url);
|
|
44
|
+
const resolved = req.resolve(`${pkgName}/${packagedRel}`);
|
|
45
|
+
if (fs.existsSync(resolved)) {
|
|
46
|
+
return fs.readFileSync(resolved, "utf-8");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// ignore and try file fallbacks below
|
|
52
|
+
}
|
|
53
|
+
// 3) Relative to this module (useful in dev or mono-repo)
|
|
54
|
+
try {
|
|
55
|
+
const here = path.resolve(__dirname, "../dist/index.html");
|
|
56
|
+
if (fs.existsSync(here))
|
|
57
|
+
return fs.readFileSync(here, "utf-8");
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// ignore
|
|
61
|
+
}
|
|
62
|
+
// 4) node_modules lookup from process.cwd()
|
|
63
|
+
try {
|
|
64
|
+
const nm = path.join(process.cwd(), "node_modules", pkgName, packagedRel);
|
|
65
|
+
if (fs.existsSync(nm))
|
|
66
|
+
return fs.readFileSync(nm, "utf-8");
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// ignore
|
|
70
|
+
}
|
|
71
|
+
// 5) fallback to process.cwd()/dist/index.html
|
|
72
|
+
try {
|
|
73
|
+
const alt = path.join(process.cwd(), "dist", "index.html");
|
|
74
|
+
if (fs.existsSync(alt))
|
|
75
|
+
return fs.readFileSync(alt, "utf-8");
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// ignore
|
|
79
|
+
}
|
|
80
|
+
// Helpful error for maintainers / users
|
|
81
|
+
throw new Error(`ortoni-report template not found (tried:\n` +
|
|
82
|
+
` - require.resolve('${pkgName}/${packagedRel}')\n` +
|
|
83
|
+
` - import('module').createRequire(...).resolve('${pkgName}/${packagedRel}')\n` +
|
|
84
|
+
` - relative ../dist/index.html\n` +
|
|
85
|
+
` - ${path.join(process.cwd(), "node_modules", pkgName, packagedRel)}\n` +
|
|
86
|
+
` - ${path.join(process.cwd(), "dist", "index.html")}\n` +
|
|
87
|
+
`Ensure 'dist/index.html' is present in the published package and package.json 'files' includes 'dist/'.`);
|
|
88
|
+
}
|