ortoni-report 4.0.2 → 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 +5 -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 +2 -2
- package/dist/mergeData.d.ts +13 -0
- package/dist/mergeData.js +182 -0
- package/dist/ortoni-report.js +1 -1
- package/dist/ortoni-report.mjs +1 -1
- 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 +84 -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
package/dist/ortoni-report.mjs
CHANGED
|
@@ -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;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
export function startReportServer(reportFolder, reportFilename, port = 2004, open) {
|
|
5
|
+
const app = express();
|
|
6
|
+
app.use(express.static(reportFolder, { index: false }));
|
|
7
|
+
app.get("/", (_req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
res.sendFile(path.resolve(reportFolder, reportFilename));
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
console.error("Ortoni Report: Error sending report file:", error);
|
|
13
|
+
res.status(500).send("Error loading report");
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
try {
|
|
17
|
+
const server = app.listen(port, () => {
|
|
18
|
+
console.log(`Server is running at http://localhost:${port} \nPress Ctrl+C to stop.`);
|
|
19
|
+
if (open === "always" || open === "on-failure") {
|
|
20
|
+
try {
|
|
21
|
+
openBrowser(`http://localhost:${port}`);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error("Ortoni Report: Error opening browser:", error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
server.on("error", (error) => {
|
|
29
|
+
if (error.code === "EADDRINUSE") {
|
|
30
|
+
console.error(`Ortoni Report: Port ${port} is already in use. Trying a different port...`);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
console.error("Ortoni Report: Server error:", error);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.error("Ortoni Report: Error starting the server:", error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function openBrowser(url) {
|
|
42
|
+
const platform = process.platform;
|
|
43
|
+
let command;
|
|
44
|
+
try {
|
|
45
|
+
if (platform === "win32") {
|
|
46
|
+
command = "cmd";
|
|
47
|
+
spawn(command, ["/c", "start", url]);
|
|
48
|
+
}
|
|
49
|
+
else if (platform === "darwin") {
|
|
50
|
+
command = "open";
|
|
51
|
+
spawn(command, [url]);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
command = "xdg-open";
|
|
55
|
+
spawn(command, [url]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error("Ortoni Report: Error opening the browser:", error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function groupResults(config, results) {
|
|
2
|
+
if (config.showProject) {
|
|
3
|
+
// Group by filePath, suite, and projectName
|
|
4
|
+
const groupedResults = results.reduce((acc, result, index) => {
|
|
5
|
+
const testId = `${result.filePath}:${result.projectName}:${result.title}`;
|
|
6
|
+
const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
|
|
7
|
+
const { filePath, suite, projectName } = result;
|
|
8
|
+
acc[filePath] = acc[filePath] || {};
|
|
9
|
+
acc[filePath][suite] = acc[filePath][suite] || {};
|
|
10
|
+
acc[filePath][suite][projectName] =
|
|
11
|
+
acc[filePath][suite][projectName] || [];
|
|
12
|
+
acc[filePath][suite][projectName].push({ ...result, index, testId, key });
|
|
13
|
+
return acc;
|
|
14
|
+
}, {});
|
|
15
|
+
return groupedResults;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
// Group by filePath and suite, ignoring projectName
|
|
19
|
+
const groupedResults = results.reduce((acc, result, index) => {
|
|
20
|
+
const testId = `${result.filePath}:${result.projectName}:${result.title}`;
|
|
21
|
+
const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
|
|
22
|
+
const { filePath, suite } = result;
|
|
23
|
+
acc[filePath] = acc[filePath] || {};
|
|
24
|
+
acc[filePath][suite] = acc[filePath][suite] || [];
|
|
25
|
+
acc[filePath][suite].push({ ...result, index, testId, key });
|
|
26
|
+
return acc;
|
|
27
|
+
}, {});
|
|
28
|
+
return groupedResults;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function normalizeFilePath(filePath: string): string;
|
|
2
|
+
export declare function formatDate(date: Date): string;
|
|
3
|
+
export declare function safeStringify(obj: any, indent?: number): string;
|
|
4
|
+
export declare function ensureHtmlExtension(filename: string): string;
|
|
5
|
+
export declare function escapeHtml(unsafe: string): string;
|
|
6
|
+
export declare function formatDateUTC(date: Date): string;
|
|
7
|
+
export declare function formatDateLocal(dateInput: Date | string): string;
|
|
8
|
+
export declare function formatDateNoTimezone(isoString: string): string;
|
|
9
|
+
type SuiteAndTitle = {
|
|
10
|
+
hierarchy: string;
|
|
11
|
+
topLevelSuite: string;
|
|
12
|
+
parentSuite: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function extractSuites(titlePath: string[]): SuiteAndTitle;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
export function normalizeFilePath(filePath) {
|
|
3
|
+
// Normalize the path to handle different separators
|
|
4
|
+
const normalizedPath = path.normalize(filePath);
|
|
5
|
+
// Get the base name of the file (removes any leading directories)
|
|
6
|
+
return path.basename(normalizedPath);
|
|
7
|
+
}
|
|
8
|
+
export function formatDate(date) {
|
|
9
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
10
|
+
const month = date.toLocaleString("default", { month: "short" });
|
|
11
|
+
const year = date.getFullYear();
|
|
12
|
+
const time = date.toLocaleTimeString();
|
|
13
|
+
return `${day}-${month}-${year} ${time}`;
|
|
14
|
+
}
|
|
15
|
+
export function safeStringify(obj, indent = 2) {
|
|
16
|
+
const cache = new Set();
|
|
17
|
+
const json = JSON.stringify(obj, (key, value) => {
|
|
18
|
+
if (typeof value === "object" && value !== null) {
|
|
19
|
+
if (cache.has(value)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
cache.add(value);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}, indent);
|
|
26
|
+
cache.clear();
|
|
27
|
+
return json;
|
|
28
|
+
}
|
|
29
|
+
export function ensureHtmlExtension(filename) {
|
|
30
|
+
const ext = path.extname(filename);
|
|
31
|
+
if (ext && ext.toLowerCase() === ".html") {
|
|
32
|
+
return filename;
|
|
33
|
+
}
|
|
34
|
+
return `${filename}.html`;
|
|
35
|
+
}
|
|
36
|
+
export function escapeHtml(unsafe) {
|
|
37
|
+
if (typeof unsafe !== "string") {
|
|
38
|
+
return String(unsafe);
|
|
39
|
+
}
|
|
40
|
+
return unsafe.replace(/[&<"']/g, function (match) {
|
|
41
|
+
const escapeMap = {
|
|
42
|
+
"&": "&",
|
|
43
|
+
"<": "<",
|
|
44
|
+
">": ">",
|
|
45
|
+
'"': """,
|
|
46
|
+
"'": "'",
|
|
47
|
+
};
|
|
48
|
+
return escapeMap[match] || match;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function formatDateUTC(date) {
|
|
52
|
+
return date.toISOString();
|
|
53
|
+
}
|
|
54
|
+
export function formatDateLocal(dateInput) {
|
|
55
|
+
const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
|
|
56
|
+
const options = {
|
|
57
|
+
year: "numeric",
|
|
58
|
+
month: "short",
|
|
59
|
+
day: "2-digit",
|
|
60
|
+
hour: "2-digit",
|
|
61
|
+
minute: "2-digit",
|
|
62
|
+
hour12: true,
|
|
63
|
+
timeZoneName: "short", // or "Asia/Kolkata"
|
|
64
|
+
};
|
|
65
|
+
return new Intl.DateTimeFormat(undefined, options).format(date);
|
|
66
|
+
}
|
|
67
|
+
export function formatDateNoTimezone(isoString) {
|
|
68
|
+
const date = new Date(isoString);
|
|
69
|
+
return date.toLocaleString("en-US", {
|
|
70
|
+
dateStyle: "medium",
|
|
71
|
+
timeStyle: "short",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function extractSuites(titlePath) {
|
|
75
|
+
const tagPattern = /@[\w]+/g;
|
|
76
|
+
const suiteParts = titlePath
|
|
77
|
+
.slice(3, titlePath.length - 1)
|
|
78
|
+
.map((p) => p.replace(tagPattern, "").trim());
|
|
79
|
+
return {
|
|
80
|
+
hierarchy: suiteParts.join(" > "),
|
|
81
|
+
topLevelSuite: suiteParts[0] ?? "",
|
|
82
|
+
parentSuite: suiteParts[suiteParts.length - 1] ?? "", // last suite
|
|
83
|
+
};
|
|
84
|
+
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -201,7 +201,7 @@ Developed and designed by [Koushik Chatterjee](https://letcode.in/contact)
|
|
|
201
201
|
**Tech Stack**
|
|
202
202
|
|
|
203
203
|
1. Report generated using Playwright custom report
|
|
204
|
-
2. UI - React and Shadcn UI
|
|
204
|
+
2. [UI - React and Shadcn UI](https://github.com/ortoniKC/ortoni-report-react)
|
|
205
205
|
3. DB - sqlite
|
|
206
206
|
4. Local host - express
|
|
207
207
|
|