k6-cucumber-steps 1.2.1 → 1.2.3

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.
@@ -20,9 +20,10 @@ program
20
20
  .option("-f, --feature <path>", "Feature file path")
21
21
  .option("-t, --tags <string>", "Cucumber tags")
22
22
  .option("-c, --config <file>", "Custom config file")
23
- .option("-r, --reporter", "Enable report generation", false)
24
- .option("-o, --overwrite", "Overwrite report files", false)
25
- .option("--cleanReports", "Clean the reports folder before running")
23
+ .option("--saveK6Script", "Keep generated k6 script files", false)
24
+ .option("--overwrite", "Overwrite report files", false)
25
+ .option("--cleanReports", "Clean the reports folder before running", false)
26
+ .option("--reporter", "Enable report generation", false)
26
27
  .option("--clean", "Alias for --cleanReports")
27
28
  .option("-p, --payloadPath <dir>", "Directory for payload files")
28
29
  .action(async (argv) => {
@@ -112,10 +113,23 @@ program
112
113
  }
113
114
  }
114
115
 
116
+ // Collect options to pass as env vars
117
+ const extraEnv = {
118
+ SAVE_K6_SCRIPT: argv.saveK6Script === true || process.env.SAVE_K6_SCRIPT === "true" || cucumberConfig.saveK6Script === true ? "true" : "false",
119
+ K6_CUCUMBER_OVERWRITE: argv.overwrite === true || process.env.K6_CUCUMBER_OVERWRITE === "true" || cucumberConfig.overwrite === true ? "true" : "false",
120
+ CLEAN_REPORTS: argv.cleanReports === true || argv.clean === true || process.env.CLEAN_REPORTS === "true" || cucumberConfig.cleanReports === true ? "true" : "false",
121
+ K6_CUCUMBER_REPORTER: argv.reporter === true || process.env.K6_CUCUMBER_REPORTER === "true" || cucumberConfig.reporter === true ? "true" : "false",
122
+ };
123
+
115
124
  // Now spawn the process
116
- const cucumberProcess = spawn("npx", ["cucumber-js", ...cliParts], {
117
- stdio: "inherit",
118
- });
125
+ const cucumberProcess = spawn(
126
+ "npx",
127
+ ["cucumber-js", ...cliParts],
128
+ {
129
+ stdio: "inherit",
130
+ env: { ...process.env, ...extraEnv },
131
+ }
132
+ );
119
133
 
120
134
  cucumberProcess.on("close", async (code) => {
121
135
  if (code === 0) {
@@ -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.1",
3
+ "version": "1.2.3",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -7,125 +7,98 @@ const resolveBody = require("../lib/helpers/resolveBody.js");
7
7
  const buildK6Script = require("../lib/helpers/buildK6Script.js");
8
8
  const generateHeaders = require("../lib/helpers/generateHeaders.js");
9
9
  const { generateK6Script, runK6Script } = require("../lib/utils/k6Runner.js");
10
+ const os = require("os");
11
+ const crypto = require("crypto");
10
12
  require("dotenv").config();
11
13
 
12
14
  /**
13
15
  * Sets the HTTP method for the k6 script.
14
- *
15
- * ```gherkin
16
- * Given I set a k6 script for {word} testing
17
- * ```
18
- * @example
19
- * Given I set a k6 script for GET testing
20
- * @category K6 Steps
21
16
  */
22
- async function Given_I_set_a_k6_script_for_method(this, method) {
17
+ Given(/^I set a k6 script for (\w+) testing$/, async function (method) {
23
18
  this.config = { method: method.toUpperCase() };
24
- }
25
- Given(/^I set a k6 script for (\w+) testing$/, Given_I_set_a_k6_script_for_method);
19
+ });
26
20
 
27
21
  /**
28
22
  * Sets k6 script options from a configuration table.
29
- *
30
- * ```gherkin
31
- * When I set to run the k6 script with the following configurations:
32
- * | virtual_users | duration | http_req_failed | http_req_duration | error_rate | stages |
33
- * ```
34
- * @example
35
- * When I set to run the k6 script with the following configurations:
36
- * | virtual_users | duration | http_req_failed | http_req_duration | error_rate | stages |
37
- * | 10 | 5 | rate<0.05 | p(95)<200 | | |
38
- * @category K6 Steps
39
23
  */
40
- async function When_I_set_k6_script_configurations(this, dataTable) {
41
- const rawRow = dataTable.hashes()[0];
42
- const row = {};
43
-
44
- // Extract example values manually from this.pickle
45
- const exampleMap = {};
46
- if (this.pickle && this.pickle.astNodeIds && this.gherkinDocument) {
47
- const scenario = this.gherkinDocument.feature.children.find((child) => {
48
- return child.scenario && child.scenario.examples?.length;
49
- });
50
-
51
- const exampleValues =
52
- scenario?.scenario?.examples?.[0]?.tableBody?.[0]?.cells?.map(
53
- (cell) => cell.value
54
- ) || [];
55
-
56
- const exampleKeys =
57
- scenario?.scenario?.examples?.[0]?.tableHeader?.cells?.map(
58
- (cell) => cell.value
59
- ) || [];
60
-
61
- exampleKeys.forEach((key, idx) => {
62
- exampleMap[key] = exampleValues[idx];
63
- });
64
- }
65
-
66
- for (const [key, value] of Object.entries(rawRow)) {
67
- row[key] = value.replace(/<([^>]+)>/g, (_, param) => {
68
- return exampleMap[param] || value;
69
- });
70
- }
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
+ }
71
51
 
72
- const validateThreshold = (value) => {
73
- const regex = /^[\w{}()<>:]+[<>=]\d+(\.\d+)?$/;
74
- if (value && !regex.test(value)) {
75
- throw new Error(`Invalid threshold format: ${value}`);
52
+ for (const [key, value] of Object.entries(rawRow)) {
53
+ row[key] = value.replace(/<([^>]+)>/g, (_, param) => {
54
+ return exampleMap[param] || value;
55
+ });
76
56
  }
77
- };
78
57
 
79
- validateThreshold(row.http_req_failed);
80
- validateThreshold(row.http_req_duration);
81
- validateThreshold(row.error_rate);
58
+ const validateThreshold = (value) => {
59
+ const regex = /^[\w{}()<>:]+[<>=]\d+(\.\d+)?$/;
60
+ if (value && !regex.test(value)) {
61
+ throw new Error(`Invalid threshold format: ${value}`);
62
+ }
63
+ };
82
64
 
83
- if (row.stages) {
84
- try {
65
+ validateThreshold(row.http_req_failed);
66
+ validateThreshold(row.http_req_duration);
67
+ validateThreshold(row.error_rate);
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.");
80
+ }
81
+ } else {
85
82
  this.config.options = {
86
- stages: JSON.parse(row.stages),
83
+ vus: parseInt(row.virtual_users),
84
+ duration: `${row.duration}s`,
87
85
  thresholds: {
88
86
  http_req_failed: [row.http_req_failed],
89
87
  http_req_duration: [row.http_req_duration],
90
88
  },
91
89
  };
92
- } catch {
93
- throw new Error("Invalid stages JSON format.");
94
90
  }
95
- } else {
96
- this.config.options = {
97
- vus: parseInt(row.virtual_users),
98
- duration: `${row.duration}s`,
99
- thresholds: {
100
- http_req_failed: [row.http_req_failed],
101
- http_req_duration: [row.http_req_duration],
102
- },
103
- };
104
- }
105
91
 
106
- if (row.error_rate) {
107
- this.config.options.thresholds.error_rate = [row.error_rate];
92
+ if (row.error_rate) {
93
+ this.config.options.thresholds.error_rate = [row.error_rate];
94
+ }
108
95
  }
109
- }
110
- When(
111
- /^I set to run the k6 script with the following configurations:$/,
112
- When_I_set_k6_script_configurations
113
96
  );
114
97
 
115
98
  /**
116
99
  * Sets request headers for the k6 script.
117
- *
118
- * ```gherkin
119
- * When I set the request headers:
120
- * | Header | Value |
121
- * ```
122
- * @example
123
- * When I set the request headers:
124
- * | Header | Value |
125
- * | Content-Type | application/json |
126
- * @category K6 Steps
127
100
  */
128
- async function When_I_set_request_headers(this, dataTable) {
101
+ When(/^I set the request headers:$/, async function (dataTable) {
129
102
  const headers = {};
130
103
  dataTable.hashes().forEach(({ Header, Value }) => {
131
104
  headers[Header] = Value;
@@ -135,274 +108,236 @@ async function When_I_set_request_headers(this, dataTable) {
135
108
  ...this.config.headers,
136
109
  ...headers,
137
110
  };
138
- }
139
- When(/^I set the request headers:$/, When_I_set_request_headers);
111
+ });
140
112
 
141
113
  /**
142
114
  * Sets endpoints for the k6 script.
143
- *
144
- * ```gherkin
145
- * When I set the following endpoints used:
146
- * """
147
- * /api/users
148
- * /api/products
149
- * """
150
- * ```
151
- * @example
152
- * When I set the following endpoints used:
153
- * """
154
- * /api/users
155
- * /api/products
156
- * """
157
- * @category K6 Steps
158
115
  */
159
- async function When_I_set_endpoints_used(this, docString) {
116
+ When(/^I set the following endpoints used:$/, async function (docString) {
160
117
  this.config.endpoints = docString
161
118
  .trim()
162
119
  .split("\n")
163
120
  .map((line) => line.trim());
164
- }
165
- When(/^I set the following endpoints used:$/, When_I_set_endpoints_used);
121
+ });
166
122
 
167
123
  /**
168
124
  * Sets the request body for a specific method and endpoint.
169
- *
170
- * ```gherkin
171
- * When I set the following {word} body is used for {string}
172
- * """
173
- * { ... }
174
- * """
175
- * ```
176
- * @example
177
- * When I set the following POST body is used for "/api/users"
178
- * """
179
- * { "username": "{{username}}" }
180
- * """
181
- * @category K6 Steps
182
125
  */
183
- async function When_I_set_body_for_method_and_endpoint(this, method, endpoint, docString) {
184
- this.config.method = method.toUpperCase();
185
- this.config.body = resolveBody(docString, process.env);
186
- this.config.endpoint = endpoint;
187
- }
188
126
  When(
189
127
  /^I set the following (\w+) body is used for "([^"]+)"$/,
190
- When_I_set_body_for_method_and_endpoint
128
+ async function (method, endpoint, docString) {
129
+ this.config.method = method.toUpperCase();
130
+ this.config.body = resolveBody(docString, process.env);
131
+ this.config.endpoint = endpoint;
132
+ }
191
133
  );
192
134
 
193
135
  /**
194
136
  * Loads a JSON payload for a method and endpoint.
195
- *
196
- * ```gherkin
197
- * When I use JSON payload from {string} for {word} to {string}
198
- * ```
199
- * @example
200
- * When I use JSON payload from "login.json" for POST to "/api/login"
201
- * @category K6 Steps
202
137
  */
203
- async function When_I_use_JSON_payload(this, fileName, method, endpoint) {
204
- const allowedMethods = ["POST", "PUT", "PATCH"];
205
- const methodUpper = method.toUpperCase();
206
-
207
- if (!allowedMethods.includes(methodUpper)) {
208
- throw new Error(
209
- `Method "${method}" is not supported. Use one of: ${allowedMethods.join(
210
- ", "
211
- )}`
212
- );
213
- }
138
+ When(
139
+ /^I use JSON payload from "([^"]+)" for (\w+) to "([^"]+)"$/,
140
+ async function (fileName, method, endpoint) {
141
+ const allowedMethods = ["POST", "PUT", "PATCH"];
142
+ const methodUpper = method.toUpperCase();
143
+
144
+ if (!allowedMethods.includes(methodUpper)) {
145
+ throw new Error(
146
+ `Method "${method}" is not supported. Use one of: ${allowedMethods.join(
147
+ ", "
148
+ )}`
149
+ );
150
+ }
214
151
 
215
- const projectRoot = path.resolve(__dirname, "..", "..");
216
- const payloadDir = this.parameters?.payloadPath || "payloads";
217
- const payloadPath = path.isAbsolute(payloadDir)
218
- ? path.join(payloadDir, fileName)
219
- : path.join(projectRoot, payloadDir, fileName);
152
+ const projectRoot = path.resolve(__dirname, "..", "..");
153
+ const payloadDir = this.parameters?.payloadPath || "payloads";
154
+ const payloadPath = path.isAbsolute(payloadDir)
155
+ ? path.join(payloadDir, fileName)
156
+ : path.join(projectRoot, payloadDir, fileName);
220
157
 
221
- if (!fs.existsSync(payloadPath)) {
222
- throw new Error(`Payload file not found: ${payloadPath}`);
223
- }
158
+ if (!fs.existsSync(payloadPath)) {
159
+ throw new Error(`Payload file not found: ${payloadPath}`);
160
+ }
224
161
 
225
- const rawTemplate = fs.readFileSync(payloadPath, "utf-8");
226
- const resolved = resolveBody(rawTemplate, {
227
- ...process.env,
228
- ...(this.aliases || {}),
229
- });
162
+ const rawTemplate = fs.readFileSync(payloadPath, "utf-8");
163
+ const resolved = resolveBody(rawTemplate, {
164
+ ...process.env,
165
+ ...(this.aliases || {}),
166
+ });
230
167
 
231
- this.config = {
232
- ...this.config,
233
- method: methodUpper,
234
- endpoint,
235
- body: resolved,
236
- headers: this.config?.headers || {},
237
- };
168
+ this.config = {
169
+ ...this.config,
170
+ method: methodUpper,
171
+ endpoint,
172
+ body: resolved,
173
+ headers: this.config?.headers || {},
174
+ };
238
175
 
239
- this.lastRequest = {
240
- method: methodUpper,
241
- endpoint,
242
- body: resolved,
243
- };
244
- }
245
- When(
246
- /^I use JSON payload from "([^"]+)" for (\w+) to "([^"]+)"$/,
247
- When_I_use_JSON_payload
176
+ this.lastRequest = {
177
+ method: methodUpper,
178
+ endpoint,
179
+ body: resolved,
180
+ };
181
+ }
248
182
  );
249
183
 
250
184
  /**
251
185
  * Sets the authentication type for the request.
252
- *
253
- * ```gherkin
254
- * When I set the authentication type to {string}
255
- * ```
256
- * @example
257
- * When I set the authentication type to "bearer_token"
258
- * @category K6 Steps
259
186
  */
260
- async function When_I_set_authentication_type(this, authType) {
187
+ When(/^I set the authentication type to "([^"]+)"$/, async function (authType) {
261
188
  this.config.headers = generateHeaders(
262
189
  authType,
263
190
  process.env,
264
191
  this.aliases || {}
265
192
  );
266
- }
267
- When(/^I set the authentication type to "([^"]+)"$/, When_I_set_authentication_type);
193
+ });
268
194
 
269
195
  /**
270
196
  * Stores a value from the last response as an alias.
271
- *
272
- * ```gherkin
273
- * Then I store the value at {string} as alias {string}
274
- * ```
275
- * @example
276
- * Then I store the value at "data.token" as alias "token"
277
- * @category K6 Steps
278
197
  */
279
- async function Then_I_store_value_as_alias(this, jsonPath, alias) {
280
- if (!this.lastResponse) {
281
- throw new Error("No previous response available.");
282
- }
198
+ Then(
199
+ /^I store the value at "([^"]+)" as alias "([^"]+)"$/,
200
+ async function (jsonPath, alias) {
201
+ if (!this.lastResponse) {
202
+ throw new Error("No previous response available.");
203
+ }
283
204
 
284
- const pathParts = jsonPath.split(".");
285
- let value = this.lastResponse;
205
+ const pathParts = jsonPath.split(".");
206
+ let value = this.lastResponse;
286
207
 
287
- for (const key of pathParts) {
288
- value = value?.[key];
289
- if (value === undefined) break;
290
- }
208
+ for (const key of pathParts) {
209
+ value = value?.[key];
210
+ if (value === undefined) break;
211
+ }
291
212
 
292
- if (value === undefined) {
293
- throw new Error(`Could not resolve path "${jsonPath}" in the response`);
294
- }
213
+ if (value === undefined) {
214
+ throw new Error(`Could not resolve path "${jsonPath}" in the response`);
215
+ }
295
216
 
296
- if (!this.aliases) this.aliases = {};
297
- this.aliases[alias] = value;
217
+ if (!this.aliases) this.aliases = {};
218
+ this.aliases[alias] = value;
298
219
 
299
- console.log(`🧩 Stored alias "${alias}":`, value);
300
- }
301
- Then(
302
- /^I store the value at "([^"]+)" as alias "([^"]+)"$/,
303
- Then_I_store_value_as_alias
220
+ console.log(`🧩 Stored alias "${alias}":`, value);
221
+ }
304
222
  );
305
223
 
306
224
  /**
307
225
  * Logs in via POST to an endpoint with a payload from a file.
308
- *
309
- * ```gherkin
310
- * When I login via POST to {string} with payload from {string}
311
- * ```
312
- * @example
313
- * When I login via POST to "/api/login" with payload from "login.json"
314
- * @category K6 Steps
315
226
  */
316
- async function When_I_login_via_POST_with_payload(this, endpoint, fileName) {
317
- const payloadDir = this.parameters?.payloadPath || "payloads";
318
- const projectRoot = path.resolve(__dirname, "..", "..");
319
- const payloadPath = path.isAbsolute(payloadDir)
320
- ? path.join(payloadDir, fileName)
321
- : path.join(projectRoot, payloadDir, fileName);
322
-
323
- if (!fs.existsSync(payloadPath)) {
324
- throw new Error(`Payload file not found: ${payloadPath}`);
325
- }
326
-
327
- const rawTemplate = fs.readFileSync(payloadPath, "utf-8");
328
- const resolved = resolveBody(rawTemplate, {
329
- ...process.env,
330
- ...(this.aliases || {}),
331
- });
227
+ When(
228
+ /^I login via POST to "([^"]+)" with payload from "([^"]+)"$/,
229
+ async function (endpoint, fileName) {
230
+ const payloadDir = this.parameters?.payloadPath || "payloads";
231
+ const projectRoot = path.resolve(__dirname, "..", "..");
232
+ const payloadPath = path.isAbsolute(payloadDir)
233
+ ? path.join(payloadDir, fileName)
234
+ : path.join(projectRoot, payloadDir, fileName);
235
+
236
+ if (!fs.existsSync(payloadPath)) {
237
+ throw new Error(`Payload file not found: ${payloadPath}`);
238
+ }
332
239
 
333
- try {
334
- const response = await fetch(`${process.env.BASE_URL}${endpoint}`, {
335
- method: "POST",
336
- headers: {
337
- "Content-Type": "application/json",
338
- },
339
- body: JSON.stringify(resolved),
240
+ const rawTemplate = fs.readFileSync(payloadPath, "utf-8");
241
+ const resolved = resolveBody(rawTemplate, {
242
+ ...process.env,
243
+ ...(this.aliases || {}),
340
244
  });
341
245
 
342
- const data = await response.json();
246
+ try {
247
+ const response = await fetch(`${process.env.BASE_URL}${endpoint}`, {
248
+ method: "POST",
249
+ headers: {
250
+ "Content-Type": "application/json",
251
+ },
252
+ body: JSON.stringify(resolved),
253
+ });
343
254
 
344
- if (!response.ok) {
345
- console.error("❌ Login request failed:", data);
346
- throw new Error(`Login request failed with status ${response.status}`);
347
- }
255
+ const data = await response.json();
348
256
 
349
- this.lastResponse = data;
350
- console.log("🔐 Login successful, response saved to alias context.");
351
- } catch (err) {
352
- console.error("❌ Login request failed:", err.message);
353
- throw new Error("Login request failed");
354
- }
355
- }
356
- When(
357
- /^I login via POST to "([^"]+)" with payload from "([^"]+)"$/,
358
- When_I_login_via_POST_with_payload
359
- );
257
+ if (!response.ok) {
258
+ console.error("❌ Login request failed:", data);
259
+ throw new Error(`Login request failed with status ${response.status}`);
260
+ }
360
261
 
361
- /**
362
- * Executes the k6 script and checks the result.
363
- *
364
- * ```gherkin
365
- * Then I see the API should handle the {word} request successfully
366
- * ```
367
- * @example
368
- * Then I see the API should handle the POST request successfully
369
- * @category K6 Steps
370
- */
371
- async function Then_I_see_API_should_handle_request(this, method) {
372
- if (!this.config || !this.config.method) {
373
- throw new Error("Configuration is missing or incomplete.");
374
- }
375
- const expectedMethod = method.toUpperCase();
376
- const actualMethod = this.config.method.toUpperCase();
377
- if (actualMethod !== expectedMethod) {
378
- throw new Error(
379
- `Mismatched HTTP method: expected "${expectedMethod}", got "${actualMethod}"`
380
- );
381
- }
382
- try {
383
- const scriptContent = buildK6Script(this.config);
384
- const scriptPath = await generateK6Script(
385
- scriptContent,
386
- "load",
387
- process.env.K6_CUCUMBER_OVERWRITE === "true"
388
- );
389
- const { stdout, stderr, code } = await runK6Script(
390
- scriptPath,
391
- process.env.K6_CUCUMBER_OVERWRITE === "true"
392
- );
393
-
394
- if (stdout) console.log(stdout);
395
- if (stderr) console.error(stderr);
396
-
397
- if (code !== 0) {
398
- throw new Error(`k6 exited with code ${code}`);
262
+ this.lastResponse = data;
263
+ console.log("🔐 Login successful, response saved to alias context.");
264
+ } catch (err) {
265
+ console.error("❌ Login request failed:", err.message);
266
+ throw new Error("Login request failed");
399
267
  }
400
- } catch (error) {
401
- console.error("k6 execution failed:", error.stack || error);
402
- throw new Error("k6 test execution failed");
403
268
  }
269
+ );
270
+
271
+ const genScriptDir = path.resolve(process.cwd(), "genScript");
272
+ if (!fs.existsSync(genScriptDir)) {
273
+ fs.mkdirSync(genScriptDir, { recursive: true });
404
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
+
405
286
  Then(
406
287
  /^I see the API should handle the (\w+) request successfully$/,
407
- Then_I_see_API_should_handle_request
288
+ { timeout: 300000 },
289
+ async function (method) {
290
+ if (!this.config || !this.config.method) {
291
+ throw new Error("Configuration is missing or incomplete.");
292
+ }
293
+ const expectedMethod = method.toUpperCase();
294
+ const actualMethod = this.config.method.toUpperCase();
295
+ if (actualMethod !== expectedMethod) {
296
+ throw new Error(
297
+ `Mismatched HTTP method: expected "${expectedMethod}", got "${actualMethod}"`
298
+ );
299
+ }
300
+ try {
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}`);
306
+
307
+ // Always run k6 automatically, regardless of VU count
308
+ const { stdout, stderr, code } = await runK6Script(
309
+ scriptPath,
310
+ process.env.K6_CUCUMBER_OVERWRITE === "true"
311
+ );
312
+ if (stdout) console.log(stdout);
313
+ if (stderr) console.error(stderr);
314
+ if (code !== 0) {
315
+ throw new Error(`k6 exited with code ${code}`);
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
+ }
333
+ } catch (error) {
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");
339
+ }
340
+ }
408
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 };