k6-cucumber-steps 1.0.41 → 1.1.2
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/.env +38 -0
- package/bin/k6-cucumber-steps.js +71 -11
- package/cucumber.js +5 -5
- package/lib/helpers/buildK6Script.js +39 -18
- package/package.json +2 -1
- package/reports/cucumber-report.html +50 -0
- package/reports/k6-report-2025-06-03T12-54-18-512Z.html +582 -0
- package/reports/k6-report-2025-06-03T12-54-33-806Z.html +582 -0
- package/reports/load-report.json +283 -0
- package/scripts/generateHtmlReports.js +53 -0
- package/scripts/linkReports.js +122 -0
- package/.env.example +0 -17
- package/generateReport.js +0 -24
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"description": "",
|
|
4
|
+
"elements": [
|
|
5
|
+
{
|
|
6
|
+
"description": "",
|
|
7
|
+
"id": "rate-limit-enforcement-for-/login-and-/bsp;api-allows-up-to-5-requests-in-10-seconds-(under-limit)",
|
|
8
|
+
"keyword": "Scenario",
|
|
9
|
+
"line": 9,
|
|
10
|
+
"name": "API allows up to 5 requests in 10 seconds (under limit)",
|
|
11
|
+
"steps": [
|
|
12
|
+
{
|
|
13
|
+
"arguments": [],
|
|
14
|
+
"keyword": "Given ",
|
|
15
|
+
"line": 5,
|
|
16
|
+
"name": "I login via POST to \"/api/v3/client/api/login\" with payload from \"login.json\"",
|
|
17
|
+
"match": {
|
|
18
|
+
"location": "step_definitions/load_test_steps.js:262"
|
|
19
|
+
},
|
|
20
|
+
"result": {
|
|
21
|
+
"status": "passed",
|
|
22
|
+
"duration": 894569875
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"arguments": [],
|
|
27
|
+
"keyword": "Then ",
|
|
28
|
+
"line": 6,
|
|
29
|
+
"name": "I store the value at \"data.token\" as alias \"auth_token\"",
|
|
30
|
+
"match": {
|
|
31
|
+
"location": "step_definitions/load_test_steps.js:237"
|
|
32
|
+
},
|
|
33
|
+
"result": {
|
|
34
|
+
"status": "passed",
|
|
35
|
+
"duration": 217708
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"arguments": [],
|
|
40
|
+
"keyword": "When ",
|
|
41
|
+
"line": 10,
|
|
42
|
+
"name": "I set a k6 script for POST testing",
|
|
43
|
+
"match": {
|
|
44
|
+
"location": "step_definitions/load_test_steps.js:28"
|
|
45
|
+
},
|
|
46
|
+
"result": {
|
|
47
|
+
"status": "passed",
|
|
48
|
+
"duration": 78040
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"arguments": [
|
|
53
|
+
{
|
|
54
|
+
"rows": [
|
|
55
|
+
{
|
|
56
|
+
"cells": [
|
|
57
|
+
"virtual_users",
|
|
58
|
+
"duration",
|
|
59
|
+
"http_req_failed",
|
|
60
|
+
"http_req_duration"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"cells": [
|
|
65
|
+
"1",
|
|
66
|
+
"10",
|
|
67
|
+
"rate<0.05",
|
|
68
|
+
"p(95)<2000"
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"keyword": "When ",
|
|
75
|
+
"line": 11,
|
|
76
|
+
"name": "I set to run the k6 script with the following configurations:",
|
|
77
|
+
"match": {
|
|
78
|
+
"location": "step_definitions/load_test_steps.js:40"
|
|
79
|
+
},
|
|
80
|
+
"result": {
|
|
81
|
+
"status": "passed",
|
|
82
|
+
"duration": 458667
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"arguments": [],
|
|
87
|
+
"keyword": "When ",
|
|
88
|
+
"line": 14,
|
|
89
|
+
"name": "I use JSON payload from \"okra.json\" for POST to \"/api/v3/client/bsp\"",
|
|
90
|
+
"match": {
|
|
91
|
+
"location": "step_definitions/load_test_steps.js:174"
|
|
92
|
+
},
|
|
93
|
+
"result": {
|
|
94
|
+
"status": "passed",
|
|
95
|
+
"duration": 1823208
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"arguments": [],
|
|
100
|
+
"keyword": "When ",
|
|
101
|
+
"line": 15,
|
|
102
|
+
"name": "I set the authentication type to \"auth_token\"",
|
|
103
|
+
"match": {
|
|
104
|
+
"location": "step_definitions/load_test_steps.js:226"
|
|
105
|
+
},
|
|
106
|
+
"result": {
|
|
107
|
+
"status": "passed",
|
|
108
|
+
"duration": 125207
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"arguments": [],
|
|
113
|
+
"keyword": "Then ",
|
|
114
|
+
"line": 16,
|
|
115
|
+
"name": "I see the API should handle the POST request successfully",
|
|
116
|
+
"match": {
|
|
117
|
+
"location": "step_definitions/load_test_steps.js:306"
|
|
118
|
+
},
|
|
119
|
+
"result": {
|
|
120
|
+
"status": "failed",
|
|
121
|
+
"duration": 14665569374,
|
|
122
|
+
"error_message": "Error: k6 test execution failed\n at CustomWorld.<anonymous> (/Users/paschal/personal/k6-cucumber-steps/step_definitions/load_test_steps.js:334:13)"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
"tags": [
|
|
127
|
+
{
|
|
128
|
+
"name": "@rate-limit",
|
|
129
|
+
"line": 1
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "@within-limit",
|
|
133
|
+
"line": 8
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
"type": "scenario"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"description": "",
|
|
140
|
+
"id": "rate-limit-enforcement-for-/login-and-/bsp;api-blocks-more-than-5-requests-in-10-seconds-(exceeds-limit)",
|
|
141
|
+
"keyword": "Scenario",
|
|
142
|
+
"line": 19,
|
|
143
|
+
"name": "API blocks more than 5 requests in 10 seconds (exceeds limit)",
|
|
144
|
+
"steps": [
|
|
145
|
+
{
|
|
146
|
+
"arguments": [],
|
|
147
|
+
"keyword": "Given ",
|
|
148
|
+
"line": 5,
|
|
149
|
+
"name": "I login via POST to \"/api/v3/client/api/login\" with payload from \"login.json\"",
|
|
150
|
+
"match": {
|
|
151
|
+
"location": "step_definitions/load_test_steps.js:262"
|
|
152
|
+
},
|
|
153
|
+
"result": {
|
|
154
|
+
"status": "passed",
|
|
155
|
+
"duration": 626931291
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"arguments": [],
|
|
160
|
+
"keyword": "Then ",
|
|
161
|
+
"line": 6,
|
|
162
|
+
"name": "I store the value at \"data.token\" as alias \"auth_token\"",
|
|
163
|
+
"match": {
|
|
164
|
+
"location": "step_definitions/load_test_steps.js:237"
|
|
165
|
+
},
|
|
166
|
+
"result": {
|
|
167
|
+
"status": "passed",
|
|
168
|
+
"duration": 569790
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"arguments": [],
|
|
173
|
+
"keyword": "When ",
|
|
174
|
+
"line": 20,
|
|
175
|
+
"name": "I set a k6 script for POST testing",
|
|
176
|
+
"match": {
|
|
177
|
+
"location": "step_definitions/load_test_steps.js:28"
|
|
178
|
+
},
|
|
179
|
+
"result": {
|
|
180
|
+
"status": "passed",
|
|
181
|
+
"duration": 76042
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"arguments": [
|
|
186
|
+
{
|
|
187
|
+
"rows": [
|
|
188
|
+
{
|
|
189
|
+
"cells": [
|
|
190
|
+
"virtual_users",
|
|
191
|
+
"duration",
|
|
192
|
+
"http_req_failed",
|
|
193
|
+
"http_req_duration"
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"cells": [
|
|
198
|
+
"6",
|
|
199
|
+
"10",
|
|
200
|
+
"rate>=0.50",
|
|
201
|
+
"p(95)<3000"
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
],
|
|
207
|
+
"keyword": "When ",
|
|
208
|
+
"line": 21,
|
|
209
|
+
"name": "I set to run the k6 script with the following configurations:",
|
|
210
|
+
"match": {
|
|
211
|
+
"location": "step_definitions/load_test_steps.js:40"
|
|
212
|
+
},
|
|
213
|
+
"result": {
|
|
214
|
+
"status": "passed",
|
|
215
|
+
"duration": 255083
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"arguments": [],
|
|
220
|
+
"keyword": "When ",
|
|
221
|
+
"line": 24,
|
|
222
|
+
"name": "I use JSON payload from \"okra.json\" for POST to \"/api/v3/client/bsp\"",
|
|
223
|
+
"match": {
|
|
224
|
+
"location": "step_definitions/load_test_steps.js:174"
|
|
225
|
+
},
|
|
226
|
+
"result": {
|
|
227
|
+
"status": "passed",
|
|
228
|
+
"duration": 1752667
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"arguments": [],
|
|
233
|
+
"keyword": "When ",
|
|
234
|
+
"line": 25,
|
|
235
|
+
"name": "I set the authentication type to \"auth_token\"",
|
|
236
|
+
"match": {
|
|
237
|
+
"location": "step_definitions/load_test_steps.js:226"
|
|
238
|
+
},
|
|
239
|
+
"result": {
|
|
240
|
+
"status": "passed",
|
|
241
|
+
"duration": 87707
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"arguments": [],
|
|
246
|
+
"keyword": "Then ",
|
|
247
|
+
"line": 26,
|
|
248
|
+
"name": "I see the API should handle the POST request successfully",
|
|
249
|
+
"match": {
|
|
250
|
+
"location": "step_definitions/load_test_steps.js:306"
|
|
251
|
+
},
|
|
252
|
+
"result": {
|
|
253
|
+
"status": "passed",
|
|
254
|
+
"duration": 14650450625
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
],
|
|
258
|
+
"tags": [
|
|
259
|
+
{
|
|
260
|
+
"name": "@rate-limit",
|
|
261
|
+
"line": 1
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"name": "@exceed-limit",
|
|
265
|
+
"line": 18
|
|
266
|
+
}
|
|
267
|
+
],
|
|
268
|
+
"type": "scenario"
|
|
269
|
+
}
|
|
270
|
+
],
|
|
271
|
+
"id": "rate-limit-enforcement-for-/login-and-/bsp",
|
|
272
|
+
"line": 2,
|
|
273
|
+
"keyword": "Feature",
|
|
274
|
+
"name": "Rate Limit Enforcement for /login and /bsp",
|
|
275
|
+
"tags": [
|
|
276
|
+
{
|
|
277
|
+
"name": "@rate-limit",
|
|
278
|
+
"line": 1
|
|
279
|
+
}
|
|
280
|
+
],
|
|
281
|
+
"uri": "features/bsp.feature"
|
|
282
|
+
}
|
|
283
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const reporter = require("cucumber-html-reporter");
|
|
4
|
+
|
|
5
|
+
function getJsonReportPath() {
|
|
6
|
+
// 1. Check for cucumber.js or custom config
|
|
7
|
+
const configFile = path.resolve("cucumber.js");
|
|
8
|
+
if (fs.existsSync(configFile)) {
|
|
9
|
+
const configText = fs.readFileSync(configFile, "utf-8");
|
|
10
|
+
|
|
11
|
+
const match = configText.match(/json:(.*?\.json)/);
|
|
12
|
+
if (match && match[1]) {
|
|
13
|
+
const reportPath = match[1].trim().replace(/['"`]/g, "");
|
|
14
|
+
console.log(`📝 Found report path in cucumber.js: ${reportPath}`);
|
|
15
|
+
return reportPath;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 2. Check environment variable
|
|
20
|
+
if (process.env.CUCUMBER_JSON) {
|
|
21
|
+
console.log(
|
|
22
|
+
`📝 Using report path from CUCUMBER_JSON: ${process.env.CUCUMBER_JSON}`
|
|
23
|
+
);
|
|
24
|
+
return process.env.CUCUMBER_JSON;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 3. Fallback to default
|
|
28
|
+
console.log("📝 Using default report path: reports/load-report.json");
|
|
29
|
+
return "reports/load-report.json";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function generateHtmlReports() {
|
|
33
|
+
const jsonReportPath = getJsonReportPath();
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(jsonReportPath)) {
|
|
36
|
+
console.warn(`⚠️ Cucumber JSON report not found at: ${jsonReportPath}`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const reportDir = path.dirname(jsonReportPath);
|
|
41
|
+
|
|
42
|
+
reporter.generate({
|
|
43
|
+
theme: "bootstrap",
|
|
44
|
+
jsonFile: jsonReportPath,
|
|
45
|
+
output: path.join(reportDir, "cucumber-report.html"),
|
|
46
|
+
reportSuiteAsScenarios: true,
|
|
47
|
+
launchReport: false,
|
|
48
|
+
name: "k6-cucumber-steps Performance Report",
|
|
49
|
+
brandTitle: "📊 Performance Test Summary",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { generateHtmlReports };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { minify } = require("html-minifier-terser");
|
|
4
|
+
|
|
5
|
+
const reportsDir = path.resolve("reports");
|
|
6
|
+
|
|
7
|
+
function findCucumberReportPath() {
|
|
8
|
+
const files = fs.readdirSync(reportsDir);
|
|
9
|
+
return files.find((f) => f.includes("cucumber") && f.endsWith(".html"));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function extractCucumberBody(filepath) {
|
|
13
|
+
const html = fs.readFileSync(filepath, "utf-8");
|
|
14
|
+
|
|
15
|
+
// Remove <script> tags to avoid JS conflicts
|
|
16
|
+
const withoutScripts = html.replace(/<script[\s\S]*?<\/script>/gi, "");
|
|
17
|
+
|
|
18
|
+
const match = withoutScripts.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
|
19
|
+
return match
|
|
20
|
+
? match[1]
|
|
21
|
+
: "<p>⚠️ Failed to extract body of Cucumber report.</p>";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function extractK6ReportBody(file) {
|
|
25
|
+
const html = fs.readFileSync(file, "utf-8");
|
|
26
|
+
const match = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
|
27
|
+
return match ? match[1] : "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildCombinedHtml({ title, k6Bodies, cucumberBody }) {
|
|
31
|
+
const tabs = k6Bodies
|
|
32
|
+
.map(
|
|
33
|
+
(_, i) => `
|
|
34
|
+
<input type="radio" name="tabs" id="tabk6${i}" ${i === 0 ? "checked" : ""}>
|
|
35
|
+
<label for="tabk6${i}">K6 Report ${i + 1}</label>
|
|
36
|
+
<div class="tab">
|
|
37
|
+
${k6Bodies[i]}
|
|
38
|
+
</div>
|
|
39
|
+
`
|
|
40
|
+
)
|
|
41
|
+
.join("\n");
|
|
42
|
+
|
|
43
|
+
const cucumberTab = `
|
|
44
|
+
<input type="radio" name="tabs" id="tabcucumber">
|
|
45
|
+
<label for="tabcucumber">Cucumber Report</label>
|
|
46
|
+
<div class="tab">
|
|
47
|
+
<div style="max-height:80vh; overflow:auto; padding:1rem;">
|
|
48
|
+
${cucumberBody}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const fullTabs = cucumberBody ? cucumberTab + tabs : tabs;
|
|
54
|
+
|
|
55
|
+
return `
|
|
56
|
+
<!DOCTYPE html>
|
|
57
|
+
<html lang="en">
|
|
58
|
+
<head>
|
|
59
|
+
<meta charset="UTF-8" />
|
|
60
|
+
<title>${title}</title>
|
|
61
|
+
<link rel="stylesheet" href="https://unpkg.com/purecss@2.0.3/build/pure-min.css" crossorigin="anonymous">
|
|
62
|
+
<style>
|
|
63
|
+
body { margin:1rem; font-family:sans-serif; }
|
|
64
|
+
.tabs { display:flex; flex-wrap:wrap; }
|
|
65
|
+
.tabs label { order:1; padding:1rem; cursor:pointer; background:#ddd; margin-right:0.2rem; border-radius:5px 5px 0 0; }
|
|
66
|
+
.tabs .tab { order:99; flex-grow:1; width:100%; display:none; padding:1rem; background:#fff; }
|
|
67
|
+
.tabs input[type="radio"] { display:none; }
|
|
68
|
+
.tabs input[type="radio"]:checked + label { background:#fff; font-weight:bold; }
|
|
69
|
+
.tabs input[type="radio"]:checked + label + .tab { display:block; }
|
|
70
|
+
</style>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<h1>${title}</h1>
|
|
74
|
+
<div class="tabs">
|
|
75
|
+
${fullTabs}
|
|
76
|
+
</div>
|
|
77
|
+
</body>
|
|
78
|
+
</html>`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function linkReports() {
|
|
82
|
+
if (!fs.existsSync(reportsDir)) {
|
|
83
|
+
console.warn("⚠️ No reports directory found.");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const cucumberFile = findCucumberReportPath();
|
|
88
|
+
const cucumberBody = cucumberFile
|
|
89
|
+
? extractCucumberBody(path.join(reportsDir, cucumberFile))
|
|
90
|
+
: null;
|
|
91
|
+
|
|
92
|
+
const k6Files = fs
|
|
93
|
+
.readdirSync(reportsDir)
|
|
94
|
+
.filter((f) => /^k6-report.*\.html$/.test(f));
|
|
95
|
+
if (k6Files.length === 0) {
|
|
96
|
+
console.warn("⚠️ No K6 reports found.");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const k6Bodies = k6Files.map((f) =>
|
|
101
|
+
extractK6ReportBody(path.join(reportsDir, f))
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const combinedHtml = buildCombinedHtml({
|
|
105
|
+
title: "Combined K6 + Cucumber Report",
|
|
106
|
+
k6Bodies,
|
|
107
|
+
cucumberBody,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const finalHtml = await minify(combinedHtml, {
|
|
111
|
+
collapseWhitespace: true,
|
|
112
|
+
removeComments: true,
|
|
113
|
+
minifyCSS: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const outPath = path.join(reportsDir, "combined-report.html");
|
|
117
|
+
fs.writeFileSync(outPath, finalHtml, "utf-8");
|
|
118
|
+
|
|
119
|
+
console.log(`✅ Combined and minified report saved to: ${outPath}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { linkReports };
|
package/.env.example
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# .env
|
|
2
|
-
TEST_ENVIRONMENT=STAGING
|
|
3
|
-
APP_VERSION=1.0.0
|
|
4
|
-
BROWSER=Chrome 100.0.4896.88
|
|
5
|
-
PLATFORM=Windows 10
|
|
6
|
-
PARALLEL=Scenarios
|
|
7
|
-
EXECUTED=Remote
|
|
8
|
-
|
|
9
|
-
# Base URLs
|
|
10
|
-
API_BASE_URL=https://test-api.example.com
|
|
11
|
-
BASE_URL=https://example.com
|
|
12
|
-
AUTH_URL=https://auth.example.com
|
|
13
|
-
PAYMENT_URL=https://payment.example.com
|
|
14
|
-
|
|
15
|
-
# Secret Parameters
|
|
16
|
-
API_KEY=your-secret-api-key-12345
|
|
17
|
-
BEARER_TOKEN=your_bearer_token_here
|
package/generateReport.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
const reporter = require("cucumber-html-reporter");
|
|
2
|
-
|
|
3
|
-
// Configuration for the HTML report
|
|
4
|
-
const options = {
|
|
5
|
-
theme: "bootstrap", // You can change the theme to 'simple', 'compact', etc.
|
|
6
|
-
jsonFile: "reports/load-results.json", // Path to the JSON file generated by Cucumber
|
|
7
|
-
output: "reports/report.html", // Path where you want to save the HTML report
|
|
8
|
-
reportSuiteAsScenarios: true, // Group scenarios by their name
|
|
9
|
-
launchReport: true, // Automatically open the report in the browser
|
|
10
|
-
metadata: {
|
|
11
|
-
browser: {
|
|
12
|
-
name: "Chrome" || "unknown",
|
|
13
|
-
version: "89" || "unknown",
|
|
14
|
-
},
|
|
15
|
-
device: "Local test machine",
|
|
16
|
-
platform: {
|
|
17
|
-
name: "MacOS",
|
|
18
|
-
version: "20.04",
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
// Generate the report
|
|
24
|
-
reporter.generate(options);
|