Skip to content

Commit bc0e0a7

Browse files
committed
chore: move spyOnLifeCycleEvents to node utils
1 parent 61c8fe7 commit bc0e0a7

File tree

5 files changed

+72
-180
lines changed

5 files changed

+72
-180
lines changed

test/node/msw-api/setup-remote-server/life-cycle-event-forwarding.node.test.ts

+11-68
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// @vitest-environment node
2-
import { http, HttpResponse, SetupApi, LifeCycleEventsMap } from 'msw'
2+
import { http, HttpResponse } from 'msw'
33
import { setupRemoteServer } from 'msw/node'
4-
import { DeferredPromise } from '@open-draft/deferred-promise'
54
import { HttpServer } from '@open-draft/test-server/http'
5+
import { spyOnLifeCycleEvents } from '../../utils'
66
import { spawnTestApp } from './utils'
77

88
const remote = setupRemoteServer()
@@ -13,63 +13,6 @@ const httpServer = new HttpServer((app) => {
1313
})
1414
})
1515

16-
function spyOnLifeCycleEvents(setupApi: SetupApi<LifeCycleEventsMap>) {
17-
const listener = vi.fn()
18-
const requestIdPromise = new DeferredPromise<string>()
19-
20-
setupApi.events
21-
.on('request:start', ({ request, requestId }) => {
22-
if (request.headers.has('upgrade')) {
23-
return
24-
}
25-
26-
requestIdPromise.resolve(requestId)
27-
listener(`[request:start] ${request.method} ${request.url} ${requestId}`)
28-
})
29-
.on('request:match', ({ request, requestId }) => {
30-
listener(`[request:match] ${request.method} ${request.url} ${requestId}`)
31-
})
32-
.on('request:unhandled', ({ request, requestId }) => {
33-
listener(
34-
`[request:unhandled] ${request.method} ${request.url} ${requestId}`,
35-
)
36-
})
37-
.on('request:end', ({ request, requestId }) => {
38-
if (request.headers.has('upgrade')) {
39-
return
40-
}
41-
42-
listener(`[request:end] ${request.method} ${request.url} ${requestId}`)
43-
})
44-
45-
setupApi.events
46-
.on('response:mocked', async ({ response, request, requestId }) => {
47-
listener(
48-
`[response:mocked] ${request.method} ${request.url} ${requestId} ${
49-
response.status
50-
} ${await response.clone().text()}`,
51-
)
52-
})
53-
.on('response:bypass', async ({ response, request, requestId }) => {
54-
if (request.headers.has('upgrade')) {
55-
return
56-
}
57-
58-
listener(
59-
`[response:bypass] ${request.method} ${request.url} ${requestId} ${
60-
response.status
61-
} ${await response.clone().text()}`,
62-
)
63-
})
64-
65-
return {
66-
listener,
67-
waitForRequestId() {
68-
return requestIdPromise
69-
},
70-
}
71-
}
72-
7316
beforeAll(async () => {
7417
await remote.listen()
7518
await httpServer.listen()
@@ -90,15 +33,15 @@ it(
9033
)
9134

9235
await using testApp = await spawnTestApp(require.resolve('./use.app.js'))
93-
const { listener, waitForRequestId } = spyOnLifeCycleEvents(remote)
36+
const { listener, requestIdPromise } = spyOnLifeCycleEvents(remote)
9437

9538
const response = await fetch(new URL('/resource', testApp.url))
96-
const requestId = await waitForRequestId()
39+
const requestId = await requestIdPromise
9740

9841
// Must respond with the mocked response defined in the test.
9942
expect(response.status).toBe(200)
10043
expect(response.statusText).toBe('OK')
101-
expect(await response.json()).toEqual({ mocked: true })
44+
await expect(response.json()).resolves.toEqual({ mocked: true })
10245

10346
// Must forward the life-cycle events to the test process.
10447
await vi.waitFor(() => {
@@ -118,14 +61,14 @@ it(
11861
'emits correct events for the request handled in the remote process',
11962
remote.boundary(async () => {
12063
await using testApp = await spawnTestApp(require.resolve('./use.app.js'))
121-
const { listener, waitForRequestId } = spyOnLifeCycleEvents(remote)
64+
const { listener, requestIdPromise } = spyOnLifeCycleEvents(remote)
12265

12366
const response = await fetch(new URL('/resource', testApp.url))
124-
const requestId = await waitForRequestId()
67+
const requestId = await requestIdPromise
12568

12669
expect(response.status).toBe(200)
12770
expect(response.statusText).toBe('OK')
128-
expect(await response.json()).toEqual([1, 2, 3])
71+
await expect(response.json()).resolves.toEqual([1, 2, 3])
12972

13073
await vi.waitFor(() => {
13174
expect(listener.mock.calls).toEqual([
@@ -144,17 +87,17 @@ it(
14487
'emits correct events for the request unhandled by either parties',
14588
remote.boundary(async () => {
14689
await using testApp = await spawnTestApp(require.resolve('./use.app.js'))
147-
const { listener, waitForRequestId } = spyOnLifeCycleEvents(remote)
90+
const { listener, requestIdPromise } = spyOnLifeCycleEvents(remote)
14891

14992
const resourceUrl = httpServer.http.url('/greeting')
15093
// Request a special route in the running app that performs a proxy request
15194
// to the resource specified in the "Location" request header.
15295
const response = await fetch(new URL('/proxy', testApp.url), {
15396
headers: {
154-
Location: resourceUrl,
97+
location: resourceUrl,
15598
},
15699
})
157-
const requestId = await waitForRequestId()
100+
const requestId = await requestIdPromise
158101

159102
expect(response.status).toBe(200)
160103
expect(response.statusText).toBe('OK')
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { invariant } from 'outvariant'
2-
import { ChildProcess, spawn } from 'child_process'
2+
import { spawn } from 'child_process'
33
import { DeferredPromise } from '@open-draft/deferred-promise'
44
import { type ListenOptions, getRemoteEnvironment } from 'msw/node'
55

@@ -83,90 +83,3 @@ export async function spawnTestApp(
8383
},
8484
}
8585
}
86-
87-
//
88-
//
89-
//
90-
91-
export class TestNodeApp {
92-
private io: ChildProcess = null as any
93-
private _url: URL | null = null
94-
95-
constructor(
96-
private readonly appSourcePath: string,
97-
private readonly options?: { contextId: string },
98-
) {}
99-
100-
get url() {
101-
invariant(
102-
this._url,
103-
'Failed to return the URL for the test Node app: the app is not running. Did you forget to call ".spawn()"?',
104-
)
105-
106-
return this._url
107-
}
108-
109-
public async start(): Promise<URL | undefined> {
110-
const spawnPromise = new DeferredPromise<URL>()
111-
112-
this.io = spawn('node', [this.appSourcePath], {
113-
// Establish an IPC between the test and the test app.
114-
// This IPC is not required for the remote interception to work.
115-
// This IPC is required for the test app to be spawned at a random port
116-
// and be able to communicate the port back to the test.
117-
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
118-
env: {
119-
...process.env,
120-
MSW_REMOTE_CONTEXT_ID: this.options?.contextId,
121-
},
122-
})
123-
124-
this.io.stdout?.on('data', (c) => console.log(c.toString()))
125-
this.io.stderr?.on('data', (c) => console.error(c.toString()))
126-
127-
this.io
128-
.on('message', (message) => {
129-
try {
130-
const url = new URL(message.toString())
131-
spawnPromise.resolve(url)
132-
} catch (error) {
133-
return
134-
}
135-
})
136-
.on('error', (error) => spawnPromise.reject(error))
137-
.on('exit', (code) => {
138-
if (code !== 0) {
139-
spawnPromise.reject(
140-
new Error(`Failed to spawn a test Node app (exit code: ${code})`),
141-
)
142-
}
143-
})
144-
145-
spawnPromise.then((url) => {
146-
this._url = url
147-
})
148-
149-
return Promise.race([
150-
spawnPromise,
151-
new Promise<undefined>((_, reject) => {
152-
setTimeout(() => {
153-
reject(new Error('Failed to spawn a test Node app within timeout'))
154-
}, 5_000)
155-
}),
156-
])
157-
}
158-
159-
public async close() {
160-
const closePromise = new DeferredPromise<void>()
161-
162-
this.io.send('SIGTERM', (error) => {
163-
if (error) {
164-
closePromise.reject(error)
165-
} else {
166-
closePromise.resolve()
167-
}
168-
})
169-
170-
return closePromise
171-
}
172-
}

test/node/msw-api/setup-server/life-cycle-events/ignore-internal-requests.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @vitest-environment node
22
import { setupServer } from 'msw/node'
3-
import { spyOnLifeCycleEvents } from '../../../../support/utils'
3+
import { spyOnLifeCycleEvents } from '../../../utils'
44

55
const server = setupServer()
66

@@ -27,7 +27,7 @@ afterAll(() => {
2727
})
2828

2929
it('does not emit life-cycle events for internal requests', async () => {
30-
const listener = spyOnLifeCycleEvents(server)
30+
const { listener } = spyOnLifeCycleEvents(server)
3131

3232
// Must emit no life-cycle events for internal requests.
3333
expect(listener).not.toHaveBeenCalled()

test/node/utils.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { DeferredPromise } from '@open-draft/deferred-promise'
2+
import { vi, afterEach } from 'vitest'
3+
import { LifeCycleEventsMap, SetupApi } from 'msw'
4+
5+
export function spyOnLifeCycleEvents(api: SetupApi<LifeCycleEventsMap>) {
6+
const listener = vi.fn()
7+
const requestIdPromise = new DeferredPromise<string>()
8+
9+
afterEach(() => listener.mockReset())
10+
11+
api.events
12+
.on('request:start', ({ request, requestId }) => {
13+
if (request.headers.has('upgrade')) {
14+
return
15+
}
16+
17+
requestIdPromise.resolve(requestId)
18+
listener(`[request:start] ${request.method} ${request.url} ${requestId}`)
19+
})
20+
.on('request:match', ({ request, requestId }) => {
21+
listener(`[request:match] ${request.method} ${request.url} ${requestId}`)
22+
})
23+
.on('request:unhandled', ({ request, requestId }) => {
24+
listener(
25+
`[request:unhandled] ${request.method} ${request.url} ${requestId}`,
26+
)
27+
})
28+
.on('request:end', ({ request, requestId }) => {
29+
if (request.headers.has('upgrade')) {
30+
return
31+
}
32+
33+
listener(`[request:end] ${request.method} ${request.url} ${requestId}`)
34+
})
35+
.on('response:mocked', async ({ response, request, requestId }) => {
36+
listener(
37+
`[response:mocked] ${request.method} ${request.url} ${requestId} ${
38+
response.status
39+
} ${await response.clone().text()}`,
40+
)
41+
})
42+
.on('response:bypass', async ({ response, request, requestId }) => {
43+
if (request.headers.has('upgrade')) {
44+
return
45+
}
46+
47+
listener(
48+
`[response:bypass] ${request.method} ${request.url} ${requestId} ${
49+
response.status
50+
} ${await response.clone().text()}`,
51+
)
52+
})
53+
54+
return {
55+
listener,
56+
requestIdPromise,
57+
}
58+
}

test/support/utils.ts

-22
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import * as path from 'node:path'
22
import { ClientRequest, IncomingMessage } from 'node:http'
3-
import { vi, afterEach } from 'vitest'
4-
import { LifeCycleEventsMap, SetupApi } from 'msw'
53

64
export function sleep(duration: number) {
75
return new Promise((resolve) => {
@@ -46,23 +44,3 @@ export async function waitForClientRequest(request: ClientRequest): Promise<{
4644
})
4745
})
4846
}
49-
50-
export function spyOnLifeCycleEvents(api: SetupApi<LifeCycleEventsMap>) {
51-
const listener = vi.fn()
52-
afterEach(() => listener.mockReset())
53-
54-
const wrapListener = (eventName: string) => {
55-
return (...args: Array<any>) => listener(eventName, ...args)
56-
}
57-
58-
api.events
59-
.on('request:start', wrapListener('request:start'))
60-
.on('request:match', wrapListener('request:match'))
61-
.on('request:unhandled', wrapListener('request:unhandled'))
62-
.on('request:end', wrapListener('request:end'))
63-
.on('response:mocked', wrapListener('response:mocked'))
64-
.on('response:bypass', wrapListener('response:bypass'))
65-
.on('unhandledException', wrapListener('unhandledException'))
66-
67-
return listener
68-
}

0 commit comments

Comments
 (0)