ortoni-report 4.0.2 → 4.0.4
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 +10 -0
- package/dist/chunk-L6VOLEP2.mjs +752 -0
- package/dist/cli.js +27 -21
- package/dist/cli.mjs +1 -1
- package/dist/helpers/HTMLGenerator.d.ts +89 -0
- package/dist/helpers/HTMLGenerator.js +164 -0
- package/dist/helpers/databaseManager.d.ts +35 -0
- package/dist/helpers/databaseManager.js +267 -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 +2 -2
- package/dist/mergeData.d.ts +13 -0
- package/dist/mergeData.js +182 -0
- package/dist/ortoni-report.js +72 -64
- package/dist/ortoni-report.mjs +2 -2
- package/dist/types/reporterConfig.d.ts +86 -0
- 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 +93 -0
- package/package.json +1 -1
- package/readme.md +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge shard JSON files into a single report while keeping ALL test results.
|
|
3
|
+
*
|
|
4
|
+
* options:
|
|
5
|
+
* - dir?: folder where shard files exist (default: "ortoni-report")
|
|
6
|
+
* - file?: output file name for final HTML (default: "ortoni-report.html")
|
|
7
|
+
* - saveHistory?: boolean | undefined -> if provided, overrides shard/userConfig
|
|
8
|
+
*/
|
|
9
|
+
export declare function mergeAllData(options?: {
|
|
10
|
+
dir?: string;
|
|
11
|
+
file?: string;
|
|
12
|
+
saveHistory?: boolean;
|
|
13
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// src/mergeAllData.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { DatabaseManager } from "./helpers/databaseManager";
|
|
5
|
+
import { HTMLGenerator } from "./helpers/HTMLGenerator";
|
|
6
|
+
import { FileManager } from "./helpers/fileManager";
|
|
7
|
+
/**
|
|
8
|
+
* Merge shard JSON files into a single report while keeping ALL test results.
|
|
9
|
+
*
|
|
10
|
+
* options:
|
|
11
|
+
* - dir?: folder where shard files exist (default: "ortoni-report")
|
|
12
|
+
* - file?: output file name for final HTML (default: "ortoni-report.html")
|
|
13
|
+
* - saveHistory?: boolean | undefined -> if provided, overrides shard/userConfig
|
|
14
|
+
*/
|
|
15
|
+
export async function mergeAllData(options = {}) {
|
|
16
|
+
const folderPath = options.dir || "ortoni-report";
|
|
17
|
+
console.info(`Ortoni Report: Merging shard files in folder: ${folderPath}`);
|
|
18
|
+
if (!fs.existsSync(folderPath)) {
|
|
19
|
+
console.error(`Ortoni Report: folder "${folderPath}" does not exist.`);
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const filenames = fs
|
|
24
|
+
.readdirSync(folderPath)
|
|
25
|
+
.filter((f) => f.startsWith("ortoni-shard-") && f.endsWith(".json"));
|
|
26
|
+
if (filenames.length === 0) {
|
|
27
|
+
console.error("Ortoni Report: ❌ No shard files found to merge.");
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// deterministic sort by numeric shard index if available (ortoni-shard-<current>-of-<total>.json)
|
|
32
|
+
const shardFileIndex = (name) => {
|
|
33
|
+
const m = name.match(/ortoni-shard-(\d+)-of-(\d+)\.json$/);
|
|
34
|
+
return m ? parseInt(m[1], 10) : null;
|
|
35
|
+
};
|
|
36
|
+
const sortedFiles = filenames
|
|
37
|
+
.map((f) => ({ f, idx: shardFileIndex(f) }))
|
|
38
|
+
.sort((a, b) => {
|
|
39
|
+
if (a.idx === null && b.idx === null)
|
|
40
|
+
return a.f.localeCompare(b.f);
|
|
41
|
+
if (a.idx === null)
|
|
42
|
+
return 1;
|
|
43
|
+
if (b.idx === null)
|
|
44
|
+
return -1;
|
|
45
|
+
return a.idx - b.idx;
|
|
46
|
+
})
|
|
47
|
+
.map((x) => x.f);
|
|
48
|
+
// Merge state
|
|
49
|
+
const allResults = []; // keep every test entry from every shard
|
|
50
|
+
const projectSet = new Set();
|
|
51
|
+
let totalDurationSum = 0; // sum of shard durations (ms)
|
|
52
|
+
let mergedUserConfig = null;
|
|
53
|
+
let mergedUserMeta = null;
|
|
54
|
+
const badShards = [];
|
|
55
|
+
const shardCounts = {};
|
|
56
|
+
const shardDurMap = {}; // used duration per shard (ms)
|
|
57
|
+
for (const file of sortedFiles) {
|
|
58
|
+
const fullPath = path.join(folderPath, file);
|
|
59
|
+
try {
|
|
60
|
+
const shardRaw = fs.readFileSync(fullPath, "utf-8");
|
|
61
|
+
const shardData = JSON.parse(shardRaw);
|
|
62
|
+
// Validate results array
|
|
63
|
+
if (!Array.isArray(shardData.results)) {
|
|
64
|
+
console.warn(`Ortoni Report: Shard ${file} missing results array — skipping.`);
|
|
65
|
+
badShards.push(file);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Append all results (keep duplicates)
|
|
69
|
+
shardData.results.forEach((r) => allResults.push(r));
|
|
70
|
+
shardCounts[file] = shardData.results.length;
|
|
71
|
+
// Merge project sets
|
|
72
|
+
if (Array.isArray(shardData.projectSet)) {
|
|
73
|
+
shardData.projectSet.forEach((p) => projectSet.add(p));
|
|
74
|
+
}
|
|
75
|
+
// Duration handling:
|
|
76
|
+
// Accept shardData.totalDuration when it is a number (including 0).
|
|
77
|
+
// Fallback: if shardData.totalDuration is missing or not a number, sum per-test durations inside the shard.
|
|
78
|
+
const rawShardDur = shardData.totalDuration;
|
|
79
|
+
let durToAdd = 0;
|
|
80
|
+
let perTestSum = 0;
|
|
81
|
+
if (typeof rawShardDur === "number") {
|
|
82
|
+
// Accept numeric durations (including 0)
|
|
83
|
+
durToAdd = rawShardDur;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// fallback: sum per-test durations (coerce to Number)
|
|
87
|
+
perTestSum = Array.isArray(shardData.results)
|
|
88
|
+
? shardData.results.reduce((acc, t) => acc + (Number(t?.duration) || 0), 0)
|
|
89
|
+
: 0;
|
|
90
|
+
durToAdd = perTestSum;
|
|
91
|
+
}
|
|
92
|
+
// accumulate
|
|
93
|
+
totalDurationSum += durToAdd;
|
|
94
|
+
shardDurMap[file] = durToAdd;
|
|
95
|
+
// Merge userConfig/userMeta conservatively (prefer first non-empty value)
|
|
96
|
+
if (shardData.userConfig) {
|
|
97
|
+
if (!mergedUserConfig)
|
|
98
|
+
mergedUserConfig = { ...shardData.userConfig };
|
|
99
|
+
else {
|
|
100
|
+
Object.keys(shardData.userConfig).forEach((k) => {
|
|
101
|
+
if (mergedUserConfig[k] === undefined ||
|
|
102
|
+
mergedUserConfig[k] === null ||
|
|
103
|
+
mergedUserConfig[k] === "") {
|
|
104
|
+
mergedUserConfig[k] = shardData.userConfig[k];
|
|
105
|
+
}
|
|
106
|
+
else if (shardData.userConfig[k] !== mergedUserConfig[k]) {
|
|
107
|
+
console.warn(`Ortoni Report: userConfig mismatch for key "${k}" between shards. Using first value "${mergedUserConfig[k]}".`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (shardData.userMeta) {
|
|
113
|
+
if (!mergedUserMeta)
|
|
114
|
+
mergedUserMeta = { ...shardData.userMeta };
|
|
115
|
+
else {
|
|
116
|
+
mergedUserMeta.meta = {
|
|
117
|
+
...(mergedUserMeta.meta || {}),
|
|
118
|
+
...(shardData.userMeta.meta || {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
console.error(`Ortoni Report: Failed to parse shard ${file}:`, err);
|
|
125
|
+
badShards.push(file);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
} // end for each shard
|
|
129
|
+
if (badShards.length > 0) {
|
|
130
|
+
console.warn(`Ortoni Report: Completed merge with ${badShards.length} bad shard(s) skipped:`, badShards);
|
|
131
|
+
}
|
|
132
|
+
// final results preserved with duplicates
|
|
133
|
+
const totalDuration = totalDurationSum; // in ms
|
|
134
|
+
// Determine whether to persist history:
|
|
135
|
+
// Priority: explicit options.saveHistory -> mergedUserConfig.saveHistory -> default true
|
|
136
|
+
const saveHistoryFromOptions = typeof options.saveHistory === "boolean" ? options.saveHistory : undefined;
|
|
137
|
+
const saveHistoryFromShard = mergedUserConfig && typeof mergedUserConfig.saveHistory === "boolean"
|
|
138
|
+
? mergedUserConfig.saveHistory
|
|
139
|
+
: undefined;
|
|
140
|
+
const saveHistory = saveHistoryFromOptions ?? saveHistoryFromShard ?? true;
|
|
141
|
+
let dbManager;
|
|
142
|
+
let runId;
|
|
143
|
+
if (saveHistory) {
|
|
144
|
+
try {
|
|
145
|
+
dbManager = new DatabaseManager();
|
|
146
|
+
const dbPath = path.join(folderPath, "ortoni-data-history.sqlite");
|
|
147
|
+
await dbManager.initialize(dbPath);
|
|
148
|
+
runId = await dbManager.saveTestRun();
|
|
149
|
+
if (typeof runId === "number") {
|
|
150
|
+
// Save all results (your saveTestResults may batch internally)
|
|
151
|
+
await dbManager.saveTestResults(runId, allResults);
|
|
152
|
+
console.info(`Ortoni Report: Saved ${allResults.length} results to DB (runId=${runId}).`);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
console.warn("Ortoni Report: Failed to create test run in DB; proceeding without saving results.");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
console.error("Ortoni Report: Error while saving history to DB. Proceeding without DB:", err);
|
|
160
|
+
dbManager = undefined;
|
|
161
|
+
runId = undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
console.info("Ortoni Report: Skipping history save (saveHistory=false). (Typical for CI runs)");
|
|
166
|
+
}
|
|
167
|
+
// Generate final report
|
|
168
|
+
const htmlGenerator = new HTMLGenerator({ ...(mergedUserConfig || {}), meta: mergedUserMeta?.meta }, dbManager);
|
|
169
|
+
const finalReportData = await htmlGenerator.generateFinalReport(
|
|
170
|
+
// filteredResults: typically filter out skipped for display (keeps existing behavior)
|
|
171
|
+
allResults.filter((r) => r.status !== "skipped"), totalDuration, allResults, projectSet // pass Set<string> as original generateFinalReport expects
|
|
172
|
+
);
|
|
173
|
+
// Write final HTML file
|
|
174
|
+
const fileManager = new FileManager(folderPath);
|
|
175
|
+
const outputFileName = options.file || "ortoni-report.html";
|
|
176
|
+
const outputPath = fileManager.writeReportFile(outputFileName, finalReportData);
|
|
177
|
+
// Logs & debugging summary
|
|
178
|
+
console.log(`✅ Final merged report generated at ${await outputPath}`);
|
|
179
|
+
console.log(`✅ Shards merged: ${sortedFiles.length}`);
|
|
180
|
+
console.log(`✅ Tests per shard:`, shardCounts);
|
|
181
|
+
console.log(`✅ Total tests merged ${allResults.length}`);
|
|
182
|
+
}
|
package/dist/ortoni-report.js
CHANGED
|
@@ -195,6 +195,67 @@ function groupResults(config, results) {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// src/utils/utils.ts
|
|
199
|
+
var import_path3 = __toESM(require("path"));
|
|
200
|
+
function normalizeFilePath(filePath) {
|
|
201
|
+
const normalizedPath = import_path3.default.normalize(filePath);
|
|
202
|
+
return import_path3.default.basename(normalizedPath);
|
|
203
|
+
}
|
|
204
|
+
function ensureHtmlExtension(filename) {
|
|
205
|
+
const ext = import_path3.default.extname(filename);
|
|
206
|
+
if (ext && ext.toLowerCase() === ".html") {
|
|
207
|
+
return filename;
|
|
208
|
+
}
|
|
209
|
+
return `${filename}.html`;
|
|
210
|
+
}
|
|
211
|
+
function escapeHtml(unsafe) {
|
|
212
|
+
if (typeof unsafe !== "string") {
|
|
213
|
+
return String(unsafe);
|
|
214
|
+
}
|
|
215
|
+
return unsafe.replace(/[&<"']/g, function(match) {
|
|
216
|
+
const escapeMap = {
|
|
217
|
+
"&": "&",
|
|
218
|
+
"<": "<",
|
|
219
|
+
">": ">",
|
|
220
|
+
'"': """,
|
|
221
|
+
"'": "'"
|
|
222
|
+
};
|
|
223
|
+
return escapeMap[match] || match;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function formatDateLocal(dateInput) {
|
|
227
|
+
if (!dateInput) return "N/A";
|
|
228
|
+
try {
|
|
229
|
+
const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
|
|
230
|
+
if (isNaN(date.getTime())) {
|
|
231
|
+
return "N/A";
|
|
232
|
+
}
|
|
233
|
+
const options = {
|
|
234
|
+
year: "numeric",
|
|
235
|
+
month: "short",
|
|
236
|
+
day: "2-digit",
|
|
237
|
+
hour: "2-digit",
|
|
238
|
+
minute: "2-digit",
|
|
239
|
+
hour12: true
|
|
240
|
+
};
|
|
241
|
+
return new Intl.DateTimeFormat("en-US", options).format(date);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
return "N/A";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function extractSuites(titlePath) {
|
|
247
|
+
const tagPattern = /@[\w]+/g;
|
|
248
|
+
const suiteParts = titlePath.slice(3, titlePath.length - 1).map((p) => p.replace(tagPattern, "").trim());
|
|
249
|
+
return {
|
|
250
|
+
hierarchy: suiteParts.join(" > "),
|
|
251
|
+
// full hierarchy
|
|
252
|
+
topLevelSuite: suiteParts[0] ?? "",
|
|
253
|
+
// first suite
|
|
254
|
+
parentSuite: suiteParts[suiteParts.length - 1] ?? ""
|
|
255
|
+
// last suite
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
198
259
|
// src/helpers/HTMLGenerator.ts
|
|
199
260
|
var HTMLGenerator = class {
|
|
200
261
|
constructor(ortoniConfig, dbManager) {
|
|
@@ -268,7 +329,7 @@ var HTMLGenerator = class {
|
|
|
268
329
|
results,
|
|
269
330
|
projectSet
|
|
270
331
|
);
|
|
271
|
-
const lastRunDate = (/* @__PURE__ */ new Date())
|
|
332
|
+
const lastRunDate = formatDateLocal(/* @__PURE__ */ new Date());
|
|
272
333
|
const testHistories = await Promise.all(
|
|
273
334
|
results.map(async (result) => {
|
|
274
335
|
const testId = `${result.filePath}:${result.projectName}:${result.title}`;
|
|
@@ -375,7 +436,7 @@ var import_ansi_to_html = __toESM(require("ansi-to-html"));
|
|
|
375
436
|
var import_path5 = __toESM(require("path"));
|
|
376
437
|
|
|
377
438
|
// src/utils/attachFiles.ts
|
|
378
|
-
var
|
|
439
|
+
var import_path4 = __toESM(require("path"));
|
|
379
440
|
var import_fs4 = __toESM(require("fs"));
|
|
380
441
|
|
|
381
442
|
// src/helpers/markdownConverter.ts
|
|
@@ -1518,7 +1579,7 @@ function convertMarkdownToHtml(markdownPath, htmlOutputPath) {
|
|
|
1518
1579
|
// src/utils/attachFiles.ts
|
|
1519
1580
|
function attachFiles(subFolder, result, testResult, config, steps, errors) {
|
|
1520
1581
|
const folderPath = config.folderPath || "ortoni-report";
|
|
1521
|
-
const attachmentsFolder =
|
|
1582
|
+
const attachmentsFolder = import_path4.default.join(
|
|
1522
1583
|
folderPath,
|
|
1523
1584
|
"ortoni-data",
|
|
1524
1585
|
"attachments",
|
|
@@ -1534,14 +1595,14 @@ function attachFiles(subFolder, result, testResult, config, steps, errors) {
|
|
|
1534
1595
|
result.attachments.forEach((attachment) => {
|
|
1535
1596
|
const { contentType, name, path: attachmentPath, body } = attachment;
|
|
1536
1597
|
if (!attachmentPath && !body) return;
|
|
1537
|
-
const fileName = attachmentPath ?
|
|
1538
|
-
const relativePath =
|
|
1598
|
+
const fileName = attachmentPath ? import_path4.default.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
|
|
1599
|
+
const relativePath = import_path4.default.join(
|
|
1539
1600
|
"ortoni-data",
|
|
1540
1601
|
"attachments",
|
|
1541
1602
|
subFolder,
|
|
1542
1603
|
fileName
|
|
1543
1604
|
);
|
|
1544
|
-
const fullPath =
|
|
1605
|
+
const fullPath = import_path4.default.join(attachmentsFolder, fileName);
|
|
1545
1606
|
if (contentType === "image/png") {
|
|
1546
1607
|
handleImage(
|
|
1547
1608
|
attachmentPath,
|
|
@@ -1632,61 +1693,6 @@ function getFileExtension(contentType) {
|
|
|
1632
1693
|
return extensions[contentType] || "unknown";
|
|
1633
1694
|
}
|
|
1634
1695
|
|
|
1635
|
-
// src/utils/utils.ts
|
|
1636
|
-
var import_path4 = __toESM(require("path"));
|
|
1637
|
-
function normalizeFilePath(filePath) {
|
|
1638
|
-
const normalizedPath = import_path4.default.normalize(filePath);
|
|
1639
|
-
return import_path4.default.basename(normalizedPath);
|
|
1640
|
-
}
|
|
1641
|
-
function ensureHtmlExtension(filename) {
|
|
1642
|
-
const ext = import_path4.default.extname(filename);
|
|
1643
|
-
if (ext && ext.toLowerCase() === ".html") {
|
|
1644
|
-
return filename;
|
|
1645
|
-
}
|
|
1646
|
-
return `${filename}.html`;
|
|
1647
|
-
}
|
|
1648
|
-
function escapeHtml(unsafe) {
|
|
1649
|
-
if (typeof unsafe !== "string") {
|
|
1650
|
-
return String(unsafe);
|
|
1651
|
-
}
|
|
1652
|
-
return unsafe.replace(/[&<"']/g, function(match) {
|
|
1653
|
-
const escapeMap = {
|
|
1654
|
-
"&": "&",
|
|
1655
|
-
"<": "<",
|
|
1656
|
-
">": ">",
|
|
1657
|
-
'"': """,
|
|
1658
|
-
"'": "'"
|
|
1659
|
-
};
|
|
1660
|
-
return escapeMap[match] || match;
|
|
1661
|
-
});
|
|
1662
|
-
}
|
|
1663
|
-
function formatDateLocal(dateInput) {
|
|
1664
|
-
const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
|
|
1665
|
-
const options = {
|
|
1666
|
-
year: "numeric",
|
|
1667
|
-
month: "short",
|
|
1668
|
-
day: "2-digit",
|
|
1669
|
-
hour: "2-digit",
|
|
1670
|
-
minute: "2-digit",
|
|
1671
|
-
hour12: true,
|
|
1672
|
-
timeZoneName: "short"
|
|
1673
|
-
// or "Asia/Kolkata"
|
|
1674
|
-
};
|
|
1675
|
-
return new Intl.DateTimeFormat(void 0, options).format(date);
|
|
1676
|
-
}
|
|
1677
|
-
function extractSuites(titlePath) {
|
|
1678
|
-
const tagPattern = /@[\w]+/g;
|
|
1679
|
-
const suiteParts = titlePath.slice(3, titlePath.length - 1).map((p) => p.replace(tagPattern, "").trim());
|
|
1680
|
-
return {
|
|
1681
|
-
hierarchy: suiteParts.join(" > "),
|
|
1682
|
-
// full hierarchy
|
|
1683
|
-
topLevelSuite: suiteParts[0] ?? "",
|
|
1684
|
-
// first suite
|
|
1685
|
-
parentSuite: suiteParts[suiteParts.length - 1] ?? ""
|
|
1686
|
-
// last suite
|
|
1687
|
-
};
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
1696
|
// src/helpers/resultProcessor.ts
|
|
1691
1697
|
var TestResultProcessor = class {
|
|
1692
1698
|
constructor(projectRoot) {
|
|
@@ -1732,7 +1738,7 @@ var TestResultProcessor = class {
|
|
|
1732
1738
|
testId: `${filePath}:${projectName}:${title}`
|
|
1733
1739
|
};
|
|
1734
1740
|
attachFiles(
|
|
1735
|
-
test.id,
|
|
1741
|
+
import_path5.default.join(test.id, `retry-${result.retry}`),
|
|
1736
1742
|
result,
|
|
1737
1743
|
testResult,
|
|
1738
1744
|
ortoniConfig,
|
|
@@ -1961,7 +1967,8 @@ var DatabaseManager = class {
|
|
|
1961
1967
|
);
|
|
1962
1968
|
return results.map((result) => ({
|
|
1963
1969
|
...result,
|
|
1964
|
-
run_date:
|
|
1970
|
+
run_date: result.run_date
|
|
1971
|
+
// Return raw ISO string to avoid parsing issues in browser
|
|
1965
1972
|
}));
|
|
1966
1973
|
} catch (error) {
|
|
1967
1974
|
console.error("OrtoniReport: Error getting test history:", error);
|
|
@@ -2044,7 +2051,8 @@ var DatabaseManager = class {
|
|
|
2044
2051
|
);
|
|
2045
2052
|
return rows.reverse().map((row) => ({
|
|
2046
2053
|
...row,
|
|
2047
|
-
run_date:
|
|
2054
|
+
run_date: row.run_date,
|
|
2055
|
+
// Return raw ISO string for chart compatibility
|
|
2048
2056
|
avg_duration: Math.round(row.avg_duration || 0)
|
|
2049
2057
|
// raw ms avg
|
|
2050
2058
|
}));
|
package/dist/ortoni-report.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
extractSuites,
|
|
9
9
|
normalizeFilePath,
|
|
10
10
|
startReportServer
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-L6VOLEP2.mjs";
|
|
12
12
|
|
|
13
13
|
// src/helpers/resultProcessor.ts
|
|
14
14
|
import AnsiToHtml from "ansi-to-html";
|
|
@@ -1317,7 +1317,7 @@ var TestResultProcessor = class {
|
|
|
1317
1317
|
testId: `${filePath}:${projectName}:${title}`
|
|
1318
1318
|
};
|
|
1319
1319
|
attachFiles(
|
|
1320
|
-
test.id,
|
|
1320
|
+
path2.join(test.id, `retry-${result.retry}`),
|
|
1321
1321
|
result,
|
|
1322
1322
|
testResult,
|
|
1323
1323
|
ortoniConfig,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for OrtoniReport.
|
|
3
|
+
*/
|
|
4
|
+
export interface OrtoniReportConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Open the report in local host (Trace viewer is accessible only in localhost)
|
|
7
|
+
* @example "always"| "never"| "on-failure";
|
|
8
|
+
* @default "never"
|
|
9
|
+
*/
|
|
10
|
+
open?: "always" | "never" | "on-failure";
|
|
11
|
+
/**
|
|
12
|
+
* The title of the HTML report.
|
|
13
|
+
* @example "Ortoni Playwright Test Report"
|
|
14
|
+
*/
|
|
15
|
+
title?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Add project on the list of the tests? (Filtering projects still works if hidden)
|
|
18
|
+
* @example true to display, false to hide.
|
|
19
|
+
* @default false
|
|
20
|
+
*/
|
|
21
|
+
showProject?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The name of the project.
|
|
24
|
+
* @example "Ortoni Project"
|
|
25
|
+
*/
|
|
26
|
+
projectName?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The name of the author.
|
|
29
|
+
* @example "Koushik Chatterjee"
|
|
30
|
+
*/
|
|
31
|
+
authorName?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The type of tests being run.
|
|
34
|
+
* @example "Regression"
|
|
35
|
+
*/
|
|
36
|
+
testType?: string;
|
|
37
|
+
/**
|
|
38
|
+
* If true, images will be encoded in base64.
|
|
39
|
+
* Not recommended to use if many screenshots are present.
|
|
40
|
+
* @default false
|
|
41
|
+
* @example true
|
|
42
|
+
*/
|
|
43
|
+
base64Image?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The local relative of the logo image.
|
|
46
|
+
* Recommended to keep within the report genrated folder.
|
|
47
|
+
* @default "ortoni-report/logo.png"
|
|
48
|
+
* @example "logo.png"
|
|
49
|
+
*/
|
|
50
|
+
logo?: string;
|
|
51
|
+
/**
|
|
52
|
+
* The filename to the html report.
|
|
53
|
+
* @example "index.html"
|
|
54
|
+
* @default "ortoni-report.html"
|
|
55
|
+
*/
|
|
56
|
+
filename?: string;
|
|
57
|
+
/**
|
|
58
|
+
* The folder name.
|
|
59
|
+
* @example "report"
|
|
60
|
+
* @default "ortoni-report"
|
|
61
|
+
*/
|
|
62
|
+
folderPath?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Port to connect
|
|
65
|
+
* @example 3600
|
|
66
|
+
*/
|
|
67
|
+
port?: number;
|
|
68
|
+
/**
|
|
69
|
+
* Display console logs?
|
|
70
|
+
* @example boolean
|
|
71
|
+
* @default true
|
|
72
|
+
*/
|
|
73
|
+
stdIO?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Metadata for the report. ['Test Cycle': 'Cycle 1', 'Test Environment':'QA', etc]
|
|
76
|
+
* @example { "key": "value" } as string
|
|
77
|
+
*/
|
|
78
|
+
meta?: Record<string, string>;
|
|
79
|
+
/**
|
|
80
|
+
* Save the history of the reports in a SQL file to be used in future reports.
|
|
81
|
+
* The history file will be saved in the report folder.
|
|
82
|
+
* @default true
|
|
83
|
+
* @example false (to disable)
|
|
84
|
+
*/
|
|
85
|
+
saveHistory?: boolean;
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface Steps {
|
|
2
|
+
snippet: string | undefined;
|
|
3
|
+
title: string;
|
|
4
|
+
location: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TestResultData {
|
|
7
|
+
suiteHierarchy: string;
|
|
8
|
+
key: string;
|
|
9
|
+
annotations: any[];
|
|
10
|
+
testTags: string[];
|
|
11
|
+
location: string;
|
|
12
|
+
retryAttemptCount: number;
|
|
13
|
+
projectName: any;
|
|
14
|
+
suite: any;
|
|
15
|
+
title: string;
|
|
16
|
+
status: "passed" | "failed" | "timedOut" | "skipped" | "interrupted" | "expected" | "unexpected" | "flaky";
|
|
17
|
+
flaky: string;
|
|
18
|
+
duration: number;
|
|
19
|
+
errors: any[];
|
|
20
|
+
steps: Steps[];
|
|
21
|
+
logs: string;
|
|
22
|
+
screenshotPath?: string | null | undefined;
|
|
23
|
+
screenshots?: string[];
|
|
24
|
+
filePath: string;
|
|
25
|
+
filters: Set<string>;
|
|
26
|
+
tracePath?: string;
|
|
27
|
+
videoPath?: string[];
|
|
28
|
+
markdownPath?: string;
|
|
29
|
+
base64Image: boolean | undefined;
|
|
30
|
+
testId: string;
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { TestResult } from "@playwright/test/reporter";
|
|
2
|
+
import { Steps, TestResultData } from "../types/testResults";
|
|
3
|
+
import { OrtoniReportConfig } from "../types/reporterConfig";
|
|
4
|
+
export declare function attachFiles(subFolder: string, result: TestResult, testResult: TestResultData, config: OrtoniReportConfig, steps?: Steps[], errors?: string[]): void;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { convertMarkdownToHtml } from "../helpers/markdownConverter";
|
|
4
|
+
export function attachFiles(subFolder, result, testResult, config, steps, errors) {
|
|
5
|
+
const folderPath = config.folderPath || "ortoni-report";
|
|
6
|
+
const attachmentsFolder = path.join(folderPath, "ortoni-data", "attachments", subFolder);
|
|
7
|
+
if (!fs.existsSync(attachmentsFolder)) {
|
|
8
|
+
fs.mkdirSync(attachmentsFolder, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
if (!result.attachments)
|
|
11
|
+
return;
|
|
12
|
+
const { base64Image } = config;
|
|
13
|
+
testResult.screenshots = [];
|
|
14
|
+
testResult.videoPath = [];
|
|
15
|
+
result.attachments.forEach((attachment) => {
|
|
16
|
+
const { contentType, name, path: attachmentPath, body } = attachment;
|
|
17
|
+
if (!attachmentPath && !body)
|
|
18
|
+
return;
|
|
19
|
+
const fileName = attachmentPath
|
|
20
|
+
? path.basename(attachmentPath)
|
|
21
|
+
: `${name}.${getFileExtension(contentType)}`;
|
|
22
|
+
const relativePath = path.join("ortoni-data", "attachments", subFolder, fileName);
|
|
23
|
+
const fullPath = path.join(attachmentsFolder, fileName);
|
|
24
|
+
if (contentType === "image/png") {
|
|
25
|
+
handleImage(attachmentPath, body, base64Image, fullPath, relativePath, testResult);
|
|
26
|
+
}
|
|
27
|
+
else if (name === "video") {
|
|
28
|
+
handleAttachment(attachmentPath, fullPath, relativePath, "videoPath", testResult);
|
|
29
|
+
}
|
|
30
|
+
else if (name === "trace") {
|
|
31
|
+
handleAttachment(attachmentPath, fullPath, relativePath, "tracePath", testResult);
|
|
32
|
+
}
|
|
33
|
+
else if (name === "error-context") {
|
|
34
|
+
handleAttachment(attachmentPath, fullPath, relativePath, "markdownPath", testResult, steps, errors);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function handleImage(attachmentPath, body, base64Image, fullPath, relativePath, testResult) {
|
|
39
|
+
let screenshotPath = "";
|
|
40
|
+
if (attachmentPath) {
|
|
41
|
+
try {
|
|
42
|
+
const screenshotContent = fs.readFileSync(attachmentPath, base64Image ? "base64" : undefined);
|
|
43
|
+
screenshotPath = base64Image
|
|
44
|
+
? `data:image/png;base64,${screenshotContent}`
|
|
45
|
+
: relativePath;
|
|
46
|
+
if (!base64Image) {
|
|
47
|
+
fs.copyFileSync(attachmentPath, fullPath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error(`OrtoniReport: Failed to read screenshot file: ${attachmentPath}`, error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (body) {
|
|
55
|
+
screenshotPath = `data:image/png;base64,${body.toString("base64")}`;
|
|
56
|
+
}
|
|
57
|
+
if (screenshotPath) {
|
|
58
|
+
testResult.screenshots?.push(screenshotPath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function handleAttachment(attachmentPath, fullPath, relativePath, resultKey, testResult, steps, errors) {
|
|
62
|
+
if (attachmentPath) {
|
|
63
|
+
fs.copyFileSync(attachmentPath, fullPath);
|
|
64
|
+
if (resultKey === "videoPath") {
|
|
65
|
+
testResult[resultKey]?.push(relativePath);
|
|
66
|
+
}
|
|
67
|
+
else if (resultKey === "tracePath") {
|
|
68
|
+
testResult[resultKey] = relativePath;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (resultKey === "markdownPath" && errors) {
|
|
72
|
+
const htmlPath = fullPath.replace(/\.md$/, ".html");
|
|
73
|
+
const htmlRelativePath = relativePath.replace(/\.md$/, ".html");
|
|
74
|
+
convertMarkdownToHtml(fullPath, htmlPath);
|
|
75
|
+
testResult[resultKey] = htmlRelativePath;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function getFileExtension(contentType) {
|
|
80
|
+
const extensions = {
|
|
81
|
+
"image/png": "png",
|
|
82
|
+
"video/webm": "webm",
|
|
83
|
+
"application/zip": "zip",
|
|
84
|
+
"text/markdown": "md",
|
|
85
|
+
};
|
|
86
|
+
return extensions[contentType] || "unknown";
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startReportServer(reportFolder: string, reportFilename: string, port: number, open: string | undefined): void;
|