Files
esc/packages/server/tests/ws-handler.test.ts
T

123 lines
3.9 KiB
TypeScript

import { describe, expect, it, afterEach, beforeEach } from "vitest"
import { serve } from "@hono/node-server"
import { app, injectWebSocket } from "../src/app"
import { registerWebSocketRoutes } from "../src/ws/handler"
import { roomManager } from "../src/rooms/index"
// Register WS routes once
registerWebSocketRoutes()
let server: ReturnType<typeof serve>
function waitForMessage(ws: WebSocket): Promise<unknown> {
return new Promise((resolve) => {
ws.addEventListener(
"message",
(event) => {
resolve(JSON.parse(event.data as string))
},
{ once: true },
)
})
}
/** Consume messages until one with the given type arrives */
function waitForMessageType(ws: WebSocket, type: string): Promise<unknown> {
return new Promise((resolve) => {
function handler(event: MessageEvent) {
const msg = JSON.parse(event.data as string) as { type: string }
if (msg.type === type) {
ws.removeEventListener("message", handler)
resolve(msg)
}
}
ws.addEventListener("message", handler)
})
}
function waitForOpen(ws: WebSocket): Promise<void> {
return new Promise((resolve) => {
if (ws.readyState === WebSocket.OPEN) {
resolve()
} else {
ws.addEventListener("open", () => resolve(), { once: true })
}
})
}
describe("WebSocket handler", () => {
let port: number
beforeEach(async () => {
roomManager.reset()
port = 3100 + Math.floor(Math.random() * 900)
server = serve({ fetch: app.fetch, port })
injectWebSocket(server)
})
afterEach(() => {
server.close()
})
it("creates a room via HTTP and connects via WebSocket", async () => {
// Create room via HTTP
const res = await fetch(`http://localhost:${port}/rooms`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ displayName: "Host" }),
})
const { data } = (await res.json()) as { data: { code: string; sessionId: string } }
expect(data.code).toMatch(/^[A-Z0-9]{4}$/)
// Connect as host via WebSocket
const ws = new WebSocket(`ws://localhost:${port}/ws/${data.code}?sessionId=${data.sessionId}`)
await waitForOpen(ws)
const msg = (await waitForMessage(ws)) as { type: string; room: { code: string } }
expect(msg.type).toBe("room_state")
expect(msg.room.code).toBe(data.code)
ws.close()
})
it("player joins room via WebSocket", async () => {
// Create room
const res = await fetch(`http://localhost:${port}/rooms`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ displayName: "Host" }),
})
const { data } = (await res.json()) as { data: { code: string; sessionId: string } }
// Connect host — consumes room_state + game_state from onOpen
const hostWs = new WebSocket(`ws://localhost:${port}/ws/${data.code}?sessionId=${data.sessionId}`)
await waitForOpen(hostWs)
await waitForMessageType(hostWs, "game_state") // drain initial messages
// Connect player (no sessionId — passive until join_room)
const playerWs = new WebSocket(`ws://localhost:${port}/ws/${data.code}`)
await waitForOpen(playerWs)
await waitForMessageType(playerWs, "game_state") // drain initial messages
// Set up listeners BEFORE sending to avoid race conditions
const playerMsgPromise = waitForMessageType(playerWs, "room_state")
const hostMsgPromise = waitForMessageType(hostWs, "player_joined")
// Player sends join_room
playerWs.send(JSON.stringify({ type: "join_room", displayName: "Player 1" }))
// Player receives room_state with sessionId
const playerMsg = (await playerMsgPromise) as { type: string; sessionId?: string }
expect(playerMsg.type).toBe("room_state")
expect(playerMsg.sessionId).toBeDefined()
// Host receives player_joined broadcast
const hostMsg = (await hostMsgPromise) as { type: string; player: { displayName: string } }
expect(hostMsg.type).toBe("player_joined")
expect(hostMsg.player.displayName).toBe("Player 1")
hostWs.close()
playerWs.close()
})
})