msw-fetch-mock 0.3.3 → 0.3.4

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
@@ -4,6 +4,8 @@
4
4
  [![npm version](https://img.shields.io/npm/v/msw-fetch-mock.svg)](https://www.npmjs.com/package/msw-fetch-mock)
5
5
  [![license](https://img.shields.io/npm/l/msw-fetch-mock.svg)](https://github.com/recca0120/msw-fetch-mock/blob/main/LICENSE.md)
6
6
 
7
+ [English](README.md) | [繁體中文](README.zh-TW.md)
8
+
7
9
  Undici-style fetch mock API built on [MSW](https://mswjs.io/) (Mock Service Worker).
8
10
 
9
11
  If you're familiar with Cloudflare Workers' `fetchMock` (from `cloudflare:test`) or Node.js undici's `MockAgent`, you already know this API.
@@ -28,7 +30,7 @@ npm install -D msw-fetch-mock msw
28
30
  ### Node.js (Vitest, Jest)
29
31
 
30
32
  ```typescript
31
- // Works with default import or explicit /node subpath
33
+ // Works with root import or explicit /node subpath
32
34
  import { fetchMock } from 'msw-fetch-mock';
33
35
  // import { fetchMock } from 'msw-fetch-mock/node';
34
36
 
@@ -78,11 +80,10 @@ If you already use MSW, pass your server to share a single interceptor:
78
80
  ```typescript
79
81
  import { setupServer } from 'msw/node';
80
82
  import { http, HttpResponse } from 'msw';
81
- import { FetchMock } from 'msw-fetch-mock';
82
- import { NodeMswAdapter } from 'msw-fetch-mock/node';
83
+ import { createFetchMock } from 'msw-fetch-mock';
83
84
 
84
85
  const server = setupServer(http.get('/api/users', () => HttpResponse.json([{ id: 1 }])));
85
- const fetchMock = new FetchMock(new NodeMswAdapter(server));
86
+ const fetchMock = createFetchMock(server);
86
87
 
87
88
  beforeAll(() => server.listen());
88
89
  afterAll(() => server.close());
@@ -92,7 +93,7 @@ afterEach(() => {
92
93
  });
93
94
  ```
94
95
 
95
- > **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(new NodeMswAdapter(server))` instead.
96
+ > **Note:** Only one MSW server can be active at a time. If a server is already listening, standalone `activate()` will throw an error. Use `createFetchMock(server)` to share an existing server.
96
97
 
97
98
  ### Legacy (MSW v1)
98
99
 
@@ -204,6 +205,7 @@ E2E tests run on every CI push across these environments:
204
205
  | Jest CJS | CJS (require) | Jest |
205
206
  | Node.js Test | ESM (import) | Node test runner |
206
207
  | Node.js CJS | CJS (require) | Node test runner |
208
+ | Legacy CJS | CJS (require) | Jest (MSW v1) |
207
209
  | Vitest Browser | ESM (import) | Vitest + Playwright |
208
210
 
209
211
  ## Documentation
@@ -236,7 +238,7 @@ pnpm test:e2e -- node-cjs
236
238
  pnpm test:e2e -- --all
237
239
  ```
238
240
 
239
- Available suites: `jest-esm`, `jest-cjs`, `node-test`, `node-cjs`, `vitest-browser`
241
+ Available suites: `jest-esm`, `jest-cjs`, `node-test`, `node-cjs`, `legacy-cjs`, `vitest-browser`
240
242
 
241
243
  ## License
242
244
 
@@ -0,0 +1,245 @@
1
+ # msw-fetch-mock
2
+
3
+ [![CI](https://github.com/recca0120/msw-fetch-mock/actions/workflows/ci.yml/badge.svg)](https://github.com/recca0120/msw-fetch-mock/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/msw-fetch-mock.svg)](https://www.npmjs.com/package/msw-fetch-mock)
5
+ [![license](https://img.shields.io/npm/l/msw-fetch-mock.svg)](https://github.com/recca0120/msw-fetch-mock/blob/main/LICENSE.md)
6
+
7
+ [English](README.md) | [繁體中文](README.zh-TW.md)
8
+
9
+ 基於 [MSW](https://mswjs.io/)(Mock Service Worker)的 Undici 風格 fetch mock API。
10
+
11
+ 如果你熟悉 Cloudflare Workers 的 `fetchMock`(來自 `cloudflare:test`)或 Node.js undici 的 `MockAgent`,你已經會使用這個 API 了。
12
+
13
+ 透過 subpath exports 同時支援 **Node.js**(`msw/node`)和**瀏覽器**(`msw/browser`)環境。
14
+
15
+ ## 系統需求
16
+
17
+ - **Node.js** >= 18
18
+ - **MSW** ^1.0.0(透過 `/legacy`)或 ^2.12.7
19
+
20
+ ## 安裝
21
+
22
+ ```bash
23
+ npm install -D msw-fetch-mock msw
24
+ ```
25
+
26
+ `msw` 是 peer dependency — 需要自行安裝你的版本。
27
+
28
+ ## 快速開始
29
+
30
+ ### Node.js(Vitest、Jest)
31
+
32
+ ```typescript
33
+ // 可使用根路徑匯入或明確的 /node 子路徑
34
+ import { fetchMock } from 'msw-fetch-mock';
35
+ // import { fetchMock } from 'msw-fetch-mock/node';
36
+
37
+ beforeAll(() => fetchMock.activate({ onUnhandledRequest: 'error' }));
38
+ afterAll(() => fetchMock.deactivate());
39
+ afterEach(() => {
40
+ fetchMock.assertNoPendingInterceptors();
41
+ fetchMock.reset();
42
+ });
43
+
44
+ it('模擬 GET 請求', async () => {
45
+ fetchMock
46
+ .get('https://api.example.com')
47
+ .intercept({ path: '/users', method: 'GET' })
48
+ .reply(200, { users: [{ id: '1', name: 'Alice' }] });
49
+
50
+ const res = await fetch('https://api.example.com/users');
51
+ const data = await res.json();
52
+
53
+ expect(data.users).toHaveLength(1);
54
+ });
55
+ ```
56
+
57
+ ### 瀏覽器(Storybook、Vitest Browser Mode)
58
+
59
+ ```typescript
60
+ import { setupWorker } from 'msw/browser';
61
+ import { createFetchMock } from 'msw-fetch-mock/browser';
62
+
63
+ const worker = setupWorker();
64
+ const fetchMock = createFetchMock(worker);
65
+
66
+ beforeAll(async () => {
67
+ await fetchMock.activate({ onUnhandledRequest: 'error' });
68
+ });
69
+ afterAll(() => fetchMock.deactivate());
70
+ afterEach(() => {
71
+ fetchMock.assertNoPendingInterceptors();
72
+ fetchMock.reset();
73
+ });
74
+ ```
75
+
76
+ ### 搭配現有的 MSW server
77
+
78
+ 如果你已經在使用 MSW,可以傳入你的 server 來共用同一個攔截器:
79
+
80
+ ```typescript
81
+ import { setupServer } from 'msw/node';
82
+ import { http, HttpResponse } from 'msw';
83
+ import { createFetchMock } from 'msw-fetch-mock';
84
+
85
+ const server = setupServer(http.get('/api/users', () => HttpResponse.json([{ id: 1 }])));
86
+ const fetchMock = createFetchMock(server);
87
+
88
+ beforeAll(() => server.listen());
89
+ afterAll(() => server.close());
90
+ afterEach(() => {
91
+ server.resetHandlers();
92
+ fetchMock.assertNoPendingInterceptors();
93
+ });
94
+ ```
95
+
96
+ > **注意:** 同一時間只能有一個 MSW server 處於啟動狀態。如果已有 server 在監聽中,獨立模式的 `activate()` 會拋出錯誤。請使用 `createFetchMock(server)` 來共用現有的 server。
97
+
98
+ ### Legacy(MSW v1)
99
+
100
+ ```typescript
101
+ import { rest } from 'msw';
102
+ import { setupServer } from 'msw/node';
103
+ import { createFetchMock } from 'msw-fetch-mock/legacy';
104
+
105
+ const server = setupServer();
106
+ const fetchMock = createFetchMock(rest, server);
107
+
108
+ beforeAll(() => fetchMock.activate());
109
+ afterAll(() => fetchMock.deactivate());
110
+ afterEach(() => {
111
+ fetchMock.assertNoPendingInterceptors();
112
+ fetchMock.reset();
113
+ });
114
+ ```
115
+
116
+ 詳見 [MSW v1 Legacy 指南](docs/msw-v1-legacy.zh-TW.md)。
117
+
118
+ ## 未處理的請求
119
+
120
+ 預設情況下 `activate()` 使用 `'error'` 模式 — 未匹配的請求會導致 `fetch()` 拒絕。這包含對**已消耗**攔截器的請求(一次性攔截器使用後,其 handler 會從 MSW 中移除)。
121
+
122
+ ```typescript
123
+ // 拒絕未匹配的請求(預設行為)
124
+ fetchMock.activate();
125
+ fetchMock.activate({ onUnhandledRequest: 'error' });
126
+
127
+ // 記錄警告但允許通過
128
+ fetchMock.activate({ onUnhandledRequest: 'warn' });
129
+
130
+ // 靜默允許通過
131
+ fetchMock.activate({ onUnhandledRequest: 'bypass' });
132
+
133
+ // 自訂回呼
134
+ fetchMock.activate({
135
+ onUnhandledRequest: (request, print) => {
136
+ if (request.url.includes('/health')) return; // 忽略
137
+ print.error(); // 阻擋其他所有請求
138
+ },
139
+ });
140
+ ```
141
+
142
+ > `enableNetConnect()` 的優先順序高於 `onUnhandledRequest` — 允許的主機一律直接通過。
143
+
144
+ ## API 概覽
145
+
146
+ ### 匯入路徑
147
+
148
+ | 路徑 | 環境 | MSW 版本 |
149
+ | ------------------------ | ----------------------------- | -------- |
150
+ | `msw-fetch-mock` | Node.js(re-exports `/node`) | v2 |
151
+ | `msw-fetch-mock/node` | Node.js | v2 |
152
+ | `msw-fetch-mock/browser` | 瀏覽器 | v2 |
153
+ | `msw-fetch-mock/legacy` | Node.js(MSW v1) | v1 |
154
+
155
+ ### `fetchMock`(單例)
156
+
157
+ 預先建立的 `FetchMock` 實例,適用於獨立使用。匯入後直接呼叫 `activate()` 即可 — 無需額外設定。
158
+ 可從 `msw-fetch-mock` 和 `msw-fetch-mock/node` 匯入。
159
+
160
+ ### `createFetchMock(server?)` / `createFetchMock(worker)`
161
+
162
+ 建立 `FetchMock` 的工廠函式,會自動搭配對應的 adapter。
163
+
164
+ - Node:`createFetchMock(server?)` — 可選擇性傳入現有的 MSW `SetupServer`
165
+ - 瀏覽器:`createFetchMock(worker)` — 傳入 MSW `SetupWorker`(必要)
166
+ - Legacy:`createFetchMock(rest, server?)` — 傳入 MSW v1 的 `rest` 物件
167
+
168
+ ### `new FetchMock(adapter?)`
169
+
170
+ 使用明確的 `MswAdapter` 建立 `FetchMock` 實例。可使用 `NodeMswAdapter` 或 `BrowserMswAdapter`。
171
+
172
+ ### 攔截與回應
173
+
174
+ ```typescript
175
+ fetchMock
176
+ .get(origin) // string、RegExp 或 function
177
+ .intercept({ path, method, headers, body, query }) // 匹配條件
178
+ .reply(status, body, options) // 定義回應
179
+ .times(n) / .persist(); // 重複控制
180
+ ```
181
+
182
+ ### 呼叫歷史
183
+
184
+ ```typescript
185
+ fetchMock.calls.lastCall(); // 最近一次
186
+ fetchMock.calls.firstCall(); // 最早一次
187
+ fetchMock.calls.nthCall(2); // 第 2 次呼叫(1-indexed)
188
+ fetchMock.calls.filterCalls({ method: 'POST', path: '/users' }, { operator: 'AND' });
189
+ ```
190
+
191
+ ### 斷言與清理
192
+
193
+ ```typescript
194
+ fetchMock.assertNoPendingInterceptors(); // 若有未消耗的攔截器則拋出錯誤
195
+ fetchMock.reset(); // 清除攔截器 + 呼叫歷史 + handlers
196
+ ```
197
+
198
+ ## 測試環境
199
+
200
+ 每次 CI 推送都會在以下環境執行 E2E 測試:
201
+
202
+ | 環境 | 模組系統 | 測試框架 |
203
+ | -------------- | ------------- | ------------------- |
204
+ | Jest ESM | ESM (import) | Jest |
205
+ | Jest CJS | CJS (require) | Jest |
206
+ | Node.js Test | ESM (import) | Node test runner |
207
+ | Node.js CJS | CJS (require) | Node test runner |
208
+ | Legacy CJS | CJS (require) | Jest (MSW v1) |
209
+ | Vitest Browser | ESM (import) | Vitest + Playwright |
210
+
211
+ ## 文件
212
+
213
+ - [API 參考](docs/api.zh-TW.md) — 完整的 API 細節、匹配選項、回應回呼
214
+ - [Cloudflare Workers 遷移指南](docs/cloudflare-migration.zh-TW.md) — 從 `cloudflare:test` fetchMock 遷移
215
+ - [MSW v1 Legacy 指南](docs/msw-v1-legacy.zh-TW.md) — 搭配 MSW v1 使用 msw-fetch-mock
216
+
217
+ ## 開發
218
+
219
+ ```bash
220
+ pnpm install
221
+ pnpm build # 使用 tsup 建置
222
+ pnpm test # 單元測試(vitest)
223
+ pnpm test:e2e # E2E 測試(jest-esm、jest-cjs、node-test、node-cjs、legacy-cjs)
224
+ ```
225
+
226
+ ### E2E 測試
227
+
228
+ E2E 測試驗證已發佈的套件能在不同 runtime 和模組系統下正常運作。腳本會建置、透過 `npm pack` 打包 tarball,然後安裝到每個 `e2e/` 專案中 — 完全模擬 CI 流程。
229
+
230
+ ```bash
231
+ # 執行預設套組(跳過 vitest-browser)
232
+ pnpm test:e2e
233
+
234
+ # 執行單一套組
235
+ pnpm test:e2e -- node-cjs
236
+
237
+ # 執行所有套組,包含 vitest-browser(會自動安裝 Playwright)
238
+ pnpm test:e2e -- --all
239
+ ```
240
+
241
+ 可用套組:`jest-esm`、`jest-cjs`、`node-test`、`node-cjs`、`legacy-cjs`、`vitest-browser`
242
+
243
+ ## 授權
244
+
245
+ [MIT](LICENSE.md)
package/docs/api.md CHANGED
@@ -425,7 +425,7 @@ Returns: `MockReplyChain`
425
425
  | `callback` | `SingleReplyCallback` | Full-control callback |
426
426
  | `options` | `{ headers?: Record<string, string> }` | Response headers |
427
427
 
428
- ### `interceptor.replyWithError(error)`
428
+ ### `interceptor.replyWithError(error?)`
429
429
 
430
430
  Replies with a network error (simulates a connection failure).
431
431
 
@@ -435,9 +435,9 @@ Replies with a network error (simulates a connection failure).
435
435
 
436
436
  Returns: `MockReplyChain`
437
437
 
438
- | Parameter | Type | Description |
439
- | --------- | ------- | ---------------------------------------- |
440
- | `error` | `Error` | The error instance (used for semantics). |
438
+ | Parameter | Type | Required | Description |
439
+ | --------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
440
+ | `error` | `Error` | No | Optional error instance. Accepted for API compatibility but not used internally — the response is always a generic network error. |
441
441
 
442
442
  ---
443
443
 
@@ -0,0 +1,682 @@
1
+ # API 參考
2
+
3
+ ## 匯入路徑
4
+
5
+ | 路徑 | 環境 | MSW 版本 |
6
+ | ------------------------ | ----------------------------- | -------- |
7
+ | `msw-fetch-mock` | Node.js(re-exports `/node`) | v2 |
8
+ | `msw-fetch-mock/node` | Node.js | v2 |
9
+ | `msw-fetch-mock/browser` | 瀏覽器 | v2 |
10
+ | `msw-fetch-mock/legacy` | Node.js(MSW v1) | v1 |
11
+
12
+ ## `fetchMock`(單例)
13
+
14
+ 預先建立的 `FetchMock` 實例,適用於獨立的 Node.js 使用。無需額外設定 — 匯入後呼叫 `activate()` 即可。
15
+
16
+ ```typescript
17
+ import { fetchMock } from 'msw-fetch-mock';
18
+
19
+ beforeAll(async () => {
20
+ await fetchMock.activate({ onUnhandledRequest: 'error' });
21
+ });
22
+ afterAll(() => fetchMock.deactivate());
23
+ afterEach(() => {
24
+ fetchMock.assertNoPendingInterceptors();
25
+ fetchMock.reset();
26
+ });
27
+ ```
28
+
29
+ ## `createFetchMock(server?)`(Node)
30
+
31
+ 建立搭配 `NodeMswAdapter` 的 `FetchMock`。可選擇性傳入現有的 MSW server。
32
+
33
+ ```typescript
34
+ import { createFetchMock } from 'msw-fetch-mock/node';
35
+ import { setupServer } from 'msw/node';
36
+
37
+ // 獨立模式
38
+ const fetchMock = createFetchMock();
39
+
40
+ // 搭配外部 MSW server
41
+ const server = setupServer();
42
+ const fetchMock = createFetchMock(server);
43
+ ```
44
+
45
+ ## `createFetchMock(worker)`(瀏覽器)
46
+
47
+ 建立搭配 `BrowserMswAdapter` 的 `FetchMock`。需要傳入 MSW worker。
48
+
49
+ ```typescript
50
+ import { setupWorker } from 'msw/browser';
51
+ import { createFetchMock } from 'msw-fetch-mock/browser';
52
+
53
+ const worker = setupWorker();
54
+ const fetchMock = createFetchMock(worker);
55
+
56
+ beforeAll(async () => {
57
+ await fetchMock.activate({ onUnhandledRequest: 'error' });
58
+ });
59
+ ```
60
+
61
+ ## `createFetchMock(rest, server?)`(Legacy)
62
+
63
+ 建立適用於 MSW v1 環境的 `FetchMock`。詳見 [MSW v1 Legacy 指南](msw-v1-legacy.zh-TW.md)。
64
+
65
+ ```typescript
66
+ import { rest } from 'msw';
67
+ import { setupServer } from 'msw/node';
68
+ import { createFetchMock } from 'msw-fetch-mock/legacy';
69
+
70
+ const server = setupServer();
71
+ const fetchMock = createFetchMock(rest, server);
72
+ ```
73
+
74
+ ## `new FetchMock(adapter?)`
75
+
76
+ 使用明確的 `MswAdapter` 建立 `FetchMock` 實例。
77
+
78
+ ```typescript
79
+ import { FetchMock } from 'msw-fetch-mock';
80
+ import { NodeMswAdapter } from 'msw-fetch-mock/node';
81
+ import { BrowserMswAdapter } from 'msw-fetch-mock/browser';
82
+
83
+ // Node 搭配外部 server
84
+ const fetchMock = new FetchMock(new NodeMswAdapter(server));
85
+
86
+ // 瀏覽器搭配 worker
87
+ const fetchMock = new FetchMock(new BrowserMswAdapter(worker));
88
+ ```
89
+
90
+ | 參數 | 型別 | 必要 | 說明 |
91
+ | --------- | ------------ | ---- | -------------------------------------------- |
92
+ | `adapter` | `MswAdapter` | 否 | 環境 adapter。建議改用 `createFetchMock()`。 |
93
+
94
+ ---
95
+
96
+ ## `FetchMock`
97
+
98
+ ### 生命週期
99
+
100
+ ```typescript
101
+ await fetchMock.activate(options?); // 開始攔截(非同步 — 瀏覽器需要 worker.start())
102
+ fetchMock.deactivate(); // 停止攔截
103
+ ```
104
+
105
+ > `activate()` 回傳 `Promise<void>`。在 Node.js 中,promise 會同步解析。在瀏覽器中,會等待 Service Worker 啟動。Vitest 和 Jest 原生支援非同步的 `beforeAll`。
106
+ >
107
+ > **衝突偵測(僅 Node):** 在獨立模式下,`activate()` 會檢查 `globalThis.fetch` 是否已被 MSW 修補。若是,會拋出錯誤,引導你使用 `createFetchMock(server)` 來共用現有 server。
108
+
109
+ #### `ActivateOptions`
110
+
111
+ | 屬性 | 型別 | 預設值 | 說明 |
112
+ | -------------------- | -------------------- | --------- | -------------------- |
113
+ | `onUnhandledRequest` | `OnUnhandledRequest` | `'error'` | 如何處理未匹配的請求 |
114
+
115
+ #### `OnUnhandledRequest`
116
+
117
+ | 值 | 行為 |
118
+ | -------------------------- | ---------------------------------------------------------------------------------------- |
119
+ | `'error'` | MSW 印出錯誤且 `fetch()` 以 `InternalError` 拒絕 |
120
+ | `'warn'` | MSW 印出警告;請求直接通過到實際網路 |
121
+ | `'bypass'` | 靜默通過到實際網路 |
122
+ | `(request, print) => void` | 自訂回呼。呼叫 `print.error()` 阻擋或 `print.warning()` 警告。不呼叫任何一個則靜默放行。 |
123
+
124
+ ```typescript
125
+ // 預設 — 拒絕未匹配的請求
126
+ await fetchMock.activate();
127
+ await fetchMock.activate({ onUnhandledRequest: 'error' });
128
+
129
+ // 自訂回呼
130
+ await fetchMock.activate({
131
+ onUnhandledRequest: (request, print) => {
132
+ if (new URL(request.url).pathname === '/health') return;
133
+ print.error();
134
+ },
135
+ });
136
+ ```
137
+
138
+ > **已消耗的攔截器:** 一次性攔截器完全消耗後,其 MSW handler 會被移除。後續對該 URL 的請求會被視為未處理,並經由 `onUnhandledRequest` 處理。這可防止已消耗的攔截器靜默通過。
139
+ >
140
+ > **優先順序:** `enableNetConnect()` 的優先順序高於 `onUnhandledRequest` — 允許的主機一律直接通過,不受未處理請求模式影響。
141
+
142
+ ### `fetchMock.calls`
143
+
144
+ 回傳 `MockCallHistory` 實例,用於檢視和管理已記錄的請求。
145
+
146
+ ```typescript
147
+ // 檢查呼叫次數
148
+ expect(fetchMock.calls.length).toBe(3);
149
+
150
+ // 檢視最後一次呼叫
151
+ const last = fetchMock.calls.lastCall();
152
+
153
+ // 清除歷史
154
+ fetchMock.calls.clear();
155
+ ```
156
+
157
+ ### `fetchMock.get(origin)`
158
+
159
+ 回傳範圍限定在指定 origin 的 `MockPool`。`origin` 參數接受三種形式:
160
+
161
+ ```typescript
162
+ // 字串 — 精確的 origin 匹配
163
+ const pool = fetchMock.get('https://api.example.com');
164
+
165
+ // RegExp — 對 URL origin 進行匹配
166
+ const pool = fetchMock.get(/\.example\.com$/);
167
+
168
+ // 函式 — 自訂 origin 判斷
169
+ const pool = fetchMock.get((origin) => origin.startsWith('https://'));
170
+ ```
171
+
172
+ | 參數 | 型別 | 必要 | 說明 |
173
+ | -------- | ------------------------------------------------- | ---- | ------------- |
174
+ | `origin` | `string \| RegExp \| (origin: string) => boolean` | 是 | Origin 匹配器 |
175
+
176
+ ### `fetchMock.disableNetConnect()`
177
+
178
+ 阻止所有實際的網路請求。未匹配的請求會拋出錯誤。
179
+
180
+ ### `fetchMock.enableNetConnect(matcher?)`
181
+
182
+ 允許實際的網路請求通過。不帶參數時允許所有請求。帶 matcher 時僅匹配的主機可通過。
183
+
184
+ ```typescript
185
+ // 允許所有網路請求
186
+ fetchMock.enableNetConnect();
187
+
188
+ // 允許特定主機(精確匹配)
189
+ fetchMock.enableNetConnect('api.example.com');
190
+
191
+ // 允許匹配 RegExp 的主機
192
+ fetchMock.enableNetConnect(/\.example\.com$/);
193
+
194
+ // 允許匹配函式的主機
195
+ fetchMock.enableNetConnect((host) => host.endsWith('.test'));
196
+ ```
197
+
198
+ | 參數 | 型別 | 必要 | 說明 |
199
+ | --------- | ----------------------------------------------- | ---- | ---------------------------- |
200
+ | `matcher` | `string \| RegExp \| (host: string) => boolean` | 否 | 主機匹配器。省略時允許全部。 |
201
+
202
+ ### `fetchMock.defaultReplyHeaders(headers)`
203
+
204
+ 設定每個回應都會包含的預設 headers。每個回應的 headers 會與預設值合併並覆蓋。
205
+
206
+ ```typescript
207
+ fetchMock.defaultReplyHeaders({
208
+ 'x-request-id': 'test-123',
209
+ 'cache-control': 'no-store',
210
+ });
211
+
212
+ // 此回應會同時包含 x-request-id 和 content-type
213
+ fetchMock
214
+ .get('https://api.example.com')
215
+ .intercept({ path: '/data' })
216
+ .reply(200, { ok: true }, { headers: { 'content-type': 'application/json' } });
217
+ ```
218
+
219
+ > `reset()` 會清除預設回應 headers。
220
+
221
+ | 參數 | 型別 | 必要 | 說明 |
222
+ | --------- | ------------------------ | ---- | -------------------------- |
223
+ | `headers` | `Record<string, string>` | 是 | 每個回應都要包含的 headers |
224
+
225
+ ### `fetchMock.enableCallHistory()`
226
+
227
+ 啟用呼叫歷史記錄。這是預設狀態。
228
+
229
+ ### `fetchMock.disableCallHistory()`
230
+
231
+ 停用呼叫歷史記錄。請求仍會被攔截和回應,但不會被記錄。適用於效能敏感的測試以減少開銷。
232
+
233
+ ```typescript
234
+ fetchMock.disableCallHistory();
235
+ // ... 請求被攔截但不被記錄
236
+ fetchMock.enableCallHistory();
237
+ // ... 請求現在會被記錄
238
+ ```
239
+
240
+ ### `fetchMock.getCallHistory()`
241
+
242
+ 回傳 `MockCallHistory` 實例。Cloudflare 相容的 `fetchMock.calls` 別名。
243
+
244
+ ### `fetchMock.clearCallHistory()`
245
+
246
+ 清除所有已記錄的呼叫。Cloudflare 相容的 `fetchMock.calls.clear()` 別名。
247
+
248
+ ### `fetchMock.clearAllCallHistory()`
249
+
250
+ `clearCallHistory()` 的別名。
251
+
252
+ ### `fetchMock.assertNoPendingInterceptors()`
253
+
254
+ 若有任何已註冊的攔截器尚未被消耗,會拋出錯誤。這是一個**純斷言** — 不會清除呼叫歷史、攔截器或 handlers。使用 `reset()` 來清理狀態。
255
+
256
+ ```typescript
257
+ afterEach(() => {
258
+ fetchMock.assertNoPendingInterceptors();
259
+ fetchMock.reset();
260
+ });
261
+ ```
262
+
263
+ ### `fetchMock.reset()`
264
+
265
+ 清除所有攔截器、呼叫歷史、預設回應 headers 和 MSW handlers。將實例重設為乾淨狀態,但不會停止 server。建議在 `afterEach` 中於斷言無待處理攔截器之後呼叫。
266
+
267
+ ```typescript
268
+ afterEach(() => {
269
+ fetchMock.assertNoPendingInterceptors();
270
+ fetchMock.reset();
271
+ });
272
+ ```
273
+
274
+ ### `fetchMock.pendingInterceptors()`
275
+
276
+ 回傳未消耗的攔截器陣列,包含中繼資料:
277
+
278
+ ```typescript
279
+ interface PendingInterceptor {
280
+ origin: string;
281
+ path: string;
282
+ method: string;
283
+ consumed: boolean;
284
+ times: number;
285
+ timesInvoked: number;
286
+ persist: boolean;
287
+ }
288
+ ```
289
+
290
+ ---
291
+
292
+ ## `MockPool`
293
+
294
+ ### `pool.intercept(options)`
295
+
296
+ 註冊一個用於匹配請求的攔截器。
297
+
298
+ ```typescript
299
+ pool.intercept({
300
+ path: '/users',
301
+ method: 'GET',
302
+ });
303
+ ```
304
+
305
+ 回傳:`MockInterceptor`
306
+
307
+ #### `InterceptOptions`
308
+
309
+ | 屬性 | 型別 | 必要 | 說明 |
310
+ | --------- | ---------------------------------------------------------------- | ---- | ---------------------------- |
311
+ | `path` | `string \| RegExp \| (path: string) => boolean` | 是 | 要匹配的 URL 路徑名 |
312
+ | `method` | `'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH'` | 否 | HTTP 方法(預設:`'GET'`) |
313
+ | `headers` | `Record<string, string \| RegExp \| (value: string) => boolean>` | 否 | Header 匹配器 |
314
+ | `body` | `string \| RegExp \| (body: string) => boolean` | 否 | 請求 body 匹配器 |
315
+ | `query` | `Record<string, string>` | 否 | Query 參數匹配器(精確匹配) |
316
+
317
+ #### 路徑匹配
318
+
319
+ ```typescript
320
+ // 精確字串
321
+ .intercept({ path: '/users' })
322
+
323
+ // RegExp
324
+ .intercept({ path: /^\/users\/\d+$/ })
325
+
326
+ // 函式
327
+ .intercept({ path: (p) => p.startsWith('/users') })
328
+ ```
329
+
330
+ #### Header 匹配
331
+
332
+ ```typescript
333
+ .intercept({
334
+ path: '/api',
335
+ method: 'POST',
336
+ headers: {
337
+ 'content-type': 'application/json', // 精確匹配
338
+ authorization: /^Bearer /, // 正則
339
+ 'x-custom': (v) => v.includes('special'), // 函式
340
+ },
341
+ })
342
+ ```
343
+
344
+ #### Body 匹配
345
+
346
+ ```typescript
347
+ .intercept({
348
+ path: '/api',
349
+ method: 'POST',
350
+ body: '{"key":"value"}', // 精確匹配
351
+ // body: /keyword/, // 正則
352
+ // body: (b) => b.includes('key'), // 函式
353
+ })
354
+ ```
355
+
356
+ #### Query 參數
357
+
358
+ ```typescript
359
+ .intercept({
360
+ path: '/search',
361
+ method: 'GET',
362
+ query: { q: 'test', page: '1' },
363
+ })
364
+ ```
365
+
366
+ ---
367
+
368
+ ## `MockInterceptor`
369
+
370
+ ### `interceptor.reply(status, body?, options?)`
371
+
372
+ 使用靜態 body 定義模擬回應。
373
+
374
+ ```typescript
375
+ // 靜態 body
376
+ .reply(200, { users: [] })
377
+
378
+ // 附帶回應 headers
379
+ .reply(200, { users: [] }, { headers: { 'x-request-id': '123' } })
380
+ ```
381
+
382
+ ### `interceptor.reply(status, callback)`
383
+
384
+ 使用動態 body 回呼定義模擬回應。
385
+
386
+ ```typescript
387
+ // 回呼(接收請求資訊)
388
+ .reply(200, (req) => {
389
+ const input = JSON.parse(req.body!);
390
+ return { echo: input };
391
+ })
392
+ ```
393
+
394
+ ### `interceptor.reply(callback)`
395
+
396
+ 單一回呼形式 — 完全控制狀態碼、body 和回應選項。
397
+
398
+ ```typescript
399
+ .reply((req) => {
400
+ const data = JSON.parse(req.body!);
401
+ return {
402
+ statusCode: 201,
403
+ data: { id: '1', ...data },
404
+ responseOptions: { headers: { 'x-created': 'true' } },
405
+ };
406
+ })
407
+ ```
408
+
409
+ 回呼接收 `{ body: string | null }`,必須回傳(或解析為):
410
+
411
+ ```typescript
412
+ interface SingleReplyResult {
413
+ statusCode: number;
414
+ data: unknown;
415
+ responseOptions?: { headers?: Record<string, string> };
416
+ }
417
+ ```
418
+
419
+ 回傳:`MockReplyChain`
420
+
421
+ | 參數 | 型別 | 說明 |
422
+ | ---------- | -------------------------------------- | ---------------- |
423
+ | `status` | `number` | HTTP 狀態碼 |
424
+ | `body` | `unknown \| (req) => unknown` | 回應 body 或回呼 |
425
+ | `callback` | `SingleReplyCallback` | 完全控制回呼 |
426
+ | `options` | `{ headers?: Record<string, string> }` | 回應 headers |
427
+
428
+ ### `interceptor.replyWithError(error?)`
429
+
430
+ 回應一個網路錯誤(模擬連線失敗)。
431
+
432
+ ```typescript
433
+ .replyWithError(new Error('connection refused'))
434
+ ```
435
+
436
+ 回傳:`MockReplyChain`
437
+
438
+ | 參數 | 型別 | 必要 | 說明 |
439
+ | ------- | ------- | ---- | ---------------------------------------------------------------------------- |
440
+ | `error` | `Error` | 否 | 可選的錯誤實例。為 API 相容性而接受,但內部不使用 — 回應一律為通用網路錯誤。 |
441
+
442
+ ---
443
+
444
+ ## `MockReplyChain`
445
+
446
+ ### `chain.times(n)`
447
+
448
+ 攔截器精確匹配 `n` 次後即被消耗。
449
+
450
+ ```typescript
451
+ .reply(200, { ok: true }).times(3)
452
+ ```
453
+
454
+ ### `chain.persist()`
455
+
456
+ 攔截器無限期匹配(永不消耗)。
457
+
458
+ ```typescript
459
+ .reply(200, { ok: true }).persist()
460
+ ```
461
+
462
+ ### `chain.delay(ms)`
463
+
464
+ 在回應送出前加入延遲。
465
+
466
+ ```typescript
467
+ .reply(200, { ok: true }).delay(500)
468
+ ```
469
+
470
+ ### `chain.replyContentLength()`
471
+
472
+ 根據 JSON 序列化後的 body 大小,自動加入 `Content-Length` header。
473
+
474
+ ```typescript
475
+ .reply(200, { ok: true }).replyContentLength()
476
+ // 回應會包含 Content-Length: 13
477
+ ```
478
+
479
+ ---
480
+
481
+ ## `MockCallHistory`
482
+
483
+ 追蹤所有通過模擬 server 的請求。
484
+
485
+ ### `history.length`
486
+
487
+ 回傳已記錄的呼叫數量。
488
+
489
+ ```typescript
490
+ expect(fetchMock.calls.length).toBe(3);
491
+ ```
492
+
493
+ ### `history.called(criteria?)`
494
+
495
+ 若有任何呼叫匹配給定條件則回傳 `true`,未提供條件時則檢查是否有任何呼叫。
496
+
497
+ ```typescript
498
+ // 有任何呼叫記錄嗎?
499
+ expect(fetchMock.calls.called()).toBe(true);
500
+
501
+ // 有匹配篩選條件的呼叫嗎?
502
+ expect(fetchMock.calls.called({ method: 'POST' })).toBe(true);
503
+ expect(fetchMock.calls.called(/\/users/)).toBe(true);
504
+ expect(fetchMock.calls.called((log) => log.path === '/users')).toBe(true);
505
+ ```
506
+
507
+ ### `history.calls()`
508
+
509
+ 回傳所有已記錄的 `MockCallHistoryLog` 條目的副本。
510
+
511
+ ### `history.firstCall(criteria?)`
512
+
513
+ 回傳第一個記錄的呼叫,若無則回傳 `undefined`。可選擇性以條件篩選。
514
+
515
+ ```typescript
516
+ const first = fetchMock.calls.firstCall();
517
+ const firstPost = fetchMock.calls.firstCall({ method: 'POST' });
518
+ ```
519
+
520
+ ### `history.lastCall(criteria?)`
521
+
522
+ 回傳最近一次記錄的呼叫,若無則回傳 `undefined`。可選擇性以條件篩選。
523
+
524
+ ```typescript
525
+ const last = fetchMock.calls.lastCall();
526
+ const lastPost = fetchMock.calls.lastCall({ method: 'POST' });
527
+ ```
528
+
529
+ ### `history.nthCall(n, criteria?)`
530
+
531
+ 回傳第 n 次呼叫(1-indexed),若無則回傳 `undefined`。可選擇性以條件篩選。
532
+
533
+ ```typescript
534
+ const second = fetchMock.calls.nthCall(2);
535
+ const secondPost = fetchMock.calls.nthCall(2, { method: 'POST' });
536
+ ```
537
+
538
+ ### `history.clear()`
539
+
540
+ 移除所有已記錄的呼叫。
541
+
542
+ ### `history.filterCalls(criteria, options?)`
543
+
544
+ 彈性篩選,有三種多載:
545
+
546
+ ```typescript
547
+ // 函式謂詞
548
+ history.filterCalls((log) => log.body?.includes('test'));
549
+
550
+ // RegExp(對 log.toString() 測試)
551
+ history.filterCalls(/POST.*\/users/);
552
+
553
+ // 結構化條件
554
+ history.filterCalls(
555
+ { method: 'POST', path: '/users' },
556
+ { operator: 'AND' } // 預設:'OR'
557
+ );
558
+ ```
559
+
560
+ #### `CallHistoryFilterCriteria`
561
+
562
+ | 屬性 | 型別 | 說明 |
563
+ | ---------- | -------- | ---------- |
564
+ | `method` | `string` | HTTP 方法 |
565
+ | `path` | `string` | URL 路徑名 |
566
+ | `origin` | `string` | URL origin |
567
+ | `protocol` | `string` | URL 協定 |
568
+ | `host` | `string` | URL 主機 |
569
+ | `port` | `string` | URL 連接埠 |
570
+ | `hash` | `string` | URL hash |
571
+ | `fullUrl` | `string` | 完整 URL |
572
+
573
+ ### `history.filterCallsByMethod(filter)`
574
+
575
+ ```typescript
576
+ history.filterCallsByMethod('POST');
577
+ history.filterCallsByMethod(/^P/); // POST、PUT、PATCH
578
+ ```
579
+
580
+ ### `history.filterCallsByPath(filter)`
581
+
582
+ ```typescript
583
+ history.filterCallsByPath('/users');
584
+ history.filterCallsByPath(/\/users\/\d+/);
585
+ ```
586
+
587
+ ### `history.filterCallsByOrigin(filter)`
588
+
589
+ ```typescript
590
+ history.filterCallsByOrigin('https://api.example.com');
591
+ history.filterCallsByOrigin(/example\.com/);
592
+ ```
593
+
594
+ ### `history.filterCallsByProtocol(filter)`
595
+
596
+ ```typescript
597
+ history.filterCallsByProtocol('https:');
598
+ ```
599
+
600
+ ### `history.filterCallsByHost(filter)`
601
+
602
+ ```typescript
603
+ history.filterCallsByHost('api.example.com');
604
+ history.filterCallsByHost(/example\.com/);
605
+ ```
606
+
607
+ ### `history.filterCallsByPort(filter)`
608
+
609
+ ```typescript
610
+ history.filterCallsByPort('8787');
611
+ ```
612
+
613
+ ### `history.filterCallsByHash(filter)`
614
+
615
+ ```typescript
616
+ history.filterCallsByHash('#section');
617
+ ```
618
+
619
+ ### `history.filterCallsByFullUrl(filter)`
620
+
621
+ ```typescript
622
+ history.filterCallsByFullUrl('https://api.example.com/users');
623
+ history.filterCallsByFullUrl(/\/users\?page=1/);
624
+ ```
625
+
626
+ ### 迭代
627
+
628
+ `MockCallHistory` 實作了 `Symbol.iterator`:
629
+
630
+ ```typescript
631
+ for (const call of history) {
632
+ console.log(call.method, call.path);
633
+ }
634
+ ```
635
+
636
+ ---
637
+
638
+ ## `MockCallHistoryLog`
639
+
640
+ 每筆記錄的呼叫都是 `MockCallHistoryLog` 實例,包含以下屬性:
641
+
642
+ | 屬性 | 型別 | 說明 |
643
+ | -------------- | ------------------------ | ----------------------------------- |
644
+ | `method` | `string` | HTTP 方法 |
645
+ | `fullUrl` | `string` | 完整 URL |
646
+ | `origin` | `string` | URL origin(`https://example.com`) |
647
+ | `path` | `string` | URL 路徑名(`/users`) |
648
+ | `searchParams` | `Record<string, string>` | Query 參數 |
649
+ | `headers` | `Record<string, string>` | 請求 headers |
650
+ | `body` | `string \| null` | 請求 body |
651
+ | `protocol` | `string` | URL 協定(`https:`) |
652
+ | `host` | `string` | URL 主機 |
653
+ | `port` | `string` | URL 連接埠 |
654
+ | `hash` | `string` | URL hash |
655
+
656
+ ### `log.json()`
657
+
658
+ 將請求 body 解析為 JSON。若 body 為 `null` 則回傳 `null`。
659
+
660
+ ```typescript
661
+ const call = fetchMock.calls.lastCall()!;
662
+ const data = call.json() as { name: string };
663
+ expect(data.name).toBe('Alice');
664
+ ```
665
+
666
+ ### `log.toMap()`
667
+
668
+ 回傳包含所有 log 屬性的 `Map`。
669
+
670
+ ```typescript
671
+ const map = call.toMap();
672
+ expect(map.get('method')).toBe('POST');
673
+ ```
674
+
675
+ ### `log.toString()`
676
+
677
+ 回傳以管線符號分隔的字串表示,用於除錯和 RegExp 匹配。
678
+
679
+ ```typescript
680
+ call.toString();
681
+ // "method->POST|protocol->https:|host->api.example.com|port->|origin->https://api.example.com|path->/users|hash->|fullUrl->https://api.example.com/users"
682
+ ```
@@ -0,0 +1,80 @@
1
+ # Cloudflare Workers 遷移指南
2
+
3
+ 如果你正在將測試從 Cloudflare Workers 的 `cloudflare:test` 遷移到標準的 Node.js 測試環境,`msw-fetch-mock` 提供了你已經熟悉的相同 `fetchMock` API。
4
+
5
+ ## API 對照
6
+
7
+ | cloudflare:test | msw-fetch-mock |
8
+ | -------------------------------------------------- | ----------------------------------------------------------- |
9
+ | `import { fetchMock } from 'cloudflare:test'` | `import { fetchMock } from 'msw-fetch-mock'` |
10
+ | `fetchMock.activate()` | `await fetchMock.activate()` |
11
+ | `fetchMock.disableNetConnect()` | `fetchMock.disableNetConnect()` |
12
+ | `fetchMock.enableNetConnect(matcher?)` | `fetchMock.enableNetConnect(matcher?)` |
13
+ | `fetchMock.deactivate()` | `fetchMock.deactivate()` |
14
+ | `fetchMock.get(origin).intercept(opts).reply(...)` | `fetchMock.get(origin).intercept(opts).reply(...)` |
15
+ | `.replyWithError(error)` | `.replyWithError(error)` |
16
+ | `.reply(...).delay(ms)` | `.reply(...).delay(ms)` |
17
+ | `fetchMock.getCallHistory()` | `fetchMock.getCallHistory()` 或 `fetchMock.calls` |
18
+ | `fetchMock.clearCallHistory()` | `fetchMock.clearCallHistory()` 或 `fetchMock.calls.clear()` |
19
+ | `fetchMock.assertNoPendingInterceptors()` | `fetchMock.assertNoPendingInterceptors()` |
20
+
21
+ ## 遷移前(cloudflare:test)
22
+
23
+ ```typescript
24
+ import { fetchMock } from 'cloudflare:test';
25
+
26
+ beforeEach(() => {
27
+ fetchMock.activate();
28
+ fetchMock.disableNetConnect();
29
+ });
30
+
31
+ afterEach(() => fetchMock.assertNoPendingInterceptors());
32
+
33
+ it('呼叫 API', async () => {
34
+ fetchMock
35
+ .get('https://api.example.com')
36
+ .intercept({ path: '/data', method: 'GET' })
37
+ .reply(200, { result: 'ok' });
38
+
39
+ const res = await fetch('https://api.example.com/data');
40
+ expect(await res.json()).toEqual({ result: 'ok' });
41
+ });
42
+ ```
43
+
44
+ ## 遷移後(msw-fetch-mock)
45
+
46
+ ```typescript
47
+ import { fetchMock } from 'msw-fetch-mock';
48
+
49
+ beforeAll(async () => {
50
+ await fetchMock.activate();
51
+ });
52
+ afterAll(() => fetchMock.deactivate());
53
+ afterEach(() => {
54
+ fetchMock.assertNoPendingInterceptors();
55
+ fetchMock.reset();
56
+ });
57
+
58
+ it('呼叫 API', async () => {
59
+ fetchMock
60
+ .get('https://api.example.com')
61
+ .intercept({ path: '/data', method: 'GET' })
62
+ .reply(200, { result: 'ok' });
63
+
64
+ const res = await fetch('https://api.example.com/data');
65
+ expect(await res.json()).toEqual({ result: 'ok' });
66
+ });
67
+ ```
68
+
69
+ ## 主要差異
70
+
71
+ | 面向 | cloudflare:test | msw-fetch-mock |
72
+ | --------------- | --------------------------------------------- | ------------------------------------------------- |
73
+ | 匯入 | `import { fetchMock } from 'cloudflare:test'` | `import { fetchMock } from 'msw-fetch-mock'` |
74
+ | Server 生命週期 | 隱式(由測試框架管理) | 顯式(`activate()` / `deactivate()`) |
75
+ | 呼叫歷史存取 | `fetchMock.getCallHistory()` | `fetchMock.getCallHistory()` 或 `fetchMock.calls` |
76
+ | 呼叫歷史清理 | 每次測試自動清理 | 在 `afterEach` 中呼叫 `reset()` |
77
+ | 未處理的請求 | 必須呼叫 `disableNetConnect()` | 預設 `onUnhandledRequest: 'error'`(直接拒絕) |
78
+ | 執行環境 | Cloudflare Workers (workerd) | Node.js |
79
+
80
+ > **注意:** `getCallHistory()` 和 `clearCallHistory()` 是為了 Cloudflare 相容性而提供的別名。你可以使用 Cloudflare 風格的方法或 `fetchMock.calls` getter — 兩者是等價的。
@@ -0,0 +1,94 @@
1
+ # MSW v1 Legacy 指南
2
+
3
+ 如果你正在使用 MSW v1(`msw@^1.x`),請使用 `msw-fetch-mock/legacy` 子路徑匯出。API 與 v2 版本相同 — 只有設定方式不同。
4
+
5
+ ## 安裝
6
+
7
+ ```bash
8
+ npm install -D msw-fetch-mock msw@^1
9
+ ```
10
+
11
+ ## 設定
12
+
13
+ ```typescript
14
+ import { rest } from 'msw';
15
+ import { setupServer } from 'msw/node';
16
+ import { createFetchMock } from 'msw-fetch-mock/legacy';
17
+
18
+ const server = setupServer();
19
+ const fetchMock = createFetchMock(rest, server);
20
+
21
+ beforeAll(() => fetchMock.activate());
22
+ afterAll(() => fetchMock.deactivate());
23
+ afterEach(() => {
24
+ fetchMock.assertNoPendingInterceptors();
25
+ fetchMock.reset();
26
+ });
27
+
28
+ it('模擬 GET 請求', async () => {
29
+ fetchMock
30
+ .get('https://api.example.com')
31
+ .intercept({ path: '/users', method: 'GET' })
32
+ .reply(200, { users: [{ id: '1', name: 'Alice' }] });
33
+
34
+ const res = await fetch('https://api.example.com/users');
35
+ const data = await res.json();
36
+
37
+ expect(data.users).toHaveLength(1);
38
+ });
39
+ ```
40
+
41
+ ## API 對照
42
+
43
+ | MSW v2(`msw-fetch-mock`) | MSW v1(`msw-fetch-mock/legacy`) |
44
+ | -------------------------------------------- | ---------------------------------------------- |
45
+ | `import { fetchMock } from 'msw-fetch-mock'` | 無單例 — 必須使用 `createFetchMock` |
46
+ | `import { createFetchMock } from '.../node'` | `import { createFetchMock } from '.../legacy'` |
47
+ | `createFetchMock(server?)` | `createFetchMock(rest, server?)` |
48
+ | `import { http } from 'msw'` | `import { rest } from 'msw'` |
49
+
50
+ ## 主要差異
51
+
52
+ | 面向 | MSW v2 | MSW v1(Legacy) |
53
+ | ---------------- | ----------------------------------------- | --------------------------------- |
54
+ | 匯入 | `msw-fetch-mock` 或 `msw-fetch-mock/node` | `msw-fetch-mock/legacy` |
55
+ | 工廠函式 | `createFetchMock(server?)` | `createFetchMock(rest, server?)` |
56
+ | 單例 | `fetchMock` 預建實例 | 不提供 — 使用 `createFetchMock` |
57
+ | Handler 內部實作 | 使用 `http.*` + `HttpResponse` | 使用 `rest.*` + `(req, res, ctx)` |
58
+ | MSW 版本 | `msw@^2.12.7` | `msw@^1.0.0` |
59
+
60
+ ## 匯出項目
61
+
62
+ `msw-fetch-mock/legacy` 子路徑匯出:
63
+
64
+ | 匯出 | 類型 | 說明 |
65
+ | -------------------------------- | ---- | ----------------------------------- |
66
+ | `createFetchMock(rest, server?)` | 函式 | 建立適用於 MSW v1 的 FetchMock 實例 |
67
+ | `FetchMock` | 類別 | 核心模擬類別(與 v2 相同) |
68
+ | `createLegacyHandlerFactory` | 函式 | 低階:建立 v1 相容的 handler 工廠 |
69
+ | `LegacyRestApi` | 型別 | MSW v1 的 `rest` 物件型別 |
70
+
71
+ ## 從 MSW v1 遷移到 v2
72
+
73
+ 當你將 MSW 從 v1 升級到 v2 時,更新你的匯入:
74
+
75
+ ```diff
76
+ - import { rest } from 'msw';
77
+ - import { createFetchMock } from 'msw-fetch-mock/legacy';
78
+ - const fetchMock = createFetchMock(rest, server);
79
+ + import { createFetchMock } from 'msw-fetch-mock/node';
80
+ + const fetchMock = createFetchMock(server);
81
+ ```
82
+
83
+ 或使用單例進行獨立模式:
84
+
85
+ ```diff
86
+ - import { rest } from 'msw';
87
+ - import { setupServer } from 'msw/node';
88
+ - import { createFetchMock } from 'msw-fetch-mock/legacy';
89
+ - const server = setupServer();
90
+ - const fetchMock = createFetchMock(rest, server);
91
+ + import { fetchMock } from 'msw-fetch-mock';
92
+ ```
93
+
94
+ 其餘 API(`get()`、`intercept()`、`reply()`、`calls` 等)維持不變。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msw-fetch-mock",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Undici-style fetch mock API built on MSW (Mock Service Worker)",
5
5
  "type": "module",
6
6
  "license": "MIT",