// @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: () => (
)
}));
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');
});
});