ortoni-report 1.1.2 → 1.1.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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Change Log:
2
2
 
3
+ ## Version 1.1.3
4
+
5
+ **New Features:**
6
+ - Added detailed steps to the testDetails section in the HTML report.
7
+ - Introduced a new flaky icon for better visual representation in the report.
8
+ - Display of test steps in the HTML report.
9
+ - Added a filter for retry tests to better categorize and display them.
10
+
11
+ **Improved:**
12
+ - Updated the package dependencies to remove vulnerabilities.
13
+ - Enhanced time formatting to include milliseconds in the duration display.
14
+ - Enhanced the calculation and display of the success rate in the HTML report.
15
+
3
16
  ## Version 1.1.2
4
17
 
5
18
  **New Features:**
Binary file
@@ -1,4 +1,4 @@
1
- import { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
1
+ import { TestStep, Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
2
2
 
3
3
  interface ReporterConfig {
4
4
  projectName?: string;
@@ -6,6 +6,22 @@ interface ReporterConfig {
6
6
  testType?: string;
7
7
  }
8
8
 
9
+ interface TestResultData {
10
+ retry: string;
11
+ isRetry: number;
12
+ projectName: any;
13
+ suite: any;
14
+ title: string;
15
+ status: "passed" | "failed" | "timedOut" | "skipped" | "interrupted" | "expected" | "unexpected" | "flaky";
16
+ flaky: string;
17
+ duration: string;
18
+ errors: any[];
19
+ steps: TestStep[];
20
+ logs: string;
21
+ screenshotPath: string | null;
22
+ filePath: any;
23
+ }
24
+
9
25
  declare class OrtoniReport implements Reporter {
10
26
  private results;
11
27
  private groupedResults;
@@ -15,9 +31,8 @@ declare class OrtoniReport implements Reporter {
15
31
  onBegin(config: FullConfig, suite: Suite): void;
16
32
  onTestBegin(test: TestCase, result: TestResult): void;
17
33
  onTestEnd(test: TestCase, result: TestResult): void;
18
- private _successRate;
19
34
  onEnd(result: FullResult): void;
20
- generateHTML(): string;
35
+ generateHTML(filteredResults: TestResultData[], totalDuration: string): string;
21
36
  }
22
37
 
23
38
  export { OrtoniReport as default };
@@ -45,11 +45,20 @@ function msToTime(duration) {
45
45
  const seconds = Math.floor(duration / 1e3 % 60);
46
46
  const minutes = Math.floor(duration / (1e3 * 60) % 60);
47
47
  const hours = Math.floor(duration / (1e3 * 60 * 60) % 24);
48
- const hoursStr = hours < 10 ? "0" + hours : hours;
49
- const minutesStr = minutes < 10 ? "0" + minutes : minutes;
50
- const secondsStr = seconds < 10 ? "0" + seconds : seconds;
51
- const millisecondsStr = milliseconds < 100 ? "0" + milliseconds : milliseconds;
52
- return `${hoursStr}:${minutesStr}:${secondsStr}.${millisecondsStr}`;
48
+ let result = "";
49
+ if (hours > 0) {
50
+ result += (hours < 10 ? "0" + hours : 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) {
59
+ result += ":" + (milliseconds < 100 ? "0" + milliseconds : milliseconds) + "ms";
60
+ }
61
+ return result;
53
62
  }
54
63
  function normalizeFilePath(filePath) {
55
64
  const normalizedPath = import_path.default.normalize(filePath);
@@ -67,7 +76,6 @@ function formatDate(date) {
67
76
  var OrtoniReport = class {
68
77
  constructor(config = {}) {
69
78
  this.results = [];
70
- this._successRate = "";
71
79
  this.config = config;
72
80
  }
73
81
  onBegin(config, suite) {
@@ -86,22 +94,25 @@ var OrtoniReport = class {
86
94
  status = "flaky";
87
95
  }
88
96
  const testResult = {
97
+ retry: result.retry > 0 ? "retry" : "",
89
98
  isRetry: result.retry,
90
- totalDuration: "",
91
99
  projectName: test.titlePath()[1],
92
- // Get the project name
93
100
  suite: test.titlePath()[3],
94
- // Adjust the index based on your suite hierarchy
95
101
  title: test.title,
96
102
  status,
97
103
  flaky: test.outcome(),
98
104
  duration: msToTime(result.duration),
99
105
  errors: result.errors.map((e) => import_safe.default.strip(e.message || e.toString())),
100
106
  steps: result.steps.map((step) => ({
101
- title: step.title,
107
+ titlePath: step.titlePath,
102
108
  category: step.category,
103
109
  duration: step.duration,
104
- status: result.status
110
+ error: step.error,
111
+ location: step.location,
112
+ parent: step.parent,
113
+ startTime: step.startTime,
114
+ steps: step.steps,
115
+ title: step.title
105
116
  })),
106
117
  logs: import_safe.default.strip(result.stdout.concat(result.stderr).map((log) => log).join("\n")),
107
118
  screenshotPath: null,
@@ -123,8 +134,8 @@ var OrtoniReport = class {
123
134
  this.results.push(testResult);
124
135
  }
125
136
  onEnd(result) {
126
- this._successRate = (this.results.filter((r) => r.status === "passed").length / this.results.length * 100).toFixed(2);
127
- this.results[0].totalDuration = msToTime(result.duration);
137
+ const filteredResults = this.results.filter((r) => r.status !== "skipped" && !r.isRetry);
138
+ const totalDuration = msToTime(result.duration);
128
139
  this.groupedResults = this.results.reduce((acc, result2, index) => {
129
140
  const filePath = result2.filePath;
130
141
  const suiteName = result2.suite;
@@ -160,28 +171,34 @@ var OrtoniReport = class {
160
171
  import_handlebars.default.registerHelper("gt", function(a, b) {
161
172
  return a > b;
162
173
  });
163
- const html = this.generateHTML();
174
+ const html = this.generateHTML(filteredResults, totalDuration);
164
175
  const outputPath = import_path2.default.resolve(process.cwd(), "ortoni-report.html");
165
176
  import_fs.default.writeFileSync(outputPath, html);
166
177
  console.log(`Ortoni HTML report generated at ${outputPath}`);
167
178
  }
168
- generateHTML() {
179
+ generateHTML(filteredResults, totalDuration) {
180
+ const totalTests = filteredResults.length;
181
+ const passedTests = this.results.filter((r) => r.status === "passed").length;
182
+ const flakyTests = this.results.filter((r) => r.flaky === "flaky").length;
183
+ const failed = filteredResults.filter((r) => r.status === "failed" || r.status === "timedOut").length;
184
+ const successRate = ((passedTests + flakyTests) / totalTests * 100).toFixed(2);
169
185
  const templateSource = import_fs.default.readFileSync(import_path2.default.resolve(__dirname, "report-template.hbs"), "utf-8");
170
186
  const template = import_handlebars.default.compile(templateSource);
171
187
  const data = {
172
- totalDuration: this.results[0].totalDuration,
188
+ totalDuration,
173
189
  suiteName: this.suiteName,
174
190
  results: this.results,
175
- passCount: this.results.filter((r) => r.status === "passed").length,
176
- failCount: this.results.filter((r) => r.status === "failed" || r.status === "timedOut").length,
191
+ retryCount: this.results.filter((r) => r.isRetry).length,
192
+ passCount: passedTests,
193
+ failCount: failed,
177
194
  skipCount: this.results.filter((r) => r.status === "skipped").length,
178
- flakyCount: this.results.filter((r) => r.flaky === "flaky").length,
179
- totalCount: this.results.length,
195
+ flakyCount: flakyTests,
196
+ totalCount: filteredResults.length,
180
197
  groupedResults: this.groupedResults,
181
198
  projectName: this.config.projectName,
182
199
  authorName: this.config.authorName,
183
200
  testType: this.config.testType,
184
- successRate: this._successRate,
201
+ successRate,
185
202
  lastRunDate: formatDate(/* @__PURE__ */ new Date())
186
203
  };
187
204
  return template(data);
@@ -11,11 +11,20 @@ function msToTime(duration) {
11
11
  const seconds = Math.floor(duration / 1e3 % 60);
12
12
  const minutes = Math.floor(duration / (1e3 * 60) % 60);
13
13
  const hours = Math.floor(duration / (1e3 * 60 * 60) % 24);
14
- const hoursStr = hours < 10 ? "0" + hours : hours;
15
- const minutesStr = minutes < 10 ? "0" + minutes : minutes;
16
- const secondsStr = seconds < 10 ? "0" + seconds : seconds;
17
- const millisecondsStr = milliseconds < 100 ? "0" + milliseconds : milliseconds;
18
- return `${hoursStr}:${minutesStr}:${secondsStr}.${millisecondsStr}`;
14
+ let result = "";
15
+ if (hours > 0) {
16
+ result += (hours < 10 ? "0" + hours : 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) {
25
+ result += ":" + (milliseconds < 100 ? "0" + milliseconds : milliseconds) + "ms";
26
+ }
27
+ return result;
19
28
  }
20
29
  function normalizeFilePath(filePath) {
21
30
  const normalizedPath = path.normalize(filePath);
@@ -33,7 +42,6 @@ function formatDate(date) {
33
42
  var OrtoniReport = class {
34
43
  constructor(config = {}) {
35
44
  this.results = [];
36
- this._successRate = "";
37
45
  this.config = config;
38
46
  }
39
47
  onBegin(config, suite) {
@@ -52,22 +60,25 @@ var OrtoniReport = class {
52
60
  status = "flaky";
53
61
  }
54
62
  const testResult = {
63
+ retry: result.retry > 0 ? "retry" : "",
55
64
  isRetry: result.retry,
56
- totalDuration: "",
57
65
  projectName: test.titlePath()[1],
58
- // Get the project name
59
66
  suite: test.titlePath()[3],
60
- // Adjust the index based on your suite hierarchy
61
67
  title: test.title,
62
68
  status,
63
69
  flaky: test.outcome(),
64
70
  duration: msToTime(result.duration),
65
71
  errors: result.errors.map((e) => colors.strip(e.message || e.toString())),
66
72
  steps: result.steps.map((step) => ({
67
- title: step.title,
73
+ titlePath: step.titlePath,
68
74
  category: step.category,
69
75
  duration: step.duration,
70
- status: result.status
76
+ error: step.error,
77
+ location: step.location,
78
+ parent: step.parent,
79
+ startTime: step.startTime,
80
+ steps: step.steps,
81
+ title: step.title
71
82
  })),
72
83
  logs: colors.strip(result.stdout.concat(result.stderr).map((log) => log).join("\n")),
73
84
  screenshotPath: null,
@@ -89,8 +100,8 @@ var OrtoniReport = class {
89
100
  this.results.push(testResult);
90
101
  }
91
102
  onEnd(result) {
92
- this._successRate = (this.results.filter((r) => r.status === "passed").length / this.results.length * 100).toFixed(2);
93
- this.results[0].totalDuration = msToTime(result.duration);
103
+ const filteredResults = this.results.filter((r) => r.status !== "skipped" && !r.isRetry);
104
+ const totalDuration = msToTime(result.duration);
94
105
  this.groupedResults = this.results.reduce((acc, result2, index) => {
95
106
  const filePath = result2.filePath;
96
107
  const suiteName = result2.suite;
@@ -126,28 +137,34 @@ var OrtoniReport = class {
126
137
  Handlebars.registerHelper("gt", function(a, b) {
127
138
  return a > b;
128
139
  });
129
- const html = this.generateHTML();
140
+ const html = this.generateHTML(filteredResults, totalDuration);
130
141
  const outputPath = path2.resolve(process.cwd(), "ortoni-report.html");
131
142
  fs.writeFileSync(outputPath, html);
132
143
  console.log(`Ortoni HTML report generated at ${outputPath}`);
133
144
  }
134
- generateHTML() {
145
+ generateHTML(filteredResults, totalDuration) {
146
+ const totalTests = filteredResults.length;
147
+ const passedTests = this.results.filter((r) => r.status === "passed").length;
148
+ const flakyTests = this.results.filter((r) => r.flaky === "flaky").length;
149
+ const failed = filteredResults.filter((r) => r.status === "failed" || r.status === "timedOut").length;
150
+ const successRate = ((passedTests + flakyTests) / totalTests * 100).toFixed(2);
135
151
  const templateSource = fs.readFileSync(path2.resolve(__dirname, "report-template.hbs"), "utf-8");
136
152
  const template = Handlebars.compile(templateSource);
137
153
  const data = {
138
- totalDuration: this.results[0].totalDuration,
154
+ totalDuration,
139
155
  suiteName: this.suiteName,
140
156
  results: this.results,
141
- passCount: this.results.filter((r) => r.status === "passed").length,
142
- failCount: this.results.filter((r) => r.status === "failed" || r.status === "timedOut").length,
157
+ retryCount: this.results.filter((r) => r.isRetry).length,
158
+ passCount: passedTests,
159
+ failCount: failed,
143
160
  skipCount: this.results.filter((r) => r.status === "skipped").length,
144
- flakyCount: this.results.filter((r) => r.flaky === "flaky").length,
145
- totalCount: this.results.length,
161
+ flakyCount: flakyTests,
162
+ totalCount: filteredResults.length,
146
163
  groupedResults: this.groupedResults,
147
164
  projectName: this.config.projectName,
148
165
  authorName: this.config.authorName,
149
166
  testType: this.config.testType,
150
- successRate: this._successRate,
167
+ successRate,
151
168
  lastRunDate: formatDate(/* @__PURE__ */ new Date())
152
169
  };
153
170
  return template(data);
@@ -1,5 +1,6 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
+
3
4
  <head>
4
5
  <meta charset="UTF-8">
5
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -29,7 +30,7 @@
29
30
  }
30
31
 
31
32
  .highlight {
32
- background-color: var(--pico-primary-background);
33
+ color: var(--pico-primary-background);
33
34
  }
34
35
 
35
36
  .filter.active {
@@ -51,9 +52,9 @@
51
52
  }
52
53
 
53
54
  .text-flaky {
54
- color: #d5d4a1;
55
+ color: #b5e35e;
55
56
  }
56
-
57
+
57
58
  div#testDetails {
58
59
  position: sticky;
59
60
  top: 0;
@@ -62,7 +63,8 @@
62
63
 
63
64
  .sidebar {
64
65
  overflow-y: auto;
65
- max-height: calc(100vh - 100px); /* Adjust as needed */
66
+ max-height: calc(100vh - 100px);
67
+ /* Adjust as needed */
66
68
  border-right: 1px solid #ddd;
67
69
  padding-right: 10px;
68
70
  }
@@ -78,7 +80,8 @@
78
80
  display: none;
79
81
  margin-bottom: 1rem;
80
82
  }
81
- .clickable {
83
+
84
+ .clickable {
82
85
  cursor: pointer;
83
86
  transition: background-color 0.3s;
84
87
  }
@@ -86,33 +89,40 @@
86
89
  .clickable:hover {
87
90
  background-color: #607D8B;
88
91
  }
92
+
89
93
  li[data-test-id] {
90
94
  overflow: hidden;
91
95
  text-overflow: ellipsis;
92
96
  white-space: nowrap;
93
97
  max-width: 25ch;
94
98
  }
95
- li img{
99
+
100
+ li img {
96
101
  max-width: 8%
97
102
  }
98
- summary img{
103
+
104
+ summary img {
99
105
  max-width: 5%
100
106
  }
107
+
101
108
  dialog article {
102
109
  max-width: 768px;
103
110
  }
111
+
104
112
  ::-webkit-scrollbar {
105
113
  width: 0px;
106
114
  background-color: transparent;
107
115
  }
108
116
 
109
117
  ::-webkit-scrollbar-thumb {
110
- background-color: var(--pico-secondary-background);;
118
+ background-color: var(--pico-secondary-background);
119
+ ;
111
120
  border-radius: 0px;
112
121
  }
113
122
 
114
123
  ::-webkit-scrollbar-thumb:hover {
115
- background-color: var(--pico-secondary-background);;
124
+ background-color: var(--pico-secondary-background);
125
+ ;
116
126
  }
117
127
 
118
128
  ::-webkit-scrollbar-track {
@@ -124,6 +134,7 @@
124
134
  }
125
135
  </style>
126
136
  </head>
137
+
127
138
  <body>
128
139
  <header class="container">
129
140
  <div class="header">
@@ -144,15 +155,15 @@
144
155
  {{#each groupedResults}}
145
156
  <details>
146
157
  <summary>
147
- <img src="node_modules/ortoni-report/dist/icon/file.png" alt="retry">
148
- <span>{{@key}}<span>
158
+ <img src="node_modules/ortoni-report/dist/icon/file.png" alt="file name">
159
+ <span>{{@key}}<span>
149
160
  </summary>
150
161
  <ul>
151
162
  {{#each this}}
152
163
  <details>
153
164
  <summary>
154
- <img src="node_modules/ortoni-report/dist/icon/test.png" alt="retry">
155
- <span>{{@key}}<span>
165
+ <img src="node_modules/ortoni-report/dist/icon/test.png" alt="test name">
166
+ <span>{{@key}}<span>
156
167
  </summary>
157
168
  <ul>
158
169
  {{#each this}}
@@ -160,25 +171,32 @@
160
171
  <summary>{{@key}}</summary>
161
172
  <ul>
162
173
  {{#each this}}
163
- <li data-suite-name="{{suite}}" data-project-name="{{projectName}}" data-test-id="{{index}}" data-test-status="{{status}}">
164
- {{#if isRetry}}
174
+ <li data-suite-name="{{suite}}" data-project-name="{{projectName}}"
175
+ data-test-id="{{index}}" data-test-status="{{status}} {{retry}}">
176
+ {{#if isRetry}}
165
177
  <img src="node_modules/ortoni-report/dist/icon/retry.png" alt="Retry">
166
- {{/if}}
167
- {{#if (eq status "passed")}}
178
+ {{/if}}
179
+ {{#if (eq status "passed")}}
168
180
  <img src="node_modules/ortoni-report/dist/icon/pass.png" alt="Pass">
169
- {{/if}}
170
- {{#if (eq status "failed")}}
181
+ {{/if}}
182
+ {{#if (eq status "failed")}}
171
183
  <img src="node_modules/ortoni-report/dist/icon/fail.png" alt="Fail">
172
184
  {{else}}
173
185
  {{#if (eq status "skipped")}}
174
- <img src="node_modules/ortoni-report/dist/icon/skip.png" alt="Skip">
175
- {{/if}}
176
- {{/if}}
177
- {{#if (eq status "timedOut")}}
186
+ <img src="node_modules/ortoni-report/dist/icon/skip.png" alt="Skip">
187
+ {{/if}}
188
+ {{/if}}
189
+ {{#if (eq status "timedOut")}}
178
190
  <img src="node_modules/ortoni-report/dist/icon/timeout.png" alt="timedOut">
179
- {{/if}}
180
- <span>{{title}}</span>
181
- </li>
191
+ {{/if}}
192
+ {{#if (eq status "flaky")}}
193
+ <img src="node_modules/ortoni-report/dist/icon/flaky.png" alt="flaky">
194
+ {{/if}}
195
+ <span>{{title}}</span>
196
+ {{#if retryCount}}
197
+ <p>Retry Count: {{retryCount}}</p>
198
+ {{/if}}
199
+ </li>
182
200
  {{/each}}
183
201
  </ul>
184
202
  </details>
@@ -227,6 +245,12 @@
227
245
  <p class="text-flaky">{{flakyCount}}</p>
228
246
  </article>
229
247
  </div>
248
+ <div>
249
+ <article class="clickable filter" data-status="retry">
250
+ <header>Retry</header>
251
+ <p class="text-skip">{{retryCount}}</p>
252
+ </article>
253
+ </div>
230
254
  </section>
231
255
  {{!-- Suite details with chart --}}
232
256
  <section>
@@ -272,33 +296,42 @@
272
296
 
273
297
  document.addEventListener('DOMContentLoaded', () => {
274
298
  const testData = {{{ json results }}};
275
- const testDetails = document.getElementById('testDetails');
276
- const summary = document.getElementById('summary');
277
- const backButton = document.querySelector('.back-button');
278
- let highlightedItem = null;
279
-
280
- function showSummary() {
281
- summary.style.display = 'block';
282
- testDetails.style.display = 'none';
283
- backButton.style.display = 'none';
284
- if (highlightedItem) {
285
- highlightedItem.classList.remove('highlight');
286
- }
299
+ const testDetails = document.getElementById('testDetails');
300
+ const summary = document.getElementById('summary');
301
+ const backButton = document.querySelector('.back-button');
302
+ let highlightedItem = null;
303
+
304
+ function showSummary() {
305
+ summary.style.display = 'block';
306
+ testDetails.style.display = 'none';
307
+ backButton.style.display = 'none';
308
+ if (highlightedItem) {
309
+ highlightedItem.classList.remove('highlight');
287
310
  }
311
+ }
288
312
 
289
- window.showSummary = showSummary;
290
-
291
- function displayTestDetails(test) {
292
- summary.style.display = 'none';
293
- testDetails.style.display = 'block';
294
- backButton.style.display = 'block';
295
- testDetails.innerHTML = `
313
+ window.showSummary = showSummary;
314
+
315
+ function displayTestDetails(test) {
316
+ summary.style.display = 'none';
317
+ testDetails.style.display = 'block';
318
+ backButton.style.display = 'block';
319
+ let statusClass = '';
320
+ let statusText = test.status.toUpperCase();
321
+ if (test.status.startsWith('passed')) {
322
+ statusClass = 'text-success';
323
+ } else if (test.status === 'flaky') {
324
+ statusClass = 'text-flaky';
325
+ } else {
326
+ statusClass = 'text-failure';
327
+ }
328
+ testDetails.innerHTML = `
296
329
  <button class="back-button" style="display: block" onclick="showSummary()">Back to Summary</button>
297
330
  <h3>${test.title}</h3>
298
331
  <div class="grid">
299
332
  <div>
300
333
  <h4>Status</h4>
301
- <p class="${test.status === 'passed' ? 'text-success' : 'text-failure'}">${test.status.toUpperCase()}</p>
334
+ <p class="${statusClass}">${statusText}</p>
302
335
  ${test.duration != '0s' ? `
303
336
  <h4>Duration</h4>
304
337
  <p>${test.duration}</p>` : ""}
@@ -319,6 +352,12 @@
319
352
  </dialog>` : ''}
320
353
  </div>
321
354
  </div>
355
+ <div class="grid">
356
+ <details>
357
+ <summary><h4>Steps</h4></summary>
358
+ <span id="stepDetails"></span>
359
+ </details>
360
+ </div>
322
361
  <div>
323
362
  ${test.errors.length ? `
324
363
  <h4>Errors</h4>
@@ -334,125 +373,147 @@
334
373
  ` : ''}
335
374
  </div>
336
375
  `;
337
- }
376
+ const stepDetailsDiv = document.getElementById('stepDetails');
377
+ const stepsList = attachSteps(test);
378
+ stepDetailsDiv.appendChild(stepsList);
379
+ }
338
380
 
339
- function attachEventListeners() {
340
- const testItems = document.querySelectorAll('[data-test-id]');
341
- testItems.forEach(item => {
342
- item.addEventListener('click', () => {
343
- const testId = item.getAttribute('data-test-id');
344
- const test = testData[testId];
345
- displayTestDetails(test);
346
- if (highlightedItem) {
347
- highlightedItem.classList.remove('highlight');
348
- }
349
- item.classList.add('highlight');
350
- highlightedItem = item;
351
- });
381
+ function attachSteps(test) {
382
+ const stepsList = document.createElement("ul");
383
+ stepsList.setAttribute("id", "steps");
384
+ stepsList.innerHTML = '';
385
+ test.steps.forEach(step => {
386
+ const li = document.createElement('li');
387
+ li.innerHTML = `<strong class=${step.error ? 'text-failure' : 'test-success'}> ${step.title}</strong>`;
388
+ stepsList.appendChild(li);
389
+ });
390
+ return stepsList;
391
+ }
392
+
393
+ function attachEventListeners() {
394
+ const testItems = document.querySelectorAll('[data-test-id]');
395
+ testItems.forEach(item => {
396
+ item.addEventListener('click', () => {
397
+ const testId = item.getAttribute('data-test-id');
398
+ const test = testData[testId];
399
+ displayTestDetails(test);
400
+ if (highlightedItem) {
401
+ highlightedItem.classList.remove('highlight');
402
+ }
403
+ item.classList.add('highlight');
404
+ highlightedItem = item;
352
405
  });
406
+ });
353
407
 
354
- // Event listeners for the filter articles
355
- const filters = document.querySelectorAll('.filter');
356
- filters.forEach(filter => {
357
- filter.addEventListener('click', () => {
358
- const status = filter.getAttribute('data-status');
359
- filters.forEach(f => {
360
- if (f.getAttribute('data-status') !== 'all') {
361
- f.classList.remove('active');
362
- }
363
- });
364
- if (status !== 'all') {
365
- filter.classList.add('active');
408
+ // Event listeners for the filter articles
409
+ const filters = document.querySelectorAll('.filter');
410
+ filters.forEach(filter => {
411
+ filter.addEventListener('click', () => {
412
+ const status = filter.getAttribute('data-status');
413
+ filters.forEach(f => {
414
+ if (f.getAttribute('data-status') !== 'all') {
415
+ f.classList.remove('active');
366
416
  }
367
- applyFilter(status);
368
417
  });
418
+ if (status !== 'all') {
419
+ filter.classList.add('active');
420
+ }
421
+ applyFilter(status);
369
422
  });
370
- }
423
+ });
424
+ }
371
425
 
372
- function applyFilter(status) {
373
- const testItems = document.querySelectorAll('[data-test-id]');
374
- const detailsElements = document.querySelectorAll('details');
375
-
376
- detailsElements.forEach(details => {
377
- let shouldShowDetails = false;
378
- const items = details.querySelectorAll('[data-test-id]');
379
- items.forEach(item => {
380
- const testStatus = item.getAttribute('data-test-status');
381
- if (status === 'all' || testStatus === status || (status === 'failed' &&
382
- (testStatus === 'failed' || testStatus === 'timedOut'))) {
383
- item.style.display = 'block'; // Show the item
384
- shouldShowDetails = true; // Set shouldShowDetails to true
385
- } else {
386
- item.style.display = 'none'; // Hide the item
387
- }
388
- });
426
+ function applyFilter(status) {
427
+ const testItems = document.querySelectorAll('li[data-test-id]');
428
+ const detailsElements = document.querySelectorAll('details');
429
+
430
+ detailsElements.forEach(details => {
431
+ let shouldShowDetails = false;
432
+ const items = details.querySelectorAll('li[data-test-id]');
433
+ items.forEach(item => {
434
+ const testStatus = item.getAttribute('data-test-status');
435
+ if (status === 'all' || testStatus.trim() === status ||
436
+ (status === 'failed' && (testStatus.trim() === 'failed' || testStatus.trim() === 'timedOut'))) {
437
+ item.style.display = 'block'; // Show the item
438
+ shouldShowDetails = true; // Set shouldShowDetails to true
439
+ } else if ((status === 'retry' && testStatus.includes('retry'))) {
440
+ item.style.display = 'block'; // Show the item
441
+ shouldShowDetails = true; // Set shouldShowDetails to true
442
+ }else if ((status === 'flaky' && testStatus.includes('flaky'))) {
443
+ item.style.display = 'block'; // Show the item
444
+ shouldShowDetails = true; // Set shouldShowDetails to true
445
+ } else {
446
+ item.style.display = 'none'; // Hide the item
447
+ }
448
+ });
389
449
  details.open = shouldShowDetails; // Open the details element if it has any matching items
390
450
  details.style.display = shouldShowDetails ? 'block' : 'none'; // Show/hide the details element
391
- });
392
- }
451
+ });
452
+ }
393
453
 
394
454
 
395
- const searchInput = document.querySelector('input[name="search"]');
396
- const detailsElements = document.querySelectorAll('details');
455
+ const searchInput = document.querySelector('input[name="search"]');
456
+ const detailsElements = document.querySelectorAll('details');
397
457
 
398
- searchInput.addEventListener('input', () => {
399
- const searchTerm = searchInput.value.toLowerCase();
400
- const testItems = document.querySelectorAll('[data-test-id]');
458
+ searchInput.addEventListener('input', () => {
459
+ const searchTerm = searchInput.value.toLowerCase();
460
+ const testItems = document.querySelectorAll('[data-test-id]');
401
461
 
402
- if (searchTerm) {
403
- detailsElements.forEach(detail => {
404
- detail.open = false; // Collapse all details initially
405
- });
462
+ if (searchTerm) {
463
+ detailsElements.forEach(detail => {
464
+ detail.open = false; // Collapse all details initially
465
+ });
406
466
 
407
- testItems.forEach(item => {
408
- const testTitle = item.textContent.toLowerCase();
409
- if (testTitle.includes(searchTerm)) {
410
- item.style.display = 'block'; // Show matching test item
411
-
412
- let parent = item.parentElement;
413
- while (parent && parent.tagName !== 'ASIDE') {
414
- if (parent.tagName === 'DETAILS') {
415
- parent.open = true;
416
- }
417
- parent = parent.parentElement;
467
+ testItems.forEach(item => {
468
+ const testTitle = item.textContent.toLowerCase();
469
+ if (testTitle.includes(searchTerm)) {
470
+ item.style.display = 'block'; // Show matching test item
471
+
472
+ let parent = item.parentElement;
473
+ while (parent && parent.tagName !== 'ASIDE') {
474
+ if (parent.tagName === 'DETAILS') {
475
+ parent.open = true;
418
476
  }
419
- } else {
420
- item.style.display = 'none';
477
+ parent = parent.parentElement;
421
478
  }
422
- });
423
- } else {
424
- testItems.forEach(item => {
425
- item.style.display = 'block';
426
- });
427
- detailsElements.forEach(detail => {
428
- detail.open = false;
429
- });
430
- }
431
- });
479
+ } else {
480
+ item.style.display = 'none';
481
+ }
482
+ });
483
+ } else {
484
+ testItems.forEach(item => {
485
+ item.style.display = 'block';
486
+ });
487
+ detailsElements.forEach(detail => {
488
+ detail.open = false;
489
+ });
490
+ }
491
+ });
432
492
 
433
- const ctx = document.getElementById('testChart').getContext('2d');
434
- new Chart(ctx, {
493
+ const ctx = document.getElementById('testChart').getContext('2d');
494
+ new Chart(ctx, {
435
495
  type: 'doughnut',
436
496
  data: {
437
- labels: ['Passed', 'Failed', 'Skipped'],
497
+ labels: ['Passed', 'Failed', 'Skipped','Flaky'],
438
498
  datasets: [{
439
- data: [{{ passCount }}, {{ failCount }}, {{ skipCount }}],
440
- backgroundColor: ['#28a745', '#dc3545', '#d5d4a1']
499
+ data: [{{ passCount }}, {{ failCount }}, {{ skipCount }}, {{flakyCount}}],
500
+ backgroundColor: ['#28a745', '#dc3545', '#d5d4a1', '#b5e35e']
441
501
  }]
442
502
  },
443
503
  options: {
444
504
  responsive: true,
445
505
  maintainAspectRatio: false,
446
- plugins: {
447
- legend: {
448
- position: 'bottom'
449
- }
506
+ plugins: {
507
+ legend: {
508
+ position: 'bottom'
450
509
  }
451
510
  }
511
+ }
452
512
  });
453
- attachEventListeners();
513
+ attachEventListeners();
454
514
  });
455
515
  </script>
456
516
  <script src="node_modules/ortoni-report/dist/utils/modal.js"></script>
457
517
  </body>
458
- </html>
518
+
519
+ </html>
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.formatDate = exports.normalizeFilePath = exports.msToTime = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ function msToTime(duration) {
9
+ const milliseconds = Math.floor((duration % 1000));
10
+ const seconds = Math.floor((duration / 1000) % 60);
11
+ const minutes = Math.floor((duration / (1000 * 60)) % 60);
12
+ const hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
13
+ let result = '';
14
+ if (hours > 0) {
15
+ result += (hours < 10 ? "0" + hours : hours) + "h:";
16
+ }
17
+ if (minutes > 0 || hours > 0) { // Include minutes if hours are included
18
+ result += (minutes < 10 ? "0" + minutes : minutes) + "m:";
19
+ }
20
+ if (seconds > 0 || minutes > 0 || hours > 0) { // Include seconds if minutes or hours are included
21
+ result += (seconds < 10 ? "0" + seconds : seconds) + "s";
22
+ }
23
+ if (milliseconds > 0) {
24
+ result += ":" + (milliseconds < 100 ? "0" + milliseconds : milliseconds) + "ms";
25
+ }
26
+ return result;
27
+ }
28
+ exports.msToTime = msToTime;
29
+ function normalizeFilePath(filePath) {
30
+ // Normalize the path to handle different separators
31
+ const normalizedPath = path_1.default.normalize(filePath);
32
+ // Get the base name of the file (removes any leading directories)
33
+ return path_1.default.basename(normalizedPath);
34
+ }
35
+ exports.normalizeFilePath = normalizeFilePath;
36
+ function formatDate(date) {
37
+ const day = String(date.getDate()).padStart(2, '0');
38
+ const month = date.toLocaleString('default', { month: 'short' });
39
+ const year = date.getFullYear();
40
+ const time = date.toLocaleTimeString();
41
+ return `${day}-${month}-${year} ${time}`;
42
+ }
43
+ exports.formatDate = formatDate;
44
+ ;
package/package.json CHANGED
@@ -1,49 +1,47 @@
1
- {
2
- "name": "ortoni-report",
3
- "version": "1.1.2",
4
- "description": "Playwright Report By LetCode with Koushik",
5
- "scripts": {
6
- "test": "npx playwright test",
7
- "build": "tsup",
8
- "release": "npm publish",
9
- "lint": "tsc"
10
- },
11
- "files": [
12
- "dist",
13
- "README.md",
14
- "CHANGELOG.md"
15
- ],
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/ortoniKC/ortoni-report"
19
- },
20
- "keywords": [
21
- "playwright report",
22
- "playwright letcode",
23
- "letcode koushik",
24
- "ortoni report",
25
- "ortoni-report",
26
- "playwright html"
27
- ],
28
- "author": "Koushik Chatterjee (LetCode with Koushik)",
29
- "license": "GPL-3.0-only",
30
- "bugs": {
31
- "url": "https://github.com/ortoniKC/ortoni-report/issues"
32
- },
33
- "homepage": "https://github.com/ortoniKC/ortoni-report#readme",
34
- "dependencies": {
35
- "colors": "^1.4.0",
36
- "handlebars": "^4.7.8",
37
- "hiq": "^4.2.3",
38
- "rimraf": "^5.0.7"
39
- },
40
- "devDependencies": {
41
- "@playwright/test": "^1.44.1",
42
- "@types/node": "^20.14.2",
43
- "tsup": "^6.5.0",
44
- "typescript": "^4.9.4"
45
- },
46
- "main": "dist/ortoni-report.js",
47
- "module": "dist/ortoni-report.mjs",
48
- "types": "dist/ortoni-report.d.ts"
49
- }
1
+ {
2
+ "name": "ortoni-report",
3
+ "version": "1.1.3",
4
+ "description": "Playwright Report By LetCode with Koushik",
5
+ "scripts": {
6
+ "test": "npx playwright test",
7
+ "build": "tsup",
8
+ "release": "npm publish",
9
+ "lint": "tsc"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "CHANGELOG.md"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/ortoniKC/ortoni-report"
19
+ },
20
+ "keywords": [
21
+ "playwright report",
22
+ "playwright letcode",
23
+ "letcode koushik",
24
+ "ortoni report",
25
+ "ortoni-report",
26
+ "playwright html"
27
+ ],
28
+ "author": "Koushik Chatterjee (LetCode with Koushik)",
29
+ "license": "GPL-3.0-only",
30
+ "bugs": {
31
+ "url": "https://github.com/ortoniKC/ortoni-report/issues"
32
+ },
33
+ "homepage": "https://github.com/ortoniKC/ortoni-report#readme",
34
+ "dependencies": {
35
+ "colors": "^1.4.0",
36
+ "handlebars": "^4.7.8"
37
+ },
38
+ "devDependencies": {
39
+ "@playwright/test": "^1.44.1",
40
+ "@types/node": "^20.14.2",
41
+ "tsup": "^6.5.0",
42
+ "typescript": "^4.9.4"
43
+ },
44
+ "main": "dist/ortoni-report.js",
45
+ "module": "dist/ortoni-report.mjs",
46
+ "types": "dist/ortoni-report.d.ts"
47
+ }
package/readme.md CHANGED
@@ -5,7 +5,7 @@ We are excited to announce the release of OrtoniReport (Playwright report - unof
5
5
  [Click here to check the live Demo](https://ortoni.netlify.app/)
6
6
 
7
7
 
8
- ![Ortoni Report](https://github.com/ortoniKC/ortoni-report/assets/58769833/f96b7697-8ec0-4c6d-a681-b305e6d5843a)
8
+ ![Ortoni Report](https://github.com/ortoniKC/ortoni-report/assets/58769833/88237970-b97f-4339-94cd-a1e4f3488b10)
9
9
 
10
10
  ## Features
11
11
 
@@ -21,9 +21,10 @@ We are excited to announce the release of OrtoniReport (Playwright report - unof
21
21
 
22
22
  4. **Summary Statistics:**
23
23
  - Provide summary statistics for total tests, passed tests, failed tests, skipped tests, and flaky tests.
24
+ - Success Rate of test suite.
24
25
 
25
26
  5. **Chart Visualization:**
26
- - Visualize test results using a doughnut chart to represent the distribution of passed, failed, and skipped tests.
27
+ - Visualize test results using a doughnut chart to represent the distribution of passed, failed, skipped and flaky tests.
27
28
 
28
29
  6. **Project Information:**
29
30
  - Include project name, author name, and test type information in the report.
@@ -47,6 +48,14 @@ We are excited to announce the release of OrtoniReport (Playwright report - unof
47
48
  12. **Ease of Use:**
48
49
  - Enable easy navigation between test results and summary sections.
49
50
 
51
+ 13. **Success Rate**
52
+ - The success rate in this project is calculated based on the outcomes of the tests executed using Playwright. The calculation considers tests that pass initially as well as tests that initially fail but pass upon retry
53
+ - Success Rate Formula
54
+ The success rate (successRate) is calculated using the following formula:
55
+ ```
56
+ const successRate: string = (((passedTests + flakyTests) / totalTests) * 100).toFixed(2);
57
+ ```
58
+
50
59
  These features collectively enhance the readability, usability, and accessibility of the test report, providing valuable insights into test execution and results.
51
60
 
52
61
  ### Configurable Report Generation
@@ -124,4 +133,4 @@ Thank you for using OrtoniReport! We hope it significantly enhances your Playwri
124
133
 
125
134
  ---
126
135
 
127
- **LetCode with Koushik**
136
+ **LetCode with Koushik**