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.
@@ -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
+ ```