learning_ai_invt_trdg/web/src/hooks/useWebSocket.dom.test.tsx

168 lines
5.1 KiB
TypeScript

// @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<string, (...args: any[]) => 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();
});
});