msw-fetch-mock 0.1.0 → 0.1.2
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 +36 -17
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/mock-server.d.ts.map +1 -1
- package/dist/mock-server.js +5 -0
- package/docs/api.md +15 -1
- package/docs/cloudflare-migration.md +10 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,19 +18,14 @@ npm install -D msw-fetch-mock msw
|
|
|
18
18
|
|
|
19
19
|
## Quick Start
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
import { setupServer } from 'msw/node';
|
|
23
|
-
import { createFetchMock } from 'msw-fetch-mock';
|
|
21
|
+
### Standalone (Cloudflare migration)
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
```typescript
|
|
24
|
+
import { fetchMock } from 'msw-fetch-mock';
|
|
27
25
|
|
|
28
26
|
beforeAll(() => fetchMock.activate());
|
|
29
27
|
afterAll(() => fetchMock.deactivate());
|
|
30
|
-
afterEach(() =>
|
|
31
|
-
fetchMock.clearCallHistory();
|
|
32
|
-
fetchMock.assertNoPendingInterceptors();
|
|
33
|
-
});
|
|
28
|
+
afterEach(() => fetchMock.assertNoPendingInterceptors());
|
|
34
29
|
|
|
35
30
|
it('mocks a GET request', async () => {
|
|
36
31
|
fetchMock
|
|
@@ -45,11 +40,37 @@ it('mocks a GET request', async () => {
|
|
|
45
40
|
});
|
|
46
41
|
```
|
|
47
42
|
|
|
43
|
+
### With an existing MSW server
|
|
44
|
+
|
|
45
|
+
If you already use MSW, pass your server to share a single interceptor:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { setupServer } from 'msw/node';
|
|
49
|
+
import { http, HttpResponse } from 'msw';
|
|
50
|
+
import { FetchMock } from 'msw-fetch-mock';
|
|
51
|
+
|
|
52
|
+
const server = setupServer(http.get('/api/users', () => HttpResponse.json([{ id: 1 }])));
|
|
53
|
+
const fetchMock = new FetchMock(server);
|
|
54
|
+
|
|
55
|
+
beforeAll(() => server.listen());
|
|
56
|
+
afterAll(() => server.close());
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
server.resetHandlers();
|
|
59
|
+
fetchMock.assertNoPendingInterceptors();
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> **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
|
+
|
|
48
65
|
## API Overview
|
|
49
66
|
|
|
50
|
-
### `
|
|
67
|
+
### `fetchMock` (singleton)
|
|
68
|
+
|
|
69
|
+
A pre-built `FetchMock` instance for standalone use. Import and call `activate()` — no setup needed.
|
|
70
|
+
|
|
71
|
+
### `new FetchMock(server?)`
|
|
51
72
|
|
|
52
|
-
Creates a `FetchMock` instance.
|
|
73
|
+
Creates a `FetchMock` instance. Pass an existing MSW `SetupServer` to share interceptors; omit to create one internally.
|
|
53
74
|
|
|
54
75
|
### Intercepting & Replying
|
|
55
76
|
|
|
@@ -64,12 +85,10 @@ fetchMock
|
|
|
64
85
|
### Call History
|
|
65
86
|
|
|
66
87
|
```typescript
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
history.nthCall(2); // 2nd call (1-indexed)
|
|
72
|
-
history.filterCalls({ method: 'POST', path: '/users' }, { operator: 'AND' });
|
|
88
|
+
fetchMock.calls.lastCall(); // most recent
|
|
89
|
+
fetchMock.calls.firstCall(); // earliest
|
|
90
|
+
fetchMock.calls.nthCall(2); // 2nd call (1-indexed)
|
|
91
|
+
fetchMock.calls.filterCalls({ method: 'POST', path: '/users' }, { operator: 'AND' });
|
|
73
92
|
```
|
|
74
93
|
|
|
75
94
|
### Assertions
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { FetchMock } from './mock-server';
|
|
1
2
|
export { createFetchMock, FetchMock } from './mock-server';
|
|
3
|
+
/** Pre-built singleton for quick standalone use (Cloudflare migration compatible). */
|
|
4
|
+
export declare const fetchMock: FetchMock;
|
|
2
5
|
export type { InterceptOptions, MockPool, MockInterceptor, MockReplyChain, ReplyOptions, PendingInterceptor, } from './mock-server';
|
|
3
6
|
export { MockCallHistory, MockCallHistoryLog } from './mock-call-history';
|
|
4
7
|
export type { MockCallHistoryLogData, CallHistoryFilterCriteria } from './mock-call-history';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,eAAe,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,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"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
+
import { FetchMock } from './mock-server';
|
|
1
2
|
export { createFetchMock, FetchMock } from './mock-server';
|
|
3
|
+
/** Pre-built singleton for quick standalone use (Cloudflare migration compatible). */
|
|
4
|
+
export const fetchMock = new FetchMock();
|
|
2
5
|
export { MockCallHistory, MockCallHistoryLog } from './mock-call-history';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock-server.d.ts","sourceRoot":"","sources":["../src/mock-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,2FAA2F;AAC3F,UAAU,eAAe;IACvB,GAAG,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;IACvC,aAAa,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;IACjD,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAChD,KAAK,IAAI,IAAI,CAAC;CACf;AAED,KAAK,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAC9D,KAAK,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC;AACjE,KAAK,kBAAkB,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC;AACzE,KAAK,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC;AAEjE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC7C,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,KAAK,aAAa,GAAG,CAAC,GAAG,EAAE;IAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAElF,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,IAAI,IAAI,CAAC;IAChB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,cAAc,CAAC;IAC9E,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,cAAc,CAAC;IAC/D,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,cAAc,CAAC;CAC9C;AAED,MAAM,WAAW,QAAQ;IACvB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAAC;CACvD;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;CAClB;AAiGD,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyB;IAChD,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAU;IACrC,OAAO,CAAC,YAAY,CAA4B;IAChD,OAAO,CAAC,iBAAiB,CAA4B;IAErD,IAAI,KAAK,IAAI,eAAe,CAE3B;gBAEW,cAAc,CAAC,EAAE,eAAe;IAK5C,QAAQ,IAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"mock-server.d.ts","sourceRoot":"","sources":["../src/mock-server.ts"],"names":[],"mappings":"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"}
|
package/dist/mock-server.js
CHANGED
|
@@ -108,6 +108,11 @@ export class FetchMock {
|
|
|
108
108
|
}
|
|
109
109
|
activate() {
|
|
110
110
|
if (this.ownsServer) {
|
|
111
|
+
const isPatched = Object.getOwnPropertySymbols(globalThis.fetch).some((s) => s.description === 'isPatchedModule');
|
|
112
|
+
if (isPatched) {
|
|
113
|
+
throw new Error('Another MSW server is already active. ' +
|
|
114
|
+
'Pass your existing server to new FetchMock(server) instead.');
|
|
115
|
+
}
|
|
111
116
|
this.server = setupServer();
|
|
112
117
|
this.server.listen({
|
|
113
118
|
onUnhandledRequest: (request, print) => {
|
package/docs/api.md
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
# API Reference
|
|
2
2
|
|
|
3
|
+
## `fetchMock` (singleton)
|
|
4
|
+
|
|
5
|
+
A pre-built `FetchMock` instance for standalone use. No setup required — just import and call `activate()`.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { fetchMock } from 'msw-fetch-mock';
|
|
9
|
+
|
|
10
|
+
beforeAll(() => fetchMock.activate());
|
|
11
|
+
afterAll(() => fetchMock.deactivate());
|
|
12
|
+
afterEach(() => fetchMock.assertNoPendingInterceptors());
|
|
13
|
+
```
|
|
14
|
+
|
|
3
15
|
## `new FetchMock(server?)`
|
|
4
16
|
|
|
5
|
-
Creates a `FetchMock` instance.
|
|
17
|
+
Creates a `FetchMock` instance. Pass an existing MSW `SetupServer` to share interceptors; omit to create one internally.
|
|
6
18
|
|
|
7
19
|
```typescript
|
|
8
20
|
import { FetchMock } from 'msw-fetch-mock';
|
|
@@ -34,6 +46,8 @@ fetchMock.deactivate(); // stop intercepting (calls server.close())
|
|
|
34
46
|
```
|
|
35
47
|
|
|
36
48
|
> If you pass an external server that you manage yourself, `activate()` / `deactivate()` are no-ops.
|
|
49
|
+
>
|
|
50
|
+
> **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.
|
|
37
51
|
|
|
38
52
|
### `fetchMock.calls`
|
|
39
53
|
|
|
@@ -6,7 +6,7 @@ If you're migrating tests from Cloudflare Workers' `cloudflare:test` to a standa
|
|
|
6
6
|
|
|
7
7
|
| cloudflare:test | msw-fetch-mock |
|
|
8
8
|
| -------------------------------------------------- | ----------------------------------------------------------- |
|
|
9
|
-
| `import { fetchMock } from 'cloudflare:test'` | `
|
|
9
|
+
| `import { fetchMock } from 'cloudflare:test'` | `import { fetchMock } from 'msw-fetch-mock'` |
|
|
10
10
|
| `fetchMock.activate()` | `fetchMock.activate()` |
|
|
11
11
|
| `fetchMock.disableNetConnect()` | `fetchMock.disableNetConnect()` |
|
|
12
12
|
| `fetchMock.enableNetConnect(matcher?)` | `fetchMock.enableNetConnect(matcher?)` |
|
|
@@ -44,11 +44,7 @@ it('calls API', async () => {
|
|
|
44
44
|
## After (msw-fetch-mock)
|
|
45
45
|
|
|
46
46
|
```typescript
|
|
47
|
-
import {
|
|
48
|
-
import { FetchMock } from 'msw-fetch-mock';
|
|
49
|
-
|
|
50
|
-
const server = setupServer();
|
|
51
|
-
const fetchMock = new FetchMock(server);
|
|
47
|
+
import { fetchMock } from 'msw-fetch-mock';
|
|
52
48
|
|
|
53
49
|
beforeAll(() => fetchMock.activate());
|
|
54
50
|
afterAll(() => fetchMock.deactivate());
|
|
@@ -67,12 +63,13 @@ it('calls API', async () => {
|
|
|
67
63
|
|
|
68
64
|
## Key Differences
|
|
69
65
|
|
|
70
|
-
| Aspect | cloudflare:test
|
|
71
|
-
| -------------------- |
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
| Call history
|
|
75
|
-
|
|
|
76
|
-
|
|
|
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 |
|
|
77
74
|
|
|
78
75
|
> **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.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Undici-style fetch mock API built on MSW (Mock Service Worker)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
16
|
-
"url": "https://github.com/recca0120/msw-fetch-mock.git"
|
|
16
|
+
"url": "git+https://github.com/recca0120/msw-fetch-mock.git"
|
|
17
17
|
},
|
|
18
18
|
"packageManager": "pnpm@10.12.4",
|
|
19
19
|
"engines": {
|