// @vitest-environment jsdom import { beforeEach, describe, expect, it, vi } from 'vitest'; import { act, renderHook, waitFor } from '@testing-library/react'; import { useWebSocket } from './useWebSocket'; const { getPlatformAccessTokenMock, ioMock } = vi.hoisted(() => ({ getPlatformAccessTokenMock: vi.fn(), ioMock: vi.fn() })); vi.mock('../lib/authSession', () => ({ getPlatformAccessToken: getPlatformAccessTokenMock })); vi.mock('socket.io-client', () => ({ io: ioMock })); describe('useWebSocket DOM/event behavior', () => { const handlers: Record void> = {}; let socketStub: any; beforeEach(() => { Object.keys(handlers).forEach((key) => delete handlers[key]); getPlatformAccessTokenMock.mockReset(); ioMock.mockReset(); socketStub = { on: vi.fn((event: string, callback: (...args: any[]) => void) => { handlers[event] = callback; return socketStub; }), close: vi.fn() }; ioMock.mockReturnValue(socketStub); }); it('skips socket connection when there is no auth session token', async () => { getPlatformAccessTokenMock.mockRejectedValue(new Error('Not authenticated')); const { result } = renderHook(() => useWebSocket('http://localhost:5000')); await waitFor(() => { expect(getPlatformAccessTokenMock).toHaveBeenCalledTimes(1); }); expect(ioMock).not.toHaveBeenCalled(); expect(result.current.connected).toBe(false); expect(result.current.botState.settings.executionMode).toBe('Alerts'); }); it('connects with token and applies websocket event updates', async () => { getPlatformAccessTokenMock.mockResolvedValue('token-abc'); const { result, unmount } = renderHook(() => useWebSocket('http://localhost:5000')); await waitFor(() => { expect(ioMock).toHaveBeenCalledTimes(1); }); const options = ioMock.mock.calls[0][1]; expect(options.auth.token).toBe('token-abc'); expect(options.transports).toEqual(['polling', 'websocket']); act(() => handlers.connect()); expect(result.current.connected).toBe(true); act(() => handlers.state({ symbols: {}, alerts: [], positions: [], orders: [], history: [], settings: { executionMode: 'Paper', riskPerTrade: 0.02, totalCapital: 2000, maxOpenTrades: 2, isAlgoEnabled: true, enabledRules: ['TrendBiasRule'] }, uptime: 10 })); expect(result.current.botState.settings.executionMode).toBe('Paper'); expect(result.current.botState.settings.isAlgoEnabled).toBe(true); act(() => handlers.symbol_update({ symbol: 'BTC/USD', data: { price: 70000, change24h: 1.2, changeToday: 0.4, session: 'NY', volatility: 'High', signal: 'BUY', priceHistory: [], rules: {}, indicators: {} } })); expect(result.current.botState.symbols['BTC/USD'].price).toBe(70000); act(() => handlers.new_alert({ timestamp: 1, type: 'info', symbol: 'BTC/USD', message: 'alert' })); expect(result.current.botState.alerts).toHaveLength(1); act(() => handlers.positions_update([{ id: 'pos-1', symbol: 'BTC/USD', side: 'BUY', size: 1, entryPrice: 70000, currentPrice: 70100, stopLoss: 69000, takeProfit: 71000, unrealizedPnl: 100, unrealizedPnlPercent: 0.14, marketValue: 70100 }])); expect(result.current.botState.positions).toHaveLength(1); act(() => handlers.orders_update([{ id: 'ord-1', symbol: 'BTC/USD', type: 'market', side: 'buy', qty: 1, price: 70000, status: 'filled', timestamp: 2 }])); expect(result.current.botState.orders).toHaveLength(1); act(() => handlers.history_update({ symbol: 'BTC/USD', side: 'BUY', entryPrice: 70000, exitPrice: 71000, size: 1, pnl: 1000, pnlPercent: 1.42, reason: 'tp', timestamp: 3 })); expect(result.current.botState.history).toHaveLength(1); act(() => handlers.settings_update({ executionMode: 'Live', riskPerTrade: 0.01, totalCapital: 5000, maxOpenTrades: 5, isAlgoEnabled: false, enabledRules: [] })); expect(result.current.botState.settings.executionMode).toBe('Live'); act(() => handlers.disconnect()); expect(result.current.connected).toBe(false); unmount(); expect(socketStub.close).toHaveBeenCalled(); }); });