import { serve } from '@hono/node-server' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import type { ServerType } from '@hono/node-server/dist/types' import { Hono } from 'hono' import { cors } from 'hono/cors' import type { WSMessageReceive } from 'hono/ws' import { WebSocket } from 'ws' import { createNodeWebSocket } from '.' describe('WebSocket helper', () => { let app: Hono let server: ServerType let injectWebSocket: ReturnType['injectWebSocket'] let upgradeWebSocket: ReturnType['upgradeWebSocket'] let wss: ReturnType['wss'] beforeEach(async () => { app = new Hono() ;({ injectWebSocket, upgradeWebSocket, wss } = createNodeWebSocket({ app })) server = await new Promise((resolve) => { const server = serve({ fetch: app.fetch, port: 3030 }, () => { resolve(server) }) }) injectWebSocket(server) }) afterEach(() => { server.close() }) it('Should be inited WebSocket Context even if upgrading process is asynchronous', async () => { const mainPromise = new Promise((resolve) => app.get( '/', upgradeWebSocket( () => new Promise((resolveWS) => setTimeout(() => { resolveWS({ onOpen() { resolve(true) }, }) }, 100) ) ) ) ) new WebSocket('ws://localhost:3030/') expect(await mainPromise).toBe(true) }) it('Should be rejected if upgradeWebSocket is not used', async () => { app.get('/', (c) => c.body('')) { const ws = new WebSocket('ws://localhost:3030/') const mainPromise = new Promise((resolve) => { ws.onerror = () => { resolve(true) } ws.onopen = () => { resolve(false) } }) expect(await mainPromise).toBe(true) } { //also should rejected on fallback const ws = new WebSocket('ws://localhost:3030/notFound') const mainPromise = new Promise((resolve) => { ws.onerror = () => { resolve(true) } ws.onopen = () => { resolve(false) } }) expect(await mainPromise).toBe(true) } }) it('Should be able to connect', async () => { const mainPromise = new Promise((resolve) => app.get( '/', upgradeWebSocket(() => ({ onOpen() { resolve(true) }, })) ) ) new WebSocket('ws://localhost:3030/') expect(await mainPromise).toBe(true) }) it('Should be able to send and receive messages', async () => { const mainPromise = new Promise((resolve) => app.get( '/', upgradeWebSocket(() => ({ onMessage(data) { resolve(data.data) }, })) ) ) const ws = new WebSocket('ws://localhost:3030/') await new Promise((resolve) => ws.on('open', resolve)) ws.send('Hello') expect(await mainPromise).toBe('Hello') }) it('Should handle multiple concurrent connections', async () => { const connectionCount = 5 let openConnections = 0 const messages: string[] = [] app.get( '/', upgradeWebSocket(() => ({ onOpen() { openConnections++ }, onMessage(data, ws) { messages.push(data.data as string) ws.send(data.data as string) }, })) ) const connections = await Promise.all( Array(connectionCount) .fill(null) .map(async () => { const ws = new WebSocket('ws://localhost:3030/') await new Promise((resolve) => ws.on('open', resolve)) return ws }) ) expect(openConnections).toBe(connectionCount) await Promise.all( connections.map((ws, index) => { return new Promise((resolve) => { ws.send(`Hello from connection ${index + 1}`) ws.on('message', () => { resolve() }) }) }) ) expect(messages.length).toBe(connectionCount) messages.forEach((msg, index) => { expect(msg).toBe(`Hello from connection ${index + 1}`) }) connections.forEach((ws) => { ws.close() }) }) it('CloseEvent should be executed without crash', async () => { const testCode = 3001 const testReason = 'Test!' app.get( '/', upgradeWebSocket(() => ({ onClose(event) { expect(event.code).toBe(testCode) expect(event.reason).toBe(testReason) }, })) ) const ws = new WebSocket('ws://localhost:3030/') await new Promise((resolve) => ws.on('open', resolve)) ws.close(testCode, testReason) }) it('Should be able to send and receive binary content with good length', async () => { const mainPromise = new Promise((resolve) => app.get( '/', upgradeWebSocket(() => ({ onMessage(data) { resolve(data.data) }, })) ) ) const binaryData = new Uint8Array(Array.from({ length: 16 }).map((_, i) => i ** 2)) const ws = new WebSocket('ws://localhost:3030/') await new Promise((resolve) => ws.on('open', resolve)) ws.send(binaryData) const receivedMessage = await mainPromise expect(receivedMessage).toBeInstanceOf(ArrayBuffer) expect((receivedMessage as ArrayBuffer).byteLength).toBe(binaryData.length) binaryData.forEach((val, idx) => { expect(new Uint8Array(receivedMessage as ArrayBuffer)[idx]).toBe(val) }) }) describe('Types', () => { it('Should not throw a type error with an app with Variables generics', () => { const app = new Hono<{ Variables: { foo: string } }>() createNodeWebSocket({ app }) }) }) it('Should client can connect when use cors()', async () => { app.use(cors()) const mainPromise = new Promise((resolve) => app.get( '/', upgradeWebSocket(() => ({ onOpen() { resolve(true) }, })) ) ) new WebSocket('ws://localhost:3030/') expect(await mainPromise).toBe(true) }) it('Should client can connect even if a response has difference', async () => { app.use(async (c, next) => { c.res = new Response(null, c.res) await next() }) const mainPromise = new Promise((resolve) => app.get( '/', upgradeWebSocket(() => ({ onOpen() { resolve(true) }, })) ) ) new WebSocket('ws://localhost:3030/') expect(await mainPromise).toBe(true) }) it('Should not async processes to create events affect message handling', async () => { const mainPromise = new Promise((resolve) => app.get( '/', upgradeWebSocket(async () => { await new Promise((resolve) => setTimeout(resolve, 100)) return { onMessage() { resolve(true) }, } }) ) ) const ws = new WebSocket('ws://localhost:3030/') ws.onopen = () => { ws.send('Hello') } expect(await mainPromise).toBe(true) }) it('Should return the wss used for the websocket helper', async () => { let clientWs: WebSocket | null = null const mainPromise = new Promise((resolve) => wss.on('connection', (ws) => { clientWs = ws resolve() }) ) app.get( '/', upgradeWebSocket(() => ({})) ) new WebSocket('ws://localhost:3030/') await mainPromise expect(clientWs).toBeTruthy() expect(wss.clients.size).toBe(1) }) })