Skip to content

Commit

Permalink
Merge branch 'main' into mikicho-patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
mikicho authored Oct 5, 2024
2 parents 84b5fd9 + 39e58a2 commit 53797c9
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 23 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@mswjs/interceptors",
"description": "Low-level HTTP/HTTPS/XHR/fetch request interception library.",
"version": "0.36.0",
"version": "0.36.3",
"main": "./lib/node/index.js",
"module": "./lib/node/index.mjs",
"types": "./lib/node/index.d.ts",
Expand Down
21 changes: 12 additions & 9 deletions src/interceptors/WebSocket/WebSocketClientConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,18 +86,21 @@ export class WebSocketClientConnection
listener: WebSocketEventListener<WebSocketClientEventMap[EventType]>,
options?: AddEventListenerOptions | boolean
): void {
const boundListener = listener.bind(this.socket)

// Store the bound listener on the original listener
// so the exact bound function can be accessed in "removeEventListener()".
Object.defineProperty(listener, kBoundListener, {
value: boundListener,
enumerable: false,
})
if (!Reflect.has(listener, kBoundListener)) {
const boundListener = listener.bind(this.socket)

// Store the bound listener on the original listener
// so the exact bound function can be accessed in "removeEventListener()".
Object.defineProperty(listener, kBoundListener, {
value: boundListener,
enumerable: false,
configurable: false,
})
}

this[kEmitter].addEventListener(
type,
boundListener as EventListener,
Reflect.get(listener, kBoundListener) as EventListener,
options
)
}
Expand Down
12 changes: 9 additions & 3 deletions src/interceptors/WebSocket/WebSocketOverride.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,22 @@ export class WebSocketOverride extends EventTarget implements WebSocket {
return
}

this.readyState = this.OPEN

this.protocol =
typeof protocols === 'string'
? protocols
: Array.isArray(protocols) && protocols.length > 0
? protocols[0]
: ''

this.dispatchEvent(bindEvent(this, new Event('open')))
/**
* @note Check that nothing has prevented this connection
* (e.g. called `client.close()` in the connection listener).
* If the connection has been prevented, never dispatch the open event,.
*/
if (this.readyState === this.CONNECTING) {
this.readyState = this.OPEN
this.dispatchEvent(bindEvent(this, new Event('open')))
}
})
}

Expand Down
20 changes: 11 additions & 9 deletions src/interceptors/WebSocket/WebSocketServerConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,18 +169,20 @@ export class WebSocketServerConnection {
listener: WebSocketEventListener<WebSocketServerEventMap[EventType]>,
options?: AddEventListenerOptions | boolean
): void {
const boundListener = listener.bind(this.client)

// Store the bound listener on the original listener
// so the exact bound function can be accessed in "removeEventListener()".
Object.defineProperty(listener, kBoundListener, {
value: boundListener,
enumerable: false,
})
if (!Reflect.has(listener, kBoundListener)) {
const boundListener = listener.bind(this.client)

// Store the bound listener on the original listener
// so the exact bound function can be accessed in "removeEventListener()".
Object.defineProperty(listener, kBoundListener, {
value: boundListener,
enumerable: false,
})
}

this[kEmitter].addEventListener(
event,
boundListener as EventListener,
Reflect.get(listener, kBoundListener) as EventListener,
options
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// @vitest-environment node-with-websocket
import { vi, it, expect, beforeAll, afterEach, afterAll } from 'vitest'
import { WebSocketServer } from 'ws'
import { WebSocketInterceptor } from '../../../../src/interceptors/WebSocket'
import { waitForWebSocketEvent } from '../utils/waitForWebSocketEvent'
import { getWsUrl } from '../utils/getWsUrl'

const wsServer = new WebSocketServer({
host: '127.0.0.1',
port: 0,
})

const interceptor = new WebSocketInterceptor()

beforeAll(async () => {
interceptor.apply()
})

afterEach(() => {
interceptor.removeAllListeners()
wsServer.clients.forEach((client) => client.close())
})

afterAll(async () => {
interceptor.dispose()
wsServer.close()
})

it('allows reusing the same function for multiple client listeners', async () => {
const clientMessageListener = vi.fn()

interceptor.on('connection', ({ client }) => {
client.addEventListener('message', clientMessageListener)
client.addEventListener('message', clientMessageListener)
client.addEventListener('message', clientMessageListener)

/**
* @note Use `process.nextTick()` because `queueMicrotask()` has a
* higher priority. We need the connection to open, handle messages,
* and then close.
*/
process.nextTick(() => {
client.close()
})
})

const socket = new WebSocket('wss://example.com')
socket.onopen = () => socket.send('hello world')

await waitForWebSocketEvent('close', socket)

/**
* @note The same event listner for the same event is deduped.
* It will only be called once. That is correct.
*/
expect(clientMessageListener).toHaveBeenCalledTimes(1)
})

it('allows reusing the same function for multiple server listeners', async () => {
wsServer.once('connection', (ws) => {
ws.send('hello from server')
queueMicrotask(() => ws.close())
})

const serverMessageListener = vi.fn()

interceptor.on('connection', ({ server }) => {
server.connect()
server.addEventListener('message', serverMessageListener)
server.addEventListener('message', serverMessageListener)
server.addEventListener('message', serverMessageListener)
})

const socket = new WebSocket(getWsUrl(wsServer))

await waitForWebSocketEvent('close', socket)

/**
* @note The same event listner for the same event is deduped.
* It will only be called once. That is correct.
*/
expect(serverMessageListener).toHaveBeenCalledTimes(1)
})
80 changes: 80 additions & 0 deletions test/modules/WebSocket/exchange/websocket.readystate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// @vitest-environment node-with-websocket
import { vi, it, expect, beforeAll, afterEach, afterAll } from 'vitest'
import { WebSocketInterceptor } from '../../../../src/interceptors/WebSocket'
import { waitForWebSocketEvent } from '../utils/waitForWebSocketEvent'
import { waitForNextTick } from '../utils/waitForNextTick'

const interceptor = new WebSocketInterceptor()

beforeAll(() => {
interceptor.apply()
})

afterEach(() => {
interceptor.removeAllListeners()
})

afterAll(() => {
interceptor.dispose()
})

it('dispatches the connection even when the client "readyState" is OPEN', async () => {
const readyStateListener = vi.fn<[number]>()

interceptor.on('connection', ({ client }) => {
// CONNECTING.
readyStateListener(client.socket.readyState)

/**
* @note Use `process.nextTick()` because it has lower priority
* than `queueMicrotask()`. If you queue a microtask here, it will
* run BEFORE a queued microtask that dispatches the open event.
*/
process.nextTick(() => {
readyStateListener(client.socket.readyState)
client.close()
})
})

const socket = new WebSocket('wss://localhost')
await waitForWebSocketEvent('close', socket)

// The client ready state must be OPEN when the connection listener is called.
expect(readyStateListener).toHaveBeenNthCalledWith(1, WebSocket.CONNECTING)
expect(readyStateListener).toHaveBeenNthCalledWith(2, WebSocket.OPEN)
expect(readyStateListener).toHaveBeenCalledTimes(2)
})

it('updates "readyState" correctly when closing the connection in the interceptor', async () => {
const readyStateListener = vi.fn<[number]>()

interceptor.on('connection', ({ client }) => {
// CONNECTING.
readyStateListener(client.socket.readyState)

client.close()

// CLOSING.
readyStateListener(client.socket.readyState)

process.nextTick(() => {
// CLOSED.
readyStateListener(client.socket.readyState)
})
})

const openEventListener = vi.fn()
const socket = new WebSocket('wss://localhost')
socket.onopen = openEventListener
await waitForWebSocketEvent('close', socket)

expect(readyStateListener).toHaveBeenNthCalledWith(1, WebSocket.CONNECTING)
// Must set ready state to CLOSING in the same frame as "client.close()".
expect(readyStateListener).toHaveBeenNthCalledWith(2, WebSocket.CLOSING)

await waitForNextTick()
// Must set ready state to CLOSED in the next frame.
expect(readyStateListener).toHaveBeenNthCalledWith(3, WebSocket.CLOSED)

expect(openEventListener).not.toHaveBeenCalled()
})
2 changes: 1 addition & 1 deletion test/modules/WebSocket/utils/waitForNextTick.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export async function waitForNextTick(): Promise<void> {
return new Promise((resolve) => {
queueMicrotask(() => resolve())
process.nextTick(() => resolve())
})
}

0 comments on commit 53797c9

Please sign in to comment.