msw-fetch-mock 0.1.2 → 0.2.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 CHANGED
@@ -23,9 +23,12 @@ npm install -D msw-fetch-mock msw
23
23
  ```typescript
24
24
  import { fetchMock } from 'msw-fetch-mock';
25
25
 
26
- beforeAll(() => fetchMock.activate());
26
+ beforeAll(() => fetchMock.activate({ onUnhandledRequest: 'error' }));
27
27
  afterAll(() => fetchMock.deactivate());
28
- afterEach(() => fetchMock.assertNoPendingInterceptors());
28
+ afterEach(() => {
29
+ fetchMock.assertNoPendingInterceptors();
30
+ fetchMock.reset();
31
+ });
29
32
 
30
33
  it('mocks a GET request', async () => {
31
34
  fetchMock
@@ -62,6 +65,32 @@ afterEach(() => {
62
65
 
63
66
  > **Note:** Only one MSW server can be active at a time. If another server is already listening, standalone `activate()` will throw an error guiding you to use `new FetchMock(server)` instead.
64
67
 
68
+ ## Unhandled Requests
69
+
70
+ By default `activate()` uses `'error'` mode — unmatched requests cause `fetch()` to reject. This includes requests to **consumed** interceptors (once a one-shot interceptor has been used, its handler is removed from MSW).
71
+
72
+ ```typescript
73
+ // Reject unmatched requests (default)
74
+ fetchMock.activate();
75
+ fetchMock.activate({ onUnhandledRequest: 'error' });
76
+
77
+ // Log a warning but allow passthrough
78
+ fetchMock.activate({ onUnhandledRequest: 'warn' });
79
+
80
+ // Silently allow passthrough
81
+ fetchMock.activate({ onUnhandledRequest: 'bypass' });
82
+
83
+ // Custom callback
84
+ fetchMock.activate({
85
+ onUnhandledRequest: (request, print) => {
86
+ if (request.url.includes('/health')) return; // ignore
87
+ print.error(); // block everything else
88
+ },
89
+ });
90
+ ```
91
+
92
+ > `enableNetConnect()` takes priority over `onUnhandledRequest` — allowed hosts always pass through.
93
+
65
94
  ## API Overview
66
95
 
67
96
  ### `fetchMock` (singleton)
@@ -91,10 +120,11 @@ fetchMock.calls.nthCall(2); // 2nd call (1-indexed)
91
120
  fetchMock.calls.filterCalls({ method: 'POST', path: '/users' }, { operator: 'AND' });
92
121
  ```
93
122
 
94
- ### Assertions
123
+ ### Assertions & Cleanup
95
124
 
96
125
  ```typescript
97
126
  fetchMock.assertNoPendingInterceptors(); // throws if unconsumed interceptors remain
127
+ fetchMock.reset(); // clears interceptors + call history + handlers
98
128
  ```
99
129
 
100
130
  ## Documentation
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { FetchMock } from './mock-server';
2
2
  export { createFetchMock, FetchMock } from './mock-server';
3
3
  /** Pre-built singleton for quick standalone use (Cloudflare migration compatible). */
4
4
  export declare const fetchMock: FetchMock;
5
- export type { InterceptOptions, MockPool, MockInterceptor, MockReplyChain, ReplyOptions, PendingInterceptor, } from './mock-server';
5
+ export type { ActivateOptions, OnUnhandledRequest, InterceptOptions, MockPool, MockInterceptor, MockReplyChain, ReplyOptions, PendingInterceptor, } from './mock-server';
6
6
  export { MockCallHistory, MockCallHistoryLog } from './mock-call-history';
7
7
  export type { MockCallHistoryLogData, CallHistoryFilterCriteria } from './mock-call-history';
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE1C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE3D,sFAAsF;AACtF,eAAO,MAAM,SAAS,WAAkB,CAAC;AACzC,YAAY,EACV,gBAAgB,EAChB,QAAQ,EACR,eAAe,EACf,cAAc,EACd,YAAY,EACZ,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC1E,YAAY,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE1C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE3D,sFAAsF;AACtF,eAAO,MAAM,SAAS,WAAkB,CAAC;AACzC,YAAY,EACV,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,QAAQ,EACR,eAAe,EACf,cAAc,EACd,YAAY,EACZ,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC1E,YAAY,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -45,21 +45,37 @@ export interface PendingInterceptor {
45
45
  timesInvoked: number;
46
46
  persist: boolean;
47
47
  }
48
+ type PrintAPI = {
49
+ warning(): void;
50
+ error(): void;
51
+ };
52
+ type OnUnhandledRequestCallback = (request: Request, print: PrintAPI) => void;
53
+ export type OnUnhandledRequest = 'bypass' | 'warn' | 'error' | OnUnhandledRequestCallback;
54
+ export interface ActivateOptions {
55
+ onUnhandledRequest?: OnUnhandledRequest;
56
+ }
48
57
  export declare class FetchMock {
49
58
  private readonly _calls;
50
59
  private server;
51
60
  private readonly ownsServer;
52
61
  private interceptors;
53
62
  private netConnectAllowed;
63
+ private mswHandlers;
54
64
  get calls(): MockCallHistory;
55
65
  constructor(externalServer?: SetupServerLike);
56
- activate(): void;
66
+ activate(options?: ActivateOptions): void;
57
67
  disableNetConnect(): void;
58
68
  enableNetConnect(matcher?: string | RegExp | ((host: string) => boolean)): void;
59
69
  private isNetConnectAllowed;
70
+ /**
71
+ * Remove consumed MSW handlers so future requests to those URLs
72
+ * go through MSW's onUnhandledRequest instead of silently passing through.
73
+ */
74
+ private syncMswHandlers;
60
75
  getCallHistory(): MockCallHistory;
61
76
  clearCallHistory(): void;
62
77
  deactivate(): void;
78
+ reset(): void;
63
79
  assertNoPendingInterceptors(): void;
64
80
  pendingInterceptors(): PendingInterceptor[];
65
81
  get(origin: string): MockPool;
@@ -1 +1 @@
1
- {"version":3,"file":"mock-server.d.ts","sourceRoot":"","sources":["../src/mock-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mock-server.d.ts","sourceRoot":"","sources":["../src/mock-server.ts"],"names":[],"mappings":"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"}
@@ -99,6 +99,8 @@ export class FetchMock {
99
99
  ownsServer;
100
100
  interceptors = [];
101
101
  netConnectAllowed = false;
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ mswHandlers = new Map();
102
104
  get calls() {
103
105
  return this._calls;
104
106
  }
@@ -106,19 +108,29 @@ export class FetchMock {
106
108
  this.server = externalServer ?? null;
107
109
  this.ownsServer = !externalServer;
108
110
  }
109
- activate() {
111
+ activate(options) {
110
112
  if (this.ownsServer) {
111
113
  const isPatched = Object.getOwnPropertySymbols(globalThis.fetch).some((s) => s.description === 'isPatchedModule');
112
114
  if (isPatched) {
113
115
  throw new Error('Another MSW server is already active. ' +
114
116
  'Pass your existing server to new FetchMock(server) instead.');
115
117
  }
118
+ const mode = options?.onUnhandledRequest ?? 'error';
116
119
  this.server = setupServer();
117
120
  this.server.listen({
118
121
  onUnhandledRequest: (request, print) => {
119
122
  if (this.isNetConnectAllowed(request))
120
123
  return;
121
- print.error();
124
+ if (typeof mode === 'function') {
125
+ mode(request, print);
126
+ }
127
+ else if (mode === 'error') {
128
+ print.error();
129
+ }
130
+ else if (mode === 'warn') {
131
+ print.warning();
132
+ }
133
+ // 'bypass' → do nothing
122
134
  },
123
135
  });
124
136
  }
@@ -141,6 +153,18 @@ export class FetchMock {
141
153
  return this.netConnectAllowed.test(host);
142
154
  return this.netConnectAllowed(host);
143
155
  }
156
+ /**
157
+ * Remove consumed MSW handlers so future requests to those URLs
158
+ * go through MSW's onUnhandledRequest instead of silently passing through.
159
+ */
160
+ syncMswHandlers() {
161
+ if (!this.server || !this.ownsServer)
162
+ return;
163
+ const activeHandlers = [...this.mswHandlers.entries()]
164
+ .filter(([p]) => !p.consumed || p.persist)
165
+ .map(([, handler]) => handler);
166
+ this.server.resetHandlers(...activeHandlers);
167
+ }
144
168
  getCallHistory() {
145
169
  return this._calls;
146
170
  }
@@ -149,19 +173,23 @@ export class FetchMock {
149
173
  }
150
174
  deactivate() {
151
175
  this.interceptors = [];
176
+ this.mswHandlers.clear();
152
177
  this._calls.clear();
153
178
  if (this.ownsServer) {
154
179
  this.server?.close();
155
180
  this.server = null;
156
181
  }
157
182
  }
158
- assertNoPendingInterceptors() {
159
- const unconsumed = this.interceptors.filter(isPending);
183
+ reset() {
160
184
  this.interceptors = [];
185
+ this.mswHandlers.clear();
161
186
  this._calls.clear();
162
187
  if (this.ownsServer) {
163
188
  this.server?.resetHandlers();
164
189
  }
190
+ }
191
+ assertNoPendingInterceptors() {
192
+ const unconsumed = this.interceptors.filter(isPending);
165
193
  if (unconsumed.length > 0) {
166
194
  const descriptions = unconsumed.map((p) => ` ${p.method} ${p.origin}${p.path}`);
167
195
  throw new Error(`Pending interceptor(s) not consumed:\n${descriptions.join('\n')}`);
@@ -207,6 +235,7 @@ export class FetchMock {
207
235
  pending.timesInvoked++;
208
236
  if (!pending.persist && pending.timesInvoked >= pending.times) {
209
237
  pending.consumed = true;
238
+ this.syncMswHandlers();
210
239
  }
211
240
  recordCall(this._calls, request, bodyText);
212
241
  return bodyText;
@@ -216,6 +245,7 @@ export class FetchMock {
216
245
  if (!this.server) {
217
246
  throw new Error('FetchMock server is not active. Call activate() before registering interceptors.');
218
247
  }
248
+ this.mswHandlers.set(pending, handler);
219
249
  this.server.use(handler);
220
250
  };
221
251
  const buildChain = (delayRef) => ({
package/docs/api.md CHANGED
@@ -7,9 +7,12 @@ A pre-built `FetchMock` instance for standalone use. No setup required — just
7
7
  ```typescript
8
8
  import { fetchMock } from 'msw-fetch-mock';
9
9
 
10
- beforeAll(() => fetchMock.activate());
10
+ beforeAll(() => fetchMock.activate({ onUnhandledRequest: 'error' }));
11
11
  afterAll(() => fetchMock.deactivate());
12
- afterEach(() => fetchMock.assertNoPendingInterceptors());
12
+ afterEach(() => {
13
+ fetchMock.assertNoPendingInterceptors();
14
+ fetchMock.reset();
15
+ });
13
16
  ```
14
17
 
15
18
  ## `new FetchMock(server?)`
@@ -41,14 +44,47 @@ const fetchMock = new FetchMock(server);
41
44
  ### Lifecycle
42
45
 
43
46
  ```typescript
44
- fetchMock.activate(); // start intercepting (calls server.listen())
45
- fetchMock.deactivate(); // stop intercepting (calls server.close())
47
+ fetchMock.activate(options?); // start intercepting (calls server.listen())
48
+ fetchMock.deactivate(); // stop intercepting (calls server.close())
46
49
  ```
47
50
 
48
51
  > If you pass an external server that you manage yourself, `activate()` / `deactivate()` are no-ops.
49
52
  >
50
53
  > **Conflict detection:** In standalone mode, `activate()` checks whether `globalThis.fetch` is already patched by MSW. If so, it throws an error guiding you to pass your existing server via `new FetchMock(server)` instead.
51
54
 
55
+ #### `ActivateOptions`
56
+
57
+ | Property | Type | Default | Description |
58
+ | -------------------- | -------------------- | --------- | --------------------------------------------------- |
59
+ | `onUnhandledRequest` | `OnUnhandledRequest` | `'error'` | How to handle requests with no matching interceptor |
60
+
61
+ #### `OnUnhandledRequest`
62
+
63
+ | Value | Behavior |
64
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
65
+ | `'error'` | MSW prints an error and `fetch()` rejects with an `InternalError` |
66
+ | `'warn'` | MSW prints a warning; the request passes through to the real network |
67
+ | `'bypass'` | Silently passes through to the real network |
68
+ | `(request, print) => void` | Custom callback. Call `print.error()` to block or `print.warning()` to warn. Return without calling either to silently bypass. |
69
+
70
+ ```typescript
71
+ // Default — reject unmatched requests
72
+ fetchMock.activate();
73
+ fetchMock.activate({ onUnhandledRequest: 'error' });
74
+
75
+ // Custom callback
76
+ fetchMock.activate({
77
+ onUnhandledRequest: (request, print) => {
78
+ if (new URL(request.url).pathname === '/health') return;
79
+ print.error();
80
+ },
81
+ });
82
+ ```
83
+
84
+ > **Consumed interceptors:** Once a one-shot interceptor has been fully consumed, its MSW handler is removed. Subsequent requests to that URL are treated as unhandled and go through `onUnhandledRequest`. This prevents consumed interceptors from silently passing through.
85
+ >
86
+ > **Priority:** `enableNetConnect()` takes priority over `onUnhandledRequest` — allowed hosts always pass through regardless of the unhandled request mode.
87
+
52
88
  ### `fetchMock.calls`
53
89
 
54
90
  Returns the `MockCallHistory` instance for inspecting and managing recorded requests.
@@ -108,13 +144,25 @@ Clears all recorded calls. Cloudflare-compatible alias for `fetchMock.calls.clea
108
144
 
109
145
  ### `fetchMock.assertNoPendingInterceptors()`
110
146
 
111
- Throws an error if any registered interceptor has not been consumed. Also **automatically clears** call history and resets handlers. Use in `afterEach` to catch missing requests.
147
+ Throws an error if any registered interceptor has not been consumed. This is a **pure assertion** — it does not clear call history, interceptors, or handlers. Use `reset()` to clean up state.
112
148
 
113
149
  ```typescript
114
- afterEach(() => fetchMock.assertNoPendingInterceptors());
150
+ afterEach(() => {
151
+ fetchMock.assertNoPendingInterceptors();
152
+ fetchMock.reset();
153
+ });
115
154
  ```
116
155
 
117
- > Call history is cleared automatically — no need for a separate `calls.clear()` call.
156
+ ### `fetchMock.reset()`
157
+
158
+ Clears all interceptors, call history, and MSW handlers. Resets the instance to a clean state without stopping the server. Use in `afterEach` after asserting no pending interceptors.
159
+
160
+ ```typescript
161
+ afterEach(() => {
162
+ fetchMock.assertNoPendingInterceptors();
163
+ fetchMock.reset();
164
+ });
165
+ ```
118
166
 
119
167
  ### `fetchMock.pendingInterceptors()`
120
168
 
@@ -48,7 +48,10 @@ import { fetchMock } from 'msw-fetch-mock';
48
48
 
49
49
  beforeAll(() => fetchMock.activate());
50
50
  afterAll(() => fetchMock.deactivate());
51
- afterEach(() => fetchMock.assertNoPendingInterceptors());
51
+ afterEach(() => {
52
+ fetchMock.assertNoPendingInterceptors();
53
+ fetchMock.reset();
54
+ });
52
55
 
53
56
  it('calls API', async () => {
54
57
  fetchMock
@@ -63,13 +66,13 @@ it('calls API', async () => {
63
66
 
64
67
  ## Key Differences
65
68
 
66
- | Aspect | cloudflare:test | msw-fetch-mock |
67
- | -------------------- | --------------------------------------------- | ------------------------------------------------- |
68
- | Import | `import { fetchMock } from 'cloudflare:test'` | `import { fetchMock } from 'msw-fetch-mock'` |
69
- | Server lifecycle | Implicit (managed by test framework) | Explicit (`activate()` / `deactivate()`) |
70
- | Call history access | `fetchMock.getCallHistory()` | `fetchMock.getCallHistory()` or `fetchMock.calls` |
71
- | Call history cleanup | Automatic per test | Automatic via `assertNoPendingInterceptors()` |
72
- | Network connect | Must call `disableNetConnect()` | MSW blocks unhandled requests by default |
73
- | Runtime | Cloudflare Workers (workerd) | Node.js |
69
+ | Aspect | cloudflare:test | msw-fetch-mock |
70
+ | -------------------- | --------------------------------------------- | -------------------------------------------------- |
71
+ | Import | `import { fetchMock } from 'cloudflare:test'` | `import { fetchMock } from 'msw-fetch-mock'` |
72
+ | Server lifecycle | Implicit (managed by test framework) | Explicit (`activate()` / `deactivate()`) |
73
+ | Call history access | `fetchMock.getCallHistory()` | `fetchMock.getCallHistory()` or `fetchMock.calls` |
74
+ | Call history cleanup | Automatic per test | `reset()` in `afterEach` |
75
+ | Unhandled requests | Must call `disableNetConnect()` | `onUnhandledRequest: 'error'` by default (rejects) |
76
+ | Runtime | Cloudflare Workers (workerd) | Node.js |
74
77
 
75
78
  > **Note:** `getCallHistory()` and `clearCallHistory()` are provided as Cloudflare-compatible aliases. You can use either the Cloudflare-style methods or the `fetchMock.calls` getter — they are equivalent.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msw-fetch-mock",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Undici-style fetch mock API built on MSW (Mock Service Worker)",
5
5
  "type": "module",
6
6
  "license": "MIT",