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>
This commit is contained in:
108
packages/server/tests/ws-handler.test.ts
Normal file
108
packages/server/tests/ws-handler.test.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user