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.
- package/README.md +190 -177
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +72 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/init.command.d.ts +4 -0
- package/dist/commands/init.command.d.ts.map +1 -0
- package/dist/commands/init.command.js +30 -0
- package/dist/commands/init.command.js.map +1 -0
- package/dist/generators/feature.parser.d.ts +12 -0
- package/dist/generators/feature.parser.d.ts.map +1 -0
- package/dist/generators/feature.parser.js +208 -0
- package/dist/generators/feature.parser.js.map +1 -0
- package/dist/generators/k6-script.generator.d.ts +11 -0
- package/dist/generators/k6-script.generator.d.ts.map +1 -0
- package/dist/generators/k6-script.generator.js +233 -0
- package/dist/generators/k6-script.generator.js.map +1 -0
- package/dist/generators/project.generator.d.ts +14 -0
- package/dist/generators/project.generator.d.ts.map +1 -0
- package/dist/generators/project.generator.js +497 -0
- package/dist/generators/project.generator.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/runners/k6.runner.d.ts +19 -0
- package/dist/runners/k6.runner.d.ts.map +1 -0
- package/dist/runners/k6.runner.js +127 -0
- package/dist/runners/k6.runner.js.map +1 -0
- package/dist/step-registry.d.ts +14 -0
- package/dist/step-registry.d.ts.map +1 -0
- package/dist/step-registry.js +36 -0
- package/dist/step-registry.js.map +1 -0
- package/dist/types/index.d.ts +35 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +40 -62
- package/LICENSE +0 -21
- package/bin/k6-cucumber-steps.js +0 -176
- package/docs/data/search.json +0 -1
- package/docs/fonts/Inconsolata-Regular.ttf +0 -0
- package/docs/fonts/OpenSans-Regular.ttf +0 -0
- package/docs/fonts/WorkSans-Bold.ttf +0 -0
- package/docs/global.html +0 -36
- package/docs/index.html +0 -91
- package/docs/k6-cucumber-steps/1.2.8/data/search.json +0 -1
- package/docs/k6-cucumber-steps/1.2.8/fonts/Inconsolata-Regular.ttf +0 -0
- package/docs/k6-cucumber-steps/1.2.8/fonts/OpenSans-Regular.ttf +0 -0
- package/docs/k6-cucumber-steps/1.2.8/fonts/WorkSans-Bold.ttf +0 -0
- package/docs/k6-cucumber-steps/1.2.8/global.html +0 -3
- package/docs/k6-cucumber-steps/1.2.8/helpers_generateHeaders.js.html +0 -38
- package/docs/k6-cucumber-steps/1.2.8/helpers_resolveBody.js.html +0 -65
- package/docs/k6-cucumber-steps/1.2.8/index.html +0 -91
- package/docs/k6-cucumber-steps/1.2.8/module-generateHeaders.html +0 -3
- package/docs/k6-cucumber-steps/1.2.8/module-resolveBody.html +0 -3
- package/docs/k6-cucumber-steps/1.2.8/scripts/core.js +0 -726
- package/docs/k6-cucumber-steps/1.2.8/scripts/core.min.js +0 -23
- package/docs/k6-cucumber-steps/1.2.8/scripts/resize.js +0 -90
- package/docs/k6-cucumber-steps/1.2.8/scripts/search.js +0 -265
- package/docs/k6-cucumber-steps/1.2.8/scripts/search.min.js +0 -6
- package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/Apache-License-2.0.txt +0 -202
- package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/fuse.js +0 -9
- package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/hljs-line-num-original.js +0 -369
- package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/hljs-line-num.js +0 -1
- package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/hljs-original.js +0 -5171
- package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/hljs.js +0 -1
- package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/popper.js +0 -5
- package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/tippy.js +0 -1
- package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/tocbot.js +0 -672
- package/docs/k6-cucumber-steps/1.2.8/scripts/third-party/tocbot.min.js +0 -1
- package/docs/k6-cucumber-steps/1.2.8/styles/clean-jsdoc-theme-base.css +0 -1159
- package/docs/k6-cucumber-steps/1.2.8/styles/clean-jsdoc-theme-dark.css +0 -412
- package/docs/k6-cucumber-steps/1.2.8/styles/clean-jsdoc-theme-light.css +0 -482
- package/docs/k6-cucumber-steps/1.2.8/styles/clean-jsdoc-theme-scrollbar.css +0 -30
- package/docs/k6-cucumber-steps/1.2.8/styles/clean-jsdoc-theme-without-scrollbar.min.css +0 -1
- package/docs/k6-cucumber-steps/1.2.8/styles/clean-jsdoc-theme.min.css +0 -1
- package/docs/k6-cucumber-steps/1.2.8/utils_k6Runner.js.html +0 -95
- package/docs/load_test_steps.js.html +0 -664
- package/docs/scripts/core.js +0 -726
- package/docs/scripts/core.min.js +0 -23
- package/docs/scripts/resize.js +0 -90
- package/docs/scripts/search.js +0 -265
- package/docs/scripts/search.min.js +0 -6
- package/docs/scripts/third-party/Apache-License-2.0.txt +0 -202
- package/docs/scripts/third-party/fuse.js +0 -9
- package/docs/scripts/third-party/hljs-line-num-original.js +0 -369
- package/docs/scripts/third-party/hljs-line-num.js +0 -1
- package/docs/scripts/third-party/hljs-original.js +0 -5171
- package/docs/scripts/third-party/hljs.js +0 -1
- package/docs/scripts/third-party/popper.js +0 -5
- package/docs/scripts/third-party/tippy.js +0 -1
- package/docs/scripts/third-party/tocbot.js +0 -672
- package/docs/scripts/third-party/tocbot.min.js +0 -1
- package/docs/styles/clean-jsdoc-theme-base.css +0 -1159
- package/docs/styles/clean-jsdoc-theme-dark.css +0 -412
- package/docs/styles/clean-jsdoc-theme-light.css +0 -482
- package/docs/styles/clean-jsdoc-theme-scrollbar.css +0 -30
- package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +0 -1
- package/docs/styles/clean-jsdoc-theme.min.css +0 -1
- package/index.js +0 -1
- package/lib/helpers/buildK6Script.d.ts +0 -1
- package/lib/helpers/buildK6Script.js +0 -103
- package/lib/helpers/generateHeaders.d.ts +0 -5
- package/lib/helpers/generateHeaders.js +0 -48
- package/lib/helpers/resolveBody.d.ts +0 -2
- package/lib/helpers/resolveBody.js +0 -78
- package/lib/helpers/resolvePayloadPath.js +0 -25
- package/lib/helpers/runK6ScriptFromWorld.js +0 -26
- package/lib/utils/k6Runner.d.ts +0 -6
- package/lib/utils/k6Runner.js +0 -89
- package/scripts/cucumber.js +0 -18
- package/scripts/linkReports.js +0 -107
- package/step_definitions/load_test_steps.d.ts +0 -89
- package/step_definitions/load_test_steps.js +0 -689
- package/step_definitions/world.js +0 -41
package/README.md
CHANGED
|
@@ -1,282 +1,295 @@
|
|
|
1
1
|
# k6-cucumber-steps 🥒🧪
|
|
2
2
|
|
|
3
3
|
<table align="center" style="margin-bottom:30px;"><tr><td align="center" width="9999" heigth="9999" >
|
|
4
|
-
<img src="
|
|
4
|
+
<img src="assets/paschal logo (2).png" alt="paschal Logo" style="margin-top:25px;" align="center"/>
|
|
5
5
|
</td></tr></table>
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/k6-cucumber-steps)
|
|
8
|
-
[](https://github.com/qaPaschalE/k6-cucumber-steps/stargazers)
|
|
15
|
-
[](https://www.npmjs.com/package/k6-cucumber-steps)
|
|
8
|
+
[](https://www.npmjs.com/package/k6-cucumber-steps)
|
|
9
|
+
[](https://github.com/qaPaschalE/k6-cucumber-steps/blob/main/LICENSE)
|
|
10
|
+
[](https://cucumber.io/)
|
|
11
|
+
[](https://nodejs.org/)
|
|
12
|
+
[](https://github.com/sponsors/qaPaschalE)
|
|
13
|
+
[](https://github.com/aPaschalE/k6-cucumber-steps/actions/workflows/k6-load-test.yml)
|
|
16
14
|
|
|
17
15
|
Run [k6](https://k6.io/) performance/load tests using [Cucumber](https://cucumber.io/) BDD syntax with ease.
|
|
18
16
|
|
|
19
|
-
👉 [View Steps Documentation](./docs/index.html)
|
|
20
|
-
|
|
21
17
|
---
|
|
22
18
|
|
|
23
19
|
## ✨ Features
|
|
24
20
|
|
|
25
|
-
- ✅ Cucumber + Gherkin for writing k6 tests
|
|
21
|
+
- ✅ Cucumber + Gherkin for writing k6 tests
|
|
22
|
+
to generate JSON and HTML reports.
|
|
26
23
|
- ✅ Flexible configuration through Cucumber data tables.
|
|
27
|
-
- ✅ Support for JSON body parsing and escaping
|
|
24
|
+
- ✅ Support for JSON body parsing and escaping
|
|
28
25
|
- ✅ Dynamic request body generation using environment variables, Faker templates, and JSON file includes.
|
|
29
26
|
- ✅ `.env` + `K6.env`-style variable resolution (`{{API_KEY}}`)
|
|
30
|
-
- ✅ Support for headers, query params, stages
|
|
27
|
+
- ✅ Support for headers, query params, stages
|
|
31
28
|
- ✅ Supports multiple authentication types: API key, Bearer token, Basic Auth, and No Auth.
|
|
32
|
-
|
|
33
|
-
- ✅
|
|
29
|
+
|
|
30
|
+
- ✅ Clean-up of temporary k6 files after execution
|
|
31
|
+
- ✅ Built-in support for **distributed load testing** with stages
|
|
34
32
|
- ✅ TypeScript-first 🧡
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
|
|
34
|
+
## ✨ Key Enhancements
|
|
35
|
+
|
|
36
|
+
- 🚀 **One-Command Setup**: Use `init` to scaffold a full k6 project with sample features and steps.
|
|
37
|
+
- 📂 **Centralized Reporting**: Automatically generates HTML and JSON reports in a dedicated `reports/` folder.
|
|
38
|
+
- 🔑 **Dynamic Auth Storage**: Store tokens from one scenario and reuse them in another via `globalThis` memory.
|
|
39
|
+
- 🛠 **JS & TS Support**: Generate your project in pure JavaScript or TypeScript.
|
|
40
|
+
- 📊 **Metric Segmentation**: Scenarios are wrapped in k6 `group()` blocks for cleaner reporting.
|
|
41
|
+
|
|
42
|
+
## ✨ New: Hybrid Performance Testing
|
|
43
|
+
|
|
44
|
+
You can now combine **Protocol-level (HTTP)** load testing and **Browser-level (Web Vitals)** testing in a single Gherkin suite.
|
|
45
|
+
|
|
46
|
+
- **API Testing**: High-concurrency stress testing at the protocol layer.
|
|
47
|
+
- **Browser Testing**: Real browser rendering metrics (LCP, CLS, FID) using k6 browser (Chromium).
|
|
38
48
|
|
|
39
49
|
---
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 🚀 Quick Start (Scaffolding a New Project)
|
|
54
|
+
|
|
55
|
+
The easiest way to start is by initializing a pre-configured project structure.
|
|
42
56
|
|
|
43
57
|
```bash
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
# 1. Create your test directory
|
|
59
|
+
mkdir my-load-tests && cd my-load-tests
|
|
46
60
|
|
|
47
|
-
|
|
61
|
+
# 2. Initialize the project (defaults to TypeScript)
|
|
62
|
+
npx k6-cucumber-steps init
|
|
48
63
|
|
|
49
|
-
|
|
64
|
+
# OR Initialize with JavaScript
|
|
65
|
+
npx k6-cucumber-steps init --lang js|ts
|
|
66
|
+
|
|
67
|
+
# 3. Install dependencies & Run
|
|
68
|
+
npm install
|
|
69
|
+
npx k6-cucumber-steps generate -l js|ts
|
|
70
|
+
npm test
|
|
50
71
|
|
|
51
|
-
```bash
|
|
52
|
-
npx k6-cucumber-steps run [options]
|
|
53
72
|
```
|
|
54
73
|
|
|
55
|
-
|
|
74
|
+
---
|
|
56
75
|
|
|
57
|
-
|
|
76
|
+
## 🛠️ Project Structure
|
|
58
77
|
|
|
59
|
-
|
|
60
|
-
- `-t, --tags <string>`: Cucumber tags to filter scenarios (e.g., `@smoke and not @regression`).
|
|
61
|
-
- `-c, --config <file>`: Custom config file (default: `cucumber.js`).
|
|
62
|
-
- `-r, --reporter`: Generate HTML and JSON reports in the `reports` directory. This is a boolean flag, so just include `-r, --reporter` to enable it.
|
|
63
|
-
- `-o, --overwrite`: Overwrite existing reports instead of appending them.
|
|
64
|
-
- `--cleanReports`, `--clean`: **Clean the `reports` directory before running.**
|
|
65
|
-
You can also set this via the `cleanReports` property in your `cucumber.js` config or with the `CLEAN_REPORTS=true` environment variable.
|
|
78
|
+
The `init` command creates a clean, industry-standard directory structure:
|
|
66
79
|
|
|
67
|
-
|
|
80
|
+
```text
|
|
81
|
+
.
|
|
82
|
+
├── data/ # User credentials and seed data
|
|
83
|
+
├── features/ # Gherkin .feature files
|
|
84
|
+
├── steps/ # Step definitions (logic)
|
|
85
|
+
├── generated/ # Compiled k6 scripts (auto-generated)
|
|
86
|
+
├── reports/ # HTML & JSON test results
|
|
87
|
+
└── package.json # Test scripts and dependencies
|
|
68
88
|
|
|
69
|
-
```bash
|
|
70
|
-
npx k6-cucumber-steps run --feature ./features/my_feature.feature --tags "@load and not @wip" --reporter --cleanReports
|
|
71
89
|
```
|
|
72
90
|
|
|
73
91
|
---
|
|
74
92
|
|
|
75
|
-
## 🛠️
|
|
93
|
+
## 🛠️ CLI Reference
|
|
76
94
|
|
|
77
|
-
|
|
95
|
+
#### Options
|
|
78
96
|
|
|
79
|
-
|
|
97
|
+
The `npx k6-cucumber-steps` command accepts the following options:
|
|
80
98
|
|
|
81
|
-
|
|
82
|
-
2. **k6:** Install k6 on your system following the instructions at [k6.io/docs/getting-started/installation/](https://k6.io/docs/getting-started/installation/).
|
|
83
|
-
3. **@cucumber/cucumber (optional):** This package is required for using Cucumber.
|
|
84
|
-
4. **cucumber-html-reporter (optional):** This package is needed if you intend to generate detailed HTML reports.
|
|
99
|
+
### `init`
|
|
85
100
|
|
|
86
|
-
|
|
101
|
+
Scaffolds a new project.
|
|
87
102
|
|
|
88
|
-
|
|
103
|
+
- `--lang <js|ts>`: Choose the project language (default: `ts`).
|
|
104
|
+
- `--force`: Overwrite existing files in the current directory.
|
|
105
|
+
- `.command("init")`
|
|
106
|
+
`description`: "Initialize a new k6-cucumber project
|
|
107
|
+
- `.argument "[path]"`: "Output directory path", "./k6-test-project"
|
|
108
|
+
- `-f, --feature <path>`: "Path to feature files", "./features"
|
|
109
|
+
- `-t, --tags <string>`: Cucumber tags to filter scenarios (e.g., `@smoke and not @regression`).
|
|
89
110
|
|
|
90
|
-
|
|
91
|
-
mkdir my-performance-test
|
|
92
|
-
cd my-performance-test
|
|
93
|
-
npm init -y
|
|
94
|
-
# or
|
|
95
|
-
yarn init -y
|
|
96
|
-
```
|
|
111
|
+
### `generate`
|
|
97
112
|
|
|
98
|
-
|
|
113
|
+
Parses your `.feature` files and creates the k6-compatible execution scripts in the `generated/` folder.
|
|
99
114
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
yarn add --dev @cucumber/cucumber k6 dotenv k6-cucumber-steps cucumber-html-reporter
|
|
104
|
-
```
|
|
115
|
+
- `.command("generate")`
|
|
116
|
+
- `.description`: ("Generate k6 scripts from feature files")
|
|
117
|
+
- `--lang <js|ts>`: Choose the project language (default: `ts`).
|
|
105
118
|
|
|
106
|
-
|
|
119
|
+
### `run` (Direct Execution)
|
|
107
120
|
|
|
108
|
-
|
|
109
|
-
// e2e/steps/index.js or .ts
|
|
110
|
-
import "k6-cucumber-steps";
|
|
111
|
-
```
|
|
121
|
+
For projects where you prefer to run single features directly.
|
|
112
122
|
|
|
113
|
-
|
|
123
|
+
- `-f, --feature <path>`: Path to specific feature.
|
|
114
124
|
|
|
115
|
-
|
|
125
|
+
---
|
|
116
126
|
|
|
117
|
-
|
|
118
|
-
mkdir features
|
|
119
|
-
# Create your .feature files inside the features directory (e.g., example.feature)
|
|
120
|
-
```
|
|
127
|
+
## 🧼 Clean-up & Maintenance
|
|
121
128
|
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
- **`npm run clean`**: Wipes the `reports/` and `generated/` folders.
|
|
130
|
+
- **`npm run report`**: Opens the latest HTML report in your default browser.
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
// cucumber.js as default name but you can optionally give it any name of choice
|
|
132
|
+
---
|
|
127
133
|
|
|
128
|
-
|
|
129
|
-
require: [
|
|
130
|
-
"path/to/steps/index.js", // You can add paths to your local step definitions here if needed
|
|
131
|
-
],
|
|
132
|
-
reporter: true, // To provide HTML and JSON report
|
|
133
|
-
format: [
|
|
134
|
-
"summary",
|
|
135
|
-
"json:reports/load-report.json", // For JSON report
|
|
136
|
-
"html:reports/cucumber-report.html", // For HTML report
|
|
137
|
-
],
|
|
138
|
-
paths: ["./features/*.feature"],
|
|
139
|
-
tags: process.env.TAGS,
|
|
140
|
-
worldParameters: {
|
|
141
|
-
payloadPath: "/payloads",
|
|
142
|
-
},
|
|
143
|
-
overwrite: false, // Default to not overwrite the report file
|
|
144
|
-
cleanReports: true, // <--- Clean the reports directory before running
|
|
145
|
-
};
|
|
146
|
-
```
|
|
134
|
+
## 🔑 Advanced Authentication Flow
|
|
147
135
|
|
|
148
|
-
**
|
|
136
|
+
We now support **Dynamic Handshake Authentication**. You can log in once in an initial scenario, store the token, and all subsequent scenarios will automatically be authenticated.
|
|
149
137
|
|
|
150
|
-
|
|
138
|
+
### Step 1: Login and Capture
|
|
151
139
|
|
|
152
|
-
|
|
153
|
-
|
|
140
|
+
```gherkin
|
|
141
|
+
Scenario: Authenticate and Store Token
|
|
142
|
+
When I authenticate with the following url and request body as "standard_user":
|
|
143
|
+
| endpoint | username | password |
|
|
144
|
+
| /login | paschal_qa | pass123 |
|
|
145
|
+
And I store "data.token" in "data/standard_user.json"
|
|
154
146
|
|
|
155
|
-
|
|
156
|
-
Add `CLEAN_REPORTS=true` to your `.env` file.
|
|
147
|
+
```
|
|
157
148
|
|
|
158
|
-
|
|
159
|
-
Set `cleanReports: true` in your `cucumber.js` config.
|
|
149
|
+
### Step 2: Reuse Token
|
|
160
150
|
|
|
161
|
-
|
|
151
|
+
```gherkin
|
|
152
|
+
Background:
|
|
153
|
+
And I am authenticated as a "standard_user" # Lookups token from memory
|
|
162
154
|
|
|
163
|
-
|
|
155
|
+
```
|
|
164
156
|
|
|
165
|
-
##
|
|
157
|
+
## 🚀 Usage
|
|
166
158
|
|
|
167
|
-
|
|
159
|
+
### Browser Testing (@browser)
|
|
168
160
|
|
|
169
|
-
|
|
170
|
-
BASE_URL=https://api.example.com
|
|
171
|
-
API_BASE_URL=https://api.example.com
|
|
172
|
-
API_KEY=your_api_key
|
|
173
|
-
BEARER_TOKEN=your_bearer_token
|
|
174
|
-
BASIC_USER=your_basic_user
|
|
175
|
-
BASIC_PASS=your_basic_pass
|
|
176
|
-
TAGS=@yourTag
|
|
177
|
-
```
|
|
161
|
+
Simply tag your scenario with `@browser`. The generator will automatically launch a Chromium instance, manage the page lifecycle, and inject the `page` object into your steps.
|
|
178
162
|
|
|
179
|
-
|
|
163
|
+
```gherkin
|
|
164
|
+
@browser
|
|
165
|
+
Scenario: Verify Homepage UI and Web Vitals
|
|
166
|
+
Given the base URL is "https://test.k6.io"
|
|
167
|
+
When I navigate to the "/" page
|
|
168
|
+
Then I see the text on the page "Collection of simple web-pages"
|
|
180
169
|
|
|
181
|
-
|
|
170
|
+
```
|
|
182
171
|
|
|
183
|
-
|
|
172
|
+
### Dynamic Auth & Storage
|
|
184
173
|
|
|
185
|
-
|
|
174
|
+
Log in via API and reuse the token across any scenario (including Browser scenarios).
|
|
186
175
|
|
|
187
176
|
```gherkin
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
| 50 | 10 | rate<0.05 | p(95)<3000 |
|
|
195
|
-
And I set the following endpoints used:
|
|
196
|
-
"""
|
|
197
|
-
/api/profile
|
|
198
|
-
https://reqres.in/api/users?page=2
|
|
199
|
-
"""
|
|
200
|
-
And I set the authentication type to "none"
|
|
201
|
-
Then I see the API should handle the GET request successfully
|
|
177
|
+
Scenario: Login and Save Session
|
|
178
|
+
When I authenticate with the following url and request body as "admin":
|
|
179
|
+
| endpoint | username | password |
|
|
180
|
+
| /login | admin | p@ss123 |
|
|
181
|
+
And I store "token" in "data/admin.json"
|
|
182
|
+
|
|
202
183
|
```
|
|
203
184
|
|
|
204
|
-
|
|
185
|
+
## 🧼 Step Definitions Reference
|
|
205
186
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
| virtual_users | duration | http_req_failed | http_req_duration |
|
|
213
|
-
| 20 | 60 | rate<0.01 | p(95)<300 |
|
|
214
|
-
When I set the authentication type to "bearer_token"
|
|
215
|
-
And I set the following endpoints used:
|
|
216
|
-
"""
|
|
217
|
-
/api/v1/users
|
|
218
|
-
"""
|
|
219
|
-
And I set the following POST body is used for "/api/v1/users"
|
|
220
|
-
"""
|
|
221
|
-
{
|
|
222
|
-
"username": "{{username}}",
|
|
223
|
-
"email": "{{faker.internet.email}}"
|
|
224
|
-
}
|
|
225
|
-
"""
|
|
226
|
-
Then I see the API should handle the POST request successfully
|
|
187
|
+
| Step Example | Layer | Description |
|
|
188
|
+
| ------------------------------------- | ------- | ---------------------------- |
|
|
189
|
+
| `When I make a GET request to "/api"` | API | Standard HTTP request. |
|
|
190
|
+
| `When I navigate to the "/home" page` | Browser | Opens URL in Chromium. |
|
|
191
|
+
| `And I click the button ".submit"` | Browser | Interacts with DOM elements. |
|
|
192
|
+
| `And I store "path" in "file.json"` | Both | Dynamic data persistence. |
|
|
227
193
|
|
|
228
|
-
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 📊 Automated Reporting
|
|
197
|
+
|
|
198
|
+
Every test run now produces a rich HTML dashboard. Your scenarios are grouped naturally, making it easy to identify which specific Gherkin scenario is causing performance bottlenecks.
|
|
199
|
+
|
|
200
|
+
**Find your reports at:**
|
|
201
|
+
|
|
202
|
+
- `reports/summary.html`: Interactive dashboard.
|
|
203
|
+
- `reports/results.json`: Full k6 metric data.
|
|
204
|
+
- `reports/tokens_debug.json`: View captured tokens during the run.
|
|
205
|
+
|
|
206
|
+
---
|
|
229
207
|
|
|
230
208
|
## Step Definitions
|
|
231
209
|
|
|
232
210
|
### Authentication Steps
|
|
233
211
|
|
|
234
212
|
```gherkin
|
|
235
|
-
When I
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
When I set the authentication type to "custom-alias"
|
|
213
|
+
When I authenticate with the following url and request body as "standard_user":
|
|
214
|
+
| endpoint | username | password |
|
|
215
|
+
| /login | paschal_qa | pass123 |
|
|
216
|
+
And I am authenticated as a "standard_user" # Lookups token from memory
|
|
240
217
|
```
|
|
241
218
|
|
|
242
|
-
###
|
|
219
|
+
### sample features
|
|
243
220
|
|
|
244
221
|
```gherkin
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
222
|
+
@smoke @vus:10 @duration:1m
|
|
223
|
+
Feature: Comprehensive API Testing
|
|
224
|
+
|
|
225
|
+
Background:
|
|
226
|
+
Given the base URL is "https://jsonplaceholder.typicode.com"
|
|
227
|
+
And I set the default headers:
|
|
228
|
+
| Content-Type | Accept |
|
|
229
|
+
| application/json | application/json |
|
|
230
|
+
|
|
231
|
+
@group:user-api @threshold:http_req_duration=p(95)<500
|
|
232
|
+
Scenario: Get specific user details
|
|
233
|
+
When I make a GET request to "/users/1"
|
|
234
|
+
Then the response status should be 200
|
|
235
|
+
And the response should contain "name"
|
|
236
|
+
|
|
237
|
+
@group:load-test @stages:0s-0,20s-10,30s-10,10s-0
|
|
238
|
+
Scenario Outline: Validate multiple user endpoints
|
|
239
|
+
When I make a GET request to "/users/<userId>"
|
|
240
|
+
Then the response status should be <expectedStatus>
|
|
241
|
+
|
|
242
|
+
Examples:
|
|
243
|
+
| userId | expectedStatus |
|
|
244
|
+
| 1 | 200 |
|
|
245
|
+
| 5 | 200 |
|
|
246
|
+
| 999 | 404 |
|
|
247
|
+
|
|
248
|
+
@group:post-api
|
|
249
|
+
Scenario: Create a post with bulk data
|
|
250
|
+
Given I have the following post data:
|
|
251
|
+
"""
|
|
252
|
+
{
|
|
253
|
+
"title": "Performance Test",
|
|
254
|
+
"body": "Testing DataTables and DocStrings",
|
|
255
|
+
"userId": 1
|
|
256
|
+
}
|
|
257
|
+
"""
|
|
258
|
+
When I make a POST request to "/posts"
|
|
259
|
+
Then the response status should be 201
|
|
260
|
+
|
|
261
|
+
|
|
252
262
|
```
|
|
253
263
|
|
|
254
264
|
### Assertion Steps
|
|
255
265
|
|
|
256
266
|
```gherkin
|
|
257
|
-
Then
|
|
267
|
+
Then the response status should be 200
|
|
268
|
+
Then the response should contain "name"
|
|
269
|
+
Then the response status should be <expectedStatus>
|
|
270
|
+
|
|
258
271
|
```
|
|
259
272
|
|
|
260
|
-
## Test Results
|
|
273
|
+
<!-- ## Test Results
|
|
261
274
|
|
|
262
275
|
Below is an example of the Cucumber report generated after running the tests:
|
|
263
276
|
<img src="assets/k6-cucumber-report.png" alt="Cucumber report generated after running the tests" width="60%" />
|
|
264
|
-
<img src="assets/k6-cucumber-report2.png" alt="Cucumber report generated after running the tests" width="60%" />
|
|
277
|
+
<img src="assets/k6-cucumber-report2.png" alt="Cucumber report generated after running the tests" width="60%" /> -->
|
|
265
278
|
|
|
266
|
-
### Explanation of the Report
|
|
279
|
+
<!-- ### Explanation of the Report
|
|
267
280
|
|
|
268
281
|
- **All Scenarios**: Total number of scenarios executed.
|
|
269
282
|
- **Passed Scenarios**: Number of scenarios that passed.
|
|
270
283
|
- **Failed Scenarios**: Number of scenarios that failed.
|
|
271
284
|
- **Metadata**: Information about the test environment (e.g., browser, platform).
|
|
272
285
|
- **Feature Overview**: Summary of the feature being tested.
|
|
273
|
-
- **Scenario Details**: Detailed steps and their execution status.
|
|
286
|
+
- **Scenario Details**: Detailed steps and their execution status. -->
|
|
274
287
|
|
|
275
|
-
## 🧼 Temporary Files Clean-up
|
|
288
|
+
<!-- ## 🧼 Temporary Files Clean-up
|
|
276
289
|
|
|
277
290
|
All generated k6 scripts and artifacts are cleaned automatically after test execution.
|
|
278
291
|
|
|
279
|
-
---
|
|
292
|
+
--- -->
|
|
280
293
|
|
|
281
294
|
## 💖 Support
|
|
282
295
|
|
|
@@ -286,4 +299,4 @@ If you find this package useful, consider [sponsoring me on GitHub](https://gith
|
|
|
286
299
|
|
|
287
300
|
MIT License - [@qaPaschalE](https://github.com/qaPaschalE)
|
|
288
301
|
|
|
289
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
302
|
+
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
// src/cli.ts
|
|
8
|
+
const commander_1 = require("commander");
|
|
9
|
+
const init_command_1 = require("./commands/init.command");
|
|
10
|
+
const feature_parser_1 = require("./generators/feature.parser");
|
|
11
|
+
const k6_script_generator_1 = require("./generators/k6-script.generator");
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
// Package info
|
|
14
|
+
const packageJson = require("../package.json");
|
|
15
|
+
commander_1.program
|
|
16
|
+
.name("k6-cucumber-steps")
|
|
17
|
+
.description("Generate k6 test scripts from Cucumber feature files")
|
|
18
|
+
.version(packageJson.version);
|
|
19
|
+
commander_1.program
|
|
20
|
+
.command("init")
|
|
21
|
+
.description("Initialize a new k6-cucumber project")
|
|
22
|
+
.option("-l, --lang <language>", "Programming language (js or ts)", "ts")
|
|
23
|
+
.argument("[path]", "Output directory path", "./k6-test-project")
|
|
24
|
+
.action(async (path, options) => {
|
|
25
|
+
const initCmd = new init_command_1.InitCommand();
|
|
26
|
+
await initCmd.execute(path, options.lang);
|
|
27
|
+
});
|
|
28
|
+
commander_1.program
|
|
29
|
+
.command("generate")
|
|
30
|
+
.description("Generate k6 scripts from feature files")
|
|
31
|
+
.option("-f, --features <path>", "Path to feature files", "./features")
|
|
32
|
+
.option("-o, --output <path>", "Output path for generated scripts", "./generated")
|
|
33
|
+
.option("-l, --lang <language>", "Output language (js or ts)", "ts")
|
|
34
|
+
.option("--tags <tags>", "Filter scenarios by tags")
|
|
35
|
+
.action(async (options) => {
|
|
36
|
+
await generateK6Scripts(options);
|
|
37
|
+
});
|
|
38
|
+
async function generateK6Scripts(options) {
|
|
39
|
+
console.log("Generating k6 scripts from feature files...");
|
|
40
|
+
const parser = new feature_parser_1.FeatureParser();
|
|
41
|
+
const features = await parser.loadAndParseFeatures(options.features);
|
|
42
|
+
// Filter by tags if provided
|
|
43
|
+
if (options.tags) {
|
|
44
|
+
const tagFilters = options.tags.split(",");
|
|
45
|
+
features.forEach((feature) => {
|
|
46
|
+
feature.scenarios = feature.scenarios.filter((scenario) => tagFilters.some((filter) => scenario.tags.includes(filter)));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// Flatten all scenarios
|
|
50
|
+
const allScenarios = features.flatMap((f) => f.scenarios);
|
|
51
|
+
const metadata = parser.loadScenarioMetadata(allScenarios);
|
|
52
|
+
const generator = new k6_script_generator_1.K6ScriptGenerator();
|
|
53
|
+
const config = {
|
|
54
|
+
language: options.lang,
|
|
55
|
+
featuresDir: options.features,
|
|
56
|
+
outputDir: options.output,
|
|
57
|
+
includeHtmlReporter: true,
|
|
58
|
+
author: "Enyimiri Chetachi Paschal (qaPaschalE)",
|
|
59
|
+
version: packageJson.version,
|
|
60
|
+
};
|
|
61
|
+
const k6Script = generator.generateK6File(allScenarios, metadata, config);
|
|
62
|
+
// Ensure output directory exists
|
|
63
|
+
if (!fs_1.default.existsSync(options.output)) {
|
|
64
|
+
fs_1.default.mkdirSync(options.output, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
const outputFile = `${options.output}/test.generated.${options.lang}`;
|
|
67
|
+
fs_1.default.writeFileSync(outputFile, k6Script);
|
|
68
|
+
console.log(`✅ Generated k6 script: ${outputFile}`);
|
|
69
|
+
console.log(`📋 Scenarios processed: ${allScenarios.length}`);
|
|
70
|
+
}
|
|
71
|
+
commander_1.program.parse();
|
|
72
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.command.d.ts","sourceRoot":"","sources":["../../src/commands/init.command.ts"],"names":[],"mappings":"AAMA,qBAAa,WAAW;IAChB,OAAO,CACX,UAAU,EAAE,MAAM,EAClB,QAAQ,GAAE,IAAI,GAAG,IAAW,GAC3B,OAAO,CAAC,IAAI,CAAC;CA0BjB"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InitCommand = void 0;
|
|
4
|
+
// src/commands/init.command.ts
|
|
5
|
+
const project_generator_1 = require("../generators/project.generator");
|
|
6
|
+
class InitCommand {
|
|
7
|
+
async execute(outputPath, language = "ts") {
|
|
8
|
+
const config = {
|
|
9
|
+
language,
|
|
10
|
+
featuresDir: "features",
|
|
11
|
+
outputDir: "generated",
|
|
12
|
+
includeHtmlReporter: true,
|
|
13
|
+
author: "Enyimiri Chetachi Paschal (qaPaschalE)",
|
|
14
|
+
version: require("../../package.json").version, // Get from your main package.json
|
|
15
|
+
};
|
|
16
|
+
console.log(`Initializing k6-cucumber-steps project...`);
|
|
17
|
+
console.log(`Language: ${language}`);
|
|
18
|
+
console.log(`Output directory: ${outputPath}`);
|
|
19
|
+
const generator = new project_generator_1.ProjectGenerator();
|
|
20
|
+
generator.generateProjectStructure(config, outputPath);
|
|
21
|
+
console.log(`✅ Project structure generated successfully in: ${outputPath}`);
|
|
22
|
+
console.log(`📋 Next steps:`);
|
|
23
|
+
console.log(` cd ${outputPath}`);
|
|
24
|
+
console.log(` npm install`);
|
|
25
|
+
console.log(` npx k6-cucumber-steps generate -l ${language}`);
|
|
26
|
+
console.log(` npm test`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.InitCommand = InitCommand;
|
|
30
|
+
//# sourceMappingURL=init.command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.command.js","sourceRoot":"","sources":["../../src/commands/init.command.ts"],"names":[],"mappings":";;;AAAA,+BAA+B;AAC/B,uEAAmE;AAKnE,MAAa,WAAW;IACtB,KAAK,CAAC,OAAO,CACX,UAAkB,EAClB,WAAwB,IAAI;QAE5B,MAAM,MAAM,GAAkB;YAC5B,QAAQ;YACR,WAAW,EAAE,UAAU;YACvB,SAAS,EAAE,WAAW;YACtB,mBAAmB,EAAE,IAAI;YACzB,MAAM,EAAE,wCAAwC;YAChD,OAAO,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC,OAAO,EAAE,kCAAkC;SACnF,CAAC;QAEF,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;QACzD,OAAO,CAAC,GAAG,CAAC,aAAa,QAAQ,EAAE,CAAC,CAAC;QACrC,OAAO,CAAC,GAAG,CAAC,qBAAqB,UAAU,EAAE,CAAC,CAAC;QAE/C,MAAM,SAAS,GAAG,IAAI,oCAAgB,EAAE,CAAC;QACzC,SAAS,CAAC,wBAAwB,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QAEvD,OAAO,CAAC,GAAG,CACT,kDAAkD,UAAU,EAAE,CAC/D,CAAC;QACF,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,SAAS,UAAU,EAAE,CAAC,CAAC;QACnC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,wCAAwC,QAAQ,EAAE,CAAC,CAAC;QAChE,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC7B,CAAC;CACF;AA9BD,kCA8BC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FeatureFile, Scenario, ScenarioMetadata } from "../types";
|
|
2
|
+
export declare class FeatureParser {
|
|
3
|
+
loadAndParseFeatures(featuresPath: string): Promise<FeatureFile[]>;
|
|
4
|
+
private parseFeature;
|
|
5
|
+
/**
|
|
6
|
+
* Helper to map Gherkin AST steps to our internal Scenario Step format,
|
|
7
|
+
* specifically handling DataTables and DocStrings.
|
|
8
|
+
*/
|
|
9
|
+
private mapGherkinStepToInternalStep;
|
|
10
|
+
loadScenarioMetadata(scenarios: Scenario[]): ScenarioMetadata[];
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=feature.parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature.parser.d.ts","sourceRoot":"","sources":["../../src/generators/feature.parser.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAQ,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEzE,qBAAa,aAAa;IAClB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IA4BxE,OAAO,CAAC,YAAY;IAyGpB;;;OAGG;IACH,OAAO,CAAC,4BAA4B;IAgCpC,oBAAoB,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,gBAAgB,EAAE;CA4BhE"}
|