k6-cucumber-steps 1.2.0 â 1.2.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.
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
module.exports = function buildK6Script(config) {
|
|
2
2
|
const { method, endpoints, endpoint, body, headers, options, baseUrl } = config;
|
|
3
3
|
|
|
4
|
-
// Prefer baseUrl from config (set by step/world), then env
|
|
5
4
|
const BASE_URL =
|
|
6
5
|
baseUrl ||
|
|
7
6
|
(config.worldParameters && config.worldParameters.baseUrl) ||
|
|
@@ -18,15 +17,19 @@ module.exports = function buildK6Script(config) {
|
|
|
18
17
|
return url.startsWith("/") ? `${BASE_URL}${url}` : url;
|
|
19
18
|
};
|
|
20
19
|
|
|
21
|
-
// Default headers/body/options for safety
|
|
22
20
|
const safeHeaders = headers && Object.keys(headers).length ? headers : {};
|
|
23
21
|
const safeBody = body !== undefined ? body : null;
|
|
24
22
|
const safeOptions = options && Object.keys(options).length ? options : {};
|
|
25
23
|
|
|
26
|
-
// Stringify for script
|
|
27
24
|
const stringifiedHeaders = JSON.stringify(safeHeaders, null, 2);
|
|
28
25
|
const stringifiedBody = JSON.stringify(safeBody, null, 2);
|
|
29
26
|
|
|
27
|
+
const reportDir =
|
|
28
|
+
process.env.REPORT_OUTPUT_DIR ||
|
|
29
|
+
process.env.K6_REPORT_DIR ||
|
|
30
|
+
process.env.npm_config_report_output_dir ||
|
|
31
|
+
"reports";
|
|
32
|
+
|
|
30
33
|
return `
|
|
31
34
|
import http from 'k6/http';
|
|
32
35
|
import { check } from 'k6';
|
|
@@ -50,7 +53,6 @@ export default function () {
|
|
|
50
53
|
? "null"
|
|
51
54
|
: "JSON.stringify(body)"
|
|
52
55
|
}, { headers });
|
|
53
|
-
console.log(\`Response Body for \${resolvedUrl${i}}: \${res${i}.body}\`);
|
|
54
56
|
check(res${i}, {
|
|
55
57
|
"status is 2xx": (r) => r.status >= 200 && r.status < 300
|
|
56
58
|
});
|
|
@@ -64,7 +66,6 @@ export default function () {
|
|
|
64
66
|
? "null"
|
|
65
67
|
: "JSON.stringify(body)"
|
|
66
68
|
}, { headers });
|
|
67
|
-
console.log(\`Response Body for \${resolvedUrl}: \${res.body}\`);
|
|
68
69
|
check(res, {
|
|
69
70
|
"status is 2xx": (r) => r.status >= 200 && r.status < 300
|
|
70
71
|
});
|
|
@@ -73,24 +74,8 @@ export default function () {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
export function handleSummary(data) {
|
|
76
|
-
const outputDir = __ENV.REPORT_OUTPUT_DIR || "reports";
|
|
77
|
-
const html = htmlReport(data);
|
|
78
|
-
const cucumberReportFile = "cucumber-report.html";
|
|
79
|
-
const cucumberLink = \`
|
|
80
|
-
<div style="text-align:center; margin-top:20px;">
|
|
81
|
-
<a href="\${cucumberReportFile}" style="font-size:18px; color:#0066cc;">
|
|
82
|
-
đ View Cucumber Test Report
|
|
83
|
-
</a>
|
|
84
|
-
</div>
|
|
85
|
-
\`;
|
|
86
|
-
|
|
87
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
88
|
-
const k6ReportFilename = \`\${outputDir}/k6-report-\${timestamp}.html\`;
|
|
89
|
-
|
|
90
|
-
console.log(\`đ K6 HTML report \${k6ReportFilename} generated successfully đ\`);
|
|
91
|
-
|
|
92
77
|
return {
|
|
93
|
-
|
|
78
|
+
"${reportDir}/k6-report.html": htmlReport(data),
|
|
94
79
|
stdout: textSummary(data, { indent: " ", enableColors: true }),
|
|
95
80
|
};
|
|
96
81
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "k6-cucumber-steps",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"test-automation"
|
|
55
55
|
],
|
|
56
56
|
"engines": {
|
|
57
|
-
"node": ">=
|
|
57
|
+
"node": ">=20"
|
|
58
58
|
},
|
|
59
59
|
"author": "qaPaschalE",
|
|
60
60
|
"description": "Cucumber step definitions for running k6 performance tests.",
|
|
@@ -3,42 +3,27 @@
|
|
|
3
3
|
const { Given, When, Then } = require("@cucumber/cucumber");
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
|
-
const { execSync } = require("child_process");
|
|
7
6
|
const resolveBody = require("../lib/helpers/resolveBody.js");
|
|
8
7
|
const buildK6Script = require("../lib/helpers/buildK6Script.js");
|
|
9
8
|
const generateHeaders = require("../lib/helpers/generateHeaders.js");
|
|
10
9
|
const { generateK6Script, runK6Script } = require("../lib/utils/k6Runner.js");
|
|
10
|
+
const os = require("os");
|
|
11
|
+
const crypto = require("crypto");
|
|
11
12
|
require("dotenv").config();
|
|
12
13
|
|
|
13
|
-
// Validate thresholds (e.g., "rate<0.01")
|
|
14
|
-
const validateThreshold = (threshold) => {
|
|
15
|
-
const regex = /^[\w{}()<>:]+[<>=]\d+(\.\d+)?$/;
|
|
16
|
-
if (!regex.test(threshold)) {
|
|
17
|
-
throw new Error(`Invalid threshold format: ${threshold}`);
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
14
|
/**
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* Given I set a k6 script for GET testing
|
|
25
|
-
* Given I set a k6 script for POST testing
|
|
15
|
+
* Sets the HTTP method for the k6 script.
|
|
26
16
|
*/
|
|
27
|
-
Given(
|
|
17
|
+
Given(/^I set a k6 script for (\w+) testing$/, async function (method) {
|
|
28
18
|
this.config = { method: method.toUpperCase() };
|
|
29
19
|
});
|
|
30
20
|
|
|
31
21
|
/**
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* When I set to run the k6 script with the following configurations:
|
|
35
|
-
* | virtual_users | duration | http_req_failed | http_req_duration | error_rate | stages |
|
|
36
|
-
* | 10 | 5 | rate<0.05 | p(95)<200 | | |
|
|
37
|
-
* | 50 | 10 | rate<0.01 | | rate<0.01 | [{"target": 10, "duration": "10s"}, {"target": 50, "duration": "30s"}] |
|
|
22
|
+
* Sets k6 script options from a configuration table.
|
|
38
23
|
*/
|
|
39
24
|
When(
|
|
40
|
-
|
|
41
|
-
function (dataTable) {
|
|
25
|
+
/^I set to run the k6 script with the following configurations:$/,
|
|
26
|
+
async function (dataTable) {
|
|
42
27
|
const rawRow = dataTable.hashes()[0];
|
|
43
28
|
const row = {};
|
|
44
29
|
|
|
@@ -70,8 +55,6 @@ When(
|
|
|
70
55
|
});
|
|
71
56
|
}
|
|
72
57
|
|
|
73
|
-
console.log("đ¨ Resolved config row:", row);
|
|
74
|
-
|
|
75
58
|
const validateThreshold = (value) => {
|
|
76
59
|
const regex = /^[\w{}()<>:]+[<>=]\d+(\.\d+)?$/;
|
|
77
60
|
if (value && !regex.test(value)) {
|
|
@@ -113,35 +96,24 @@ When(
|
|
|
113
96
|
);
|
|
114
97
|
|
|
115
98
|
/**
|
|
116
|
-
*
|
|
117
|
-
* @example
|
|
118
|
-
* When I set the request headers:
|
|
119
|
-
* | Header | Value |
|
|
120
|
-
* | Content-Type | application/json |
|
|
121
|
-
* | Authorization | Bearer your_auth_token |
|
|
99
|
+
* Sets request headers for the k6 script.
|
|
122
100
|
*/
|
|
123
|
-
When(
|
|
101
|
+
When(/^I set the request headers:$/, async function (dataTable) {
|
|
124
102
|
const headers = {};
|
|
125
103
|
dataTable.hashes().forEach(({ Header, Value }) => {
|
|
126
104
|
headers[Header] = Value;
|
|
127
105
|
});
|
|
128
106
|
|
|
129
107
|
this.config.headers = {
|
|
130
|
-
...this.config.headers,
|
|
108
|
+
...this.config.headers,
|
|
131
109
|
...headers,
|
|
132
110
|
};
|
|
133
111
|
});
|
|
134
112
|
|
|
135
113
|
/**
|
|
136
|
-
*
|
|
137
|
-
* @example
|
|
138
|
-
* When I set the following endpoints used:
|
|
139
|
-
* """
|
|
140
|
-
* /api/users
|
|
141
|
-
* /api/products
|
|
142
|
-
* """
|
|
114
|
+
* Sets endpoints for the k6 script.
|
|
143
115
|
*/
|
|
144
|
-
When(
|
|
116
|
+
When(/^I set the following endpoints used:$/, async function (docString) {
|
|
145
117
|
this.config.endpoints = docString
|
|
146
118
|
.trim()
|
|
147
119
|
.split("\n")
|
|
@@ -149,30 +121,23 @@ When("I set the following endpoints used:", function (docString) {
|
|
|
149
121
|
});
|
|
150
122
|
|
|
151
123
|
/**
|
|
152
|
-
*
|
|
153
|
-
* @param {string} endpoint - The endpoint for the body.
|
|
154
|
-
* @param {string} docString - Multiline string containing the request body (can use placeholders).
|
|
155
|
-
* @example
|
|
156
|
-
* When I set the following POST body is used for "/api/users"
|
|
157
|
-
* """
|
|
158
|
-
* {
|
|
159
|
-
* "username": "{{username}}",
|
|
160
|
-
* "email": "{{faker.internet.email}}"
|
|
161
|
-
* }
|
|
162
|
-
* """
|
|
124
|
+
* Sets the request body for a specific method and endpoint.
|
|
163
125
|
*/
|
|
164
126
|
When(
|
|
165
|
-
|
|
166
|
-
function (method, endpoint, docString) {
|
|
127
|
+
/^I set the following (\w+) body is used for "([^"]+)"$/,
|
|
128
|
+
async function (method, endpoint, docString) {
|
|
167
129
|
this.config.method = method.toUpperCase();
|
|
168
130
|
this.config.body = resolveBody(docString, process.env);
|
|
169
131
|
this.config.endpoint = endpoint;
|
|
170
132
|
}
|
|
171
133
|
);
|
|
172
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Loads a JSON payload for a method and endpoint.
|
|
137
|
+
*/
|
|
173
138
|
When(
|
|
174
|
-
|
|
175
|
-
function (fileName, method, endpoint) {
|
|
139
|
+
/^I use JSON payload from "([^"]+)" for (\w+) to "([^"]+)"$/,
|
|
140
|
+
async function (fileName, method, endpoint) {
|
|
176
141
|
const allowedMethods = ["POST", "PUT", "PATCH"];
|
|
177
142
|
const methodUpper = method.toUpperCase();
|
|
178
143
|
|
|
@@ -188,7 +153,7 @@ When(
|
|
|
188
153
|
const payloadDir = this.parameters?.payloadPath || "payloads";
|
|
189
154
|
const payloadPath = path.isAbsolute(payloadDir)
|
|
190
155
|
? path.join(payloadDir, fileName)
|
|
191
|
-
: path.join(
|
|
156
|
+
: path.join(projectRoot, payloadDir, fileName);
|
|
192
157
|
|
|
193
158
|
if (!fs.existsSync(payloadPath)) {
|
|
194
159
|
throw new Error(`Payload file not found: ${payloadPath}`);
|
|
@@ -201,7 +166,7 @@ When(
|
|
|
201
166
|
});
|
|
202
167
|
|
|
203
168
|
this.config = {
|
|
204
|
-
...this.config,
|
|
169
|
+
...this.config,
|
|
205
170
|
method: methodUpper,
|
|
206
171
|
endpoint,
|
|
207
172
|
body: resolved,
|
|
@@ -217,14 +182,9 @@ When(
|
|
|
217
182
|
);
|
|
218
183
|
|
|
219
184
|
/**
|
|
220
|
-
*
|
|
221
|
-
* @example
|
|
222
|
-
* When I set the authentication type to "bearer_token"
|
|
223
|
-
* When I set the authentication type to "api_key"
|
|
224
|
-
* When I set the authentication type to "basic"
|
|
225
|
-
* When I set the authentication type to "none"
|
|
185
|
+
* Sets the authentication type for the request.
|
|
226
186
|
*/
|
|
227
|
-
When(
|
|
187
|
+
When(/^I set the authentication type to "([^"]+)"$/, async function (authType) {
|
|
228
188
|
this.config.headers = generateHeaders(
|
|
229
189
|
authType,
|
|
230
190
|
process.env,
|
|
@@ -233,11 +193,11 @@ When("I set the authentication type to {string}", function (authType) {
|
|
|
233
193
|
});
|
|
234
194
|
|
|
235
195
|
/**
|
|
236
|
-
*
|
|
196
|
+
* Stores a value from the last response as an alias.
|
|
237
197
|
*/
|
|
238
198
|
Then(
|
|
239
|
-
|
|
240
|
-
function (jsonPath, alias) {
|
|
199
|
+
/^I store the value at "([^"]+)" as alias "([^"]+)"$/,
|
|
200
|
+
async function (jsonPath, alias) {
|
|
241
201
|
if (!this.lastResponse) {
|
|
242
202
|
throw new Error("No previous response available.");
|
|
243
203
|
}
|
|
@@ -260,20 +220,24 @@ Then(
|
|
|
260
220
|
console.log(`đ§Š Stored alias "${alias}":`, value);
|
|
261
221
|
}
|
|
262
222
|
);
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Logs in via POST to an endpoint with a payload from a file.
|
|
226
|
+
*/
|
|
263
227
|
When(
|
|
264
|
-
|
|
228
|
+
/^I login via POST to "([^"]+)" with payload from "([^"]+)"$/,
|
|
265
229
|
async function (endpoint, fileName) {
|
|
266
230
|
const payloadDir = this.parameters?.payloadPath || "payloads";
|
|
231
|
+
const projectRoot = path.resolve(__dirname, "..", "..");
|
|
267
232
|
const payloadPath = path.isAbsolute(payloadDir)
|
|
268
233
|
? path.join(payloadDir, fileName)
|
|
269
|
-
: path.join(
|
|
234
|
+
: path.join(projectRoot, payloadDir, fileName);
|
|
270
235
|
|
|
271
236
|
if (!fs.existsSync(payloadPath)) {
|
|
272
237
|
throw new Error(`Payload file not found: ${payloadPath}`);
|
|
273
238
|
}
|
|
274
239
|
|
|
275
240
|
const rawTemplate = fs.readFileSync(payloadPath, "utf-8");
|
|
276
|
-
|
|
277
241
|
const resolved = resolveBody(rawTemplate, {
|
|
278
242
|
...process.env,
|
|
279
243
|
...(this.aliases || {}),
|
|
@@ -295,7 +259,7 @@ When(
|
|
|
295
259
|
throw new Error(`Login request failed with status ${response.status}`);
|
|
296
260
|
}
|
|
297
261
|
|
|
298
|
-
this.lastResponse = data;
|
|
262
|
+
this.lastResponse = data;
|
|
299
263
|
console.log("đ Login successful, response saved to alias context.");
|
|
300
264
|
} catch (err) {
|
|
301
265
|
console.error("â Login request failed:", err.message);
|
|
@@ -304,15 +268,24 @@ When(
|
|
|
304
268
|
}
|
|
305
269
|
);
|
|
306
270
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
271
|
+
const genScriptDir = path.resolve(process.cwd(), "genScript");
|
|
272
|
+
if (!fs.existsSync(genScriptDir)) {
|
|
273
|
+
fs.mkdirSync(genScriptDir, { recursive: true });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Determine the report/output directory from env, CLI, or default to "reports"
|
|
277
|
+
const reportDir =
|
|
278
|
+
process.env.REPORT_OUTPUT_DIR ||
|
|
279
|
+
process.env.K6_REPORT_DIR ||
|
|
280
|
+
(process.env.npm_config_report_output_dir || "reports");
|
|
281
|
+
|
|
282
|
+
if (!fs.existsSync(reportDir)) {
|
|
283
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
284
|
+
}
|
|
285
|
+
|
|
313
286
|
Then(
|
|
314
|
-
|
|
315
|
-
{ timeout:
|
|
287
|
+
/^I see the API should handle the (\w+) request successfully$/,
|
|
288
|
+
{ timeout: 300000 },
|
|
316
289
|
async function (method) {
|
|
317
290
|
if (!this.config || !this.config.method) {
|
|
318
291
|
throw new Error("Configuration is missing or incomplete.");
|
|
@@ -325,32 +298,46 @@ Then(
|
|
|
325
298
|
);
|
|
326
299
|
}
|
|
327
300
|
try {
|
|
328
|
-
// Build the k6 script content
|
|
329
301
|
const scriptContent = buildK6Script(this.config);
|
|
302
|
+
const uniqueId = crypto.randomBytes(8).toString("hex");
|
|
303
|
+
const scriptPath = path.join(reportDir, `k6-script-${uniqueId}.js`);
|
|
304
|
+
fs.writeFileSync(scriptPath, scriptContent, "utf-8");
|
|
305
|
+
console.log(`â
k6 script generated at: ${scriptPath}`);
|
|
330
306
|
|
|
331
|
-
//
|
|
332
|
-
const scriptPath = await generateK6Script(
|
|
333
|
-
scriptContent,
|
|
334
|
-
"load",
|
|
335
|
-
process.env.K6_CUCUMBER_OVERWRITE === "true"
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
// Run the k6 script and capture output
|
|
307
|
+
// Always run k6 automatically, regardless of VU count
|
|
339
308
|
const { stdout, stderr, code } = await runK6Script(
|
|
340
309
|
scriptPath,
|
|
341
310
|
process.env.K6_CUCUMBER_OVERWRITE === "true"
|
|
342
311
|
);
|
|
343
|
-
|
|
344
312
|
if (stdout) console.log(stdout);
|
|
345
313
|
if (stderr) console.error(stderr);
|
|
346
|
-
|
|
347
314
|
if (code !== 0) {
|
|
348
315
|
throw new Error(`k6 exited with code ${code}`);
|
|
349
316
|
}
|
|
317
|
+
|
|
318
|
+
// Remove the script unless saveK6Script is true in env/config/cli
|
|
319
|
+
const saveK6Script =
|
|
320
|
+
process.env.saveK6Script === "true" ||
|
|
321
|
+
process.env.SAVE_K6_SCRIPT === "true" ||
|
|
322
|
+
this.parameters?.saveK6Script === true;
|
|
323
|
+
|
|
324
|
+
if (!saveK6Script) {
|
|
325
|
+
try {
|
|
326
|
+
fs.unlinkSync(scriptPath);
|
|
327
|
+
} catch (cleanupErr) {
|
|
328
|
+
console.warn(`Warning: Could not delete temp script file: ${scriptPath}`);
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
console.log(`âšī¸ k6 script kept at: ${scriptPath}`);
|
|
332
|
+
}
|
|
350
333
|
} catch (error) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
334
|
+
console.error(
|
|
335
|
+
"Failed to generate or run k6 script:",
|
|
336
|
+
error.stack || error
|
|
337
|
+
);
|
|
338
|
+
throw new Error("k6 script generation or execution failed");
|
|
354
339
|
}
|
|
355
340
|
}
|
|
356
341
|
);
|
|
342
|
+
|
|
343
|
+
// Repeat this pattern for all other step definitions!
|
|
@@ -1,53 +0,0 @@
|
|
|
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 };
|