@scenarist/msw-adapter 0.1.2 → 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 +29 -15
- package/dist/conversion/response-builder.d.ts +1 -1
- package/dist/conversion/response-builder.js +1 -1
- package/dist/handlers/dynamic-handler.d.ts +2 -2
- package/dist/handlers/dynamic-handler.d.ts.map +1 -1
- package/dist/handlers/dynamic-handler.js +7 -7
- package/dist/index.d.ts +6 -6
- package/dist/index.js +4 -4
- package/dist/matching/mock-matcher.d.ts +1 -1
- package/dist/matching/mock-matcher.js +1 -1
- package/dist/matching/url-matcher.js +4 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ This package is an internal implementation detail of Scenarist and is **NOT publ
|
|
|
7
7
|
## Why is this package private?
|
|
8
8
|
|
|
9
9
|
**You should install a framework adapter instead:**
|
|
10
|
+
|
|
10
11
|
- ✅ `@scenarist/express-adapter` - For Express applications
|
|
11
12
|
- ✅ `@scenarist/nextjs-adapter` - For Next.js applications
|
|
12
13
|
- ✅ `@scenarist/core` - For building custom adapters
|
|
@@ -21,6 +22,7 @@ This package is an internal implementation detail of Scenarist and is **NOT publ
|
|
|
21
22
|
6. **Cleaner API surface** - Framework adapters ARE the public API, msw-adapter is hidden complexity
|
|
22
23
|
|
|
23
24
|
**Dependency structure:**
|
|
25
|
+
|
|
24
26
|
```
|
|
25
27
|
Your App
|
|
26
28
|
├─ @scenarist/express-adapter (public, install this!)
|
|
@@ -36,7 +38,7 @@ Your App
|
|
|
36
38
|
|
|
37
39
|
## What is Scenarist?
|
|
38
40
|
|
|
39
|
-
**Scenarist** enables concurrent
|
|
41
|
+
**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. This package provides the MSW integration layer that makes it all work.
|
|
40
42
|
|
|
41
43
|
**The big picture:**
|
|
42
44
|
|
|
@@ -70,6 +72,7 @@ The MSW adapter is the bridge between Scenarist's serializable mock definitions
|
|
|
70
72
|
This adapter implements all 25 Scenarist capabilities:
|
|
71
73
|
|
|
72
74
|
### Request Matching (6 capabilities)
|
|
75
|
+
|
|
73
76
|
- **Body matching** (partial match) - Match on request body fields
|
|
74
77
|
- **Header matching** (exact, case-insensitive) - Match on header values
|
|
75
78
|
- **Query matching** (exact) - Match on query parameters
|
|
@@ -78,12 +81,14 @@ This adapter implements all 25 Scenarist capabilities:
|
|
|
78
81
|
- **Fallback mocks** - Mocks without criteria act as catch-all
|
|
79
82
|
|
|
80
83
|
### Response Sequences (4 capabilities)
|
|
84
|
+
|
|
81
85
|
- **Single responses** - Return same response every time
|
|
82
86
|
- **Response sequences** - Ordered responses for polling scenarios
|
|
83
87
|
- **Repeat modes** - `last`, `cycle`, `none` behaviors
|
|
84
88
|
- **Sequence exhaustion** - Skip exhausted sequences to fallback
|
|
85
89
|
|
|
86
90
|
### Stateful Mocks (6 capabilities)
|
|
91
|
+
|
|
87
92
|
- **State capture** - Extract values from requests
|
|
88
93
|
- **State injection** - Inject state into responses via templates
|
|
89
94
|
- **Array append** - Syntax: `stateKey[]` for arrays
|
|
@@ -92,12 +97,14 @@ This adapter implements all 25 Scenarist capabilities:
|
|
|
92
97
|
- **State reset** - Fresh state on scenario switch
|
|
93
98
|
|
|
94
99
|
### URL Patterns (4 capabilities)
|
|
100
|
+
|
|
95
101
|
- **Exact matches** - `https://api.example.com/users`
|
|
96
102
|
- **Wildcards** - `*/api/*`, `https://*/users`
|
|
97
103
|
- **Path parameters** - `/users/:id`, `/posts/:postId/comments/:commentId`
|
|
98
104
|
- **Native RegExp** - `/\/api\/v\d+\//`, `/\/users\/\d+$/` (weak comparison per MSW behavior)
|
|
99
105
|
|
|
100
106
|
### MSW Integration (6 capabilities)
|
|
107
|
+
|
|
101
108
|
- **Dynamic handler generation** - Single handler routes at runtime
|
|
102
109
|
- **Response building** - Status codes, JSON bodies, headers, delays
|
|
103
110
|
- **Automatic default fallback** - Collects default + active scenario mocks together
|
|
@@ -131,28 +138,35 @@ You typically don't install or use this directly. Instead, use a framework-speci
|
|
|
131
138
|
Converts string patterns into MSW-compatible URL matchers:
|
|
132
139
|
|
|
133
140
|
```typescript
|
|
134
|
-
import { matchesUrl } from
|
|
141
|
+
import { matchesUrl } from "@scenarist/msw-adapter";
|
|
135
142
|
|
|
136
|
-
matchesUrl(
|
|
143
|
+
matchesUrl(
|
|
144
|
+
"https://api.github.com/users/octocat",
|
|
145
|
+
"GET",
|
|
146
|
+
"https://api.github.com/users/:username",
|
|
147
|
+
);
|
|
137
148
|
// Returns: { matches: true, params: { username: 'octocat' } }
|
|
138
149
|
|
|
139
|
-
matchesUrl(
|
|
150
|
+
matchesUrl("https://api.stripe.com/v1/charges", "POST", "*/v1/charges");
|
|
140
151
|
// Returns: { matches: true, params: {} }
|
|
141
152
|
```
|
|
142
153
|
|
|
143
154
|
**Supported patterns (three types with different hostname matching):**
|
|
144
155
|
|
|
145
156
|
**1. Pathname-only patterns** (origin-agnostic - match ANY hostname)
|
|
157
|
+
|
|
146
158
|
- Path params: `/users/:id`, `/posts/:postId/comments/:commentId`
|
|
147
159
|
- Exact: `/api/users`
|
|
148
160
|
- Wildcards: `/api/*`, `*/users/*`
|
|
149
161
|
|
|
150
162
|
**2. Full URL patterns** (hostname-specific - match ONLY specified hostname)
|
|
163
|
+
|
|
151
164
|
- Exact: `https://api.example.com/users`
|
|
152
165
|
- Path params: `https://api.example.com/users/:id`
|
|
153
166
|
- Wildcards: `https://*/users`, `https://api.example.com/*`
|
|
154
167
|
|
|
155
168
|
**3. Native RegExp** (origin-agnostic - MSW weak comparison)
|
|
169
|
+
|
|
156
170
|
- `/\/api\/v\d+\//` (matches /api/v1/, /api/v2/, etc.)
|
|
157
171
|
- `/\/users\/\d+$/` (matches /users/123 at any origin)
|
|
158
172
|
|
|
@@ -179,15 +193,15 @@ const mock = findMatchingMock(mocks, 'GET', 'https://api.example.com/users/123')
|
|
|
179
193
|
Converts `ScenaristMock` to MSW `HttpResponse`:
|
|
180
194
|
|
|
181
195
|
```typescript
|
|
182
|
-
import { buildResponse } from
|
|
196
|
+
import { buildResponse } from "@scenarist/msw-adapter";
|
|
183
197
|
|
|
184
198
|
const mockDef: ScenaristMock = {
|
|
185
|
-
method:
|
|
186
|
-
url:
|
|
199
|
+
method: "GET",
|
|
200
|
+
url: "/api/user",
|
|
187
201
|
response: {
|
|
188
202
|
status: 200,
|
|
189
|
-
body: { id:
|
|
190
|
-
headers: {
|
|
203
|
+
body: { id: "123", name: "John" },
|
|
204
|
+
headers: { "X-Custom": "value" },
|
|
191
205
|
delay: 100,
|
|
192
206
|
},
|
|
193
207
|
};
|
|
@@ -201,11 +215,11 @@ const response = await buildResponse(mockDef);
|
|
|
201
215
|
The core integration - a single MSW handler that routes dynamically:
|
|
202
216
|
|
|
203
217
|
```typescript
|
|
204
|
-
import { createDynamicHandler } from
|
|
205
|
-
import { setupServer } from
|
|
218
|
+
import { createDynamicHandler } from "@scenarist/msw-adapter";
|
|
219
|
+
import { setupServer } from "msw/node";
|
|
206
220
|
|
|
207
221
|
const handler = createDynamicHandler({
|
|
208
|
-
getTestId: () => testIdStorage.getStore() ??
|
|
222
|
+
getTestId: () => testIdStorage.getStore() ?? "default-test",
|
|
209
223
|
getActiveScenario: (testId) => manager.getActiveScenario(testId),
|
|
210
224
|
getScenarioDefinition: (scenarioId) => manager.getScenarioById(scenarioId),
|
|
211
225
|
strictMode: false,
|
|
@@ -216,6 +230,7 @@ server.listen();
|
|
|
216
230
|
```
|
|
217
231
|
|
|
218
232
|
**How it works:**
|
|
233
|
+
|
|
219
234
|
1. Gets test ID from AsyncLocalStorage (or other context)
|
|
220
235
|
2. Looks up active scenario for that test ID
|
|
221
236
|
3. **Collects mocks from BOTH default AND active scenario** for matching URL + method
|
|
@@ -290,10 +305,9 @@ export const createDynamicHandler = (
|
|
|
290
305
|
Full TypeScript support with strict mode enabled.
|
|
291
306
|
|
|
292
307
|
**Exported types:**
|
|
308
|
+
|
|
293
309
|
```typescript
|
|
294
|
-
import type {
|
|
295
|
-
DynamicHandlerOptions,
|
|
296
|
-
} from '@scenarist/msw-adapter';
|
|
310
|
+
import type { DynamicHandlerOptions } from "@scenarist/msw-adapter";
|
|
297
311
|
```
|
|
298
312
|
|
|
299
313
|
## Testing
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { HttpHandler } from
|
|
2
|
-
import type { ActiveScenario, ScenaristScenario, ResponseSelector } from
|
|
1
|
+
import type { HttpHandler } from "msw";
|
|
2
|
+
import type { ActiveScenario, ScenaristScenario, ResponseSelector } from "@scenarist/core";
|
|
3
3
|
export type DynamicHandlerOptions = {
|
|
4
4
|
readonly getTestId: (request: Request) => string;
|
|
5
5
|
readonly getActiveScenario: (testId: string) => ActiveScenario | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-handler.d.ts","sourceRoot":"","sources":["../../src/handlers/dynamic-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EAIjB,gBAAgB,EACjB,MAAM,iBAAiB,CAAC;AAIzB,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IACjD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,cAAc,GAAG,SAAS,CAAC;IAC3E,QAAQ,CAAC,qBAAqB,EAAE,CAC9B,UAAU,EAAE,MAAM,KACf,iBAAiB,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;CAC7C,CAAC;
|
|
1
|
+
{"version":3,"file":"dynamic-handler.d.ts","sourceRoot":"","sources":["../../src/handlers/dynamic-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EAIjB,gBAAgB,EACjB,MAAM,iBAAiB,CAAC;AAIzB,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IACjD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,cAAc,GAAG,SAAS,CAAC;IAC3E,QAAQ,CAAC,qBAAqB,EAAE,CAC9B,UAAU,EAAE,MAAM,KACf,iBAAiB,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;CAC7C,CAAC;AAiGF,eAAO,MAAM,oBAAoB,GAC/B,SAAS,qBAAqB,KAC7B,WAmCF,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { http, passthrough } from
|
|
2
|
-
import { buildResponse } from
|
|
3
|
-
import { matchesUrl } from
|
|
1
|
+
import { http, passthrough } from "msw";
|
|
2
|
+
import { buildResponse } from "../conversion/response-builder.js";
|
|
3
|
+
import { matchesUrl } from "../matching/url-matcher.js";
|
|
4
4
|
/**
|
|
5
5
|
* Extract HttpRequestContext from MSW Request object.
|
|
6
6
|
* Converts MSW request to the format expected by ResponseSelector.
|
|
@@ -8,7 +8,7 @@ import { matchesUrl } from '../matching/url-matcher.js';
|
|
|
8
8
|
const extractHttpRequestContext = async (request) => {
|
|
9
9
|
// Parse request body if present
|
|
10
10
|
let body = undefined;
|
|
11
|
-
if (request.method !==
|
|
11
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
12
12
|
try {
|
|
13
13
|
const clonedRequest = request.clone();
|
|
14
14
|
body = await clonedRequest.json();
|
|
@@ -55,7 +55,7 @@ const getMocksFromScenarios = (activeScenario, getScenarioDefinition, method, ur
|
|
|
55
55
|
const mocksWithParams = [];
|
|
56
56
|
// Step 1: ALWAYS include default scenario mocks first
|
|
57
57
|
// These act as fallback when active scenario mocks don't match
|
|
58
|
-
const defaultScenario = getScenarioDefinition(
|
|
58
|
+
const defaultScenario = getScenarioDefinition("default");
|
|
59
59
|
if (defaultScenario) {
|
|
60
60
|
defaultScenario.mocks.forEach((mock) => {
|
|
61
61
|
const methodMatches = mock.method.toUpperCase() === method.toUpperCase();
|
|
@@ -82,10 +82,10 @@ const getMocksFromScenarios = (activeScenario, getScenarioDefinition, method, ur
|
|
|
82
82
|
return mocksWithParams;
|
|
83
83
|
};
|
|
84
84
|
export const createDynamicHandler = (options) => {
|
|
85
|
-
return http.all(
|
|
85
|
+
return http.all("*", async ({ request }) => {
|
|
86
86
|
const testId = options.getTestId(request);
|
|
87
87
|
const activeScenario = options.getActiveScenario(testId);
|
|
88
|
-
const scenarioId = activeScenario?.scenarioId ??
|
|
88
|
+
const scenarioId = activeScenario?.scenarioId ?? "default";
|
|
89
89
|
// Extract request context for matching
|
|
90
90
|
const context = await extractHttpRequestContext(request);
|
|
91
91
|
// Get candidate mocks from active or default scenario
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { matchesUrl } from
|
|
2
|
-
export type { UrlMatchResult } from
|
|
3
|
-
export { findMatchingMock } from
|
|
4
|
-
export { buildResponse } from
|
|
5
|
-
export { createDynamicHandler } from
|
|
6
|
-
export type { DynamicHandlerOptions } from
|
|
1
|
+
export { matchesUrl } from "./matching/url-matcher.js";
|
|
2
|
+
export type { UrlMatchResult } from "./matching/url-matcher.js";
|
|
3
|
+
export { findMatchingMock } from "./matching/mock-matcher.js";
|
|
4
|
+
export { buildResponse } from "./conversion/response-builder.js";
|
|
5
|
+
export { createDynamicHandler } from "./handlers/dynamic-handler.js";
|
|
6
|
+
export type { DynamicHandlerOptions } from "./handlers/dynamic-handler.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// These exports are primarily intended for use by Scenarist adapters
|
|
3
3
|
// (e.g., express-adapter, fastify-adapter). End users should use the
|
|
4
4
|
// higher-level adapter packages rather than these low-level utilities directly.
|
|
5
|
-
export { matchesUrl } from
|
|
6
|
-
export { findMatchingMock } from
|
|
7
|
-
export { buildResponse } from
|
|
8
|
-
export { createDynamicHandler } from
|
|
5
|
+
export { matchesUrl } from "./matching/url-matcher.js";
|
|
6
|
+
export { findMatchingMock } from "./matching/mock-matcher.js";
|
|
7
|
+
export { buildResponse } from "./conversion/response-builder.js";
|
|
8
|
+
export { createDynamicHandler } from "./handlers/dynamic-handler.js";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { ScenaristMock } from
|
|
1
|
+
import type { ScenaristMock } from "@scenarist/core";
|
|
2
2
|
export declare const findMatchingMock: (mocks: ReadonlyArray<ScenaristMock>, method: string, url: string) => ScenaristMock | undefined;
|
|
3
3
|
//# sourceMappingURL=mock-matcher.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { match } from
|
|
1
|
+
import { match } from "path-to-regexp";
|
|
2
2
|
/**
|
|
3
3
|
* Extract pathname from URL string, or return as-is if not a valid URL.
|
|
4
4
|
*
|
|
@@ -17,7 +17,7 @@ const extractPathnameOrReturnAsIs = (url) => {
|
|
|
17
17
|
const match = urlPattern.exec(url);
|
|
18
18
|
if (match) {
|
|
19
19
|
// Return everything after the host (group 1), or '/' if no path
|
|
20
|
-
return match[1] ||
|
|
20
|
+
return match[1] || "/";
|
|
21
21
|
}
|
|
22
22
|
// Not a full URL, return as-is (already a pathname)
|
|
23
23
|
return url;
|
|
@@ -39,7 +39,7 @@ const extractParams = (params) => {
|
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
41
|
// Keep strings and arrays (MSW documented: string | string[])
|
|
42
|
-
return typeof value ===
|
|
42
|
+
return typeof value === "string" || Array.isArray(value);
|
|
43
43
|
}));
|
|
44
44
|
};
|
|
45
45
|
/**
|
|
@@ -128,7 +128,7 @@ export const matchesUrl = (pattern, requestUrl) => {
|
|
|
128
128
|
const patternPath = extractPathnameOrReturnAsIs(pattern);
|
|
129
129
|
let requestPath = extractPathnameOrReturnAsIs(requestUrl);
|
|
130
130
|
// Strip query parameters from request path
|
|
131
|
-
const queryIndex = requestPath.indexOf(
|
|
131
|
+
const queryIndex = requestPath.indexOf("?");
|
|
132
132
|
if (queryIndex !== -1) {
|
|
133
133
|
requestPath = requestPath.substring(0, queryIndex);
|
|
134
134
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scenarist/msw-adapter",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Internal: MSW integration layer for Scenarist framework adapters",
|
|
5
5
|
"author": "Paul Hammond (citypaul) <paul@packsoftware.co.uk>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"path-to-regexp": "^6.3.0",
|
|
45
|
-
"@scenarist/core": "0.1.
|
|
45
|
+
"@scenarist/core": "0.1.3"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"msw": "^2.0.0"
|