k6-cucumber-steps
Version:
Cucumber step definitions for running k6 performance tests.
664 lines (611 loc) • 34.3 kB
HTML
<html lang="en" style="font-size:16px"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Source: load_test_steps.js</title><!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]--><script src="scripts/third-party/hljs.js" defer="defer"></script><script src="scripts/third-party/hljs-line-num.js" defer="defer"></script><script src="scripts/third-party/popper.js" defer="defer"></script><script src="scripts/third-party/tippy.js" defer="defer"></script><script src="scripts/third-party/tocbot.min.js"></script><script>var baseURL="/",locationPathname="";baseURL=(locationPathname=document.location.pathname).substr(0,locationPathname.lastIndexOf("/")+1)</script><link rel="stylesheet" href="styles/clean-jsdoc-theme.min.css"><svg aria-hidden="true" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none"><defs><symbol id="copy-icon" viewbox="0 0 488.3 488.3"><g><path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z"/><path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z"/></g></symbol><symbol id="search-icon" viewBox="0 0 512 512"><g><g><path d="M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474 c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323 c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848 S326.847,409.323,225.474,409.323z"/></g></g><g><g><path d="M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328 c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z"/></g></g></symbol><symbol id="font-size-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol id="add-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/></symbol><symbol id="minus-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 11h14v2H5z"/></symbol><symbol id="dark-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></symbol><symbol id="light-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol><symbol id="reset-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"/></symbol><symbol id="down-icon" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z"></path></symbol><symbol id="codepen-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16.5 13.202L13 15.535v3.596L19.197 15 16.5 13.202zM14.697 12L12 10.202 9.303 12 12 13.798 14.697 12zM20 10.869L18.303 12 20 13.131V10.87zM19.197 9L13 4.869v3.596l3.5 2.333L19.197 9zM7.5 10.798L11 8.465V4.869L4.803 9 7.5 10.798zM4.803 15L11 19.131v-3.596l-3.5-2.333L4.803 15zM4 13.131L5.697 12 4 10.869v2.262zM2 9a1 1 0 0 1 .445-.832l9-6a1 1 0 0 1 1.11 0l9 6A1 1 0 0 1 22 9v6a1 1 0 0 1-.445.832l-9 6a1 1 0 0 1-1.11 0l-9-6A1 1 0 0 1 2 15V9z"/></symbol><symbol id="close-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></symbol><symbol id="menu-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></symbol></defs></svg></head><body data-theme="dark"><div class="sidebar-container"><div class="sidebar" id="sidebar"><div class="sidebar-items-container"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-global"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#CustomWorld">CustomWorld</a></div><div class="sidebar-section-children"><a href="global.html#Given_I_set_k6_script_for_method_testing">Given_I_set_k6_script_for_method_testing</a></div><div class="sidebar-section-children"><a href="global.html#K6Config">K6Config</a></div><div class="sidebar-section-children"><a href="global.html#Then_I_store_value_as_alias">Then_I_store_value_as_alias</a></div><div class="sidebar-section-children"><a href="global.html#When_I_login_via_POST_with_payload_from_file">When_I_login_via_POST_with_payload_from_file</a></div><div class="sidebar-section-children"><a href="global.html#When_I_set_authentication_type">When_I_set_authentication_type</a></div><div class="sidebar-section-children"><a href="global.html#When_I_set_endpoints_used">When_I_set_endpoints_used</a></div><div class="sidebar-section-children"><a href="global.html#When_I_set_k6_script_configurations">When_I_set_k6_script_configurations</a></div><div class="sidebar-section-children"><a href="global.html#When_I_set_method_body_for_endpoint">When_I_set_method_body_for_endpoint</a></div><div class="sidebar-section-children"><a href="global.html#When_I_set_request_headers">When_I_set_request_headers</a></div><div class="sidebar-section-children"><a href="global.html#When_I_use_JSON_payload_from_file_for_method_to_endpoint">When_I_use_JSON_payload_from_file_for_method_to_endpoint</a></div></div></div></div></div><div class="navbar-container" id="VuAckcnZhf"><nav class="navbar"><div class="navbar-left-items"></div><div class="navbar-right-items"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#light-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div><nav></nav></nav></div><div class="toc-container"><div class="toc-content"><span class="bold">On this page</span><div id="eed4d2a0bfd64539bb9df78095dec881"></div></div></div><div class="body-wrapper"><div class="main-content"><div class="main-wrapper"><section id="source-page" class="source-page"><header><h1 id="title" class="has-anchor">load_test_steps.js</h1></header><article><pre class="prettyprint source lang-js"><code>// e2e/step_definitions/load_test_steps.js
import { Given, When, Then } from "@cucumber/cucumber";
import fs from "fs";
import path from "path";
import crypto from "crypto";
import * as dotenv from "dotenv";
import resolveBody from "../lib/helpers/resolveBody.js";
import buildK6Script from "../lib/helpers/buildK6Script.js";
import generateHeaders from "../lib/helpers/generateHeaders.js";
import { runK6Script } from "../lib/utils/k6Runner.js";
dotenv.config();
/**
* @typedef {Object} CustomWorld
* @property {Object} config
* @property {Object} aliases
* @property {Object} lastResponse
* @property {Object} parameters
* @property {Function} log
*/
/**
* @typedef {Object} K6Config
* @property {string} method - HTTP method for the request (e.g., "GET", "POST").
* @property {string} [endpoint] - The specific endpoint for a single request.
* @property {string[]} [endpoints] - An array of endpoints for multiple requests.
* @property {Object} [headers] - Request headers.
* @property {string} [body] - Request body content.
* @property {Object} options - k6 test options (vus, duration, stages, thresholds).
* @property {Object} options.thresholds - k6 metric thresholds.
* @property {string[]} options.thresholds.http_req_failed - Thresholds for failed HTTP requests.
* @property {string[]} options.thresholds.http_req_duration - Thresholds for request duration.
* @property {string[]} [options.thresholds.error_rate] - Optional threshold for error rate.
* @property {number} [options.vus] - Number of virtual users (for open model).
* @property {string} [options.duration] - Test duration (for open model, e.g., "30s").
* @property {Array<Object>} [options.stages] - Array of stages for a stepped load model.
*/
// ===================================================================================
// K6 SCRIPT CONFIGURATION STEPS
// ===================================================================================
/**
* Initializes the k6 script configuration by setting the primary HTTP method for the load test.
*
* ```gherkin
* Given I set a k6 script for {word} testing
* ```
*
* @param {string} method - The HTTP method (e.g., "GET", "POST", "PUT", "DELETE").
* @example
* Given I set a k6 script for GET testing
* Given I set a k6 script for POST testing
* @remarks
* This step typically starts the definition of a k6 load test scenario.
* It sets `this.config.method` in the Cucumber World context.
* Subsequent steps will build upon this configuration.
* @category k6 Configuration Steps
*/
export async function Given_I_set_k6_script_for_method_testing(method) {
/** @type {CustomWorld} */ (this).config = { method: method.toUpperCase() };
/** @type {CustomWorld} */ (this).log?.(
`⚙️ Initialized k6 script for ${method.toUpperCase()} testing.`
);
}
Given(
/^I set a k6 script for (\w+) testing$/,
Given_I_set_k6_script_for_method_testing
);
/**
* Configures the k6 script options (VUs, duration, stages, thresholds) from a data table.
*
* ```gherkin
* When I set to run the k6 script with the following configurations:
* | virtual_users | duration | stages | http_req_failed | http_req_duration | error_rate |
* | 10 | 30 | | p(99)<0.01 | p(99)<500 | rate<0.01 |
* | | | [{"duration":"10s","target":10}] | p(90)<0.01 | p(90)<200 | rate<0.001 |
* ```
*
* @param {DataTable} dataTable - A Cucumber data table containing k6 configuration parameters.
* Expected columns: `virtual_users`, `duration`, `stages` (JSON string), `http_req_failed`, `http_req_duration`, `error_rate`.
* @example
* When I set to run the k6 script with the following configurations:
* | virtual_users | duration | http_req_failed | http_req_duration |
* | 50 | 60 | p(99)<0.01 | p(99)<1000 |
* When I set to run the k6 script with the following configurations:
* | stages | http_req_failed | http_req_duration | error_rate |
* | [{"duration":"10s","target":10}, {"duration":"20s","target":50}] | p(99)<0.01 | p(99)<500 | rate<0.01 |
* @remarks
* This step populates `this.config.options`. It intelligently handles either a simple
* `virtual_users`/`duration` model or a complex `stages` array. Threshold formats are validated.
* Example values from scenario outlines are resolved if present.
* @category k6 Configuration Steps
*/
export async function When_I_set_k6_script_configurations(dataTable) {
/** @type {CustomWorld} */ (this);
const rawRow = dataTable.hashes()[0];
const row = {};
const exampleMap = {};
if (this.pickle && this.pickle.astNodeIds && this.gherkinDocument) {
const scenarioNodeId = this.pickle.astNodeIds.find((id) =>
id.startsWith("Scenario")
);
if (scenarioNodeId) {
const scenario = this.gherkinDocument.feature.children.find(
(child) => child.scenario && child.scenario.id === scenarioNodeId
)?.scenario;
if (scenario && scenario.examples && scenario.examples.length > 0) {
const exampleTable = scenario.examples[0].tableBody?.[0];
const headerCells = scenario.examples[0].tableHeader?.cells || [];
const dataCells = exampleTable?.cells || [];
headerCells.forEach((cell, idx) => {
exampleMap[cell.value] = dataCells[idx]?.value;
});
}
}
}
for (const [key, value] of Object.entries(rawRow)) {
row[key] = value.replace(/<([^>]+)>/g, (_, param) => {
if (exampleMap.hasOwnProperty(param)) {
return exampleMap[param];
}
return `<${param}>`;
});
}
const validateThreshold = (value, thresholdName) => {
const regex = /^[\w{}()<>:]+([<>=]=?)\d+(\.\d+)?$/;
if (value && !regex.test(value)) {
throw new Error(
`Invalid k6 threshold format for '${thresholdName}': "${value}". Expected format like 'p(99)<500' or 'rate<0.01'.`
);
}
};
validateThreshold(row.http_req_failed, "http_req_failed");
validateThreshold(row.http_req_duration, "http_req_duration");
if (row.error_rate) {
validateThreshold(row.error_rate, "error_rate");
}
let k6Options;
if (row.stages) {
try {
k6Options = {
stages: JSON.parse(row.stages),
thresholds: {
http_req_failed: [row.http_req_failed],
http_req_duration: [row.http_req_duration],
},
};
} catch (e) {
throw new Error(`Invalid 'stages' JSON format: ${e.message}`);
}
} else if (row.virtual_users && row.duration) {
k6Options = {
vus: parseInt(row.virtual_users),
duration: `${row.duration}s`,
thresholds: {
http_req_failed: [row.http_req_failed],
http_req_duration: [row.http_req_duration],
},
};
} else {
throw new Error(
"k6 configuration requires either 'stages' or 'virtual_users' and 'duration' to be set."
);
}
if (row.error_rate) {
k6Options.thresholds.error_rate = [row.error_rate];
}
this.config.options = k6Options;
this.log?.(
`⚙️ k6 script configured with options: ${JSON.stringify(k6Options)}`
);
}
When(
/^I set to run the k6 script with the following configurations:$/,
When_I_set_k6_script_configurations
);
/**
* Sets request headers for the k6 script. Headers are merged with any existing headers.
*
* ```gherkin
* When I set the request headers:
* | Header | Value |
* | Content-Type | application/json |
* | Authorization | Bearer <my_token> |
* ```
*
* @param {DataTable} dataTable - A Cucumber data table with 'Header' and 'Value' columns.
*
* @example
* When I set the request headers:
* | Header | Value |
* | Content-Type | application/json |
* | X-Custom-Header| MyValue |
*
* @remarks
* This step updates `this.config.headers`. Values can include placeholders
* (e.g., `<my_token>`) if your `resolveBody` or `generateHeaders` helpers handle them.
* Note: `generateHeaders` is specifically used for authentication types. If your headers
* contain dynamic values beyond simple alias resolution, ensure your helpers support it.
* @category k6 Configuration Steps
*/
export async function When_I_set_request_headers(dataTable) {
/** @type {CustomWorld} */ (this);
const headers = {};
dataTable.hashes().forEach(({ Header, Value }) => {
headers[Header] = Value;
});
this.config.headers = {
...(this.config.headers || {}),
...headers,
};
this.log?.(`⚙️ Request headers set: ${JSON.stringify(this.config.headers)}`);
}
When(/^I set the request headers:$/, When_I_set_request_headers);
/**
* Sets the list of endpoints to be used in the k6 script. These are typically used when
* the k6 script iterates over multiple URLs.
*
* ```gherkin
* When I set the following endpoints used:
* /api/v1/users
* /api/v1/products
* /api/v1/orders
* ```
*
* @param {string} docString - A DocString containing a newline-separated list of endpoints.
*
* @example
* When I set the following endpoints used:
* /health
* /status
* /metrics
*
* @remarks
* This step populates `this.config.endpoints` as an array of strings.
* Ensure these endpoints are relative to your k6 `BASE_URL`.
* @category k6 Configuration Steps
*/
export async function When_I_set_endpoints_used(docString) {
/** @type {CustomWorld} */ (this);
this.config.endpoints = docString
.trim()
.split("\n")
.map((line) => line.trim());
if (this.log)
this.log(`⚙️ Endpoints set: ${JSON.stringify(this.config.endpoints)}`);
}
When(/^I set the following endpoints used:$/, When_I_set_endpoints_used);
/**
* Sets the request body for a specific HTTP method and endpoint.
*
* ```gherkin
* When I set the following POST body is used for "/api/v1/create"
* { "name": "test", "email": "test@example.com" }
* ```
*
* @param {string} method - The HTTP method (e.g., "POST", "PUT").
* @param {string} endpoint - The specific endpoint URL for this body.
* @param {string} docString - A DocString containing the request body content (e.g., JSON).
*
* @example
* When I set the following PUT body is used for "/api/v1/update/1"
* { "status": "active" }
*
* @remarks
* This step sets `this.config.method`, `this.config.endpoint`, and `this.config.body`.
* The `resolveBody` helper is used to process the DocString, allowing for dynamic values
* from environment variables.
* @category k6 Configuration Steps
*/
export async function When_I_set_method_body_for_endpoint(
method,
endpoint,
docString
) {
/** @type {CustomWorld} */ (this);
this.config.method = method.toUpperCase();
this.config.endpoint = endpoint;
this.config.body = resolveBody(docString, process.env);
this.log?.(
`⚙️ Body set for ${this.config.method} to "${
this.config.endpoint
}". Body preview: ${this.config.body.slice(0, 100)}...`
);
}
When(
/^I set the following (\w+) body is used for "([^"]+)"$/,
When_I_set_method_body_for_endpoint
);
/**
* Loads a JSON payload from a file to be used as the request body for a specific
* method and endpoint in the k6 script.
*
* ```gherkin
* When I use JSON payload from "user_create.json" for POST to "/api/v1/users"
* ```
*
* @param {string} fileName - The name of the JSON payload file (e.g., "user_data.json").
* @param {string} method - The HTTP method (only "POST", "PUT", "PATCH" are supported for bodies).
* @param {string} endpoint - The specific endpoint URL.
*
* @example
* When I use JSON payload from "login_payload.json" for POST to "/auth/login"
*
* @remarks
* This step reads the JSON file, resolves any placeholders within it (using `resolveBody`),
* and sets `this.config.method`, `this.config.endpoint`, and `this.config.body`.
* It also stores `lastRequest` in `this.lastRequest`.
* The payload file path is resolved relative to `payloads` directory or `this.parameters.payloadPath`.
* @category k6 Configuration Steps
*/
export async function When_I_use_JSON_payload_from_file_for_method_to_endpoint(
fileName,
method,
endpoint
) {
/** @type {CustomWorld} */ (this);
const allowedMethods = ["POST", "PUT", "PATCH"];
const methodUpper = method.toUpperCase();
if (!allowedMethods.includes(methodUpper)) {
throw new Error(
`Method "${method}" is not supported for JSON payloads from files. Use one of: ${allowedMethods.join(
", "
)}`
);
}
const projectRoot = path.resolve(__dirname, "..", "..");
const payloadDir = this.parameters?.payloadPath || "payloads";
const payloadPath = path.isAbsolute(payloadDir)
? path.join(payloadDir, fileName)
: path.join(projectRoot, payloadDir, fileName);
if (!fs.existsSync(payloadPath)) {
throw new Error(`Payload file not found: "${payloadPath}"`);
}
const rawTemplate = fs.readFileSync(payloadPath, "utf-8");
const resolved = resolveBody(rawTemplate, {
...process.env,
...(this.aliases || {}),
});
this.config = {
...(this.config || {}),
method: methodUpper,
endpoint,
body: resolved,
headers: this.config?.headers || {},
};
this.lastRequest = {
method: methodUpper,
endpoint,
body: resolved,
};
this.log?.(
`⚙️ JSON payload from "${fileName}" used for ${methodUpper} to "${endpoint}".`
);
}
When(
/^I use JSON payload from "([^"]+)" for (\w+) to "([^"]+)"$/,
When_I_use_JSON_payload_from_file_for_method_to_endpoint
);
/**
* Sets the authentication type for the k6 request, generating relevant headers.
*
* ```gherkin
* When I set the authentication type to "BearerToken"
* ```
*
* @param {string} authType - The type of authentication (e.g., "BearerToken", "BasicAuth", "APIKey").
*
* @example
* When I set the authentication type to "BearerToken"
*
* @remarks
* This step uses the `generateHeaders` helper to create or modify `this.config.headers`
* based on the specified `authType` and environment variables/aliases.
* Ensure your `generateHeaders` helper is configured to handle the `authType` and retrieve
* necessary credentials (e.g., from `process.env` or `this.aliases`).
* @category k6 Configuration Steps
*/
export async function When_I_set_authentication_type(authType) {
/** @type {CustomWorld} */ (this);
this.config.headers = generateHeaders(
authType,
process.env,
this.aliases || {}
);
this.log?.(`⚙️ Authentication type set to "${authType}". Headers updated.`);
}
When(
/^I set the authentication type to "([^"]+)"$/,
When_I_set_authentication_type
);
/**
* Stores a value from the last API response into the Cucumber World's aliases context.
*
* ```gherkin
* Then I store the value at "data.token" as alias "authToken"
* ```
*
* @param {string} jsonPath - A dot-separated JSON path to the value in the last response (e.g., "data.user.id").
* @param {string} alias - The name of the alias to store the value under (e.g., "userId").
*
* @example
* Then I store the value at "token" as alias "accessToken"
* Then I store the value at "user.profile.email" as alias "userEmail"
*
* @remarks
* This step expects `this.lastResponse` to contain a parsed JSON object.
* It traverses the `jsonPath` to extract the desired value and saves it into
* `this.aliases`. This alias can then be used in subsequent steps or payload resolutions.
* @category Data Management Steps
*/
export async function Then_I_store_value_as_alias(jsonPath, alias) {
/** @type {CustomWorld} */ (this);
if (!this.lastResponse) {
throw new Error(
"No previous API response available to extract value from. Ensure a login or request step was executed."
);
}
const pathParts = jsonPath.split(".");
let value = this.lastResponse;
for (const key of pathParts) {
if (value && typeof value === "object" && key in value) {
value = value[key];
} else {
value = undefined;
break;
}
}
if (value === undefined) {
throw new Error(
`Could not resolve path "${jsonPath}" in the last response. Value is undefined.`
);
}
if (!this.aliases) this.aliases = {};
this.aliases[alias] = value;
this.log?.(
`🧩 Stored alias "${alias}" from response path "${jsonPath}". Value: ${JSON.stringify(
value
).slice(0, 100)}...`
);
}
Then(
/^I store the value at "([^"]+)" as alias "([^"]+)"$/,
Then_I_store_value_as_alias
);
/**
* Logs in via a POST request to a specified endpoint using a JSON payload from a file.
* The response data is stored for subsequent steps.
*
* ```gherkin
* When I login via POST to "/auth/login" with payload from "admin_credentials.json"
* ```
*
* @param {string} endpoint - The API endpoint for the login request (relative to `BASE_URL`).
* @param {string} fileName - The name of the JSON file containing login credentials.
*
* @example
* When I login via POST to "/api/login" with payload from "user_creds.json"
*
* @remarks
* This step constructs and executes a `fetch` POST request. It reads the payload from
* the specified file (resolved from `payloads` directory), resolves placeholders in the payload,
* sends the request, and stores the JSON response in `this.lastResponse`.
* It throws an error if the login request fails (non-2xx status).
* @category Authentication Steps
*/
export async function When_I_login_via_POST_with_payload_from_file(
endpoint,
fileName
) {
/** @type {CustomWorld} */ (this);
const payloadDir = this.parameters?.payloadPath || "payloads";
const projectRoot = path.resolve(__dirname, "..", "..");
const payloadPath = path.isAbsolute(payloadDir)
? path.join(payloadDir, fileName)
: path.join(projectRoot, payloadDir, fileName);
if (!fs.existsSync(payloadPath)) {
throw new Error(`Payload file not found: "${payloadPath}"`);
}
const rawTemplate = fs.readFileSync(payloadPath, "utf-8");
const resolved = resolveBody(rawTemplate, {
...process.env,
...(this.aliases || {}),
});
try {
const baseUrl = process.env.BASE_URL;
if (!baseUrl) {
throw new Error("Missing BASE_URL environment variable.");
}
const fullUrl = `${baseUrl.replace(/\/+$/, "")}${endpoint}`;
this.log?.(
`🔐 Attempting login via POST to "${fullUrl}" with payload from "${fileName}".`
);
const response = await fetch(fullUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(resolved),
});
const data = await response.json();
if (!response.ok) {
this.log?.(
`❌ Login request failed for "${fullUrl}". Status: ${
response.status
}. Response body: ${JSON.stringify(data).slice(0, 100)}...`
);
throw new Error(
`Login request failed with status ${response.status} for endpoint "${endpoint}".`
);
}
this.lastResponse = data;
this.log?.(
"🔐 Login successful, response data saved to 'this.lastResponse'."
);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.log?.(`❌ Login request failed: ${message}`);
throw new Error(
`Login request failed for endpoint "${endpoint}": ${message}`
);
}
}
When(
/^I login via POST to "([^"]+)" with payload from "([^"]+)"$/,
When_I_login_via_POST_with_payload_from_file
);
const genScriptDir = path.resolve(process.cwd(), "genScript");
if (!fs.existsSync(genScriptDir)) {
fs.mkdirSync(genScriptDir, { recursive: true });
}
const reportDir =
process.env.REPORT_OUTPUT_DIR ||
process.env.K6_REPORT_DIR ||
process.env.npm_config_report_output_dir ||
"reports";
if (!fs.existsSync(reportDir)) {
fs.mkdirSync(reportDir, { recursive: true });
}
Then(
/^I see the API should handle the (\w+) request successfully$/,
{ timeout: 300000 },
async function (method) {
if (!this.config || !this.config.method) {
throw new Error("Configuration is missing or incomplete.");
}
const expectedMethod = method.toUpperCase();
const actualMethod = this.config.method.toUpperCase();
if (actualMethod !== expectedMethod) {
throw new Error(
`Mismatched HTTP method: expected "${expectedMethod}", got "${actualMethod}"`
);
}
try {
const scriptContent = buildK6Script(this.config);
const uniqueId = crypto.randomBytes(8).toString("hex");
const scriptFileName = `k6-script-${uniqueId}.js`;
const scriptPath = path.join(reportDir, scriptFileName);
fs.writeFileSync(scriptPath, scriptContent, "utf-8");
this.log?.(`✅ k6 script generated at: "${scriptPath}"`);
this.log?.(`🚀 Running k6 script: "${scriptFileName}"...`);
const { stdout, stderr, code } = await runK6Script(
scriptPath,
process.env.K6_CUCUMBER_OVERWRITE === "true"
);
if (stdout) this.log?.(`k6 STDOUT:\n${stdout}`);
if (stderr) this.log?.(`k6 STDERR:\n${stderr}`);
if (code !== 0) {
throw new Error(
`k6 process exited with code ${code}. Check k6 output for details.`
);
}
this.log?.(
`✅ k6 script executed successfully for ${expectedMethod} request.`
);
const saveK6Script =
process.env.saveK6Script === "true" ||
process.env.SAVE_K6_SCRIPT === "true" ||
this.parameters?.saveK6Script === true;
if (!saveK6Script) {
try {
fs.unlinkSync(scriptPath);
this.log?.(`🧹 Temporary k6 script deleted: "${scriptPath}"`);
} catch (cleanupErr) {
this.log?.(
`⚠️ Warning: Could not delete temporary k6 script file: "${scriptPath}". Error: ${
cleanupErr instanceof Error
? cleanupErr.message
: String(cleanupErr)
}`
);
}
} else {
this.log?.(
`ℹ️ k6 script kept at: "${scriptPath}". Set SAVE_K6_SCRIPT=false to delete automatically.`
);
}
} catch (error) {
this.log?.(
`❌ Failed to generate or run k6 script: ${
error instanceof Error ? error.message : String(error)
}`
);
throw new Error(
`k6 script generation or execution failed: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
);
</code></pre></article></section></div></div></div><div class="search-container" id="PkfLWpAbet" style="display:none"><div class="wrapper" id="iCxFxjkHbP"><button class="icon-button search-close-button" id="VjLlGakifb" aria-label="close search"><svg><use xlink:href="#close-icon"></use></svg></button><div class="search-box-c"><svg><use xlink:href="#search-icon"></use></svg> <input type="text" id="vpcKVYIppa" class="search-input" placeholder="Search..." autofocus></div><div class="search-result-c" id="fWwVHRuDuN"><span class="search-result-c-text">Type anything to view search result</span></div></div></div><div class="mobile-menu-icon-container"><button class="icon-button" id="mobile-menu" data-isopen="false" aria-label="menu"><svg><use xlink:href="#menu-icon"></use></svg></button></div><div id="mobile-sidebar" class="mobile-sidebar-container"><div class="mobile-sidebar-wrapper"><div class="mobile-nav-links"></div><div class="mobile-sidebar-items-c"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-global"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#CustomWorld">CustomWorld</a></div><div class="sidebar-section-children"><a href="global.html#Given_I_set_k6_script_for_method_testing">Given_I_set_k6_script_for_method_testing</a></div><div class="sidebar-section-children"><a href="global.html#K6Config">K6Config</a></div><div class="sidebar-section-children"><a href="global.html#Then_I_store_value_as_alias">Then_I_store_value_as_alias</a></div><div class="sidebar-section-children"><a href="global.html#When_I_login_via_POST_with_payload_from_file">When_I_login_via_POST_with_payload_from_file</a></div><div class="sidebar-section-children"><a href="global.html#When_I_set_authentication_type">When_I_set_authentication_type</a></div><div class="sidebar-section-children"><a href="global.html#When_I_set_endpoints_used">When_I_set_endpoints_used</a></div><div class="sidebar-section-children"><a href="global.html#When_I_set_k6_script_configurations">When_I_set_k6_script_configurations</a></div><div class="sidebar-section-children"><a href="global.html#When_I_set_method_body_for_endpoint">When_I_set_method_body_for_endpoint</a></div><div class="sidebar-section-children"><a href="global.html#When_I_set_request_headers">When_I_set_request_headers</a></div><div class="sidebar-section-children"><a href="global.html#When_I_use_JSON_payload_from_file_for_method_to_endpoint">When_I_use_JSON_payload_from_file_for_method_to_endpoint</a></div></div></div><div class="mobile-navbar-actions"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#light-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div></div></div><script type="text/javascript" src="scripts/core.min.js"></script><script src="scripts/search.min.js" defer="defer"></script><script src="scripts/third-party/fuse.js" defer="defer"></script><script type="text/javascript">var tocbotInstance=tocbot.init({tocSelector:"#eed4d2a0bfd64539bb9df78095dec881",contentSelector:".main-content",headingSelector:"h1, h2, h3",hasInnerContainers:!0,scrollContainer:".main-content",headingsOffset:130,onClick:bringLinkToView})</script></body></html>