k6-cucumber-steps 1.2.1 â 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
module.exports = function buildK6Script(config) {
|
|
2
2
|
const { method, endpoints, endpoint, body, headers, options, baseUrl } = config;
|
|
3
3
|
|
|
4
|
-
// Prefer baseUrl from config (set by step/world), then env
|
|
5
4
|
const BASE_URL =
|
|
6
5
|
baseUrl ||
|
|
7
6
|
(config.worldParameters && config.worldParameters.baseUrl) ||
|
|
@@ -18,15 +17,19 @@ module.exports = function buildK6Script(config) {
|
|
|
18
17
|
return url.startsWith("/") ? `${BASE_URL}${url}` : url;
|
|
19
18
|
};
|
|
20
19
|
|
|
21
|
-
// Default headers/body/options for safety
|
|
22
20
|
const safeHeaders = headers && Object.keys(headers).length ? headers : {};
|
|
23
21
|
const safeBody = body !== undefined ? body : null;
|
|
24
22
|
const safeOptions = options && Object.keys(options).length ? options : {};
|
|
25
23
|
|
|
26
|
-
// Stringify for script
|
|
27
24
|
const stringifiedHeaders = JSON.stringify(safeHeaders, null, 2);
|
|
28
25
|
const stringifiedBody = JSON.stringify(safeBody, null, 2);
|
|
29
26
|
|
|
27
|
+
const reportDir =
|
|
28
|
+
process.env.REPORT_OUTPUT_DIR ||
|
|
29
|
+
process.env.K6_REPORT_DIR ||
|
|
30
|
+
process.env.npm_config_report_output_dir ||
|
|
31
|
+
"reports";
|
|
32
|
+
|
|
30
33
|
return `
|
|
31
34
|
import http from 'k6/http';
|
|
32
35
|
import { check } from 'k6';
|
|
@@ -50,7 +53,6 @@ export default function () {
|
|
|
50
53
|
? "null"
|
|
51
54
|
: "JSON.stringify(body)"
|
|
52
55
|
}, { headers });
|
|
53
|
-
console.log(\`Response Body for \${resolvedUrl${i}}: \${res${i}.body}\`);
|
|
54
56
|
check(res${i}, {
|
|
55
57
|
"status is 2xx": (r) => r.status >= 200 && r.status < 300
|
|
56
58
|
});
|
|
@@ -64,7 +66,6 @@ export default function () {
|
|
|
64
66
|
? "null"
|
|
65
67
|
: "JSON.stringify(body)"
|
|
66
68
|
}, { headers });
|
|
67
|
-
console.log(\`Response Body for \${resolvedUrl}: \${res.body}\`);
|
|
68
69
|
check(res, {
|
|
69
70
|
"status is 2xx": (r) => r.status >= 200 && r.status < 300
|
|
70
71
|
});
|
|
@@ -73,24 +74,8 @@ export default function () {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
export function handleSummary(data) {
|
|
76
|
-
const outputDir = __ENV.REPORT_OUTPUT_DIR || "reports";
|
|
77
|
-
const html = htmlReport(data);
|
|
78
|
-
const cucumberReportFile = "cucumber-report.html";
|
|
79
|
-
const cucumberLink = \`
|
|
80
|
-
<div style="text-align:center; margin-top:20px;">
|
|
81
|
-
<a href="\${cucumberReportFile}" style="font-size:18px; color:#0066cc;">
|
|
82
|
-
đ View Cucumber Test Report
|
|
83
|
-
</a>
|
|
84
|
-
</div>
|
|
85
|
-
\`;
|
|
86
|
-
|
|
87
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
88
|
-
const k6ReportFilename = \`\${outputDir}/k6-report-\${timestamp}.html\`;
|
|
89
|
-
|
|
90
|
-
console.log(\`đ K6 HTML report \${k6ReportFilename} generated successfully đ\`);
|
|
91
|
-
|
|
92
77
|
return {
|
|
93
|
-
|
|
78
|
+
"${reportDir}/k6-report.html": htmlReport(data),
|
|
94
79
|
stdout: textSummary(data, { indent: " ", enableColors: true }),
|
|
95
80
|
};
|
|
96
81
|
}
|
package/package.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
158
|
+
if (!fs.existsSync(payloadPath)) {
|
|
159
|
+
throw new Error(`Payload file not found: ${payloadPath}`);
|
|
160
|
+
}
|
|
224
161
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
162
|
+
const rawTemplate = fs.readFileSync(payloadPath, "utf-8");
|
|
163
|
+
const resolved = resolveBody(rawTemplate, {
|
|
164
|
+
...process.env,
|
|
165
|
+
...(this.aliases || {}),
|
|
166
|
+
});
|
|
230
167
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
168
|
+
this.config = {
|
|
169
|
+
...this.config,
|
|
170
|
+
method: methodUpper,
|
|
171
|
+
endpoint,
|
|
172
|
+
body: resolved,
|
|
173
|
+
headers: this.config?.headers || {},
|
|
174
|
+
};
|
|
238
175
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
285
|
-
|
|
205
|
+
const pathParts = jsonPath.split(".");
|
|
206
|
+
let value = this.lastResponse;
|
|
286
207
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
208
|
+
for (const key of pathParts) {
|
|
209
|
+
value = value?.[key];
|
|
210
|
+
if (value === undefined) break;
|
|
211
|
+
}
|
|
291
212
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
213
|
+
if (value === undefined) {
|
|
214
|
+
throw new Error(`Could not resolve path "${jsonPath}" in the response`);
|
|
215
|
+
}
|
|
295
216
|
|
|
296
|
-
|
|
297
|
-
|
|
217
|
+
if (!this.aliases) this.aliases = {};
|
|
218
|
+
this.aliases[alias] = value;
|
|
298
219
|
|
|
299
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
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,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 };
|