import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it, vi } from 'vitest'; import { DEFAULT_BOT_STATE, appendAlertUpdate, appendHistoryUpdate, applySymbolUpdate, replaceOrdersUpdate, replacePositionsUpdate, replaceSettingsUpdate, useWebSocket } from './useWebSocket'; vi.mock('../lib/supabaseClient', () => ({ supabase: { auth: { getSession: vi.fn(async () => ({ data: { session: null } })) } } })); vi.mock('socket.io-client', () => ({ io: vi.fn(() => ({ on: vi.fn(), close: vi.fn() })) })); describe('useWebSocket', () => { it('provides default bot state before any socket updates', () => { const Probe = () => { const { botState, connected } = useWebSocket('http://localhost:5000'); return React.createElement( 'div', null, `${botState.settings.executionMode}|${botState.settings.maxOpenTrades}|${connected ? '1' : '0'}|${Object.keys(botState.symbols).length}` ); }; const html = renderToStaticMarkup(React.createElement(Probe)); expect(html).toContain('Alerts|3|0|0'); }); it('applies symbol updates without mutating previous symbols', () => { const prev = { ...DEFAULT_BOT_STATE, symbols: { 'BTC/USD': { price: 100, change24h: 1, changeToday: 0.5, session: 'NY', volatility: 'High', signal: 'BUY', priceHistory: [], rules: {}, indicators: {} } } } as any; const next = applySymbolUpdate(prev, 'ETH/USD', { price: 200, change24h: -0.5, changeToday: 0.1, session: 'LDN', volatility: 'Med', signal: 'NONE', priceHistory: [], rules: {}, indicators: {} } as any); expect(Object.keys(prev.symbols)).toEqual(['BTC/USD']); expect(Object.keys(next.symbols)).toEqual(['BTC/USD', 'ETH/USD']); expect(next.symbols['ETH/USD'].price).toBe(200); }); it('appends alert and history updates in order', () => { const withAlert = appendAlertUpdate(DEFAULT_BOT_STATE, { timestamp: 1, type: 'info', symbol: 'BTC/USD', message: 'alert-1' }); const withHistory = appendHistoryUpdate(withAlert, { symbol: 'BTC/USD', side: 'BUY', entryPrice: 100, exitPrice: 110, size: 1, pnl: 10, pnlPercent: 10, reason: 'tp', timestamp: 2 }); expect(withAlert.alerts).toHaveLength(1); expect(withAlert.alerts[0].message).toBe('alert-1'); expect(withHistory.history).toHaveLength(1); expect(withHistory.history[0].pnl).toBe(10); }); it('replaces positions, orders and settings atomically', () => { const withPositions = replacePositionsUpdate(DEFAULT_BOT_STATE, [{ id: 'p1', symbol: 'BTC/USD', side: 'BUY', size: 1, entryPrice: 100, currentPrice: 101, stopLoss: 95, takeProfit: 110, unrealizedPnl: 1, unrealizedPnlPercent: 1, marketValue: 101 }]); const withOrders = replaceOrdersUpdate(withPositions, [{ id: 'o1', symbol: 'BTC/USD', type: 'market', side: 'buy', qty: 1, price: 100, status: 'filled', timestamp: 1 }]); const withSettings = replaceSettingsUpdate(withOrders, { executionMode: 'Paper', riskPerTrade: 0.02, totalCapital: 5000, maxOpenTrades: 5, isAlgoEnabled: true, enabledRules: ['TrendBiasRule'] }); expect(withSettings.positions).toHaveLength(1); expect(withSettings.orders).toHaveLength(1); expect(withSettings.settings.executionMode).toBe('Paper'); expect(withSettings.settings.isAlgoEnabled).toBe(true); }); });