k6-cucumber-steps 1.0.40 โ†’ 1.1.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.
@@ -8,5 +8,6 @@
8
8
  "cucumber.glue": [
9
9
  "step_definitions/*.js",
10
10
  "src/features/stepDefinitions/*.js"
11
- ]
11
+ ],
12
+ "liveServer.settings.port": 5501
12
13
  }
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
-
2
+ //bin/k6-cucumber-steps.js
3
3
  const path = require("path");
4
4
  const fs = require("fs");
5
5
  const { spawn } = require("child_process");
6
6
  const yargs = require("yargs");
7
7
  require("dotenv").config();
8
+ const { generateHtmlReports } = require("../scripts/generateHtmlReports");
9
+ const { linkReports } = require("../scripts/linkReports");
8
10
 
9
11
  console.log(`
10
12
  -----------------------------------------
@@ -95,9 +97,32 @@ if (configOptions.require && Array.isArray(configOptions.require)) {
95
97
 
96
98
  // Determine base report name
97
99
  const reportsDir = path.join(process.cwd(), "reports");
100
+ // Clean and prepare reports directory
101
+ const cleanReportsDir = () => {
102
+ if (fs.existsSync(reportsDir)) {
103
+ try {
104
+ fs.rmSync(reportsDir, { recursive: true, force: true });
105
+ console.log("๐Ÿงน Cleaned existing reports directory.");
106
+ } catch (err) {
107
+ console.error("โŒ Failed to clean reports directory:", err.message);
108
+ process.exit(1);
109
+ }
110
+ }
111
+
112
+ try {
113
+ fs.mkdirSync(reportsDir, { recursive: true });
114
+ console.log("๐Ÿ“ Created fresh reports directory.");
115
+ } catch (err) {
116
+ console.error("โŒ Failed to create reports directory:", err.message);
117
+ process.exit(1);
118
+ }
119
+ };
120
+
121
+ cleanReportsDir();
122
+
98
123
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
99
124
 
100
- let baseReportName = "load-results";
125
+ let baseReportName = "load-report";
101
126
  if (featureFiles.length === 1) {
102
127
  const nameFromFeature = path.basename(featureFiles[0], ".feature");
103
128
  baseReportName = nameFromFeature || baseReportName;
@@ -145,12 +170,30 @@ const cucumberProcess = spawn("npx", cucumberArgs, {
145
170
  });
146
171
 
147
172
  cucumberProcess.on("close", (code) => {
148
- console.log(`\n-----------------------------------------`);
149
173
  if (code === 0) {
174
+ console.log("-----------------------------------------");
150
175
  console.log("โœ… k6-cucumber-steps execution completed successfully.");
176
+
177
+ generateHtmlReports(); // Cucumber HTML
178
+ console.log(
179
+ "๐Ÿš€ Cucumber HTML report reports/cucumber-report.html generated successfully ๐Ÿ‘"
180
+ );
181
+
182
+ const k6ReportPath = detectMostRecentK6Report(); // Add this helper function
183
+ if (k6ReportPath) {
184
+ console.log(
185
+ `๐Ÿ“„ K6 HTML report ${k6ReportPath} generated successfully ๐Ÿ‘`
186
+ );
187
+ }
188
+
189
+ linkReports(); // Link both reports
190
+
191
+ console.log("-----------------------------------------");
151
192
  } else {
193
+ console.error("-----------------------------------------");
152
194
  console.error("โŒ k6-cucumber-steps execution failed.");
195
+ console.error("-----------------------------------------");
153
196
  }
154
- console.log(`-----------------------------------------\n`);
197
+
155
198
  process.exit(code);
156
199
  });
package/cucumber.js CHANGED
@@ -1,15 +1,15 @@
1
1
  module.exports = {
2
2
  default: {
3
- require: ["./step_definitions/**/*.js"],
3
+ // require: ["./step_definitions/**/*.js"],
4
4
  format: [
5
5
  "summary",
6
- "json:./src/examples/reports/load-report.json",
7
- "html:./src/examples/reports/report.html",
6
+ "json:./reports/load-report.json",
7
+ "html:./reports/cucumber-report.html",
8
8
  ],
9
- paths: ["./src/examples/features/loadTestTemplate.feature"],
10
- tags: process.env.TAGS,
9
+ paths: ["./features/bsp.feature"],
10
+ tags: "@load",
11
11
  worldParameters: {
12
- payloadPath: "apps/qa/performance/payloads",
12
+ payloadPath: "src/examples/payloads",
13
13
  },
14
14
  overwrite: true,
15
15
  reporter: true,
@@ -1,14 +1,12 @@
1
1
  module.exports = function buildK6Script(config) {
2
2
  const { method, endpoints, endpoint, body, headers, options } = config;
3
3
 
4
- // Ensure at least one of `endpoints` or `endpoint` is defined
5
4
  if (!endpoints?.length && !endpoint) {
6
5
  throw new Error(
7
6
  'Either "endpoints" or "endpoint" must be defined in the configuration.'
8
7
  );
9
8
  }
10
9
 
11
- // Prefer API_BASE_URL, fallback to BASE_URL
12
10
  const BASE_URL = process.env.API_BASE_URL || process.env.BASE_URL;
13
11
  if (!BASE_URL) {
14
12
  throw new Error(
@@ -16,53 +14,66 @@ module.exports = function buildK6Script(config) {
16
14
  );
17
15
  }
18
16
 
19
- // Resolve relative endpoints by prepending BASE_URL
20
17
  const resolveEndpoint = (url) => {
21
18
  return url.startsWith("/") ? `${BASE_URL}${url}` : url;
22
19
  };
23
20
 
21
+ const stringifiedHeaders = JSON.stringify(headers, null, 2);
22
+ const stringifiedBody = JSON.stringify(body, null, 2);
23
+
24
24
  return `
25
25
  import http from 'k6/http';
26
26
  import { check } from 'k6';
27
+ import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js";
28
+ import { textSummary } from "https://jslib.k6.io/k6-summary/0.0.1/index.js";
27
29
 
28
30
  export const options = ${JSON.stringify(options, null, 2)};
29
31
 
30
32
  export default function () {
33
+ const headers = ${stringifiedHeaders};
34
+ const body = ${stringifiedBody};
35
+
31
36
  ${
32
37
  endpoints?.length
33
38
  ? endpoints
34
39
  .map(
35
40
  (url, i) => `
36
- const resolvedUrl${i} = "${resolveEndpoint(url)}";
37
- const res${i} = http.request("${method}", resolvedUrl${i}, ${
41
+ const resolvedUrl${i} = "${resolveEndpoint(url)}";
42
+ const res${i} = http.request("${method}", resolvedUrl${i}, ${
38
43
  method === "GET" || method === "DELETE"
39
44
  ? "null"
40
- : JSON.stringify(body)
41
- }, {
42
- headers: ${JSON.stringify(headers, null, 2)}
43
- });
44
- console.log(\`Response Body for \${resolvedUrl${i}}: \${res${i}.body}\`);
45
- check(res${i}, {
46
- "status is 2xx": (r) => r.status >= 200 && r.status < 300
47
- });
45
+ : "JSON.stringify(body)"
46
+ }, { headers });
47
+ console.log(\`Response Body for \${resolvedUrl${i}}: \${res${i}.body}\`);
48
+ check(res${i}, {
49
+ "status is 2xx": (r) => r.status >= 200 && r.status < 300
50
+ });
48
51
  `
49
52
  )
50
53
  .join("\n")
51
54
  : `
52
- const resolvedUrl = "${resolveEndpoint(endpoint)}";
53
- const res = http.request("${method}", resolvedUrl, ${
55
+ const resolvedUrl = "${resolveEndpoint(endpoint)}";
56
+ const res = http.request("${method}", resolvedUrl, ${
54
57
  method === "GET" || method === "DELETE"
55
58
  ? "null"
56
- : JSON.stringify(body)
57
- }, {
58
- headers: ${JSON.stringify(headers, null, 2)}
59
- });
60
- console.log(\`Response Body for \${resolvedUrl}: \${res.body}\`);
61
- check(res, {
62
- "status is 2xx": (r) => r.status >= 200 && r.status < 300
63
- });
59
+ : "JSON.stringify(body)"
60
+ }, { headers });
61
+ console.log(\`Response Body for \${resolvedUrl}: \${res.body}\`);
62
+ check(res, {
63
+ "status is 2xx": (r) => r.status >= 200 && r.status < 300
64
+ });
64
65
  `
65
66
  }
66
67
  }
67
- `;
68
+
69
+ export function handleSummary(data) {
70
+ const outputDir = __ENV.REPORT_OUTPUT_DIR || "reports";
71
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
72
+ const filename = \`\${outputDir}/k6-report-\${timestamp}.html\`;
73
+ return {
74
+ [filename]: htmlReport(data),
75
+ stdout: textSummary(data, { indent: " ", enableColors: true }),
76
+ };
77
+ }
78
+ `;
68
79
  };
@@ -60,46 +60,43 @@ const runK6Script = async (scriptPath, overwrite = false) => {
60
60
  const customLogo = `${chalkGreen} with @qaPaschalE's ${chalkYellow}k6-cucumber-steps v${packageJson.version}${resetColor}`;
61
61
 
62
62
  return new Promise(async (resolve, reject) => {
63
- exec(
64
- `k6 run --vus 1 --iterations 1 "${scriptPath}"`,
65
- async (error, stdout, stderr) => {
66
- // Split the k6 logo lines
67
- const logoLines = stdout.split("\n");
63
+ exec(`k6 run "${scriptPath}"`, async (error, stdout, stderr) => {
64
+ // Split the k6 logo lines
65
+ const logoLines = stdout.split("\n");
68
66
 
69
- // Insert the custom logo under "Grafana" (on the third line)
70
- let modifiedStdout = "";
71
- for (let i = 0; i < logoLines.length; i++) {
72
- modifiedStdout += logoLines[i];
73
- if (i === 5) {
74
- // Target the third line (index 2) of the k6 logo
75
- modifiedStdout += ` ${customLogo}\n`;
76
- }
77
- modifiedStdout += "\n";
78
- }
79
-
80
- // Handle errors and cleanup
81
- if (error) {
82
- console.error("k6 error:", error);
83
- console.error("k6 stdout:", modifiedStdout);
84
- await delay(3000); // Wait for 3 seconds
85
- console.error("k6 stderr:", stderr);
86
- reject(new Error(`k6 test execution failed: ${error.message}`));
87
- } else if (stderr) {
88
- console.log("k6 stdout:", modifiedStdout);
89
- await delay(3000); // Wait for 3 seconds
90
- resolve(stdout);
91
- } else {
92
- console.log("k6 stdout:", modifiedStdout);
93
- await delay(3000); // Wait for 3 seconds
94
- resolve(stdout);
67
+ // Insert the custom logo under "Grafana" (on the third line)
68
+ let modifiedStdout = "";
69
+ for (let i = 0; i < logoLines.length; i++) {
70
+ modifiedStdout += logoLines[i];
71
+ if (i === 5) {
72
+ // Target the third line (index 2) of the k6 logo
73
+ modifiedStdout += ` ${customLogo}\n`;
95
74
  }
75
+ modifiedStdout += "\n";
76
+ }
96
77
 
97
- // Clean up the temporary script file
98
- fs.unlink(scriptPath).catch((err) =>
99
- console.error("Error deleting temporary k6 script:", err)
100
- );
78
+ // Handle errors and cleanup
79
+ if (error) {
80
+ console.error("k6 error:", error);
81
+ console.error("k6 stdout:", modifiedStdout);
82
+ await delay(3000); // Wait for 3 seconds
83
+ console.error("k6 stderr:", stderr);
84
+ reject(new Error(`k6 test execution failed: ${error.message}`));
85
+ } else if (stderr) {
86
+ console.log("k6 stdout:", modifiedStdout);
87
+ await delay(3000); // Wait for 3 seconds
88
+ resolve(stdout);
89
+ } else {
90
+ console.log("k6 stdout:", modifiedStdout);
91
+ await delay(3000); // Wait for 3 seconds
92
+ resolve(stdout);
101
93
  }
102
- );
94
+
95
+ // Clean up the temporary script file
96
+ fs.unlink(scriptPath).catch((err) =>
97
+ console.error("Error deleting temporary k6 script:", err)
98
+ );
99
+ });
103
100
  });
104
101
  };
105
102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k6-cucumber-steps",
3
- "version": "1.0.40",
3
+ "version": "1.1.1",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,53 @@
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 };
@@ -0,0 +1,60 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const reportsDir = path.resolve("reports");
5
+
6
+ /**
7
+ * Adds internal cross-links between reports.
8
+ */
9
+ function addLinksToReport(targetFile, otherFiles) {
10
+ const content = fs.readFileSync(targetFile, "utf-8");
11
+
12
+ // Inject the Cucumber Report tab before the Request Metrics tab
13
+ const cucumberTab = `
14
+ <input type="radio" name="tabs" id="tabcucumber">
15
+ <label for="tabcucumber"><i class="fas fa-file-alt"></i> &nbsp; Cucumber Report</label>
16
+ <div class="tab">
17
+ <iframe src="cucumber-report.html" style="width:100%; height:600px; border:none;"></iframe>
18
+ </div>
19
+ `;
20
+
21
+ const modifiedContent = content
22
+ .replace(
23
+ /<input type="radio" name="tabs" id="tabone"/,
24
+ `${cucumberTab}\n<input type="radio" name="tabs" id="tabone"`
25
+ )
26
+ // Remove standalone links to cucumber-report.html or k6-report.html
27
+ .replace(
28
+ /<div style="padding:10px;margin-top:20px;text-align:center">[\s\S]*?View (Cucumber|k6).*?<\/div>/,
29
+ ""
30
+ );
31
+
32
+ fs.writeFileSync(targetFile, modifiedContent, "utf-8");
33
+ }
34
+
35
+ /**
36
+ * Auto-link all HTML reports in the reports directory.
37
+ */
38
+ function linkReports() {
39
+ if (!fs.existsSync(reportsDir)) {
40
+ console.warn("โš ๏ธ No reports directory found.");
41
+ return;
42
+ }
43
+
44
+ const htmlFiles = fs
45
+ .readdirSync(reportsDir)
46
+ .filter((f) => f.endsWith(".html"))
47
+ .map((f) => path.join(reportsDir, f));
48
+
49
+ if (htmlFiles.length < 2) {
50
+ console.warn("โš ๏ธ Not enough HTML files to link.");
51
+ return;
52
+ }
53
+
54
+ for (const file of htmlFiles) {
55
+ const others = htmlFiles.filter((f) => f !== file);
56
+ addLinksToReport(file, others);
57
+ }
58
+ }
59
+
60
+ module.exports = { linkReports };
@@ -35,26 +35,56 @@ Given("I set a k6 script for {word} testing", function (method) {
35
35
  * When I set to run the k6 script with the following configurations:
36
36
  * | virtual_users | duration | http_req_failed | http_req_duration | error_rate | stages |
37
37
  * | 10 | 5 | rate<0.05 | p(95)<200 | | |
38
- * | 50 | 10 | | | rate<0.01 | [{"target": 10, "duration": "10s"}, {"target": 50, "duration": "30s"}] |
38
+ * | 50 | 10 | rate<0.01 | | rate<0.01 | [{"target": 10, "duration": "10s"}, {"target": 50, "duration": "30s"}] |
39
39
  */
40
40
  When(
41
41
  "I set to run the k6 script with the following configurations:",
42
42
  function (dataTable) {
43
- const row = dataTable.hashes()[0];
43
+ const rawRow = dataTable.hashes()[0];
44
+ const row = {};
45
+
46
+ // Extract example values manually from this.pickle
47
+ const exampleMap = {};
48
+ if (this.pickle && this.pickle.astNodeIds && this.gherkinDocument) {
49
+ const scenario = this.gherkinDocument.feature.children.find((child) => {
50
+ return child.scenario && child.scenario.examples?.length;
51
+ });
52
+
53
+ const exampleValues =
54
+ scenario?.scenario?.examples?.[0]?.tableBody?.[0]?.cells?.map(
55
+ (cell) => cell.value
56
+ ) || [];
57
+
58
+ const exampleKeys =
59
+ scenario?.scenario?.examples?.[0]?.tableHeader?.cells?.map(
60
+ (cell) => cell.value
61
+ ) || [];
62
+
63
+ exampleKeys.forEach((key, idx) => {
64
+ exampleMap[key] = exampleValues[idx];
65
+ });
66
+ }
67
+
68
+ for (const [key, value] of Object.entries(rawRow)) {
69
+ row[key] = value.replace(/<([^>]+)>/g, (_, param) => {
70
+ return exampleMap[param] || value;
71
+ });
72
+ }
73
+
74
+ console.log("๐Ÿšจ Resolved config row:", row);
44
75
 
45
- // Validate thresholds only if they are not placeholders
46
- const validateIfNotPlaceholder = (value) => {
47
- if (value && !/^<.*>$/.test(value)) {
48
- validateThreshold(value);
76
+ const validateThreshold = (value) => {
77
+ const regex = /^[\w{}()<>:]+[<>=]\d+(\.\d+)?$/;
78
+ if (value && !regex.test(value)) {
79
+ throw new Error(`Invalid threshold format: ${value}`);
49
80
  }
50
81
  };
51
82
 
52
- validateIfNotPlaceholder(row.http_req_failed);
53
- validateIfNotPlaceholder(row.http_req_duration);
54
- validateIfNotPlaceholder(row.error_rate);
83
+ validateThreshold(row.http_req_failed);
84
+ validateThreshold(row.http_req_duration);
85
+ validateThreshold(row.error_rate);
55
86
 
56
87
  if (row.stages) {
57
- // User provided a stages definition (JSON array)
58
88
  try {
59
89
  this.config.options = {
60
90
  stages: JSON.parse(row.stages),
@@ -63,11 +93,10 @@ When(
63
93
  http_req_duration: [row.http_req_duration],
64
94
  },
65
95
  };
66
- } catch (err) {
96
+ } catch {
67
97
  throw new Error("Invalid stages JSON format.");
68
98
  }
69
99
  } else {
70
- // Default to VUs and duration
71
100
  this.config.options = {
72
101
  vus: parseInt(row.virtual_users),
73
102
  duration: `${row.duration}s`,
@@ -78,7 +107,7 @@ When(
78
107
  };
79
108
  }
80
109
 
81
- if (row.error_rate && !/^<.*>$/.test(row.error_rate)) {
110
+ if (row.error_rate) {
82
111
  this.config.options.thresholds.error_rate = [row.error_rate];
83
112
  }
84
113
  }
@@ -171,14 +200,11 @@ When(
171
200
  });
172
201
 
173
202
  this.config = {
203
+ ...this.config, // โœ… Keep previously set options!
174
204
  method: methodUpper,
175
205
  endpoint,
176
206
  body: resolved,
177
207
  headers: this.config?.headers || {},
178
- options: {
179
- vus: 1,
180
- iterations: 1,
181
- },
182
208
  };
183
209
 
184
210
  this.lastRequest = {
@@ -1,16 +1,39 @@
1
1
  const { setWorldConstructor } = require("@cucumber/cucumber");
2
2
 
3
3
  class CustomWorld {
4
- constructor({ parameters }) {
4
+ constructor({ parameters, pickle }) {
5
5
  this.options = {};
6
6
  this.configurations = {};
7
7
  this.endpoints = [];
8
8
  this.authType = "";
9
9
  this.postBody = {};
10
+ this.aliases = {};
11
+ this.lastRequest = null;
12
+ this.lastResponse = null;
13
+
10
14
  this.overwrite =
11
15
  parameters?.overwrite ||
12
16
  process.env.K6_CUCUMBER_OVERWRITE === "true" ||
13
17
  false;
18
+
19
+ // โœ… Store scenario example values from <placeholders>
20
+ this.parameters = {};
21
+
22
+ if (parameters) {
23
+ this.parameters = { ...parameters };
24
+ }
25
+
26
+ if (pickle?.steps?.length) {
27
+ const matches = pickle.steps.flatMap((step) => {
28
+ return [...(step.text?.matchAll(/<([^>]+)>/g) || [])];
29
+ });
30
+ matches.forEach((match) => {
31
+ const key = match[1];
32
+ if (parameters?.[key]) {
33
+ this.parameters[key] = parameters[key];
34
+ }
35
+ });
36
+ }
14
37
  }
15
38
  }
16
39
 
package/.env.example DELETED
@@ -1,17 +0,0 @@
1
- # .env
2
- TEST_ENVIRONMENT=STAGING
3
- APP_VERSION=1.0.0
4
- BROWSER=Chrome 100.0.4896.88
5
- PLATFORM=Windows 10
6
- PARALLEL=Scenarios
7
- EXECUTED=Remote
8
-
9
- # Base URLs
10
- API_URL=https://test-api.example.com
11
- BASE_URL=https://example.com
12
- AUTH_URL=https://auth.example.com
13
- PAYMENT_URL=https://payment.example.com
14
-
15
- # Secret Parameters
16
- API_KEY=your-secret-api-key-12345
17
- BEARER_TOKEN=your_bearer_token_here
package/generateReport.js DELETED
@@ -1,24 +0,0 @@
1
- const reporter = require("cucumber-html-reporter");
2
-
3
- // Configuration for the HTML report
4
- const options = {
5
- theme: "bootstrap", // You can change the theme to 'simple', 'compact', etc.
6
- jsonFile: "reports/load-results.json", // Path to the JSON file generated by Cucumber
7
- output: "reports/report.html", // Path where you want to save the HTML report
8
- reportSuiteAsScenarios: true, // Group scenarios by their name
9
- launchReport: true, // Automatically open the report in the browser
10
- metadata: {
11
- browser: {
12
- name: "Chrome" || "unknown",
13
- version: "89" || "unknown",
14
- },
15
- device: "Local test machine",
16
- platform: {
17
- name: "MacOS",
18
- version: "20.04",
19
- },
20
- },
21
- };
22
-
23
- // Generate the report
24
- reporter.generate(options);