@scenarist/core 0.1.1 → 0.1.3
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 +179 -94
- package/dist/adapters/in-memory-registry.d.ts +2 -2
- package/dist/adapters/in-memory-sequence-tracker.d.ts +2 -2
- package/dist/adapters/in-memory-sequence-tracker.d.ts.map +1 -1
- package/dist/adapters/in-memory-sequence-tracker.js +5 -5
- package/dist/adapters/in-memory-state-manager.d.ts +1 -1
- package/dist/adapters/in-memory-state-manager.d.ts.map +1 -1
- package/dist/adapters/in-memory-state-manager.js +42 -14
- package/dist/adapters/in-memory-store.d.ts +2 -2
- package/dist/adapters/index.d.ts +4 -4
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/constants/headers.js +1 -1
- package/dist/constants/index.d.ts +1 -1
- package/dist/constants/index.js +1 -1
- package/dist/contracts/framework-adapter.d.ts +17 -3
- package/dist/contracts/framework-adapter.d.ts.map +1 -1
- package/dist/contracts/index.d.ts +1 -1
- package/dist/domain/config-builder.d.ts +1 -1
- package/dist/domain/config-builder.d.ts.map +1 -1
- package/dist/domain/config-builder.js +4 -4
- package/dist/domain/index.d.ts +4 -4
- package/dist/domain/index.js +4 -4
- package/dist/domain/path-extraction.d.ts +1 -1
- package/dist/domain/path-extraction.d.ts.map +1 -1
- package/dist/domain/path-extraction.js +24 -9
- package/dist/domain/regex-matching.d.ts +1 -1
- package/dist/domain/regex-matching.js +1 -1
- package/dist/domain/response-selector.d.ts.map +1 -1
- package/dist/domain/response-selector.js +10 -5
- package/dist/domain/scenario-manager.d.ts.map +1 -1
- package/dist/domain/scenario-manager.js +3 -3
- package/dist/domain/template-replacement.d.ts.map +1 -1
- package/dist/domain/template-replacement.js +11 -9
- package/dist/index.d.ts +7 -7
- package/dist/index.js +4 -4
- package/dist/ports/driven/scenario-registry.d.ts +1 -1
- package/dist/ports/driven/scenario-store.d.ts +1 -1
- package/dist/ports/driven/sequence-tracker.d.ts +1 -1
- package/dist/ports/driving/scenario-manager.d.ts +1 -1
- package/dist/ports/driving/scenario-manager.d.ts.map +1 -1
- package/dist/ports/index.d.ts +7 -7
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/schemas/index.d.ts +4 -4
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +4 -4
- package/dist/schemas/match-criteria.d.ts +1 -1
- package/dist/schemas/match-criteria.d.ts.map +1 -1
- package/dist/schemas/match-criteria.js +5 -5
- package/dist/schemas/scenario-definition.d.ts +1 -1
- package/dist/schemas/scenario-definition.d.ts.map +1 -1
- package/dist/schemas/scenario-definition.js +27 -9
- package/dist/schemas/scenario-requests.d.ts +1 -1
- package/dist/schemas/scenario-requests.js +2 -2
- package/dist/schemas/scenarios-object.d.ts +1 -1
- package/dist/schemas/scenarios-object.js +3 -3
- package/dist/types/config.d.ts +2 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/scenario.d.ts +2 -2
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
# @scenarist/core
|
|
2
2
|
|
|
3
3
|
> **Note:** This is an internal package. Users should install framework-specific adapters instead:
|
|
4
|
+
>
|
|
4
5
|
> - **Express:** `@scenarist/express-adapter`
|
|
5
6
|
> - **Next.js:** `@scenarist/nextjs-adapter`
|
|
6
7
|
> - **Playwright:** `@scenarist/playwright-helpers`
|
|
7
8
|
>
|
|
8
9
|
> All types are re-exported from adapters for convenience. See the [full documentation](https://scenarist.io) for usage guides.
|
|
9
10
|
|
|
10
|
-
The core domain logic for Scenarist - a framework-agnostic library for managing MSW mock scenarios
|
|
11
|
+
The core domain logic for Scenarist - a framework-agnostic library for managing MSW mock scenarios for scenario-based testing where your real code runs.
|
|
11
12
|
|
|
12
13
|
## What is Scenarist?
|
|
13
14
|
|
|
14
|
-
**Scenarist** enables concurrent
|
|
15
|
+
**Scenarist** enables concurrent tests to run with different backend states by switching mock scenarios at runtime via test IDs. Your real application code executes while external API responses are controlled by scenarios. No application restarts needed, no complex per-test mocking, just simple scenario switching.
|
|
15
16
|
|
|
16
17
|
**Problem it solves:**
|
|
17
18
|
|
|
18
19
|
Testing multiple backend states (success, errors, loading, edge cases) traditionally requires:
|
|
20
|
+
|
|
19
21
|
- Restarting your app for each scenario
|
|
20
22
|
- Complex per-test MSW handler setup
|
|
21
23
|
- Serial test execution to avoid conflicts
|
|
@@ -27,11 +29,14 @@ Define scenarios once, switch at runtime via HTTP calls, run tests in parallel w
|
|
|
27
29
|
|
|
28
30
|
```typescript
|
|
29
31
|
// Define once
|
|
30
|
-
const errorScenario = {
|
|
32
|
+
const errorScenario = {
|
|
33
|
+
id: "error",
|
|
34
|
+
mocks: [{ method: "GET", url: "*/api/*", response: { status: 500 } }],
|
|
35
|
+
};
|
|
31
36
|
|
|
32
37
|
// Switch instantly
|
|
33
|
-
await switchScenario(
|
|
34
|
-
await switchScenario(
|
|
38
|
+
await switchScenario("test-1", "error"); // Test 1 sees errors
|
|
39
|
+
await switchScenario("test-2", "success"); // Test 2 sees success (parallel!)
|
|
35
40
|
|
|
36
41
|
// Tests run concurrently with different backend states
|
|
37
42
|
```
|
|
@@ -39,26 +44,31 @@ await switchScenario('test-2', 'success'); // Test 2 sees success (parallel!)
|
|
|
39
44
|
## Why Use Scenarist?
|
|
40
45
|
|
|
41
46
|
**Runtime Scenario Switching**
|
|
47
|
+
|
|
42
48
|
- Change entire backend state with one API call
|
|
43
49
|
- No server restarts between tests
|
|
44
50
|
- Instant feedback during development
|
|
45
51
|
|
|
46
52
|
**True Parallel Testing**
|
|
53
|
+
|
|
47
54
|
- 100+ tests run concurrently with different scenarios
|
|
48
55
|
- Each test ID has isolated scenario state
|
|
49
56
|
- No conflicts between tests
|
|
50
57
|
|
|
51
58
|
**Reusable Scenarios**
|
|
59
|
+
|
|
52
60
|
- Define scenarios once, use across all tests
|
|
53
61
|
- Version control your mock scenarios
|
|
54
62
|
- Share scenarios across teams
|
|
55
63
|
|
|
56
64
|
**Framework-Agnostic Core**
|
|
65
|
+
|
|
57
66
|
- Zero framework dependencies
|
|
58
67
|
- Works with Express, Next.js, and any Node.js framework
|
|
59
68
|
- Hexagonal architecture enables custom adapters
|
|
60
69
|
|
|
61
70
|
**Type-Safe & Tested**
|
|
71
|
+
|
|
62
72
|
- TypeScript strict mode throughout
|
|
63
73
|
- 100% test coverage
|
|
64
74
|
- Immutable, declarative data structures
|
|
@@ -80,6 +90,7 @@ src/
|
|
|
80
90
|
### Hexagonal Architecture Principles
|
|
81
91
|
|
|
82
92
|
**Types (Data Structures):**
|
|
93
|
+
|
|
83
94
|
- Defined with `type` keyword
|
|
84
95
|
- All properties are `readonly` for immutability
|
|
85
96
|
- Use declarative patterns (no imperative functions, closures, or hidden logic)
|
|
@@ -87,37 +98,43 @@ src/
|
|
|
87
98
|
- Examples: `ScenaristScenario`, `ScenaristConfig`, `ActiveScenario`, `ScenaristMock`
|
|
88
99
|
|
|
89
100
|
**Ports (Behavior Contracts):**
|
|
101
|
+
|
|
90
102
|
- Defined with `interface` keyword (domain ports) or `type` keyword (adapter contracts)
|
|
91
103
|
- Contracts that adapters must implement
|
|
92
104
|
- Domain ports: `ScenarioManager`, `ScenarioStore`, `RequestContext`
|
|
93
105
|
- Adapter contract: `ScenaristAdapter<TMiddleware>`, `BaseAdapterOptions`
|
|
94
106
|
|
|
95
107
|
**Domain (Implementations):**
|
|
108
|
+
|
|
96
109
|
- Pure TypeScript functions and factory patterns
|
|
97
110
|
- No framework dependencies
|
|
98
111
|
- Implements the core business logic
|
|
99
112
|
|
|
100
113
|
## Core Capabilities
|
|
101
114
|
|
|
102
|
-
Scenarist provides 20+ powerful features for
|
|
115
|
+
Scenarist provides 20+ powerful features for scenario-based testing. All capabilities are framework-agnostic and available via any adapter (Express, Next.js, etc.).
|
|
103
116
|
|
|
104
117
|
### Request Matching (11 capabilities)
|
|
105
118
|
|
|
106
119
|
**1. Body matching (partial match)**
|
|
120
|
+
|
|
107
121
|
- Match requests based on request body fields
|
|
108
122
|
- Additional fields in request are ignored
|
|
109
123
|
- Perfect for testing different payload scenarios
|
|
110
124
|
|
|
111
125
|
**2. Header matching (case-insensitive)**
|
|
126
|
+
|
|
112
127
|
- Match requests based on header values
|
|
113
128
|
- Header names are case-insensitive
|
|
114
129
|
- Ideal for user tier testing (`x-user-tier: premium`)
|
|
115
130
|
|
|
116
131
|
**3. Query parameter matching**
|
|
132
|
+
|
|
117
133
|
- Match requests based on query string parameters
|
|
118
134
|
- Enables different responses for filtered requests
|
|
119
135
|
|
|
120
136
|
**4. String matching (6 modes)**
|
|
137
|
+
|
|
121
138
|
- **Plain string** (`"value"`): Exact match (backward compatible)
|
|
122
139
|
- **Equals** (`{ equals: "value" }`): Explicit exact match
|
|
123
140
|
- **Contains** (`{ contains: "substring" }`): Substring matching
|
|
@@ -126,15 +143,18 @@ Scenarist provides 20+ powerful features for E2E testing. All capabilities are f
|
|
|
126
143
|
- **Regex** (`{ regex: { source: "pattern", flags: "i" } }`): Pattern matching with ReDoS protection
|
|
127
144
|
|
|
128
145
|
**5. ReDoS protection for regex matching**
|
|
146
|
+
|
|
129
147
|
- Validates regex patterns before execution using `redos-detector`
|
|
130
148
|
- Prevents catastrophic backtracking attacks
|
|
131
149
|
- Rejects unsafe patterns at scenario registration
|
|
132
150
|
|
|
133
151
|
**6. Combined matching (all criteria together)**
|
|
152
|
+
|
|
134
153
|
- Combine body + headers + query parameters
|
|
135
154
|
- ALL criteria must pass for mock to apply
|
|
136
155
|
|
|
137
156
|
**7. Specificity-based selection**
|
|
157
|
+
|
|
138
158
|
- Most specific mock wins regardless of position
|
|
139
159
|
- Calculated score: body fields + headers + query params
|
|
140
160
|
- No need to carefully order your mocks
|
|
@@ -144,20 +164,24 @@ Scenarist provides 20+ powerful features for E2E testing. All capabilities are f
|
|
|
144
164
|
- Last fallback wins enables active scenario fallbacks to override default fallbacks
|
|
145
165
|
|
|
146
166
|
**8. Fallback mocks**
|
|
167
|
+
|
|
147
168
|
- Mocks without match criteria act as catch-all
|
|
148
169
|
- Specific mocks always take precedence
|
|
149
170
|
- Perfect for default responses
|
|
150
171
|
- When multiple fallbacks exist, last one wins (enables override pattern)
|
|
151
172
|
|
|
152
173
|
**9. Number and boolean matching**
|
|
174
|
+
|
|
153
175
|
- Automatically stringifies number/boolean criteria values
|
|
154
176
|
- Enables matching against numeric query params and body fields
|
|
155
177
|
|
|
156
178
|
**10. Null matching**
|
|
179
|
+
|
|
157
180
|
- `null` criteria matches empty string (`""`)
|
|
158
181
|
- Useful for optional fields
|
|
159
182
|
|
|
160
183
|
**11. Type coercion**
|
|
184
|
+
|
|
161
185
|
- All match values converted to strings before comparison
|
|
162
186
|
- Consistent behavior across headers, query params, and body
|
|
163
187
|
|
|
@@ -185,11 +209,13 @@ Per [RFC 2616 Section 4.2](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.htm
|
|
|
185
209
|
```
|
|
186
210
|
|
|
187
211
|
**Implementation Details:**
|
|
212
|
+
|
|
188
213
|
- Core's `ResponseSelector` normalizes both request headers AND criteria headers to lowercase
|
|
189
214
|
- Adapters pass headers as-is (no normalization required)
|
|
190
215
|
- Works regardless of framework (Express, Next.js, etc.)
|
|
191
216
|
|
|
192
217
|
**Why This Matters:**
|
|
218
|
+
|
|
193
219
|
- Browser and client libraries may send headers with any casing
|
|
194
220
|
- Tests remain portable across different HTTP clients
|
|
195
221
|
- Standards-compliant behavior prevents matching bugs
|
|
@@ -197,55 +223,67 @@ Per [RFC 2616 Section 4.2](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.htm
|
|
|
197
223
|
### Response Sequences (4 capabilities)
|
|
198
224
|
|
|
199
225
|
**7. Single responses**
|
|
226
|
+
|
|
200
227
|
- Return same response every time
|
|
201
228
|
- Simplest mock definition
|
|
202
229
|
|
|
203
230
|
**8. Response sequences (ordered)**
|
|
231
|
+
|
|
204
232
|
- Return different response on each call
|
|
205
233
|
- Perfect for polling APIs (pending → processing → complete)
|
|
206
234
|
|
|
207
235
|
**9. Repeat modes (last, cycle, none)**
|
|
236
|
+
|
|
208
237
|
- `last`: Stay at final response forever
|
|
209
238
|
- `cycle`: Loop back to first response
|
|
210
239
|
- `none`: Mark as exhausted after last response
|
|
211
240
|
|
|
212
241
|
**10. Sequence exhaustion with fallback**
|
|
242
|
+
|
|
213
243
|
- Exhausted sequences (`repeat: none`) skip to next mock
|
|
214
244
|
- Enables rate limiting scenarios
|
|
215
245
|
|
|
216
246
|
### Stateful Mocks (6 capabilities)
|
|
217
247
|
|
|
218
248
|
**11. State capture from requests**
|
|
249
|
+
|
|
219
250
|
- Extract values from request body, headers, or query
|
|
220
251
|
- Store in per-test-ID state
|
|
221
252
|
|
|
222
253
|
**12. State injection via templates**
|
|
254
|
+
|
|
223
255
|
- Inject captured state into responses using `{{state.X}}`
|
|
224
256
|
- Dynamic responses based on earlier requests
|
|
225
257
|
|
|
226
258
|
**13. Array append support**
|
|
259
|
+
|
|
227
260
|
- Syntax: `stateKey[]` appends to array
|
|
228
261
|
- Perfect for shopping cart scenarios
|
|
229
262
|
|
|
230
263
|
**14. Nested state paths**
|
|
264
|
+
|
|
231
265
|
- Support dot notation: `user.profile.name`
|
|
232
266
|
- Both capture and injection support nesting
|
|
233
267
|
|
|
234
268
|
**15. State isolation per test ID**
|
|
269
|
+
|
|
235
270
|
- Each test ID has isolated state
|
|
236
271
|
- Parallel tests don't interfere
|
|
237
272
|
|
|
238
273
|
**16. State reset on scenario switch**
|
|
274
|
+
|
|
239
275
|
- State cleared when switching scenarios
|
|
240
276
|
- Fresh state for each scenario
|
|
241
277
|
|
|
242
278
|
### Core Features (4 capabilities)
|
|
243
279
|
|
|
244
280
|
**17. Multiple API mocking**
|
|
281
|
+
|
|
245
282
|
- Mock any number of external APIs
|
|
246
283
|
- Combine APIs in single scenario
|
|
247
284
|
|
|
248
285
|
**18. Automatic default scenario fallback**
|
|
286
|
+
|
|
249
287
|
- Active scenarios automatically inherit mocks from default scenario
|
|
250
288
|
- Default + active scenario mocks collected together
|
|
251
289
|
- Specificity-based selection chooses best match
|
|
@@ -253,10 +291,12 @@ Per [RFC 2616 Section 4.2](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.htm
|
|
|
253
291
|
- No explicit fallback mocks needed in specialized scenarios
|
|
254
292
|
|
|
255
293
|
**19. Test ID isolation (parallel tests)**
|
|
294
|
+
|
|
256
295
|
- Run 100+ tests concurrently
|
|
257
296
|
- Each test ID has isolated scenario/state
|
|
258
297
|
|
|
259
298
|
**20. Scenario switching at runtime**
|
|
299
|
+
|
|
260
300
|
- Change backend state with one HTTP call
|
|
261
301
|
- No application restart needed
|
|
262
302
|
|
|
@@ -313,17 +353,17 @@ import {
|
|
|
313
353
|
InMemoryScenarioStore,
|
|
314
354
|
buildConfig,
|
|
315
355
|
type ScenaristScenario,
|
|
316
|
-
} from
|
|
356
|
+
} from "@scenarist/core";
|
|
317
357
|
|
|
318
358
|
// 1. Define scenarios (declarative patterns)
|
|
319
359
|
const defaultScenario: ScenaristScenario = {
|
|
320
|
-
id:
|
|
321
|
-
name:
|
|
322
|
-
description:
|
|
360
|
+
id: "default",
|
|
361
|
+
name: "Default Scenario",
|
|
362
|
+
description: "Baseline mocks for all APIs",
|
|
323
363
|
mocks: [
|
|
324
364
|
{
|
|
325
|
-
method:
|
|
326
|
-
url:
|
|
365
|
+
method: "GET",
|
|
366
|
+
url: "https://api.example.com/users",
|
|
327
367
|
response: {
|
|
328
368
|
status: 200,
|
|
329
369
|
body: { users: [] },
|
|
@@ -333,17 +373,17 @@ const defaultScenario: ScenaristScenario = {
|
|
|
333
373
|
};
|
|
334
374
|
|
|
335
375
|
const happyPathScenario: ScenaristScenario = {
|
|
336
|
-
id:
|
|
337
|
-
name:
|
|
338
|
-
description:
|
|
376
|
+
id: "happy-path",
|
|
377
|
+
name: "Happy Path",
|
|
378
|
+
description: "All API calls succeed",
|
|
339
379
|
devToolEnabled: true,
|
|
340
380
|
mocks: [
|
|
341
381
|
{
|
|
342
|
-
method:
|
|
343
|
-
url:
|
|
382
|
+
method: "GET",
|
|
383
|
+
url: "https://api.example.com/users",
|
|
344
384
|
response: {
|
|
345
385
|
status: 200,
|
|
346
|
-
body: { users: [{ id: 1, name:
|
|
386
|
+
body: { users: [{ id: 1, name: "Test User" }] },
|
|
347
387
|
},
|
|
348
388
|
},
|
|
349
389
|
],
|
|
@@ -351,11 +391,11 @@ const happyPathScenario: ScenaristScenario = {
|
|
|
351
391
|
|
|
352
392
|
// 2. Build configuration (declarative plain data)
|
|
353
393
|
const config = buildConfig({
|
|
354
|
-
enabled: process.env.NODE_ENV !==
|
|
394
|
+
enabled: process.env.NODE_ENV !== "production", // Evaluated first!
|
|
355
395
|
defaultScenario: defaultScenario, // REQUIRED - fallback for unmocked requests
|
|
356
|
-
strictMode: false,
|
|
396
|
+
strictMode: false, // true = error on unmocked requests, false = passthrough
|
|
357
397
|
headers: {
|
|
358
|
-
testId:
|
|
398
|
+
testId: "x-scenarist-test-id",
|
|
359
399
|
},
|
|
360
400
|
});
|
|
361
401
|
|
|
@@ -371,10 +411,10 @@ manager.registerScenario(defaultScenario); // Must register default
|
|
|
371
411
|
manager.registerScenario(happyPathScenario);
|
|
372
412
|
|
|
373
413
|
// 6. Switch to a scenario
|
|
374
|
-
const result = manager.switchScenario(
|
|
414
|
+
const result = manager.switchScenario("test-123", "happy-path");
|
|
375
415
|
|
|
376
416
|
if (result.success) {
|
|
377
|
-
console.log(
|
|
417
|
+
console.log("Scenario activated!");
|
|
378
418
|
}
|
|
379
419
|
```
|
|
380
420
|
|
|
@@ -384,15 +424,15 @@ Scenarist supports 5 matching strategies for headers, query params, and body fie
|
|
|
384
424
|
|
|
385
425
|
```typescript
|
|
386
426
|
const scenario: ScenaristScenario = {
|
|
387
|
-
id:
|
|
427
|
+
id: "string-matching-examples",
|
|
388
428
|
mocks: [
|
|
389
429
|
// 1. Exact match (default)
|
|
390
430
|
{
|
|
391
|
-
method:
|
|
392
|
-
url:
|
|
431
|
+
method: "GET",
|
|
432
|
+
url: "/api/products",
|
|
393
433
|
match: {
|
|
394
434
|
headers: {
|
|
395
|
-
|
|
435
|
+
"x-user-tier": "premium", // Must match exactly
|
|
396
436
|
},
|
|
397
437
|
},
|
|
398
438
|
response: { status: 200, body: { products: [] } },
|
|
@@ -400,11 +440,11 @@ const scenario: ScenaristScenario = {
|
|
|
400
440
|
|
|
401
441
|
// 2. Explicit exact match (same as above)
|
|
402
442
|
{
|
|
403
|
-
method:
|
|
404
|
-
url:
|
|
443
|
+
method: "GET",
|
|
444
|
+
url: "/api/products",
|
|
405
445
|
match: {
|
|
406
446
|
headers: {
|
|
407
|
-
|
|
447
|
+
"x-user-tier": { equals: "premium" },
|
|
408
448
|
},
|
|
409
449
|
},
|
|
410
450
|
response: { status: 200, body: { products: [] } },
|
|
@@ -412,23 +452,23 @@ const scenario: ScenaristScenario = {
|
|
|
412
452
|
|
|
413
453
|
// 3. Contains (substring match)
|
|
414
454
|
{
|
|
415
|
-
method:
|
|
416
|
-
url:
|
|
455
|
+
method: "GET",
|
|
456
|
+
url: "/api/products",
|
|
417
457
|
match: {
|
|
418
458
|
headers: {
|
|
419
|
-
|
|
459
|
+
"x-campaign": { contains: "summer" }, // Matches 'summer-sale', 'mega-summer-event', etc.
|
|
420
460
|
},
|
|
421
461
|
},
|
|
422
|
-
response: { status: 200, body: { campaign:
|
|
462
|
+
response: { status: 200, body: { campaign: "summer" } },
|
|
423
463
|
},
|
|
424
464
|
|
|
425
465
|
// 4. Starts with (prefix match)
|
|
426
466
|
{
|
|
427
|
-
method:
|
|
428
|
-
url:
|
|
467
|
+
method: "GET",
|
|
468
|
+
url: "/api/keys",
|
|
429
469
|
match: {
|
|
430
470
|
headers: {
|
|
431
|
-
|
|
471
|
+
"x-api-key": { startsWith: "sk_" }, // Matches 'sk_test_123', 'sk_live_456', etc.
|
|
432
472
|
},
|
|
433
473
|
},
|
|
434
474
|
response: { status: 200, body: { valid: true } },
|
|
@@ -436,11 +476,11 @@ const scenario: ScenaristScenario = {
|
|
|
436
476
|
|
|
437
477
|
// 5. Ends with (suffix match)
|
|
438
478
|
{
|
|
439
|
-
method:
|
|
440
|
-
url:
|
|
479
|
+
method: "GET",
|
|
480
|
+
url: "/api/users",
|
|
441
481
|
match: {
|
|
442
482
|
query: {
|
|
443
|
-
email: { endsWith:
|
|
483
|
+
email: { endsWith: "@company.com" }, // Matches 'john@company.com', 'admin@company.com', etc.
|
|
444
484
|
},
|
|
445
485
|
},
|
|
446
486
|
response: { status: 200, body: { users: [] } },
|
|
@@ -448,19 +488,19 @@ const scenario: ScenaristScenario = {
|
|
|
448
488
|
|
|
449
489
|
// 6. Regex (pattern match with ReDoS protection)
|
|
450
490
|
{
|
|
451
|
-
method:
|
|
452
|
-
url:
|
|
491
|
+
method: "GET",
|
|
492
|
+
url: "/api/products",
|
|
453
493
|
match: {
|
|
454
494
|
headers: {
|
|
455
495
|
referer: {
|
|
456
496
|
regex: {
|
|
457
|
-
source:
|
|
458
|
-
flags:
|
|
497
|
+
source: "/premium|/vip", // Matches any referer containing '/premium' or '/vip'
|
|
498
|
+
flags: "i", // Case-insensitive
|
|
459
499
|
},
|
|
460
500
|
},
|
|
461
501
|
},
|
|
462
502
|
},
|
|
463
|
-
response: { status: 200, body: { tier:
|
|
503
|
+
response: { status: 200, body: { tier: "premium" } },
|
|
464
504
|
},
|
|
465
505
|
],
|
|
466
506
|
};
|
|
@@ -472,34 +512,34 @@ You can use native JavaScript RegExp objects directly instead of the serialized
|
|
|
472
512
|
|
|
473
513
|
```typescript
|
|
474
514
|
const scenario: ScenaristScenario = {
|
|
475
|
-
id:
|
|
515
|
+
id: "native-regex-examples",
|
|
476
516
|
mocks: [
|
|
477
517
|
// Native RegExp in URL matching
|
|
478
518
|
{
|
|
479
|
-
method:
|
|
480
|
-
url: /\/api\/v\d+\/products/,
|
|
519
|
+
method: "GET",
|
|
520
|
+
url: /\/api\/v\d+\/products/, // Matches /api/v1/products, /api/v2/products, etc.
|
|
481
521
|
response: { status: 200, body: { products: [] } },
|
|
482
522
|
},
|
|
483
523
|
|
|
484
524
|
// Native RegExp in header matching
|
|
485
525
|
{
|
|
486
|
-
method:
|
|
487
|
-
url:
|
|
526
|
+
method: "GET",
|
|
527
|
+
url: "/api/products",
|
|
488
528
|
match: {
|
|
489
529
|
headers: {
|
|
490
|
-
referer: /\/premium|\/vip/i,
|
|
530
|
+
referer: /\/premium|\/vip/i, // Case-insensitive pattern
|
|
491
531
|
},
|
|
492
532
|
},
|
|
493
|
-
response: { status: 200, body: { tier:
|
|
533
|
+
response: { status: 200, body: { tier: "premium" } },
|
|
494
534
|
},
|
|
495
535
|
|
|
496
536
|
// Both forms are equivalent and have same ReDoS protection
|
|
497
537
|
{
|
|
498
|
-
method:
|
|
499
|
-
url:
|
|
538
|
+
method: "GET",
|
|
539
|
+
url: "/api/data",
|
|
500
540
|
match: {
|
|
501
541
|
query: {
|
|
502
|
-
filter: /^\w+$/,
|
|
542
|
+
filter: /^\w+$/, // Native RegExp
|
|
503
543
|
// Same as: { regex: { source: '^\\w+$', flags: '' } }
|
|
504
544
|
},
|
|
505
545
|
},
|
|
@@ -516,9 +556,13 @@ const scenario: ScenaristScenario = {
|
|
|
516
556
|
Scenarist supports three pattern types with different hostname matching behaviors:
|
|
517
557
|
|
|
518
558
|
**1. Pathname-only patterns** (origin-agnostic)
|
|
559
|
+
|
|
519
560
|
```typescript
|
|
520
|
-
{
|
|
561
|
+
{
|
|
562
|
+
url: "/api/users/:id";
|
|
563
|
+
}
|
|
521
564
|
```
|
|
565
|
+
|
|
522
566
|
- **Matches ANY hostname** - environment-agnostic
|
|
523
567
|
- Best for mocks that should work across localhost, staging, production
|
|
524
568
|
- Example: `/api/users/123` matches requests to:
|
|
@@ -527,9 +571,13 @@ Scenarist supports three pattern types with different hostname matching behavior
|
|
|
527
571
|
- `https://api.production.com/api/users/123` ✅
|
|
528
572
|
|
|
529
573
|
**2. Full URL patterns** (hostname-specific)
|
|
574
|
+
|
|
530
575
|
```typescript
|
|
531
|
-
{
|
|
576
|
+
{
|
|
577
|
+
url: "http://api.example.com/users/:id";
|
|
578
|
+
}
|
|
532
579
|
```
|
|
580
|
+
|
|
533
581
|
- **Matches ONLY the specified hostname** - environment-specific
|
|
534
582
|
- Best for mocks that should only apply to specific domains
|
|
535
583
|
- Example: `http://api.example.com/users/:id` matches:
|
|
@@ -538,9 +586,13 @@ Scenarist supports three pattern types with different hostname matching behavior
|
|
|
538
586
|
- `https://api.example.com/users/123` ❌ (different protocol)
|
|
539
587
|
|
|
540
588
|
**3. Native RegExp patterns** (origin-agnostic)
|
|
589
|
+
|
|
541
590
|
```typescript
|
|
542
|
-
{
|
|
591
|
+
{
|
|
592
|
+
url: /\/users\/\d+/;
|
|
593
|
+
}
|
|
543
594
|
```
|
|
595
|
+
|
|
544
596
|
- **Matches ANY hostname** - substring matching (MSW weak comparison)
|
|
545
597
|
- Best for flexible pattern matching across environments
|
|
546
598
|
- Example: `/\/users\/\d+/` matches:
|
|
@@ -555,30 +607,30 @@ Scenarist supports three pattern types with different hostname matching behavior
|
|
|
555
607
|
const defaultScenario = {
|
|
556
608
|
mocks: [
|
|
557
609
|
{
|
|
558
|
-
url:
|
|
559
|
-
response: { status: 200, body: { products: [] } }
|
|
560
|
-
}
|
|
561
|
-
]
|
|
610
|
+
url: "/api/products", // Works in dev, staging, prod
|
|
611
|
+
response: { status: 200, body: { products: [] } },
|
|
612
|
+
},
|
|
613
|
+
],
|
|
562
614
|
};
|
|
563
615
|
|
|
564
616
|
// ✅ Use full URL patterns when hostname matters
|
|
565
617
|
const productionOnlyScenario = {
|
|
566
618
|
mocks: [
|
|
567
619
|
{
|
|
568
|
-
url:
|
|
569
|
-
response: { status: 403, body: { error:
|
|
570
|
-
}
|
|
571
|
-
]
|
|
620
|
+
url: "https://api.production.com/admin", // Only matches production
|
|
621
|
+
response: { status: 403, body: { error: "Admin disabled in prod" } },
|
|
622
|
+
},
|
|
623
|
+
],
|
|
572
624
|
};
|
|
573
625
|
|
|
574
626
|
// ✅ Use RegExp for flexible pattern matching
|
|
575
627
|
const versionAgnosticScenario = {
|
|
576
628
|
mocks: [
|
|
577
629
|
{
|
|
578
|
-
url: /\/api\/v\d+\/users/,
|
|
579
|
-
response: { status: 200, body: { users: [] } }
|
|
580
|
-
}
|
|
581
|
-
]
|
|
630
|
+
url: /\/api\/v\d+\/users/, // Matches /api/v1/users, /api/v2/users, etc.
|
|
631
|
+
response: { status: 200, body: { users: [] } },
|
|
632
|
+
},
|
|
633
|
+
],
|
|
582
634
|
};
|
|
583
635
|
```
|
|
584
636
|
|
|
@@ -590,36 +642,48 @@ Here are helpful regex patterns for common use cases:
|
|
|
590
642
|
|
|
591
643
|
```typescript
|
|
592
644
|
// API versioning - match any version number
|
|
593
|
-
{
|
|
645
|
+
{
|
|
646
|
+
url: /\/api\/v\d+\//;
|
|
647
|
+
}
|
|
594
648
|
// Matches: /api/v1/, /api/v2/, /api/v10/, etc.
|
|
595
649
|
|
|
596
650
|
// Numeric IDs only (reject non-numeric)
|
|
597
|
-
{
|
|
651
|
+
{
|
|
652
|
+
url: /\/users\/\d+$/;
|
|
653
|
+
}
|
|
598
654
|
// Matches: /users/123 ✅
|
|
599
655
|
// Rejects: /users/abc ❌
|
|
600
656
|
|
|
601
657
|
// File extensions
|
|
602
|
-
{
|
|
658
|
+
{
|
|
659
|
+
url: /\.json$/;
|
|
660
|
+
}
|
|
603
661
|
// Matches: /data.json, /api/users.json ✅
|
|
604
662
|
// Rejects: /data.xml, /users ❌
|
|
605
663
|
|
|
606
664
|
// Optional trailing slash
|
|
607
|
-
{
|
|
665
|
+
{
|
|
666
|
+
url: /\/products\/?$/;
|
|
667
|
+
}
|
|
608
668
|
// Matches: /products ✅ and /products/ ✅
|
|
609
669
|
|
|
610
670
|
// Multiple extensions
|
|
611
|
-
{
|
|
671
|
+
{
|
|
672
|
+
url: /\.(jpg|png|gif)$/i;
|
|
673
|
+
}
|
|
612
674
|
// Matches: image.jpg, photo.PNG, avatar.gif ✅
|
|
613
675
|
|
|
614
676
|
// UUID format (simplified)
|
|
615
|
-
{
|
|
677
|
+
{
|
|
678
|
+
url: /\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
679
|
+
}
|
|
616
680
|
// Matches: /550e8400-e29b-41d4-a716-446655440000 ✅
|
|
617
681
|
|
|
618
682
|
// Subdomain matching
|
|
619
683
|
{
|
|
620
684
|
match: {
|
|
621
685
|
headers: {
|
|
622
|
-
host: /^(api|cdn)\.example\.com
|
|
686
|
+
host: /^(api|cdn)\.example\.com$/;
|
|
623
687
|
}
|
|
624
688
|
}
|
|
625
689
|
}
|
|
@@ -634,15 +698,29 @@ Here are helpful regex patterns for common use cases:
|
|
|
634
698
|
|
|
635
699
|
```typescript
|
|
636
700
|
// ❌ REJECTED - Catastrophic backtracking
|
|
637
|
-
{
|
|
701
|
+
{
|
|
702
|
+
url: /(a+)+b/;
|
|
703
|
+
}
|
|
638
704
|
// Error: Unsafe regex pattern detected
|
|
639
705
|
|
|
640
706
|
// ❌ REJECTED - Exponential time complexity
|
|
641
|
-
{
|
|
707
|
+
{
|
|
708
|
+
match: {
|
|
709
|
+
headers: {
|
|
710
|
+
referer: {
|
|
711
|
+
regex: {
|
|
712
|
+
source: "(x+x+)+y";
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
642
718
|
// Error: Unsafe regex pattern detected
|
|
643
719
|
|
|
644
720
|
// ✅ SAFE - Linear time complexity
|
|
645
|
-
{
|
|
721
|
+
{
|
|
722
|
+
url: /\/api\/[^/]+\/users/;
|
|
723
|
+
}
|
|
646
724
|
// Matches safely with bounded backtracking
|
|
647
725
|
```
|
|
648
726
|
|
|
@@ -667,7 +745,7 @@ All adapters must accept these base options:
|
|
|
667
745
|
```typescript
|
|
668
746
|
type BaseAdapterOptions<T extends ScenaristScenarios> = {
|
|
669
747
|
readonly enabled: boolean;
|
|
670
|
-
readonly scenarios: T;
|
|
748
|
+
readonly scenarios: T; // REQUIRED - scenarios object (must have 'default' key)
|
|
671
749
|
readonly strictMode?: boolean;
|
|
672
750
|
readonly headers?: {
|
|
673
751
|
readonly testId?: string;
|
|
@@ -690,14 +768,12 @@ Adapters can extend this with framework-specific options:
|
|
|
690
768
|
|
|
691
769
|
```typescript
|
|
692
770
|
// Express adapter
|
|
693
|
-
type ExpressAdapterOptions<T extends ScenaristScenarios> =
|
|
694
|
-
// Add Express-specific options if needed
|
|
695
|
-
};
|
|
771
|
+
type ExpressAdapterOptions<T extends ScenaristScenarios> =
|
|
772
|
+
BaseAdapterOptions<T> & { // Add Express-specific options if needed };
|
|
696
773
|
|
|
697
774
|
// Next.js adapter
|
|
698
|
-
type NextJSAdapterOptions<T extends ScenaristScenarios> =
|
|
699
|
-
// Add Next.js-specific options if needed
|
|
700
|
-
};
|
|
775
|
+
type NextJSAdapterOptions<T extends ScenaristScenarios> =
|
|
776
|
+
BaseAdapterOptions<T> & { // Add Next.js-specific options if needed };
|
|
701
777
|
```
|
|
702
778
|
|
|
703
779
|
### ScenaristAdapter<T, TMiddleware>
|
|
@@ -706,11 +782,17 @@ All adapters must return an object matching this contract:
|
|
|
706
782
|
|
|
707
783
|
```typescript
|
|
708
784
|
type ScenaristAdapter<T extends ScenaristScenarios, TMiddleware = unknown> = {
|
|
709
|
-
readonly config: ScenaristConfig;
|
|
710
|
-
readonly middleware?: TMiddleware;
|
|
711
|
-
readonly switchScenario: (
|
|
785
|
+
readonly config: ScenaristConfig; // Resolved configuration
|
|
786
|
+
readonly middleware?: TMiddleware; // Framework-specific middleware (optional - Next.js doesn't have global middleware)
|
|
787
|
+
readonly switchScenario: (
|
|
788
|
+
testId: string,
|
|
789
|
+
scenarioId: ScenarioIds<T>,
|
|
790
|
+
variant?: string,
|
|
791
|
+
) => ScenaristResult<void, Error>;
|
|
712
792
|
readonly getActiveScenario: (testId: string) => ActiveScenario | undefined;
|
|
713
|
-
readonly getScenarioById: (
|
|
793
|
+
readonly getScenarioById: (
|
|
794
|
+
scenarioId: ScenarioIds<T>,
|
|
795
|
+
) => ScenaristScenario | undefined;
|
|
714
796
|
readonly listScenarios: () => ReadonlyArray<ScenaristScenario>;
|
|
715
797
|
readonly clearScenario: (testId: string) => void;
|
|
716
798
|
readonly start: () => void;
|
|
@@ -719,10 +801,12 @@ type ScenaristAdapter<T extends ScenaristScenarios, TMiddleware = unknown> = {
|
|
|
719
801
|
```
|
|
720
802
|
|
|
721
803
|
The generic parameters:
|
|
804
|
+
|
|
722
805
|
- `T extends ScenaristScenarios`: The scenarios object type for type-safe scenario IDs
|
|
723
806
|
- `TMiddleware`: Framework-specific middleware type
|
|
724
807
|
|
|
725
808
|
Examples:
|
|
809
|
+
|
|
726
810
|
- Express: `ScenaristAdapter<typeof scenarios, Router>`
|
|
727
811
|
- Next.js: `ScenaristAdapter<typeof scenarios, never>` (no global middleware)
|
|
728
812
|
|
|
@@ -731,7 +815,7 @@ Examples:
|
|
|
731
815
|
```typescript
|
|
732
816
|
// Express adapter implementation
|
|
733
817
|
export const createScenarist = <T extends ScenaristScenarios>(
|
|
734
|
-
options: ExpressAdapterOptions<T
|
|
818
|
+
options: ExpressAdapterOptions<T>,
|
|
735
819
|
): ScenaristAdapter<T, Router> => {
|
|
736
820
|
// Implementation automatically wires:
|
|
737
821
|
// - MSW server with dynamic handler
|
|
@@ -747,8 +831,9 @@ export const createScenarist = <T extends ScenaristScenarios>(
|
|
|
747
831
|
|
|
748
832
|
return {
|
|
749
833
|
config: resolvedConfig,
|
|
750
|
-
middleware: router,
|
|
751
|
-
switchScenario: (id, scenario, variant) =>
|
|
834
|
+
middleware: router, // Express Router
|
|
835
|
+
switchScenario: (id, scenario, variant) =>
|
|
836
|
+
manager.switchScenario(id, scenario, variant),
|
|
752
837
|
// ... all other required methods
|
|
753
838
|
start: () => server.listen(),
|
|
754
839
|
stop: async () => server.close(),
|