@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,517 @@
1
+ ---
2
+ name: katalyst-bdd-architecture
3
+ description: Understand and extend the Katalyst BDD framework architecture. Use when creating custom adapters, adding new step definitions, understanding the hexagonal (ports and adapters) pattern, customizing the fixture system, or extending framework functionality.
4
+ ---
5
+
6
+ # Katalyst BDD Architecture Guide
7
+
8
+ This skill explains the framework's hexagonal architecture and how to extend it.
9
+
10
+ ## Architecture Overview
11
+
12
+ The framework uses **Ports and Adapters** (Hexagonal) architecture:
13
+
14
+ ```
15
+ ┌─────────────────────────────────────────────────────────┐
16
+ │ Test Layer │
17
+ │ ┌─────────────────┐ ┌─────────────────┐ │
18
+ │ │ Feature Files │ │ Step Definitions │ │
19
+ │ │ (Gherkin) │ │ (TypeScript) │ │
20
+ │ └────────┬────────┘ └────────┬─────────┘ │
21
+ │ │ │ │
22
+ │ └────────┬───────────┘ │
23
+ │ ▼ │
24
+ │ ┌─────────────────────────────────────────────────┐ │
25
+ │ │ Fixture System │ │
26
+ │ │ (createBddTest + World) │ │
27
+ │ └─────────────────────┬───────────────────────────┘ │
28
+ └────────────────────────┼────────────────────────────────┘
29
+ │
30
+ ┌────────────────────────┼────────────────────────────────┐
31
+ │ Port Layer (Interfaces) │
32
+ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
33
+ │ │ ApiPort │ │ UiPort │ │ TuiPort │ │AuthPort │ ... │
34
+ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
35
+ └───────┼──────────┼──────────┼──────────┼───────────────┘
36
+ │ │ │ │
37
+ ┌───────┼──────────┼──────────┼──────────┼───────────────┐
38
+ │ ▼ ▼ ▼ ▼ │
39
+ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │
40
+ │ │Playwright││Playwright││TuiTester│ │Universal │ │
41
+ │ │ApiAdapter││UiAdapter ││Adapter │ │AuthAdapter │ │
42
+ │ └────┬────┘ └────┬────┘ └────┬────┘ └─────────────┘ │
43
+ │ │ │ │ Adapter Layer │
44
+ └───────┼──────────┼──────────┼──────────────────────────┘
45
+ │ │ │
46
+ ▼ ▼ ▼
47
+ [Playwright] [Playwright] [tui-tester/tmux]
48
+ ```
49
+
50
+ ## Core Concepts
51
+
52
+ ### Ports (Interfaces)
53
+ Ports define **what** operations are available, not **how** they work:
54
+
55
+ ```typescript
56
+ // ApiPort - HTTP API operations
57
+ interface ApiPort {
58
+ sendJson(method: ApiMethod, path: string, body?: unknown): Promise<ApiResult>;
59
+ sendForm(method: string, path: string, form: Record<string, string>): Promise<ApiResult>;
60
+ }
61
+
62
+ // UiPort - Browser UI operations
63
+ interface UiPort {
64
+ goto(path: string): Promise<void>;
65
+ clickButton(name: string): Promise<void>;
66
+ fillLabel(label: string, value: string): Promise<void>;
67
+ expectText(text: string): Promise<void>;
68
+ // ... more methods
69
+ }
70
+
71
+ // TuiPort - Terminal UI operations
72
+ interface TuiPort {
73
+ start(): Promise<void>;
74
+ typeText(text: string): Promise<void>;
75
+ expectText(text: string): Promise<void>;
76
+ // ... more methods
77
+ }
78
+ ```
79
+
80
+ ### Adapters (Implementations)
81
+ Adapters implement ports using specific technologies:
82
+
83
+ ```typescript
84
+ // PlaywrightApiAdapter implements ApiPort using Playwright's APIRequestContext
85
+ class PlaywrightApiAdapter implements ApiPort {
86
+ constructor(private request: APIRequestContext) {}
87
+
88
+ async sendJson(method, path, body) {
89
+ const response = await this.request.fetch(path, { method, data: body });
90
+ // ... process response
91
+ }
92
+ }
93
+
94
+ // PlaywrightUiAdapter implements UiPort using Playwright's Page
95
+ class PlaywrightUiAdapter implements UiPort {
96
+ constructor(private page: Page) {}
97
+
98
+ async goto(path) {
99
+ await this.page.goto(path);
100
+ }
101
+
102
+ async clickButton(name) {
103
+ await this.page.getByRole('button', { name }).click();
104
+ }
105
+ }
106
+ ```
107
+
108
+ ### Benefits
109
+
110
+ 1. **Testability** - Mock ports to unit test step logic
111
+ 2. **Flexibility** - Swap implementations without changing tests
112
+ 3. **Clarity** - Clear separation between what and how
113
+ 4. **Reusability** - Same steps work with different adapters
114
+
115
+ ## World State
116
+
117
+ The World object holds test state:
118
+
119
+ ```typescript
120
+ type World = {
121
+ vars: Record<string, string>; // Test variables
122
+ headers: Record<string, string>; // HTTP headers for requests
123
+ cleanup: CleanupItem[]; // Resources to clean up
124
+ skipCleanup?: boolean; // Whether to skip cleanup
125
+
126
+ // Populated after API calls
127
+ lastResponse?: APIResponse;
128
+ lastStatus?: number;
129
+ lastText?: string;
130
+ lastJson?: unknown;
131
+ lastHeaders?: Record<string, string>;
132
+ lastContentType?: string;
133
+ };
134
+ ```
135
+
136
+ ## The Fixture System
137
+
138
+ ### createBddTest Function
139
+
140
+ The core function that wires everything together:
141
+
142
+ ```typescript
143
+ import { createBddTest } from '@esimplicitylabs/katalyst-xspec';
144
+
145
+ const test = createBddTest({
146
+ // Optional: Override default adapters
147
+ createApi: (ctx) => new PlaywrightApiAdapter(ctx.apiRequest),
148
+ createUi: (ctx) => new PlaywrightUiAdapter(ctx.page),
149
+ createAuth: (ctx) => new UniversalAuthAdapter({ api: ctx.api, ui: ctx.ui }),
150
+ createCleanup: () => new DefaultCleanupAdapter(),
151
+ createTui: () => new TuiTesterAdapter({ command: ['node', 'cli.js'] }),
152
+
153
+ // Optional: Custom world factory
154
+ worldFactory: () => ({
155
+ vars: {},
156
+ headers: {},
157
+ cleanup: [],
158
+ }),
159
+ });
160
+ ```
161
+
162
+ ### Context Available in Factories
163
+
164
+ ```typescript
165
+ createApi: (ctx) => {
166
+ ctx.apiRequest; // Playwright APIRequestContext
167
+ ctx.page; // Playwright Page (for @ui)
168
+ // Return your ApiPort implementation
169
+ }
170
+
171
+ createUi: (ctx) => {
172
+ ctx.page; // Playwright Page
173
+ // Return your UiPort implementation
174
+ }
175
+
176
+ createAuth: (ctx) => {
177
+ ctx.api; // The created ApiPort
178
+ ctx.ui; // The created UiPort
179
+ // Return your AuthPort implementation
180
+ }
181
+ ```
182
+
183
+ ## Creating Custom Steps
184
+
185
+ ### Step Registration
186
+
187
+ ```typescript
188
+ import { Given, When, Then } from '@cucumber/cucumber';
189
+
190
+ // Basic step
191
+ When('I do something with {string}', async ({ world }, param: string) => {
192
+ world.vars['result'] = param;
193
+ });
194
+
195
+ // Step with tag restriction
196
+ When('I make API call', { tags: '@api or @hybrid' }, async ({ api, world }) => {
197
+ const result = await api.sendJson('GET', '/endpoint');
198
+ world.lastJson = result.json;
199
+ });
200
+
201
+ // Step with multiple fixtures
202
+ When('I verify in both layers', { tags: '@hybrid' }, async ({ api, ui, world }) => {
203
+ await api.sendJson('POST', '/data', { value: 'test' });
204
+ await ui.goto('/data');
205
+ await ui.expectText('test');
206
+ });
207
+ ```
208
+
209
+ ### Available Fixtures in Steps
210
+
211
+ ```typescript
212
+ {
213
+ world, // World state object
214
+ api, // ApiPort adapter
215
+ ui, // UiPort adapter
216
+ tui, // TuiPort adapter (if configured)
217
+ auth, // AuthPort adapter
218
+ cleanup, // CleanupPort adapter
219
+ page, // Playwright Page (raw access)
220
+ apiRequest, // Playwright APIRequestContext (raw access)
221
+ }
222
+ ```
223
+
224
+ ### Step with Data Table
225
+
226
+ ```typescript
227
+ When('I fill form with:', async ({ ui }, dataTable: DataTable) => {
228
+ const rows = dataTable.hashes();
229
+ for (const row of rows) {
230
+ await ui.fillLabel(row.Field, row.Value);
231
+ }
232
+ });
233
+ ```
234
+
235
+ ### Step with Doc String
236
+
237
+ ```typescript
238
+ When('I send JSON:', async ({ api, world }, docString: string) => {
239
+ const body = JSON.parse(docString);
240
+ const result = await api.sendJson('POST', '/endpoint', body);
241
+ world.lastJson = result.json;
242
+ });
243
+ ```
244
+
245
+ ## Creating Custom Adapters
246
+
247
+ ### Custom API Adapter
248
+
249
+ ```typescript
250
+ import { ApiPort, ApiResult, ApiMethod } from '@esimplicitylabs/katalyst-xspec';
251
+ import axios from 'axios';
252
+
253
+ class AxiosApiAdapter implements ApiPort {
254
+ private client = axios.create({
255
+ baseURL: process.env.API_BASE_URL,
256
+ });
257
+
258
+ async sendJson(
259
+ method: ApiMethod,
260
+ path: string,
261
+ body?: unknown,
262
+ headers?: Record<string, string>
263
+ ): Promise<ApiResult> {
264
+ try {
265
+ const response = await this.client.request({
266
+ method,
267
+ url: path,
268
+ data: body,
269
+ headers,
270
+ });
271
+
272
+ return {
273
+ status: response.status,
274
+ text: JSON.stringify(response.data),
275
+ json: response.data,
276
+ headers: response.headers as Record<string, string>,
277
+ contentType: response.headers['content-type'],
278
+ response: response as any,
279
+ };
280
+ } catch (error: any) {
281
+ return {
282
+ status: error.response?.status || 500,
283
+ text: error.message,
284
+ json: error.response?.data,
285
+ headers: {},
286
+ response: error.response,
287
+ };
288
+ }
289
+ }
290
+
291
+ async sendForm(
292
+ method: 'POST' | 'PUT' | 'PATCH',
293
+ path: string,
294
+ form: Record<string, string>,
295
+ headers?: Record<string, string>
296
+ ): Promise<ApiResult> {
297
+ return this.sendJson(method, path, form, {
298
+ 'Content-Type': 'application/x-www-form-urlencoded',
299
+ ...headers,
300
+ });
301
+ }
302
+ }
303
+ ```
304
+
305
+ ### Custom Auth Adapter
306
+
307
+ ```typescript
308
+ import { AuthPort, World, ApiPort, UiPort } from '@esimplicitylabs/katalyst-xspec';
309
+
310
+ class CustomAuthAdapter implements AuthPort {
311
+ constructor(private deps: { api: ApiPort; ui: UiPort }) {}
312
+
313
+ async apiLoginAsAdmin(world: World): Promise<void> {
314
+ const result = await this.deps.api.sendJson('POST', '/auth/admin', {
315
+ apiKey: process.env.ADMIN_API_KEY,
316
+ });
317
+
318
+ if (result.json?.token) {
319
+ world.headers['Authorization'] = `Bearer ${result.json.token}`;
320
+ }
321
+ }
322
+
323
+ async apiLoginAsUser(world: World): Promise<void> {
324
+ const result = await this.deps.api.sendJson('POST', '/auth/login', {
325
+ email: process.env.DEFAULT_USER_USERNAME,
326
+ password: process.env.DEFAULT_USER_PASSWORD,
327
+ });
328
+
329
+ if (result.json?.token) {
330
+ world.headers['Authorization'] = `Bearer ${result.json.token}`;
331
+ }
332
+ }
333
+
334
+ async uiLoginAsAdmin(world: World): Promise<void> {
335
+ await this.deps.ui.goto('/admin/login');
336
+ await this.deps.ui.fillLabel('Admin Key', process.env.ADMIN_KEY!);
337
+ await this.deps.ui.clickButton('Login');
338
+ }
339
+
340
+ async uiLoginAsUser(world: World): Promise<void> {
341
+ await this.deps.ui.goto('/login');
342
+ await this.deps.ui.fillLabel('Email', process.env.DEFAULT_USER_USERNAME!);
343
+ await this.deps.ui.fillLabel('Password', process.env.DEFAULT_USER_PASSWORD!);
344
+ await this.deps.ui.clickButton('Sign In');
345
+ }
346
+
347
+ apiSetBearer(world: World, token: string): void {
348
+ world.headers['Authorization'] = `Bearer ${token}`;
349
+ }
350
+ }
351
+ ```
352
+
353
+ ### Using Custom Adapters
354
+
355
+ ```typescript
356
+ // fixtures.ts
357
+ import { createBddTest } from '@esimplicitylabs/katalyst-xspec';
358
+ import { AxiosApiAdapter } from './adapters/axios-api';
359
+ import { CustomAuthAdapter } from './adapters/custom-auth';
360
+
361
+ export const test = createBddTest({
362
+ createApi: () => new AxiosApiAdapter(),
363
+ createAuth: ({ api, ui }) => new CustomAuthAdapter({ api, ui }),
364
+ });
365
+ ```
366
+
367
+ ## Adding New Ports
368
+
369
+ If you need capabilities not covered by existing ports:
370
+
371
+ ### 1. Define the Port Interface
372
+
373
+ ```typescript
374
+ // ports/email.port.ts
375
+ export interface EmailPort {
376
+ sendEmail(to: string, subject: string, body: string): Promise<void>;
377
+ getInbox(address: string): Promise<Email[]>;
378
+ waitForEmail(address: string, subject: string): Promise<Email>;
379
+ }
380
+
381
+ export interface Email {
382
+ from: string;
383
+ to: string;
384
+ subject: string;
385
+ body: string;
386
+ receivedAt: Date;
387
+ }
388
+ ```
389
+
390
+ ### 2. Create an Adapter
391
+
392
+ ```typescript
393
+ // adapters/mailhog-email.adapter.ts
394
+ import { EmailPort, Email } from '../ports/email.port';
395
+
396
+ export class MailhogEmailAdapter implements EmailPort {
397
+ constructor(private baseUrl: string) {}
398
+
399
+ async sendEmail(to: string, subject: string, body: string): Promise<void> {
400
+ // Implementation using Mailhog API
401
+ }
402
+
403
+ async getInbox(address: string): Promise<Email[]> {
404
+ // Implementation
405
+ }
406
+
407
+ async waitForEmail(address: string, subject: string): Promise<Email> {
408
+ // Implementation with polling
409
+ }
410
+ }
411
+ ```
412
+
413
+ ### 3. Add to Fixtures
414
+
415
+ ```typescript
416
+ // fixtures.ts
417
+ import { createBddTest } from '@esimplicitylabs/katalyst-xspec';
418
+ import { MailhogEmailAdapter } from './adapters/mailhog-email';
419
+
420
+ // Extend the test fixture
421
+ const baseTest = createBddTest();
422
+
423
+ export const test = baseTest.extend({
424
+ email: async ({}, use) => {
425
+ const adapter = new MailhogEmailAdapter(process.env.MAILHOG_URL!);
426
+ await use(adapter);
427
+ },
428
+ });
429
+ ```
430
+
431
+ ### 4. Create Steps
432
+
433
+ ```typescript
434
+ // steps/email.steps.ts
435
+ import { When, Then } from '@cucumber/cucumber';
436
+
437
+ When('I send an email to {string} with subject {string}',
438
+ async ({ email }, to: string, subject: string) => {
439
+ await email.sendEmail(to, subject, 'Test body');
440
+ }
441
+ );
442
+
443
+ Then('I should receive an email at {string} with subject {string}',
444
+ async ({ email }, address: string, subject: string) => {
445
+ const mail = await email.waitForEmail(address, subject);
446
+ expect(mail).toBeDefined();
447
+ }
448
+ );
449
+ ```
450
+
451
+ ## Utility Functions
452
+
453
+ ### Variable Interpolation
454
+
455
+ ```typescript
456
+ import { interpolate } from '@esimplicitylabs/katalyst-xspec';
457
+
458
+ const template = 'Hello {name}, your ID is {id}';
459
+ const result = interpolate(template, { name: 'John', id: '123' });
460
+ // Result: 'Hello John, your ID is 123'
461
+ ```
462
+
463
+ ### JSON Path Selection
464
+
465
+ ```typescript
466
+ import { selectPath } from '@esimplicitylabs/katalyst-xspec';
467
+
468
+ const data = {
469
+ user: {
470
+ name: 'John',
471
+ roles: ['admin', 'user']
472
+ }
473
+ };
474
+
475
+ selectPath(data, 'user.name'); // 'John'
476
+ selectPath(data, 'user.roles[0]'); // 'admin'
477
+ ```
478
+
479
+ ### Tag Helpers
480
+
481
+ ```typescript
482
+ import { tagsForProject, resolveExtraTags } from '@esimplicitylabs/katalyst-xspec';
483
+
484
+ // Build tag expression with defaults
485
+ tagsForProject({ projectTag: '@api' });
486
+ // Result: 'not @Skip and not @ignore and @api'
487
+
488
+ // With extra tags
489
+ tagsForProject({ projectTag: '@api', extraTags: '@smoke' });
490
+ // Result: 'not @Skip and not @ignore and @api and (@smoke)'
491
+ ```
492
+
493
+ ## File Organization
494
+
495
+ Recommended structure for extensions:
496
+
497
+ ```
498
+ features/
499
+ ├── steps/
500
+ │ ├── fixtures.ts # Main fixture configuration
501
+ │ ├── steps.ts # Step registration
502
+ │ └── custom/
503
+ │ ├── email.steps.ts # Custom email steps
504
+ │ └── reporting.steps.ts
505
+ ├── adapters/
506
+ │ ├── custom-api.adapter.ts
507
+ │ ├── custom-auth.adapter.ts
508
+ │ └── email.adapter.ts
509
+ └── ports/
510
+ └── email.port.ts
511
+ ```
512
+
513
+ ## See Also
514
+
515
+ - [API Ports Reference](references/ports.md)
516
+ - [Built-in Adapters](references/adapters.md)
517
+ - [Custom Steps Guide](references/custom-steps.md)