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 +13 -0
- package/dist/icon/flaky.png +0 -0
- package/dist/ortoni-report.d.ts +18 -3
- package/dist/ortoni-report.js +38 -21
- package/dist/ortoni-report.mjs +38 -21
- package/dist/report-template.hbs +197 -136
- package/dist/types/reporterConfig.js +2 -0
- package/dist/types/testResults.js +2 -0
- package/dist/utils/utils.js +44 -0
- package/package.json +47 -49
- package/readme.md +12 -3
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
|
package/dist/ortoni-report.d.ts
CHANGED
|
@@ -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 };
|
package/dist/ortoni-report.js
CHANGED
|
@@ -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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
107
|
+
titlePath: step.titlePath,
|
|
102
108
|
category: step.category,
|
|
103
109
|
duration: step.duration,
|
|
104
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
|
188
|
+
totalDuration,
|
|
173
189
|
suiteName: this.suiteName,
|
|
174
190
|
results: this.results,
|
|
175
|
-
|
|
176
|
-
|
|
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:
|
|
179
|
-
totalCount:
|
|
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
|
|
201
|
+
successRate,
|
|
185
202
|
lastRunDate: formatDate(/* @__PURE__ */ new Date())
|
|
186
203
|
};
|
|
187
204
|
return template(data);
|
package/dist/ortoni-report.mjs
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
73
|
+
titlePath: step.titlePath,
|
|
68
74
|
category: step.category,
|
|
69
75
|
duration: step.duration,
|
|
70
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
|
154
|
+
totalDuration,
|
|
139
155
|
suiteName: this.suiteName,
|
|
140
156
|
results: this.results,
|
|
141
|
-
|
|
142
|
-
|
|
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:
|
|
145
|
-
totalCount:
|
|
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
|
|
167
|
+
successRate,
|
|
151
168
|
lastRunDate: formatDate(/* @__PURE__ */ new Date())
|
|
152
169
|
};
|
|
153
170
|
return template(data);
|
package/dist/report-template.hbs
CHANGED
|
@@ -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
|
-
|
|
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: #
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
|
|
100
|
+
li img {
|
|
96
101
|
max-width: 8%
|
|
97
102
|
}
|
|
98
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
178
|
+
{{/if}}
|
|
179
|
+
{{#if (eq status "passed")}}
|
|
168
180
|
<img src="node_modules/ortoni-report/dist/icon/pass.png" alt="Pass">
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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="${
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
396
|
-
|
|
455
|
+
const searchInput = document.querySelector('input[name="search"]');
|
|
456
|
+
const detailsElements = document.querySelectorAll('details');
|
|
397
457
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
458
|
+
searchInput.addEventListener('input', () => {
|
|
459
|
+
const searchTerm = searchInput.value.toLowerCase();
|
|
460
|
+
const testItems = document.querySelectorAll('[data-test-id]');
|
|
401
461
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
462
|
+
if (searchTerm) {
|
|
463
|
+
detailsElements.forEach(detail => {
|
|
464
|
+
detail.open = false; // Collapse all details initially
|
|
465
|
+
});
|
|
406
466
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
420
|
-
item.style.display = 'none';
|
|
477
|
+
parent = parent.parentElement;
|
|
421
478
|
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
434
|
-
|
|
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
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
506
|
+
plugins: {
|
|
507
|
+
legend: {
|
|
508
|
+
position: 'bottom'
|
|
450
509
|
}
|
|
451
510
|
}
|
|
511
|
+
}
|
|
452
512
|
});
|
|
453
|
-
|
|
513
|
+
attachEventListeners();
|
|
454
514
|
});
|
|
455
515
|
</script>
|
|
456
516
|
<script src="node_modules/ortoni-report/dist/utils/modal.js"></script>
|
|
457
517
|
</body>
|
|
458
|
-
|
|
518
|
+
|
|
519
|
+
</html>
|
|
@@ -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.
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
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
|
-

|
|
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
|
|
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**
|