fix(B9): surface builder strategies in strategies tab
This commit is contained in:
parent
81c52479ab
commit
70db4c9a04
52
web/src/lib/profileApi.dom.test.ts
Normal file
52
web/src/lib/profileApi.dom.test.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
@ -55,6 +55,12 @@ async function apiRequest<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
return body as T;
|
||||
}
|
||||
|
||||
function notifyProfilesUpdated() {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new Event('profiles-updated'));
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCurrentUserProfile(): Promise<CurrentUserProfile> {
|
||||
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<TradeProfi
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
notifyProfilesUpdated();
|
||||
return response.profile;
|
||||
}
|
||||
|
||||
@ -102,6 +110,7 @@ export async function setTradeProfileActive(id: string, isActive: boolean): Prom
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ is_active: isActive }),
|
||||
});
|
||||
notifyProfilesUpdated();
|
||||
return response.profile;
|
||||
}
|
||||
|
||||
@ -109,4 +118,5 @@ export async function deleteTradeProfile(id: string): Promise<void> {
|
||||
await apiRequest<{ success: boolean }>(`/api/profiles/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
notifyProfilesUpdated();
|
||||
}
|
||||
|
||||
81
web/src/tabs/MyStrategiesTab.dom.test.tsx
Normal file
81
web/src/tabs/MyStrategiesTab.dom.test.tsx
Normal file
@ -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: () => <div>Strategy wizard</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../backtest/components/BacktestRunnerPanel', () => ({
|
||||
BacktestRunnerPanel: () => <div>Backtest runner</div>,
|
||||
}));
|
||||
|
||||
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(<MyStrategiesTab botState={botState} />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@ -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<{
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginTop: '12px' }}>
|
||||
<span style={{ background: 'rgba(0,255,136,0.05)', color: '#00ff88', fontSize: '10px', fontWeight: 900, padding: '4px 10px', borderRadius: '6px', border: '1px solid rgba(0,255,136,0.1)', textTransform: 'uppercase' }}>
|
||||
{profile.symbols}
|
||||
</span>
|
||||
<span style={{ background: 'rgba(255,255,255,0.03)', color: '#666', fontSize: '10px', fontWeight: 900, padding: '4px 10px', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.05)', textTransform: 'uppercase' }}>
|
||||
V4.0 Core
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span style={{ background: 'rgba(255,255,255,0.03)', color: '#666', fontSize: '10px', fontWeight: 900, padding: '4px 10px', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.05)', textTransform: 'uppercase' }}>
|
||||
{strategyKindLabel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4. Operational DNA (Specs) */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px', marginBottom: '28px' }}>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user