import { describe, it, expect, beforeEach, mock } from 'bun:test'; import { WebSocketHandler } from './WebSocketHandler'; import { GameManager } from './GameManager'; import { GameInstance } from './GameInstance'; // Mock ElysiaWS type for testing purposes - fully compatible with standard WebSocket type MockElysiaWS = { send: ReturnType; close: ReturnType; on: ReturnType; _messageCallback: ((message: string) => void) | null; _closeCallback: (() => void) | null; _errorCallback: ((error: Error) => void) | null; data: { gameId?: string; playerId?: string; query: Record; }; // Standard WebSocket properties binaryType: 'blob' | 'arraybuffer'; bufferedAmount: number; extensions: string; onclose: ((this: WebSocket, ev: CloseEvent) => any) | null; onerror: ((this: WebSocket, ev: Event) => any) | null; onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null; onopen: ((this: WebSocket, ev: Event) => any) | null; protocol: string; readyState: number; url: string; CLOSED: number; CONNECTING: number; OPEN: number; CLOSING: number; dispatchEvent: ReturnType; addEventListener: ReturnType; removeEventListener: ReturnType; ping: ReturnType; // Bun.js specific pong: ReturnType; // Bun.js specific subscribe: ReturnType; // Bun.js specific unsubscribe: ReturnType; // Bun.js specific }; describe('WebSocketHandler', () => { let gameManager: GameManager; let webSocketHandler: WebSocketHandler; let mockWs: MockElysiaWS; beforeEach(() => { mockWs = { // Mock standard WebSocket methods send: mock(() => {}), close: mock(() => {}), // Mock custom 'on' method for attaching callbacks on: mock((event: string, callback: (...args: any[]) => void) => { if (event === 'message') (mockWs as any)._messageCallback = callback; if (event === 'close') (mockWs as any)._closeCallback = callback; if (event === 'error') (mockWs as any)._errorCallback = callback; }), _messageCallback: null, _closeCallback: null, _errorCallback: null, data: { query: {} }, // Initialize all standard WebSocket properties binaryType: 'blob', bufferedAmount: 0, extensions: '', onclose: null, onerror: null, onmessage: null, onopen: null, protocol: '', readyState: 1, url: '', CLOSED: 3, CONNECTING: 0, OPEN: 1, CLOSING: 2, dispatchEvent: mock(() => {}), addEventListener: mock(() => {}), removeEventListener: mock(() => {}), ping: mock(() => {}), pong: mock(() => {}), subscribe: mock(() => {}), unsubscribe: mock(() => {}), }; gameManager = new GameManager(); webSocketHandler = new WebSocketHandler(gameManager); }); const triggerMessage = (ws: MockElysiaWS, message: string) => { if (ws._messageCallback) { ws._messageCallback(message); } }; const triggerClose = (ws: MockElysiaWS) => { if (ws._closeCallback) { ws._closeCallback(); } }; const triggerError = (ws: MockElysiaWS, error: Error) => { if (ws._errorCallback) { ws._errorCallback(error); } }; const createNewMockWs = (): MockElysiaWS => ({ send: mock(() => {}), close: mock(() => {}), on: mock((event: string, callback: (...args: any[]) => void) => { if (event === 'message') (createNewMockWs() as any)._messageCallback = callback; if (event === 'close') (createNewMockWs() as any)._closeCallback = callback; if (event === 'error') (createNewMockWs() as any)._errorCallback = callback; }), _messageCallback: null, _closeCallback: null, _errorCallback: null, data: { query: {} }, binaryType: 'blob', bufferedAmount: 0, extensions: '', onclose: null, onerror: null, onmessage: null, onopen: null, protocol: '', readyState: 1, url: '', CLOSED: 3, CONNECTING: 0, OPEN: 1, CLOSING: 2, dispatchEvent: mock(() => {}), addEventListener: mock(() => {}), removeEventListener: mock(() => {}), ping: mock(() => {}), pong: mock(() => {}), subscribe: mock(() => {}), unsubscribe: mock(() => {}), }); it('should register a new connection', () => { mockWs.data.gameId = 'test-game'; mockWs.data.playerId = 'player-alpha'; mockWs.data.query.gameId = 'test-game'; mockWs.data.query.playerId = 'player-alpha'; webSocketHandler.handleConnection(mockWs); expect((webSocketHandler as any).connections.get('test-game')).toContain( mockWs, ); }); it('should process a join_game message for an already connected client', () => { const gameId = gameManager.createGame().id; mockWs.data.query.gameId = gameId; mockWs.data.query.playerId = 'player1'; mockWs.data.gameId = gameId; mockWs.data.playerId = 'player1'; webSocketHandler.handleConnection(mockWs); const joinGameMessage = JSON.stringify({ type: 'join_game', gameId: gameId, playerId: 'player1', }); triggerMessage(mockWs, joinGameMessage); expect(mockWs.send).toHaveBeenCalledWith( expect.stringContaining('
{ const game = gameManager.createGame(); game.addPlayer('player1'); game.addPlayer('player2'); game.currentPlayer = 'black'; mockWs.data.gameId = game.id; mockWs.data.playerId = 'player1'; mockWs.data.query.gameId = game.id; mockWs.data.query.playerId = 'player1'; webSocketHandler.handleConnection(mockWs); const makeMoveMessage = JSON.stringify({ type: 'make_move', gameId: game.id, playerId: 'player1', row: 7, col: 7, }); triggerMessage(mockWs, makeMoveMessage); expect(mockWs.send).toHaveBeenCalledWith( expect.stringContaining('
{ const game = gameManager.createGame(); game.addPlayer('player1'); game.addPlayer('player2'); game.currentPlayer = 'black'; mockWs.data.gameId = game.id; mockWs.data.playerId = 'player1'; mockWs.data.query.gameId = game.id; mockWs.data.query.playerId = 'player1'; webSocketHandler.handleConnection(mockWs); const makeMoveMessage1 = JSON.stringify({ type: 'make_move', gameId: game.id, playerId: 'player1', row: 7, col: 7, }); triggerMessage(mockWs, makeMoveMessage1); mockWs.send.mockClear(); triggerMessage(mockWs, makeMoveMessage1); expect(mockWs.send).toHaveBeenCalledWith( JSON.stringify({ type: 'error', error: 'Cell already occupied' }), ); }); it('should handle ping/pong messages', () => { webSocketHandler.handleConnection(mockWs); const pingMessage = JSON.stringify({ type: 'ping' }); triggerMessage(mockWs, pingMessage); expect(mockWs.send).toHaveBeenCalledWith(JSON.stringify({ type: 'pong' })); }); it('should handle player disconnection and notify others', () => { const game = gameManager.createGame(); const player1Ws = createNewMockWs(); const player2Ws = createNewMockWs(); player1Ws.data.gameId = game.id; player1Ws.data.playerId = 'player1'; player1Ws.data.query.gameId = game.id; player1Ws.data.query.playerId = 'player1'; player2Ws.data.gameId = game.id; player2Ws.data.playerId = 'player2'; player2Ws.data.query.gameId = game.id; player2Ws.data.query.playerId = 'player2'; webSocketHandler.handleConnection(player1Ws); webSocketHandler.handleConnection(player2Ws); player1Ws.send.mockClear(); player2Ws.send.mockClear(); webSocketHandler.handleDisconnect(player2Ws); expect(player1Ws.send).toHaveBeenCalledWith( expect.stringContaining('
(call as string).includes('player2 disconnected')), ).toBeTrue(); expect((webSocketHandler as any).connections.get(game.id)).not.toContain( player2Ws, ); }); it('should send error for unknown message type', () => { webSocketHandler.handleConnection(mockWs); const unknownMessage = JSON.stringify({ type: 'unknown_type' }); triggerMessage(mockWs, unknownMessage); expect(mockWs.send).toHaveBeenCalledWith( JSON.stringify({ type: 'error', error: 'Unknown message type' }), ); }); it('should send error for invalid JSON message', () => { webSocketHandler.handleConnection(mockWs); const invalidJsonMessage = 'not a json'; triggerMessage(mockWs, invalidJsonMessage); expect(mockWs.send).toHaveBeenCalledWith( JSON.stringify({ type: 'error', error: 'Invalid message format' }), ); }); it('should broadcast game state to a specific client when targetWs is provided', () => { const game = gameManager.createGame(); game.addPlayer('player1'); game.addPlayer('player2'); game.currentPlayer = 'black'; const player1Ws = createNewMockWs(); player1Ws.data.gameId = game.id; player1Ws.data.playerId = 'player1'; player1Ws.data.query.gameId = game.id; player1Ws.data.query.playerId = 'player1'; webSocketHandler.handleConnection(player1Ws); const player2Ws = createNewMockWs(); player2Ws.data.gameId = game.id; player2Ws.data.playerId = 'player2'; player2Ws.data.query.gameId = game.id; player2Ws.data.query.playerId = 'player2'; webSocketHandler.handleConnection(player2Ws); player1Ws.send.mockClear(); player2Ws.send.mockClear(); webSocketHandler.broadcastGameUpdate(game.id, game, null, null, player1Ws); expect(player1Ws.send).toHaveBeenCalledWith( expect.stringContaining('
{ const game = gameManager.createGame(); game.addPlayer('player1'); game.addPlayer('player2'); game.currentPlayer = 'black'; const player1Ws = createNewMockWs(); player1Ws.data.gameId = game.id; player1Ws.data.playerId = 'player1'; player1Ws.data.query.gameId = game.id; player1Ws.data.query.playerId = 'player1'; webSocketHandler.handleConnection(player1Ws); const player2Ws = createNewMockWs(); player2Ws.data.gameId = game.id; player2Ws.data.playerId = 'player2'; player2Ws.data.query.gameId = game.id; player2Ws.data.query.playerId = 'player2'; webSocketHandler.handleConnection(player2Ws); player1Ws.send.mockClear(); player2Ws.send.mockClear(); webSocketHandler.broadcastGameUpdate(game.id, game); expect(player1Ws.send).toHaveBeenCalledWith( expect.stringContaining('