@esimplicitylabs/katalyst-xspec 0.6.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/LICENSE +7 -0
- package/README.md +69 -0
- package/bin/katalyst-xspec.cjs +54 -0
- package/cli/init.cjs +679 -0
- package/cli/stubs.cjs +365 -0
- package/cli/upgrade.cjs +1014 -0
- package/dist/chunk-ACAXOGKZ.js +1611 -0
- package/dist/index.d.ts +881 -0
- package/dist/index.js +1091 -0
- package/dist/steps/index.d.ts +151 -0
- package/dist/steps/index.js +50 -0
- package/package.json +80 -0
- package/scripts/postinstall.cjs +85 -0
- package/skills/katalyst-bdd-architecture/SKILL.md +517 -0
- package/skills/katalyst-bdd-architecture/references/adapters.md +310 -0
- package/skills/katalyst-bdd-architecture/references/custom-steps.md +360 -0
- package/skills/katalyst-bdd-architecture/references/ports.md +256 -0
- package/skills/katalyst-bdd-create-test/SKILL.md +366 -0
- package/skills/katalyst-bdd-create-test/references/api-patterns.md +371 -0
- package/skills/katalyst-bdd-create-test/references/hybrid-patterns.md +420 -0
- package/skills/katalyst-bdd-create-test/references/tui-patterns.md +458 -0
- package/skills/katalyst-bdd-create-test/references/ui-patterns.md +415 -0
- package/skills/katalyst-bdd-quickstart/SKILL.md +292 -0
- package/skills/katalyst-bdd-step-reference/SKILL.md +147 -0
- package/skills/katalyst-bdd-step-reference/references/api-steps.md +247 -0
- package/skills/katalyst-bdd-step-reference/references/shared-steps.md +340 -0
- package/skills/katalyst-bdd-step-reference/references/tui-steps.md +483 -0
- package/skills/katalyst-bdd-step-reference/references/ui-steps.md +521 -0
- package/skills/katalyst-bdd-troubleshooting/SKILL.md +449 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# Adapters Reference
|
|
2
|
+
|
|
3
|
+
Complete reference for all built-in adapters in the Katalyst BDD framework.
|
|
4
|
+
|
|
5
|
+
## PlaywrightApiAdapter
|
|
6
|
+
|
|
7
|
+
Implements `ApiPort` using Playwright's `APIRequestContext`.
|
|
8
|
+
|
|
9
|
+
### Constructor
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
class PlaywrightApiAdapter implements ApiPort {
|
|
13
|
+
constructor(private readonly request: APIRequestContext) {}
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Configuration
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// In fixtures.ts
|
|
21
|
+
import { createBddTest, PlaywrightApiAdapter } from '@esimplicitylabs/katalyst-xspec';
|
|
22
|
+
|
|
23
|
+
const test = createBddTest({
|
|
24
|
+
createApi: ({ apiRequest }) => new PlaywrightApiAdapter(apiRequest),
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Environment Variables
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
API_BASE_URL=http://localhost:3000
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Features
|
|
35
|
+
|
|
36
|
+
- Automatic JSON serialization/deserialization
|
|
37
|
+
- Header management via World state
|
|
38
|
+
- Response capture for assertions
|
|
39
|
+
|
|
40
|
+
## PlaywrightUiAdapter
|
|
41
|
+
|
|
42
|
+
Implements `UiPort` using Playwright's `Page`.
|
|
43
|
+
|
|
44
|
+
### Constructor
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
class PlaywrightUiAdapter implements UiPort {
|
|
48
|
+
constructor(private readonly page: Page) {}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Configuration
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { createBddTest, PlaywrightUiAdapter } from '@esimplicitylabs/katalyst-xspec';
|
|
56
|
+
|
|
57
|
+
const test = createBddTest({
|
|
58
|
+
createUi: ({ page }) => new PlaywrightUiAdapter(page),
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Environment Variables
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
FRONTEND_URL=http://localhost:3000
|
|
66
|
+
BASE_URL=http://localhost:3000
|
|
67
|
+
HEADLESS=true
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Features
|
|
71
|
+
|
|
72
|
+
- Intelligent element location (by role, label, text, etc.)
|
|
73
|
+
- Automatic waiting for elements
|
|
74
|
+
- Multiple click modes (normal, force, dispatch)
|
|
75
|
+
- Screenshot and debugging support
|
|
76
|
+
|
|
77
|
+
## TuiTesterAdapter
|
|
78
|
+
|
|
79
|
+
Implements `TuiPort` using the `tui-tester` library (wraps tmux).
|
|
80
|
+
|
|
81
|
+
### Constructor
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
class TuiTesterAdapter implements TuiPort {
|
|
85
|
+
constructor(config: TuiConfig) {}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type TuiConfig = {
|
|
89
|
+
command: string[]; // Command to run
|
|
90
|
+
size?: { cols: number; rows: number }; // Terminal size
|
|
91
|
+
cwd?: string; // Working directory
|
|
92
|
+
env?: Record<string, string>; // Environment variables
|
|
93
|
+
debug?: boolean; // Enable debug output
|
|
94
|
+
snapshotDir?: string; // Snapshot directory
|
|
95
|
+
shell?: string; // Shell to use
|
|
96
|
+
};
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Configuration
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { createBddTest, TuiTesterAdapter } from '@esimplicitylabs/katalyst-xspec';
|
|
103
|
+
|
|
104
|
+
const test = createBddTest({
|
|
105
|
+
createTui: () => new TuiTesterAdapter({
|
|
106
|
+
command: ['node', 'dist/cli.js'],
|
|
107
|
+
size: { cols: 100, rows: 30 },
|
|
108
|
+
cwd: process.cwd(),
|
|
109
|
+
env: { NODE_ENV: 'test' },
|
|
110
|
+
debug: false,
|
|
111
|
+
snapshotDir: './snapshots',
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Prerequisites
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# macOS
|
|
120
|
+
brew install tmux
|
|
121
|
+
|
|
122
|
+
# Ubuntu/Debian
|
|
123
|
+
sudo apt-get install tmux
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Environment Variables
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
TUI_COLS=80
|
|
130
|
+
TUI_ROWS=24
|
|
131
|
+
DEBUG=false
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Features
|
|
135
|
+
|
|
136
|
+
- Full terminal emulation
|
|
137
|
+
- Keyboard input (including modifiers)
|
|
138
|
+
- Screen capture and assertions
|
|
139
|
+
- Snapshot testing
|
|
140
|
+
- Mouse support (for TUI apps that support it)
|
|
141
|
+
|
|
142
|
+
## UniversalAuthAdapter
|
|
143
|
+
|
|
144
|
+
Implements `AuthPort` for both API and UI authentication.
|
|
145
|
+
|
|
146
|
+
### Constructor
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
class UniversalAuthAdapter implements AuthPort {
|
|
150
|
+
constructor(private readonly deps: { api: ApiPort; ui: UiPort }) {}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Configuration
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { createBddTest, UniversalAuthAdapter } from '@esimplicitylabs/katalyst-xspec';
|
|
158
|
+
|
|
159
|
+
const test = createBddTest({
|
|
160
|
+
createAuth: ({ api, ui }) => new UniversalAuthAdapter({ api, ui }),
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Environment Variables
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Admin credentials
|
|
168
|
+
DEFAULT_ADMIN_USERNAME=admin@example.com
|
|
169
|
+
DEFAULT_ADMIN_PASSWORD=admin123
|
|
170
|
+
|
|
171
|
+
# User credentials
|
|
172
|
+
DEFAULT_USER_USERNAME=user@example.com
|
|
173
|
+
DEFAULT_USER_PASSWORD=user123
|
|
174
|
+
|
|
175
|
+
# Auth endpoint
|
|
176
|
+
API_AUTH_LOGIN_PATH=/auth/login
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Behavior
|
|
180
|
+
|
|
181
|
+
**API Authentication:**
|
|
182
|
+
1. POSTs to `API_AUTH_LOGIN_PATH`
|
|
183
|
+
2. Stores token in `world.headers['Authorization']`
|
|
184
|
+
3. If credentials not set, skips silently with `console.warn`
|
|
185
|
+
|
|
186
|
+
**UI Authentication:**
|
|
187
|
+
1. Navigates to `UI_LOGIN_PATH` (default: `/login`)
|
|
188
|
+
2. Fills fields by placeholder (configurable via `UI_USERNAME_FIELD`, `UI_PASSWORD_FIELD`)
|
|
189
|
+
3. Clicks login button (configurable via `UI_LOGIN_BUTTON`)
|
|
190
|
+
4. If credentials not set, skips silently with `console.warn`
|
|
191
|
+
|
|
192
|
+
> **Note:** No hardcoded default credentials are used. All credentials must be set via env vars.
|
|
193
|
+
|
|
194
|
+
## DefaultCleanupAdapter
|
|
195
|
+
|
|
196
|
+
Implements `CleanupPort` for automatic resource cleanup.
|
|
197
|
+
|
|
198
|
+
### Constructor
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
class DefaultCleanupAdapter implements CleanupPort {
|
|
202
|
+
constructor(input?: {
|
|
203
|
+
rules?: CleanupRule[];
|
|
204
|
+
allowHeuristic?: boolean;
|
|
205
|
+
}) {}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
type CleanupRule = {
|
|
209
|
+
varMatch: string; // Variable name pattern
|
|
210
|
+
method?: 'DELETE' | 'POST' | 'PATCH' | 'PUT'; // Default: DELETE
|
|
211
|
+
path: string; // Cleanup path (with {id} placeholder)
|
|
212
|
+
body?: unknown; // Optional request body
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Configuration
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { createBddTest, DefaultCleanupAdapter } from '@esimplicitylabs/katalyst-xspec';
|
|
220
|
+
|
|
221
|
+
const test = createBddTest({
|
|
222
|
+
createCleanup: () => new DefaultCleanupAdapter({
|
|
223
|
+
rules: [
|
|
224
|
+
{ varMatch: 'userId', path: '/admin/users/{id}' },
|
|
225
|
+
{ varMatch: 'projectId', path: '/projects/{id}' },
|
|
226
|
+
],
|
|
227
|
+
allowHeuristic: true,
|
|
228
|
+
}),
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Environment Variables
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
# JSON array of cleanup rules (no built-in rules -- consumers must define their own)
|
|
236
|
+
CLEANUP_RULES='[{"varMatch":"userId","path":"/api/users/{id}"}]'
|
|
237
|
+
|
|
238
|
+
# Allow heuristic matching
|
|
239
|
+
CLEANUP_ALLOW_ALL=false
|
|
240
|
+
|
|
241
|
+
# Static auth token for cleanup (alternative to login-based auth)
|
|
242
|
+
CLEANUP_AUTH_TOKEN=your-admin-token
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Behavior
|
|
246
|
+
|
|
247
|
+
1. Matches variable names against rules (from `CLEANUP_RULES` env var or constructor)
|
|
248
|
+
2. At test teardown, executes cleanup requests (DELETE by default)
|
|
249
|
+
3. Authenticates via `getCleanupAuth` (configurable on `createBddTest`)
|
|
250
|
+
4. Recognizes UUIDs, prefixed IDs, numeric IDs, MongoDB ObjectIDs, CUIDs, and ULIDs
|
|
251
|
+
|
|
252
|
+
### Heuristic Matching
|
|
253
|
+
|
|
254
|
+
When `allowHeuristic` is true, the adapter guesses cleanup paths:
|
|
255
|
+
- `userId` → DELETE `/users/{id}`
|
|
256
|
+
- `projectId` → DELETE `/projects/{id}`
|
|
257
|
+
|
|
258
|
+
## FetchInterceptAuthAdapter
|
|
259
|
+
|
|
260
|
+
Helper for UI authentication via fetch request interception.
|
|
261
|
+
|
|
262
|
+
### Functions
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// Setup fetch interception with auth data
|
|
266
|
+
async function setupFetchIntercept(
|
|
267
|
+
page: Page,
|
|
268
|
+
authData: AuthData,
|
|
269
|
+
config?: InterceptConfig
|
|
270
|
+
): Promise<void>;
|
|
271
|
+
|
|
272
|
+
// Bypass auth with specific user
|
|
273
|
+
async function setupBypassAuth(
|
|
274
|
+
page: Page,
|
|
275
|
+
userId: string,
|
|
276
|
+
roles: string[],
|
|
277
|
+
tenantId?: string
|
|
278
|
+
): Promise<void>;
|
|
279
|
+
|
|
280
|
+
// Use bearer token
|
|
281
|
+
async function setupBearerAuth(
|
|
282
|
+
page: Page,
|
|
283
|
+
token: string
|
|
284
|
+
): Promise<void>;
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Usage
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// In a custom auth adapter
|
|
291
|
+
class CustomUiAuthAdapter implements AuthPort {
|
|
292
|
+
async uiLoginAsAdmin(world: World): Promise<void> {
|
|
293
|
+
await setupBypassAuth(this.page, 'admin-id', ['admin']);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### How It Works
|
|
299
|
+
|
|
300
|
+
1. Injects a script via `page.addInitScript()`
|
|
301
|
+
2. Intercepts all fetch requests
|
|
302
|
+
3. Adds authentication headers automatically
|
|
303
|
+
4. Works without actual login flow
|
|
304
|
+
|
|
305
|
+
### Benefits
|
|
306
|
+
|
|
307
|
+
- Faster tests (no login page interaction)
|
|
308
|
+
- Test protected pages directly
|
|
309
|
+
- Switch users mid-test
|
|
310
|
+
- Test role-based access
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# Custom Steps Guide
|
|
2
|
+
|
|
3
|
+
How to create custom step definitions for the Katalyst BDD framework.
|
|
4
|
+
|
|
5
|
+
## Basic Step Structure
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { Given, When, Then } from '@cucumber/cucumber';
|
|
9
|
+
|
|
10
|
+
When('I do something', async ({ world }) => {
|
|
11
|
+
// Step implementation
|
|
12
|
+
});
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Step with Parameters
|
|
16
|
+
|
|
17
|
+
### String Parameter
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
When('I click the {string} button', async ({ ui }, buttonName: string) => {
|
|
21
|
+
await ui.clickButton(buttonName);
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Usage:**
|
|
26
|
+
```gherkin
|
|
27
|
+
When I click the "Submit" button
|
|
28
|
+
When I click the "Cancel" button
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Integer Parameter
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
Then('the response status should be {int}', async ({ world }, status: number) => {
|
|
35
|
+
expect(world.lastStatus).toBe(status);
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Usage:**
|
|
40
|
+
```gherkin
|
|
41
|
+
Then the response status should be 200
|
|
42
|
+
Then the response status should be 404
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Multiple Parameters
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
When('I fill {string} with {string}', async ({ ui }, field: string, value: string) => {
|
|
49
|
+
await ui.fillLabel(field, value);
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Usage:**
|
|
54
|
+
```gherkin
|
|
55
|
+
When I fill "Email" with "test@example.com"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Step with Doc String
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
When('I POST {string} with JSON body:', async ({ api, world }, path: string, docString: string) => {
|
|
62
|
+
const body = JSON.parse(docString);
|
|
63
|
+
const result = await api.sendJson('POST', path, body, world.headers);
|
|
64
|
+
world.lastStatus = result.status;
|
|
65
|
+
world.lastJson = result.json;
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Usage:**
|
|
70
|
+
```gherkin
|
|
71
|
+
When I POST "/users" with JSON body:
|
|
72
|
+
"""
|
|
73
|
+
{
|
|
74
|
+
"name": "Test User",
|
|
75
|
+
"email": "test@example.com"
|
|
76
|
+
}
|
|
77
|
+
"""
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Step with Data Table
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { DataTable } from '@cucumber/cucumber';
|
|
84
|
+
|
|
85
|
+
When('I fill the form:', async ({ ui }, dataTable: DataTable) => {
|
|
86
|
+
const rows = dataTable.hashes();
|
|
87
|
+
// rows = [{ Field: 'Email', Value: 'test@...' }, ...]
|
|
88
|
+
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
await ui.fillLabel(row.Field, row.Value);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Usage:**
|
|
96
|
+
```gherkin
|
|
97
|
+
When I fill the form:
|
|
98
|
+
| Field | Value |
|
|
99
|
+
| Email | test@example.com |
|
|
100
|
+
| Password | secret123 |
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Data Table Methods
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Get as array of hashes (row objects)
|
|
107
|
+
const rows = dataTable.hashes();
|
|
108
|
+
// [{ Field: 'Email', Value: '...' }, { Field: 'Password', Value: '...' }]
|
|
109
|
+
|
|
110
|
+
// Get raw 2D array
|
|
111
|
+
const raw = dataTable.raw();
|
|
112
|
+
// [['Field', 'Value'], ['Email', '...'], ['Password', '...']]
|
|
113
|
+
|
|
114
|
+
// Get rows as arrays (without header)
|
|
115
|
+
const rowsArray = dataTable.rows();
|
|
116
|
+
// [['Email', '...'], ['Password', '...']]
|
|
117
|
+
|
|
118
|
+
// Get as key-value pairs (2 column table)
|
|
119
|
+
const pairs = dataTable.rowsHash();
|
|
120
|
+
// { Email: '...', Password: '...' }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Tag-Restricted Steps
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// Only available in @api scenarios
|
|
127
|
+
When('I make an API call', { tags: '@api' }, async ({ api }) => {
|
|
128
|
+
// ...
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Available in @api or @hybrid
|
|
132
|
+
When('I GET {string}', { tags: '@api or @hybrid' }, async ({ api, world }, path) => {
|
|
133
|
+
// ...
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Only available in @ui
|
|
137
|
+
When('I click something', { tags: '@ui' }, async ({ ui }) => {
|
|
138
|
+
// ...
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Available everywhere (no tag restriction)
|
|
142
|
+
Given('I set variable {string} to {string}', async ({ world }, name, value) => {
|
|
143
|
+
world.vars[name] = value;
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Available Fixtures
|
|
148
|
+
|
|
149
|
+
Steps receive these fixtures:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
When('my step', async (fixtures) => {
|
|
153
|
+
const {
|
|
154
|
+
world, // World state object
|
|
155
|
+
api, // ApiPort adapter
|
|
156
|
+
ui, // UiPort adapter
|
|
157
|
+
tui, // TuiPort adapter (if configured)
|
|
158
|
+
auth, // AuthPort adapter
|
|
159
|
+
cleanup, // CleanupPort adapter
|
|
160
|
+
page, // Playwright Page (raw)
|
|
161
|
+
apiRequest, // Playwright APIRequestContext (raw)
|
|
162
|
+
} = fixtures;
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Using World State
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Store a variable
|
|
170
|
+
Given('I set {string} to {string}', async ({ world }, name, value) => {
|
|
171
|
+
world.vars[name] = value;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Read a variable
|
|
175
|
+
When('I use the variable {string}', async ({ world }, name) => {
|
|
176
|
+
const value = world.vars[name];
|
|
177
|
+
// Use value...
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Store API response
|
|
181
|
+
When('I make request', async ({ api, world }) => {
|
|
182
|
+
const result = await api.sendJson('GET', '/endpoint');
|
|
183
|
+
world.lastStatus = result.status;
|
|
184
|
+
world.lastJson = result.json;
|
|
185
|
+
world.lastText = result.text;
|
|
186
|
+
world.lastHeaders = result.headers;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Set headers for subsequent requests
|
|
190
|
+
Given('I set auth header', async ({ world }) => {
|
|
191
|
+
world.headers['Authorization'] = 'Bearer token';
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Variable Interpolation
|
|
196
|
+
|
|
197
|
+
Use the `interpolate` utility:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { interpolate } from '@esimplicitylabs/katalyst-xspec';
|
|
201
|
+
|
|
202
|
+
When('I GET {string}', async ({ api, world }, path) => {
|
|
203
|
+
// Replaces {varName} with world.vars values
|
|
204
|
+
const interpolatedPath = interpolate(path, world.vars);
|
|
205
|
+
await api.sendJson('GET', interpolatedPath);
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Registering Steps
|
|
210
|
+
|
|
211
|
+
### Register Individual Categories
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// steps.ts
|
|
215
|
+
import { test } from './fixtures';
|
|
216
|
+
import {
|
|
217
|
+
registerApiSteps,
|
|
218
|
+
registerUiSteps,
|
|
219
|
+
registerTuiSteps,
|
|
220
|
+
registerSharedSteps,
|
|
221
|
+
registerHybridSuite,
|
|
222
|
+
} from '@esimplicitylabs/katalyst-xspec/steps';
|
|
223
|
+
|
|
224
|
+
// Register specific categories
|
|
225
|
+
registerApiSteps(test);
|
|
226
|
+
registerUiSteps(test);
|
|
227
|
+
registerSharedSteps(test);
|
|
228
|
+
|
|
229
|
+
export { test };
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Register All Steps
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { test } from './fixtures';
|
|
236
|
+
import { registerAllSteps } from '@esimplicitylabs/katalyst-xspec/steps';
|
|
237
|
+
|
|
238
|
+
registerAllSteps(test);
|
|
239
|
+
|
|
240
|
+
export { test };
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Register Custom Steps
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// custom-steps.ts
|
|
247
|
+
import { test } from './fixtures';
|
|
248
|
+
import { Given, When, Then } from '@cucumber/cucumber';
|
|
249
|
+
|
|
250
|
+
// Define custom steps
|
|
251
|
+
When('I do my custom thing', async ({ world }) => {
|
|
252
|
+
// Implementation
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
Given('I have custom setup', async ({ api }) => {
|
|
256
|
+
// Implementation
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Import in steps.ts
|
|
260
|
+
export { test };
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Step Organization
|
|
264
|
+
|
|
265
|
+
Recommended file structure:
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
features/steps/
|
|
269
|
+
├── fixtures.ts # Adapter configuration
|
|
270
|
+
├── steps.ts # Main step registration
|
|
271
|
+
└── custom/
|
|
272
|
+
├── auth.steps.ts # Custom auth steps
|
|
273
|
+
├── data.steps.ts # Data setup steps
|
|
274
|
+
└── verify.steps.ts # Custom verification steps
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Example: Complete Custom Step File
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// features/steps/custom/reporting.steps.ts
|
|
281
|
+
import { When, Then } from '@cucumber/cucumber';
|
|
282
|
+
import { expect } from '@playwright/test';
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Custom steps for report generation testing
|
|
286
|
+
*/
|
|
287
|
+
|
|
288
|
+
When('I generate a {string} report', { tags: '@api or @hybrid' },
|
|
289
|
+
async ({ api, world }, reportType: string) => {
|
|
290
|
+
const result = await api.sendJson('POST', '/reports/generate', {
|
|
291
|
+
type: reportType,
|
|
292
|
+
format: 'pdf',
|
|
293
|
+
}, world.headers);
|
|
294
|
+
|
|
295
|
+
world.lastStatus = result.status;
|
|
296
|
+
world.lastJson = result.json;
|
|
297
|
+
|
|
298
|
+
if (result.json?.reportId) {
|
|
299
|
+
world.vars['reportId'] = result.json.reportId;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
When('I wait for the report to complete', { tags: '@api or @hybrid' },
|
|
305
|
+
async ({ api, world }) => {
|
|
306
|
+
const reportId = world.vars['reportId'];
|
|
307
|
+
let attempts = 0;
|
|
308
|
+
const maxAttempts = 30;
|
|
309
|
+
|
|
310
|
+
while (attempts < maxAttempts) {
|
|
311
|
+
const result = await api.sendJson('GET', `/reports/${reportId}`, undefined, world.headers);
|
|
312
|
+
|
|
313
|
+
if (result.json?.status === 'completed') {
|
|
314
|
+
world.vars['reportUrl'] = result.json.downloadUrl;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
319
|
+
attempts++;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
throw new Error('Report generation timed out');
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
Then('the report should be downloadable', { tags: '@api or @hybrid' },
|
|
327
|
+
async ({ api, world }) => {
|
|
328
|
+
const reportUrl = world.vars['reportUrl'];
|
|
329
|
+
expect(reportUrl).toBeDefined();
|
|
330
|
+
|
|
331
|
+
const result = await api.sendJson('GET', reportUrl, undefined, world.headers);
|
|
332
|
+
expect(result.status).toBe(200);
|
|
333
|
+
expect(result.contentType).toContain('application/pdf');
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
When('I view the report in the UI', { tags: '@ui or @hybrid' },
|
|
338
|
+
async ({ ui, world }) => {
|
|
339
|
+
const reportId = world.vars['reportId'];
|
|
340
|
+
await ui.goto(`/reports/${reportId}`);
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
Then('I should see the report preview', { tags: '@ui or @hybrid' },
|
|
345
|
+
async ({ ui }) => {
|
|
346
|
+
await ui.expectText('Report Preview');
|
|
347
|
+
await ui.expectElementState('first', 'pdf-viewer', 'locator', 'visible');
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Best Practices
|
|
353
|
+
|
|
354
|
+
1. **Keep steps focused** - Each step should do one thing
|
|
355
|
+
2. **Use meaningful names** - Steps should read like English
|
|
356
|
+
3. **Handle errors gracefully** - Provide helpful error messages
|
|
357
|
+
4. **Use World for state** - Don't use module-level variables
|
|
358
|
+
5. **Tag appropriately** - Restrict steps to relevant test types
|
|
359
|
+
6. **Document complex steps** - Add JSDoc comments
|
|
360
|
+
7. **Reuse existing steps** - Don't duplicate functionality
|