@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 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;
@@ -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;
@@ -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
+ }
@@ -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>;
@@ -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>;