From ed8175eb1f5c6e179051709bfd3fa6f2ccb01db6 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Mon, 4 May 2026 16:07:31 -0700 Subject: [PATCH] fix(B2,B3): wire ticker header actions Route the ticker header watchlist and alert controls to their existing screens and replace the symbol placeholder with the research profile company name while preserving a symbol fallback. Refs: docs/AUDIT_REDESIGN.md items B2 and B3. Co-Authored-By: GPT-5 Codex --- web/src/views/HomeView.dom.test.tsx | 87 +++++++++++++++++++++++++++++ web/src/views/HomeView.tsx | 42 +++++++++++--- 2 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 web/src/views/HomeView.dom.test.tsx diff --git a/web/src/views/HomeView.dom.test.tsx b/web/src/views/HomeView.dom.test.tsx new file mode 100644 index 0000000..1875da6 --- /dev/null +++ b/web/src/views/HomeView.dom.test.tsx @@ -0,0 +1,87 @@ +// @vitest-environment jsdom +import { describe, expect, it, vi } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { AppContext, type AppContextValue } from '../context/AppContext'; +import { TickerHeader } from './HomeView'; + +const { fetchResearchProfileMock } = vi.hoisted(() => ({ + fetchResearchProfileMock: vi.fn(), +})); + +vi.mock('../lib/marketApi', async () => { + const actual = await vi.importActual('../lib/marketApi'); + return { + ...actual, + fetchResearchProfile: fetchResearchProfileMock, + }; +}); + +const appContext: AppContextValue = { + botState: { + settings: { isAlgoEnabled: true }, + symbols: { + AAPL: { + price: 212.42, + changeToday: 1.5, + }, + }, + health: { tradingLoopHealthy: true, reconciliationLoopHealthy: true, capitalInvariantViolations: 0 }, + alerts: [], + positions: [], + orders: [], + history: [], + uptime: 0, + } as any, + socket: null, + connected: true, + activeSymbol: 'AAPL', + setActiveSymbol: vi.fn(), + isAdmin: false, + user: { id: 'u1' }, + profile: {}, + showBacktestTab: false, + showMarketplaceTab: false, + handleSignOut: vi.fn(), +}; + +function renderTickerHeader() { + return render( + + + + } /> + Watchlist route} /> + Alerts route} /> + + + , + ); +} + +describe('TickerHeader', () => { + it('loads the company name from the research profile', async () => { + fetchResearchProfileMock.mockResolvedValueOnce({ companyName: 'Apple Inc.' }); + + renderTickerHeader(); + + expect(screen.getByRole('heading', { name: 'AAPL' })).toBeInTheDocument(); + await waitFor(() => expect(screen.getByText('Apple Inc.')).toBeInTheDocument()); + }); + + it('opens watchlist and alerts routes from header actions', async () => { + fetchResearchProfileMock.mockResolvedValue({ companyName: 'Apple Inc.' }); + const user = userEvent.setup(); + + const firstRender = renderTickerHeader(); + + await user.click(screen.getByRole('button', { name: /open watchlist/i })); + expect(screen.getByText('Watchlist route')).toBeInTheDocument(); + firstRender.unmount(); + + renderTickerHeader(); + await user.click(screen.getByRole('button', { name: /open alerts/i })); + expect(screen.getByText('Alerts route')).toBeInTheDocument(); + }); +}); diff --git a/web/src/views/HomeView.tsx b/web/src/views/HomeView.tsx index 7e7ba5b..1267f83 100644 --- a/web/src/views/HomeView.tsx +++ b/web/src/views/HomeView.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import { Star, Bell, BarChart2, Loader2 } from 'lucide-react'; import { AreaChart, Area, Bar, ComposedChart, Line, LineChart, XAxis, YAxis, Tooltip, @@ -132,14 +133,35 @@ function calculateBollingerBands(closes: number[], period = 20, deviations = 2) return { upper, middle, lower }; } +function normalizeResearchProfile(profile: any): any { + return Array.isArray(profile) ? profile[0] : profile; +} + // ─── Ticker header ──────────────────────────────────────────────────────────── -function TickerHeader({ symbol }: { symbol: string }) { +export function TickerHeader({ symbol }: { symbol: string }) { + const navigate = useNavigate(); const { botState } = useAppContext(); const data = botState.symbols?.[symbol]; const price = data?.price ?? 0; const change = data?.changeToday ?? 0; const changePct = price > 0 ? (change / (price - change)) * 100 : 0; const positive = change >= 0; + const [companyName, setCompanyName] = useState(symbol); + + useEffect(() => { + let cancelled = false; + setCompanyName(symbol); + fetchResearchProfile(symbol) + .then(profile => { + if (cancelled) return; + const company = normalizeResearchProfile(profile)?.companyName; + setCompanyName(typeof company === 'string' && company.trim() ? company.trim() : symbol); + }) + .catch(() => { + if (!cancelled) setCompanyName(symbol); + }); + return () => { cancelled = true; }; + }, [symbol]); return (
@@ -148,13 +170,15 @@ function TickerHeader({ symbol }: { symbol: string }) { {symbol} - {/* Company name placeholder — Phase 4 will fill from FMP */} - {symbol} + {companyName}
- {/* Saved badge */} - -