Skip to content

Commit 0cc656a

Browse files
feat(SetupServerApi): allow using custom interceptors (#2464)
Co-authored-by: Artem Zakharchenko <[email protected]>
1 parent 50ce6a4 commit 0cc656a

File tree

6 files changed

+65
-10
lines changed

6 files changed

+65
-10
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ test('returns a mocked response', async ({ loadExample, fetch }) => {
193193

194194
##### Running all browser tests
195195

196+
Make sure Playwright chromium has been installed before running browser tests.
197+
196198
```sh
197199
pnpm test:browser
198200
```

src/native/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function setupServer(
1515
// Provision request interception via patching the `XMLHttpRequest` class only
1616
// in React Native. There is no `http`/`https` modules in that environment.
1717
return new SetupServerCommonApi(
18-
[FetchInterceptor, XMLHttpRequestInterceptor],
18+
[new FetchInterceptor(), new XMLHttpRequestInterceptor()],
1919
handlers,
2020
)
2121
}

src/node/SetupServerApi.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AsyncLocalStorage } from 'node:async_hooks'
2+
import type { HttpRequestEventMap, Interceptor } from '@mswjs/interceptors'
23
import { ClientRequestInterceptor } from '@mswjs/interceptors/ClientRequest'
34
import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest'
45
import { FetchInterceptor } from '@mswjs/interceptors/fetch'
@@ -47,16 +48,19 @@ class AsyncHandlersController implements HandlersController {
4748
return handlers.concat(initialHandlers)
4849
}
4950
}
50-
5151
export class SetupServerApi
5252
extends SetupServerCommonApi
5353
implements SetupServer
5454
{
55-
constructor(handlers: Array<RequestHandler | WebSocketHandler>) {
56-
super(
57-
[ClientRequestInterceptor, XMLHttpRequestInterceptor, FetchInterceptor],
58-
handlers,
59-
)
55+
constructor(
56+
handlers: Array<RequestHandler | WebSocketHandler>,
57+
interceptors: Array<Interceptor<HttpRequestEventMap>> = [
58+
new ClientRequestInterceptor(),
59+
new XMLHttpRequestInterceptor(),
60+
new FetchInterceptor(),
61+
],
62+
) {
63+
super(interceptors, handlers)
6064

6165
this.handlersController = new AsyncHandlersController(handlers)
6266
}

src/node/SetupServerCommonApi.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ export class SetupServerCommonApi
3737
private resolvedOptions: RequiredDeep<SharedOptions>
3838

3939
constructor(
40-
interceptors: Array<{ new (): Interceptor<HttpRequestEventMap> }>,
40+
interceptors: Array<Interceptor<HttpRequestEventMap>>,
4141
handlers: Array<RequestHandler | WebSocketHandler>,
4242
) {
4343
super(...handlers)
4444

4545
this.interceptor = new BatchInterceptor({
4646
name: 'setup-server',
47-
interceptors: interceptors.map((Interceptor) => new Interceptor()),
47+
interceptors,
4848
})
4949

5050
this.resolvedOptions = {} as RequiredDeep<SharedOptions>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// @vitest-environment node
2+
import nodeHttp from 'node:http'
3+
import { HttpResponse, http } from 'msw'
4+
import { SetupServerApi } from 'msw/node'
5+
import { FetchInterceptor } from '@mswjs/interceptors/fetch'
6+
import { waitForClientRequest } from '../../../../support/utils'
7+
8+
const server = new SetupServerApi(
9+
[
10+
http.get('http://localhost', () => {
11+
return HttpResponse.text('hello world')
12+
}),
13+
],
14+
[new FetchInterceptor()],
15+
)
16+
17+
beforeAll(() => {
18+
server.listen()
19+
})
20+
21+
afterAll(() => {
22+
server.close()
23+
})
24+
25+
test('uses only the provided interceptors', async () => {
26+
{
27+
const response = await fetch('http://localhost')
28+
29+
// Must receive a mocked response per the defined interceptor + handler.
30+
expect(response.status).toBe(200)
31+
await expect(response.text()).resolves.toBe('hello world')
32+
}
33+
34+
{
35+
const request = nodeHttp.get('http://localhost')
36+
const requestPromise = waitForClientRequest(request)
37+
38+
// Must receive a connection error since no intereceptor handles this client.
39+
await expect(requestPromise).rejects.toThrow('ECONNREFUSED')
40+
}
41+
})

test/support/utils.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,15 @@ export async function waitForClientRequest(request: ClientRequest): Promise<{
2424
responseText: string
2525
}> {
2626
return new Promise((resolve, reject) => {
27-
request.once('error', reject)
27+
request.once('error', (error) => {
28+
/**
29+
* @note Since Node.js v20, Node.js may throw an AggregateError
30+
* that doesn't have the `message` property and thus won't be handled
31+
* here correctly. Instead, use the error's `code` as the rejection reason.
32+
* The code stays consistent across Node.js versions.
33+
*/
34+
reject('code' in error ? error.code : error)
35+
})
2836
request.once('abort', () => reject(new Error('Request was aborted')))
2937

3038
request.on('response', (response) => {

0 commit comments

Comments
 (0)