Files
esc/packages/server/tests/ws-handler.test.ts
Felix Förtsch ff311b5fac implement foundation + room system (Plan 1 of 5)
Bun workspace monorepo with shared, server, client packages.
Server: Hono + @hono/node-ws, Drizzle + PostgreSQL, in-memory room manager
with WebSocket broadcasting, HTTP room creation, DB persistence layer.
Client: React 19 + Vite + Tailwind v4 + shadcn/ui, TanStack Router with
landing/display/host/player routes, Zustand store, WebSocket connection hook.
20 tests passing (room manager unit + WS integration).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 12:52:32 +01:00

109 lines
3.3 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 },
)
})
}
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
const hostWs = new WebSocket(`ws://localhost:${port}/ws/${data.code}?sessionId=${data.sessionId}`)
await waitForOpen(hostWs)
await waitForMessage(hostWs) // room_state
// Connect player (no sessionId)
const playerWs = new WebSocket(`ws://localhost:${port}/ws/${data.code}`)
await waitForOpen(playerWs)
await waitForMessage(playerWs) // initial room_state
// Set up listeners BEFORE sending to avoid race conditions
const playerMsgPromise = waitForMessage(playerWs)
const hostMsgPromise = waitForMessage(hostWs)
// 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()
})
})