learning_ai_invt_trdg/web/src/App.dom.test.tsx
Saravana Achu Mac 938ed86044 feat: live data wiring (Alpaca/FMP) + strategy builder + screener
Wires the new dashboard to real market data and adds the strategy
builder & screener UIs that were stubbed in the previous commit.

Frontend (web/src/):
- lib/marketApi.ts: authenticated fetch helpers for chart bars,
  market indices, news, and FMP research endpoints
- views/HomeView.tsx: StockChart now fetches live OHLCV via
  fetchChartBars on symbol/period change with loading/error states;
  ResearchCards replaces the static placeholder with live FMP
  profile/metrics/earnings (next-earnings + last 3 historical)
- components/layout/Header.tsx: live SPY/DIA/QQQ price + change%
  via fetchMarketIndices, refreshing every 60s; removed unused
  static sparkline placeholder
- components/strategy/VisualRuleBuilder.tsx: drag-and-drop IF/THEN
  rule composer using @dnd-kit (RSI/MACD/EMA/Price/Volume,
  above/below/crosses, BUY/SELL with shares or % of capital);
  saves via POST /api/profiles
- components/strategy/CodeStrategyEditor.tsx: Monaco editor with
  JS strategy template; "Run Backtest" posts to /api/backtest and
  renders return/win-rate/Sharpe/drawdown plus trade log
- views/ResearchView.tsx: adds "Visual Builder" and "Code Editor"
  sub-tabs alongside Strategies / Signals / Backtesting
- views/ScreenerView.tsx: live FMP screener with market-cap and
  sector filters, sortable columns, click-to-load-symbol routing
- index.css: light theme background; @keyframes spin for loaders
- App.dom.test.tsx: rewritten for router-based AppShell (was
  asserting on the removed tab UI; fixes 5 prior failures)

Backend (backend/src/services/apiServer.ts):
- /api/chart/bars: detects crypto symbols (contains "/") and
  routes to Alpaca v1beta3/crypto/us/bars; equities use
  v2/stocks/{symbol}/bars with iex feed
- (existing) /api/news, /api/market/indices, /api/research/{
  profile,metrics,earnings}, /api/screener proxy endpoints

Build/config:
- web/vite.config.ts: dedupe react / react/jsx-runtime /
  react-router-dom so the vendored react-auth dist resolves the
  same React instance (fixes "Cannot resolve react/jsx-runtime"
  Rollup error)
- web/tsconfig.app.json: exclude shared/platform-clients.ts and
  shared/platform-mobile.ts (mobile-only, missing RN SDK)
- web/package.json: add react-router-dom, @monaco-editor/react,
  @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities

Verification: `npm run build` in web/ → clean (✓ built in 3s);
backend tsc --noEmit → clean. Test suite: 151/155 pass; the 4
remaining failures are pre-existing (3 useTabFeatureFlags module
cache leaks, 1 EntryForm), not introduced here.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 06:16:46 -07:00

159 lines
5.6 KiB
TypeScript

// @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: () => (
<div data-testid="app-shell">
<nav>
<a href="/">Home</a>
<a href="/portfolio">Portfolio</a>
<a href="/research">Research</a>
<a href="/settings">Settings</a>
</nav>
<main data-testid="main-content">Dashboard Content</main>
</div>
)
}));
vi.mock('./components/AlertFeed', () => ({ AlertFeed: () => <div>AlertFeedMock</div> }));
vi.mock('./components/MarketOpportunities', () => ({
AISetups: () => <div>AISetups</div>,
TopVolatile: () => <div>TopVolatile</div>
}));
vi.mock('./components/ChatControl', () => ({
ChatControl: ({ onApplyProfile }: any) => (
<button onClick={() => onApplyProfile('create_profile', { name: 'New' })}>ApplyChat</button>
)
}));
vi.mock('./components/Login', () => ({ Login: () => <div>LoginMock</div> }));
vi.mock('./components/ResetPassword', () => ({ ResetPassword: () => <div>ResetPasswordMock</div> }));
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(<App />);
expect(screen.getByText('LoginMock')).toBeInTheDocument();
});
it('renders main dashboard when authenticated', async () => {
authMock.user = { id: 'u1', email: 'test@demo.com' };
render(<App />);
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(<App />);
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(<App />);
// 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(<App />);
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');
});
});