k6-cucumber-steps 1.2.27 โ†’ 2.0.1

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.
Files changed (116) hide show
  1. package/README.md +190 -177
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +72 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/init.command.d.ts +4 -0
  7. package/dist/commands/init.command.d.ts.map +1 -0
  8. package/dist/commands/init.command.js +30 -0
  9. package/dist/commands/init.command.js.map +1 -0
  10. package/dist/generators/feature.parser.d.ts +12 -0
  11. package/dist/generators/feature.parser.d.ts.map +1 -0
  12. package/dist/generators/feature.parser.js +208 -0
  13. package/dist/generators/feature.parser.js.map +1 -0
  14. package/dist/generators/k6-script.generator.d.ts +11 -0
  15. package/dist/generators/k6-script.generator.d.ts.map +1 -0
  16. package/dist/generators/k6-script.generator.js +233 -0
  17. package/dist/generators/k6-script.generator.js.map +1 -0
  18. package/dist/generators/project.generator.d.ts +14 -0
  19. package/dist/generators/project.generator.d.ts.map +1 -0
  20. package/dist/generators/project.generator.js +497 -0
  21. package/dist/generators/project.generator.js.map +1 -0
  22. package/dist/index.d.ts +24 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +53 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/runners/k6.runner.d.ts +19 -0
  27. package/dist/runners/k6.runner.d.ts.map +1 -0
  28. package/dist/runners/k6.runner.js +127 -0
  29. package/dist/runners/k6.runner.js.map +1 -0
  30. package/dist/step-registry.d.ts +14 -0
  31. package/dist/step-registry.d.ts.map +1 -0
  32. package/dist/step-registry.js +36 -0
  33. package/dist/step-registry.js.map +1 -0
  34. package/dist/types/index.d.ts +35 -0
  35. package/dist/types/index.d.ts.map +1 -0
  36. package/dist/types/index.js +3 -0
  37. package/dist/types/index.js.map +1 -0
  38. package/package.json +40 -62
  39. package/LICENSE +0 -21
  40. package/bin/k6-cucumber-steps.js +0 -176
  41. package/docs/data/search.json +0 -1
  42. package/docs/fonts/Inconsolata-Regular.ttf +0 -0
  43. package/docs/fonts/OpenSans-Regular.ttf +0 -0
  44. package/docs/fonts/WorkSans-Bold.ttf +0 -0
  45. package/docs/global.html +0 -36
  46. package/docs/index.html +0 -91
  47. package/docs/k6-cucumber-steps/1.2.8/data/search.json +0 -1
  48. package/docs/k6-cucumber-steps/1.2.8/fonts/Inconsolata-Regular.ttf +0 -0
  49. package/docs/k6-cucumber-steps/1.2.8/fonts/OpenSans-Regular.ttf +0 -0
  50. package/docs/k6-cucumber-steps/1.2.8/fonts/WorkSans-Bold.ttf +0 -0
  51. package/docs/k6-cucumber-steps/1.2.8/global.html +0 -3
  52. package/docs/k6-cucumber-steps/1.2.8/helpers_generateHeaders.js.html +0 -38
  53. package/docs/k6-cucumber-steps/1.2.8/helpers_resolveBody.js.html +0 -65
  54. package/docs/k6-cucumber-steps/1.2.8/index.html +0 -91
  55. package/docs/k6-cucumber-steps/1.2.8/module-generateHeaders.html +0 -3
  56. package/docs/k6-cucumber-steps/1.2.8/module-resolveBody.html +0 -3
  57. package/docs/k6-cucumber-steps/1.2.8/scripts/core.js +0 -726
  58. package/docs/k6-cucumber-steps/1.2.8/scripts/core.min.js +0 -23
  59. package/docs/k6-cucumber-steps/1.2.8/scripts/resize.js +0 -90
  60. package/docs/k6-cucumber-steps/1.2.8/scripts/search.js +0 -265
  61. package/docs/k6-cucumber-steps/1.2.8/scripts/search.min.js +0 -6
  62. package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/Apache-License-2.0.txt +0 -202
  63. package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/fuse.js +0 -9
  64. package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/hljs-line-num-original.js +0 -369
  65. package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/hljs-line-num.js +0 -1
  66. package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/hljs-original.js +0 -5171
  67. package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/hljs.js +0 -1
  68. package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/popper.js +0 -5
  69. package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/tippy.js +0 -1
  70. package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/tocbot.js +0 -672
  71. package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/tocbot.min.js +0 -1
  72. package/docs/k6-cucumber-steps/1.2.8/styles/clean-jsdoc-theme-base.css +0 -1159
  73. package/docs/k6-cucumber-steps/1.2.8/styles/clean-jsdoc-theme-dark.css +0 -412
  74. package/docs/k6-cucumber-steps/1.2.8/styles/clean-jsdoc-theme-light.css +0 -482
  75. package/docs/k6-cucumber-steps/1.2.8/styles/clean-jsdoc-theme-scrollbar.css +0 -30
  76. package/docs/k6-cucumber-steps/1.2.8/styles/clean-jsdoc-theme-without-scrollbar.min.css +0 -1
  77. package/docs/k6-cucumber-steps/1.2.8/styles/clean-jsdoc-theme.min.css +0 -1
  78. package/docs/k6-cucumber-steps/1.2.8/utils_k6Runner.js.html +0 -95
  79. package/docs/load_test_steps.js.html +0 -664
  80. package/docs/scripts/core.js +0 -726
  81. package/docs/scripts/core.min.js +0 -23
  82. package/docs/scripts/resize.js +0 -90
  83. package/docs/scripts/search.js +0 -265
  84. package/docs/scripts/search.min.js +0 -6
  85. package/docs/scripts/third-party/Apache-License-2.0.txt +0 -202
  86. package/docs/scripts/third-party/fuse.js +0 -9
  87. package/docs/scripts/third-party/hljs-line-num-original.js +0 -369
  88. package/docs/scripts/third-party/hljs-line-num.js +0 -1
  89. package/docs/scripts/third-party/hljs-original.js +0 -5171
  90. package/docs/scripts/third-party/hljs.js +0 -1
  91. package/docs/scripts/third-party/popper.js +0 -5
  92. package/docs/scripts/third-party/tippy.js +0 -1
  93. package/docs/scripts/third-party/tocbot.js +0 -672
  94. package/docs/scripts/third-party/tocbot.min.js +0 -1
  95. package/docs/styles/clean-jsdoc-theme-base.css +0 -1159
  96. package/docs/styles/clean-jsdoc-theme-dark.css +0 -412
  97. package/docs/styles/clean-jsdoc-theme-light.css +0 -482
  98. package/docs/styles/clean-jsdoc-theme-scrollbar.css +0 -30
  99. package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +0 -1
  100. package/docs/styles/clean-jsdoc-theme.min.css +0 -1
  101. package/index.js +0 -1
  102. package/lib/helpers/buildK6Script.d.ts +0 -1
  103. package/lib/helpers/buildK6Script.js +0 -103
  104. package/lib/helpers/generateHeaders.d.ts +0 -5
  105. package/lib/helpers/generateHeaders.js +0 -48
  106. package/lib/helpers/resolveBody.d.ts +0 -2
  107. package/lib/helpers/resolveBody.js +0 -78
  108. package/lib/helpers/resolvePayloadPath.js +0 -25
  109. package/lib/helpers/runK6ScriptFromWorld.js +0 -26
  110. package/lib/utils/k6Runner.d.ts +0 -6
  111. package/lib/utils/k6Runner.js +0 -89
  112. package/scripts/cucumber.js +0 -18
  113. package/scripts/linkReports.js +0 -107
  114. package/step_definitions/load_test_steps.d.ts +0 -89
  115. package/step_definitions/load_test_steps.js +0 -689
  116. package/step_definitions/world.js +0 -41
@@ -1,689 +0,0 @@
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
- import resolvePayloadPath from "../lib/helpers/resolvePayloadPath.js";
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();
15
-
16
- /**
17
- * @typedef {Object} CustomWorld
18
- * @property {Object} config
19
- * @property {Object} aliases
20
- * @property {Object} lastResponse
21
- * @property {Object} parameters
22
- * @property {Function} log
23
- */
24
-
25
- /**
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.
40
- */
41
-
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
- );
73
-
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
- });
122
- }
123
- }
124
- }
125
-
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];
130
- }
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),
156
- thresholds: {
157
- http_req_failed: [row.http_req_failed],
158
- http_req_duration: [row.http_req_duration],
159
- },
160
- };
161
- } catch (e) {
162
- throw new Error(`Invalid 'stages' JSON format: ${e.message}`);
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
- }
178
-
179
- if (row.error_rate) {
180
- k6Options.thresholds.error_rate = [row.error_rate];
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
191
- );
192
-
193
- /**
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
217
- */
218
- export async function When_I_set_request_headers(dataTable) {
219
- /** @type {CustomWorld} */ (this);
220
- const headers = {};
221
- dataTable.hashes().forEach(({ Header, Value }) => {
222
- headers[Header] = Value;
223
- });
224
-
225
- this.config.headers = {
226
- ...(this.config.headers || {}),
227
- ...headers,
228
- };
229
- this.log?.(`โš™๏ธ Request headers set: ${JSON.stringify(this.config.headers)}`);
230
- }
231
- When(/^I set the request headers:$/, When_I_set_request_headers);
232
-
233
- /**
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
256
- */
257
- export async function When_I_set_endpoints_used(docString) {
258
- /** @type {CustomWorld} */ (this);
259
- this.config.endpoints = docString
260
- .trim()
261
- .split("\n")
262
- .map((line) => line.trim());
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);
267
-
268
- /**
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
289
- */
290
- export async function When_I_set_method_body_for_endpoint(
291
- method,
292
- endpoint,
293
- docString
294
- ) {
295
- /** @type {CustomWorld} */ (this);
296
- const methodUpper = method.toUpperCase();
297
- const payloadDir = this.parameters?.payloadPath || "payloads";
298
- const doc = docString.trim();
299
-
300
- let body = "";
301
-
302
- // Try resolving from file if it looks like a filename
303
- const isLikelyFile = /^[\w\-.]+(\.json)?$/.test(doc);
304
- const fileName = doc.endsWith(".json") ? doc : `${doc}.json`;
305
-
306
- try {
307
- if (isLikelyFile) {
308
- const filePath = resolvePayloadPath(fileName, payloadDir);
309
- const fileContent = fs.readFileSync(filePath, "utf-8");
310
- body = resolveBody(fileContent, {
311
- ...process.env,
312
- ...(this.aliases || {}),
313
- });
314
- this.log?.(`๐Ÿ“ Loaded payload from file: "${fileName}"`);
315
- } else {
316
- throw new Error("Skipping file load; using raw input as body.");
317
- }
318
- } catch (e) {
319
- // If file doesn't exist or error occurs, treat as inline string
320
- body = resolveBody(doc, {
321
- ...process.env,
322
- ...(this.aliases || {}),
323
- });
324
- this.log?.("๐Ÿ“ Using docString directly as payload body.");
325
- }
326
-
327
- this.config = {
328
- ...(this.config || {}),
329
- method: methodUpper,
330
- endpoint,
331
- body,
332
- headers: this.config?.headers || {},
333
- };
334
-
335
- this.lastRequest = {
336
- method: methodUpper,
337
- endpoint,
338
- body,
339
- };
340
-
341
- this.log?.(
342
- `โš™๏ธ Body set for ${methodUpper} to "${endpoint}". Body preview: ${body.slice(
343
- 0,
344
- 100
345
- )}...`
346
- );
347
- }
348
- When(
349
- `^I set the following (\w+) body is used for "([^"]+)"$`,
350
- When_I_set_method_body_for_endpoint
351
- );
352
- /**
353
- * Loads a JSON payload from a file to be used as the request body for a specific
354
- * method and endpoint in the k6 script.
355
- *
356
- * ```gherkin
357
- * When I use JSON payload from "user_create.json" for POST to "/api/v1/users"
358
- * ```
359
- *
360
- * @param {string} fileName - The name of the JSON payload file (e.g., "user_data.json").
361
- * @param {string} method - The HTTP method (only "POST", "PUT", "PATCH" are supported for bodies).
362
- * @param {string} endpoint - The specific endpoint URL.
363
- *
364
- * @example
365
- * When I use JSON payload from "login_payload.json" for POST to "/auth/login"
366
- *
367
- * @remarks
368
- * This step reads the JSON file, resolves any placeholders within it (using `resolveBody`),
369
- * and sets `this.config.method`, `this.config.endpoint`, and `this.config.body`.
370
- * It also stores `lastRequest` in `this.lastRequest`.
371
- * The payload file path is resolved relative to `payloads` directory or `this.parameters.payloadPath`.
372
- * @category k6 Configuration Steps
373
- */
374
- export async function When_I_use_JSON_payload_from_file_for_method_to_endpoint(
375
- fileName,
376
- method,
377
- endpoint
378
- ) {
379
- /** @type {CustomWorld} */ (this);
380
- const allowedMethods = ["POST", "PUT", "PATCH"];
381
- const methodUpper = method.toUpperCase();
382
-
383
- if (!allowedMethods.includes(methodUpper)) {
384
- throw new Error(
385
- `Method "${method}" is not supported for JSON payloads from files. Use one of: ${allowedMethods.join(
386
- ", "
387
- )}`
388
- );
389
- }
390
-
391
- const payloadDir = this.parameters?.payloadPath || "payloads";
392
- const payloadPath = resolvePayloadPath(fileName, payloadDir);
393
-
394
- const rawTemplate = fs.readFileSync(payloadPath, "utf-8");
395
- const resolved = resolveBody(rawTemplate, {
396
- ...process.env,
397
- ...(this.aliases || {}),
398
- });
399
-
400
- this.config = {
401
- ...(this.config || {}),
402
- method: methodUpper,
403
- endpoint,
404
- body: resolved,
405
- headers: this.config?.headers || {},
406
- };
407
-
408
- this.lastRequest = {
409
- method: methodUpper,
410
- endpoint,
411
- body: resolved,
412
- };
413
- this.log?.(
414
- `โš™๏ธ JSON payload from "${fileName}" used for ${methodUpper} to "${endpoint}".`
415
- );
416
- }
417
- When(
418
- /^I use JSON payload from "([^"]+)" for (\w+) to "([^"]+)"$/,
419
- When_I_use_JSON_payload_from_file_for_method_to_endpoint
420
- );
421
-
422
- /**
423
- * Sets the authentication type for the k6 request, generating relevant headers.
424
- *
425
- * ```gherkin
426
- * When I set the authentication type to "BearerToken"
427
- * ```
428
- *
429
- * @param {string} authType - The type of authentication (e.g., "BearerToken", "BasicAuth", "APIKey").
430
- *
431
- * @example
432
- * When I set the authentication type to "BearerToken"
433
- *
434
- * @remarks
435
- * This step uses the `generateHeaders` helper to create or modify `this.config.headers`
436
- * based on the specified `authType` and environment variables/aliases.
437
- * Ensure your `generateHeaders` helper is configured to handle the `authType` and retrieve
438
- * necessary credentials (e.g., from `process.env` or `this.aliases`).
439
- * @category k6 Configuration Steps
440
- */
441
- export async function When_I_set_authentication_type(authType) {
442
- /** @type {CustomWorld} */ (this);
443
- this.config.headers = generateHeaders(
444
- authType,
445
- process.env,
446
- this.aliases || {}
447
- );
448
- this.log?.(`โš™๏ธ Authentication type set to "${authType}". Headers updated.`);
449
- }
450
- When(
451
- /^I set the authentication type to "([^"]+)"$/,
452
- When_I_set_authentication_type
453
- );
454
-
455
- /**
456
- * Stores a value from the last API response into the Cucumber World's aliases context.
457
- *
458
- * ```gherkin
459
- * Then I store the value at "data.token" as alias "authToken"
460
- * ```
461
- *
462
- * @param {string} jsonPath - A dot-separated JSON path to the value in the last response (e.g., "data.user.id").
463
- * @param {string} alias - The name of the alias to store the value under (e.g., "userId").
464
- *
465
- * @example
466
- * Then I store the value at "token" as alias "accessToken"
467
- * Then I store the value at "user.profile.email" as alias "userEmail"
468
- *
469
- * @remarks
470
- * This step expects `this.lastResponse` to contain a parsed JSON object.
471
- * It traverses the `jsonPath` to extract the desired value and saves it into
472
- * `this.aliases`. This alias can then be used in subsequent steps or payload resolutions.
473
- * @category Data Management Steps
474
- */
475
- export async function Then_I_store_value_as_alias(jsonPath, alias) {
476
- /** @type {CustomWorld} */ (this);
477
- if (!this.lastResponse) {
478
- throw new Error(
479
- "No previous API response available to extract value from. Ensure a login or request step was executed."
480
- );
481
- }
482
-
483
- const pathParts = jsonPath.split(".");
484
- let value = this.lastResponse;
485
-
486
- for (const key of pathParts) {
487
- if (value && typeof value === "object" && key in value) {
488
- value = value[key];
489
- } else {
490
- value = undefined;
491
- break;
492
- }
493
- }
494
-
495
- if (value === undefined) {
496
- throw new Error(
497
- `Could not resolve path "${jsonPath}" in the last response. Value is undefined.`
498
- );
499
- }
500
-
501
- if (!this.aliases) this.aliases = {};
502
- this.aliases[alias] = value;
503
-
504
- this.log?.(
505
- `๐Ÿงฉ Stored alias "${alias}" from response path "${jsonPath}". Value: ${JSON.stringify(
506
- value
507
- ).slice(0, 100)}...`
508
- );
509
- }
510
- Then(
511
- /^I store the value at "([^"]+)" as alias "([^"]+)"$/,
512
- Then_I_store_value_as_alias
513
- );
514
-
515
- /**
516
- * Logs in via a POST request to a specified endpoint using a JSON payload from a file.
517
- * The response data is stored for subsequent steps.
518
- *
519
- * ```gherkin
520
- * When I login via POST to "/auth/login" with payload from "admin_credentials.json"
521
- * ```
522
- *
523
- * @param {string} endpoint - The API endpoint for the login request (relative to `BASE_URL`).
524
- * @param {string} fileName - The name of the JSON file containing login credentials.
525
- *
526
- * @example
527
- * When I login via POST to "/api/login" with payload from "user_creds.json"
528
- *
529
- * @remarks
530
- * This step constructs and executes a `fetch` POST request. It reads the payload from
531
- * the specified file (resolved from `payloads` directory), resolves placeholders in the payload,
532
- * sends the request, and stores the JSON response in `this.lastResponse`.
533
- * It throws an error if the login request fails (non-2xx status).
534
- * @category Authentication Steps
535
- */
536
- export async function When_I_login_via_POST_with_payload_from_file(
537
- endpoint,
538
- fileName
539
- ) {
540
- /** @type {CustomWorld} */ (this);
541
- const payloadDir = this.parameters?.payloadPath || "payloads";
542
- const payloadPath = resolvePayloadPath(fileName, payloadDir);
543
-
544
- const rawTemplate = fs.readFileSync(payloadPath, "utf-8");
545
- const resolved = resolveBody(rawTemplate, {
546
- ...process.env,
547
- ...(this.aliases || {}),
548
- });
549
-
550
- try {
551
- const baseUrl = process.env.BASE_URL;
552
- if (!baseUrl) {
553
- throw new Error("Missing BASE_URL environment variable.");
554
- }
555
- const fullUrl = `${baseUrl.replace(/\/+$/, "")}${endpoint}`;
556
-
557
- this.log?.(
558
- `๐Ÿ” Attempting login via POST to "${fullUrl}" with payload from "${fileName}".`
559
- );
560
-
561
- const response = await fetch(fullUrl, {
562
- method: "POST",
563
- headers: {
564
- "Content-Type": "application/json",
565
- },
566
- body: JSON.stringify(resolved),
567
- });
568
-
569
- const data = await response.json();
570
-
571
- if (!response.ok) {
572
- this.log?.(
573
- `โŒ Login request failed for "${fullUrl}". Status: ${
574
- response.status
575
- }. Response body: ${JSON.stringify(data).slice(0, 100)}...`
576
- );
577
- throw new Error(
578
- `Login request failed with status ${response.status} for endpoint "${endpoint}".`
579
- );
580
- }
581
-
582
- this.lastResponse = data;
583
- this.log?.(
584
- "๐Ÿ” Login successful, response data saved to 'this.lastResponse'."
585
- );
586
- } catch (err) {
587
- const message = err instanceof Error ? err.message : String(err);
588
- this.log?.(`โŒ Login request failed: ${message}`);
589
- throw new Error(
590
- `Login request failed for endpoint "${endpoint}": ${message}`
591
- );
592
- }
593
- }
594
- When(
595
- /^I login via POST to "([^"]+)" with payload from "([^"]+)"$/,
596
- When_I_login_via_POST_with_payload_from_file
597
- );
598
-
599
- const genScriptDir = path.resolve(process.cwd(), "genScript");
600
- if (!fs.existsSync(genScriptDir)) {
601
- fs.mkdirSync(genScriptDir, { recursive: true });
602
- }
603
-
604
- const reportDir =
605
- process.env.REPORT_OUTPUT_DIR ||
606
- process.env.K6_REPORT_DIR ||
607
- process.env.npm_config_report_output_dir ||
608
- "reports";
609
-
610
- if (!fs.existsSync(reportDir)) {
611
- fs.mkdirSync(reportDir, { recursive: true });
612
- }
613
-
614
- Then(
615
- /^I see the API should handle the (\w+) request successfully$/,
616
- { timeout: 300000 },
617
- async function (method) {
618
- if (!this.config || !this.config.method) {
619
- throw new Error("Configuration is missing or incomplete.");
620
- }
621
- const expectedMethod = method.toUpperCase();
622
- const actualMethod = this.config.method.toUpperCase();
623
- if (actualMethod !== expectedMethod) {
624
- throw new Error(
625
- `Mismatched HTTP method: expected "${expectedMethod}", got "${actualMethod}"`
626
- );
627
- }
628
- try {
629
- const scriptContent = buildK6Script(this.config);
630
- const uniqueId = crypto.randomBytes(8).toString("hex");
631
- const scriptFileName = `k6-script-${uniqueId}.js`;
632
- const scriptPath = path.join(reportDir, scriptFileName);
633
- fs.writeFileSync(scriptPath, scriptContent, "utf-8");
634
- this.log?.(`โœ… k6 script generated at: "${scriptPath}"`);
635
-
636
- this.log?.(`๐Ÿš€ Running k6 script: "${scriptFileName}"...`);
637
- const { stdout, stderr, code } = await runK6Script(
638
- scriptPath,
639
- process.env.K6_CUCUMBER_OVERWRITE === "true"
640
- );
641
- if (stdout) this.log?.(`k6 STDOUT:\n${stdout}`);
642
- if (stderr) this.log?.(`k6 STDERR:\n${stderr}`);
643
-
644
- if (code !== 0) {
645
- throw new Error(
646
- `k6 process exited with code ${code}. Check k6 output for details.`
647
- );
648
- }
649
- this.log?.(
650
- `โœ… k6 script executed successfully for ${expectedMethod} request.`
651
- );
652
-
653
- const saveK6Script =
654
- process.env.saveK6Script === "true" ||
655
- process.env.SAVE_K6_SCRIPT === "true" ||
656
- this.parameters?.saveK6Script === true;
657
-
658
- if (!saveK6Script) {
659
- try {
660
- fs.unlinkSync(scriptPath);
661
- this.log?.(`๐Ÿงน Temporary k6 script deleted: "${scriptPath}"`);
662
- } catch (cleanupErr) {
663
- this.log?.(
664
- `โš ๏ธ Warning: Could not delete temporary k6 script file: "${scriptPath}". Error: ${
665
- cleanupErr instanceof Error
666
- ? cleanupErr.message
667
- : String(cleanupErr)
668
- }`
669
- );
670
- }
671
- } else {
672
- this.log?.(
673
- `โ„น๏ธ k6 script kept at: "${scriptPath}". Set SAVE_K6_SCRIPT=false to delete automatically.`
674
- );
675
- }
676
- } catch (error) {
677
- this.log?.(
678
- `โŒ Failed to generate or run k6 script: ${
679
- error instanceof Error ? error.message : String(error)
680
- }`
681
- );
682
- throw new Error(
683
- `k6 script generation or execution failed: ${
684
- error instanceof Error ? error.message : String(error)
685
- }`
686
- );
687
- }
688
- }
689
- );