k6-cucumber-steps 2.0.6 โ 2.0.8-alpha.0
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 +300 -9
- package/dist/cli.js +17 -8
- package/dist/cli.js.map +1 -1
- package/dist/generators/k6-script.generator.d.ts.map +1 -1
- package/dist/generators/k6-script.generator.js +53 -35
- package/dist/generators/k6-script.generator.js.map +1 -1
- package/dist/generators/project.generator.d.ts +1 -4
- package/dist/generators/project.generator.d.ts.map +1 -1
- package/dist/generators/project.generator.js +66 -926
- package/dist/generators/project.generator.js.map +1 -1
- package/dist/generators/samples/sample-features.generator.d.ts +7 -0
- package/dist/generators/samples/sample-features.generator.d.ts.map +1 -0
- package/dist/generators/samples/sample-features.generator.js +196 -0
- package/dist/generators/samples/sample-features.generator.js.map +1 -0
- package/dist/generators/samples/sample-steps.generator.d.ts +6 -0
- package/dist/generators/samples/sample-steps.generator.d.ts.map +1 -0
- package/dist/generators/samples/sample-steps.generator.js +1183 -0
- package/dist/generators/samples/sample-steps.generator.js.map +1 -0
- package/dist/generators/samples/steps-metadata.generator.d.ts +12 -0
- package/dist/generators/samples/steps-metadata.generator.d.ts.map +1 -0
- package/dist/generators/samples/steps-metadata.generator.js +502 -0
- package/dist/generators/samples/steps-metadata.generator.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/metadata.json +645 -0
- package/package.json +10 -6
- package/payload.json.example +15 -0
package/README.md
CHANGED
|
@@ -16,17 +16,57 @@ Run [k6](https://k6.io/) performance/load tests using [Cucumber](https://cucumbe
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## ๐ Step Definitions Documentation
|
|
20
|
+
|
|
21
|
+
All step definitions are fully documented and available in multiple formats:
|
|
22
|
+
|
|
23
|
+
### 1. TypeDoc API Documentation
|
|
24
|
+
Interactive HTML documentation with search and navigation:
|
|
25
|
+
|
|
26
|
+
**Online:** [View TypeDoc Documentation](https://qapaschale.github.io/k6-cucumber-steps/docs/)
|
|
27
|
+
|
|
28
|
+
**Locally:**
|
|
29
|
+
```bash
|
|
30
|
+
# Generate documentation
|
|
31
|
+
npm run docs
|
|
32
|
+
|
|
33
|
+
# View in browser
|
|
34
|
+
npm run docs:serve
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Step Metadata (JSON)
|
|
38
|
+
When you initialize a project, a `steps/metadata.json` file is generated containing:
|
|
39
|
+
- All available step patterns
|
|
40
|
+
- Function names and parameters
|
|
41
|
+
- Categories (HTTP, Browser, Assertions, etc.)
|
|
42
|
+
- Descriptions for each step
|
|
43
|
+
|
|
44
|
+
### 3. TypeScript Type Definitions
|
|
45
|
+
The `src/steps.d.ts` file provides:
|
|
46
|
+
- Full TypeScript type definitions
|
|
47
|
+
- JSDoc comments with examples
|
|
48
|
+
- IntelliSense support in IDEs
|
|
49
|
+
|
|
50
|
+
### 4. README Reference
|
|
51
|
+
See the [Step Definitions Reference](#step-definitions-reference) section below for common usage examples.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
19
55
|
## โจ Features
|
|
20
56
|
|
|
21
57
|
- โ
Cucumber + Gherkin for writing k6 tests
|
|
22
58
|
to generate JSON and HTML reports.
|
|
23
59
|
- โ
Flexible configuration through Cucumber data tables.
|
|
24
60
|
- โ
Support for JSON body parsing and escaping
|
|
25
|
-
- โ
Dynamic request body generation using
|
|
61
|
+
- โ
**Environment Variable Support**: Dynamic request body generation using `{{VARIABLE_NAME}}` placeholders
|
|
62
|
+
- โ
**Enhanced Auth Storage**: Store tokens with aliases for cross-scenario reuse
|
|
26
63
|
- โ
`.env` + `K6.env`-style variable resolution (`{{API_KEY}}`)
|
|
27
64
|
- โ
Support for headers, query params, stages
|
|
28
65
|
- โ
Supports multiple authentication types: API key, Bearer token, Basic Auth, and No Auth.
|
|
29
|
-
|
|
66
|
+
- โ
**Extended HTTP Methods**: GET, POST, PUT, PATCH with body support
|
|
67
|
+
- โ
**Response Time Assertions**: Validate API performance with millisecond/second thresholds
|
|
68
|
+
- โ
**Property Validation**: Deep nested property checks, boolean assertions, empty checks
|
|
69
|
+
- โ
**Alias System**: Store and compare response values across scenarios
|
|
30
70
|
- โ
Clean-up of temporary k6 files after execution
|
|
31
71
|
- โ
Built-in support for **distributed load testing** with stages
|
|
32
72
|
- โ
TypeScript-first ๐งก
|
|
@@ -39,6 +79,87 @@ Run [k6](https://k6.io/) performance/load tests using [Cucumber](https://cucumbe
|
|
|
39
79
|
- ๐ **JS & TS Support**: Generate your project in pure JavaScript or TypeScript.
|
|
40
80
|
- ๐ **Metric Segmentation**: Scenarios are wrapped in k6 `group()` blocks for cleaner reporting.
|
|
41
81
|
|
|
82
|
+
## ๐ What's New in v2.0.8
|
|
83
|
+
|
|
84
|
+
### โ ๏ธ Breaking Change: Step Definition Prefix
|
|
85
|
+
|
|
86
|
+
All step definitions now use the **`k6` prefix** to avoid conflicts with other step libraries and make it clear these are k6-specific steps.
|
|
87
|
+
|
|
88
|
+
**Before:**
|
|
89
|
+
```gherkin
|
|
90
|
+
Given the base URL is "{{API_BASE_URL}}"
|
|
91
|
+
When I make a GET request to "/users/1"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**After:**
|
|
95
|
+
```gherkin
|
|
96
|
+
Given the k6 base URL is "{{API_BASE_URL}}"
|
|
97
|
+
When I k6 make a GET request to "/users/1"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### ๐ Environment Variable Support
|
|
101
|
+
|
|
102
|
+
Replace placeholders in your tests using `{{VARIABLE_NAME}}` syntax. Values are resolved from:
|
|
103
|
+
- `__ENV` (k6 environment variables)
|
|
104
|
+
- `K6_*` prefixed variables
|
|
105
|
+
- Global context variables
|
|
106
|
+
|
|
107
|
+
```gherkin
|
|
108
|
+
Background:
|
|
109
|
+
Given the k6 base URL is "{{API_BASE_URL}}"
|
|
110
|
+
|
|
111
|
+
Scenario: Login with environment credentials
|
|
112
|
+
When I k6 authenticate with the following url and request body as "user":
|
|
113
|
+
| endpoint | userName | password |
|
|
114
|
+
| /login | {{TEST_USER_USERNAME}} | {{TEST_USER_PASSWORD}} |
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### ๐ Enhanced Alias System
|
|
118
|
+
|
|
119
|
+
Store response data with custom aliases and reuse across scenarios:
|
|
120
|
+
|
|
121
|
+
```gherkin
|
|
122
|
+
Scenario: Store and reuse values
|
|
123
|
+
When I k6 make a POST request to "/login"
|
|
124
|
+
And I k6 store response "data.accessToken" as "authToken"
|
|
125
|
+
Then the k6 alias "authToken" should not be empty
|
|
126
|
+
|
|
127
|
+
Scenario: Compare against stored values
|
|
128
|
+
Then the k6 response property "userName" should be alias "expectedUsername"
|
|
129
|
+
And the k6 response property "message" should contain alias "expectedMessage"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### ๐ New Assertion Steps
|
|
133
|
+
|
|
134
|
+
| Step | Description | Example |
|
|
135
|
+
|------|-------------|---------|
|
|
136
|
+
| `theResponsePropertyShouldNotBeEmpty` | Validate property has a value | `Then the k6 response property "data.token" should not be empty` |
|
|
137
|
+
| `theResponsePropertyShouldBeTrue/False` | Boolean assertions | `Then the k6 response property "success" should be true` |
|
|
138
|
+
| `theResponsePropertyShouldHaveProperty` | Check nested properties | `Then the k6 response property "data" should have property "user"` |
|
|
139
|
+
| `theResponseTimeShouldBeLessThan...` | Performance assertions | `Then the k6 response time should be less than "500" milliseconds` |
|
|
140
|
+
| `theAliasShouldNotBeEmpty` | Validate stored aliases | `Then the k6 alias "authToken" should not be empty` |
|
|
141
|
+
| `theAliasShouldBeEqualTo` | Compare alias to value | `Then the k6 alias "username" should be equal to "test_user"` |
|
|
142
|
+
|
|
143
|
+
### ๐ Extended HTTP Support
|
|
144
|
+
|
|
145
|
+
- **PUT requests**: `When I k6 make a PUT request to "/users/1"`
|
|
146
|
+
- **PUT with body**: `When I k6 make a PUT request to "/users/1" with body:`
|
|
147
|
+
- **PATCH requests**: `When I k6 make a PATCH request to "/api/settings"`
|
|
148
|
+
- **PATCH with body**: `When I k6 make a PATCH request to "/api/settings" with body:`
|
|
149
|
+
|
|
150
|
+
### ๐จ๏ธ Debug Helpers
|
|
151
|
+
|
|
152
|
+
```gherkin
|
|
153
|
+
And I k6 print alias "authToken" # Print a specific alias
|
|
154
|
+
And I k6 print all aliases # Print all stored aliases
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### ๐งน Utility Steps
|
|
158
|
+
|
|
159
|
+
```gherkin
|
|
160
|
+
Given I k6 clear auth token # Remove Authorization header
|
|
161
|
+
```
|
|
162
|
+
|
|
42
163
|
## โจ New: Hybrid Performance Testing
|
|
43
164
|
|
|
44
165
|
You can now combine **Protocol-level (HTTP)** load testing and **Browser-level (Web Vitals)** testing in a single Gherkin suite.
|
|
@@ -137,6 +258,54 @@ For projects where you prefer to run single features directly.
|
|
|
137
258
|
|
|
138
259
|
---
|
|
139
260
|
|
|
261
|
+
## ๐ Environment Variables Support
|
|
262
|
+
|
|
263
|
+
The generated project includes `dotenv-cli` for easy environment variable management.
|
|
264
|
+
|
|
265
|
+
### Using .env file
|
|
266
|
+
|
|
267
|
+
1. Create a `.env` file in your project root:
|
|
268
|
+
```bash
|
|
269
|
+
API_BASE_URL=https://api.example.com
|
|
270
|
+
AUTH_BASE_URL=https://auth.example.com
|
|
271
|
+
TEST_USER_USERNAME=myuser
|
|
272
|
+
TEST_USER_PASSWORD=mypassword
|
|
273
|
+
POST_TITLE=My Test Post
|
|
274
|
+
CLIENT_ID=my-client-id
|
|
275
|
+
CLIENT_SECRET=my-secret
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
2. Run tests with environment variables:
|
|
279
|
+
```bash
|
|
280
|
+
# Using dotenv-cli to load .env file
|
|
281
|
+
npx dotenv-cli -- k6 run generated/test.generated.ts
|
|
282
|
+
|
|
283
|
+
# Or add to your package.json scripts:
|
|
284
|
+
"test:env": "dotenv-cli -- k6 run generated/test.generated.ts"
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Using K6_ prefixed variables
|
|
288
|
+
|
|
289
|
+
You can also use `K6_` prefixed environment variables directly:
|
|
290
|
+
```bash
|
|
291
|
+
K6_API_BASE_URL=https://api.example.com k6 run generated/test.generated.ts
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### In your feature files
|
|
295
|
+
|
|
296
|
+
Use `{{VARIABLE_NAME}}` syntax to reference environment variables:
|
|
297
|
+
```gherkin
|
|
298
|
+
Background:
|
|
299
|
+
Given the k6 base URL is "{{API_BASE_URL}}"
|
|
300
|
+
|
|
301
|
+
Scenario: Login with environment credentials
|
|
302
|
+
When I k6 authenticate with the following url and request body as "user":
|
|
303
|
+
| endpoint | userName | password |
|
|
304
|
+
| /login | {{TEST_USER_USERNAME}} | {{TEST_USER_PASSWORD}} |
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
140
309
|
## ๐ Advanced Authentication Flow
|
|
141
310
|
|
|
142
311
|
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.
|
|
@@ -188,14 +357,23 @@ Scenario: Login and Save Session
|
|
|
188
357
|
|
|
189
358
|
```
|
|
190
359
|
|
|
360
|
+
<a name="step-definitions-reference"></a>
|
|
191
361
|
## ๐งผ Step Definitions Reference
|
|
192
362
|
|
|
193
363
|
| Step Example | Layer | Description |
|
|
194
364
|
| ------------------------------------- | ------- | ---------------------------- |
|
|
195
|
-
| `When I make a GET request to "/api"` | API | Standard HTTP request. |
|
|
196
|
-
| `When I
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
365
|
+
| `When I k6 make a GET request to "/api"` | API | Standard HTTP request. |
|
|
366
|
+
| `When I k6 make a POST request to "/api"` | API | Create resource with stored body |
|
|
367
|
+
| `When I k6 make a PUT request to "/api"` | API | Update resource with stored body |
|
|
368
|
+
| `When I k6 make a PATCH request to "/api"` | API | Partial update with stored body |
|
|
369
|
+
| `When I k6 navigate to the "/home" page` | Browser | Opens URL in Chromium. |
|
|
370
|
+
| `And I k6 click the button ".submit"` | Browser | Interacts with DOM elements. |
|
|
371
|
+
| `And I k6 store "path" in "file.json"` | Both | Dynamic data persistence. |
|
|
372
|
+
| `And I k6 store response "data.token" as "authToken"` | API | Store response with alias |
|
|
373
|
+
| `Then the k6 response property "id" should be "123"` | API | Validate property value |
|
|
374
|
+
| `Then the k6 response property "success" should be true` | API | Boolean assertion |
|
|
375
|
+
| `Then the k6 response time should be less than "500" milliseconds` | API | Performance check |
|
|
376
|
+
| `Then the k6 alias "authToken" should not be empty` | API | Validate stored alias |
|
|
199
377
|
|
|
200
378
|
---
|
|
201
379
|
|
|
@@ -216,13 +394,126 @@ Every test run now produces a rich HTML dashboard. Your scenarios are grouped na
|
|
|
216
394
|
### Authentication Steps
|
|
217
395
|
|
|
218
396
|
```gherkin
|
|
219
|
-
When I authenticate with the following url and request body as "standard_user":
|
|
397
|
+
When I k6 authenticate with the following url and request body as "standard_user":
|
|
220
398
|
| endpoint | username | password |
|
|
221
399
|
| /login | paschal_qa | pass123 |
|
|
222
|
-
And I am authenticated as a "standard_user" # Lookups token from memory
|
|
400
|
+
And I k6 am authenticated as a "standard_user" # Lookups token from memory
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Environment Variable Steps
|
|
404
|
+
|
|
405
|
+
```gherkin
|
|
406
|
+
Background:
|
|
407
|
+
Given the k6 base URL is "{{API_BASE_URL}}" # Resolves from __ENV or .env
|
|
408
|
+
|
|
409
|
+
Scenario: Use environment variables in request body
|
|
410
|
+
Given I k6 have the following post data:
|
|
411
|
+
"""
|
|
412
|
+
{
|
|
413
|
+
"username": "{{TEST_USER_USERNAME}}",
|
|
414
|
+
"password": "{{TEST_USER_PASSWORD}}"
|
|
415
|
+
}
|
|
416
|
+
"""
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Payload JSON File Steps
|
|
420
|
+
|
|
421
|
+
Load request body from a JSON file with support for both environment variables and aliases.
|
|
422
|
+
|
|
423
|
+
```gherkin
|
|
424
|
+
Scenario: Use payload.json file with env vars and aliases
|
|
425
|
+
# First, store a token as an alias
|
|
426
|
+
When I k6 authenticate with the following url and request body as "user":
|
|
427
|
+
| endpoint | username | password |
|
|
428
|
+
| /login | testuser | pass123 |
|
|
429
|
+
And I k6 store response "accessToken" as "authToken"
|
|
430
|
+
|
|
431
|
+
# Load payload from file (supports {{VARIABLE_NAME}} and {{alias:NAME}})
|
|
432
|
+
Given I k6 use payload json from file "payload.json"
|
|
433
|
+
When I k6 make a POST request to "/api/users"
|
|
434
|
+
|
|
435
|
+
# Or combine loading and request in one step
|
|
436
|
+
When I k6 make a POST request to "/api/users" with payload from "data/create-user.json"
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**File Resolution Order:**
|
|
440
|
+
1. `data/{fileName}` if exists
|
|
441
|
+
2. `{fileName}` in project root if exists
|
|
442
|
+
3. `payload.json` in project root as fallback
|
|
443
|
+
|
|
444
|
+
**Template Syntax:**
|
|
445
|
+
- `{{VARIABLE_NAME}}` - Replaced with environment variable value
|
|
446
|
+
- `{{alias:NAME}}` - Replaced with stored alias value
|
|
447
|
+
|
|
448
|
+
**Example payload.json:**
|
|
449
|
+
```json
|
|
450
|
+
{
|
|
451
|
+
"title": "{{POST_TITLE}}",
|
|
452
|
+
"author": "{{alias:username}}",
|
|
453
|
+
"token": "{{alias:authToken}}",
|
|
454
|
+
"body": "Content with {{VARIABLE_NAME}} support"
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Alias & Storage Steps
|
|
459
|
+
|
|
460
|
+
```gherkin
|
|
461
|
+
Scenario: Store and reuse values
|
|
462
|
+
When I k6 make a POST request to "/login"
|
|
463
|
+
And I k6 store response "data.accessToken" as "authToken"
|
|
464
|
+
Then the k6 alias "authToken" should not be empty
|
|
465
|
+
|
|
466
|
+
# Compare response against stored alias
|
|
467
|
+
Then the k6 response property "userName" should be alias "expectedUsername"
|
|
468
|
+
|
|
469
|
+
# Debug: print stored values
|
|
470
|
+
And I k6 print alias "authToken"
|
|
471
|
+
And I k6 print all aliases
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Response Assertion Steps
|
|
475
|
+
|
|
476
|
+
```gherkin
|
|
477
|
+
# Property validation
|
|
478
|
+
Then the k6 response property "data.id" should be "123"
|
|
479
|
+
Then the k6 response property "data.token" should not be empty
|
|
480
|
+
Then the k6 response property "success" should be true
|
|
481
|
+
Then the k6 response property "deleted" should be false
|
|
482
|
+
Then the k6 response property "user" should have property "email"
|
|
483
|
+
Then the k6 response property "message" should contain "success"
|
|
484
|
+
|
|
485
|
+
# Performance assertions
|
|
486
|
+
Then the k6 response time should be less than "500" milliseconds
|
|
487
|
+
Then the k6 response time should be less than "2" seconds
|
|
488
|
+
|
|
489
|
+
# Alias comparisons
|
|
490
|
+
Then the k6 alias "authToken" should not be empty
|
|
491
|
+
Then the k6 alias "username" should be equal to "test_user"
|
|
492
|
+
Then the k6 response property "token" should be alias "expectedToken"
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### HTTP Request Steps
|
|
496
|
+
|
|
497
|
+
```gherkin
|
|
498
|
+
# GET requests
|
|
499
|
+
When I k6 make a GET request to "/users/1"
|
|
500
|
+
When I k6 make a GET request to "/users/1" with headers:
|
|
501
|
+
| Authorization | Content-Type |
|
|
502
|
+
| Bearer abc123 | application/json |
|
|
503
|
+
|
|
504
|
+
# POST requests
|
|
505
|
+
When I k6 make a POST request to "/users"
|
|
506
|
+
|
|
507
|
+
# PUT requests
|
|
508
|
+
When I k6 make a PUT request to "/users/1"
|
|
509
|
+
When I k6 make a PUT request to "/users/1" with body:
|
|
510
|
+
|
|
511
|
+
# PATCH requests
|
|
512
|
+
When I k6 make a PATCH request to "/settings"
|
|
513
|
+
When I k6 make a PATCH request to "/settings" with body:
|
|
223
514
|
```
|
|
224
515
|
|
|
225
|
-
###
|
|
516
|
+
### Sample Features
|
|
226
517
|
|
|
227
518
|
```gherkin
|
|
228
519
|
@smoke @vus:10 @duration:1m
|
package/dist/cli.js
CHANGED
|
@@ -30,6 +30,7 @@ commander_1.program
|
|
|
30
30
|
.description("Generate k6 scripts from feature files")
|
|
31
31
|
.option("-f, --features <path>", "Path to feature files", "./features")
|
|
32
32
|
.option("-o, --output <path>", "Output path for generated scripts", "./generated")
|
|
33
|
+
.option("--exclude-tags <tags>", "Exclude scenarios by tags (comma-separated)")
|
|
33
34
|
.option("-l, --lang <language>", "Output language (js or ts)", "ts")
|
|
34
35
|
.option("--tags <tags>", "Filter scenarios by tags")
|
|
35
36
|
.action(async (options) => {
|
|
@@ -38,12 +39,19 @@ commander_1.program
|
|
|
38
39
|
async function generateK6Scripts(options) {
|
|
39
40
|
console.log("Generating k6 scripts from feature files...");
|
|
40
41
|
const parser = new feature_parser_1.FeatureParser();
|
|
41
|
-
const features = await parser.loadAndParseFeatures(options.features);
|
|
42
|
-
//
|
|
42
|
+
const features = await parser.loadAndParseFeatures(options.features || "./features");
|
|
43
|
+
// Include by tags
|
|
43
44
|
if (options.tags) {
|
|
44
|
-
const
|
|
45
|
+
const includeTags = options.tags.split(",").map(t => t.trim()).filter(t => t);
|
|
45
46
|
features.forEach((feature) => {
|
|
46
|
-
feature.scenarios = feature.scenarios.filter((scenario) =>
|
|
47
|
+
feature.scenarios = feature.scenarios.filter((scenario) => includeTags.some(tag => scenario.tags.includes(tag)));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// Exclude by tags
|
|
51
|
+
if (options.excludeTags) {
|
|
52
|
+
const excludeTags = options.excludeTags.split(",").map(t => t.trim()).filter(t => t);
|
|
53
|
+
features.forEach((feature) => {
|
|
54
|
+
feature.scenarios = feature.scenarios.filter((scenario) => !excludeTags.some(tag => scenario.tags.includes(tag)));
|
|
47
55
|
});
|
|
48
56
|
}
|
|
49
57
|
// Flatten all scenarios
|
|
@@ -52,16 +60,17 @@ async function generateK6Scripts(options) {
|
|
|
52
60
|
const generator = new k6_script_generator_1.K6ScriptGenerator();
|
|
53
61
|
const config = {
|
|
54
62
|
language: options.lang,
|
|
55
|
-
featuresDir: options.features,
|
|
56
|
-
outputDir: options.output,
|
|
63
|
+
featuresDir: options.features || "./features",
|
|
64
|
+
outputDir: options.output || "./generated",
|
|
57
65
|
includeHtmlReporter: true,
|
|
58
66
|
author: "Enyimiri Chetachi Paschal (qaPaschalE)",
|
|
59
67
|
version: packageJson.version,
|
|
60
68
|
};
|
|
61
69
|
const k6Script = generator.generateK6File(allScenarios, metadata, config);
|
|
70
|
+
const outputDir = options.output || "./generated";
|
|
62
71
|
// Ensure output directory exists
|
|
63
|
-
if (!fs_1.default.existsSync(
|
|
64
|
-
fs_1.default.mkdirSync(
|
|
72
|
+
if (!fs_1.default.existsSync(outputDir)) {
|
|
73
|
+
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
|
65
74
|
}
|
|
66
75
|
const outputFile = `${options.output}/test.generated.${options.lang}`;
|
|
67
76
|
fs_1.default.writeFileSync(outputFile, k6Script);
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;;;;AACA,aAAa;AACb,yCAAoC;AACpC,0DAAsD;AACtD,gEAA4D;AAC5D,0EAAqE;AAErE,4CAAoB;AAEpB,eAAe;AACf,MAAM,WAAW,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE/C,mBAAO;KACJ,IAAI,CAAC,mBAAmB,CAAC;KACzB,WAAW,CAAC,sDAAsD,CAAC;KACnE,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;AAEhC,mBAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,WAAW,CAAC,sCAAsC,CAAC;KACnD,MAAM,CAAC,uBAAuB,EAAE,iCAAiC,EAAE,IAAI,CAAC;KACxE,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,EAAE,GAAG,CAAC;KAChD,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;IAC9B,MAAM,OAAO,GAAG,IAAI,0BAAW,EAAE,CAAC;IAClC,MAAM,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,IAAmB,CAAC,CAAC;AAC3D,CAAC,CAAC,CAAC;AAEL,mBAAO;KACJ,OAAO,CAAC,UAAU,CAAC;KACnB,WAAW,CAAC,wCAAwC,CAAC;KACrD,MAAM,CAAC,uBAAuB,EAAE,uBAAuB,EAAE,YAAY,CAAC;KACtE,MAAM,CACL,qBAAqB,EACrB,mCAAmC,EACnC,aAAa,CACd;KACA,MAAM,CAAC,uBAAuB,EAAE,4BAA4B,EAAE,IAAI,CAAC;KACnE,MAAM,CAAC,eAAe,EAAE,0BAA0B,CAAC;KACnD,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;IACxB,MAAM,iBAAiB,CAAC,OAAO,CAAC,CAAC;AACnC,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"k6-script.generator.d.ts","sourceRoot":"","sources":["../../src/generators/k6-script.generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"k6-script.generator.d.ts","sourceRoot":"","sources":["../../src/generators/k6-script.generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACrE,qBAAa,iBAAiB;IAC5B,cAAc,CACZ,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,EAAE,gBAAgB,EAAE,EAC5B,MAAM,EAAE,aAAa,GACpB,MAAM;IAiGT,OAAO,CAAC,eAAe;IA6BvB,OAAO,CAAC,eAAe;IA8FvB,OAAO,CAAC,eAAe;IAuBvB,OAAO,CAAC,iBAAiB;IAyBzB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,oBAAoB;CAuG7B"}
|
|
@@ -12,18 +12,16 @@ globalThis.lastResponse = globalThis.lastResponse || {};
|
|
|
12
12
|
`;
|
|
13
13
|
const imports = this.generateImports(config, metadata);
|
|
14
14
|
const options = this.generateOptions(metadata);
|
|
15
|
-
const testFunction = this.generateTestFunction(scenarios, metadata);
|
|
16
|
-
// โ
|
|
15
|
+
const testFunction = this.generateTestFunction(scenarios, metadata, config);
|
|
16
|
+
// โ
Simplified teardown: just return the tokens (no globalThis needed)
|
|
17
17
|
const teardownFn = config.language === "ts"
|
|
18
18
|
? `export function teardown(tokensFromDefault: Record<string, any>) {
|
|
19
|
-
|
|
20
|
-
globalThis.exportedTokens = tokensFromDefault;
|
|
19
|
+
return tokensFromDefault;
|
|
21
20
|
}`
|
|
22
21
|
: `export function teardown(tokensFromDefault) {
|
|
23
|
-
|
|
24
|
-
globalThis.exportedTokens = tokensFromDefault;
|
|
22
|
+
return tokensFromDefault;
|
|
25
23
|
}`;
|
|
26
|
-
// โ
|
|
24
|
+
// โ
handleSummary already uses data.teardown_data โ no change needed
|
|
27
25
|
const handleSummaryFn = config.language === "ts"
|
|
28
26
|
? `export function handleSummary(data: any): Record<string, any> {
|
|
29
27
|
const reports: Record<string, any> = {
|
|
@@ -34,18 +32,18 @@ globalThis.lastResponse = globalThis.lastResponse || {};
|
|
|
34
32
|
|
|
35
33
|
console.log('--- Summary File Write Audit ---');
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
if (data.setup_data && !Object.keys(tokens).length) {
|
|
39
|
-
tokens = data.setup_data;
|
|
40
|
-
}
|
|
41
|
-
|
|
35
|
+
const tokens = data.teardown_data || {};
|
|
42
36
|
const keys = Object.keys(tokens).filter(k => k.endsWith('.json'));
|
|
43
37
|
console.log('Tokens found:', keys.length > 0 ? keys.join(', ') : 'None');
|
|
44
38
|
|
|
45
39
|
for (const [path, tokenValue] of Object.entries(tokens)) {
|
|
46
40
|
if (path.endsWith('.json')) {
|
|
47
41
|
const fullPath = path.startsWith('./') ? path : \`./\${path}\`;
|
|
48
|
-
reports[fullPath] = JSON.stringify(
|
|
42
|
+
reports[fullPath] = JSON.stringify(
|
|
43
|
+
typeof tokenValue === 'string' ? { access_token: tokenValue } : tokenValue,
|
|
44
|
+
null,
|
|
45
|
+
2
|
|
46
|
+
);
|
|
49
47
|
console.log(\`โ
Writing: \${fullPath}\`);
|
|
50
48
|
}
|
|
51
49
|
}
|
|
@@ -61,25 +59,24 @@ globalThis.lastResponse = globalThis.lastResponse || {};
|
|
|
61
59
|
|
|
62
60
|
console.log('--- Summary File Write Audit ---');
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
if (data.setup_data && !Object.keys(tokens).length) {
|
|
66
|
-
tokens = data.setup_data;
|
|
67
|
-
}
|
|
68
|
-
|
|
62
|
+
const tokens = data["root_group::teardown"] || {};
|
|
69
63
|
const keys = Object.keys(tokens).filter(k => k.endsWith('.json'));
|
|
70
64
|
console.log('Tokens found:', keys.length > 0 ? keys.join(', ') : 'None');
|
|
71
65
|
|
|
72
66
|
for (const [path, tokenValue] of Object.entries(tokens)) {
|
|
73
67
|
if (path.endsWith('.json')) {
|
|
74
|
-
|
|
75
|
-
reports[
|
|
76
|
-
|
|
68
|
+
// โ
Use path directly โ no ./ prefix
|
|
69
|
+
reports[path] = JSON.stringify(
|
|
70
|
+
typeof tokenValue === 'string' ? { access_token: tokenValue } : tokenValue,
|
|
71
|
+
null,
|
|
72
|
+
2
|
|
73
|
+
);
|
|
74
|
+
console.log(\`โ
Writing: \${path}\`);
|
|
77
75
|
}
|
|
78
76
|
}
|
|
79
77
|
|
|
80
78
|
return reports;
|
|
81
79
|
}`;
|
|
82
|
-
// โ
Now embed the resolved strings into the final output
|
|
83
80
|
return `
|
|
84
81
|
${header}
|
|
85
82
|
${imports}
|
|
@@ -87,7 +84,6 @@ ${imports}
|
|
|
87
84
|
${options}
|
|
88
85
|
|
|
89
86
|
export function setup() {
|
|
90
|
-
// We must return an object here to initialize the "data" channel
|
|
91
87
|
return { v: Date.now() };
|
|
92
88
|
}
|
|
93
89
|
|
|
@@ -98,19 +94,28 @@ ${teardownFn}
|
|
|
98
94
|
${handleSummaryFn}
|
|
99
95
|
`;
|
|
100
96
|
}
|
|
101
|
-
generateImports(config,
|
|
97
|
+
generateImports(config, meta) {
|
|
102
98
|
const ext = config.language === "ts" ? "ts" : "js";
|
|
103
|
-
const hasBrowser =
|
|
99
|
+
const hasBrowser = meta.some(m => m.tags?.includes("browser")); // โ now used!
|
|
104
100
|
const baseImports = [
|
|
105
101
|
'import http from "k6/http";',
|
|
106
102
|
'import { check, sleep, group } from "k6";',
|
|
107
|
-
'import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js";',
|
|
108
|
-
'import { textSummary } from "https://jslib.k6.io/k6-summary/0.1.0/index.js";',
|
|
109
103
|
];
|
|
104
|
+
// Add @ts-ignore for remote modules in TS mode
|
|
105
|
+
if (config.language === "ts") {
|
|
106
|
+
baseImports.push('// @ts-ignore: k6 resolves remote modules at runtime');
|
|
107
|
+
baseImports.push('import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js";');
|
|
108
|
+
baseImports.push('// @ts-ignore: k6 resolves remote modules at runtime');
|
|
109
|
+
baseImports.push('import { textSummary } from "https://jslib.k6.io/k6-summary/0.1.0/index.js";');
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
baseImports.push('import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js";');
|
|
113
|
+
baseImports.push('import { textSummary } from "https://jslib.k6.io/k6-summary/0.1.0/index.js";');
|
|
114
|
+
}
|
|
110
115
|
if (hasBrowser) {
|
|
111
116
|
baseImports.push('import { browser } from "k6/browser";');
|
|
112
117
|
}
|
|
113
|
-
baseImports.push(`import * as steps from "../steps/sample.steps.${ext}"
|
|
118
|
+
baseImports.push(`import * as steps from "../steps/sample.steps.${ext}";`);
|
|
114
119
|
return baseImports.join("\n");
|
|
115
120
|
}
|
|
116
121
|
generateOptions(metadata) {
|
|
@@ -233,6 +238,14 @@ ${handleSummaryFn}
|
|
|
233
238
|
.replace(/[^a-zA-Z0-9\s]/g, " ")
|
|
234
239
|
.trim();
|
|
235
240
|
const words = clean.split(/\s+/).filter(w => w);
|
|
241
|
+
// Check if the step starts with "k6" or contains "k6" as a keyword
|
|
242
|
+
// If text contains "k6", move it to the front
|
|
243
|
+
const k6Index = words.findIndex(w => w.toLowerCase() === 'k6');
|
|
244
|
+
if (k6Index > 0) {
|
|
245
|
+
// Remove 'k6' from its position and put it at the front
|
|
246
|
+
const k6Word = words.splice(k6Index, 1)[0];
|
|
247
|
+
words.unshift(k6Word);
|
|
248
|
+
}
|
|
236
249
|
return words.map((w, i) => i === 0 ? w.toLowerCase() : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
237
250
|
}
|
|
238
251
|
extractArguments(text) {
|
|
@@ -242,7 +255,7 @@ ${handleSummaryFn}
|
|
|
242
255
|
return matches.join(", ");
|
|
243
256
|
}
|
|
244
257
|
// Inside K6ScriptGenerator.ts
|
|
245
|
-
generateTestFunction(scenarios, metadata) {
|
|
258
|
+
generateTestFunction(scenarios, metadata, config) {
|
|
246
259
|
const hasAnyBrowser = metadata.some((m) => m.tags?.includes("browser"));
|
|
247
260
|
const lines = [];
|
|
248
261
|
// Open browser if any scenario uses it
|
|
@@ -252,7 +265,7 @@ ${handleSummaryFn}
|
|
|
252
265
|
lines.push(` page = await browser.newPage();`);
|
|
253
266
|
lines.push(` console.log('Browser page opened once for all scenarios');`);
|
|
254
267
|
// Set fallback base URL only if needed
|
|
255
|
-
lines.push(` steps.
|
|
268
|
+
lines.push(` steps.k6TheBaseUrlIs("https://demoqa.com");`);
|
|
256
269
|
lines.push(` console.log('Fallback baseUrl set to:', globalThis.baseUrl || 'not set');`);
|
|
257
270
|
lines.push(``);
|
|
258
271
|
}
|
|
@@ -271,7 +284,7 @@ ${handleSummaryFn}
|
|
|
271
284
|
const args = argsStr ? argsStr.split(/,\s*/).filter(Boolean) : [];
|
|
272
285
|
const needsPage = [
|
|
273
286
|
"navigate", "click", "see", "fill", "type", "press", "waitfor", "wait", "locator",
|
|
274
|
-
"select", "title", "url", "element", "shouldsee", "shouldnotsee"
|
|
287
|
+
"select", "title", "url", "element", "shouldsee", "shouldnotsee", "button", "find"
|
|
275
288
|
].some(kw => name.toLowerCase().includes(kw)) &&
|
|
276
289
|
!["thebaseurlis", "thebaseurl"].some(kw => name.toLowerCase().includes(kw));
|
|
277
290
|
const params = [];
|
|
@@ -286,9 +299,12 @@ ${handleSummaryFn}
|
|
|
286
299
|
lines.push(` ${callPrefix}steps.${name}(${params.join(", ")});`);
|
|
287
300
|
});
|
|
288
301
|
lines.push(` } catch (err) {`);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
302
|
+
const stackAccessCode = config.language === "ts"
|
|
303
|
+
? "(err as any)?.stack || 'No stack'"
|
|
304
|
+
: "err?.stack || 'No stack'";
|
|
305
|
+
lines.push(` console.error('Error in ${scenario.name}:', err);`);
|
|
306
|
+
lines.push(` console.error('Stack:', ${stackAccessCode});`);
|
|
307
|
+
lines.push(` }`);
|
|
292
308
|
}
|
|
293
309
|
else {
|
|
294
310
|
// Non-browser (HTTP/API) steps
|
|
@@ -325,7 +341,9 @@ ${handleSummaryFn}
|
|
|
325
341
|
return `
|
|
326
342
|
export default async function () {
|
|
327
343
|
${lines.join("\n")}
|
|
328
|
-
|
|
344
|
+
console.log('๐ Final savedTokens:', JSON.stringify(globalThis.savedTokens));
|
|
345
|
+
|
|
346
|
+
return globalThis.savedTokens || {};
|
|
329
347
|
}`;
|
|
330
348
|
}
|
|
331
349
|
}
|