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