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.
@@ -1,13 +1,54 @@
1
- import { TestStep, Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
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: TestStep[];
61
+ steps: Steps[];
21
62
  logs: string;
22
- screenshotPath: string | null;
23
- filePath: any;
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;
@@ -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
- let status = result.status;
91
- if (test.outcome() === "flaky") {
92
- status = "flaky";
93
- }
94
- this.projectSet.add(test.titlePath()[1]);
95
- const testResult = {
96
- retry: result.retry > 0 ? "retry" : "",
97
- isRetry: result.retry,
98
- projectName: test.titlePath()[1],
99
- suite: test.titlePath()[3],
100
- title: test.title,
101
- status,
102
- flaky: test.outcome(),
103
- duration: msToTime(result.duration),
104
- errors: result.errors.map((e) => import_safe.default.strip(e.message || e.toString())),
105
- steps: result.steps.map((step) => ({
106
- titlePath: step.titlePath,
107
- category: step.category,
108
- duration: step.duration,
109
- error: step.error,
110
- location: step.location,
111
- parent: step.parent,
112
- startTime: step.startTime,
113
- steps: step.steps,
114
- title: step.title
115
- })),
116
- logs: import_safe.default.strip(result.stdout.concat(result.stderr).map((log) => log).join("\n")),
117
- screenshotPath: null,
118
- filePath: normalizeFilePath(test.titlePath()[2]),
119
- projects: this.projectSet
120
- };
121
- if (result.attachments) {
122
- const screenshot = result.attachments.find((attachment) => attachment.name === "screenshot");
123
- if (screenshot && screenshot.path) {
124
- const screenshotContent = import_fs.default.readFileSync(screenshot.path, "base64");
125
- testResult.screenshotPath = screenshotContent;
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
- const filteredResults = this.results.filter((r) => r.status !== "skipped" && !r.isRetry);
132
- const totalDuration = msToTime(result.duration);
133
- this.groupedResults = this.results.reduce((acc, result2, index) => {
134
- const filePath = result2.filePath;
135
- const suiteName = result2.suite;
136
- const projectName = result2.projectName;
137
- if (!acc[filePath]) {
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
- return options.inverse(this);
164
- });
165
- import_handlebars.default.registerHelper("gt", function(a, b) {
166
- return a > b;
167
- });
168
- const html = this.generateHTML(filteredResults, totalDuration);
169
- const outputPath = import_path2.default.resolve(process.cwd(), "ortoni-report.html");
170
- import_fs.default.writeFileSync(outputPath, html);
171
- console.log(`Ortoni HTML report generated at ${outputPath}`);
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
- const totalTests = filteredResults.length;
175
- const passedTests = this.results.filter((r) => r.status === "passed").length;
176
- const flakyTests = this.results.filter((r) => r.flaky === "flaky").length;
177
- const failed = filteredResults.filter((r) => r.status === "failed" || r.status === "timedOut").length;
178
- const successRate = ((passedTests + flakyTests) / totalTests * 100).toFixed(2);
179
- const templateSource = import_fs.default.readFileSync(import_path2.default.resolve(__dirname, "report-template.hbs"), "utf-8");
180
- const template = import_handlebars.default.compile(templateSource);
181
- const data = {
182
- totalDuration,
183
- suiteName: this.suiteName,
184
- results: this.results,
185
- retryCount: this.results.filter((r) => r.isRetry).length,
186
- passCount: passedTests,
187
- failCount: failed,
188
- skipCount: this.results.filter((r) => r.status === "skipped").length,
189
- flakyCount: flakyTests,
190
- totalCount: filteredResults.length,
191
- groupedResults: this.groupedResults,
192
- projectName: this.config.projectName,
193
- authorName: this.config.authorName,
194
- testType: this.config.testType,
195
- preferredTheme: this.config.preferredTheme,
196
- successRate,
197
- lastRunDate: formatDate(/* @__PURE__ */ new Date()),
198
- projects: this.projectSet
199
- };
200
- return template(data);
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
- }