import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it, vi } from 'vitest'; import type { BotState } from '../hooks/useWebSocket'; import { dedupeLivePositions, OverviewTab } from './OverviewTab'; vi.mock('../components/AuthContext', () => ({ useAuth: () => ({ user: null, profile: { role: 'admin' } }) })); vi.mock('../lib/supabaseClient', () => { const query: any = { select: vi.fn(() => query), order: vi.fn(() => query), eq: vi.fn(() => query), limit: vi.fn(() => Promise.resolve({ data: [], error: null })) }; return { supabase: { from: vi.fn(() => query) } }; }); const createBotState = (): BotState => ({ symbols: { 'BTC/USDT': { price: 70123.12, change24h: 1.25, changeToday: 0.44, session: 'NY', volatility: 'High', signal: 'BUY', signalTime: Date.now() - 120000, tradingMode: 'Paper', activePosition: null, priceHistory: [ { timestamp: Date.now() - 600000, price: 69990 }, { timestamp: Date.now(), price: 70123.12 } ], rules: { TrendBiasRule: { passed: true, reason: 'Trend aligned' } }, profileSignals: { profileA: { profileName: 'High Risk Scalper', signal: 'BUY', passed: true, reason: 'Rule stack passed' } }, indicators: { ema50_4h: 68000, ema200_4h: 66000, rsi_1h: 58 } } }, alerts: [], positions: [ { id: 'pos-1', symbol: 'BTC/USDT', side: 'BUY', size: 0.15, entryPrice: 70000, currentPrice: 70123.12, stopLoss: 69000, takeProfit: 71500, unrealizedPnl: 18.47, unrealizedPnlPercent: 0.17, marketValue: 10518.47, profileId: 'profileA', profileName: 'High Risk Scalper', tradeId: 'TRD-OVR-1' } ], orders: [], history: [ { symbol: 'BTC/USDT', side: 'BUY', entryPrice: 69000, exitPrice: 70000, size: 0.1, pnl: 100, pnlPercent: 1.45, reason: 'Target hit', timestamp: Date.now() - 3600000, profileId: 'profileA', trade_id: 'TRD-HIST-1', source: 'BOT' } ], settings: { executionMode: 'Paper', riskPerTrade: 0.01, totalCapital: 15000, maxOpenTrades: 6, isAlgoEnabled: true, enabledRules: ['TrendBiasRule', 'MomentumRule'] }, uptime: 3600000 }); describe('OverviewTab', () => { it('dedupes runtime positions by trade id and preserves profile metadata', () => { const baseState = createBotState(); baseState.positions = [ { ...baseState.positions[0], id: 'pos-no-profile', profileId: undefined, profileName: undefined }, { ...baseState.positions[0], id: 'pos-profile', profileId: 'profileA', profileName: 'High Risk Scalper' } ]; const deduped = dedupeLivePositions(baseState.positions); expect(deduped).toHaveLength(1); expect(deduped[0].profileId).toBe('profileA'); expect(deduped[0].tradeId).toBe('TRD-OVR-1'); }); it('renders market readiness with capital, pnl, and signal cards', () => { const html = renderToStaticMarkup( React.createElement(OverviewTab, { botState: createBotState() }) ); expect(html).toContain('Market Readiness'); expect(html).toContain('Capital Used'); expect(html).toContain('Net P&L'); expect(html).toContain('P&L Duration'); expect(html).toContain('BTC/USDT'); expect(html).toContain('SIGNAL ACTIVE'); expect(html).toContain('Win Rate Window'); }); it('renders stable fallback values when symbols and history are empty', () => { const emptyState = createBotState(); emptyState.symbols = {}; emptyState.positions = []; emptyState.history = []; const html = renderToStaticMarkup( React.createElement(OverviewTab, { botState: emptyState }) ); expect(html).toContain('Account (Fallback)'); expect(html).toContain('No signal'); expect(html).toContain('Awaiting trade history'); }); });