msw-fetch-mock 0.1.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/LICENSE.md +21 -0
- package/README.md +88 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/mock-call-history.d.ts +65 -0
- package/dist/mock-call-history.d.ts.map +1 -0
- package/dist/mock-call-history.js +140 -0
- package/dist/mock-server.d.ts +69 -0
- package/dist/mock-server.d.ts.map +1 -0
- package/dist/mock-server.js +269 -0
- package/docs/api.md +472 -0
- package/docs/cloudflare-migration.md +78 -0
- package/package.json +59 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 recca0120
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# msw-fetch-mock
|
|
2
|
+
|
|
3
|
+
[](https://github.com/recca0120/msw-fetch-mock/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/msw-fetch-mock)
|
|
5
|
+
[](https://github.com/recca0120/msw-fetch-mock/blob/main/LICENSE.md)
|
|
6
|
+
|
|
7
|
+
Undici-style fetch mock API built on [MSW](https://mswjs.io/) (Mock Service Worker).
|
|
8
|
+
|
|
9
|
+
If you're familiar with Cloudflare Workers' `fetchMock` (from `cloudflare:test`) or Node.js undici's `MockAgent`, you already know this API.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -D msw-fetch-mock msw
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`msw` is a peer dependency — you provide your own version.
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { setupServer } from 'msw/node';
|
|
23
|
+
import { createFetchMock } from 'msw-fetch-mock';
|
|
24
|
+
|
|
25
|
+
const server = setupServer();
|
|
26
|
+
const fetchMock = createFetchMock(server);
|
|
27
|
+
|
|
28
|
+
beforeAll(() => fetchMock.activate());
|
|
29
|
+
afterAll(() => fetchMock.deactivate());
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
fetchMock.clearCallHistory();
|
|
32
|
+
fetchMock.assertNoPendingInterceptors();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('mocks a GET request', async () => {
|
|
36
|
+
fetchMock
|
|
37
|
+
.get('https://api.example.com')
|
|
38
|
+
.intercept({ path: '/users', method: 'GET' })
|
|
39
|
+
.reply(200, { users: [{ id: '1', name: 'Alice' }] });
|
|
40
|
+
|
|
41
|
+
const res = await fetch('https://api.example.com/users');
|
|
42
|
+
const data = await res.json();
|
|
43
|
+
|
|
44
|
+
expect(data.users).toHaveLength(1);
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API Overview
|
|
49
|
+
|
|
50
|
+
### `createFetchMock(server?)`
|
|
51
|
+
|
|
52
|
+
Creates a `FetchMock` instance. Optionally accepts an existing MSW `SetupServer`; creates one internally if omitted.
|
|
53
|
+
|
|
54
|
+
### Intercepting & Replying
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
fetchMock
|
|
58
|
+
.get(origin) // select origin
|
|
59
|
+
.intercept({ path, method, headers, body, query }) // match criteria
|
|
60
|
+
.reply(status, body, options) // define response
|
|
61
|
+
.times(n) / .persist(); // repeat control
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Call History
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const history = fetchMock.getCallHistory();
|
|
68
|
+
history.calls(); // all calls
|
|
69
|
+
history.lastCall(); // most recent
|
|
70
|
+
history.firstCall(); // earliest
|
|
71
|
+
history.nthCall(2); // 2nd call (1-indexed)
|
|
72
|
+
history.filterCalls({ method: 'POST', path: '/users' }, { operator: 'AND' });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Assertions
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
fetchMock.assertNoPendingInterceptors(); // throws if unconsumed interceptors remain
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Documentation
|
|
82
|
+
|
|
83
|
+
- [API Reference](docs/api.md) — full API details, matching options, reply callbacks
|
|
84
|
+
- [Cloudflare Workers Migration](docs/cloudflare-migration.md) — migrating from `cloudflare:test` fetchMock
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
[MIT](LICENSE.md)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createFetchMock, FetchMock } from './mock-server';
|
|
2
|
+
export type { InterceptOptions, MockPool, MockInterceptor, MockReplyChain, ReplyOptions, PendingInterceptor, } from './mock-server';
|
|
3
|
+
export { MockCallHistory, MockCallHistoryLog } from './mock-call-history';
|
|
4
|
+
export type { MockCallHistoryLogData, CallHistoryFilterCriteria } from './mock-call-history';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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;AAC3D,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
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface MockCallHistoryLogData {
|
|
2
|
+
body: string | null;
|
|
3
|
+
method: string;
|
|
4
|
+
headers: Record<string, string>;
|
|
5
|
+
fullUrl: string;
|
|
6
|
+
origin: string;
|
|
7
|
+
path: string;
|
|
8
|
+
searchParams: Record<string, string>;
|
|
9
|
+
protocol: string;
|
|
10
|
+
host: string;
|
|
11
|
+
port: string;
|
|
12
|
+
hash: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class MockCallHistoryLog {
|
|
15
|
+
readonly body: string | null;
|
|
16
|
+
readonly method: string;
|
|
17
|
+
readonly headers: Record<string, string>;
|
|
18
|
+
readonly fullUrl: string;
|
|
19
|
+
readonly origin: string;
|
|
20
|
+
readonly path: string;
|
|
21
|
+
readonly searchParams: Record<string, string>;
|
|
22
|
+
readonly protocol: string;
|
|
23
|
+
readonly host: string;
|
|
24
|
+
readonly port: string;
|
|
25
|
+
readonly hash: string;
|
|
26
|
+
constructor(data: MockCallHistoryLogData);
|
|
27
|
+
json(): unknown;
|
|
28
|
+
toMap(): Map<string, string | null | Record<string, string>>;
|
|
29
|
+
toString(): string;
|
|
30
|
+
}
|
|
31
|
+
export interface CallHistoryFilterCriteria {
|
|
32
|
+
method?: string;
|
|
33
|
+
path?: string;
|
|
34
|
+
origin?: string;
|
|
35
|
+
protocol?: string;
|
|
36
|
+
host?: string;
|
|
37
|
+
port?: string;
|
|
38
|
+
hash?: string;
|
|
39
|
+
fullUrl?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare class MockCallHistory {
|
|
42
|
+
private logs;
|
|
43
|
+
get length(): number;
|
|
44
|
+
record(data: MockCallHistoryLogData): void;
|
|
45
|
+
called(criteria?: ((log: MockCallHistoryLog) => boolean) | CallHistoryFilterCriteria | RegExp): boolean;
|
|
46
|
+
calls(): MockCallHistoryLog[];
|
|
47
|
+
firstCall(criteria?: ((log: MockCallHistoryLog) => boolean) | CallHistoryFilterCriteria | RegExp): MockCallHistoryLog | undefined;
|
|
48
|
+
lastCall(criteria?: ((log: MockCallHistoryLog) => boolean) | CallHistoryFilterCriteria | RegExp): MockCallHistoryLog | undefined;
|
|
49
|
+
nthCall(n: number, criteria?: ((log: MockCallHistoryLog) => boolean) | CallHistoryFilterCriteria | RegExp): MockCallHistoryLog | undefined;
|
|
50
|
+
clear(): void;
|
|
51
|
+
[Symbol.iterator](): Iterator<MockCallHistoryLog>;
|
|
52
|
+
filterCalls(criteria: ((log: MockCallHistoryLog) => boolean) | CallHistoryFilterCriteria | RegExp, options?: {
|
|
53
|
+
operator?: 'AND' | 'OR';
|
|
54
|
+
}): MockCallHistoryLog[];
|
|
55
|
+
private filterBy;
|
|
56
|
+
filterCallsByMethod(filter: string | RegExp): MockCallHistoryLog[];
|
|
57
|
+
filterCallsByPath(filter: string | RegExp): MockCallHistoryLog[];
|
|
58
|
+
filterCallsByOrigin(filter: string | RegExp): MockCallHistoryLog[];
|
|
59
|
+
filterCallsByProtocol(filter: string | RegExp): MockCallHistoryLog[];
|
|
60
|
+
filterCallsByHost(filter: string | RegExp): MockCallHistoryLog[];
|
|
61
|
+
filterCallsByPort(filter: string | RegExp): MockCallHistoryLog[];
|
|
62
|
+
filterCallsByHash(filter: string | RegExp): MockCallHistoryLog[];
|
|
63
|
+
filterCallsByFullUrl(filter: string | RegExp): MockCallHistoryLog[];
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=mock-call-history.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-call-history.d.ts","sourceRoot":"","sources":["../src/mock-call-history.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,kBAAkB;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,sBAAsB;IAcxC,IAAI,IAAI,OAAO;IAKf,KAAK,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAgB5D,QAAQ,IAAI,MAAM;CAYnB;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAA4B;IAExC,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,MAAM,CAAC,IAAI,EAAE,sBAAsB,GAAG,IAAI;IAI1C,MAAM,CACJ,QAAQ,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,kBAAkB,KAAK,OAAO,CAAC,GAAG,yBAAyB,GAAG,MAAM,GACrF,OAAO;IAKV,KAAK,IAAI,kBAAkB,EAAE;IAI7B,SAAS,CACP,QAAQ,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,kBAAkB,KAAK,OAAO,CAAC,GAAG,yBAAyB,GAAG,MAAM,GACrF,kBAAkB,GAAG,SAAS;IAKjC,QAAQ,CACN,QAAQ,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,kBAAkB,KAAK,OAAO,CAAC,GAAG,yBAAyB,GAAG,MAAM,GACrF,kBAAkB,GAAG,SAAS;IAMjC,OAAO,CACL,CAAC,EAAE,MAAM,EACT,QAAQ,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,kBAAkB,KAAK,OAAO,CAAC,GAAG,yBAAyB,GAAG,MAAM,GACrF,kBAAkB,GAAG,SAAS;IAKjC,KAAK,IAAI,IAAI;IAIb,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,kBAAkB,CAAC;IAIjD,WAAW,CACT,QAAQ,EAAE,CAAC,CAAC,GAAG,EAAE,kBAAkB,KAAK,OAAO,CAAC,GAAG,yBAAyB,GAAG,MAAM,EACrF,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,KAAK,GAAG,IAAI,CAAA;KAAE,GACpC,kBAAkB,EAAE;IAsBvB,OAAO,CAAC,QAAQ;IAShB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,kBAAkB,EAAE;IAIlE,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,kBAAkB,EAAE;IAIhE,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,kBAAkB,EAAE;IAIlE,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,kBAAkB,EAAE;IAIpE,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,kBAAkB,EAAE;IAIhE,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,kBAAkB,EAAE;IAIhE,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,kBAAkB,EAAE;IAIhE,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,kBAAkB,EAAE;CAGpE"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export class MockCallHistoryLog {
|
|
2
|
+
body;
|
|
3
|
+
method;
|
|
4
|
+
headers;
|
|
5
|
+
fullUrl;
|
|
6
|
+
origin;
|
|
7
|
+
path;
|
|
8
|
+
searchParams;
|
|
9
|
+
protocol;
|
|
10
|
+
host;
|
|
11
|
+
port;
|
|
12
|
+
hash;
|
|
13
|
+
constructor(data) {
|
|
14
|
+
this.body = data.body;
|
|
15
|
+
this.method = data.method;
|
|
16
|
+
this.headers = data.headers;
|
|
17
|
+
this.fullUrl = data.fullUrl;
|
|
18
|
+
this.origin = data.origin;
|
|
19
|
+
this.path = data.path;
|
|
20
|
+
this.searchParams = data.searchParams;
|
|
21
|
+
this.protocol = data.protocol;
|
|
22
|
+
this.host = data.host;
|
|
23
|
+
this.port = data.port;
|
|
24
|
+
this.hash = data.hash;
|
|
25
|
+
}
|
|
26
|
+
json() {
|
|
27
|
+
if (this.body === null)
|
|
28
|
+
return null;
|
|
29
|
+
return JSON.parse(this.body);
|
|
30
|
+
}
|
|
31
|
+
toMap() {
|
|
32
|
+
return new Map([
|
|
33
|
+
['body', this.body],
|
|
34
|
+
['method', this.method],
|
|
35
|
+
['headers', this.headers],
|
|
36
|
+
['fullUrl', this.fullUrl],
|
|
37
|
+
['origin', this.origin],
|
|
38
|
+
['path', this.path],
|
|
39
|
+
['searchParams', this.searchParams],
|
|
40
|
+
['protocol', this.protocol],
|
|
41
|
+
['host', this.host],
|
|
42
|
+
['port', this.port],
|
|
43
|
+
['hash', this.hash],
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
toString() {
|
|
47
|
+
return [
|
|
48
|
+
`method->${this.method}`,
|
|
49
|
+
`protocol->${this.protocol}`,
|
|
50
|
+
`host->${this.host}`,
|
|
51
|
+
`port->${this.port}`,
|
|
52
|
+
`origin->${this.origin}`,
|
|
53
|
+
`path->${this.path}`,
|
|
54
|
+
`hash->${this.hash}`,
|
|
55
|
+
`fullUrl->${this.fullUrl}`,
|
|
56
|
+
].join('|');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export class MockCallHistory {
|
|
60
|
+
logs = [];
|
|
61
|
+
get length() {
|
|
62
|
+
return this.logs.length;
|
|
63
|
+
}
|
|
64
|
+
record(data) {
|
|
65
|
+
this.logs.push(data instanceof MockCallHistoryLog ? data : new MockCallHistoryLog(data));
|
|
66
|
+
}
|
|
67
|
+
called(criteria) {
|
|
68
|
+
if (criteria === undefined)
|
|
69
|
+
return this.logs.length > 0;
|
|
70
|
+
return this.filterCalls(criteria).length > 0;
|
|
71
|
+
}
|
|
72
|
+
calls() {
|
|
73
|
+
return [...this.logs];
|
|
74
|
+
}
|
|
75
|
+
firstCall(criteria) {
|
|
76
|
+
if (criteria === undefined)
|
|
77
|
+
return this.logs[0];
|
|
78
|
+
return this.filterCalls(criteria)[0];
|
|
79
|
+
}
|
|
80
|
+
lastCall(criteria) {
|
|
81
|
+
if (criteria === undefined)
|
|
82
|
+
return this.logs[this.logs.length - 1];
|
|
83
|
+
const filtered = this.filterCalls(criteria);
|
|
84
|
+
return filtered[filtered.length - 1];
|
|
85
|
+
}
|
|
86
|
+
nthCall(n, criteria) {
|
|
87
|
+
if (criteria === undefined)
|
|
88
|
+
return this.logs[n - 1];
|
|
89
|
+
return this.filterCalls(criteria)[n - 1];
|
|
90
|
+
}
|
|
91
|
+
clear() {
|
|
92
|
+
this.logs = [];
|
|
93
|
+
}
|
|
94
|
+
[Symbol.iterator]() {
|
|
95
|
+
return this.logs[Symbol.iterator]();
|
|
96
|
+
}
|
|
97
|
+
filterCalls(criteria, options) {
|
|
98
|
+
if (typeof criteria === 'function') {
|
|
99
|
+
return this.logs.filter(criteria);
|
|
100
|
+
}
|
|
101
|
+
if (criteria instanceof RegExp) {
|
|
102
|
+
return this.logs.filter((log) => criteria.test(log.toString()));
|
|
103
|
+
}
|
|
104
|
+
const operator = options?.operator ?? 'OR';
|
|
105
|
+
const keys = Object.keys(criteria);
|
|
106
|
+
const predicates = keys
|
|
107
|
+
.filter((key) => criteria[key] !== undefined)
|
|
108
|
+
.map((key) => (log) => log[key] === criteria[key]);
|
|
109
|
+
if (predicates.length === 0)
|
|
110
|
+
return [...this.logs];
|
|
111
|
+
return this.logs.filter((log) => operator === 'AND' ? predicates.every((p) => p(log)) : predicates.some((p) => p(log)));
|
|
112
|
+
}
|
|
113
|
+
filterBy(field, filter) {
|
|
114
|
+
return this.logs.filter((log) => typeof filter === 'string' ? log[field] === filter : filter.test(String(log[field])));
|
|
115
|
+
}
|
|
116
|
+
filterCallsByMethod(filter) {
|
|
117
|
+
return this.filterBy('method', filter);
|
|
118
|
+
}
|
|
119
|
+
filterCallsByPath(filter) {
|
|
120
|
+
return this.filterBy('path', filter);
|
|
121
|
+
}
|
|
122
|
+
filterCallsByOrigin(filter) {
|
|
123
|
+
return this.filterBy('origin', filter);
|
|
124
|
+
}
|
|
125
|
+
filterCallsByProtocol(filter) {
|
|
126
|
+
return this.filterBy('protocol', filter);
|
|
127
|
+
}
|
|
128
|
+
filterCallsByHost(filter) {
|
|
129
|
+
return this.filterBy('host', filter);
|
|
130
|
+
}
|
|
131
|
+
filterCallsByPort(filter) {
|
|
132
|
+
return this.filterBy('port', filter);
|
|
133
|
+
}
|
|
134
|
+
filterCallsByHash(filter) {
|
|
135
|
+
return this.filterBy('hash', filter);
|
|
136
|
+
}
|
|
137
|
+
filterCallsByFullUrl(filter) {
|
|
138
|
+
return this.filterBy('fullUrl', filter);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { MockCallHistory } from './mock-call-history';
|
|
2
|
+
/** Structural type to avoid cross-package nominal type mismatch on MSW's private fields */
|
|
3
|
+
interface SetupServerLike {
|
|
4
|
+
use(...handlers: Array<unknown>): void;
|
|
5
|
+
resetHandlers(...handlers: Array<unknown>): void;
|
|
6
|
+
listen(options?: Record<string, unknown>): void;
|
|
7
|
+
close(): void;
|
|
8
|
+
}
|
|
9
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
10
|
+
type PathMatcher = string | RegExp | ((path: string) => boolean);
|
|
11
|
+
type HeaderValueMatcher = string | RegExp | ((value: string) => boolean);
|
|
12
|
+
type BodyMatcher = string | RegExp | ((body: string) => boolean);
|
|
13
|
+
export interface InterceptOptions {
|
|
14
|
+
path: PathMatcher;
|
|
15
|
+
method?: HttpMethod;
|
|
16
|
+
headers?: Record<string, HeaderValueMatcher>;
|
|
17
|
+
body?: BodyMatcher;
|
|
18
|
+
query?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
export interface ReplyOptions {
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
type ReplyCallback = (req: {
|
|
24
|
+
body: string | null;
|
|
25
|
+
}) => unknown | Promise<unknown>;
|
|
26
|
+
export interface MockReplyChain {
|
|
27
|
+
times(n: number): void;
|
|
28
|
+
persist(): void;
|
|
29
|
+
delay(ms: number): void;
|
|
30
|
+
}
|
|
31
|
+
export interface MockInterceptor {
|
|
32
|
+
reply(status: number, body?: unknown, options?: ReplyOptions): MockReplyChain;
|
|
33
|
+
reply(status: number, callback: ReplyCallback): MockReplyChain;
|
|
34
|
+
replyWithError(error: Error): MockReplyChain;
|
|
35
|
+
}
|
|
36
|
+
export interface MockPool {
|
|
37
|
+
intercept(options: InterceptOptions): MockInterceptor;
|
|
38
|
+
}
|
|
39
|
+
export interface PendingInterceptor {
|
|
40
|
+
origin: string;
|
|
41
|
+
path: string;
|
|
42
|
+
method: string;
|
|
43
|
+
consumed: boolean;
|
|
44
|
+
times: number;
|
|
45
|
+
timesInvoked: number;
|
|
46
|
+
persist: boolean;
|
|
47
|
+
}
|
|
48
|
+
export declare class FetchMock {
|
|
49
|
+
private readonly _calls;
|
|
50
|
+
private server;
|
|
51
|
+
private readonly ownsServer;
|
|
52
|
+
private interceptors;
|
|
53
|
+
private netConnectAllowed;
|
|
54
|
+
get calls(): MockCallHistory;
|
|
55
|
+
constructor(externalServer?: SetupServerLike);
|
|
56
|
+
activate(): void;
|
|
57
|
+
disableNetConnect(): void;
|
|
58
|
+
enableNetConnect(matcher?: string | RegExp | ((host: string) => boolean)): void;
|
|
59
|
+
private isNetConnectAllowed;
|
|
60
|
+
getCallHistory(): MockCallHistory;
|
|
61
|
+
clearCallHistory(): void;
|
|
62
|
+
deactivate(): void;
|
|
63
|
+
assertNoPendingInterceptors(): void;
|
|
64
|
+
pendingInterceptors(): PendingInterceptor[];
|
|
65
|
+
get(origin: string): MockPool;
|
|
66
|
+
}
|
|
67
|
+
export declare function createFetchMock(externalServer?: SetupServerLike): FetchMock;
|
|
68
|
+
export {};
|
|
69
|
+
//# sourceMappingURL=mock-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-server.d.ts","sourceRoot":"","sources":["../src/mock-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { http, HttpResponse } from 'msw';
|
|
2
|
+
import { setupServer } from 'msw/node';
|
|
3
|
+
import { MockCallHistory } from './mock-call-history';
|
|
4
|
+
function isPending(p) {
|
|
5
|
+
if (p.persist)
|
|
6
|
+
return p.timesInvoked === 0;
|
|
7
|
+
return p.timesInvoked < p.times;
|
|
8
|
+
}
|
|
9
|
+
function escapeRegExp(str) {
|
|
10
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11
|
+
}
|
|
12
|
+
function matchesValue(value, matcher) {
|
|
13
|
+
if (typeof matcher === 'string')
|
|
14
|
+
return value === matcher;
|
|
15
|
+
if (matcher instanceof RegExp)
|
|
16
|
+
return matcher.test(value);
|
|
17
|
+
return matcher(value);
|
|
18
|
+
}
|
|
19
|
+
function getHttpMethod(method) {
|
|
20
|
+
const methods = {
|
|
21
|
+
GET: http.get,
|
|
22
|
+
POST: http.post,
|
|
23
|
+
PUT: http.put,
|
|
24
|
+
DELETE: http.delete,
|
|
25
|
+
PATCH: http.patch,
|
|
26
|
+
};
|
|
27
|
+
return methods[method];
|
|
28
|
+
}
|
|
29
|
+
function matchPath(request, origin, pathMatcher) {
|
|
30
|
+
if (typeof pathMatcher === 'string')
|
|
31
|
+
return true; // string paths are matched by MSW URL pattern
|
|
32
|
+
const url = new URL(request.url);
|
|
33
|
+
const originPrefix = new URL(origin).pathname.replace(/\/$/, '');
|
|
34
|
+
const fullPath = url.pathname + url.search;
|
|
35
|
+
const relativePath = fullPath.startsWith(originPrefix)
|
|
36
|
+
? fullPath.slice(originPrefix.length)
|
|
37
|
+
: fullPath;
|
|
38
|
+
return matchesValue(relativePath, pathMatcher);
|
|
39
|
+
}
|
|
40
|
+
function matchQuery(request, query) {
|
|
41
|
+
if (!query)
|
|
42
|
+
return true;
|
|
43
|
+
const url = new URL(request.url);
|
|
44
|
+
for (const [key, value] of Object.entries(query)) {
|
|
45
|
+
if (url.searchParams.get(key) !== value)
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
function matchHeaders(request, headers) {
|
|
51
|
+
if (!headers)
|
|
52
|
+
return true;
|
|
53
|
+
for (const [key, matcher] of Object.entries(headers)) {
|
|
54
|
+
const value = request.headers.get(key);
|
|
55
|
+
if (value === null || !matchesValue(value, matcher))
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
function matchBody(bodyText, bodyMatcher) {
|
|
61
|
+
if (!bodyMatcher)
|
|
62
|
+
return true;
|
|
63
|
+
return matchesValue(bodyText ?? '', bodyMatcher);
|
|
64
|
+
}
|
|
65
|
+
function recordCall(callHistory, request, bodyText) {
|
|
66
|
+
const url = new URL(request.url);
|
|
67
|
+
const requestHeaders = {};
|
|
68
|
+
request.headers.forEach((value, key) => {
|
|
69
|
+
requestHeaders[key] = value;
|
|
70
|
+
});
|
|
71
|
+
const searchParams = {};
|
|
72
|
+
url.searchParams.forEach((value, key) => {
|
|
73
|
+
searchParams[key] = value;
|
|
74
|
+
});
|
|
75
|
+
callHistory.record({
|
|
76
|
+
body: bodyText,
|
|
77
|
+
method: request.method,
|
|
78
|
+
headers: requestHeaders,
|
|
79
|
+
fullUrl: url.origin + url.pathname + url.search,
|
|
80
|
+
origin: url.origin,
|
|
81
|
+
path: url.pathname,
|
|
82
|
+
searchParams,
|
|
83
|
+
protocol: url.protocol,
|
|
84
|
+
host: url.host,
|
|
85
|
+
port: url.port,
|
|
86
|
+
hash: url.hash,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function buildResponse(status, responseBody, replyOptions) {
|
|
90
|
+
const headers = replyOptions?.headers ? new Headers(replyOptions.headers) : undefined;
|
|
91
|
+
if (responseBody === null || responseBody === undefined) {
|
|
92
|
+
return new HttpResponse(null, { status, headers });
|
|
93
|
+
}
|
|
94
|
+
return HttpResponse.json(responseBody, { status, headers });
|
|
95
|
+
}
|
|
96
|
+
export class FetchMock {
|
|
97
|
+
_calls = new MockCallHistory();
|
|
98
|
+
server;
|
|
99
|
+
ownsServer;
|
|
100
|
+
interceptors = [];
|
|
101
|
+
netConnectAllowed = false;
|
|
102
|
+
get calls() {
|
|
103
|
+
return this._calls;
|
|
104
|
+
}
|
|
105
|
+
constructor(externalServer) {
|
|
106
|
+
this.server = externalServer ?? null;
|
|
107
|
+
this.ownsServer = !externalServer;
|
|
108
|
+
}
|
|
109
|
+
activate() {
|
|
110
|
+
if (this.ownsServer) {
|
|
111
|
+
this.server = setupServer();
|
|
112
|
+
this.server.listen({
|
|
113
|
+
onUnhandledRequest: (request, print) => {
|
|
114
|
+
if (this.isNetConnectAllowed(request))
|
|
115
|
+
return;
|
|
116
|
+
print.error();
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
disableNetConnect() {
|
|
122
|
+
this.netConnectAllowed = false;
|
|
123
|
+
}
|
|
124
|
+
enableNetConnect(matcher) {
|
|
125
|
+
this.netConnectAllowed = matcher ?? true;
|
|
126
|
+
}
|
|
127
|
+
isNetConnectAllowed(request) {
|
|
128
|
+
if (this.netConnectAllowed === true)
|
|
129
|
+
return true;
|
|
130
|
+
if (this.netConnectAllowed === false)
|
|
131
|
+
return false;
|
|
132
|
+
const host = new URL(request.url).host;
|
|
133
|
+
if (typeof this.netConnectAllowed === 'string')
|
|
134
|
+
return host === this.netConnectAllowed;
|
|
135
|
+
if (this.netConnectAllowed instanceof RegExp)
|
|
136
|
+
return this.netConnectAllowed.test(host);
|
|
137
|
+
return this.netConnectAllowed(host);
|
|
138
|
+
}
|
|
139
|
+
getCallHistory() {
|
|
140
|
+
return this._calls;
|
|
141
|
+
}
|
|
142
|
+
clearCallHistory() {
|
|
143
|
+
this._calls.clear();
|
|
144
|
+
}
|
|
145
|
+
deactivate() {
|
|
146
|
+
this.interceptors = [];
|
|
147
|
+
this._calls.clear();
|
|
148
|
+
if (this.ownsServer) {
|
|
149
|
+
this.server?.close();
|
|
150
|
+
this.server = null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
assertNoPendingInterceptors() {
|
|
154
|
+
const unconsumed = this.interceptors.filter(isPending);
|
|
155
|
+
this.interceptors = [];
|
|
156
|
+
this._calls.clear();
|
|
157
|
+
if (this.ownsServer) {
|
|
158
|
+
this.server?.resetHandlers();
|
|
159
|
+
}
|
|
160
|
+
if (unconsumed.length > 0) {
|
|
161
|
+
const descriptions = unconsumed.map((p) => ` ${p.method} ${p.origin}${p.path}`);
|
|
162
|
+
throw new Error(`Pending interceptor(s) not consumed:\n${descriptions.join('\n')}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
pendingInterceptors() {
|
|
166
|
+
return this.interceptors.filter(isPending).map((p) => ({ ...p }));
|
|
167
|
+
}
|
|
168
|
+
get(origin) {
|
|
169
|
+
return {
|
|
170
|
+
intercept: (options) => {
|
|
171
|
+
const method = options.method ?? 'GET';
|
|
172
|
+
const pathStr = typeof options.path === 'string'
|
|
173
|
+
? options.path
|
|
174
|
+
: typeof options.path === 'function'
|
|
175
|
+
? '<function>'
|
|
176
|
+
: options.path.toString();
|
|
177
|
+
const pending = {
|
|
178
|
+
origin,
|
|
179
|
+
path: pathStr,
|
|
180
|
+
method,
|
|
181
|
+
consumed: false,
|
|
182
|
+
times: 1,
|
|
183
|
+
timesInvoked: 0,
|
|
184
|
+
persist: false,
|
|
185
|
+
};
|
|
186
|
+
this.interceptors.push(pending);
|
|
187
|
+
const urlPattern = typeof options.path === 'string'
|
|
188
|
+
? `${origin}${options.path}`
|
|
189
|
+
: new RegExp(`^${escapeRegExp(origin)}`);
|
|
190
|
+
const matchAndConsume = async (request) => {
|
|
191
|
+
if (!pending.persist && pending.timesInvoked >= pending.times)
|
|
192
|
+
return;
|
|
193
|
+
if (!matchPath(request, origin, options.path))
|
|
194
|
+
return;
|
|
195
|
+
if (!matchQuery(request, options.query))
|
|
196
|
+
return;
|
|
197
|
+
if (!matchHeaders(request, options.headers))
|
|
198
|
+
return;
|
|
199
|
+
const bodyText = (await request.text()) || null;
|
|
200
|
+
if (!matchBody(bodyText, options.body))
|
|
201
|
+
return;
|
|
202
|
+
pending.timesInvoked++;
|
|
203
|
+
if (!pending.persist && pending.timesInvoked >= pending.times) {
|
|
204
|
+
pending.consumed = true;
|
|
205
|
+
}
|
|
206
|
+
recordCall(this._calls, request, bodyText);
|
|
207
|
+
return bodyText;
|
|
208
|
+
};
|
|
209
|
+
const registerHandler = (handlerFn) => {
|
|
210
|
+
const handler = getHttpMethod(method)(urlPattern, async ({ request }) => handlerFn(request));
|
|
211
|
+
if (!this.server) {
|
|
212
|
+
throw new Error('FetchMock server is not active. Call activate() before registering interceptors.');
|
|
213
|
+
}
|
|
214
|
+
this.server.use(handler);
|
|
215
|
+
};
|
|
216
|
+
const buildChain = (delayRef) => ({
|
|
217
|
+
times(n) {
|
|
218
|
+
pending.times = n;
|
|
219
|
+
pending.consumed = false;
|
|
220
|
+
},
|
|
221
|
+
persist() {
|
|
222
|
+
pending.persist = true;
|
|
223
|
+
pending.consumed = false;
|
|
224
|
+
},
|
|
225
|
+
delay(ms) {
|
|
226
|
+
delayRef.ms = ms;
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
reply: (status, bodyOrCallback, replyOptions) => {
|
|
231
|
+
const delayRef = { ms: 0 };
|
|
232
|
+
registerHandler(async (request) => {
|
|
233
|
+
const bodyText = await matchAndConsume(request);
|
|
234
|
+
if (bodyText === undefined)
|
|
235
|
+
return;
|
|
236
|
+
if (delayRef.ms > 0) {
|
|
237
|
+
await new Promise((resolve) => setTimeout(resolve, delayRef.ms));
|
|
238
|
+
}
|
|
239
|
+
let responseBody;
|
|
240
|
+
if (typeof bodyOrCallback === 'function') {
|
|
241
|
+
responseBody = await bodyOrCallback({
|
|
242
|
+
body: bodyText || null,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
responseBody = bodyOrCallback;
|
|
247
|
+
}
|
|
248
|
+
return buildResponse(status, responseBody, replyOptions);
|
|
249
|
+
});
|
|
250
|
+
return buildChain(delayRef);
|
|
251
|
+
},
|
|
252
|
+
replyWithError: () => {
|
|
253
|
+
const delayRef = { ms: 0 };
|
|
254
|
+
registerHandler(async (request) => {
|
|
255
|
+
const bodyText = await matchAndConsume(request);
|
|
256
|
+
if (bodyText === undefined)
|
|
257
|
+
return;
|
|
258
|
+
return HttpResponse.error();
|
|
259
|
+
});
|
|
260
|
+
return buildChain(delayRef);
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
export function createFetchMock(externalServer) {
|
|
268
|
+
return new FetchMock(externalServer);
|
|
269
|
+
}
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
## `new FetchMock(server?)`
|
|
4
|
+
|
|
5
|
+
Creates a `FetchMock` instance.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { FetchMock } from 'msw-fetch-mock';
|
|
9
|
+
|
|
10
|
+
// Standalone (creates internal MSW server)
|
|
11
|
+
const fetchMock = new FetchMock();
|
|
12
|
+
|
|
13
|
+
// With external MSW server
|
|
14
|
+
import { setupServer } from 'msw/node';
|
|
15
|
+
const server = setupServer();
|
|
16
|
+
const fetchMock = new FetchMock(server);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| Parameter | Type | Required | Description |
|
|
20
|
+
| --------- | ------------- | -------- | ------------------------------------------------------- |
|
|
21
|
+
| `server` | `SetupServer` | No | Existing MSW server. Creates one internally if omitted. |
|
|
22
|
+
|
|
23
|
+
> `createFetchMock(server?)` is also available as a backward-compatible factory function.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## `FetchMock`
|
|
28
|
+
|
|
29
|
+
### Lifecycle
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
fetchMock.activate(); // start intercepting (calls server.listen())
|
|
33
|
+
fetchMock.deactivate(); // stop intercepting (calls server.close())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> If you pass an external server that you manage yourself, `activate()` / `deactivate()` are no-ops.
|
|
37
|
+
|
|
38
|
+
### `fetchMock.calls`
|
|
39
|
+
|
|
40
|
+
Returns the `MockCallHistory` instance for inspecting and managing recorded requests.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// Check call count
|
|
44
|
+
expect(fetchMock.calls.length).toBe(3);
|
|
45
|
+
|
|
46
|
+
// Inspect last call
|
|
47
|
+
const last = fetchMock.calls.lastCall();
|
|
48
|
+
|
|
49
|
+
// Clear history
|
|
50
|
+
fetchMock.calls.clear();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `fetchMock.get(origin)`
|
|
54
|
+
|
|
55
|
+
Returns a `MockPool` scoped to the given origin.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const pool = fetchMock.get('https://api.example.com');
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `fetchMock.disableNetConnect()`
|
|
62
|
+
|
|
63
|
+
Prevents any real network requests. Unmatched requests will throw.
|
|
64
|
+
|
|
65
|
+
### `fetchMock.enableNetConnect(matcher?)`
|
|
66
|
+
|
|
67
|
+
Allows real network requests to pass through. Without arguments, all requests are allowed. With a matcher, only matching hosts pass through.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Allow all network requests
|
|
71
|
+
fetchMock.enableNetConnect();
|
|
72
|
+
|
|
73
|
+
// Allow specific host (exact match)
|
|
74
|
+
fetchMock.enableNetConnect('api.example.com');
|
|
75
|
+
|
|
76
|
+
// Allow hosts matching a RegExp
|
|
77
|
+
fetchMock.enableNetConnect(/\.example\.com$/);
|
|
78
|
+
|
|
79
|
+
// Allow hosts matching a function
|
|
80
|
+
fetchMock.enableNetConnect((host) => host.endsWith('.test'));
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Parameter | Type | Required | Description |
|
|
84
|
+
| --------- | ----------------------------------------------- | -------- | ------------------------------------ |
|
|
85
|
+
| `matcher` | `string \| RegExp \| (host: string) => boolean` | No | Host matcher. Allows all if omitted. |
|
|
86
|
+
|
|
87
|
+
### `fetchMock.getCallHistory()`
|
|
88
|
+
|
|
89
|
+
Returns the `MockCallHistory` instance. Cloudflare-compatible alias for `fetchMock.calls`.
|
|
90
|
+
|
|
91
|
+
### `fetchMock.clearCallHistory()`
|
|
92
|
+
|
|
93
|
+
Clears all recorded calls. Cloudflare-compatible alias for `fetchMock.calls.clear()`.
|
|
94
|
+
|
|
95
|
+
### `fetchMock.assertNoPendingInterceptors()`
|
|
96
|
+
|
|
97
|
+
Throws an error if any registered interceptor has not been consumed. Also **automatically clears** call history and resets handlers. Use in `afterEach` to catch missing requests.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
afterEach(() => fetchMock.assertNoPendingInterceptors());
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
> Call history is cleared automatically — no need for a separate `calls.clear()` call.
|
|
104
|
+
|
|
105
|
+
### `fetchMock.pendingInterceptors()`
|
|
106
|
+
|
|
107
|
+
Returns an array of unconsumed interceptors with metadata:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
interface PendingInterceptor {
|
|
111
|
+
origin: string;
|
|
112
|
+
path: string;
|
|
113
|
+
method: string;
|
|
114
|
+
consumed: boolean;
|
|
115
|
+
times: number;
|
|
116
|
+
timesInvoked: number;
|
|
117
|
+
persist: boolean;
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## `MockPool`
|
|
124
|
+
|
|
125
|
+
### `pool.intercept(options)`
|
|
126
|
+
|
|
127
|
+
Registers an interceptor for matching requests.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
pool.intercept({
|
|
131
|
+
path: '/users',
|
|
132
|
+
method: 'GET',
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Returns: `MockInterceptor`
|
|
137
|
+
|
|
138
|
+
#### `InterceptOptions`
|
|
139
|
+
|
|
140
|
+
| Property | Type | Required | Description |
|
|
141
|
+
| --------- | ---------------------------------------------------------------- | -------- | -------------------------------------- |
|
|
142
|
+
| `path` | `string \| RegExp \| (path: string) => boolean` | Yes | URL pathname to match |
|
|
143
|
+
| `method` | `'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH'` | No | HTTP method (default: `'GET'`) |
|
|
144
|
+
| `headers` | `Record<string, string \| RegExp \| (value: string) => boolean>` | No | Header matchers |
|
|
145
|
+
| `body` | `string \| RegExp \| (body: string) => boolean` | No | Request body matcher |
|
|
146
|
+
| `query` | `Record<string, string>` | No | Query parameter matchers (exact match) |
|
|
147
|
+
|
|
148
|
+
#### Path Matching
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// Exact string
|
|
152
|
+
.intercept({ path: '/users' })
|
|
153
|
+
|
|
154
|
+
// RegExp
|
|
155
|
+
.intercept({ path: /^\/users\/\d+$/ })
|
|
156
|
+
|
|
157
|
+
// Function
|
|
158
|
+
.intercept({ path: (p) => p.startsWith('/users') })
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### Header Matching
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
.intercept({
|
|
165
|
+
path: '/api',
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: {
|
|
168
|
+
'content-type': 'application/json', // exact match
|
|
169
|
+
authorization: /^Bearer /, // regex
|
|
170
|
+
'x-custom': (v) => v.includes('special'), // function
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### Body Matching
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
.intercept({
|
|
179
|
+
path: '/api',
|
|
180
|
+
method: 'POST',
|
|
181
|
+
body: '{"key":"value"}', // exact match
|
|
182
|
+
// body: /keyword/, // regex
|
|
183
|
+
// body: (b) => b.includes('key'), // function
|
|
184
|
+
})
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### Query Parameters
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
.intercept({
|
|
191
|
+
path: '/search',
|
|
192
|
+
method: 'GET',
|
|
193
|
+
query: { q: 'test', page: '1' },
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## `MockInterceptor`
|
|
200
|
+
|
|
201
|
+
### `interceptor.reply(status, body?, options?)`
|
|
202
|
+
|
|
203
|
+
Defines the mock response.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// Static body
|
|
207
|
+
.reply(200, { users: [] })
|
|
208
|
+
|
|
209
|
+
// With response headers
|
|
210
|
+
.reply(200, { users: [] }, { headers: { 'x-request-id': '123' } })
|
|
211
|
+
|
|
212
|
+
// Callback (receives request info)
|
|
213
|
+
.reply(200, (req) => {
|
|
214
|
+
const input = JSON.parse(req.body!);
|
|
215
|
+
return { echo: input };
|
|
216
|
+
})
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Returns: `MockReplyChain`
|
|
220
|
+
|
|
221
|
+
| Parameter | Type | Description |
|
|
222
|
+
| --------- | -------------------------------------- | ------------------------- |
|
|
223
|
+
| `status` | `number` | HTTP status code |
|
|
224
|
+
| `body` | `unknown \| (req) => unknown` | Response body or callback |
|
|
225
|
+
| `options` | `{ headers?: Record<string, string> }` | Response headers |
|
|
226
|
+
|
|
227
|
+
### `interceptor.replyWithError(error)`
|
|
228
|
+
|
|
229
|
+
Replies with a network error (simulates a connection failure).
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
.replyWithError(new Error('connection refused'))
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Returns: `MockReplyChain`
|
|
236
|
+
|
|
237
|
+
| Parameter | Type | Description |
|
|
238
|
+
| --------- | ------- | ---------------------------------------- |
|
|
239
|
+
| `error` | `Error` | The error instance (used for semantics). |
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## `MockReplyChain`
|
|
244
|
+
|
|
245
|
+
### `chain.times(n)`
|
|
246
|
+
|
|
247
|
+
Interceptor will match exactly `n` times, then be consumed.
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
.reply(200, { ok: true }).times(3)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### `chain.persist()`
|
|
254
|
+
|
|
255
|
+
Interceptor will match indefinitely (never consumed).
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
.reply(200, { ok: true }).persist()
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### `chain.delay(ms)`
|
|
262
|
+
|
|
263
|
+
Adds a delay before the response is sent.
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
.reply(200, { ok: true }).delay(500)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## `MockCallHistory`
|
|
272
|
+
|
|
273
|
+
Tracks all requests that pass through the mock server.
|
|
274
|
+
|
|
275
|
+
### `history.length`
|
|
276
|
+
|
|
277
|
+
Returns the number of recorded calls.
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
expect(fetchMock.calls.length).toBe(3);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### `history.called(criteria?)`
|
|
284
|
+
|
|
285
|
+
Returns `true` if any calls match the given criteria, or if any calls exist when no criteria is provided.
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// Any calls recorded?
|
|
289
|
+
expect(fetchMock.calls.called()).toBe(true);
|
|
290
|
+
|
|
291
|
+
// Calls matching a filter?
|
|
292
|
+
expect(fetchMock.calls.called({ method: 'POST' })).toBe(true);
|
|
293
|
+
expect(fetchMock.calls.called(/\/users/)).toBe(true);
|
|
294
|
+
expect(fetchMock.calls.called((log) => log.path === '/users')).toBe(true);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### `history.calls()`
|
|
298
|
+
|
|
299
|
+
Returns a copy of all recorded `MockCallHistoryLog` entries.
|
|
300
|
+
|
|
301
|
+
### `history.firstCall(criteria?)`
|
|
302
|
+
|
|
303
|
+
Returns the first recorded call, or `undefined`. Optionally filters by criteria.
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
const first = fetchMock.calls.firstCall();
|
|
307
|
+
const firstPost = fetchMock.calls.firstCall({ method: 'POST' });
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### `history.lastCall(criteria?)`
|
|
311
|
+
|
|
312
|
+
Returns the most recent recorded call, or `undefined`. Optionally filters by criteria.
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
const last = fetchMock.calls.lastCall();
|
|
316
|
+
const lastPost = fetchMock.calls.lastCall({ method: 'POST' });
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### `history.nthCall(n, criteria?)`
|
|
320
|
+
|
|
321
|
+
Returns the nth call (1-indexed), or `undefined`. Optionally filters by criteria.
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
const second = fetchMock.calls.nthCall(2);
|
|
325
|
+
const secondPost = fetchMock.calls.nthCall(2, { method: 'POST' });
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### `history.clear()`
|
|
329
|
+
|
|
330
|
+
Removes all recorded calls.
|
|
331
|
+
|
|
332
|
+
### `history.filterCalls(criteria, options?)`
|
|
333
|
+
|
|
334
|
+
Flexible filtering with three overloads:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// Function predicate
|
|
338
|
+
history.filterCalls((log) => log.body?.includes('test'));
|
|
339
|
+
|
|
340
|
+
// RegExp (tested against log.toString())
|
|
341
|
+
history.filterCalls(/POST.*\/users/);
|
|
342
|
+
|
|
343
|
+
// Structured criteria
|
|
344
|
+
history.filterCalls(
|
|
345
|
+
{ method: 'POST', path: '/users' },
|
|
346
|
+
{ operator: 'AND' } // default: 'OR'
|
|
347
|
+
);
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
#### `CallHistoryFilterCriteria`
|
|
351
|
+
|
|
352
|
+
| Property | Type | Description |
|
|
353
|
+
| ---------- | -------- | ------------ |
|
|
354
|
+
| `method` | `string` | HTTP method |
|
|
355
|
+
| `path` | `string` | URL pathname |
|
|
356
|
+
| `origin` | `string` | URL origin |
|
|
357
|
+
| `protocol` | `string` | URL protocol |
|
|
358
|
+
| `host` | `string` | URL host |
|
|
359
|
+
| `port` | `string` | URL port |
|
|
360
|
+
| `hash` | `string` | URL hash |
|
|
361
|
+
| `fullUrl` | `string` | Complete URL |
|
|
362
|
+
|
|
363
|
+
### `history.filterCallsByMethod(filter)`
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
history.filterCallsByMethod('POST');
|
|
367
|
+
history.filterCallsByMethod(/^P/); // POST, PUT, PATCH
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### `history.filterCallsByPath(filter)`
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
history.filterCallsByPath('/users');
|
|
374
|
+
history.filterCallsByPath(/\/users\/\d+/);
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### `history.filterCallsByOrigin(filter)`
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
history.filterCallsByOrigin('https://api.example.com');
|
|
381
|
+
history.filterCallsByOrigin(/example\.com/);
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### `history.filterCallsByProtocol(filter)`
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
history.filterCallsByProtocol('https:');
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### `history.filterCallsByHost(filter)`
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
history.filterCallsByHost('api.example.com');
|
|
394
|
+
history.filterCallsByHost(/example\.com/);
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### `history.filterCallsByPort(filter)`
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
history.filterCallsByPort('8787');
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### `history.filterCallsByHash(filter)`
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
history.filterCallsByHash('#section');
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### `history.filterCallsByFullUrl(filter)`
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
history.filterCallsByFullUrl('https://api.example.com/users');
|
|
413
|
+
history.filterCallsByFullUrl(/\/users\?page=1/);
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Iteration
|
|
417
|
+
|
|
418
|
+
`MockCallHistory` implements `Symbol.iterator`:
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
for (const call of history) {
|
|
422
|
+
console.log(call.method, call.path);
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## `MockCallHistoryLog`
|
|
429
|
+
|
|
430
|
+
Each recorded call is an instance of `MockCallHistoryLog` with the following properties:
|
|
431
|
+
|
|
432
|
+
| Property | Type | Description |
|
|
433
|
+
| -------------- | ------------------------ | ---------------------------------- |
|
|
434
|
+
| `method` | `string` | HTTP method |
|
|
435
|
+
| `fullUrl` | `string` | Complete URL |
|
|
436
|
+
| `origin` | `string` | URL origin (`https://example.com`) |
|
|
437
|
+
| `path` | `string` | URL pathname (`/users`) |
|
|
438
|
+
| `searchParams` | `Record<string, string>` | Query parameters |
|
|
439
|
+
| `headers` | `Record<string, string>` | Request headers |
|
|
440
|
+
| `body` | `string \| null` | Request body |
|
|
441
|
+
| `protocol` | `string` | URL protocol (`https:`) |
|
|
442
|
+
| `host` | `string` | URL host |
|
|
443
|
+
| `port` | `string` | URL port |
|
|
444
|
+
| `hash` | `string` | URL hash |
|
|
445
|
+
|
|
446
|
+
### `log.json()`
|
|
447
|
+
|
|
448
|
+
Parses the request body as JSON. Returns `null` if body is `null`.
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
const call = fetchMock.calls.lastCall()!;
|
|
452
|
+
const data = call.json() as { name: string };
|
|
453
|
+
expect(data.name).toBe('Alice');
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### `log.toMap()`
|
|
457
|
+
|
|
458
|
+
Returns a `Map` of all log properties.
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
const map = call.toMap();
|
|
462
|
+
expect(map.get('method')).toBe('POST');
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### `log.toString()`
|
|
466
|
+
|
|
467
|
+
Returns a pipe-delimited string representation for debugging and RegExp matching.
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
call.toString();
|
|
471
|
+
// "method->POST|protocol->https:|host->api.example.com|port->|origin->https://api.example.com|path->/users|hash->|fullUrl->https://api.example.com/users"
|
|
472
|
+
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Cloudflare Workers Migration Guide
|
|
2
|
+
|
|
3
|
+
If you're migrating tests from Cloudflare Workers' `cloudflare:test` to a standard Node.js test environment, `msw-fetch-mock` provides the same `fetchMock` API you already know.
|
|
4
|
+
|
|
5
|
+
## API Comparison
|
|
6
|
+
|
|
7
|
+
| cloudflare:test | msw-fetch-mock |
|
|
8
|
+
| -------------------------------------------------- | ----------------------------------------------------------- |
|
|
9
|
+
| `import { fetchMock } from 'cloudflare:test'` | `const fetchMock = new FetchMock(server)` |
|
|
10
|
+
| `fetchMock.activate()` | `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()` or `fetchMock.calls` |
|
|
18
|
+
| `fetchMock.clearCallHistory()` | `fetchMock.clearCallHistory()` or `fetchMock.calls.clear()` |
|
|
19
|
+
| `fetchMock.assertNoPendingInterceptors()` | `fetchMock.assertNoPendingInterceptors()` |
|
|
20
|
+
|
|
21
|
+
## Before (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('calls 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
|
+
## After (msw-fetch-mock)
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { setupServer } from 'msw/node';
|
|
48
|
+
import { FetchMock } from 'msw-fetch-mock';
|
|
49
|
+
|
|
50
|
+
const server = setupServer();
|
|
51
|
+
const fetchMock = new FetchMock(server);
|
|
52
|
+
|
|
53
|
+
beforeAll(() => fetchMock.activate());
|
|
54
|
+
afterAll(() => fetchMock.deactivate());
|
|
55
|
+
afterEach(() => fetchMock.assertNoPendingInterceptors());
|
|
56
|
+
|
|
57
|
+
it('calls API', async () => {
|
|
58
|
+
fetchMock
|
|
59
|
+
.get('https://api.example.com')
|
|
60
|
+
.intercept({ path: '/data', method: 'GET' })
|
|
61
|
+
.reply(200, { result: 'ok' });
|
|
62
|
+
|
|
63
|
+
const res = await fetch('https://api.example.com/data');
|
|
64
|
+
expect(await res.json()).toEqual({ result: 'ok' });
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Key Differences
|
|
69
|
+
|
|
70
|
+
| Aspect | cloudflare:test | msw-fetch-mock |
|
|
71
|
+
| -------------------- | ------------------------------------ | ------------------------------------------------- |
|
|
72
|
+
| Server lifecycle | Implicit (managed by test framework) | Explicit (`activate()` / `deactivate()`) |
|
|
73
|
+
| Call history access | `fetchMock.getCallHistory()` | `fetchMock.getCallHistory()` or `fetchMock.calls` |
|
|
74
|
+
| Call history cleanup | Automatic per test | Automatic via `assertNoPendingInterceptors()` |
|
|
75
|
+
| Network connect | Must call `disableNetConnect()` | MSW blocks unhandled requests by default |
|
|
76
|
+
| Runtime | Cloudflare Workers (workerd) | Node.js |
|
|
77
|
+
|
|
78
|
+
> **Note:** `getCallHistory()` and `clearCallHistory()` are provided as Cloudflare-compatible aliases. You can use either the Cloudflare-style methods or the `fetchMock.calls` getter — they are equivalent.
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "msw-fetch-mock",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Undici-style fetch mock API built on MSW (Mock Service Worker)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"msw",
|
|
9
|
+
"fetch",
|
|
10
|
+
"mock",
|
|
11
|
+
"testing",
|
|
12
|
+
"undici"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/recca0120/msw-fetch-mock.git"
|
|
17
|
+
},
|
|
18
|
+
"packageManager": "pnpm@10.12.4",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"source": "./src/index.ts",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"docs",
|
|
32
|
+
"LICENSE.md",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"prepare": "lefthook install",
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"test": "vitest",
|
|
39
|
+
"test:run": "vitest run",
|
|
40
|
+
"lint": "eslint src",
|
|
41
|
+
"lint:fix": "eslint src --fix",
|
|
42
|
+
"format": "prettier --write .",
|
|
43
|
+
"format:check": "prettier --check .",
|
|
44
|
+
"prepublishOnly": "tsc"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"msw": "^2.12.7"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@eslint/js": "^9.39.2",
|
|
51
|
+
"eslint": "^9.39.2",
|
|
52
|
+
"lefthook": "^2.0.15",
|
|
53
|
+
"msw": "^2.12.7",
|
|
54
|
+
"prettier": "^3.8.1",
|
|
55
|
+
"typescript": "^5.9.3",
|
|
56
|
+
"typescript-eslint": "^8.53.1",
|
|
57
|
+
"vitest": "^4.0.17"
|
|
58
|
+
}
|
|
59
|
+
}
|