@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,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)
|