@r0hitsharma/http-client-msw 0.12.0-rohit-fork-ci.1
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 +251 -0
- package/dist/base-url.d.ts +46 -0
- package/dist/base-url.js +65 -0
- package/dist/browser.d.ts +50 -0
- package/dist/browser.js +59 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/mock-api.d.ts +75 -0
- package/dist/mock-api.js +35 -0
- package/dist/mock-delay.d.ts +35 -0
- package/dist/mock-delay.js +50 -0
- package/dist/mock-store.d.ts +76 -0
- package/dist/mock-store.js +114 -0
- package/dist/node.d.ts +41 -0
- package/dist/node.js +41 -0
- package/dist/seeded-rng.d.ts +27 -0
- package/dist/seeded-rng.js +52 -0
- package/dist/setup.d.ts +54 -0
- package/dist/setup.js +26 -0
- package/dist/worker-options.d.ts +55 -0
- package/dist/worker-options.js +49 -0
- package/package.json +59 -0
- package/src/base-url.test.ts +177 -0
- package/src/base-url.ts +80 -0
- package/src/browser.ts +88 -0
- package/src/index.ts +53 -0
- package/src/mock-api.test.ts +148 -0
- package/src/mock-api.ts +96 -0
- package/src/mock-api.types.test.ts +119 -0
- package/src/mock-delay.test.ts +51 -0
- package/src/mock-delay.ts +68 -0
- package/src/mock-store.test.ts +263 -0
- package/src/mock-store.ts +180 -0
- package/src/node.ts +61 -0
- package/src/seeded-rng.test.ts +106 -0
- package/src/seeded-rng.ts +77 -0
- package/src/setup.test.ts +137 -0
- package/src/setup.ts +71 -0
- package/src/test-fixtures.ts +113 -0
- package/src/worker-options.test.ts +128 -0
- package/src/worker-options.ts +108 -0
package/README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# @r0hitsharma/http-client-msw
|
|
2
|
+
|
|
3
|
+
Typed [msw](https://mswjs.io) mocks for `@r0hitsharma/http-client-core`.
|
|
4
|
+
|
|
5
|
+
The generated OpenAPI `paths` type **is** the endpoint definition. The same type
|
|
6
|
+
that drives `createApiClient` and `createQueryApi` drives the mock handlers, so a
|
|
7
|
+
handler path, its params, and the body it answers with are checked against the
|
|
8
|
+
contract — a fixture that no longer matches the API fails to typecheck instead of
|
|
9
|
+
quietly making every test that reads it a false pass. See
|
|
10
|
+
[DESIGN.md](./DESIGN.md) for the contract, the `openapi-msw` verdict, and the
|
|
11
|
+
deliberate limits.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -D @r0hitsharma/http-client-msw msw
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`msw` is a peer dependency: the service worker script is generated from the
|
|
20
|
+
installed msw and version-checked at start, so the app has to own the version.
|
|
21
|
+
|
|
22
|
+
Generate the worker script once and commit it:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx msw init public --save
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`public/mockServiceWorker.js` is a build artifact that belongs in git — the app
|
|
29
|
+
serves it, and `--save` records the path in `package.json` so msw warns when an
|
|
30
|
+
upgrade leaves it stale. Adjust `public` if the bundler serves static files from
|
|
31
|
+
elsewhere.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### 1. Generate types
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx uikit-openapi-generate --schema openapi.json --output src/api.types.ts
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The same `src/api.types.ts` the client and query layer already use. Nothing about
|
|
42
|
+
the mocks is described twice.
|
|
43
|
+
|
|
44
|
+
### 2. Write the handlers
|
|
45
|
+
|
|
46
|
+
Keep them in one module — a `src/mocks/` folder in the app, or a workspace
|
|
47
|
+
package (`@your-scope/api-mocks`) when a Playwright suite and a unit-test suite
|
|
48
|
+
both consume them:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// src/mocks/index.ts
|
|
52
|
+
import {
|
|
53
|
+
createMockApi,
|
|
54
|
+
createMockStore,
|
|
55
|
+
createSeededRng,
|
|
56
|
+
mockDelay,
|
|
57
|
+
setupMocks,
|
|
58
|
+
} from '@r0hitsharma/http-client-msw';
|
|
59
|
+
|
|
60
|
+
import type { paths } from '../api.types';
|
|
61
|
+
|
|
62
|
+
// The same baseUrl the app passes to `createApiClient`.
|
|
63
|
+
const mock = createMockApi<paths>({ baseUrl: '/api' });
|
|
64
|
+
|
|
65
|
+
const rng = createSeededRng(1337);
|
|
66
|
+
const positions = createMockStore(() =>
|
|
67
|
+
Array.from({ length: 8 }, (_, index) => ({
|
|
68
|
+
id: `p${index + 1}`,
|
|
69
|
+
label: `Position ${index + 1}`,
|
|
70
|
+
health: rng.int(50, 200) / 100,
|
|
71
|
+
})),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
export const mocks = setupMocks(
|
|
75
|
+
[
|
|
76
|
+
mock.get('/positions', async ({ query, response }) => {
|
|
77
|
+
await mockDelay(300);
|
|
78
|
+
const limit = Number(query.get('limit') ?? '25');
|
|
79
|
+
|
|
80
|
+
return response(200).json(positions.list().slice(0, limit));
|
|
81
|
+
}),
|
|
82
|
+
|
|
83
|
+
mock.get('/positions/{id}', ({ params, response }) => {
|
|
84
|
+
const position = positions.get(params.id);
|
|
85
|
+
|
|
86
|
+
// Both branches are checked: 200 takes a Position, 404 takes the
|
|
87
|
+
// operation's own error body.
|
|
88
|
+
return position
|
|
89
|
+
? response(200).json(position)
|
|
90
|
+
: response(404).json({ message: `no position ${params.id}` });
|
|
91
|
+
}),
|
|
92
|
+
|
|
93
|
+
mock.post('/positions/{id}/close', ({ params, response }) =>
|
|
94
|
+
positions.remove(params.id)
|
|
95
|
+
? response(204).empty()
|
|
96
|
+
: response(404).json({ message: 'already closed' }),
|
|
97
|
+
),
|
|
98
|
+
],
|
|
99
|
+
// Everything a handler writes to is restored by `reset()`. Order matters:
|
|
100
|
+
// callbacks run as declared, and the store's seed function draws from the rng,
|
|
101
|
+
// so the rng has to be rewound first or each reset re-seeds from a different
|
|
102
|
+
// point in the sequence.
|
|
103
|
+
{ onReset: [rng.reset, positions.reset] },
|
|
104
|
+
);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For a route the OpenAPI document does not describe at all — an auth callback on
|
|
108
|
+
another host, say — `mock.untyped` is msw's own `http` object, and
|
|
109
|
+
`response.untyped(new Response(...))` returns an arbitrary response from a typed
|
|
110
|
+
handler.
|
|
111
|
+
|
|
112
|
+
### 3. Run them in the browser (dev, and Playwright)
|
|
113
|
+
|
|
114
|
+
Start the worker **before** rendering, so no component can fire a request the
|
|
115
|
+
worker is not yet intercepting:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
// src/main.tsx
|
|
119
|
+
import { createRoot } from 'react-dom/client';
|
|
120
|
+
|
|
121
|
+
import { App } from './App';
|
|
122
|
+
|
|
123
|
+
if (import.meta.env.VITE_API_MOCKS === '1') {
|
|
124
|
+
const { setupMockWorker } = await import(
|
|
125
|
+
'@r0hitsharma/http-client-msw/browser'
|
|
126
|
+
);
|
|
127
|
+
const { mocks } = await import('./mocks');
|
|
128
|
+
|
|
129
|
+
// `baseUrl` here is the app's public base path, not the API base: it locates
|
|
130
|
+
// `mockServiceWorker.js` for a subpath deployment.
|
|
131
|
+
const mockWorker = setupMockWorker(mocks, {
|
|
132
|
+
baseUrl: import.meta.env.BASE_URL,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Reachable from a Playwright test; see below.
|
|
136
|
+
Object.assign(window, { resetMocks: () => mockWorker.reset() });
|
|
137
|
+
|
|
138
|
+
await mockWorker.start();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
createRoot(document.getElementById('root')!).render(<App />);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
VITE_API_MOCKS=1 npm run dev
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The gate matters as much as the mocks. Vite replaces `import.meta.env.VITE_*`
|
|
149
|
+
statically, so with the flag unset the whole branch is dead code and neither msw
|
|
150
|
+
nor the fixtures reach a production bundle — which is only true because both
|
|
151
|
+
imports are dynamic and behind the check. A runtime `process.env` read or a
|
|
152
|
+
static `import` of the mocks module would bundle them either way.
|
|
153
|
+
|
|
154
|
+
`start()` is idempotent, so a hot reload or a test fixture may call it again
|
|
155
|
+
without re-registering the worker. `stop()` releases that, so a later `start()`
|
|
156
|
+
registers again rather than resolving into a stopped worker.
|
|
157
|
+
|
|
158
|
+
For a Playwright suite, serve the app with the same flag on — the browser worker
|
|
159
|
+
answers from the same handlers — and reset between tests through the hook the
|
|
160
|
+
entry exposed:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// tests/positions.spec.ts
|
|
164
|
+
declare global {
|
|
165
|
+
interface Window {
|
|
166
|
+
resetMocks?: () => void;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
test.beforeEach(async ({ page }) => {
|
|
171
|
+
await page.goto('/');
|
|
172
|
+
await page.evaluate(() => window.resetMocks?.());
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
A full reload re-runs the entry and re-seeds the stores anyway; the explicit
|
|
177
|
+
`resetMocks()` matters when a test navigates within the app instead.
|
|
178
|
+
|
|
179
|
+
### 4. Run them in vitest
|
|
180
|
+
|
|
181
|
+
Same handler array, node interceptors:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// src/test-setup.ts
|
|
185
|
+
import { setupMockServer } from '@r0hitsharma/http-client-msw/node';
|
|
186
|
+
import { afterAll, afterEach, beforeAll } from 'vitest';
|
|
187
|
+
|
|
188
|
+
import { mocks } from './mocks';
|
|
189
|
+
|
|
190
|
+
const mockServer = setupMockServer(mocks);
|
|
191
|
+
|
|
192
|
+
// `onUnhandledRequest` defaults to 'error': an unmocked request in a suite is a
|
|
193
|
+
// hole in the fixtures, not something to warn about and scroll past.
|
|
194
|
+
beforeAll(() => mockServer.listen());
|
|
195
|
+
afterEach(() => mockServer.reset());
|
|
196
|
+
afterAll(() => mockServer.close());
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
// vitest.config.ts
|
|
201
|
+
export default defineConfig({
|
|
202
|
+
test: { setupFiles: ['./src/test-setup.ts'] },
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`reset()` covers both kinds of leakage between tests: state a handler wrote, and
|
|
207
|
+
handlers a test installed with `mockServer.server.use(...)`.
|
|
208
|
+
|
|
209
|
+
An origin-relative `baseUrl` works in both environments because handler paths are
|
|
210
|
+
matched on any origin by default — msw would otherwise leave a relative path
|
|
211
|
+
unmatched under `setupServer`, where every request URL is absolute. Pass
|
|
212
|
+
`origin: 'exact'` to `createMockApi` to opt out; see
|
|
213
|
+
[DESIGN.md](./DESIGN.md#handler-path-and-origin-matching).
|
|
214
|
+
|
|
215
|
+
## API surface
|
|
216
|
+
|
|
217
|
+
| Export | What it does |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| `createMockApi<TPaths>(options?)` | Typed handler factories per method, plus `untyped` |
|
|
220
|
+
| `setupMocks(handlers, options?)` | Bundles handlers with their state resets, environment-neutral |
|
|
221
|
+
| `setupMockWorker(mocks, options?)` | **`/browser`.** Serves them from a service worker; idempotent `start()` |
|
|
222
|
+
| `setupMockServer(mocks)` | **`/node`.** Serves them from msw's node interceptors |
|
|
223
|
+
| `createMockStore(seedFn, options?)` | In-memory collection so a write shows up in the next read |
|
|
224
|
+
| `createSeededRng(seed)` | Deterministic PRNG for reproducible generated fixtures |
|
|
225
|
+
| `mockDelay(ms \| { test, dev })` | Env-aware latency; no delay under test by default |
|
|
226
|
+
| `resolveMockDelay` / `isTestEnvironment` | The delay decision, for a consumer's own helpers |
|
|
227
|
+
| `resolveWorkerScriptUrl` / `normalizeApiBaseUrl` / `resolveHandlerBase` / `isAbsoluteUrl` | The URL primitives |
|
|
228
|
+
| `buildWorkerStartOptions` / `createIdempotentStart` | **`/browser`.** The start decisions, unit-testable |
|
|
229
|
+
|
|
230
|
+
Handler and resolver types are re-exported as `MockHandler`,
|
|
231
|
+
`MockResponseResolver`, `MockPathsFor`, `MockRequestBodyFor`, and
|
|
232
|
+
`MockResponseBodyFor`, so a helper written around a resolver types against the
|
|
233
|
+
same msw copy this package resolves.
|
|
234
|
+
|
|
235
|
+
## Not in v1
|
|
236
|
+
|
|
237
|
+
Handlers generated from the OpenAPI document, runtime request-body validation,
|
|
238
|
+
named scenario switching, GraphQL/websocket typing, per-test handler isolation
|
|
239
|
+
via msw's `boundary`, and fault injection are deliberately out of scope — see
|
|
240
|
+
[DESIGN.md](./DESIGN.md#deliberately-out-of-v1).
|
|
241
|
+
|
|
242
|
+
## Peer dependencies
|
|
243
|
+
|
|
244
|
+
- `msw` (`^2.10.5`)
|
|
245
|
+
|
|
246
|
+
## See also
|
|
247
|
+
|
|
248
|
+
- [http-client-core](../http-client-core) for the client factory, the OpenAPI
|
|
249
|
+
type generator, and the zod helpers
|
|
250
|
+
- [http-client-react](../http-client-react) for the TanStack Query layer keyed off
|
|
251
|
+
the same `paths` type
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two different base URLs live in this package and they normalize to opposite
|
|
3
|
+
* shapes, so both live here rather than being inlined at their call sites:
|
|
4
|
+
*
|
|
5
|
+
* - the **API** base (`createMockApi({ baseUrl })`) is prepended to OpenAPI path
|
|
6
|
+
* templates, which already start with `/`, so it must not end in one;
|
|
7
|
+
* - the **app** base (`setupMockWorker(mocks, { baseUrl })`) is the public base
|
|
8
|
+
* path a subpath deployment is served from, and the worker script filename is
|
|
9
|
+
* appended to it, so it must end in one.
|
|
10
|
+
*/
|
|
11
|
+
/** How an origin-relative API base is matched. See {@link resolveHandlerBase}. */
|
|
12
|
+
export type MockOriginMatching = 'any' | 'exact';
|
|
13
|
+
/**
|
|
14
|
+
* Whether the base carries its own scheme. A protocol-relative `//host` does
|
|
15
|
+
* not, and so is *not* absolute here — see {@link resolveHandlerBase}, which
|
|
16
|
+
* has to prefix one for msw to match it at all.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isAbsoluteUrl(url: string): boolean;
|
|
19
|
+
/** Strips trailing slashes so `${base}${'/things'}` never doubles up. */
|
|
20
|
+
export declare function normalizeApiBaseUrl(baseUrl?: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* The prefix every handler path is built on.
|
|
23
|
+
*
|
|
24
|
+
* msw resolves a relative handler path against `document.baseURI` in the browser
|
|
25
|
+
* and leaves it relative in node, where request URLs are always absolute — so a
|
|
26
|
+
* relative path silently matches nothing under `setupServer`. Prefixing an
|
|
27
|
+
* origin-relative base with msw's `*` wildcard makes one handler array match in
|
|
28
|
+
* both, which is what lets the same mocks serve dev, vitest, and Playwright.
|
|
29
|
+
*
|
|
30
|
+
* `'exact'` opts out and keeps the path relative — same-origin matching only,
|
|
31
|
+
* and node tests then need an absolute `baseUrl`. An absolute base already pins
|
|
32
|
+
* the origin, so the setting does not apply to one.
|
|
33
|
+
*
|
|
34
|
+
* A protocol-relative `//host` is prefixed under *both* settings. It pins the
|
|
35
|
+
* host but not the scheme, and msw matches a bare `//host` pattern against
|
|
36
|
+
* neither `http:` nor `https:` — the request escapes to the real network. The
|
|
37
|
+
* wildcard stands in for the scheme only, so the host stays pinned.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveHandlerBase(baseUrl: string | undefined, origin: MockOriginMatching): string;
|
|
40
|
+
/**
|
|
41
|
+
* The URL msw's service worker script is served from. It follows the app's
|
|
42
|
+
* public base path — `import.meta.env.BASE_URL` under Vite — because a bundler
|
|
43
|
+
* copies `public/mockServiceWorker.js` to `${base}mockServiceWorker.js`, and the
|
|
44
|
+
* worker's scope is limited to the directory it is served from.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveWorkerScriptUrl(baseUrl?: string): string;
|
package/dist/base-url.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two different base URLs live in this package and they normalize to opposite
|
|
3
|
+
* shapes, so both live here rather than being inlined at their call sites:
|
|
4
|
+
*
|
|
5
|
+
* - the **API** base (`createMockApi({ baseUrl })`) is prepended to OpenAPI path
|
|
6
|
+
* templates, which already start with `/`, so it must not end in one;
|
|
7
|
+
* - the **app** base (`setupMockWorker(mocks, { baseUrl })`) is the public base
|
|
8
|
+
* path a subpath deployment is served from, and the worker script filename is
|
|
9
|
+
* appended to it, so it must end in one.
|
|
10
|
+
*/
|
|
11
|
+
/** A scheme plus authority — `https://api.test`. Pins both scheme and host. */
|
|
12
|
+
const ABSOLUTE_URL = /^[a-z][a-z\d+.-]*:\/\//i;
|
|
13
|
+
/** A protocol-relative `//api.test` — pins the host, leaves the scheme open. */
|
|
14
|
+
const PROTOCOL_RELATIVE_URL = /^\/\//;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the base carries its own scheme. A protocol-relative `//host` does
|
|
17
|
+
* not, and so is *not* absolute here — see {@link resolveHandlerBase}, which
|
|
18
|
+
* has to prefix one for msw to match it at all.
|
|
19
|
+
*/
|
|
20
|
+
export function isAbsoluteUrl(url) {
|
|
21
|
+
return ABSOLUTE_URL.test(url);
|
|
22
|
+
}
|
|
23
|
+
/** Strips trailing slashes so `${base}${'/things'}` never doubles up. */
|
|
24
|
+
export function normalizeApiBaseUrl(baseUrl) {
|
|
25
|
+
if (!baseUrl)
|
|
26
|
+
return '';
|
|
27
|
+
return baseUrl.replace(/\/+$/, '');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The prefix every handler path is built on.
|
|
31
|
+
*
|
|
32
|
+
* msw resolves a relative handler path against `document.baseURI` in the browser
|
|
33
|
+
* and leaves it relative in node, where request URLs are always absolute — so a
|
|
34
|
+
* relative path silently matches nothing under `setupServer`. Prefixing an
|
|
35
|
+
* origin-relative base with msw's `*` wildcard makes one handler array match in
|
|
36
|
+
* both, which is what lets the same mocks serve dev, vitest, and Playwright.
|
|
37
|
+
*
|
|
38
|
+
* `'exact'` opts out and keeps the path relative — same-origin matching only,
|
|
39
|
+
* and node tests then need an absolute `baseUrl`. An absolute base already pins
|
|
40
|
+
* the origin, so the setting does not apply to one.
|
|
41
|
+
*
|
|
42
|
+
* A protocol-relative `//host` is prefixed under *both* settings. It pins the
|
|
43
|
+
* host but not the scheme, and msw matches a bare `//host` pattern against
|
|
44
|
+
* neither `http:` nor `https:` — the request escapes to the real network. The
|
|
45
|
+
* wildcard stands in for the scheme only, so the host stays pinned.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveHandlerBase(baseUrl, origin) {
|
|
48
|
+
const normalized = normalizeApiBaseUrl(baseUrl);
|
|
49
|
+
if (isAbsoluteUrl(normalized))
|
|
50
|
+
return normalized;
|
|
51
|
+
const pinsHost = PROTOCOL_RELATIVE_URL.test(normalized);
|
|
52
|
+
if (origin === 'exact' && !pinsHost)
|
|
53
|
+
return normalized;
|
|
54
|
+
return `*${normalized}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The URL msw's service worker script is served from. It follows the app's
|
|
58
|
+
* public base path — `import.meta.env.BASE_URL` under Vite — because a bundler
|
|
59
|
+
* copies `public/mockServiceWorker.js` to `${base}mockServiceWorker.js`, and the
|
|
60
|
+
* worker's scope is limited to the directory it is served from.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveWorkerScriptUrl(baseUrl) {
|
|
63
|
+
const base = baseUrl && baseUrl.length > 0 ? baseUrl : '/';
|
|
64
|
+
return `${base.endsWith('/') ? base : `${base}/`}mockServiceWorker.js`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser entry: `@r0hitsharma/http-client-msw/browser`.
|
|
3
|
+
*
|
|
4
|
+
* Kept behind its own subpath so `msw/browser` — and the service-worker
|
|
5
|
+
* machinery it pulls in — never reaches a node test's module graph, and so an
|
|
6
|
+
* app bundle that imports only this entry does not drag in `msw/node`.
|
|
7
|
+
*/
|
|
8
|
+
import { type SetupWorker } from 'msw/browser';
|
|
9
|
+
import type { MockSetup } from './setup.js';
|
|
10
|
+
import { type MockWorkerOptions } from './worker-options.js';
|
|
11
|
+
/**
|
|
12
|
+
* Re-exported here rather than from the package root: their types reference
|
|
13
|
+
* `msw/browser`, which does not resolve under a node-only condition set.
|
|
14
|
+
*/
|
|
15
|
+
export { buildWorkerStartOptions, createIdempotentStart, type IdempotentStart, type MockWorkerOptions, } from './worker-options.js';
|
|
16
|
+
export type MockWorker = {
|
|
17
|
+
/** The underlying msw worker, for `use()` and lifecycle events. */
|
|
18
|
+
worker: SetupWorker;
|
|
19
|
+
/** Registers and activates the worker. Idempotent; safe to await anywhere. */
|
|
20
|
+
start: () => Promise<void>;
|
|
21
|
+
/** Stops interception. A later `start()` re-registers the worker. */
|
|
22
|
+
stop: () => void;
|
|
23
|
+
/** Runs the setup's state resets, then drops runtime handler overrides. */
|
|
24
|
+
reset: () => void;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Serves a {@link MockSetup} from a service worker.
|
|
28
|
+
*
|
|
29
|
+
* Call `start()` and await it **before** rendering, so no component can fire a
|
|
30
|
+
* request the worker is not yet intercepting:
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* // src/main.tsx
|
|
34
|
+
* if (import.meta.env.VITE_API_MOCKS === '1') {
|
|
35
|
+
* const { setupMockWorker } = await import(
|
|
36
|
+
* '@r0hitsharma/http-client-msw/browser'
|
|
37
|
+
* );
|
|
38
|
+
* const { mocks } = await import('./mocks');
|
|
39
|
+
*
|
|
40
|
+
* await setupMockWorker(mocks, { baseUrl: import.meta.env.BASE_URL }).start();
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* createRoot(document.getElementById('root')!).render(<App />);
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* The dynamic imports are what keep msw and the fixtures out of a production
|
|
47
|
+
* bundle: with a statically analysable `import.meta.env` flag, the whole branch
|
|
48
|
+
* is dead code a bundler drops.
|
|
49
|
+
*/
|
|
50
|
+
export declare function setupMockWorker(mocks: MockSetup, options?: MockWorkerOptions): MockWorker;
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser entry: `@r0hitsharma/http-client-msw/browser`.
|
|
3
|
+
*
|
|
4
|
+
* Kept behind its own subpath so `msw/browser` — and the service-worker
|
|
5
|
+
* machinery it pulls in — never reaches a node test's module graph, and so an
|
|
6
|
+
* app bundle that imports only this entry does not drag in `msw/node`.
|
|
7
|
+
*/
|
|
8
|
+
import { setupWorker } from 'msw/browser';
|
|
9
|
+
import { buildWorkerStartOptions, createIdempotentStart, } from './worker-options.js';
|
|
10
|
+
/**
|
|
11
|
+
* Re-exported here rather than from the package root: their types reference
|
|
12
|
+
* `msw/browser`, which does not resolve under a node-only condition set.
|
|
13
|
+
*/
|
|
14
|
+
export { buildWorkerStartOptions, createIdempotentStart, } from './worker-options.js';
|
|
15
|
+
/**
|
|
16
|
+
* Serves a {@link MockSetup} from a service worker.
|
|
17
|
+
*
|
|
18
|
+
* Call `start()` and await it **before** rendering, so no component can fire a
|
|
19
|
+
* request the worker is not yet intercepting:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* // src/main.tsx
|
|
23
|
+
* if (import.meta.env.VITE_API_MOCKS === '1') {
|
|
24
|
+
* const { setupMockWorker } = await import(
|
|
25
|
+
* '@r0hitsharma/http-client-msw/browser'
|
|
26
|
+
* );
|
|
27
|
+
* const { mocks } = await import('./mocks');
|
|
28
|
+
*
|
|
29
|
+
* await setupMockWorker(mocks, { baseUrl: import.meta.env.BASE_URL }).start();
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* createRoot(document.getElementById('root')!).render(<App />);
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* The dynamic imports are what keep msw and the fixtures out of a production
|
|
36
|
+
* bundle: with a statically analysable `import.meta.env` flag, the whole branch
|
|
37
|
+
* is dead code a bundler drops.
|
|
38
|
+
*/
|
|
39
|
+
export function setupMockWorker(mocks, options = {}) {
|
|
40
|
+
const worker = setupWorker(...mocks.handlers);
|
|
41
|
+
const startOptions = buildWorkerStartOptions(options);
|
|
42
|
+
const { start, invalidate } = createIdempotentStart(async () => {
|
|
43
|
+
await worker.start(startOptions);
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
worker,
|
|
47
|
+
start,
|
|
48
|
+
stop: () => {
|
|
49
|
+
worker.stop();
|
|
50
|
+
// A stopped worker intercepts nothing, so the next `start()` has to
|
|
51
|
+
// re-register rather than resolve from the memo.
|
|
52
|
+
invalidate();
|
|
53
|
+
},
|
|
54
|
+
reset: () => {
|
|
55
|
+
mocks.resetState();
|
|
56
|
+
worker.resetHandlers();
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed msw mocks keyed off a generated OpenAPI `paths` type — the same type
|
|
3
|
+
* that drives `createApiClient` and `createQueryApi`, so handlers, params, and
|
|
4
|
+
* fixture bodies are checked against one contract.
|
|
5
|
+
*
|
|
6
|
+
* Environment wiring lives behind subpath entries so neither environment's msw
|
|
7
|
+
* import ends up in the other's bundle:
|
|
8
|
+
*
|
|
9
|
+
* - `@r0hitsharma/http-client-msw/browser` — `setupMockWorker`
|
|
10
|
+
* - `@r0hitsharma/http-client-msw/node` — `setupMockServer`
|
|
11
|
+
*
|
|
12
|
+
* Nothing here references `msw/browser` or `msw/node`, so this entry stays
|
|
13
|
+
* resolvable from either environment.
|
|
14
|
+
*/
|
|
15
|
+
export { createMockApi, type MockApi, type MockApiOptions, type MockHandler, type MockOriginMatching, type MockPathsFor, type MockRequestBodyFor, type MockRequestHandler, type MockResponseBodyFor, type MockResponseResolver, type MockResponseResolverInfo, } from './mock-api.js';
|
|
16
|
+
export { isAbsoluteUrl, normalizeApiBaseUrl, resolveHandlerBase, resolveWorkerScriptUrl, } from './base-url.js';
|
|
17
|
+
export { isTestEnvironment, mockDelay, type MockDelayInput, resolveMockDelay, } from './mock-delay.js';
|
|
18
|
+
export { createMockStore, type MockStore, type MockStoreOptions, } from './mock-store.js';
|
|
19
|
+
export { createSeededRng, type SeededRng } from './seeded-rng.js';
|
|
20
|
+
export { type MockResetCallback, type MockSetup, type MockSetupHandler, type MockSetupOptions, setupMocks, } from './setup.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed msw mocks keyed off a generated OpenAPI `paths` type — the same type
|
|
3
|
+
* that drives `createApiClient` and `createQueryApi`, so handlers, params, and
|
|
4
|
+
* fixture bodies are checked against one contract.
|
|
5
|
+
*
|
|
6
|
+
* Environment wiring lives behind subpath entries so neither environment's msw
|
|
7
|
+
* import ends up in the other's bundle:
|
|
8
|
+
*
|
|
9
|
+
* - `@r0hitsharma/http-client-msw/browser` — `setupMockWorker`
|
|
10
|
+
* - `@r0hitsharma/http-client-msw/node` — `setupMockServer`
|
|
11
|
+
*
|
|
12
|
+
* Nothing here references `msw/browser` or `msw/node`, so this entry stays
|
|
13
|
+
* resolvable from either environment.
|
|
14
|
+
*/
|
|
15
|
+
export { createMockApi, } from './mock-api.js';
|
|
16
|
+
export { isAbsoluteUrl, normalizeApiBaseUrl, resolveHandlerBase, resolveWorkerScriptUrl, } from './base-url.js';
|
|
17
|
+
export { isTestEnvironment, mockDelay, resolveMockDelay, } from './mock-delay.js';
|
|
18
|
+
export { createMockStore, } from './mock-store.js';
|
|
19
|
+
export { createSeededRng } from './seeded-rng.js';
|
|
20
|
+
export { setupMocks, } from './setup.js';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type OpenApiHttpHandlers } from 'openapi-msw';
|
|
2
|
+
import { type MockOriginMatching } from './base-url.js';
|
|
3
|
+
/**
|
|
4
|
+
* The msw handler types this package's surface is expressed in. They are
|
|
5
|
+
* re-exported so a consumer writing a helper around a resolver types against
|
|
6
|
+
* the copy of msw this package resolves, rather than importing msw types in one
|
|
7
|
+
* file and ours in another.
|
|
8
|
+
*/
|
|
9
|
+
export type { HttpHandler as MockHandler, RequestHandler as MockRequestHandler, } from 'msw';
|
|
10
|
+
export type { MockOriginMatching } from './base-url.js';
|
|
11
|
+
export type { ResponseResolver as MockResponseResolver, ResponseResolverInfo as MockResponseResolverInfo, } from 'openapi-msw';
|
|
12
|
+
export type { PathsFor as MockPathsFor, RequestBodyFor as MockRequestBodyFor, ResponseBodyFor as MockResponseBodyFor, } from 'openapi-msw';
|
|
13
|
+
export type MockApiOptions = {
|
|
14
|
+
/**
|
|
15
|
+
* Prepended to every handler path, for an API mounted under a prefix. Given
|
|
16
|
+
* `'/api'`, a handler declared on `/things/{id}` matches `/api/things/:id`.
|
|
17
|
+
* Pass the same value the app passes to `createApiClient`.
|
|
18
|
+
*
|
|
19
|
+
* Either an origin-relative path with a leading slash (`'/api'`) or an absolute
|
|
20
|
+
* URL (`'https://api.test/v1'`); a trailing slash is tolerated. It is
|
|
21
|
+
* concatenated with the OpenAPI path, so anything else — a query string, a
|
|
22
|
+
* fragment, a missing leading slash — yields a pattern that matches nothing,
|
|
23
|
+
* which surfaces as an unhandled request rather than an error.
|
|
24
|
+
*/
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
/**
|
|
27
|
+
* How an origin-relative `baseUrl` is matched.
|
|
28
|
+
*
|
|
29
|
+
* - `'any'` (default) prefixes handler paths with msw's `*` origin wildcard, so
|
|
30
|
+
* one handler array matches both the relative request a browser app makes
|
|
31
|
+
* and the absolute URL a node test has to issue. That is what makes the same
|
|
32
|
+
* mocks reusable across dev, vitest, and Playwright.
|
|
33
|
+
* - `'exact'` leaves paths relative: same-origin matching only, and a node
|
|
34
|
+
* test then needs an absolute `baseUrl` of its own.
|
|
35
|
+
*
|
|
36
|
+
* An absolute `baseUrl` already pins the origin, so this does not apply to
|
|
37
|
+
* one.
|
|
38
|
+
*/
|
|
39
|
+
origin?: MockOriginMatching;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A typed handler factory per HTTP method, plus `untyped` — msw's own `http`
|
|
43
|
+
* object, for the rare route that is not in the OpenAPI document at all (an
|
|
44
|
+
* auth callback on another host, say).
|
|
45
|
+
*/
|
|
46
|
+
export type MockApi<TPaths extends object> = OpenApiHttpHandlers<TPaths>;
|
|
47
|
+
/**
|
|
48
|
+
* Creates typed msw request-handler factories bound to a generated OpenAPI
|
|
49
|
+
* `paths` type.
|
|
50
|
+
*
|
|
51
|
+
* The generated `TPaths` is the only endpoint definition: which methods exist on
|
|
52
|
+
* which paths, what path and query params they take, and what body each status
|
|
53
|
+
* may return are all read off it. A handler for a path the API does not have, or
|
|
54
|
+
* one that answers with a body the operation does not declare, fails to compile
|
|
55
|
+
* — which is the whole point of the layer, since a fixture that silently drifts
|
|
56
|
+
* from the contract makes every test that depends on it a false pass.
|
|
57
|
+
*
|
|
58
|
+
* `TPaths` must be passed explicitly; there is no value argument to infer it
|
|
59
|
+
* from — which is why the constraint is `object` rather than the `{}` that
|
|
60
|
+
* openapi-msw itself accepts. Every primitive but `null` and `undefined`
|
|
61
|
+
* satisfies `{}`, so a mistyped type argument compiled into a factory offering
|
|
62
|
+
* no paths at all.
|
|
63
|
+
*
|
|
64
|
+
* ```ts
|
|
65
|
+
* const mock = createMockApi<paths>({ baseUrl: '/api' });
|
|
66
|
+
*
|
|
67
|
+
* const handlers = [
|
|
68
|
+
* mock.get('/things/{id}', ({ params, response }) =>
|
|
69
|
+
* response(200).json({ id: params.id, name: 'Thing' }),
|
|
70
|
+
* ),
|
|
71
|
+
* mock.get('/things', ({ response }) => response(500).json({ message: 'nope' })),
|
|
72
|
+
* ];
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare function createMockApi<TPaths extends object>(options?: MockApiOptions): MockApi<TPaths>;
|
package/dist/mock-api.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createOpenApiHttp } from 'openapi-msw';
|
|
2
|
+
import { resolveHandlerBase } from './base-url.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates typed msw request-handler factories bound to a generated OpenAPI
|
|
5
|
+
* `paths` type.
|
|
6
|
+
*
|
|
7
|
+
* The generated `TPaths` is the only endpoint definition: which methods exist on
|
|
8
|
+
* which paths, what path and query params they take, and what body each status
|
|
9
|
+
* may return are all read off it. A handler for a path the API does not have, or
|
|
10
|
+
* one that answers with a body the operation does not declare, fails to compile
|
|
11
|
+
* — which is the whole point of the layer, since a fixture that silently drifts
|
|
12
|
+
* from the contract makes every test that depends on it a false pass.
|
|
13
|
+
*
|
|
14
|
+
* `TPaths` must be passed explicitly; there is no value argument to infer it
|
|
15
|
+
* from — which is why the constraint is `object` rather than the `{}` that
|
|
16
|
+
* openapi-msw itself accepts. Every primitive but `null` and `undefined`
|
|
17
|
+
* satisfies `{}`, so a mistyped type argument compiled into a factory offering
|
|
18
|
+
* no paths at all.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* const mock = createMockApi<paths>({ baseUrl: '/api' });
|
|
22
|
+
*
|
|
23
|
+
* const handlers = [
|
|
24
|
+
* mock.get('/things/{id}', ({ params, response }) =>
|
|
25
|
+
* response(200).json({ id: params.id, name: 'Thing' }),
|
|
26
|
+
* ),
|
|
27
|
+
* mock.get('/things', ({ response }) => response(500).json({ message: 'nope' })),
|
|
28
|
+
* ];
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function createMockApi(options = {}) {
|
|
32
|
+
return createOpenApiHttp({
|
|
33
|
+
baseUrl: resolveHandlerBase(options.baseUrl, options.origin ?? 'any'),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Latency is what makes a mock exercise the states a real API forces an app
|
|
3
|
+
* through — pending spinners, skeletons, optimistic UI, race conditions. The same
|
|
4
|
+
* latency in a test suite is dead time, so the amount is per-environment and the
|
|
5
|
+
* default under test is none.
|
|
6
|
+
*/
|
|
7
|
+
export type MockDelayInput = number | {
|
|
8
|
+
/** Milliseconds under test. Defaults to `0`. */
|
|
9
|
+
test?: number;
|
|
10
|
+
/** Milliseconds everywhere else. Defaults to `0`. */
|
|
11
|
+
dev?: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* True when running under a test runner: `NODE_ENV === 'test'` (vitest, jest) or
|
|
15
|
+
* Vite's `MODE === 'test'`. Read defensively because neither `process` nor
|
|
16
|
+
* `import.meta.env` exists in every environment this package runs in.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isTestEnvironment(): boolean;
|
|
19
|
+
/** The pure resolution `mockDelay` applies, split out so both branches are testable. */
|
|
20
|
+
export declare function resolveMockDelay(input: MockDelayInput, isTest: boolean): number;
|
|
21
|
+
/**
|
|
22
|
+
* Waits, in dev; resolves immediately under test unless a test delay is asked
|
|
23
|
+
* for.
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* mock.get('/things', async ({ response }) => {
|
|
27
|
+
* await mockDelay(400);
|
|
28
|
+
* return response(200).json(things.list());
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* // Keep a little latency under test, for a pending-state assertion.
|
|
32
|
+
* await mockDelay({ dev: 400, test: 10 });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function mockDelay(input?: MockDelayInput): Promise<void>;
|