diff --git a/frontend/src/test/details.test.tsx b/frontend/src/test/details.test.tsx new file mode 100644 index 0000000..a246ae7 --- /dev/null +++ b/frontend/src/test/details.test.tsx @@ -0,0 +1,268 @@ +import { describe, it, expect } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import { renderRoute } from './render'; + +describe('Document Detail page', () => { + it('renders document title', async () => { + renderRoute('/documents/d1'); + await waitFor(() => { + expect(screen.getByText('Apple Q4 Earnings Beat')).toBeInTheDocument(); + }); + }); + + it('shows document type and source type', async () => { + renderRoute('/documents/d1'); + await waitFor(() => { + expect(screen.getByText('news')).toBeInTheDocument(); + }); + expect(screen.getByText('news_api')).toBeInTheDocument(); + }); + + it('shows publisher', async () => { + renderRoute('/documents/d1'); + await waitFor(() => { + expect(screen.getByText('Reuters')).toBeInTheDocument(); + }); + }); + + it('renders metadata section', async () => { + renderRoute('/documents/d1'); + await waitFor(() => { + expect(screen.getByText('Metadata')).toBeInTheDocument(); + }); + expect(screen.getByText('Language')).toBeInTheDocument(); + expect(screen.getByText('en')).toBeInTheDocument(); + expect(screen.getByText('Content Hash')).toBeInTheDocument(); + }); + + it('renders parse quality score', async () => { + renderRoute('/documents/d1'); + await waitFor(() => { + expect(screen.getByText('Parse Quality')).toBeInTheDocument(); + }); + expect(screen.getByText('0.92')).toBeInTheDocument(); + }); + + it('shows no intelligence message when null', async () => { + renderRoute('/documents/d1'); + await waitFor(() => { + expect(screen.getByText('No intelligence extraction available')).toBeInTheDocument(); + }); + }); + + it('renders storage references section', async () => { + renderRoute('/documents/d1'); + await waitFor(() => { + expect(screen.getByText('Storage References')).toBeInTheDocument(); + }); + }); +}); + +describe('Recommendation Detail page', () => { + it('renders ticker as heading', async () => { + renderRoute('/recommendations/r1'); + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'AAPL' })).toBeInTheDocument(); + }); + }); + + it('shows action and mode badges', async () => { + renderRoute('/recommendations/r1'); + await waitFor(() => { + expect(screen.getByText('buy')).toBeInTheDocument(); + }); + expect(screen.getByText('paper')).toBeInTheDocument(); + }); + + it('shows time horizon', async () => { + renderRoute('/recommendations/r1'); + await waitFor(() => { + expect(screen.getByText('Horizon')).toBeInTheDocument(); + }); + expect(screen.getByText('7d')).toBeInTheDocument(); + }); + + it('shows risk classification', async () => { + renderRoute('/recommendations/r1'); + await waitFor(() => { + expect(screen.getByText('Risk')).toBeInTheDocument(); + }); + expect(screen.getByText('moderate')).toBeInTheDocument(); + }); + + it('shows portfolio and max loss percentages', async () => { + renderRoute('/recommendations/r1'); + await waitFor(() => { + expect(screen.getByText('Portfolio %')).toBeInTheDocument(); + }); + expect(screen.getByText('5.0%')).toBeInTheDocument(); + expect(screen.getByText('2.00%')).toBeInTheDocument(); + }); + + it('shows thesis', async () => { + renderRoute('/recommendations/r1'); + await waitFor(() => { + expect(screen.getByText('Thesis')).toBeInTheDocument(); + }); + expect(screen.getByText('Strong earnings momentum')).toBeInTheDocument(); + }); + + it('shows evidence section with zero count', async () => { + renderRoute('/recommendations/r1'); + await waitFor(() => { + expect(screen.getByText('Evidence (0)')).toBeInTheDocument(); + }); + expect(screen.getByText('No evidence linked')).toBeInTheDocument(); + }); +}); + +describe('Trend Detail page', () => { + it('renders entity ID as heading', async () => { + renderRoute('/trends/t1'); + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'AAPL' })).toBeInTheDocument(); + }); + }); + + it('shows trend direction', async () => { + renderRoute('/trends/t1'); + await waitFor(() => { + expect(screen.getByText('bullish')).toBeInTheDocument(); + }); + }); + + it('shows window badge', async () => { + renderRoute('/trends/t1'); + await waitFor(() => { + expect(screen.getByText('AAPL')).toBeInTheDocument(); + }); + expect(screen.getAllByText('7d').length).toBeGreaterThanOrEqual(1); + }); + + it('shows contradiction score', async () => { + renderRoute('/trends/t1'); + await waitFor(() => { + expect(screen.getByText('Contradiction')).toBeInTheDocument(); + }); + expect(screen.getByText('10%')).toBeInTheDocument(); + }); + + it('shows dominant catalysts', async () => { + renderRoute('/trends/t1'); + await waitFor(() => { + expect(screen.getByText('Dominant Catalysts')).toBeInTheDocument(); + }); + expect(screen.getByText('earnings')).toBeInTheDocument(); + }); + + it('shows contributing evidence section', async () => { + renderRoute('/trends/t1'); + await waitFor(() => { + expect(screen.getByText('Contributing Evidence (0)')).toBeInTheDocument(); + }); + expect(screen.getByText('No evidence records')).toBeInTheDocument(); + }); + + it('shows trend projection panel', async () => { + renderRoute('/trends/t1'); + await waitFor(() => { + expect(screen.getByText('Trend Projection')).toBeInTheDocument(); + }); + // Mock projection: projected_direction: 'bearish', diverges_from_current: true + expect(screen.getByText('DIVERGENCE')).toBeInTheDocument(); + expect(screen.getByText('bearish')).toBeInTheDocument(); + }); + + it('shows macro contribution percentage', async () => { + renderRoute('/trends/t1'); + await waitFor(() => { + expect(screen.getByText('Macro Contribution')).toBeInTheDocument(); + }); + // macro_contribution_pct: 0.3 → 30% + expect(screen.getByText('30%')).toBeInTheDocument(); + }); +}); + +describe('Global Event Detail page', () => { + it('renders page heading', async () => { + renderRoute('/macro/events/me1'); + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Global Event' })).toBeInTheDocument(); + }); + }); + + it('shows severity badge', async () => { + renderRoute('/macro/events/me1'); + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Global Event' })).toBeInTheDocument(); + }); + // severity: 'high' + expect(screen.getByText('high')).toBeInTheDocument(); + }); + + it('shows event summary', async () => { + renderRoute('/macro/events/me1'); + await waitFor(() => { + expect(screen.getByText('Summary')).toBeInTheDocument(); + }); + expect(screen.getByText('US tariffs on Chinese semiconductors')).toBeInTheDocument(); + }); + + it('shows key facts', async () => { + renderRoute('/macro/events/me1'); + await waitFor(() => { + expect(screen.getByText('Key Facts')).toBeInTheDocument(); + }); + expect(screen.getByText('25% tariff')).toBeInTheDocument(); + expect(screen.getByText('Effective in 30 days')).toBeInTheDocument(); + }); + + it('shows affected regions', async () => { + renderRoute('/macro/events/me1'); + await waitFor(() => { + expect(screen.getByText('Regions')).toBeInTheDocument(); + }); + expect(screen.getByText('US')).toBeInTheDocument(); + expect(screen.getByText('CN')).toBeInTheDocument(); + }); + + it('shows affected companies table', async () => { + renderRoute('/macro/events/me1'); + await waitFor(() => { + expect(screen.getByText(/Affected Companies/)).toBeInTheDocument(); + }); + // Mock has 1 impact: AAPL with negative direction + expect(screen.getByText('AAPL')).toBeInTheDocument(); + expect(screen.getByText('negative')).toBeInTheDocument(); + }); + + it('shows estimated duration', async () => { + renderRoute('/macro/events/me1'); + await waitFor(() => { + expect(screen.getByText('medium term')).toBeInTheDocument(); + }); + }); +}); + +describe('Company Detail page', () => { + it('renders company ticker as heading', async () => { + renderRoute('/companies/1'); + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'AAPL' })).toBeInTheDocument(); + }); + }); + + it('shows company name', async () => { + renderRoute('/companies/1'); + await waitFor(() => { + expect(screen.getByText('Apple Inc.')).toBeInTheDocument(); + }); + }); + + it('shows sector and exchange', async () => { + renderRoute('/companies/1'); + await waitFor(() => { + expect(screen.getByText('NASDAQ')).toBeInTheDocument(); + }); + }); +});