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
- [k6ReportFilename]: html.replace("</body>", \`\${cucumberLink}</body>\`),
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.0",
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": ">=18"
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
- * @param {string} method - HTTP method (e.g., GET, POST).
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("I set a k6 script for {word} testing", function (method) {
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
- * @param {DataTable} dataTable - Table with configurations.
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
- "I set to run the k6 script with the following configurations:",
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
- * @param {DataTable} dataTable - Table with header and value for request headers.
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("I set the request headers:", function (dataTable) {
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, // preserve auth headers if set
108
+ ...this.config.headers,
131
109
  ...headers,
132
110
  };
133
111
  });
134
112
 
135
113
  /**
136
- * @param {string} docString - Multiline string containing the endpoints.
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("I set the following endpoints used:", function (docString) {
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
- * @param {string} method - HTTP method (e.g., POST, PUT).
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
- "I set the following {word} body is used for {string}",
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
- "I use JSON payload from {string} for {word} to {string}",
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(__dirname, "..", "..", payloadDir, fileName);
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, // ✅ Keep previously set options!
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
- * @param {string} authType - Authentication type (api_key, bearer_token, basic, none).
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("I set the authentication type to {string}", function (authType) {
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
- * Then I store the value at "data.token" as alias "token"
196
+ * Stores a value from the last response as an alias.
237
197
  */
238
198
  Then(
239
- "I store the value at {string} as alias {string}",
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
- "I login via POST to {string} with payload from {string}",
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(__dirname, "..", "..", payloadDir, fileName);
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; // ✅ Makes aliasing work
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
- * @param {string} method - HTTP method of the request.
309
- * @example
310
- * Then I see the API should handle the GET request successfully
311
- * Then I see the API should handle the POST request successfully
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
- "I see the API should handle the {word} request successfully",
315
- { timeout: 60000 },
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
- // Generate the k6 script file
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
- // Print the full error for debugging
352
- console.error("k6 execution failed:", error.stack || error);
353
- throw new Error("k6 test execution failed");
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,3 +1,4 @@
1
+ //world.js
1
2
  const { setWorldConstructor } = require("@cucumber/cucumber");
2
3
 
3
4
  class CustomWorld {
@@ -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 };