@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.
Files changed (29) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +69 -0
  3. package/bin/katalyst-xspec.cjs +54 -0
  4. package/cli/init.cjs +679 -0
  5. package/cli/stubs.cjs +365 -0
  6. package/cli/upgrade.cjs +1014 -0
  7. package/dist/chunk-ACAXOGKZ.js +1611 -0
  8. package/dist/index.d.ts +881 -0
  9. package/dist/index.js +1091 -0
  10. package/dist/steps/index.d.ts +151 -0
  11. package/dist/steps/index.js +50 -0
  12. package/package.json +80 -0
  13. package/scripts/postinstall.cjs +85 -0
  14. package/skills/katalyst-bdd-architecture/SKILL.md +517 -0
  15. package/skills/katalyst-bdd-architecture/references/adapters.md +310 -0
  16. package/skills/katalyst-bdd-architecture/references/custom-steps.md +360 -0
  17. package/skills/katalyst-bdd-architecture/references/ports.md +256 -0
  18. package/skills/katalyst-bdd-create-test/SKILL.md +366 -0
  19. package/skills/katalyst-bdd-create-test/references/api-patterns.md +371 -0
  20. package/skills/katalyst-bdd-create-test/references/hybrid-patterns.md +420 -0
  21. package/skills/katalyst-bdd-create-test/references/tui-patterns.md +458 -0
  22. package/skills/katalyst-bdd-create-test/references/ui-patterns.md +415 -0
  23. package/skills/katalyst-bdd-quickstart/SKILL.md +292 -0
  24. package/skills/katalyst-bdd-step-reference/SKILL.md +147 -0
  25. package/skills/katalyst-bdd-step-reference/references/api-steps.md +247 -0
  26. package/skills/katalyst-bdd-step-reference/references/shared-steps.md +340 -0
  27. package/skills/katalyst-bdd-step-reference/references/tui-steps.md +483 -0
  28. package/skills/katalyst-bdd-step-reference/references/ui-steps.md +521 -0
  29. 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