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
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DatabaseManager,
|
|
4
|
+
FileManager,
|
|
5
|
+
HTMLGenerator,
|
|
6
|
+
startReportServer
|
|
7
|
+
} from "./chunk-4RZ5C7KY.mjs";
|
|
8
|
+
|
|
9
|
+
// src/cli.ts
|
|
10
|
+
import { program } from "commander";
|
|
11
|
+
import * as fs2 from "fs";
|
|
12
|
+
import * as path2 from "path";
|
|
13
|
+
|
|
14
|
+
// src/mergeData.ts
|
|
15
|
+
import * as fs from "fs";
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
async function mergeAllData(options = {}) {
|
|
18
|
+
const folderPath = options.dir || "ortoni-report";
|
|
19
|
+
console.info(`Ortoni Report: Merging shard files in folder: ${folderPath}`);
|
|
20
|
+
if (!fs.existsSync(folderPath)) {
|
|
21
|
+
console.error(`Ortoni Report: folder "${folderPath}" does not exist.`);
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const filenames = fs.readdirSync(folderPath).filter((f) => f.startsWith("ortoni-shard-") && f.endsWith(".json"));
|
|
26
|
+
if (filenames.length === 0) {
|
|
27
|
+
console.error("Ortoni Report: \u274C No shard files found to merge.");
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const shardFileIndex = (name) => {
|
|
32
|
+
const m = name.match(/ortoni-shard-(\d+)-of-(\d+)\.json$/);
|
|
33
|
+
return m ? parseInt(m[1], 10) : null;
|
|
34
|
+
};
|
|
35
|
+
const sortedFiles = filenames.map((f) => ({ f, idx: shardFileIndex(f) })).sort((a, b) => {
|
|
36
|
+
if (a.idx === null && b.idx === null) return a.f.localeCompare(b.f);
|
|
37
|
+
if (a.idx === null) return 1;
|
|
38
|
+
if (b.idx === null) return -1;
|
|
39
|
+
return a.idx - b.idx;
|
|
40
|
+
}).map((x) => x.f);
|
|
41
|
+
const allResults = [];
|
|
42
|
+
const projectSet = /* @__PURE__ */ new Set();
|
|
43
|
+
let totalDurationSum = 0;
|
|
44
|
+
let mergedUserConfig = null;
|
|
45
|
+
let mergedUserMeta = null;
|
|
46
|
+
const badShards = [];
|
|
47
|
+
const shardCounts = {};
|
|
48
|
+
const shardDurMap = {};
|
|
49
|
+
for (const file of sortedFiles) {
|
|
50
|
+
const fullPath = path.join(folderPath, file);
|
|
51
|
+
try {
|
|
52
|
+
const shardRaw = fs.readFileSync(fullPath, "utf-8");
|
|
53
|
+
const shardData = JSON.parse(shardRaw);
|
|
54
|
+
if (!Array.isArray(shardData.results)) {
|
|
55
|
+
console.warn(
|
|
56
|
+
`Ortoni Report: Shard ${file} missing results array \u2014 skipping.`
|
|
57
|
+
);
|
|
58
|
+
badShards.push(file);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
shardData.results.forEach((r) => allResults.push(r));
|
|
62
|
+
shardCounts[file] = shardData.results.length;
|
|
63
|
+
if (Array.isArray(shardData.projectSet)) {
|
|
64
|
+
shardData.projectSet.forEach((p) => projectSet.add(p));
|
|
65
|
+
}
|
|
66
|
+
const rawShardDur = shardData.totalDuration;
|
|
67
|
+
let durToAdd = 0;
|
|
68
|
+
let perTestSum = 0;
|
|
69
|
+
if (typeof rawShardDur === "number") {
|
|
70
|
+
durToAdd = rawShardDur;
|
|
71
|
+
} else {
|
|
72
|
+
perTestSum = Array.isArray(shardData.results) ? shardData.results.reduce(
|
|
73
|
+
(acc, t) => acc + (Number(t?.duration) || 0),
|
|
74
|
+
0
|
|
75
|
+
) : 0;
|
|
76
|
+
durToAdd = perTestSum;
|
|
77
|
+
}
|
|
78
|
+
totalDurationSum += durToAdd;
|
|
79
|
+
shardDurMap[file] = durToAdd;
|
|
80
|
+
if (shardData.userConfig) {
|
|
81
|
+
if (!mergedUserConfig) mergedUserConfig = { ...shardData.userConfig };
|
|
82
|
+
else {
|
|
83
|
+
Object.keys(shardData.userConfig).forEach((k) => {
|
|
84
|
+
if (mergedUserConfig[k] === void 0 || mergedUserConfig[k] === null || mergedUserConfig[k] === "") {
|
|
85
|
+
mergedUserConfig[k] = shardData.userConfig[k];
|
|
86
|
+
} else if (shardData.userConfig[k] !== mergedUserConfig[k]) {
|
|
87
|
+
console.warn(
|
|
88
|
+
`Ortoni Report: userConfig mismatch for key "${k}" between shards. Using first value "${mergedUserConfig[k]}".`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (shardData.userMeta) {
|
|
95
|
+
if (!mergedUserMeta) mergedUserMeta = { ...shardData.userMeta };
|
|
96
|
+
else {
|
|
97
|
+
mergedUserMeta.meta = {
|
|
98
|
+
...mergedUserMeta.meta || {},
|
|
99
|
+
...shardData.userMeta.meta || {}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error(`Ortoni Report: Failed to parse shard ${file}:`, err);
|
|
105
|
+
badShards.push(file);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (badShards.length > 0) {
|
|
110
|
+
console.warn(
|
|
111
|
+
`Ortoni Report: Completed merge with ${badShards.length} bad shard(s) skipped:`,
|
|
112
|
+
badShards
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const totalDuration = totalDurationSum;
|
|
116
|
+
const saveHistoryFromOptions = typeof options.saveHistory === "boolean" ? options.saveHistory : void 0;
|
|
117
|
+
const saveHistoryFromShard = mergedUserConfig && typeof mergedUserConfig.saveHistory === "boolean" ? mergedUserConfig.saveHistory : void 0;
|
|
118
|
+
const saveHistory = saveHistoryFromOptions ?? saveHistoryFromShard ?? true;
|
|
119
|
+
let dbManager;
|
|
120
|
+
let runId;
|
|
121
|
+
if (saveHistory) {
|
|
122
|
+
try {
|
|
123
|
+
dbManager = new DatabaseManager();
|
|
124
|
+
const dbPath = path.join(folderPath, "ortoni-data-history.sqlite");
|
|
125
|
+
await dbManager.initialize(dbPath);
|
|
126
|
+
runId = await dbManager.saveTestRun();
|
|
127
|
+
if (typeof runId === "number") {
|
|
128
|
+
await dbManager.saveTestResults(runId, allResults);
|
|
129
|
+
console.info(
|
|
130
|
+
`Ortoni Report: Saved ${allResults.length} results to DB (runId=${runId}).`
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
console.warn(
|
|
134
|
+
"Ortoni Report: Failed to create test run in DB; proceeding without saving results."
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(
|
|
139
|
+
"Ortoni Report: Error while saving history to DB. Proceeding without DB:",
|
|
140
|
+
err
|
|
141
|
+
);
|
|
142
|
+
dbManager = void 0;
|
|
143
|
+
runId = void 0;
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
console.info(
|
|
147
|
+
"Ortoni Report: Skipping history save (saveHistory=false). (Typical for CI runs)"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const htmlGenerator = new HTMLGenerator(
|
|
151
|
+
{ ...mergedUserConfig || {}, meta: mergedUserMeta?.meta },
|
|
152
|
+
dbManager
|
|
153
|
+
);
|
|
154
|
+
const finalReportData = await htmlGenerator.generateFinalReport(
|
|
155
|
+
// filteredResults: typically filter out skipped for display (keeps existing behavior)
|
|
156
|
+
allResults.filter((r) => r.status !== "skipped"),
|
|
157
|
+
totalDuration,
|
|
158
|
+
allResults,
|
|
159
|
+
projectSet
|
|
160
|
+
// pass Set<string> as original generateFinalReport expects
|
|
161
|
+
);
|
|
162
|
+
const fileManager = new FileManager(folderPath);
|
|
163
|
+
const outputFileName = options.file || "ortoni-report.html";
|
|
164
|
+
const outputPath = fileManager.writeReportFile(
|
|
165
|
+
outputFileName,
|
|
166
|
+
finalReportData
|
|
167
|
+
);
|
|
168
|
+
console.log(`\u2705 Final merged report generated at ${await outputPath}`);
|
|
169
|
+
console.log(`\u2705 Shards merged: ${sortedFiles.length}`);
|
|
170
|
+
console.log(`\u2705 Tests per shard:`, shardCounts);
|
|
171
|
+
console.log(`\u2705 Total tests merged ${allResults.length}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/cli.ts
|
|
175
|
+
program.version("4.0.1").description("Ortoni Report - CLI");
|
|
176
|
+
program.command("show-report").description("Open Ortoni Report").option(
|
|
177
|
+
"-d, --dir <path>",
|
|
178
|
+
"Path to the folder containing the report",
|
|
179
|
+
"ortoni-report"
|
|
180
|
+
).option(
|
|
181
|
+
"-f, --file <filename>",
|
|
182
|
+
"Name of the report file",
|
|
183
|
+
"ortoni-report.html"
|
|
184
|
+
).option("-p, --port <port>", "Port to run the server", "2004").action((options) => {
|
|
185
|
+
const projectRoot = process.cwd();
|
|
186
|
+
const folderPath = path2.resolve(projectRoot, options.dir);
|
|
187
|
+
const filePath = path2.resolve(folderPath, options.file);
|
|
188
|
+
const port = parseInt(options.port) || 2004;
|
|
189
|
+
if (!fs2.existsSync(filePath)) {
|
|
190
|
+
console.error(
|
|
191
|
+
`\u274C Error: The file "${filePath}" does not exist in "${folderPath}".`
|
|
192
|
+
);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
startReportServer(folderPath, path2.basename(filePath), port, "always");
|
|
196
|
+
});
|
|
197
|
+
program.command("merge-report").description("Merge sharded reports into one final report").option(
|
|
198
|
+
"-d, --dir <path>",
|
|
199
|
+
"Path to the folder containing shard files",
|
|
200
|
+
"ortoni-report"
|
|
201
|
+
).option("-f, --file <filename>", "Output report file", "ortoni-report.html").action(async (options) => {
|
|
202
|
+
await mergeAllData({
|
|
203
|
+
dir: options.dir,
|
|
204
|
+
file: options.file,
|
|
205
|
+
saveHistory: false
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { TestResultData } from "../types/testResults";
|
|
2
|
+
import { OrtoniReportConfig } from "../types/reporterConfig";
|
|
3
|
+
import { DatabaseManager } from "./databaseManager";
|
|
4
|
+
export declare class HTMLGenerator {
|
|
5
|
+
private ortoniConfig;
|
|
6
|
+
private dbManager?;
|
|
7
|
+
constructor(ortoniConfig: OrtoniReportConfig, dbManager?: DatabaseManager);
|
|
8
|
+
generateFinalReport(filteredResults: TestResultData[], totalDuration: number, results: TestResultData[], projectSet: Set<string>): Promise<{
|
|
9
|
+
summary: {
|
|
10
|
+
overAllResult: {
|
|
11
|
+
pass: number;
|
|
12
|
+
fail: number;
|
|
13
|
+
skip: number;
|
|
14
|
+
retry: number;
|
|
15
|
+
flaky: number;
|
|
16
|
+
total: number;
|
|
17
|
+
};
|
|
18
|
+
successRate: string;
|
|
19
|
+
lastRunDate: string;
|
|
20
|
+
totalDuration: number;
|
|
21
|
+
stats: {
|
|
22
|
+
projectNames: string[];
|
|
23
|
+
totalTests: number[];
|
|
24
|
+
passedTests: number[];
|
|
25
|
+
failedTests: number[];
|
|
26
|
+
skippedTests: number[];
|
|
27
|
+
retryTests: number[];
|
|
28
|
+
flakyTests: number[];
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
testResult: {
|
|
32
|
+
tests: any;
|
|
33
|
+
testHistories: {
|
|
34
|
+
testId: string;
|
|
35
|
+
history: any[];
|
|
36
|
+
}[];
|
|
37
|
+
allTags: string[];
|
|
38
|
+
set: Set<string>;
|
|
39
|
+
};
|
|
40
|
+
userConfig: {
|
|
41
|
+
projectName: string;
|
|
42
|
+
authorName: string;
|
|
43
|
+
type: string;
|
|
44
|
+
title: string;
|
|
45
|
+
};
|
|
46
|
+
userMeta: {
|
|
47
|
+
meta: Record<string, string>;
|
|
48
|
+
};
|
|
49
|
+
preferences: {
|
|
50
|
+
logo: string;
|
|
51
|
+
showProject: boolean;
|
|
52
|
+
};
|
|
53
|
+
analytics: {
|
|
54
|
+
reportData: {
|
|
55
|
+
summary: {};
|
|
56
|
+
trends: {};
|
|
57
|
+
flakyTests: any[];
|
|
58
|
+
slowTests: any[];
|
|
59
|
+
note: string;
|
|
60
|
+
} | {
|
|
61
|
+
summary: {};
|
|
62
|
+
trends: {};
|
|
63
|
+
flakyTests: any[];
|
|
64
|
+
slowTests: any[];
|
|
65
|
+
note?: undefined;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
}>;
|
|
69
|
+
/**
|
|
70
|
+
* Return safe analytics/report data.
|
|
71
|
+
* If no dbManager is provided, return empty defaults and a note explaining why.
|
|
72
|
+
*/
|
|
73
|
+
getReportData(): Promise<{
|
|
74
|
+
summary: {};
|
|
75
|
+
trends: {};
|
|
76
|
+
flakyTests: any[];
|
|
77
|
+
slowTests: any[];
|
|
78
|
+
note: string;
|
|
79
|
+
} | {
|
|
80
|
+
summary: {};
|
|
81
|
+
trends: {};
|
|
82
|
+
flakyTests: any[];
|
|
83
|
+
slowTests: any[];
|
|
84
|
+
note?: undefined;
|
|
85
|
+
}>;
|
|
86
|
+
private prepareReportData;
|
|
87
|
+
private calculateProjectResults;
|
|
88
|
+
private extractProjectStats;
|
|
89
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { groupResults } from "../utils/groupProjects";
|
|
2
|
+
export class HTMLGenerator {
|
|
3
|
+
constructor(ortoniConfig, dbManager) {
|
|
4
|
+
this.ortoniConfig = ortoniConfig;
|
|
5
|
+
this.dbManager = dbManager; // may be undefined in CI or when saveHistory=false
|
|
6
|
+
}
|
|
7
|
+
async generateFinalReport(filteredResults, totalDuration, results, projectSet) {
|
|
8
|
+
const data = await this.prepareReportData(filteredResults, totalDuration, results, projectSet);
|
|
9
|
+
return data;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Return safe analytics/report data.
|
|
13
|
+
* If no dbManager is provided, return empty defaults and a note explaining why.
|
|
14
|
+
*/
|
|
15
|
+
async getReportData() {
|
|
16
|
+
if (!this.dbManager) {
|
|
17
|
+
return {
|
|
18
|
+
summary: {},
|
|
19
|
+
trends: {},
|
|
20
|
+
flakyTests: [],
|
|
21
|
+
slowTests: [],
|
|
22
|
+
note: "Test history/trends are unavailable (saveHistory disabled or DB not initialized).",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const [summary, trends, flakyTests, slowTests] = await Promise.all([
|
|
27
|
+
this.dbManager.getSummaryData
|
|
28
|
+
? this.dbManager.getSummaryData()
|
|
29
|
+
: Promise.resolve({}),
|
|
30
|
+
this.dbManager.getTrends
|
|
31
|
+
? this.dbManager.getTrends()
|
|
32
|
+
: Promise.resolve({}),
|
|
33
|
+
this.dbManager.getFlakyTests
|
|
34
|
+
? this.dbManager.getFlakyTests()
|
|
35
|
+
: Promise.resolve([]),
|
|
36
|
+
this.dbManager.getSlowTests
|
|
37
|
+
? this.dbManager.getSlowTests()
|
|
38
|
+
: Promise.resolve([]),
|
|
39
|
+
]);
|
|
40
|
+
return {
|
|
41
|
+
summary: summary ?? {},
|
|
42
|
+
trends: trends ?? {},
|
|
43
|
+
flakyTests: flakyTests ?? [],
|
|
44
|
+
slowTests: slowTests ?? [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
console.warn("HTMLGenerator: failed to read analytics from DB, continuing without history.", err);
|
|
49
|
+
return {
|
|
50
|
+
summary: {},
|
|
51
|
+
trends: {},
|
|
52
|
+
flakyTests: [],
|
|
53
|
+
slowTests: [],
|
|
54
|
+
note: "Test history/trends could not be loaded due to a DB error.",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async prepareReportData(filteredResults, totalDuration, results, projectSet) {
|
|
59
|
+
const totalTests = filteredResults.length;
|
|
60
|
+
const passedTests = results.filter((r) => r.status === "passed").length;
|
|
61
|
+
const flakyTests = results.filter((r) => r.status === "flaky").length;
|
|
62
|
+
const failed = filteredResults.filter((r) => r.status === "failed" || r.status === "timedOut").length;
|
|
63
|
+
const successRate = totalTests === 0
|
|
64
|
+
? "0.00"
|
|
65
|
+
: (((passedTests + flakyTests) / totalTests) * 100).toFixed(2);
|
|
66
|
+
const allTags = new Set();
|
|
67
|
+
results.forEach((result) => (result.testTags || []).forEach((tag) => allTags.add(tag)));
|
|
68
|
+
const projectResults = this.calculateProjectResults(filteredResults, results, projectSet);
|
|
69
|
+
const lastRunDate = new Date().toLocaleString();
|
|
70
|
+
// Fetch per-test histories only if DB manager exists; otherwise return empty history arrays.
|
|
71
|
+
const testHistories = await Promise.all(results.map(async (result) => {
|
|
72
|
+
const testId = `${result.filePath}:${result.projectName}:${result.title}`;
|
|
73
|
+
if (!this.dbManager || !this.dbManager.getTestHistory) {
|
|
74
|
+
return {
|
|
75
|
+
testId,
|
|
76
|
+
history: [],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const history = await this.dbManager.getTestHistory(testId);
|
|
81
|
+
return {
|
|
82
|
+
testId,
|
|
83
|
+
history: history ?? [],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
// If a single test history fails, return empty history for that test and continue
|
|
88
|
+
console.warn(`HTMLGenerator: failed to read history for ${testId}`, err);
|
|
89
|
+
return {
|
|
90
|
+
testId,
|
|
91
|
+
history: [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}));
|
|
95
|
+
// Fetch analytics/reportData using the safe getter (this will handle missing DB)
|
|
96
|
+
const reportData = await this.getReportData();
|
|
97
|
+
return {
|
|
98
|
+
summary: {
|
|
99
|
+
overAllResult: {
|
|
100
|
+
pass: passedTests,
|
|
101
|
+
fail: failed,
|
|
102
|
+
skip: results.filter((r) => r.status === "skipped").length,
|
|
103
|
+
retry: results.filter((r) => r.retryAttemptCount).length,
|
|
104
|
+
flaky: flakyTests,
|
|
105
|
+
total: filteredResults.length,
|
|
106
|
+
},
|
|
107
|
+
successRate,
|
|
108
|
+
lastRunDate,
|
|
109
|
+
totalDuration,
|
|
110
|
+
stats: this.extractProjectStats(projectResults),
|
|
111
|
+
},
|
|
112
|
+
testResult: {
|
|
113
|
+
tests: groupResults(this.ortoniConfig, results),
|
|
114
|
+
testHistories,
|
|
115
|
+
allTags: Array.from(allTags),
|
|
116
|
+
set: projectSet,
|
|
117
|
+
},
|
|
118
|
+
userConfig: {
|
|
119
|
+
projectName: this.ortoniConfig.projectName,
|
|
120
|
+
authorName: this.ortoniConfig.authorName,
|
|
121
|
+
type: this.ortoniConfig.testType,
|
|
122
|
+
title: this.ortoniConfig.title,
|
|
123
|
+
},
|
|
124
|
+
userMeta: {
|
|
125
|
+
meta: this.ortoniConfig.meta,
|
|
126
|
+
},
|
|
127
|
+
preferences: {
|
|
128
|
+
logo: this.ortoniConfig.logo || undefined,
|
|
129
|
+
showProject: this.ortoniConfig.showProject || false,
|
|
130
|
+
},
|
|
131
|
+
analytics: {
|
|
132
|
+
reportData: reportData,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
calculateProjectResults(filteredResults, results, projectSet) {
|
|
137
|
+
return Array.from(projectSet).map((projectName) => {
|
|
138
|
+
const projectTests = filteredResults.filter((r) => r.projectName === projectName);
|
|
139
|
+
const allProjectTests = results.filter((r) => r.projectName === projectName);
|
|
140
|
+
return {
|
|
141
|
+
projectName,
|
|
142
|
+
passedTests: projectTests.filter((r) => r.status === "passed").length,
|
|
143
|
+
failedTests: projectTests.filter((r) => r.status === "failed" || r.status === "timedOut").length,
|
|
144
|
+
skippedTests: allProjectTests.filter((r) => r.status === "skipped")
|
|
145
|
+
.length,
|
|
146
|
+
retryTests: allProjectTests.filter((r) => r.retryAttemptCount).length,
|
|
147
|
+
flakyTests: allProjectTests.filter((r) => r.status === "flaky").length,
|
|
148
|
+
totalTests: projectTests.length,
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
extractProjectStats(projectResults) {
|
|
153
|
+
return {
|
|
154
|
+
projectNames: projectResults.map((result) => result.projectName),
|
|
155
|
+
totalTests: projectResults.map((result) => result.totalTests),
|
|
156
|
+
passedTests: projectResults.map((result) => result.passedTests),
|
|
157
|
+
failedTests: projectResults.map((result) => result.failedTests),
|
|
158
|
+
skippedTests: projectResults.map((result) => result.skippedTests),
|
|
159
|
+
retryTests: projectResults.map((result) => result.retryTests),
|
|
160
|
+
flakyTests: projectResults.map((result) => result.flakyTests),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { TestResultData } from "../types/testResults";
|
|
2
|
+
export declare class DatabaseManager {
|
|
3
|
+
private db;
|
|
4
|
+
initialize(dbPath: string): Promise<void>;
|
|
5
|
+
private createTables;
|
|
6
|
+
private createIndexes;
|
|
7
|
+
saveTestRun(): Promise<number | null>;
|
|
8
|
+
saveTestResults(runId: number, results: TestResultData[]): Promise<void>;
|
|
9
|
+
getTestHistory(testId: string, limit?: number): Promise<any[]>;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
getSummaryData(): Promise<{
|
|
12
|
+
totalRuns: number;
|
|
13
|
+
totalTests: number;
|
|
14
|
+
passed: number;
|
|
15
|
+
failed: number;
|
|
16
|
+
passRate: number;
|
|
17
|
+
avgDuration: number;
|
|
18
|
+
}>;
|
|
19
|
+
getTrends(limit?: number): Promise<{
|
|
20
|
+
run_date: string;
|
|
21
|
+
passed: number;
|
|
22
|
+
failed: number;
|
|
23
|
+
avg_duration: number;
|
|
24
|
+
}[]>;
|
|
25
|
+
getFlakyTests(limit?: number): Promise<{
|
|
26
|
+
test_id: string;
|
|
27
|
+
total: number;
|
|
28
|
+
flaky: number;
|
|
29
|
+
avg_duration: number;
|
|
30
|
+
}[]>;
|
|
31
|
+
getSlowTests(limit?: number): Promise<{
|
|
32
|
+
test_id: string;
|
|
33
|
+
avg_duration: number;
|
|
34
|
+
}[]>;
|
|
35
|
+
}
|