// @vitest-environment jsdom import { beforeEach, describe, expect, it, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import { OverviewTab, dedupeLivePositions } from './OverviewTab'; import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket'; const { authMock, fetchTradeProfilesMock, canonicalLifecycleHookState } = vi.hoisted(() => { return { authMock: { user: { id: 'u1' }, profile: { role: 'user' } }, fetchTradeProfilesMock: vi.fn(), canonicalLifecycleHookState: { snapshot: null as any, loading: false, error: null as string | null } }; }); vi.mock('../components/AuthContext', () => ({ useAuth: () => ({ ...authMock, refreshProfile: vi.fn() }) })); vi.mock('../lib/profileApi', () => ({ fetchTradeProfiles: fetchTradeProfilesMock })); vi.mock('../hooks/useCanonicalLifecycle', () => ({ useCanonicalLifecycle: () => ({ snapshot: canonicalLifecycleHookState.snapshot, loading: canonicalLifecycleHookState.loading, error: canonicalLifecycleHookState.error }) })); describe('OverviewTab coverage maximization', () => { let mockBotState: any; beforeEach(() => { vi.clearAllMocks(); authMock.profile.role = 'user'; canonicalLifecycleHookState.snapshot = null; canonicalLifecycleHookState.loading = false; canonicalLifecycleHookState.error = null; mockBotState = JSON.parse(JSON.stringify(DEFAULT_BOT_STATE)); mockBotState.symbols = { 'BTC/USDT': { signal: 'BUY', volatility: 'LOW', session: 'US', indicators: { rsi_1h: 60, ema50_4h: 50000, ema200_4h: 40000 }, profileSignals: { 'p1': { signal: 'BUY', passed: true } } } }; fetchTradeProfilesMock.mockResolvedValue([ { id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 5000, strategy_config: { execution: { cooldownMinutes: 5 } } } ]); }); it('covers all uptime and duration formatting paths', async () => { mockBotState.uptime = 45000; const { unmount } = render(); await waitFor(() => expect(screen.getByText('45s')).toBeInTheDocument()); unmount(); mockBotState.uptime = 3600000 * 25 + 60000; render(); await waitFor(() => expect(screen.getByText('25h 1m')).toBeInTheDocument()); }); it('covers Capital Sync Warning branch', async () => { mockBotState.positions = [ { symbol: 'BTC/USDT', side: 'BUY', entryPrice: 60000, size: 0.1, profileId: 'p1' } ]; render(); await waitFor(() => expect(screen.getByText(/Capital sync warning/i)).toBeInTheDocument()); }); it('covers fallback capital logic', async () => { fetchTradeProfilesMock.mockResolvedValue([]); mockBotState.settings.totalCapital = 25000; render(); // The label in bot-status-bar is "Allocated:" await waitFor(() => expect(screen.getByText(/^Allocated:$/i).parentElement).toHaveTextContent(/\$25,?000\.00/)); }); it('covers Signal Active branches', async () => { fetchTradeProfilesMock.mockResolvedValueOnce([ { id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 0, strategy_config: { execution: { cooldownMinutes: 5 } } } ]); const { unmount } = render(); await waitFor(() => expect(screen.getByText(/Signal active, no allocation/i)).toBeInTheDocument()); unmount(); fetchTradeProfilesMock.mockResolvedValueOnce([ { id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 5000, strategy_config: { execution: { cooldownMinutes: 5 } } } ]); render(); await waitFor(() => expect(screen.getByText(/Signal active, waiting entry/i)).toBeInTheDocument()); }); it('shows blocked state when directional signal is blocked by execution guard', async () => { mockBotState.symbols['BTC/USDT'].profileSignals = { p1: { signal: 'BUY', passed: true, execution: { status: 'BLOCKED', code: 'trading_paused', reason: 'Trading is paused by control plane.' } } }; render(); await waitFor(() => expect(screen.getByText(/Signal blocked by guard/i)).toBeInTheDocument()); }); it('falls back to live profile positions when canonical lifecycle is unavailable', async () => { mockBotState.positions = [ { id: 'pos-live-1', symbol: 'BTC/USDT', side: 'BUY', size: 1, entryPrice: 1000, currentPrice: 1010, stopLoss: 950, takeProfit: 1100, unrealizedPnl: 10, unrealizedPnlPercent: 1, marketValue: 1010, profileId: 'p1', profileName: 'Alpha', tradeId: 'TRD-LIVE-1' } ]; render(); await waitFor(() => expect(screen.getByText(/In Position \(1 open\)/i)).toBeInTheDocument()); }); it('prefers live-flat utilization when canonical lifecycle has stale open notional', async () => { canonicalLifecycleHookState.snapshot = { generatedAt: Date.now(), diagnostics: { orderRows: 1, lifecycleRows: 1, openPositions: 1, realizedTrades: 0, truncated: false }, profiles: [{ id: 'p1', name: 'Alpha', allocatedCapital: 5000, isActive: true }], lifecycleRows: [], openPositions: [], realizedTrades: [], aggregates: { total: { openTrades: 1, openNotional: 20205.03, realizedPnl: 0, unrealizedPnl: 0, netPnl: 0, tradeCount: 0, wins: 0, winRate: 0 }, byProfile: { p1: { profileId: 'p1', profileName: 'Alpha', allocatedCapital: 5000, isActive: true, openTrades: 1, openNotional: 20205.03, realizedPnl: 0, unrealizedPnl: 0, netPnl: 0, tradeCount: 0, wins: 0, winRate: 0, lastClosedTradeAt: 0 } } } }; mockBotState.positions = []; render(); await waitFor(() => expect(screen.getByText(/Lifecycle sync pending/i)).toBeInTheDocument()); expect(screen.queryByText(/Capital sync warning/i)).not.toBeInTheDocument(); }); it('covers Admin role data fetching', async () => { authMock.profile.role = 'admin'; render(); await waitFor(() => expect(fetchTradeProfilesMock).toHaveBeenCalledWith({ scope: 'all' })); }); it('covers error handling in fetchData', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); fetchTradeProfilesMock.mockRejectedValueOnce(new Error('Query Fail')); render(); await waitFor(() => expect(consoleSpy).toHaveBeenCalled()); consoleSpy.mockRestore(); }); it('covers Mixed signal label rendering for customer', async () => { mockBotState.symbols['BTC/USDT'].signal = 'MIXED'; render(); await waitFor(() => expect(screen.getByText(/Conditions mixed — holding off/i)).toBeInTheDocument()); }); it('covers Mixed signal label rendering for admin', async () => { authMock.profile.role = 'admin'; mockBotState.symbols['BTC/USDT'].signal = 'MIXED'; render(); await waitFor(() => expect(screen.getByText(/PROFILE SIGNALS MIXED/i)).toBeInTheDocument()); }); }); describe('dedupeLivePositions logic', () => { it('merges positions with identical tradeId', () => { const p1 = { symbol: 'BTC', side: 'BUY', tradeId: 'T1', profileId: 'P1', entryPrice: 1, size: 1 } as any; const p2 = { symbol: 'BTC', side: 'BUY', tradeId: 'T1', profileId: 'P1', entryPrice: 1, size: 2 } as any; const result = dedupeLivePositions([p1, p2]); expect(result).toHaveLength(1); expect(result[0].size).toBe(2); }); it('merges positions using fallback key', () => { const p1 = { symbol: 'BTC', side: 'BUY', tradeId: '', profileId: 'P1', entryPrice: 1, size: 1 } as any; const p2 = { symbol: 'BTC', side: 'BUY', tradeId: '', profileId: 'P1', entryPrice: 1, size: 2 } as any; const result = dedupeLivePositions([p1, p2]); expect(result).toHaveLength(1); expect(result[0].size).toBe(2); }); it('handles null/undefined gracefully', () => { expect(dedupeLivePositions(undefined)).toEqual([]); expect(dedupeLivePositions(null as any)).toEqual([]); }); });