diff --git a/web/src/lib/profileApi.dom.test.ts b/web/src/lib/profileApi.dom.test.ts new file mode 100644 index 0000000..53df40c --- /dev/null +++ b/web/src/lib/profileApi.dom.test.ts @@ -0,0 +1,52 @@ +// @vitest-environment jsdom +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createTradeProfile } from './profileApi'; + +vi.mock('./authSession', () => ({ + getPlatformAccessToken: vi.fn(async () => 'test-token'), +})); + +vi.mock('./runtime', () => ({ + tradingRuntime: { tradingApiUrl: 'https://trading.test' }, +})); + +describe('profileApi profile update notifications', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('dispatches profiles-updated after creating a trade profile', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, + json: async () => ({ + profile: { + id: 'profile-visual-1', + name: 'Visual Strategy', + symbols: 'AAPL', + allocated_capital: 1000, + risk_per_trade_percent: 1, + is_active: false, + strategy_config: { type: 'visual', rules: [] }, + }, + }), + } as Response); + const listener = vi.fn(); + window.addEventListener('profiles-updated', listener); + + await createTradeProfile({ + name: 'Visual Strategy', + symbols: 'AAPL', + allocated_capital: 1000, + risk_per_trade_percent: 1, + is_active: false, + strategy_config: { type: 'visual', rules: [] }, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://trading.test/api/profiles', + expect.objectContaining({ method: 'POST' }), + ); + expect(listener).toHaveBeenCalledTimes(1); + window.removeEventListener('profiles-updated', listener); + }); +}); diff --git a/web/src/lib/profileApi.ts b/web/src/lib/profileApi.ts index 859440b..7b7eb84 100644 --- a/web/src/lib/profileApi.ts +++ b/web/src/lib/profileApi.ts @@ -55,6 +55,12 @@ async function apiRequest(path: string, init?: RequestInit): Promise { return body as T; } +function notifyProfilesUpdated() { + if (typeof window !== 'undefined') { + window.dispatchEvent(new Event('profiles-updated')); + } +} + export async function fetchCurrentUserProfile(): Promise { const response = await apiRequest<{ profile: CurrentUserProfile }>('/api/me/profile'); return response.profile; @@ -86,6 +92,7 @@ export async function createTradeProfile(payload: TradeProfilePayload): Promise< method: 'POST', body: JSON.stringify(payload), }); + notifyProfilesUpdated(); return response.profile; } @@ -94,6 +101,7 @@ export async function updateTradeProfile(id: string, payload: Partial { await apiRequest<{ success: boolean }>(`/api/profiles/${id}`, { method: 'DELETE', }); + notifyProfilesUpdated(); } diff --git a/web/src/tabs/MyStrategiesTab.dom.test.tsx b/web/src/tabs/MyStrategiesTab.dom.test.tsx new file mode 100644 index 0000000..877e3df --- /dev/null +++ b/web/src/tabs/MyStrategiesTab.dom.test.tsx @@ -0,0 +1,81 @@ +// @vitest-environment jsdom +import { render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { MyStrategiesTab } from './MyStrategiesTab'; + +const { fetchTradeProfilesMock } = vi.hoisted(() => ({ + fetchTradeProfilesMock: vi.fn(), +})); + +vi.mock('../components/AuthContext', () => ({ + useAuth: () => ({ + user: { id: 'user-1', email: 'trader@example.test' }, + profile: { subscription_tier: 'pro' }, + }), +})); + +vi.mock('../lib/profileApi', () => ({ + fetchTradeProfiles: fetchTradeProfilesMock, + setTradeProfileActive: vi.fn(), + deleteTradeProfile: vi.fn(), +})); + +vi.mock('../lib/StrategyExplanationService', () => ({ + getStrategyExplanation: () => ({ reason: 'Ready for review', recommendation: 'Review sizing.' }), +})); + +vi.mock('../backtest/useBacktestFeatureGate', () => ({ + useBacktestFeatureGate: () => ({ enabled: false, loading: false }), +})); + +vi.mock('../components/StrategyWizard', () => ({ + StrategyWizard: () =>
Strategy wizard
, +})); + +vi.mock('../backtest/components/BacktestRunnerPanel', () => ({ + BacktestRunnerPanel: () =>
Backtest runner
, +})); + +const botState = { + connected: true, + symbols: { + AAPL: { change24h: 1.2 }, + MSFT: { change24h: -0.4 }, + }, +}; + +describe('MyStrategiesTab builder strategy visibility', () => { + beforeEach(() => { + fetchTradeProfilesMock.mockReset(); + }); + + it('shows saved visual and code strategies with their builder type badges', async () => { + fetchTradeProfilesMock.mockResolvedValue([ + { + id: 'visual-1', + name: 'Visual Momentum', + symbols: 'AAPL', + allocated_capital: 1000, + risk_per_trade_percent: 1, + is_active: false, + strategy_config: { type: 'visual', rules: [] }, + }, + { + id: 'code-1', + name: 'Code Mean Reversion', + symbols: 'MSFT', + allocated_capital: 2000, + risk_per_trade_percent: 1, + is_active: false, + strategy_config: { type: 'code', language: 'javascript', code: 'function strategy() {}' }, + }, + ]); + + render(); + + expect(await screen.findByText('Visual Momentum')).toBeInTheDocument(); + expect(screen.getByText('Code Mean Reversion')).toBeInTheDocument(); + expect(screen.getByText('Visual Builder')).toBeInTheDocument(); + expect(screen.getByText('Code Strategy')).toBeInTheDocument(); + }); +}); diff --git a/web/src/tabs/MyStrategiesTab.tsx b/web/src/tabs/MyStrategiesTab.tsx index 90e066d..12025ac 100644 --- a/web/src/tabs/MyStrategiesTab.tsx +++ b/web/src/tabs/MyStrategiesTab.tsx @@ -27,8 +27,14 @@ import { StrategyWizard } from '../components/StrategyWizard'; import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel'; import { useBacktestFeatureGate } from '../backtest/useBacktestFeatureGate'; import { deleteTradeProfile, fetchTradeProfiles, setTradeProfileActive } from '../lib/profileApi'; - -const ActiveStrategyCard: React.FC<{ + +function getStrategyKindLabel(config: any) { + if (config?.type === 'visual') return 'Visual Builder'; + if (config?.type === 'code') return 'Code Strategy'; + return 'V4.0 Core'; +} + +const ActiveStrategyCard: React.FC<{ profile: any; botState: any; tier: string; @@ -39,9 +45,10 @@ const ActiveStrategyCard: React.FC<{ isExpanded: boolean; onToggleExpand: (id: string) => void; }> = ({ profile, botState, tier, onToggle, onEdit, onBacktest, onDelete, isExpanded, onToggleExpand }) => { - const config = profile.strategy_config; - const isAggressive = config?.execution?.minRulePassRatio < 0.9; - const isSafe = config?.execution?.minRulePassRatio >= 1.0; + const config = profile.strategy_config; + const isAggressive = config?.execution?.minRulePassRatio < 0.9; + const isSafe = config?.execution?.minRulePassRatio >= 1.0; + const strategyKindLabel = getStrategyKindLabel(config); const explanation = getStrategyExplanation(profile, botState); @@ -115,12 +122,12 @@ const ActiveStrategyCard: React.FC<{
{profile.symbols} - - - V4.0 Core - -
- + + + {strategyKindLabel} + + + {/* 4. Operational DNA (Specs) */}