k6-cucumber-steps 1.2.8 โ 1.2.9
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/README.md +4 -2
- package/package.json +9 -3
- package/step_definitions/load_test_steps.js +523 -205
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# k6-cucumber-steps ๐ฅ๐งช
|
|
2
2
|
|
|
3
3
|
<table align="center" style="margin-bottom:30px;"><tr><td align="center" width="9999" heigth="9999" >
|
|
4
|
-
<img src="assets/paschal
|
|
4
|
+
<img src="https://github.com/qaPaschalE/k6-cucumber-steps/blob/main/assets/paschal%20logo%20(2).png?raw=true" alt="paschal Logo" style="margin-top:25px;" align="center"/>
|
|
5
5
|
</td></tr></table>
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/k6-cucumber-steps)
|
|
8
8
|
[](https://k6.io/)
|
|
9
9
|
[](https://github.com/qaPaschalE/k6-cucumber-steps/blob/main/LICENSE)
|
|
10
10
|
[](https://cucumber.io/)
|
|
11
|
-
[](https://nodejs.org/)
|
|
12
12
|
[](https://github.com/qaPaschalE/k6-cucumber-steps/actions/workflows/k6-load-test.yml)
|
|
13
13
|
[](https://github.com/qaPaschalE/k6-cucumber-steps/issues)
|
|
14
14
|
[](https://github.com/qaPaschalE/k6-cucumber-steps/stargazers)
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
Run [k6](https://k6.io/) performance/load tests using [Cucumber](https://cucumber.io/) BDD syntax with ease.
|
|
18
18
|
|
|
19
|
+
๐ [View Steps Documentation](https://qapaschale.github.io/k6-cucumber-steps/)
|
|
20
|
+
|
|
19
21
|
---
|
|
20
22
|
|
|
21
23
|
## โจ Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "k6-cucumber-steps",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"test": "cucumber-js features/ --require-module @babel/register --require step_definitions/",
|
|
24
24
|
"build": "npm run clean && npm run compile",
|
|
25
25
|
"clean": "rm -rf dist/",
|
|
26
|
-
"compile": "babel src/ -d dist/"
|
|
26
|
+
"compile": "babel src/ -d dist/",
|
|
27
|
+
"docs": "jsdoc --configure jsdoc.json --verbose"
|
|
27
28
|
},
|
|
28
29
|
"cucumber": {
|
|
29
30
|
"require": [
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
"engines": {
|
|
57
58
|
"node": ">=20"
|
|
58
59
|
},
|
|
59
|
-
"author": "qaPaschalE",
|
|
60
|
+
"author": "qaPaschalE <paschal.enyimiri@gmail.com>",
|
|
60
61
|
"description": "Cucumber step definitions for running k6 performance tests.",
|
|
61
62
|
"peerDependencies": {
|
|
62
63
|
"@cucumber/cucumber": "*",
|
|
@@ -72,5 +73,10 @@
|
|
|
72
73
|
"commander": "^14.0.0",
|
|
73
74
|
"dotenv": "^16.5.0",
|
|
74
75
|
"html-minifier-terser": "^7.2.0"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"clean-jsdoc-theme": "^4.3.0",
|
|
79
|
+
"jsdoc": "^4.0.4",
|
|
80
|
+
"taffydb": "^2.7.3"
|
|
75
81
|
}
|
|
76
82
|
}
|
|
@@ -1,271 +1,571 @@
|
|
|
1
|
-
// load_test_steps.js
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
// e2e/step_definitions/load_test_steps.js
|
|
2
|
+
|
|
3
|
+
import { Given, When, Then } from "@cucumber/cucumber";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import crypto from "crypto";
|
|
7
|
+
import * as dotenv from "dotenv";
|
|
8
|
+
|
|
9
|
+
import resolveBody from "../lib/helpers/resolveBody.js";
|
|
10
|
+
import buildK6Script from "../lib/helpers/buildK6Script.js";
|
|
11
|
+
import generateHeaders from "../lib/helpers/generateHeaders.js";
|
|
12
|
+
import { runK6Script } from "../lib/utils/k6Runner.js";
|
|
13
|
+
|
|
14
|
+
dotenv.config();
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
|
-
*
|
|
17
|
+
* @typedef {Object} CustomWorld
|
|
18
|
+
* @property {Object} config
|
|
19
|
+
* @property {Object} aliases
|
|
20
|
+
* @property {Object} lastResponse
|
|
21
|
+
* @property {Object} parameters
|
|
22
|
+
* @property {Function} log
|
|
16
23
|
*/
|
|
17
|
-
Given(/^I set a k6 script for (\w+) testing$/, async function (method) {
|
|
18
|
-
this.config = { method: method.toUpperCase() };
|
|
19
|
-
});
|
|
20
24
|
|
|
21
25
|
/**
|
|
22
|
-
*
|
|
26
|
+
* @typedef {Object} K6Config
|
|
27
|
+
* @property {string} method - HTTP method for the request (e.g., "GET", "POST").
|
|
28
|
+
* @property {string} [endpoint] - The specific endpoint for a single request.
|
|
29
|
+
* @property {string[]} [endpoints] - An array of endpoints for multiple requests.
|
|
30
|
+
* @property {Object} [headers] - Request headers.
|
|
31
|
+
* @property {string} [body] - Request body content.
|
|
32
|
+
* @property {Object} options - k6 test options (vus, duration, stages, thresholds).
|
|
33
|
+
* @property {Object} options.thresholds - k6 metric thresholds.
|
|
34
|
+
* @property {string[]} options.thresholds.http_req_failed - Thresholds for failed HTTP requests.
|
|
35
|
+
* @property {string[]} options.thresholds.http_req_duration - Thresholds for request duration.
|
|
36
|
+
* @property {string[]} [options.thresholds.error_rate] - Optional threshold for error rate.
|
|
37
|
+
* @property {number} [options.vus] - Number of virtual users (for open model).
|
|
38
|
+
* @property {string} [options.duration] - Test duration (for open model, e.g., "30s").
|
|
39
|
+
* @property {Array<Object>} [options.stages] - Array of stages for a stepped load model.
|
|
23
40
|
*/
|
|
24
|
-
When(
|
|
25
|
-
/^I set to run the k6 script with the following configurations:$/,
|
|
26
|
-
async function (dataTable) {
|
|
27
|
-
const rawRow = dataTable.hashes()[0];
|
|
28
|
-
const row = {};
|
|
29
|
-
|
|
30
|
-
// Extract example values manually from this.pickle
|
|
31
|
-
const exampleMap = {};
|
|
32
|
-
if (this.pickle && this.pickle.astNodeIds && this.gherkinDocument) {
|
|
33
|
-
const scenario = this.gherkinDocument.feature.children.find((child) => {
|
|
34
|
-
return child.scenario && child.scenario.examples?.length;
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const exampleValues =
|
|
38
|
-
scenario?.scenario?.examples?.[0]?.tableBody?.[0]?.cells?.map(
|
|
39
|
-
(cell) => cell.value
|
|
40
|
-
) || [];
|
|
41
|
-
|
|
42
|
-
const exampleKeys =
|
|
43
|
-
scenario?.scenario?.examples?.[0]?.tableHeader?.cells?.map(
|
|
44
|
-
(cell) => cell.value
|
|
45
|
-
) || [];
|
|
46
|
-
|
|
47
|
-
exampleKeys.forEach((key, idx) => {
|
|
48
|
-
exampleMap[key] = exampleValues[idx];
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
// ===================================================================================
|
|
43
|
+
// K6 SCRIPT CONFIGURATION STEPS
|
|
44
|
+
// ===================================================================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initializes the k6 script configuration by setting the primary HTTP method for the load test.
|
|
48
|
+
*
|
|
49
|
+
* ```gherkin
|
|
50
|
+
* Given I set a k6 script for {word} testing
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @param {string} method - The HTTP method (e.g., "GET", "POST", "PUT", "DELETE").
|
|
54
|
+
* @example
|
|
55
|
+
* Given I set a k6 script for GET testing
|
|
56
|
+
* Given I set a k6 script for POST testing
|
|
57
|
+
* @remarks
|
|
58
|
+
* This step typically starts the definition of a k6 load test scenario.
|
|
59
|
+
* It sets `this.config.method` in the Cucumber World context.
|
|
60
|
+
* Subsequent steps will build upon this configuration.
|
|
61
|
+
* @category k6 Configuration Steps
|
|
62
|
+
*/
|
|
63
|
+
export async function Given_I_set_k6_script_for_method_testing(method) {
|
|
64
|
+
/** @type {CustomWorld} */ (this).config = { method: method.toUpperCase() };
|
|
65
|
+
/** @type {CustomWorld} */ (this).log?.(
|
|
66
|
+
`โ๏ธ Initialized k6 script for ${method.toUpperCase()} testing.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
Given(
|
|
70
|
+
/^I set a k6 script for (\w+) testing$/,
|
|
71
|
+
Given_I_set_k6_script_for_method_testing
|
|
72
|
+
);
|
|
57
73
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Configures the k6 script options (VUs, duration, stages, thresholds) from a data table.
|
|
76
|
+
*
|
|
77
|
+
* ```gherkin
|
|
78
|
+
* When I set to run the k6 script with the following configurations:
|
|
79
|
+
* | virtual_users | duration | stages | http_req_failed | http_req_duration | error_rate |
|
|
80
|
+
* | 10 | 30 | | p(99)<0.01 | p(99)<500 | rate<0.01 |
|
|
81
|
+
* | | | [{"duration":"10s","target":10}] | p(90)<0.01 | p(90)<200 | rate<0.001 |
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @param {DataTable} dataTable - A Cucumber data table containing k6 configuration parameters.
|
|
85
|
+
* Expected columns: `virtual_users`, `duration`, `stages` (JSON string), `http_req_failed`, `http_req_duration`, `error_rate`.
|
|
86
|
+
* @example
|
|
87
|
+
* When I set to run the k6 script with the following configurations:
|
|
88
|
+
* | virtual_users | duration | http_req_failed | http_req_duration |
|
|
89
|
+
* | 50 | 60 | p(99)<0.01 | p(99)<1000 |
|
|
90
|
+
* When I set to run the k6 script with the following configurations:
|
|
91
|
+
* | stages | http_req_failed | http_req_duration | error_rate |
|
|
92
|
+
* | [{"duration":"10s","target":10}, {"duration":"20s","target":50}] | p(99)<0.01 | p(99)<500 | rate<0.01 |
|
|
93
|
+
* @remarks
|
|
94
|
+
* This step populates `this.config.options`. It intelligently handles either a simple
|
|
95
|
+
* `virtual_users`/`duration` model or a complex `stages` array. Threshold formats are validated.
|
|
96
|
+
* Example values from scenario outlines are resolved if present.
|
|
97
|
+
* @category k6 Configuration Steps
|
|
98
|
+
*/
|
|
99
|
+
export async function When_I_set_k6_script_configurations(dataTable) {
|
|
100
|
+
/** @type {CustomWorld} */ (this);
|
|
101
|
+
const rawRow = dataTable.hashes()[0];
|
|
102
|
+
const row = {};
|
|
103
|
+
|
|
104
|
+
const exampleMap = {};
|
|
105
|
+
if (this.pickle && this.pickle.astNodeIds && this.gherkinDocument) {
|
|
106
|
+
const scenarioNodeId = this.pickle.astNodeIds.find((id) =>
|
|
107
|
+
id.startsWith("Scenario")
|
|
108
|
+
);
|
|
109
|
+
if (scenarioNodeId) {
|
|
110
|
+
const scenario = this.gherkinDocument.feature.children.find(
|
|
111
|
+
(child) => child.scenario && child.scenario.id === scenarioNodeId
|
|
112
|
+
)?.scenario;
|
|
113
|
+
|
|
114
|
+
if (scenario && scenario.examples && scenario.examples.length > 0) {
|
|
115
|
+
const exampleTable = scenario.examples[0].tableBody?.[0];
|
|
116
|
+
const headerCells = scenario.examples[0].tableHeader?.cells || [];
|
|
117
|
+
const dataCells = exampleTable?.cells || [];
|
|
118
|
+
|
|
119
|
+
headerCells.forEach((cell, idx) => {
|
|
120
|
+
exampleMap[cell.value] = dataCells[idx]?.value;
|
|
121
|
+
});
|
|
62
122
|
}
|
|
63
|
-
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
64
125
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (row.stages) {
|
|
70
|
-
try {
|
|
71
|
-
this.config.options = {
|
|
72
|
-
stages: JSON.parse(row.stages),
|
|
73
|
-
thresholds: {
|
|
74
|
-
http_req_failed: [row.http_req_failed],
|
|
75
|
-
http_req_duration: [row.http_req_duration],
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
} catch {
|
|
79
|
-
throw new Error("Invalid stages JSON format.");
|
|
126
|
+
for (const [key, value] of Object.entries(rawRow)) {
|
|
127
|
+
row[key] = value.replace(/<([^>]+)>/g, (_, param) => {
|
|
128
|
+
if (exampleMap.hasOwnProperty(param)) {
|
|
129
|
+
return exampleMap[param];
|
|
80
130
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
131
|
+
return `<${param}>`;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const validateThreshold = (value, thresholdName) => {
|
|
136
|
+
const regex = /^[\w{}()<>:]+([<>=]=?)\d+(\.\d+)?$/;
|
|
137
|
+
if (value && !regex.test(value)) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Invalid k6 threshold format for '${thresholdName}': "${value}". Expected format like 'p(99)<500' or 'rate<0.01'.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
validateThreshold(row.http_req_failed, "http_req_failed");
|
|
145
|
+
validateThreshold(row.http_req_duration, "http_req_duration");
|
|
146
|
+
if (row.error_rate) {
|
|
147
|
+
validateThreshold(row.error_rate, "error_rate");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let k6Options;
|
|
151
|
+
|
|
152
|
+
if (row.stages) {
|
|
153
|
+
try {
|
|
154
|
+
k6Options = {
|
|
155
|
+
stages: JSON.parse(row.stages),
|
|
85
156
|
thresholds: {
|
|
86
157
|
http_req_failed: [row.http_req_failed],
|
|
87
158
|
http_req_duration: [row.http_req_duration],
|
|
88
159
|
},
|
|
89
160
|
};
|
|
161
|
+
} catch (e) {
|
|
162
|
+
throw new Error(`Invalid 'stages' JSON format: ${e.message}`);
|
|
90
163
|
}
|
|
164
|
+
} else if (row.virtual_users && row.duration) {
|
|
165
|
+
k6Options = {
|
|
166
|
+
vus: parseInt(row.virtual_users),
|
|
167
|
+
duration: `${row.duration}s`,
|
|
168
|
+
thresholds: {
|
|
169
|
+
http_req_failed: [row.http_req_failed],
|
|
170
|
+
http_req_duration: [row.http_req_duration],
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
} else {
|
|
174
|
+
throw new Error(
|
|
175
|
+
"k6 configuration requires either 'stages' or 'virtual_users' and 'duration' to be set."
|
|
176
|
+
);
|
|
177
|
+
}
|
|
91
178
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
179
|
+
if (row.error_rate) {
|
|
180
|
+
k6Options.thresholds.error_rate = [row.error_rate];
|
|
95
181
|
}
|
|
182
|
+
|
|
183
|
+
this.config.options = k6Options;
|
|
184
|
+
this.log?.(
|
|
185
|
+
`โ๏ธ k6 script configured with options: ${JSON.stringify(k6Options)}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
When(
|
|
189
|
+
/^I set to run the k6 script with the following configurations:$/,
|
|
190
|
+
When_I_set_k6_script_configurations
|
|
96
191
|
);
|
|
97
192
|
|
|
98
193
|
/**
|
|
99
|
-
* Sets request headers for the k6 script.
|
|
194
|
+
* Sets request headers for the k6 script. Headers are merged with any existing headers.
|
|
195
|
+
*
|
|
196
|
+
* ```gherkin
|
|
197
|
+
* When I set the request headers:
|
|
198
|
+
* | Header | Value |
|
|
199
|
+
* | Content-Type | application/json |
|
|
200
|
+
* | Authorization | Bearer <my_token> |
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* @param {DataTable} dataTable - A Cucumber data table with 'Header' and 'Value' columns.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* When I set the request headers:
|
|
207
|
+
* | Header | Value |
|
|
208
|
+
* | Content-Type | application/json |
|
|
209
|
+
* | X-Custom-Header| MyValue |
|
|
210
|
+
*
|
|
211
|
+
* @remarks
|
|
212
|
+
* This step updates `this.config.headers`. Values can include placeholders
|
|
213
|
+
* (e.g., `<my_token>`) if your `resolveBody` or `generateHeaders` helpers handle them.
|
|
214
|
+
* Note: `generateHeaders` is specifically used for authentication types. If your headers
|
|
215
|
+
* contain dynamic values beyond simple alias resolution, ensure your helpers support it.
|
|
216
|
+
* @category k6 Configuration Steps
|
|
100
217
|
*/
|
|
101
|
-
|
|
218
|
+
export async function When_I_set_request_headers(dataTable) {
|
|
219
|
+
/** @type {CustomWorld} */ (this);
|
|
102
220
|
const headers = {};
|
|
103
221
|
dataTable.hashes().forEach(({ Header, Value }) => {
|
|
104
222
|
headers[Header] = Value;
|
|
105
223
|
});
|
|
106
224
|
|
|
107
225
|
this.config.headers = {
|
|
108
|
-
...this.config.headers,
|
|
226
|
+
...(this.config.headers || {}),
|
|
109
227
|
...headers,
|
|
110
228
|
};
|
|
111
|
-
});
|
|
229
|
+
this.log?.(`โ๏ธ Request headers set: ${JSON.stringify(this.config.headers)}`);
|
|
230
|
+
}
|
|
231
|
+
When(/^I set the request headers:$/, When_I_set_request_headers);
|
|
112
232
|
|
|
113
233
|
/**
|
|
114
|
-
* Sets endpoints
|
|
234
|
+
* Sets the list of endpoints to be used in the k6 script. These are typically used when
|
|
235
|
+
* the k6 script iterates over multiple URLs.
|
|
236
|
+
*
|
|
237
|
+
* ```gherkin
|
|
238
|
+
* When I set the following endpoints used:
|
|
239
|
+
* /api/v1/users
|
|
240
|
+
* /api/v1/products
|
|
241
|
+
* /api/v1/orders
|
|
242
|
+
* ```
|
|
243
|
+
*
|
|
244
|
+
* @param {string} docString - A DocString containing a newline-separated list of endpoints.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* When I set the following endpoints used:
|
|
248
|
+
* /health
|
|
249
|
+
* /status
|
|
250
|
+
* /metrics
|
|
251
|
+
*
|
|
252
|
+
* @remarks
|
|
253
|
+
* This step populates `this.config.endpoints` as an array of strings.
|
|
254
|
+
* Ensure these endpoints are relative to your k6 `BASE_URL`.
|
|
255
|
+
* @category k6 Configuration Steps
|
|
115
256
|
*/
|
|
116
|
-
|
|
257
|
+
export async function When_I_set_endpoints_used(docString) {
|
|
258
|
+
/** @type {CustomWorld} */ (this);
|
|
117
259
|
this.config.endpoints = docString
|
|
118
260
|
.trim()
|
|
119
261
|
.split("\n")
|
|
120
262
|
.map((line) => line.trim());
|
|
121
|
-
|
|
263
|
+
if (this.log)
|
|
264
|
+
this.log(`โ๏ธ Endpoints set: ${JSON.stringify(this.config.endpoints)}`);
|
|
265
|
+
}
|
|
266
|
+
When(/^I set the following endpoints used:$/, When_I_set_endpoints_used);
|
|
122
267
|
|
|
123
268
|
/**
|
|
124
|
-
* Sets the request body for a specific method and endpoint.
|
|
269
|
+
* Sets the request body for a specific HTTP method and endpoint.
|
|
270
|
+
*
|
|
271
|
+
* ```gherkin
|
|
272
|
+
* When I set the following POST body is used for "/api/v1/create"
|
|
273
|
+
* { "name": "test", "email": "test@example.com" }
|
|
274
|
+
* ```
|
|
275
|
+
*
|
|
276
|
+
* @param {string} method - The HTTP method (e.g., "POST", "PUT").
|
|
277
|
+
* @param {string} endpoint - The specific endpoint URL for this body.
|
|
278
|
+
* @param {string} docString - A DocString containing the request body content (e.g., JSON).
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* When I set the following PUT body is used for "/api/v1/update/1"
|
|
282
|
+
* { "status": "active" }
|
|
283
|
+
*
|
|
284
|
+
* @remarks
|
|
285
|
+
* This step sets `this.config.method`, `this.config.endpoint`, and `this.config.body`.
|
|
286
|
+
* The `resolveBody` helper is used to process the DocString, allowing for dynamic values
|
|
287
|
+
* from environment variables.
|
|
288
|
+
* @category k6 Configuration Steps
|
|
125
289
|
*/
|
|
290
|
+
export async function When_I_set_method_body_for_endpoint(
|
|
291
|
+
method,
|
|
292
|
+
endpoint,
|
|
293
|
+
docString
|
|
294
|
+
) {
|
|
295
|
+
/** @type {CustomWorld} */ (this);
|
|
296
|
+
this.config.method = method.toUpperCase();
|
|
297
|
+
this.config.endpoint = endpoint;
|
|
298
|
+
this.config.body = resolveBody(docString, process.env);
|
|
299
|
+
this.log?.(
|
|
300
|
+
`โ๏ธ Body set for ${this.config.method} to "${
|
|
301
|
+
this.config.endpoint
|
|
302
|
+
}". Body preview: ${this.config.body.slice(0, 100)}...`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
126
305
|
When(
|
|
127
306
|
/^I set the following (\w+) body is used for "([^"]+)"$/,
|
|
128
|
-
|
|
129
|
-
this.config.method = method.toUpperCase();
|
|
130
|
-
this.config.body = resolveBody(docString, process.env);
|
|
131
|
-
this.config.endpoint = endpoint;
|
|
132
|
-
}
|
|
307
|
+
When_I_set_method_body_for_endpoint
|
|
133
308
|
);
|
|
134
309
|
|
|
135
310
|
/**
|
|
136
|
-
* Loads a JSON payload
|
|
311
|
+
* Loads a JSON payload from a file to be used as the request body for a specific
|
|
312
|
+
* method and endpoint in the k6 script.
|
|
313
|
+
*
|
|
314
|
+
* ```gherkin
|
|
315
|
+
* When I use JSON payload from "user_create.json" for POST to "/api/v1/users"
|
|
316
|
+
* ```
|
|
317
|
+
*
|
|
318
|
+
* @param {string} fileName - The name of the JSON payload file (e.g., "user_data.json").
|
|
319
|
+
* @param {string} method - The HTTP method (only "POST", "PUT", "PATCH" are supported for bodies).
|
|
320
|
+
* @param {string} endpoint - The specific endpoint URL.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* When I use JSON payload from "login_payload.json" for POST to "/auth/login"
|
|
324
|
+
*
|
|
325
|
+
* @remarks
|
|
326
|
+
* This step reads the JSON file, resolves any placeholders within it (using `resolveBody`),
|
|
327
|
+
* and sets `this.config.method`, `this.config.endpoint`, and `this.config.body`.
|
|
328
|
+
* It also stores `lastRequest` in `this.lastRequest`.
|
|
329
|
+
* The payload file path is resolved relative to `payloads` directory or `this.parameters.payloadPath`.
|
|
330
|
+
* @category k6 Configuration Steps
|
|
137
331
|
*/
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
332
|
+
export async function When_I_use_JSON_payload_from_file_for_method_to_endpoint(
|
|
333
|
+
fileName,
|
|
334
|
+
method,
|
|
335
|
+
endpoint
|
|
336
|
+
) {
|
|
337
|
+
/** @type {CustomWorld} */ (this);
|
|
338
|
+
const allowedMethods = ["POST", "PUT", "PATCH"];
|
|
339
|
+
const methodUpper = method.toUpperCase();
|
|
340
|
+
|
|
341
|
+
if (!allowedMethods.includes(methodUpper)) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`Method "${method}" is not supported for JSON payloads from files. Use one of: ${allowedMethods.join(
|
|
344
|
+
", "
|
|
345
|
+
)}`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
151
348
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
349
|
+
const projectRoot = path.resolve(__dirname, "..", "..");
|
|
350
|
+
const payloadDir = this.parameters?.payloadPath || "payloads";
|
|
351
|
+
const payloadPath = path.isAbsolute(payloadDir)
|
|
352
|
+
? path.join(payloadDir, fileName)
|
|
353
|
+
: path.join(projectRoot, payloadDir, fileName);
|
|
157
354
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
355
|
+
if (!fs.existsSync(payloadPath)) {
|
|
356
|
+
throw new Error(`Payload file not found: "${payloadPath}"`);
|
|
357
|
+
}
|
|
161
358
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
359
|
+
const rawTemplate = fs.readFileSync(payloadPath, "utf-8");
|
|
360
|
+
const resolved = resolveBody(rawTemplate, {
|
|
361
|
+
...process.env,
|
|
362
|
+
...(this.aliases || {}),
|
|
363
|
+
});
|
|
167
364
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
365
|
+
this.config = {
|
|
366
|
+
...(this.config || {}),
|
|
367
|
+
method: methodUpper,
|
|
368
|
+
endpoint,
|
|
369
|
+
body: resolved,
|
|
370
|
+
headers: this.config?.headers || {},
|
|
371
|
+
};
|
|
175
372
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
373
|
+
this.lastRequest = {
|
|
374
|
+
method: methodUpper,
|
|
375
|
+
endpoint,
|
|
376
|
+
body: resolved,
|
|
377
|
+
};
|
|
378
|
+
this.log?.(
|
|
379
|
+
`โ๏ธ JSON payload from "${fileName}" used for ${methodUpper} to "${endpoint}".`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
When(
|
|
383
|
+
/^I use JSON payload from "([^"]+)" for (\w+) to "([^"]+)"$/,
|
|
384
|
+
When_I_use_JSON_payload_from_file_for_method_to_endpoint
|
|
182
385
|
);
|
|
183
386
|
|
|
184
387
|
/**
|
|
185
|
-
* Sets the authentication type for the request.
|
|
388
|
+
* Sets the authentication type for the k6 request, generating relevant headers.
|
|
389
|
+
*
|
|
390
|
+
* ```gherkin
|
|
391
|
+
* When I set the authentication type to "BearerToken"
|
|
392
|
+
* ```
|
|
393
|
+
*
|
|
394
|
+
* @param {string} authType - The type of authentication (e.g., "BearerToken", "BasicAuth", "APIKey").
|
|
395
|
+
*
|
|
396
|
+
* @example
|
|
397
|
+
* When I set the authentication type to "BearerToken"
|
|
398
|
+
*
|
|
399
|
+
* @remarks
|
|
400
|
+
* This step uses the `generateHeaders` helper to create or modify `this.config.headers`
|
|
401
|
+
* based on the specified `authType` and environment variables/aliases.
|
|
402
|
+
* Ensure your `generateHeaders` helper is configured to handle the `authType` and retrieve
|
|
403
|
+
* necessary credentials (e.g., from `process.env` or `this.aliases`).
|
|
404
|
+
* @category k6 Configuration Steps
|
|
186
405
|
*/
|
|
187
|
-
|
|
406
|
+
export async function When_I_set_authentication_type(authType) {
|
|
407
|
+
/** @type {CustomWorld} */ (this);
|
|
188
408
|
this.config.headers = generateHeaders(
|
|
189
409
|
authType,
|
|
190
410
|
process.env,
|
|
191
411
|
this.aliases || {}
|
|
192
412
|
);
|
|
193
|
-
});
|
|
413
|
+
this.log?.(`โ๏ธ Authentication type set to "${authType}". Headers updated.`);
|
|
414
|
+
}
|
|
415
|
+
When(
|
|
416
|
+
/^I set the authentication type to "([^"]+)"$/,
|
|
417
|
+
When_I_set_authentication_type
|
|
418
|
+
);
|
|
194
419
|
|
|
195
420
|
/**
|
|
196
|
-
* Stores a value from the last response
|
|
421
|
+
* Stores a value from the last API response into the Cucumber World's aliases context.
|
|
422
|
+
*
|
|
423
|
+
* ```gherkin
|
|
424
|
+
* Then I store the value at "data.token" as alias "authToken"
|
|
425
|
+
* ```
|
|
426
|
+
*
|
|
427
|
+
* @param {string} jsonPath - A dot-separated JSON path to the value in the last response (e.g., "data.user.id").
|
|
428
|
+
* @param {string} alias - The name of the alias to store the value under (e.g., "userId").
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* Then I store the value at "token" as alias "accessToken"
|
|
432
|
+
* Then I store the value at "user.profile.email" as alias "userEmail"
|
|
433
|
+
*
|
|
434
|
+
* @remarks
|
|
435
|
+
* This step expects `this.lastResponse` to contain a parsed JSON object.
|
|
436
|
+
* It traverses the `jsonPath` to extract the desired value and saves it into
|
|
437
|
+
* `this.aliases`. This alias can then be used in subsequent steps or payload resolutions.
|
|
438
|
+
* @category Data Management Steps
|
|
197
439
|
*/
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
440
|
+
export async function Then_I_store_value_as_alias(jsonPath, alias) {
|
|
441
|
+
/** @type {CustomWorld} */ (this);
|
|
442
|
+
if (!this.lastResponse) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
"No previous API response available to extract value from. Ensure a login or request step was executed."
|
|
445
|
+
);
|
|
446
|
+
}
|
|
204
447
|
|
|
205
|
-
|
|
206
|
-
|
|
448
|
+
const pathParts = jsonPath.split(".");
|
|
449
|
+
let value = this.lastResponse;
|
|
207
450
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
451
|
+
for (const key of pathParts) {
|
|
452
|
+
if (value && typeof value === "object" && key in value) {
|
|
453
|
+
value = value[key];
|
|
454
|
+
} else {
|
|
455
|
+
value = undefined;
|
|
456
|
+
break;
|
|
211
457
|
}
|
|
458
|
+
}
|
|
212
459
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
460
|
+
if (value === undefined) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`Could not resolve path "${jsonPath}" in the last response. Value is undefined.`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
216
465
|
|
|
217
|
-
|
|
218
|
-
|
|
466
|
+
if (!this.aliases) this.aliases = {};
|
|
467
|
+
this.aliases[alias] = value;
|
|
219
468
|
|
|
220
|
-
|
|
221
|
-
|
|
469
|
+
this.log?.(
|
|
470
|
+
`๐งฉ Stored alias "${alias}" from response path "${jsonPath}". Value: ${JSON.stringify(
|
|
471
|
+
value
|
|
472
|
+
).slice(0, 100)}...`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
Then(
|
|
476
|
+
/^I store the value at "([^"]+)" as alias "([^"]+)"$/,
|
|
477
|
+
Then_I_store_value_as_alias
|
|
222
478
|
);
|
|
223
479
|
|
|
224
480
|
/**
|
|
225
|
-
* Logs in via POST to
|
|
481
|
+
* Logs in via a POST request to a specified endpoint using a JSON payload from a file.
|
|
482
|
+
* The response data is stored for subsequent steps.
|
|
483
|
+
*
|
|
484
|
+
* ```gherkin
|
|
485
|
+
* When I login via POST to "/auth/login" with payload from "admin_credentials.json"
|
|
486
|
+
* ```
|
|
487
|
+
*
|
|
488
|
+
* @param {string} endpoint - The API endpoint for the login request (relative to `BASE_URL`).
|
|
489
|
+
* @param {string} fileName - The name of the JSON file containing login credentials.
|
|
490
|
+
*
|
|
491
|
+
* @example
|
|
492
|
+
* When I login via POST to "/api/login" with payload from "user_creds.json"
|
|
493
|
+
*
|
|
494
|
+
* @remarks
|
|
495
|
+
* This step constructs and executes a `fetch` POST request. It reads the payload from
|
|
496
|
+
* the specified file (resolved from `payloads` directory), resolves placeholders in the payload,
|
|
497
|
+
* sends the request, and stores the JSON response in `this.lastResponse`.
|
|
498
|
+
* It throws an error if the login request fails (non-2xx status).
|
|
499
|
+
* @category Authentication Steps
|
|
226
500
|
*/
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const resolved = resolveBody(rawTemplate, {
|
|
242
|
-
...process.env,
|
|
243
|
-
...(this.aliases || {}),
|
|
244
|
-
});
|
|
501
|
+
export async function When_I_login_via_POST_with_payload_from_file(
|
|
502
|
+
endpoint,
|
|
503
|
+
fileName
|
|
504
|
+
) {
|
|
505
|
+
/** @type {CustomWorld} */ (this);
|
|
506
|
+
const payloadDir = this.parameters?.payloadPath || "payloads";
|
|
507
|
+
const projectRoot = path.resolve(__dirname, "..", "..");
|
|
508
|
+
const payloadPath = path.isAbsolute(payloadDir)
|
|
509
|
+
? path.join(payloadDir, fileName)
|
|
510
|
+
: path.join(projectRoot, payloadDir, fileName);
|
|
511
|
+
|
|
512
|
+
if (!fs.existsSync(payloadPath)) {
|
|
513
|
+
throw new Error(`Payload file not found: "${payloadPath}"`);
|
|
514
|
+
}
|
|
245
515
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
},
|
|
252
|
-
body: JSON.stringify(resolved),
|
|
253
|
-
});
|
|
516
|
+
const rawTemplate = fs.readFileSync(payloadPath, "utf-8");
|
|
517
|
+
const resolved = resolveBody(rawTemplate, {
|
|
518
|
+
...process.env,
|
|
519
|
+
...(this.aliases || {}),
|
|
520
|
+
});
|
|
254
521
|
|
|
255
|
-
|
|
522
|
+
try {
|
|
523
|
+
const baseUrl = process.env.BASE_URL;
|
|
524
|
+
if (!baseUrl) {
|
|
525
|
+
throw new Error("Missing BASE_URL environment variable.");
|
|
526
|
+
}
|
|
527
|
+
const fullUrl = `${baseUrl.replace(/\/+$/, "")}${endpoint}`;
|
|
528
|
+
|
|
529
|
+
this.log?.(
|
|
530
|
+
`๐ Attempting login via POST to "${fullUrl}" with payload from "${fileName}".`
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const response = await fetch(fullUrl, {
|
|
534
|
+
method: "POST",
|
|
535
|
+
headers: {
|
|
536
|
+
"Content-Type": "application/json",
|
|
537
|
+
},
|
|
538
|
+
body: JSON.stringify(resolved),
|
|
539
|
+
});
|
|
256
540
|
|
|
257
|
-
|
|
258
|
-
console.error("โ Login request failed:", data);
|
|
259
|
-
throw new Error(`Login request failed with status ${response.status}`);
|
|
260
|
-
}
|
|
541
|
+
const data = await response.json();
|
|
261
542
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
543
|
+
if (!response.ok) {
|
|
544
|
+
this.log?.(
|
|
545
|
+
`โ Login request failed for "${fullUrl}". Status: ${
|
|
546
|
+
response.status
|
|
547
|
+
}. Response body: ${JSON.stringify(data).slice(0, 100)}...`
|
|
548
|
+
);
|
|
549
|
+
throw new Error(
|
|
550
|
+
`Login request failed with status ${response.status} for endpoint "${endpoint}".`
|
|
551
|
+
);
|
|
267
552
|
}
|
|
553
|
+
|
|
554
|
+
this.lastResponse = data;
|
|
555
|
+
this.log?.(
|
|
556
|
+
"๐ Login successful, response data saved to 'this.lastResponse'."
|
|
557
|
+
);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
560
|
+
this.log?.(`โ Login request failed: ${message}`);
|
|
561
|
+
throw new Error(
|
|
562
|
+
`Login request failed for endpoint "${endpoint}": ${message}`
|
|
563
|
+
);
|
|
268
564
|
}
|
|
565
|
+
}
|
|
566
|
+
When(
|
|
567
|
+
/^I login via POST to "([^"]+)" with payload from "([^"]+)"$/,
|
|
568
|
+
When_I_login_via_POST_with_payload_from_file
|
|
269
569
|
);
|
|
270
570
|
|
|
271
571
|
const genScriptDir = path.resolve(process.cwd(), "genScript");
|
|
@@ -273,11 +573,11 @@ if (!fs.existsSync(genScriptDir)) {
|
|
|
273
573
|
fs.mkdirSync(genScriptDir, { recursive: true });
|
|
274
574
|
}
|
|
275
575
|
|
|
276
|
-
// Determine the report/output directory from env, CLI, or default to "reports"
|
|
277
576
|
const reportDir =
|
|
278
577
|
process.env.REPORT_OUTPUT_DIR ||
|
|
279
578
|
process.env.K6_REPORT_DIR ||
|
|
280
|
-
|
|
579
|
+
process.env.npm_config_report_output_dir ||
|
|
580
|
+
"reports";
|
|
281
581
|
|
|
282
582
|
if (!fs.existsSync(reportDir)) {
|
|
283
583
|
fs.mkdirSync(reportDir, { recursive: true });
|
|
@@ -300,22 +600,28 @@ Then(
|
|
|
300
600
|
try {
|
|
301
601
|
const scriptContent = buildK6Script(this.config);
|
|
302
602
|
const uniqueId = crypto.randomBytes(8).toString("hex");
|
|
303
|
-
const
|
|
603
|
+
const scriptFileName = `k6-script-${uniqueId}.js`;
|
|
604
|
+
const scriptPath = path.join(reportDir, scriptFileName);
|
|
304
605
|
fs.writeFileSync(scriptPath, scriptContent, "utf-8");
|
|
305
|
-
|
|
606
|
+
this.log?.(`โ
k6 script generated at: "${scriptPath}"`);
|
|
306
607
|
|
|
307
|
-
|
|
608
|
+
this.log?.(`๐ Running k6 script: "${scriptFileName}"...`);
|
|
308
609
|
const { stdout, stderr, code } = await runK6Script(
|
|
309
610
|
scriptPath,
|
|
310
611
|
process.env.K6_CUCUMBER_OVERWRITE === "true"
|
|
311
612
|
);
|
|
312
|
-
if (stdout)
|
|
313
|
-
if (stderr)
|
|
613
|
+
if (stdout) this.log?.(`k6 STDOUT:\n${stdout}`);
|
|
614
|
+
if (stderr) this.log?.(`k6 STDERR:\n${stderr}`);
|
|
615
|
+
|
|
314
616
|
if (code !== 0) {
|
|
315
|
-
throw new Error(
|
|
617
|
+
throw new Error(
|
|
618
|
+
`k6 process exited with code ${code}. Check k6 output for details.`
|
|
619
|
+
);
|
|
316
620
|
}
|
|
621
|
+
this.log?.(
|
|
622
|
+
`โ
k6 script executed successfully for ${expectedMethod} request.`
|
|
623
|
+
);
|
|
317
624
|
|
|
318
|
-
// Remove the script unless saveK6Script is true in env/config/cli
|
|
319
625
|
const saveK6Script =
|
|
320
626
|
process.env.saveK6Script === "true" ||
|
|
321
627
|
process.env.SAVE_K6_SCRIPT === "true" ||
|
|
@@ -324,20 +630,32 @@ Then(
|
|
|
324
630
|
if (!saveK6Script) {
|
|
325
631
|
try {
|
|
326
632
|
fs.unlinkSync(scriptPath);
|
|
633
|
+
this.log?.(`๐งน Temporary k6 script deleted: "${scriptPath}"`);
|
|
327
634
|
} catch (cleanupErr) {
|
|
328
|
-
|
|
635
|
+
this.log?.(
|
|
636
|
+
`โ ๏ธ Warning: Could not delete temporary k6 script file: "${scriptPath}". Error: ${
|
|
637
|
+
cleanupErr instanceof Error
|
|
638
|
+
? cleanupErr.message
|
|
639
|
+
: String(cleanupErr)
|
|
640
|
+
}`
|
|
641
|
+
);
|
|
329
642
|
}
|
|
330
643
|
} else {
|
|
331
|
-
|
|
644
|
+
this.log?.(
|
|
645
|
+
`โน๏ธ k6 script kept at: "${scriptPath}". Set SAVE_K6_SCRIPT=false to delete automatically.`
|
|
646
|
+
);
|
|
332
647
|
}
|
|
333
648
|
} catch (error) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
649
|
+
this.log?.(
|
|
650
|
+
`โ Failed to generate or run k6 script: ${
|
|
651
|
+
error instanceof Error ? error.message : String(error)
|
|
652
|
+
}`
|
|
653
|
+
);
|
|
654
|
+
throw new Error(
|
|
655
|
+
`k6 script generation or execution failed: ${
|
|
656
|
+
error instanceof Error ? error.message : String(error)
|
|
657
|
+
}`
|
|
337
658
|
);
|
|
338
|
-
throw new Error("k6 script generation or execution failed");
|
|
339
659
|
}
|
|
340
660
|
}
|
|
341
661
|
);
|
|
342
|
-
|
|
343
|
-
// Repeat this pattern for all other step definitions!
|