ortoni-report 1.1.6 → 1.1.8

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 CHANGED
@@ -1,4 +1,22 @@
1
1
  # Change Log:
2
+ ## Version 1.1.8
3
+
4
+ ### Fixed
5
+ - CSS issue
6
+
7
+ ## Version 1.1.7
8
+
9
+ ### Fixed
10
+ - Layout issue where the test column extends off-screen
11
+ - Screenshot layout of tests executed on mobile viewport
12
+
13
+ ### New
14
+ - Add custom project/organization logo
15
+ - Exception handling in `ortoni-report.ts`
16
+ - Added docs to the `OrtoniReportConfig`
17
+
18
+ ### Improved
19
+ - Margin on Test List for better spacing
2
20
 
3
21
  ## Version 1.1.6
4
22
  - Implemented debounce function to reduce search event handling frequency.
package/dist/css/main.css CHANGED
@@ -22442,4 +22442,4 @@ has-background-danger.is-hoverable:active {
22442
22442
  pointer-events: all !important;
22443
22443
  }
22444
22444
 
22445
- /*# sourceMappingURL=style.css.map */
22445
+ /*# sourceMappingURL=style.css.map */
@@ -1,13 +1,45 @@
1
- import { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
2
-
1
+ import { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
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
+ */
8
34
  base64Image?: boolean;
9
- }
10
-
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;
41
+ }
42
+
11
43
  interface Steps {
12
44
  snippet: string | undefined;
13
45
  title: string;
@@ -34,8 +66,8 @@ interface TestResultData {
34
66
  tracePath?: string;
35
67
  videoPath?: string;
36
68
  base64Image: boolean | undefined;
37
- }
38
-
69
+ }
70
+
39
71
  declare class OrtoniReport implements Reporter {
40
72
  private projectRoot;
41
73
  private results;
@@ -50,6 +82,6 @@ declare class OrtoniReport implements Reporter {
50
82
  onTestEnd(test: TestCase, result: TestResult): void;
51
83
  onEnd(result: FullResult): void;
52
84
  generateHTML(filteredResults: TestResultData[], totalDuration: string): string;
53
- }
54
-
55
- export { OrtoniReportConfig, OrtoniReport as default };
85
+ }
86
+
87
+ export { OrtoniReportConfig, OrtoniReport as default };
@@ -1,242 +1,247 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
-
30
- // src/ortoni-report.ts
31
- var ortoni_report_exports = {};
32
- __export(ortoni_report_exports, {
33
- default: () => OrtoniReport
34
- });
35
- module.exports = __toCommonJS(ortoni_report_exports);
36
- var import_fs = __toESM(require("fs"));
37
- var import_path2 = __toESM(require("path"));
38
- var import_handlebars = __toESM(require("handlebars"));
39
- var import_safe = __toESM(require("colors/safe"));
40
-
41
- // src/utils/utils.ts
42
- var import_path = __toESM(require("path"));
43
- function msToTime(duration) {
44
- const milliseconds = Math.floor(duration % 1e3);
45
- const seconds = Math.floor(duration / 1e3 % 60);
46
- const minutes = Math.floor(duration / (1e3 * 60) % 60);
47
- const hours = Math.floor(duration / (1e3 * 60 * 60) % 24);
48
- let result = "";
49
- if (hours > 0) {
50
- result += `${hours}h:`;
51
- }
52
- if (minutes > 0 || hours > 0) {
53
- result += `${minutes < 10 ? "0" + minutes : minutes}m:`;
54
- }
55
- if (seconds > 0 || minutes > 0 || hours > 0) {
56
- result += `${seconds < 10 ? "0" + seconds : seconds}s`;
57
- }
58
- if (milliseconds > 0 && !(seconds > 0 || minutes > 0 || hours > 0)) {
59
- result += `${milliseconds}ms`;
60
- } else if (milliseconds > 0) {
61
- result += `:${milliseconds < 100 ? "0" + milliseconds : milliseconds}ms`;
62
- }
63
- return result;
64
- }
65
- function normalizeFilePath(filePath) {
66
- const normalizedPath = import_path.default.normalize(filePath);
67
- return import_path.default.basename(normalizedPath);
68
- }
69
- function formatDate(date) {
70
- const day = String(date.getDate()).padStart(2, "0");
71
- const month = date.toLocaleString("default", { month: "short" });
72
- const year = date.getFullYear();
73
- const time = date.toLocaleTimeString();
74
- return `${day}-${month}-${year} ${time}`;
75
- }
76
-
77
- // src/ortoni-report.ts
78
- var OrtoniReport = class {
79
- constructor(config = {}) {
80
- this.projectRoot = "";
81
- this.results = [];
82
- this.projectSet = /* @__PURE__ */ new Set();
83
- // TODO: add tags to the filter dropdown
84
- this.tagsSet = /* @__PURE__ */ new Set();
85
- this.config = config;
86
- }
87
- onBegin(config, suite) {
88
- this.results = [];
89
- this.projectRoot = config.rootDir;
90
- }
91
- onTestBegin(test, result) {
92
- }
93
- onTestEnd(test, result) {
94
- let status = result.status;
95
- if (test.outcome() === "flaky") {
96
- status = "flaky";
97
- }
98
- const projectName = test.titlePath()[1];
99
- this.projectSet.add(projectName);
100
- const location = test.location;
101
- const filePath = normalizeFilePath(test.titlePath()[2]);
102
- const tagPattern = /@[\w]+/g;
103
- const testTags = test.title.match(tagPattern) || [];
104
- const title = test.title.replace(tagPattern, "").trim();
105
- const suiteTags = test.titlePath()[3].match(tagPattern) || [];
106
- const suite = test.titlePath()[3].replace(tagPattern, "").trim();
107
- const testResult = {
108
- suiteTags,
109
- testTags,
110
- location: `${filePath}:${location.line}:${location.column}`,
111
- retry: result.retry > 0 ? "retry" : "",
112
- isRetry: result.retry,
113
- projectName,
114
- suite,
115
- title,
116
- status,
117
- flaky: test.outcome(),
118
- duration: msToTime(result.duration),
119
- errors: result.errors.map((e) => import_safe.default.strip(e.message || e.toString())),
120
- steps: result.steps.map((step) => {
121
- const location2 = step.location ? `${import_path2.default.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
122
- return {
123
- snippet: import_safe.default.strip(step.error?.snippet || ""),
124
- title: step.title,
125
- location: step.error ? location2 : ""
126
- };
127
- }),
128
- logs: import_safe.default.strip(result.stdout.concat(result.stderr).map((log) => log).join("\n")),
129
- filePath,
130
- projects: this.projectSet,
131
- base64Image: this.config.base64Image
132
- };
133
- if (result.attachments) {
134
- const screenshot = result.attachments.find((attachment) => attachment.name === "screenshot");
135
- if (this.config.base64Image) {
136
- if (screenshot && screenshot.path) {
137
- const screenshotContent = import_fs.default.readFileSync(screenshot.path, "base64");
138
- testResult.screenshotPath = `data:image/png;base64,${screenshotContent}`;
139
- }
140
- } else {
141
- if (screenshot && screenshot.path) {
142
- testResult.screenshotPath = import_path2.default.resolve(screenshot.path);
143
- }
144
- }
145
- const tracePath = result.attachments.find((attachment) => attachment.name === "trace");
146
- if (tracePath?.path) {
147
- testResult.tracePath = import_path2.default.resolve(__dirname, tracePath.path);
148
- }
149
- const videoPath = result.attachments.find((attachment) => attachment.name === "video");
150
- if (videoPath?.path) {
151
- testResult.videoPath = import_path2.default.resolve(__dirname, videoPath.path);
152
- }
153
- }
154
- this.results.push(testResult);
155
- }
156
- onEnd(result) {
157
- const filteredResults = this.results.filter((r) => r.status !== "skipped" && !r.isRetry);
158
- const totalDuration = msToTime(result.duration);
159
- this.groupedResults = this.results.reduce((acc, result2, index) => {
160
- const filePath = result2.filePath;
161
- const suiteName = result2.suite;
162
- const projectName = result2.projectName;
163
- if (!acc[filePath]) {
164
- acc[filePath] = {};
165
- }
166
- if (!acc[filePath][suiteName]) {
167
- acc[filePath][suiteName] = {};
168
- }
169
- if (!acc[filePath][suiteName][projectName]) {
170
- acc[filePath][suiteName][projectName] = [];
171
- }
172
- acc[filePath][suiteName][projectName].push({ ...result2, index });
173
- return acc;
174
- }, {});
175
- import_handlebars.default.registerHelper("json", function(context) {
176
- return safeStringify(context);
177
- });
178
- import_handlebars.default.registerHelper("eq", function(actualStatus, expectedStatus) {
179
- return actualStatus === expectedStatus;
180
- });
181
- import_handlebars.default.registerHelper("or", () => {
182
- var args = Array.prototype.slice.call(arguments);
183
- var options = args.pop();
184
- for (var i = 0; i < args.length; i++) {
185
- if (args[i]) {
186
- return options.fn(this);
187
- }
188
- }
189
- return options.inverse(this);
190
- });
191
- import_handlebars.default.registerHelper("gt", function(a, b) {
192
- return a > b;
193
- });
194
- const html = this.generateHTML(filteredResults, totalDuration);
195
- const outputPath = import_path2.default.resolve(process.cwd(), "ortoni-report.html");
196
- import_fs.default.writeFileSync(outputPath, html);
197
- console.log(`Ortoni HTML report generated at ${outputPath}`);
198
- }
199
- generateHTML(filteredResults, totalDuration) {
200
- const totalTests = filteredResults.length;
201
- const passedTests = this.results.filter((r) => r.status === "passed").length;
202
- const flakyTests = this.results.filter((r) => r.flaky === "flaky").length;
203
- const failed = filteredResults.filter((r) => r.status === "failed" || r.status === "timedOut").length;
204
- const successRate = ((passedTests + flakyTests) / totalTests * 100).toFixed(2);
205
- const templateSource = import_fs.default.readFileSync(import_path2.default.resolve(__dirname, "report-template.hbs"), "utf-8");
206
- const template = import_handlebars.default.compile(templateSource);
207
- const data = {
208
- totalDuration,
209
- suiteName: this.suiteName,
210
- results: this.results,
211
- retryCount: this.results.filter((r) => r.isRetry).length,
212
- passCount: passedTests,
213
- failCount: failed,
214
- skipCount: this.results.filter((r) => r.status === "skipped").length,
215
- flakyCount: flakyTests,
216
- totalCount: filteredResults.length,
217
- groupedResults: this.groupedResults,
218
- projectName: this.config.projectName,
219
- authorName: this.config.authorName,
220
- testType: this.config.testType,
221
- preferredTheme: this.config.preferredTheme,
222
- successRate,
223
- lastRunDate: formatDate(/* @__PURE__ */ new Date()),
224
- projects: this.projectSet
225
- };
226
- return template(data);
227
- }
228
- };
229
- function safeStringify(obj, indent = 2) {
230
- const cache = /* @__PURE__ */ new Set();
231
- const json = JSON.stringify(obj, (key, value) => {
232
- if (typeof value === "object" && value !== null) {
233
- if (cache.has(value)) {
234
- return;
235
- }
236
- cache.add(value);
237
- }
238
- return value;
239
- }, indent);
240
- cache.clear();
241
- return json;
242
- }
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/ortoni-report.ts
31
+ var ortoni_report_exports = {};
32
+ __export(ortoni_report_exports, {
33
+ default: () => OrtoniReport
34
+ });
35
+ module.exports = __toCommonJS(ortoni_report_exports);
36
+ var import_fs = __toESM(require("fs"));
37
+ var import_path2 = __toESM(require("path"));
38
+ var import_handlebars = __toESM(require("handlebars"));
39
+ var import_safe = __toESM(require("colors/safe"));
40
+
41
+ // src/utils/utils.ts
42
+ var import_path = __toESM(require("path"));
43
+ function msToTime(duration) {
44
+ const milliseconds = Math.floor(duration % 1e3);
45
+ const seconds = Math.floor(duration / 1e3 % 60);
46
+ const minutes = Math.floor(duration / (1e3 * 60) % 60);
47
+ const hours = Math.floor(duration / (1e3 * 60 * 60) % 24);
48
+ let result = "";
49
+ if (hours > 0) {
50
+ result += `${hours}h:`;
51
+ }
52
+ if (minutes > 0 || hours > 0) {
53
+ result += `${minutes < 10 ? "0" + minutes : minutes}m:`;
54
+ }
55
+ if (seconds > 0 || minutes > 0 || hours > 0) {
56
+ result += `${seconds < 10 ? "0" + seconds : seconds}s`;
57
+ }
58
+ if (milliseconds > 0 && !(seconds > 0 || minutes > 0 || hours > 0)) {
59
+ result += `${milliseconds}ms`;
60
+ } else if (milliseconds > 0) {
61
+ result += `:${milliseconds < 100 ? "0" + milliseconds : milliseconds}ms`;
62
+ }
63
+ return result;
64
+ }
65
+ function normalizeFilePath(filePath) {
66
+ const normalizedPath = import_path.default.normalize(filePath);
67
+ return import_path.default.basename(normalizedPath);
68
+ }
69
+ function formatDate(date) {
70
+ const day = String(date.getDate()).padStart(2, "0");
71
+ const month = date.toLocaleString("default", { month: "short" });
72
+ const year = date.getFullYear();
73
+ const time = date.toLocaleTimeString();
74
+ return `${day}-${month}-${year} ${time}`;
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
+ }
90
+
91
+ // src/ortoni-report.ts
92
+ var OrtoniReport = class {
93
+ constructor(config = {}) {
94
+ this.projectRoot = "";
95
+ this.results = [];
96
+ this.projectSet = /* @__PURE__ */ new Set();
97
+ this.tagsSet = /* @__PURE__ */ new Set();
98
+ this.config = config;
99
+ }
100
+ onBegin(config, suite) {
101
+ this.results = [];
102
+ this.projectRoot = config.rootDir;
103
+ }
104
+ onTestBegin(test, result) {
105
+ }
106
+ onTestEnd(test, result) {
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
+ }
171
+ }
172
+ this.results.push(testResult);
173
+ } catch (error) {
174
+ console.error("OrtoniReport: Error processing test end:", error);
175
+ }
176
+ }
177
+ onEnd(result) {
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] = {};
187
+ }
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
+ }
210
+ }
211
+ generateHTML(filteredResults, totalDuration) {
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
+ }
246
+ }
247
+ };
@@ -1,211 +1,216 @@
1
- // src/ortoni-report.ts
2
- import fs from "fs";
3
- import path2 from "path";
4
- import Handlebars from "handlebars";
5
- import colors from "colors/safe";
6
-
7
- // src/utils/utils.ts
8
- import path from "path";
9
- function msToTime(duration) {
10
- const milliseconds = Math.floor(duration % 1e3);
11
- const seconds = Math.floor(duration / 1e3 % 60);
12
- const minutes = Math.floor(duration / (1e3 * 60) % 60);
13
- const hours = Math.floor(duration / (1e3 * 60 * 60) % 24);
14
- let result = "";
15
- if (hours > 0) {
16
- result += `${hours}h:`;
17
- }
18
- if (minutes > 0 || hours > 0) {
19
- result += `${minutes < 10 ? "0" + minutes : minutes}m:`;
20
- }
21
- if (seconds > 0 || minutes > 0 || hours > 0) {
22
- result += `${seconds < 10 ? "0" + seconds : seconds}s`;
23
- }
24
- if (milliseconds > 0 && !(seconds > 0 || minutes > 0 || hours > 0)) {
25
- result += `${milliseconds}ms`;
26
- } else if (milliseconds > 0) {
27
- result += `:${milliseconds < 100 ? "0" + milliseconds : milliseconds}ms`;
28
- }
29
- return result;
30
- }
31
- function normalizeFilePath(filePath) {
32
- const normalizedPath = path.normalize(filePath);
33
- return path.basename(normalizedPath);
34
- }
35
- function formatDate(date) {
36
- const day = String(date.getDate()).padStart(2, "0");
37
- const month = date.toLocaleString("default", { month: "short" });
38
- const year = date.getFullYear();
39
- const time = date.toLocaleTimeString();
40
- return `${day}-${month}-${year} ${time}`;
41
- }
42
-
43
- // src/ortoni-report.ts
44
- var OrtoniReport = class {
45
- constructor(config = {}) {
46
- this.projectRoot = "";
47
- this.results = [];
48
- this.projectSet = /* @__PURE__ */ new Set();
49
- // TODO: add tags to the filter dropdown
50
- this.tagsSet = /* @__PURE__ */ new Set();
51
- this.config = config;
52
- }
53
- onBegin(config, suite) {
54
- this.results = [];
55
- this.projectRoot = config.rootDir;
56
- }
57
- onTestBegin(test, result) {
58
- }
59
- onTestEnd(test, result) {
60
- let status = result.status;
61
- if (test.outcome() === "flaky") {
62
- status = "flaky";
63
- }
64
- const projectName = test.titlePath()[1];
65
- this.projectSet.add(projectName);
66
- const location = test.location;
67
- const filePath = normalizeFilePath(test.titlePath()[2]);
68
- const tagPattern = /@[\w]+/g;
69
- const testTags = test.title.match(tagPattern) || [];
70
- const title = test.title.replace(tagPattern, "").trim();
71
- const suiteTags = test.titlePath()[3].match(tagPattern) || [];
72
- const suite = test.titlePath()[3].replace(tagPattern, "").trim();
73
- const testResult = {
74
- suiteTags,
75
- testTags,
76
- location: `${filePath}:${location.line}:${location.column}`,
77
- retry: result.retry > 0 ? "retry" : "",
78
- isRetry: result.retry,
79
- projectName,
80
- suite,
81
- title,
82
- status,
83
- flaky: test.outcome(),
84
- duration: msToTime(result.duration),
85
- errors: result.errors.map((e) => colors.strip(e.message || e.toString())),
86
- steps: result.steps.map((step) => {
87
- const location2 = step.location ? `${path2.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
88
- return {
89
- snippet: colors.strip(step.error?.snippet || ""),
90
- title: step.title,
91
- location: step.error ? location2 : ""
92
- };
93
- }),
94
- logs: colors.strip(result.stdout.concat(result.stderr).map((log) => log).join("\n")),
95
- filePath,
96
- projects: this.projectSet,
97
- base64Image: this.config.base64Image
98
- };
99
- if (result.attachments) {
100
- const screenshot = result.attachments.find((attachment) => attachment.name === "screenshot");
101
- if (this.config.base64Image) {
102
- if (screenshot && screenshot.path) {
103
- const screenshotContent = fs.readFileSync(screenshot.path, "base64");
104
- testResult.screenshotPath = `data:image/png;base64,${screenshotContent}`;
105
- }
106
- } else {
107
- if (screenshot && screenshot.path) {
108
- testResult.screenshotPath = path2.resolve(screenshot.path);
109
- }
110
- }
111
- const tracePath = result.attachments.find((attachment) => attachment.name === "trace");
112
- if (tracePath?.path) {
113
- testResult.tracePath = path2.resolve(__dirname, tracePath.path);
114
- }
115
- const videoPath = result.attachments.find((attachment) => attachment.name === "video");
116
- if (videoPath?.path) {
117
- testResult.videoPath = path2.resolve(__dirname, videoPath.path);
118
- }
119
- }
120
- this.results.push(testResult);
121
- }
122
- onEnd(result) {
123
- const filteredResults = this.results.filter((r) => r.status !== "skipped" && !r.isRetry);
124
- const totalDuration = msToTime(result.duration);
125
- this.groupedResults = this.results.reduce((acc, result2, index) => {
126
- const filePath = result2.filePath;
127
- const suiteName = result2.suite;
128
- const projectName = result2.projectName;
129
- if (!acc[filePath]) {
130
- acc[filePath] = {};
131
- }
132
- if (!acc[filePath][suiteName]) {
133
- acc[filePath][suiteName] = {};
134
- }
135
- if (!acc[filePath][suiteName][projectName]) {
136
- acc[filePath][suiteName][projectName] = [];
137
- }
138
- acc[filePath][suiteName][projectName].push({ ...result2, index });
139
- return acc;
140
- }, {});
141
- Handlebars.registerHelper("json", function(context) {
142
- return safeStringify(context);
143
- });
144
- Handlebars.registerHelper("eq", function(actualStatus, expectedStatus) {
145
- return actualStatus === expectedStatus;
146
- });
147
- Handlebars.registerHelper("or", () => {
148
- var args = Array.prototype.slice.call(arguments);
149
- var options = args.pop();
150
- for (var i = 0; i < args.length; i++) {
151
- if (args[i]) {
152
- return options.fn(this);
153
- }
154
- }
155
- return options.inverse(this);
156
- });
157
- Handlebars.registerHelper("gt", function(a, b) {
158
- return a > b;
159
- });
160
- const html = this.generateHTML(filteredResults, totalDuration);
161
- const outputPath = path2.resolve(process.cwd(), "ortoni-report.html");
162
- fs.writeFileSync(outputPath, html);
163
- console.log(`Ortoni HTML report generated at ${outputPath}`);
164
- }
165
- generateHTML(filteredResults, totalDuration) {
166
- const totalTests = filteredResults.length;
167
- const passedTests = this.results.filter((r) => r.status === "passed").length;
168
- const flakyTests = this.results.filter((r) => r.flaky === "flaky").length;
169
- const failed = filteredResults.filter((r) => r.status === "failed" || r.status === "timedOut").length;
170
- const successRate = ((passedTests + flakyTests) / totalTests * 100).toFixed(2);
171
- const templateSource = fs.readFileSync(path2.resolve(__dirname, "report-template.hbs"), "utf-8");
172
- const template = Handlebars.compile(templateSource);
173
- const data = {
174
- totalDuration,
175
- suiteName: this.suiteName,
176
- results: this.results,
177
- retryCount: this.results.filter((r) => r.isRetry).length,
178
- passCount: passedTests,
179
- failCount: failed,
180
- skipCount: this.results.filter((r) => r.status === "skipped").length,
181
- flakyCount: flakyTests,
182
- totalCount: filteredResults.length,
183
- groupedResults: this.groupedResults,
184
- projectName: this.config.projectName,
185
- authorName: this.config.authorName,
186
- testType: this.config.testType,
187
- preferredTheme: this.config.preferredTheme,
188
- successRate,
189
- lastRunDate: formatDate(/* @__PURE__ */ new Date()),
190
- projects: this.projectSet
191
- };
192
- return template(data);
193
- }
194
- };
195
- function safeStringify(obj, indent = 2) {
196
- const cache = /* @__PURE__ */ new Set();
197
- const json = JSON.stringify(obj, (key, value) => {
198
- if (typeof value === "object" && value !== null) {
199
- if (cache.has(value)) {
200
- return;
201
- }
202
- cache.add(value);
203
- }
204
- return value;
205
- }, indent);
206
- cache.clear();
207
- return json;
208
- }
209
- export {
210
- OrtoniReport as default
211
- };
1
+ // src/ortoni-report.ts
2
+ import fs from "fs";
3
+ import path2 from "path";
4
+ import Handlebars from "handlebars";
5
+ import colors from "colors/safe";
6
+
7
+ // src/utils/utils.ts
8
+ import path from "path";
9
+ function msToTime(duration) {
10
+ const milliseconds = Math.floor(duration % 1e3);
11
+ const seconds = Math.floor(duration / 1e3 % 60);
12
+ const minutes = Math.floor(duration / (1e3 * 60) % 60);
13
+ const hours = Math.floor(duration / (1e3 * 60 * 60) % 24);
14
+ let result = "";
15
+ if (hours > 0) {
16
+ result += `${hours}h:`;
17
+ }
18
+ if (minutes > 0 || hours > 0) {
19
+ result += `${minutes < 10 ? "0" + minutes : minutes}m:`;
20
+ }
21
+ if (seconds > 0 || minutes > 0 || hours > 0) {
22
+ result += `${seconds < 10 ? "0" + seconds : seconds}s`;
23
+ }
24
+ if (milliseconds > 0 && !(seconds > 0 || minutes > 0 || hours > 0)) {
25
+ result += `${milliseconds}ms`;
26
+ } else if (milliseconds > 0) {
27
+ result += `:${milliseconds < 100 ? "0" + milliseconds : milliseconds}ms`;
28
+ }
29
+ return result;
30
+ }
31
+ function normalizeFilePath(filePath) {
32
+ const normalizedPath = path.normalize(filePath);
33
+ return path.basename(normalizedPath);
34
+ }
35
+ function formatDate(date) {
36
+ const day = String(date.getDate()).padStart(2, "0");
37
+ const month = date.toLocaleString("default", { month: "short" });
38
+ const year = date.getFullYear();
39
+ const time = date.toLocaleTimeString();
40
+ return `${day}-${month}-${year} ${time}`;
41
+ }
42
+ function safeStringify(obj, indent = 2) {
43
+ const cache = /* @__PURE__ */ new Set();
44
+ const json = JSON.stringify(obj, (key, value) => {
45
+ if (typeof value === "object" && value !== null) {
46
+ if (cache.has(value)) {
47
+ return;
48
+ }
49
+ cache.add(value);
50
+ }
51
+ return value;
52
+ }, indent);
53
+ cache.clear();
54
+ return json;
55
+ }
56
+
57
+ // src/ortoni-report.ts
58
+ var OrtoniReport = class {
59
+ constructor(config = {}) {
60
+ this.projectRoot = "";
61
+ this.results = [];
62
+ this.projectSet = /* @__PURE__ */ new Set();
63
+ this.tagsSet = /* @__PURE__ */ new Set();
64
+ this.config = config;
65
+ }
66
+ onBegin(config, suite) {
67
+ this.results = [];
68
+ this.projectRoot = config.rootDir;
69
+ }
70
+ onTestBegin(test, result) {
71
+ }
72
+ onTestEnd(test, result) {
73
+ try {
74
+ let status = result.status;
75
+ if (test.outcome() === "flaky") {
76
+ status = "flaky";
77
+ }
78
+ const projectName = test.titlePath()[1];
79
+ this.projectSet.add(projectName);
80
+ const location = test.location;
81
+ const filePath = normalizeFilePath(test.titlePath()[2]);
82
+ const tagPattern = /@[\w]+/g;
83
+ const testTags = test.title.match(tagPattern) || [];
84
+ const title = test.title.replace(tagPattern, "").trim();
85
+ const suiteTags = test.titlePath()[3].match(tagPattern) || [];
86
+ const suite = test.titlePath()[3].replace(tagPattern, "").trim();
87
+ const testResult = {
88
+ suiteTags,
89
+ testTags,
90
+ location: `${filePath}:${location.line}:${location.column}`,
91
+ retry: result.retry > 0 ? "retry" : "",
92
+ isRetry: result.retry,
93
+ projectName,
94
+ suite,
95
+ title,
96
+ status,
97
+ flaky: test.outcome(),
98
+ duration: msToTime(result.duration),
99
+ errors: result.errors.map((e) => colors.strip(e.message || e.toString())),
100
+ steps: result.steps.map((step) => {
101
+ const location2 = step.location ? `${path2.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
102
+ return {
103
+ snippet: colors.strip(step.error?.snippet || ""),
104
+ title: step.title,
105
+ location: step.error ? location2 : ""
106
+ };
107
+ }),
108
+ logs: colors.strip(result.stdout.concat(result.stderr).map((log) => log).join("\n")),
109
+ filePath,
110
+ projects: this.projectSet,
111
+ base64Image: this.config.base64Image
112
+ };
113
+ if (result.attachments) {
114
+ const screenshot = result.attachments.find((attachment) => attachment.name === "screenshot");
115
+ if (this.config.base64Image) {
116
+ if (screenshot && screenshot.path) {
117
+ try {
118
+ const screenshotContent = fs.readFileSync(screenshot.path, "base64");
119
+ testResult.screenshotPath = `data:image/png;base64,${screenshotContent}`;
120
+ } catch (error) {
121
+ console.error(`OrtoniReport: Failed to read screenshot file: ${screenshot.path}`, error);
122
+ }
123
+ }
124
+ } else {
125
+ if (screenshot && screenshot.path) {
126
+ testResult.screenshotPath = path2.resolve(screenshot.path);
127
+ }
128
+ }
129
+ const tracePath = result.attachments.find((attachment) => attachment.name === "trace");
130
+ if (tracePath?.path) {
131
+ testResult.tracePath = path2.resolve(__dirname, tracePath.path);
132
+ }
133
+ const videoPath = result.attachments.find((attachment) => attachment.name === "video");
134
+ if (videoPath?.path) {
135
+ testResult.videoPath = path2.resolve(__dirname, videoPath.path);
136
+ }
137
+ }
138
+ this.results.push(testResult);
139
+ } catch (error) {
140
+ console.error("OrtoniReport: Error processing test end:", error);
141
+ }
142
+ }
143
+ onEnd(result) {
144
+ try {
145
+ const filteredResults = this.results.filter((r) => r.status !== "skipped" && !r.isRetry);
146
+ const totalDuration = msToTime(result.duration);
147
+ this.groupedResults = this.results.reduce((acc, result2, index) => {
148
+ const filePath = result2.filePath;
149
+ const suiteName = result2.suite;
150
+ const projectName = result2.projectName;
151
+ if (!acc[filePath]) {
152
+ acc[filePath] = {};
153
+ }
154
+ if (!acc[filePath][suiteName]) {
155
+ acc[filePath][suiteName] = {};
156
+ }
157
+ if (!acc[filePath][suiteName][projectName]) {
158
+ acc[filePath][suiteName][projectName] = [];
159
+ }
160
+ acc[filePath][suiteName][projectName].push({ ...result2, index });
161
+ return acc;
162
+ }, {});
163
+ Handlebars.registerHelper("json", function(context) {
164
+ return safeStringify(context);
165
+ });
166
+ Handlebars.registerHelper("eq", function(actualStatus, expectedStatus) {
167
+ return actualStatus === expectedStatus;
168
+ });
169
+ const html = this.generateHTML(filteredResults, totalDuration);
170
+ const outputPath = path2.resolve(process.cwd(), "ortoni-report.html");
171
+ fs.writeFileSync(outputPath, html);
172
+ console.log(`Ortoni HTML report generated at ${outputPath}`);
173
+ } catch (error) {
174
+ console.error("OrtoniReport: Error generating report:", error);
175
+ }
176
+ }
177
+ generateHTML(filteredResults, totalDuration) {
178
+ try {
179
+ const totalTests = filteredResults.length;
180
+ const passedTests = this.results.filter((r) => r.status === "passed").length;
181
+ const flakyTests = this.results.filter((r) => r.flaky === "flaky").length;
182
+ const failed = filteredResults.filter((r) => r.status === "failed" || r.status === "timedOut").length;
183
+ const successRate = ((passedTests + flakyTests) / totalTests * 100).toFixed(2);
184
+ const templateSource = fs.readFileSync(path2.resolve(__dirname, "report-template.hbs"), "utf-8");
185
+ const template = Handlebars.compile(templateSource);
186
+ const logo = this.config.logo;
187
+ const data = {
188
+ logo: logo ? path2.resolve(logo) : void 0,
189
+ totalDuration,
190
+ suiteName: this.suiteName,
191
+ results: this.results,
192
+ retryCount: this.results.filter((r) => r.isRetry).length,
193
+ passCount: passedTests,
194
+ failCount: failed,
195
+ skipCount: this.results.filter((r) => r.status === "skipped").length,
196
+ flakyCount: flakyTests,
197
+ totalCount: filteredResults.length,
198
+ groupedResults: this.groupedResults,
199
+ projectName: this.config.projectName,
200
+ authorName: this.config.authorName,
201
+ testType: this.config.testType,
202
+ preferredTheme: this.config.preferredTheme,
203
+ successRate,
204
+ lastRunDate: formatDate(/* @__PURE__ */ new Date()),
205
+ projects: this.projectSet
206
+ };
207
+ return template(data);
208
+ } catch (error) {
209
+ console.error("OrtoniReport: Error generating HTML:", error);
210
+ return `<html><body><h1>Report generation failed</h1><pre>${error.stack}</pre></body></html>`;
211
+ }
212
+ }
213
+ };
214
+ export {
215
+ OrtoniReport as default
216
+ };
@@ -4,8 +4,8 @@
4
4
  <head>
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <meta name="description" content="Plawright HTML report by LetCode Koushik - V1.1.6">
8
- <title>Playwright Test Report</title>
7
+ <meta name="description" content="Playwright HTML report by LetCode Koushik - V1.1.7">
8
+ <title>Ortoni Playwright Test Report</title>
9
9
  <link rel="icon" href="node_modules/ortoni-report/dist/icon/32.png" type="image/x-icon">
10
10
  <link rel="stylesheet" href="node_modules/ortoni-report/dist/css/main.css">
11
11
  </head>
@@ -13,33 +13,28 @@
13
13
  #summary, #testDetails, button#back-to-summary {
14
14
  transition: opacity 0.2s ease-in-out;
15
15
  }
16
- .filter.active {
16
+
17
+ .filter.active {
17
18
  background-color: var(--bulma-background-active);
18
19
  }
19
-
20
20
  ::-webkit-scrollbar {
21
- width: 0px;
22
- background-color: transparent;
21
+ width: 8px;
22
+ height: 8px;
23
23
  }
24
24
 
25
- ::-webkit-scrollbar-thumb {
26
- background-color: var(--pico-secondary-background);
27
- ;
28
- border-radius: 0px;
25
+ ::-webkit-scrollbar-track {
26
+ background: #f1f1f1;
29
27
  }
30
28
 
31
- ::-webkit-scrollbar-thumb:hover {
32
- background-color: var(--pico-secondary-background);
33
- ;
29
+ ::-webkit-scrollbar-thumb {
30
+ background: #888;
31
+ border-radius: 4px;
34
32
  }
35
33
 
36
- ::-webkit-scrollbar-track {
37
- background-color: #f1f1f1;
34
+ ::-webkit-scrollbar-thumb:hover {
35
+ background: #555;
38
36
  }
39
37
 
40
- ::-webkit-scrollbar-corner {
41
- background-color: #fff;
42
- }
43
38
  div#testDetails {
44
39
  position: sticky;
45
40
  top: 0;
@@ -74,17 +69,27 @@
74
69
  details[open] > summary::after {
75
70
  transform: rotate(0deg);
76
71
  }
72
+ .logoimage{
73
+ max-width: 100px;
74
+ }
77
75
 
78
76
  </style>
79
77
 
80
78
  <body>
81
- <!-- Header -->
82
79
  <section class="section">
83
80
  <header class="container">
84
81
  <div class="columns is-vcentered">
85
- <div class="column is-one-third">
86
- {{!-- Custom Project Name --}}
87
- {{#if projectName}}<h1 class="title">{{projectName}}</h1>{{/if}}
82
+ <div class="column is-two-fifths">
83
+ <div class="columns is-desktop is-vcentered is-multiline">
84
+ {{#if logo}}
85
+ <div class="column-1">
86
+ <figure class="image logoimage">
87
+ <img src="{{logo}}" />
88
+ </figure>
89
+ </div>
90
+ {{/if}}
91
+ {{#if projectName}}<div class="column"><span class="title">{{projectName}}</span></div>{{/if}}
92
+ </div>
88
93
  </div>
89
94
  <div class="column">
90
95
  <div class="control">
@@ -99,12 +104,10 @@
99
104
  </div>
100
105
  </header>
101
106
  </section>
102
-
103
- <!-- Main Content -->
104
107
  <section class="section">
105
108
  <main class="container">
106
109
  <div class="columns">
107
- <aside class="column is-one-third sidebar">
110
+ <aside class="column is-two-fifths">
108
111
  <div class="columns is-mobile">
109
112
  <div class="column">
110
113
  <h2 class="title is-4">Tests</h2>
@@ -120,7 +123,7 @@
120
123
  </div>
121
124
  </div>
122
125
  </div>
123
- <div class="content">
126
+ <div class="content sidebar">
124
127
  {{#each groupedResults}}
125
128
  <details class="box">
126
129
  <summary class="is-size-5 has-icon-right">
@@ -133,7 +136,7 @@
133
136
  </summary>
134
137
  <ul>
135
138
  {{#each this}}
136
- <details>
139
+ <details class="mt-1">
137
140
  <summary class="is-size-5 is-capitalized">
138
141
  <div class="icon-text">
139
142
  <span class="icon has-text-info">
@@ -144,7 +147,7 @@
144
147
  </summary>
145
148
  <ul>
146
149
  {{#each this}}
147
- <details>
150
+ <details class="mb-1">
148
151
  <summary class="is-capitalized is-size-6">{{@key}}</summary>
149
152
  <ul>
150
153
  {{#each this}}
@@ -202,7 +205,7 @@
202
205
  {{/each}}
203
206
  </div>
204
207
  </aside>
205
- <section class="column">
208
+ <section class="column is-three-fifths">
206
209
  {{!-- Overall summary --}}
207
210
  <div id="summary">
208
211
  <div class="columns is-multiline">
@@ -438,13 +441,13 @@
438
441
  <div id="testImage" class="modal">
439
442
  <div class="modal-background"></div>
440
443
  <div class="modal-content">
441
- <p class="image is-16by9">
444
+ <p class="image">
442
445
  <img src="${test.base64Image ? test.screenshotPath : `file://${test.screenshotPath}`}" alt="Screenshot">
443
446
  </p>
444
447
  </div>
445
448
  <button onclick="closeModal()" class="modal-close is-large" aria-label="close"></button>
446
449
  </div>
447
- <figure class="image">
450
+ <figure class="image box">
448
451
  <img onclick="openModal()" src="${test.base64Image ? test.screenshotPath : `file://${test.screenshotPath}`}" alt="Screenshot">
449
452
  </figure>` : ''}
450
453
  </div>
@@ -634,7 +637,7 @@
634
637
  maintainAspectRatio: false,
635
638
  plugins: {
636
639
  legend: {
637
- position: 'bottom'
640
+ position: 'right'
638
641
  }
639
642
  }
640
643
  }
@@ -646,5 +649,4 @@
646
649
  </script>
647
650
  <script src="node_modules/ortoni-report/dist/utils/modal.js"></script>
648
651
  </body>
649
-
650
652
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ortoni-report",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Playwright Report By LetCode with Koushik",
5
5
  "scripts": {
6
6
  "test": "npx playwright test",
package/readme.md CHANGED
@@ -40,6 +40,9 @@ Explore the live demo: [OrtoniReport Demo](https://ortoni.netlify.app/)
40
40
  - Easily integrate with Playwright configurations using TypeScript/JavaScript.
41
41
  - Configure reporting preferences within your Playwright setup.
42
42
 
43
+ 10. **Add logo to the report**
44
+ - Add relative or absolute path of the image to the config
45
+
43
46
  ### Configurable Report Generation
44
47
 
45
48
  Configure OrtoniReport in your `playwright.config.ts`:
@@ -49,12 +52,13 @@ import { defineConfig } from '@playwright/test';
49
52
  import { OrtoniReportConfig } from 'ortoni-report';
50
53
 
51
54
  const reportConfig: OrtoniReportConfig = {
52
- projectName: 'Ortoni',
53
- testType: 'Regression',
54
- authorName: 'Koushik',
55
- preferredTheme: 'light',
56
- base64Image: false, // Example configuration
57
- };
55
+ logo: "path/logo.png",
56
+ authorName: "LetCode Koushik",
57
+ base64Image: false,
58
+ preferredTheme: "dark",
59
+ projectName: "Ortoni Demo Report",
60
+ testType: "Smoke"
61
+ }
58
62
 
59
63
  export default defineConfig({
60
64
  reporter: [['ortoni-report', reportConfig], ['dot']],