@scenarist/core 0.1.2 → 0.1.16

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 (62) hide show
  1. package/README.md +179 -94
  2. package/dist/adapters/in-memory-registry.d.ts +2 -2
  3. package/dist/adapters/in-memory-sequence-tracker.d.ts +2 -2
  4. package/dist/adapters/in-memory-sequence-tracker.d.ts.map +1 -1
  5. package/dist/adapters/in-memory-sequence-tracker.js +5 -5
  6. package/dist/adapters/in-memory-state-manager.d.ts +1 -1
  7. package/dist/adapters/in-memory-state-manager.d.ts.map +1 -1
  8. package/dist/adapters/in-memory-state-manager.js +34 -14
  9. package/dist/adapters/in-memory-store.d.ts +2 -2
  10. package/dist/adapters/index.d.ts +4 -4
  11. package/dist/adapters/index.d.ts.map +1 -1
  12. package/dist/adapters/index.js +4 -4
  13. package/dist/constants/headers.js +1 -1
  14. package/dist/constants/index.d.ts +1 -1
  15. package/dist/constants/index.js +1 -1
  16. package/dist/contracts/framework-adapter.d.ts +17 -3
  17. package/dist/contracts/framework-adapter.d.ts.map +1 -1
  18. package/dist/contracts/index.d.ts +1 -1
  19. package/dist/domain/config-builder.d.ts +1 -1
  20. package/dist/domain/config-builder.d.ts.map +1 -1
  21. package/dist/domain/config-builder.js +4 -4
  22. package/dist/domain/index.d.ts +4 -4
  23. package/dist/domain/index.js +4 -4
  24. package/dist/domain/path-extraction.d.ts +1 -1
  25. package/dist/domain/path-extraction.d.ts.map +1 -1
  26. package/dist/domain/path-extraction.js +24 -9
  27. package/dist/domain/regex-matching.d.ts +1 -1
  28. package/dist/domain/regex-matching.js +1 -1
  29. package/dist/domain/response-selector.d.ts.map +1 -1
  30. package/dist/domain/response-selector.js +10 -5
  31. package/dist/domain/scenario-manager.d.ts.map +1 -1
  32. package/dist/domain/scenario-manager.js +3 -3
  33. package/dist/domain/template-replacement.d.ts.map +1 -1
  34. package/dist/domain/template-replacement.js +7 -7
  35. package/dist/index.d.ts +7 -7
  36. package/dist/index.js +4 -4
  37. package/dist/ports/driven/scenario-registry.d.ts +1 -1
  38. package/dist/ports/driven/scenario-store.d.ts +1 -1
  39. package/dist/ports/driven/sequence-tracker.d.ts +1 -1
  40. package/dist/ports/driving/scenario-manager.d.ts +1 -1
  41. package/dist/ports/driving/scenario-manager.d.ts.map +1 -1
  42. package/dist/ports/index.d.ts +7 -7
  43. package/dist/ports/index.d.ts.map +1 -1
  44. package/dist/schemas/index.d.ts +4 -4
  45. package/dist/schemas/index.d.ts.map +1 -1
  46. package/dist/schemas/index.js +4 -4
  47. package/dist/schemas/match-criteria.d.ts +1 -1
  48. package/dist/schemas/match-criteria.d.ts.map +1 -1
  49. package/dist/schemas/match-criteria.js +5 -5
  50. package/dist/schemas/scenario-definition.d.ts +1 -1
  51. package/dist/schemas/scenario-definition.d.ts.map +1 -1
  52. package/dist/schemas/scenario-definition.js +27 -9
  53. package/dist/schemas/scenario-requests.d.ts +1 -1
  54. package/dist/schemas/scenario-requests.js +2 -2
  55. package/dist/schemas/scenarios-object.d.ts +1 -1
  56. package/dist/schemas/scenarios-object.js +3 -3
  57. package/dist/types/config.d.ts +2 -2
  58. package/dist/types/config.d.ts.map +1 -1
  59. package/dist/types/index.d.ts +3 -3
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/types/scenario.d.ts +2 -2
  62. 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 in E2E testing environments.
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 E2E tests to run with different backend states by switching mock scenarios at runtime via test IDs. No application restarts needed, no complex per-test mocking, just simple scenario switching.
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 = { id: 'error', mocks: [{ method: 'GET', url: '*/api/*', response: { status: 500 } }] };
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('test-1', 'error'); // Test 1 sees errors
34
- await switchScenario('test-2', 'success'); // Test 2 sees success (parallel!)
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 E2E testing. All capabilities are framework-agnostic and available via any adapter (Express, Next.js, etc.).
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 '@scenarist/core';
356
+ } from "@scenarist/core";
317
357
 
318
358
  // 1. Define scenarios (declarative patterns)
319
359
  const defaultScenario: ScenaristScenario = {
320
- id: 'default',
321
- name: 'Default Scenario',
322
- description: 'Baseline mocks for all APIs',
360
+ id: "default",
361
+ name: "Default Scenario",
362
+ description: "Baseline mocks for all APIs",
323
363
  mocks: [
324
364
  {
325
- method: 'GET',
326
- url: 'https://api.example.com/users',
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: 'happy-path',
337
- name: 'Happy Path',
338
- description: 'All API calls succeed',
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: 'GET',
343
- url: 'https://api.example.com/users',
382
+ method: "GET",
383
+ url: "https://api.example.com/users",
344
384
  response: {
345
385
  status: 200,
346
- body: { users: [{ id: 1, name: 'Test User' }] },
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 !== 'production', // Evaluated first!
394
+ enabled: process.env.NODE_ENV !== "production", // Evaluated first!
355
395
  defaultScenario: defaultScenario, // REQUIRED - fallback for unmocked requests
356
- strictMode: false, // true = error on unmocked requests, false = passthrough
396
+ strictMode: false, // true = error on unmocked requests, false = passthrough
357
397
  headers: {
358
- testId: 'x-scenarist-test-id',
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('test-123', 'happy-path');
414
+ const result = manager.switchScenario("test-123", "happy-path");
375
415
 
376
416
  if (result.success) {
377
- console.log('Scenario activated!');
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: 'string-matching-examples',
427
+ id: "string-matching-examples",
388
428
  mocks: [
389
429
  // 1. Exact match (default)
390
430
  {
391
- method: 'GET',
392
- url: '/api/products',
431
+ method: "GET",
432
+ url: "/api/products",
393
433
  match: {
394
434
  headers: {
395
- 'x-user-tier': 'premium', // Must match exactly
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: 'GET',
404
- url: '/api/products',
443
+ method: "GET",
444
+ url: "/api/products",
405
445
  match: {
406
446
  headers: {
407
- 'x-user-tier': { equals: 'premium' },
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: 'GET',
416
- url: '/api/products',
455
+ method: "GET",
456
+ url: "/api/products",
417
457
  match: {
418
458
  headers: {
419
- 'x-campaign': { contains: 'summer' }, // Matches 'summer-sale', 'mega-summer-event', etc.
459
+ "x-campaign": { contains: "summer" }, // Matches 'summer-sale', 'mega-summer-event', etc.
420
460
  },
421
461
  },
422
- response: { status: 200, body: { campaign: 'summer' } },
462
+ response: { status: 200, body: { campaign: "summer" } },
423
463
  },
424
464
 
425
465
  // 4. Starts with (prefix match)
426
466
  {
427
- method: 'GET',
428
- url: '/api/keys',
467
+ method: "GET",
468
+ url: "/api/keys",
429
469
  match: {
430
470
  headers: {
431
- 'x-api-key': { startsWith: 'sk_' }, // Matches 'sk_test_123', 'sk_live_456', etc.
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: 'GET',
440
- url: '/api/users',
479
+ method: "GET",
480
+ url: "/api/users",
441
481
  match: {
442
482
  query: {
443
- email: { endsWith: '@company.com' }, // Matches 'john@company.com', 'admin@company.com', etc.
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: 'GET',
452
- url: '/api/products',
491
+ method: "GET",
492
+ url: "/api/products",
453
493
  match: {
454
494
  headers: {
455
495
  referer: {
456
496
  regex: {
457
- source: '/premium|/vip', // Matches any referer containing '/premium' or '/vip'
458
- flags: 'i', // Case-insensitive
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: 'premium' } },
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: 'native-regex-examples',
515
+ id: "native-regex-examples",
476
516
  mocks: [
477
517
  // Native RegExp in URL matching
478
518
  {
479
- method: 'GET',
480
- url: /\/api\/v\d+\/products/, // Matches /api/v1/products, /api/v2/products, etc.
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: 'GET',
487
- url: '/api/products',
526
+ method: "GET",
527
+ url: "/api/products",
488
528
  match: {
489
529
  headers: {
490
- referer: /\/premium|\/vip/i, // Case-insensitive pattern
530
+ referer: /\/premium|\/vip/i, // Case-insensitive pattern
491
531
  },
492
532
  },
493
- response: { status: 200, body: { tier: 'premium' } },
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: 'GET',
499
- url: '/api/data',
538
+ method: "GET",
539
+ url: "/api/data",
500
540
  match: {
501
541
  query: {
502
- filter: /^\w+$/, // Native RegExp
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
- { url: '/api/users/:id' }
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
- { url: 'http://api.example.com/users/:id' }
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
- { url: /\/users\/\d+/ }
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: '/api/products', // Works in dev, staging, prod
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: 'https://api.production.com/admin', // Only matches production
569
- response: { status: 403, body: { error: 'Admin disabled in prod' } }
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/, // Matches /api/v1/users, /api/v2/users, etc.
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
- { url: /\/api\/v\d+\// }
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
- { url: /\/users\/\d+$/ }
651
+ {
652
+ url: /\/users\/\d+$/;
653
+ }
598
654
  // Matches: /users/123 ✅
599
655
  // Rejects: /users/abc ❌
600
656
 
601
657
  // File extensions
602
- { url: /\.json$/ }
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
- { url: /\/products\/?$/ }
665
+ {
666
+ url: /\/products\/?$/;
667
+ }
608
668
  // Matches: /products ✅ and /products/ ✅
609
669
 
610
670
  // Multiple extensions
611
- { url: /\.(jpg|png|gif)$/i }
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
- { url: /\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i }
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
- { url: /(a+)+b/ }
701
+ {
702
+ url: /(a+)+b/;
703
+ }
638
704
  // Error: Unsafe regex pattern detected
639
705
 
640
706
  // ❌ REJECTED - Exponential time complexity
641
- { match: { headers: { referer: { regex: { source: '(x+x+)+y' } } } } }
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
- { url: /\/api\/[^/]+\/users/ }
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; // REQUIRED - scenarios object (must have 'default' key)
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> = BaseAdapterOptions<T> & {
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> = BaseAdapterOptions<T> & {
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; // Resolved configuration
710
- readonly middleware?: TMiddleware; // Framework-specific middleware (optional - Next.js doesn't have global middleware)
711
- readonly switchScenario: (testId: string, scenarioId: ScenarioIds<T>, variant?: string) => ScenaristResult<void, Error>;
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: (scenarioId: ScenarioIds<T>) => ScenaristScenario | undefined;
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, // Express Router
751
- switchScenario: (id, scenario, variant) => manager.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(),