Skip to content

Commit

Permalink
chore: move spyOnLifeCycleEvents to node utils
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito committed Jan 9, 2025
1 parent 61c8fe7 commit bc0e0a7
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 180 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// @vitest-environment node
import { http, HttpResponse, SetupApi, LifeCycleEventsMap } from 'msw'
import { http, HttpResponse } from 'msw'
import { setupRemoteServer } from 'msw/node'
import { DeferredPromise } from '@open-draft/deferred-promise'
import { HttpServer } from '@open-draft/test-server/http'
import { spyOnLifeCycleEvents } from '../../utils'
import { spawnTestApp } from './utils'

const remote = setupRemoteServer()
Expand All @@ -13,63 +13,6 @@ const httpServer = new HttpServer((app) => {
})
})

function spyOnLifeCycleEvents(setupApi: SetupApi<LifeCycleEventsMap>) {
const listener = vi.fn()
const requestIdPromise = new DeferredPromise<string>()

setupApi.events
.on('request:start', ({ request, requestId }) => {
if (request.headers.has('upgrade')) {
return
}

requestIdPromise.resolve(requestId)
listener(`[request:start] ${request.method} ${request.url} ${requestId}`)
})
.on('request:match', ({ request, requestId }) => {
listener(`[request:match] ${request.method} ${request.url} ${requestId}`)
})
.on('request:unhandled', ({ request, requestId }) => {
listener(
`[request:unhandled] ${request.method} ${request.url} ${requestId}`,
)
})
.on('request:end', ({ request, requestId }) => {
if (request.headers.has('upgrade')) {
return
}

listener(`[request:end] ${request.method} ${request.url} ${requestId}`)
})

setupApi.events
.on('response:mocked', async ({ response, request, requestId }) => {
listener(
`[response:mocked] ${request.method} ${request.url} ${requestId} ${
response.status
} ${await response.clone().text()}`,
)
})
.on('response:bypass', async ({ response, request, requestId }) => {
if (request.headers.has('upgrade')) {
return
}

listener(
`[response:bypass] ${request.method} ${request.url} ${requestId} ${
response.status
} ${await response.clone().text()}`,
)
})

return {
listener,
waitForRequestId() {
return requestIdPromise
},
}
}

beforeAll(async () => {
await remote.listen()
await httpServer.listen()
Expand All @@ -90,15 +33,15 @@ it(
)

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

const response = await fetch(new URL('/resource', testApp.url))
const requestId = await waitForRequestId()
const requestId = await requestIdPromise

// Must respond with the mocked response defined in the test.
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(await response.json()).toEqual({ mocked: true })
await expect(response.json()).resolves.toEqual({ mocked: true })

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

const response = await fetch(new URL('/resource', testApp.url))
const requestId = await waitForRequestId()
const requestId = await requestIdPromise

expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(await response.json()).toEqual([1, 2, 3])
await expect(response.json()).resolves.toEqual([1, 2, 3])

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

const resourceUrl = httpServer.http.url('/greeting')
// Request a special route in the running app that performs a proxy request
// to the resource specified in the "Location" request header.
const response = await fetch(new URL('/proxy', testApp.url), {
headers: {
Location: resourceUrl,
location: resourceUrl,
},
})
const requestId = await waitForRequestId()
const requestId = await requestIdPromise

expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
Expand Down
89 changes: 1 addition & 88 deletions test/node/msw-api/setup-remote-server/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { invariant } from 'outvariant'
import { ChildProcess, spawn } from 'child_process'
import { spawn } from 'child_process'
import { DeferredPromise } from '@open-draft/deferred-promise'
import { type ListenOptions, getRemoteEnvironment } from 'msw/node'

Expand Down Expand Up @@ -83,90 +83,3 @@ export async function spawnTestApp(
},
}
}

//
//
//

export class TestNodeApp {
private io: ChildProcess = null as any
private _url: URL | null = null

constructor(
private readonly appSourcePath: string,
private readonly options?: { contextId: string },
) {}

get url() {
invariant(
this._url,
'Failed to return the URL for the test Node app: the app is not running. Did you forget to call ".spawn()"?',
)

return this._url
}

public async start(): Promise<URL | undefined> {
const spawnPromise = new DeferredPromise<URL>()

this.io = spawn('node', [this.appSourcePath], {
// Establish an IPC between the test and the test app.
// This IPC is not required for the remote interception to work.
// This IPC is required for the test app to be spawned at a random port
// and be able to communicate the port back to the test.
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
env: {
...process.env,
MSW_REMOTE_CONTEXT_ID: this.options?.contextId,
},
})

this.io.stdout?.on('data', (c) => console.log(c.toString()))
this.io.stderr?.on('data', (c) => console.error(c.toString()))

this.io
.on('message', (message) => {
try {
const url = new URL(message.toString())
spawnPromise.resolve(url)
} catch (error) {
return
}
})
.on('error', (error) => spawnPromise.reject(error))
.on('exit', (code) => {
if (code !== 0) {
spawnPromise.reject(
new Error(`Failed to spawn a test Node app (exit code: ${code})`),
)
}
})

spawnPromise.then((url) => {
this._url = url
})

return Promise.race([
spawnPromise,
new Promise<undefined>((_, reject) => {
setTimeout(() => {
reject(new Error('Failed to spawn a test Node app within timeout'))
}, 5_000)
}),
])
}

public async close() {
const closePromise = new DeferredPromise<void>()

this.io.send('SIGTERM', (error) => {
if (error) {
closePromise.reject(error)
} else {
closePromise.resolve()
}
})

return closePromise
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @vitest-environment node
import { setupServer } from 'msw/node'
import { spyOnLifeCycleEvents } from '../../../../support/utils'
import { spyOnLifeCycleEvents } from '../../../utils'

const server = setupServer()

Expand All @@ -27,7 +27,7 @@ afterAll(() => {
})

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

// Must emit no life-cycle events for internal requests.
expect(listener).not.toHaveBeenCalled()
Expand Down
58 changes: 58 additions & 0 deletions test/node/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { DeferredPromise } from '@open-draft/deferred-promise'
import { vi, afterEach } from 'vitest'
import { LifeCycleEventsMap, SetupApi } from 'msw'

export function spyOnLifeCycleEvents(api: SetupApi<LifeCycleEventsMap>) {
const listener = vi.fn()
const requestIdPromise = new DeferredPromise<string>()

afterEach(() => listener.mockReset())

api.events
.on('request:start', ({ request, requestId }) => {
if (request.headers.has('upgrade')) {
return
}

requestIdPromise.resolve(requestId)
listener(`[request:start] ${request.method} ${request.url} ${requestId}`)
})
.on('request:match', ({ request, requestId }) => {
listener(`[request:match] ${request.method} ${request.url} ${requestId}`)
})
.on('request:unhandled', ({ request, requestId }) => {
listener(
`[request:unhandled] ${request.method} ${request.url} ${requestId}`,
)
})
.on('request:end', ({ request, requestId }) => {
if (request.headers.has('upgrade')) {
return
}

listener(`[request:end] ${request.method} ${request.url} ${requestId}`)
})
.on('response:mocked', async ({ response, request, requestId }) => {
listener(
`[response:mocked] ${request.method} ${request.url} ${requestId} ${
response.status
} ${await response.clone().text()}`,
)
})
.on('response:bypass', async ({ response, request, requestId }) => {
if (request.headers.has('upgrade')) {
return
}

listener(
`[response:bypass] ${request.method} ${request.url} ${requestId} ${
response.status
} ${await response.clone().text()}`,
)
})

return {
listener,
requestIdPromise,
}
}
22 changes: 0 additions & 22 deletions test/support/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import * as path from 'node:path'
import { ClientRequest, IncomingMessage } from 'node:http'
import { vi, afterEach } from 'vitest'
import { LifeCycleEventsMap, SetupApi } from 'msw'

export function sleep(duration: number) {
return new Promise((resolve) => {
Expand Down Expand Up @@ -46,23 +44,3 @@ export async function waitForClientRequest(request: ClientRequest): Promise<{
})
})
}

export function spyOnLifeCycleEvents(api: SetupApi<LifeCycleEventsMap>) {
const listener = vi.fn()
afterEach(() => listener.mockReset())

const wrapListener = (eventName: string) => {
return (...args: Array<any>) => listener(eventName, ...args)
}

api.events
.on('request:start', wrapListener('request:start'))
.on('request:match', wrapListener('request:match'))
.on('request:unhandled', wrapListener('request:unhandled'))
.on('request:end', wrapListener('request:end'))
.on('response:mocked', wrapListener('response:mocked'))
.on('response:bypass', wrapListener('response:bypass'))
.on('unhandledException', wrapListener('unhandledException'))

return listener
}

0 comments on commit bc0e0a7

Please sign in to comment.