// @vitest-environment jsdom import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import App, { resolveProfileNameForAction, buildChatApplyPayload } from './App'; const { authMock, socketMock, fetchTradeProfilesMock, createTradeProfileMock, updateTradeProfileMock } = vi.hoisted(() => ({ authMock: { user: null as any, profile: null as any, loading: false, signOut: vi.fn() }, socketMock: { botState: { settings: { isAlgoEnabled: true }, symbols: {}, health: { tradingLoopHealthy: true, reconciliationLoopHealthy: true, capitalInvariantViolations: 0 }, alerts: [], positions: [], orders: [], history: [], uptime: 0, }, connected: true, socket: null, }, fetchTradeProfilesMock: vi.fn(), createTradeProfileMock: vi.fn(), updateTradeProfileMock: vi.fn() })); vi.mock('./components/AuthContext', () => ({ useAuth: () => authMock })); vi.mock('./hooks/useWebSocket', () => ({ useWebSocket: () => socketMock, DEFAULT_BOT_STATE: { settings: { isAlgoEnabled: true }, symbols: {}, health: { tradingLoopHealthy: true, reconciliationLoopHealthy: true, capitalInvariantViolations: 0 }, alerts: [], positions: [], orders: [], history: [], uptime: 0, } })); vi.mock('./lib/profileApi', () => ({ fetchTradeProfiles: fetchTradeProfilesMock, createTradeProfile: createTradeProfileMock, updateTradeProfile: updateTradeProfileMock })); // Mock all layout and view components — they have external dependencies vi.mock('./components/layout/AppShell', () => ({ AppShell: () => (
Dashboard Content
) })); vi.mock('./components/AlertFeed', () => ({ AlertFeed: () =>
AlertFeedMock
})); vi.mock('./components/MarketOpportunities', () => ({ AISetups: () =>
AISetups
, TopVolatile: () =>
TopVolatile
})); vi.mock('./components/ChatControl', () => ({ ChatControl: ({ onApplyProfile }: any) => ( ) })); vi.mock('./components/Login', () => ({ Login: () =>
LoginMock
})); vi.mock('./components/ResetPassword', () => ({ ResetPassword: () =>
ResetPasswordMock
})); vi.mock('./backtest/flags', () => ({ useBacktestFeatureGate: () => ({ enabled: false, loading: false }), isBacktestBuildEnabled: () => false, })); vi.mock('./hooks/useTabFeatureFlags', () => ({ useTabFeatureFlags: () => ({ flags: { marketplace: false } }) })); describe('App Component DOM', () => { beforeEach(() => { vi.useFakeTimers(); authMock.user = null; authMock.profile = null; authMock.loading = false; socketMock.botState = { settings: { isAlgoEnabled: true }, symbols: {}, health: { tradingLoopHealthy: true, reconciliationLoopHealthy: true, capitalInvariantViolations: 0 }, alerts: [], positions: [], orders: [], history: [], uptime: 0, }; fetchTradeProfilesMock.mockResolvedValue([]); createTradeProfileMock.mockResolvedValue({}); updateTradeProfileMock.mockResolvedValue({}); }); afterEach(() => { vi.useRealTimers(); }); it('renders login when not authenticated', () => { render(); expect(screen.getByText('LoginMock')).toBeInTheDocument(); }); it('renders main dashboard when authenticated', async () => { authMock.user = { id: 'u1', email: 'test@demo.com' }; render(); expect(screen.getByTestId('app-shell')).toBeInTheDocument(); expect(screen.getByTestId('main-content')).toBeInTheDocument(); }); it('shows sidebar nav links when authenticated', () => { authMock.user = { id: 'u1', email: 'test@demo.com' }; render(); expect(screen.getByText('Portfolio')).toBeInTheDocument(); expect(screen.getByText('Research')).toBeInTheDocument(); expect(screen.getByText('Settings')).toBeInTheDocument(); }); it('admin sees settings route (not hidden)', () => { authMock.user = { id: 'u1', email: 'test@demo.com' }; authMock.profile = { role: 'admin' }; render(); // Admin can access settings which includes admin panel expect(screen.getByText('Settings')).toBeInTheDocument(); }); it('non-admin user still has settings nav', () => { authMock.user = { id: 'u1', email: 'test@demo.com' }; authMock.profile = { role: 'user' }; render(); expect(screen.getByText('Settings')).toBeInTheDocument(); }); }); describe('App Logic Helpers', () => { it('resolveProfileNameForAction appends time on collision', () => { const profiles = [{ name: 'Alpha' }]; const res = resolveProfileNameForAction('create_profile', 'Alpha', profiles, () => '123'); expect(res).toBe('Alpha (123)'); }); it('buildChatApplyPayload uses defaults', () => { const payload = buildChatApplyPayload({}, 'u1', 'Name'); expect(payload.symbols).toBe('BTC/USDT'); }); });