ortoni-report 1.1.5 → 1.1.7
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 +24 -0
- package/dist/css/main.css +22445 -22445
- package/dist/ortoni-report.d.ts +50 -4
- package/dist/ortoni-report.js +149 -118
- package/dist/ortoni-report.mjs +149 -118
- package/dist/report-template.hbs +311 -255
- package/dist/utils/modal.js +10 -3
- package/package.json +3 -2
- package/readme.md +64 -116
package/dist/ortoni-report.d.ts
CHANGED
|
@@ -1,13 +1,54 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for OrtoniReport.
|
|
5
|
+
*/
|
|
3
6
|
interface OrtoniReportConfig {
|
|
7
|
+
/**
|
|
8
|
+
* The name of the project.
|
|
9
|
+
* @example "Ortoni Project"
|
|
10
|
+
*/
|
|
4
11
|
projectName?: string;
|
|
12
|
+
/**
|
|
13
|
+
* The name of the author.
|
|
14
|
+
* @example "John Doe"
|
|
15
|
+
*/
|
|
5
16
|
authorName?: string;
|
|
17
|
+
/**
|
|
18
|
+
* The type of tests being run.
|
|
19
|
+
* @example "Regression"
|
|
20
|
+
*/
|
|
6
21
|
testType?: string;
|
|
22
|
+
/**
|
|
23
|
+
* The preferred theme for the report.
|
|
24
|
+
* Can be either "light" or "dark".
|
|
25
|
+
* @default "System theme"
|
|
26
|
+
* @example "dark"
|
|
27
|
+
*/
|
|
7
28
|
preferredTheme?: 'light' | 'dark';
|
|
29
|
+
/**
|
|
30
|
+
* If true, images will be encoded in base64.
|
|
31
|
+
* @default false
|
|
32
|
+
* @example true
|
|
33
|
+
*/
|
|
34
|
+
base64Image?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* The local relative or absolute path to the logo image.
|
|
37
|
+
* @example "./assets/logo.png"
|
|
38
|
+
* @example "/absolute/path/to/logo.png"
|
|
39
|
+
*/
|
|
40
|
+
logo?: string;
|
|
8
41
|
}
|
|
9
42
|
|
|
43
|
+
interface Steps {
|
|
44
|
+
snippet: string | undefined;
|
|
45
|
+
title: string;
|
|
46
|
+
location: string;
|
|
47
|
+
}
|
|
10
48
|
interface TestResultData {
|
|
49
|
+
suiteTags: string[];
|
|
50
|
+
testTags: string[];
|
|
51
|
+
location: string;
|
|
11
52
|
retry: string;
|
|
12
53
|
isRetry: number;
|
|
13
54
|
projectName: any;
|
|
@@ -17,14 +58,18 @@ interface TestResultData {
|
|
|
17
58
|
flaky: string;
|
|
18
59
|
duration: string;
|
|
19
60
|
errors: any[];
|
|
20
|
-
steps:
|
|
61
|
+
steps: Steps[];
|
|
21
62
|
logs: string;
|
|
22
|
-
screenshotPath
|
|
23
|
-
filePath:
|
|
63
|
+
screenshotPath?: string | null | undefined;
|
|
64
|
+
filePath: string;
|
|
24
65
|
projects: Set<string>;
|
|
66
|
+
tracePath?: string;
|
|
67
|
+
videoPath?: string;
|
|
68
|
+
base64Image: boolean | undefined;
|
|
25
69
|
}
|
|
26
70
|
|
|
27
71
|
declare class OrtoniReport implements Reporter {
|
|
72
|
+
private projectRoot;
|
|
28
73
|
private results;
|
|
29
74
|
private groupedResults;
|
|
30
75
|
private suiteName;
|
|
@@ -33,6 +78,7 @@ declare class OrtoniReport implements Reporter {
|
|
|
33
78
|
onBegin(config: FullConfig, suite: Suite): void;
|
|
34
79
|
onTestBegin(test: TestCase, result: TestResult): void;
|
|
35
80
|
private projectSet;
|
|
81
|
+
private tagsSet;
|
|
36
82
|
onTestEnd(test: TestCase, result: TestResult): void;
|
|
37
83
|
onEnd(result: FullResult): void;
|
|
38
84
|
generateHTML(filteredResults: TestResultData[], totalDuration: string): string;
|
package/dist/ortoni-report.js
CHANGED
|
@@ -73,144 +73,175 @@ function formatDate(date) {
|
|
|
73
73
|
const time = date.toLocaleTimeString();
|
|
74
74
|
return `${day}-${month}-${year} ${time}`;
|
|
75
75
|
}
|
|
76
|
+
function safeStringify(obj, indent = 2) {
|
|
77
|
+
const cache = /* @__PURE__ */ new Set();
|
|
78
|
+
const json = JSON.stringify(obj, (key, value) => {
|
|
79
|
+
if (typeof value === "object" && value !== null) {
|
|
80
|
+
if (cache.has(value)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
cache.add(value);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}, indent);
|
|
87
|
+
cache.clear();
|
|
88
|
+
return json;
|
|
89
|
+
}
|
|
76
90
|
|
|
77
91
|
// src/ortoni-report.ts
|
|
78
92
|
var OrtoniReport = class {
|
|
79
93
|
constructor(config = {}) {
|
|
94
|
+
this.projectRoot = "";
|
|
80
95
|
this.results = [];
|
|
81
96
|
this.projectSet = /* @__PURE__ */ new Set();
|
|
97
|
+
this.tagsSet = /* @__PURE__ */ new Set();
|
|
82
98
|
this.config = config;
|
|
83
99
|
}
|
|
84
100
|
onBegin(config, suite) {
|
|
85
101
|
this.results = [];
|
|
102
|
+
this.projectRoot = config.rootDir;
|
|
86
103
|
}
|
|
87
104
|
onTestBegin(test, result) {
|
|
88
105
|
}
|
|
89
106
|
onTestEnd(test, result) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
107
|
+
try {
|
|
108
|
+
let status = result.status;
|
|
109
|
+
if (test.outcome() === "flaky") {
|
|
110
|
+
status = "flaky";
|
|
111
|
+
}
|
|
112
|
+
const projectName = test.titlePath()[1];
|
|
113
|
+
this.projectSet.add(projectName);
|
|
114
|
+
const location = test.location;
|
|
115
|
+
const filePath = normalizeFilePath(test.titlePath()[2]);
|
|
116
|
+
const tagPattern = /@[\w]+/g;
|
|
117
|
+
const testTags = test.title.match(tagPattern) || [];
|
|
118
|
+
const title = test.title.replace(tagPattern, "").trim();
|
|
119
|
+
const suiteTags = test.titlePath()[3].match(tagPattern) || [];
|
|
120
|
+
const suite = test.titlePath()[3].replace(tagPattern, "").trim();
|
|
121
|
+
const testResult = {
|
|
122
|
+
suiteTags,
|
|
123
|
+
testTags,
|
|
124
|
+
location: `${filePath}:${location.line}:${location.column}`,
|
|
125
|
+
retry: result.retry > 0 ? "retry" : "",
|
|
126
|
+
isRetry: result.retry,
|
|
127
|
+
projectName,
|
|
128
|
+
suite,
|
|
129
|
+
title,
|
|
130
|
+
status,
|
|
131
|
+
flaky: test.outcome(),
|
|
132
|
+
duration: msToTime(result.duration),
|
|
133
|
+
errors: result.errors.map((e) => import_safe.default.strip(e.message || e.toString())),
|
|
134
|
+
steps: result.steps.map((step) => {
|
|
135
|
+
const location2 = step.location ? `${import_path2.default.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
|
|
136
|
+
return {
|
|
137
|
+
snippet: import_safe.default.strip(step.error?.snippet || ""),
|
|
138
|
+
title: step.title,
|
|
139
|
+
location: step.error ? location2 : ""
|
|
140
|
+
};
|
|
141
|
+
}),
|
|
142
|
+
logs: import_safe.default.strip(result.stdout.concat(result.stderr).map((log) => log).join("\n")),
|
|
143
|
+
filePath,
|
|
144
|
+
projects: this.projectSet,
|
|
145
|
+
base64Image: this.config.base64Image
|
|
146
|
+
};
|
|
147
|
+
if (result.attachments) {
|
|
148
|
+
const screenshot = result.attachments.find((attachment) => attachment.name === "screenshot");
|
|
149
|
+
if (this.config.base64Image) {
|
|
150
|
+
if (screenshot && screenshot.path) {
|
|
151
|
+
try {
|
|
152
|
+
const screenshotContent = import_fs.default.readFileSync(screenshot.path, "base64");
|
|
153
|
+
testResult.screenshotPath = `data:image/png;base64,${screenshotContent}`;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(`OrtoniReport: Failed to read screenshot file: ${screenshot.path}`, error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
if (screenshot && screenshot.path) {
|
|
160
|
+
testResult.screenshotPath = import_path2.default.resolve(screenshot.path);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const tracePath = result.attachments.find((attachment) => attachment.name === "trace");
|
|
164
|
+
if (tracePath?.path) {
|
|
165
|
+
testResult.tracePath = import_path2.default.resolve(__dirname, tracePath.path);
|
|
166
|
+
}
|
|
167
|
+
const videoPath = result.attachments.find((attachment) => attachment.name === "video");
|
|
168
|
+
if (videoPath?.path) {
|
|
169
|
+
testResult.videoPath = import_path2.default.resolve(__dirname, videoPath.path);
|
|
170
|
+
}
|
|
126
171
|
}
|
|
172
|
+
this.results.push(testResult);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error("OrtoniReport: Error processing test end:", error);
|
|
127
175
|
}
|
|
128
|
-
this.results.push(testResult);
|
|
129
176
|
}
|
|
130
177
|
onEnd(result) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
acc[filePath]
|
|
139
|
-
|
|
140
|
-
if (!acc[filePath][suiteName]) {
|
|
141
|
-
acc[filePath][suiteName] = {};
|
|
142
|
-
}
|
|
143
|
-
if (!acc[filePath][suiteName][projectName]) {
|
|
144
|
-
acc[filePath][suiteName][projectName] = [];
|
|
145
|
-
}
|
|
146
|
-
acc[filePath][suiteName][projectName].push({ ...result2, index });
|
|
147
|
-
return acc;
|
|
148
|
-
}, {});
|
|
149
|
-
import_handlebars.default.registerHelper("json", function(context) {
|
|
150
|
-
return safeStringify(context);
|
|
151
|
-
});
|
|
152
|
-
import_handlebars.default.registerHelper("eq", function(actualStatus, expectedStatus) {
|
|
153
|
-
return actualStatus === expectedStatus;
|
|
154
|
-
});
|
|
155
|
-
import_handlebars.default.registerHelper("or", () => {
|
|
156
|
-
var args = Array.prototype.slice.call(arguments);
|
|
157
|
-
var options = args.pop();
|
|
158
|
-
for (var i = 0; i < args.length; i++) {
|
|
159
|
-
if (args[i]) {
|
|
160
|
-
return options.fn(this);
|
|
178
|
+
try {
|
|
179
|
+
const filteredResults = this.results.filter((r) => r.status !== "skipped" && !r.isRetry);
|
|
180
|
+
const totalDuration = msToTime(result.duration);
|
|
181
|
+
this.groupedResults = this.results.reduce((acc, result2, index) => {
|
|
182
|
+
const filePath = result2.filePath;
|
|
183
|
+
const suiteName = result2.suite;
|
|
184
|
+
const projectName = result2.projectName;
|
|
185
|
+
if (!acc[filePath]) {
|
|
186
|
+
acc[filePath] = {};
|
|
161
187
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
188
|
+
if (!acc[filePath][suiteName]) {
|
|
189
|
+
acc[filePath][suiteName] = {};
|
|
190
|
+
}
|
|
191
|
+
if (!acc[filePath][suiteName][projectName]) {
|
|
192
|
+
acc[filePath][suiteName][projectName] = [];
|
|
193
|
+
}
|
|
194
|
+
acc[filePath][suiteName][projectName].push({ ...result2, index });
|
|
195
|
+
return acc;
|
|
196
|
+
}, {});
|
|
197
|
+
import_handlebars.default.registerHelper("json", function(context) {
|
|
198
|
+
return safeStringify(context);
|
|
199
|
+
});
|
|
200
|
+
import_handlebars.default.registerHelper("eq", function(actualStatus, expectedStatus) {
|
|
201
|
+
return actualStatus === expectedStatus;
|
|
202
|
+
});
|
|
203
|
+
const html = this.generateHTML(filteredResults, totalDuration);
|
|
204
|
+
const outputPath = import_path2.default.resolve(process.cwd(), "ortoni-report.html");
|
|
205
|
+
import_fs.default.writeFileSync(outputPath, html);
|
|
206
|
+
console.log(`Ortoni HTML report generated at ${outputPath}`);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error("OrtoniReport: Error generating report:", error);
|
|
209
|
+
}
|
|
172
210
|
}
|
|
173
211
|
generateHTML(filteredResults, totalDuration) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
212
|
+
try {
|
|
213
|
+
const totalTests = filteredResults.length;
|
|
214
|
+
const passedTests = this.results.filter((r) => r.status === "passed").length;
|
|
215
|
+
const flakyTests = this.results.filter((r) => r.flaky === "flaky").length;
|
|
216
|
+
const failed = filteredResults.filter((r) => r.status === "failed" || r.status === "timedOut").length;
|
|
217
|
+
const successRate = ((passedTests + flakyTests) / totalTests * 100).toFixed(2);
|
|
218
|
+
const templateSource = import_fs.default.readFileSync(import_path2.default.resolve(__dirname, "report-template.hbs"), "utf-8");
|
|
219
|
+
const template = import_handlebars.default.compile(templateSource);
|
|
220
|
+
const logo = this.config.logo;
|
|
221
|
+
const data = {
|
|
222
|
+
logo: logo ? import_path2.default.resolve(logo) : void 0,
|
|
223
|
+
totalDuration,
|
|
224
|
+
suiteName: this.suiteName,
|
|
225
|
+
results: this.results,
|
|
226
|
+
retryCount: this.results.filter((r) => r.isRetry).length,
|
|
227
|
+
passCount: passedTests,
|
|
228
|
+
failCount: failed,
|
|
229
|
+
skipCount: this.results.filter((r) => r.status === "skipped").length,
|
|
230
|
+
flakyCount: flakyTests,
|
|
231
|
+
totalCount: filteredResults.length,
|
|
232
|
+
groupedResults: this.groupedResults,
|
|
233
|
+
projectName: this.config.projectName,
|
|
234
|
+
authorName: this.config.authorName,
|
|
235
|
+
testType: this.config.testType,
|
|
236
|
+
preferredTheme: this.config.preferredTheme,
|
|
237
|
+
successRate,
|
|
238
|
+
lastRunDate: formatDate(/* @__PURE__ */ new Date()),
|
|
239
|
+
projects: this.projectSet
|
|
240
|
+
};
|
|
241
|
+
return template(data);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error("OrtoniReport: Error generating HTML:", error);
|
|
244
|
+
return `<html><body><h1>Report generation failed</h1><pre>${error.stack}</pre></body></html>`;
|
|
245
|
+
}
|
|
201
246
|
}
|
|
202
247
|
};
|
|
203
|
-
function safeStringify(obj, indent = 2) {
|
|
204
|
-
const cache = /* @__PURE__ */ new Set();
|
|
205
|
-
const json = JSON.stringify(obj, (key, value) => {
|
|
206
|
-
if (typeof value === "object" && value !== null) {
|
|
207
|
-
if (cache.has(value)) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
cache.add(value);
|
|
211
|
-
}
|
|
212
|
-
return value;
|
|
213
|
-
}, indent);
|
|
214
|
-
cache.clear();
|
|
215
|
-
return json;
|
|
216
|
-
}
|