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 CHANGED
@@ -7,7 +7,7 @@ PARALLEL=Scenarios
7
7
  EXECUTED=Remote
8
8
 
9
9
  # Base URLs
10
- API_URL=https://test-api.example.com
10
+ API_BASE_URL=https://test-api.example.com
11
11
  BASE_URL=https://example.com
12
12
  AUTH_URL=https://auth.example.com
13
13
  PAYMENT_URL=https://payment.example.com
@@ -8,5 +8,6 @@
8
8
  "cucumber.glue": [
9
9
  "step_definitions/*.js",
10
10
  "src/features/stepDefinitions/*.js"
11
- ]
11
+ ],
12
+ "liveServer.settings.port": 5501
12
13
  }
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/loadTestTemplate.feature"],
10
- tags: process.env.TAGS,
9
+ paths: ["./src/examples/features/*.feature"],
10
+ tags: "@load",
11
11
  worldParameters: {
12
- payloadPath: "apps/qa/performance/payloads",
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
- // Do nothing extra
24
+ // No auth, just default content-type
25
25
  } else if (aliases[authType]) {
26
- // Dynamic alias token support
26
+ // Custom alias support
27
27
  headers["Authorization"] = `Bearer ${getValue(authType)}`;
28
28
  } else {
29
29
  throw new Error(
@@ -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
- `k6 run --vus 1 --iterations 1 "${scriptPath}"`,
65
- async (error, stdout, stderr) => {
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
- // Insert the custom logo under "Grafana" (on the third line)
70
- let modifiedStdout = "";
71
- for (let i = 0; i < logoLines.length; i++) {
72
- modifiedStdout += logoLines[i];
73
- if (i === 5) {
74
- // Target the third line (index 2) of the k6 logo
75
- modifiedStdout += ` ${customLogo}\n`;
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
- // Clean up the temporary script file
98
- fs.unlink(scriptPath).catch((err) =>
99
- console.error("Error deleting temporary k6 script:", err)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k6-cucumber-steps",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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 | | | rate<0.01 | [{"target": 10, "duration": "10s"}, {"target": 50, "duration": "30s"}] |
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 row = dataTable.hashes()[0];
43
+ const rawRow = dataTable.hashes()[0];
44
+ const row = {};
44
45
 
45
- // Validate thresholds only if they are not placeholders
46
- const validateIfNotPlaceholder = (value) => {
47
- if (value && !/^<.*>$/.test(value)) {
48
- validateThreshold(value);
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
- validateIfNotPlaceholder(row.http_req_failed);
53
- validateIfNotPlaceholder(row.http_req_duration);
54
- validateIfNotPlaceholder(row.error_rate);
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 (err) {
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 && !/^<.*>$/.test(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(authType, process.env);
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