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>
159 lines
5.6 KiB
TypeScript
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');
|
|
});
|
|
});
|