k6-cucumber-steps 1.0.39 → 1.0.41
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.example +1 -1
- package/.vscode/settings.json +2 -1
- package/cucumber.js +3 -3
- package/lib/helpers/buildK6Script.js +12 -9
- package/lib/helpers/generateHeaders.js +2 -2
- package/lib/utils/k6Runner.js +33 -36
- package/package.json +1 -1
- package/step_definitions/load_test_steps.js +49 -18
- package/step_definitions/world.js +24 -1
package/.env.example
CHANGED
package/.vscode/settings.json
CHANGED
package/cucumber.js
CHANGED
|
@@ -6,10 +6,10 @@ module.exports = {
|
|
|
6
6
|
"json:./src/examples/reports/load-report.json",
|
|
7
7
|
"html:./src/examples/reports/report.html",
|
|
8
8
|
],
|
|
9
|
-
paths: ["./src/examples/features
|
|
10
|
-
tags:
|
|
9
|
+
paths: ["./src/examples/features/*.feature"],
|
|
10
|
+
tags: "@load",
|
|
11
11
|
worldParameters: {
|
|
12
|
-
payloadPath: "
|
|
12
|
+
payloadPath: "src/examples/payloads",
|
|
13
13
|
},
|
|
14
14
|
overwrite: true,
|
|
15
15
|
reporter: true,
|
|
@@ -21,6 +21,10 @@ module.exports = function buildK6Script(config) {
|
|
|
21
21
|
return url.startsWith("/") ? `${BASE_URL}${url}` : url;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
// Convert to inline JS object (not stringified) so K6 can call JSON.stringify() itself
|
|
25
|
+
const stringifiedHeaders = JSON.stringify(headers, null, 2);
|
|
26
|
+
const stringifiedBody = JSON.stringify(body, null, 2);
|
|
27
|
+
|
|
24
28
|
return `
|
|
25
29
|
import http from 'k6/http';
|
|
26
30
|
import { check } from 'k6';
|
|
@@ -28,6 +32,9 @@ import { check } from 'k6';
|
|
|
28
32
|
export const options = ${JSON.stringify(options, null, 2)};
|
|
29
33
|
|
|
30
34
|
export default function () {
|
|
35
|
+
const headers = ${stringifiedHeaders};
|
|
36
|
+
const body = ${stringifiedBody};
|
|
37
|
+
|
|
31
38
|
${
|
|
32
39
|
endpoints?.length
|
|
33
40
|
? endpoints
|
|
@@ -37,15 +44,13 @@ export default function () {
|
|
|
37
44
|
const res${i} = http.request("${method}", resolvedUrl${i}, ${
|
|
38
45
|
method === "GET" || method === "DELETE"
|
|
39
46
|
? "null"
|
|
40
|
-
: JSON.stringify(body)
|
|
41
|
-
}, {
|
|
42
|
-
headers: ${JSON.stringify(headers, null, 2)}
|
|
43
|
-
});
|
|
47
|
+
: "JSON.stringify(body)"
|
|
48
|
+
}, { headers });
|
|
44
49
|
console.log(\`Response Body for \${resolvedUrl${i}}: \${res${i}.body}\`);
|
|
45
50
|
check(res${i}, {
|
|
46
51
|
"status is 2xx": (r) => r.status >= 200 && r.status < 300
|
|
47
52
|
});
|
|
48
|
-
|
|
53
|
+
`
|
|
49
54
|
)
|
|
50
55
|
.join("\n")
|
|
51
56
|
: `
|
|
@@ -53,10 +58,8 @@ export default function () {
|
|
|
53
58
|
const res = http.request("${method}", resolvedUrl, ${
|
|
54
59
|
method === "GET" || method === "DELETE"
|
|
55
60
|
? "null"
|
|
56
|
-
: JSON.stringify(body)
|
|
57
|
-
}, {
|
|
58
|
-
headers: ${JSON.stringify(headers, null, 2)}
|
|
59
|
-
});
|
|
61
|
+
: "JSON.stringify(body)"
|
|
62
|
+
}, { headers });
|
|
60
63
|
console.log(\`Response Body for \${resolvedUrl}: \${res.body}\`);
|
|
61
64
|
check(res, {
|
|
62
65
|
"status is 2xx": (r) => r.status >= 200 && r.status < 300
|
|
@@ -21,9 +21,9 @@ module.exports = function generateHeaders(authType, env, aliases = {}) {
|
|
|
21
21
|
).toString("base64");
|
|
22
22
|
headers["Authorization"] = `Basic ${base64}`;
|
|
23
23
|
} else if (authType === "none") {
|
|
24
|
-
//
|
|
24
|
+
// No auth, just default content-type
|
|
25
25
|
} else if (aliases[authType]) {
|
|
26
|
-
//
|
|
26
|
+
// ✅ Custom alias support
|
|
27
27
|
headers["Authorization"] = `Bearer ${getValue(authType)}`;
|
|
28
28
|
} else {
|
|
29
29
|
throw new Error(
|
package/lib/utils/k6Runner.js
CHANGED
|
@@ -60,46 +60,43 @@ const runK6Script = async (scriptPath, overwrite = false) => {
|
|
|
60
60
|
const customLogo = `${chalkGreen} with @qaPaschalE's ${chalkYellow}k6-cucumber-steps v${packageJson.version}${resetColor}`;
|
|
61
61
|
|
|
62
62
|
return new Promise(async (resolve, reject) => {
|
|
63
|
-
exec(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// Split the k6 logo lines
|
|
67
|
-
const logoLines = stdout.split("\n");
|
|
63
|
+
exec(`k6 run "${scriptPath}"`, async (error, stdout, stderr) => {
|
|
64
|
+
// Split the k6 logo lines
|
|
65
|
+
const logoLines = stdout.split("\n");
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
modifiedStdout += "\n";
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Handle errors and cleanup
|
|
81
|
-
if (error) {
|
|
82
|
-
console.error("k6 error:", error);
|
|
83
|
-
console.error("k6 stdout:", modifiedStdout);
|
|
84
|
-
await delay(3000); // Wait for 3 seconds
|
|
85
|
-
console.error("k6 stderr:", stderr);
|
|
86
|
-
reject(new Error(`k6 test execution failed: ${error.message}`));
|
|
87
|
-
} else if (stderr) {
|
|
88
|
-
console.log("k6 stdout:", modifiedStdout);
|
|
89
|
-
await delay(3000); // Wait for 3 seconds
|
|
90
|
-
resolve(stdout);
|
|
91
|
-
} else {
|
|
92
|
-
console.log("k6 stdout:", modifiedStdout);
|
|
93
|
-
await delay(3000); // Wait for 3 seconds
|
|
94
|
-
resolve(stdout);
|
|
67
|
+
// Insert the custom logo under "Grafana" (on the third line)
|
|
68
|
+
let modifiedStdout = "";
|
|
69
|
+
for (let i = 0; i < logoLines.length; i++) {
|
|
70
|
+
modifiedStdout += logoLines[i];
|
|
71
|
+
if (i === 5) {
|
|
72
|
+
// Target the third line (index 2) of the k6 logo
|
|
73
|
+
modifiedStdout += ` ${customLogo}\n`;
|
|
95
74
|
}
|
|
75
|
+
modifiedStdout += "\n";
|
|
76
|
+
}
|
|
96
77
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
);
|
|
78
|
+
// Handle errors and cleanup
|
|
79
|
+
if (error) {
|
|
80
|
+
console.error("k6 error:", error);
|
|
81
|
+
console.error("k6 stdout:", modifiedStdout);
|
|
82
|
+
await delay(3000); // Wait for 3 seconds
|
|
83
|
+
console.error("k6 stderr:", stderr);
|
|
84
|
+
reject(new Error(`k6 test execution failed: ${error.message}`));
|
|
85
|
+
} else if (stderr) {
|
|
86
|
+
console.log("k6 stdout:", modifiedStdout);
|
|
87
|
+
await delay(3000); // Wait for 3 seconds
|
|
88
|
+
resolve(stdout);
|
|
89
|
+
} else {
|
|
90
|
+
console.log("k6 stdout:", modifiedStdout);
|
|
91
|
+
await delay(3000); // Wait for 3 seconds
|
|
92
|
+
resolve(stdout);
|
|
101
93
|
}
|
|
102
|
-
|
|
94
|
+
|
|
95
|
+
// Clean up the temporary script file
|
|
96
|
+
fs.unlink(scriptPath).catch((err) =>
|
|
97
|
+
console.error("Error deleting temporary k6 script:", err)
|
|
98
|
+
);
|
|
99
|
+
});
|
|
103
100
|
});
|
|
104
101
|
};
|
|
105
102
|
|
package/package.json
CHANGED
|
@@ -35,26 +35,56 @@ Given("I set a k6 script for {word} testing", function (method) {
|
|
|
35
35
|
* When I set to run the k6 script with the following configurations:
|
|
36
36
|
* | virtual_users | duration | http_req_failed | http_req_duration | error_rate | stages |
|
|
37
37
|
* | 10 | 5 | rate<0.05 | p(95)<200 | | |
|
|
38
|
-
* | 50 | 10 |
|
|
38
|
+
* | 50 | 10 | rate<0.01 | | rate<0.01 | [{"target": 10, "duration": "10s"}, {"target": 50, "duration": "30s"}] |
|
|
39
39
|
*/
|
|
40
40
|
When(
|
|
41
41
|
"I set to run the k6 script with the following configurations:",
|
|
42
42
|
function (dataTable) {
|
|
43
|
-
const
|
|
43
|
+
const rawRow = dataTable.hashes()[0];
|
|
44
|
+
const row = {};
|
|
44
45
|
|
|
45
|
-
//
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
// Extract example values manually from this.pickle
|
|
47
|
+
const exampleMap = {};
|
|
48
|
+
if (this.pickle && this.pickle.astNodeIds && this.gherkinDocument) {
|
|
49
|
+
const scenario = this.gherkinDocument.feature.children.find((child) => {
|
|
50
|
+
return child.scenario && child.scenario.examples?.length;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const exampleValues =
|
|
54
|
+
scenario?.scenario?.examples?.[0]?.tableBody?.[0]?.cells?.map(
|
|
55
|
+
(cell) => cell.value
|
|
56
|
+
) || [];
|
|
57
|
+
|
|
58
|
+
const exampleKeys =
|
|
59
|
+
scenario?.scenario?.examples?.[0]?.tableHeader?.cells?.map(
|
|
60
|
+
(cell) => cell.value
|
|
61
|
+
) || [];
|
|
62
|
+
|
|
63
|
+
exampleKeys.forEach((key, idx) => {
|
|
64
|
+
exampleMap[key] = exampleValues[idx];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const [key, value] of Object.entries(rawRow)) {
|
|
69
|
+
row[key] = value.replace(/<([^>]+)>/g, (_, param) => {
|
|
70
|
+
return exampleMap[param] || value;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log("🚨 Resolved config row:", row);
|
|
75
|
+
|
|
76
|
+
const validateThreshold = (value) => {
|
|
77
|
+
const regex = /^[\w{}()<>:]+[<>=]\d+(\.\d+)?$/;
|
|
78
|
+
if (value && !regex.test(value)) {
|
|
79
|
+
throw new Error(`Invalid threshold format: ${value}`);
|
|
49
80
|
}
|
|
50
81
|
};
|
|
51
82
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
83
|
+
validateThreshold(row.http_req_failed);
|
|
84
|
+
validateThreshold(row.http_req_duration);
|
|
85
|
+
validateThreshold(row.error_rate);
|
|
55
86
|
|
|
56
87
|
if (row.stages) {
|
|
57
|
-
// User provided a stages definition (JSON array)
|
|
58
88
|
try {
|
|
59
89
|
this.config.options = {
|
|
60
90
|
stages: JSON.parse(row.stages),
|
|
@@ -63,11 +93,10 @@ When(
|
|
|
63
93
|
http_req_duration: [row.http_req_duration],
|
|
64
94
|
},
|
|
65
95
|
};
|
|
66
|
-
} catch
|
|
96
|
+
} catch {
|
|
67
97
|
throw new Error("Invalid stages JSON format.");
|
|
68
98
|
}
|
|
69
99
|
} else {
|
|
70
|
-
// Default to VUs and duration
|
|
71
100
|
this.config.options = {
|
|
72
101
|
vus: parseInt(row.virtual_users),
|
|
73
102
|
duration: `${row.duration}s`,
|
|
@@ -78,7 +107,7 @@ When(
|
|
|
78
107
|
};
|
|
79
108
|
}
|
|
80
109
|
|
|
81
|
-
if (row.error_rate
|
|
110
|
+
if (row.error_rate) {
|
|
82
111
|
this.config.options.thresholds.error_rate = [row.error_rate];
|
|
83
112
|
}
|
|
84
113
|
}
|
|
@@ -171,14 +200,11 @@ When(
|
|
|
171
200
|
});
|
|
172
201
|
|
|
173
202
|
this.config = {
|
|
203
|
+
...this.config, // ✅ Keep previously set options!
|
|
174
204
|
method: methodUpper,
|
|
175
205
|
endpoint,
|
|
176
206
|
body: resolved,
|
|
177
207
|
headers: this.config?.headers || {},
|
|
178
|
-
options: {
|
|
179
|
-
vus: 1,
|
|
180
|
-
iterations: 1,
|
|
181
|
-
},
|
|
182
208
|
};
|
|
183
209
|
|
|
184
210
|
this.lastRequest = {
|
|
@@ -198,8 +224,13 @@ When(
|
|
|
198
224
|
* When I set the authentication type to "none"
|
|
199
225
|
*/
|
|
200
226
|
When("I set the authentication type to {string}", function (authType) {
|
|
201
|
-
this.config.headers = generateHeaders(
|
|
227
|
+
this.config.headers = generateHeaders(
|
|
228
|
+
authType,
|
|
229
|
+
process.env,
|
|
230
|
+
this.aliases || {}
|
|
231
|
+
);
|
|
202
232
|
});
|
|
233
|
+
|
|
203
234
|
/**
|
|
204
235
|
* Then I store the value at "data.token" as alias "token"
|
|
205
236
|
*/
|
|
@@ -1,16 +1,39 @@
|
|
|
1
1
|
const { setWorldConstructor } = require("@cucumber/cucumber");
|
|
2
2
|
|
|
3
3
|
class CustomWorld {
|
|
4
|
-
constructor({ parameters }) {
|
|
4
|
+
constructor({ parameters, pickle }) {
|
|
5
5
|
this.options = {};
|
|
6
6
|
this.configurations = {};
|
|
7
7
|
this.endpoints = [];
|
|
8
8
|
this.authType = "";
|
|
9
9
|
this.postBody = {};
|
|
10
|
+
this.aliases = {};
|
|
11
|
+
this.lastRequest = null;
|
|
12
|
+
this.lastResponse = null;
|
|
13
|
+
|
|
10
14
|
this.overwrite =
|
|
11
15
|
parameters?.overwrite ||
|
|
12
16
|
process.env.K6_CUCUMBER_OVERWRITE === "true" ||
|
|
13
17
|
false;
|
|
18
|
+
|
|
19
|
+
// ✅ Store scenario example values from <placeholders>
|
|
20
|
+
this.parameters = {};
|
|
21
|
+
|
|
22
|
+
if (parameters) {
|
|
23
|
+
this.parameters = { ...parameters };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (pickle?.steps?.length) {
|
|
27
|
+
const matches = pickle.steps.flatMap((step) => {
|
|
28
|
+
return [...(step.text?.matchAll(/<([^>]+)>/g) || [])];
|
|
29
|
+
});
|
|
30
|
+
matches.forEach((match) => {
|
|
31
|
+
const key = match[1];
|
|
32
|
+
if (parameters?.[key]) {
|
|
33
|
+
this.parameters[key] = parameters[key];
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
14
37
|
}
|
|
15
38
|
}
|
|
16
39
|
|