123 lines
3.9 KiB
TypeScript
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()
|
|
})
|
|
})
|