feat: add 40 interaction tests for Positions, Orders, Trends, Recommendations pages
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { renderRoute } from './render';
|
||||
import { server } from './mocks/server';
|
||||
|
||||
describe('Orders page', () => {
|
||||
// -------------------------------------------------------------------------
|
||||
// 1. Renders order list with data
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders order list with ticker and status', async () => {
|
||||
renderRoute('/orders');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('filled')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 2. Displays order details — side, type, quantity
|
||||
// -------------------------------------------------------------------------
|
||||
it('displays side, type, and quantity', async () => {
|
||||
renderRoute('/orders');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
const row = screen.getByText('AAPL').closest('tr')!;
|
||||
expect(row).toHaveTextContent('buy');
|
||||
expect(row).toHaveTextContent('market');
|
||||
expect(row).toHaveTextContent('10');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 3. Displays fill price in USD format
|
||||
// -------------------------------------------------------------------------
|
||||
it('displays fill price formatted as USD', async () => {
|
||||
renderRoute('/orders');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('$185.50')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 4. Empty state
|
||||
// -------------------------------------------------------------------------
|
||||
it('shows empty state when no orders exist', async () => {
|
||||
server.use(
|
||||
http.get('/api/orders', () => HttpResponse.json([])),
|
||||
);
|
||||
renderRoute('/orders');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No data')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 5. Page title and ticker filter render
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders page title and ticker filter', async () => {
|
||||
renderRoute('/orders');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('heading', { name: 'Orders' })).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByPlaceholderText('Ticker…')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 6. Row click navigates to order detail
|
||||
// -------------------------------------------------------------------------
|
||||
it('navigates to order detail on row click', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderRoute('/orders');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
const row = screen.getByText('AAPL').closest('tr')!;
|
||||
await user.click(row);
|
||||
// Order detail page should render with the order ticker
|
||||
await waitFor(() => {
|
||||
// OrderDetail shows the ticker as h1 plus side and status badges
|
||||
expect(screen.getByText('Type')).toBeInTheDocument();
|
||||
expect(screen.getByText('Fill Price')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 7. Order detail shows all fields
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders order detail with all fields', async () => {
|
||||
renderRoute('/orders/o1');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Type')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Quantity')).toBeInTheDocument();
|
||||
expect(screen.getByText('Fill Price')).toBeInTheDocument();
|
||||
expect(screen.getByText('Fill Qty')).toBeInTheDocument();
|
||||
expect(screen.getByText('$185.50')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 8. Order detail shows events section
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders events section on order detail', async () => {
|
||||
renderRoute('/orders/o1');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Events (0)')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('No events')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 9. Multiple orders render correctly
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders multiple orders', async () => {
|
||||
server.use(
|
||||
http.get('/api/orders', () => HttpResponse.json([
|
||||
{ id: 'o1', recommendation_id: 'r1', broker_account_id: null, ticker: 'AAPL', side: 'buy', order_type: 'market', quantity: 10, limit_price: null, stop_price: null, status: 'filled', broker_order_id: null, submitted_at: '2026-04-10T19:01:00Z', fill_price: 185.50, fill_quantity: 10, created_at: '2026-04-10T19:01:00Z' },
|
||||
{ id: 'o2', recommendation_id: 'r2', broker_account_id: null, ticker: 'MSFT', side: 'sell', order_type: 'limit', quantity: 5, limit_price: 420.00, stop_price: null, status: 'pending', broker_order_id: null, submitted_at: null, fill_price: null, fill_quantity: null, created_at: '2026-04-11T10:00:00Z' },
|
||||
])),
|
||||
);
|
||||
renderRoute('/orders');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('MSFT')).toBeInTheDocument();
|
||||
expect(screen.getByText('pending')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 10. Null fill price shows dash
|
||||
// -------------------------------------------------------------------------
|
||||
it('shows dash for null fill price', async () => {
|
||||
server.use(
|
||||
http.get('/api/orders', () => HttpResponse.json([
|
||||
{ id: 'o2', recommendation_id: 'r2', broker_account_id: null, ticker: 'GOOG', side: 'buy', order_type: 'limit', quantity: 3, limit_price: 170.00, stop_price: null, status: 'pending', broker_order_id: null, submitted_at: null, fill_price: null, fill_quantity: null, created_at: '2026-04-11T10:00:00Z' },
|
||||
])),
|
||||
);
|
||||
renderRoute('/orders');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('GOOG')).toBeInTheDocument();
|
||||
});
|
||||
const row = screen.getByText('GOOG').closest('tr')!;
|
||||
expect(row).toHaveTextContent('—');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { screen, waitFor, within } from '@testing-library/react';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { renderRoute } from './render';
|
||||
import { server } from './mocks/server';
|
||||
|
||||
describe('Positions page', () => {
|
||||
// -------------------------------------------------------------------------
|
||||
// 1. Renders positions table with data
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders positions table with ticker and quantity', async () => {
|
||||
renderRoute('/positions');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
const row = screen.getByText('AAPL').closest('tr')!;
|
||||
expect(row).toHaveTextContent('10');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 2. Displays formatted USD prices
|
||||
// -------------------------------------------------------------------------
|
||||
it('displays entry and current prices in USD format', async () => {
|
||||
renderRoute('/positions');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('$185.50')).toBeInTheDocument();
|
||||
expect(screen.getByText('$188.20')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 3. Displays unrealized and realized PnL
|
||||
// -------------------------------------------------------------------------
|
||||
it('displays unrealized and realized PnL', async () => {
|
||||
renderRoute('/positions');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
const row = screen.getByText('AAPL').closest('tr')!;
|
||||
expect(row).toHaveTextContent('$27.00');
|
||||
expect(row).toHaveTextContent('$0.00');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 4. PnL color coding — green for positive
|
||||
// -------------------------------------------------------------------------
|
||||
it('applies green color to positive unrealized PnL', async () => {
|
||||
renderRoute('/positions');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
const row = screen.getByText('AAPL').closest('tr')!;
|
||||
const pnlEl = within(row).getByText('$27.00');
|
||||
expect(pnlEl.className).toContain('text-green');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 5. Totals row renders with correct sums
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders totals row with correct sums', async () => {
|
||||
renderRoute('/positions');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Totals')).toBeInTheDocument();
|
||||
});
|
||||
const totalsRow = screen.getByText('Totals').closest('tr')!;
|
||||
const cells = within(totalsRow).getAllByRole('cell');
|
||||
// Totals: label, qty, cost basis, market value, unrealized, realized, empty
|
||||
expect(cells[0]).toHaveTextContent('Totals');
|
||||
expect(cells[1]).toHaveTextContent('10');
|
||||
// cost basis = 10 * 185.50 = 1855.00
|
||||
expect(cells[2]).toHaveTextContent('$1855.00');
|
||||
// market value = 10 * 188.20 = 1882.00
|
||||
expect(cells[3]).toHaveTextContent('$1882.00');
|
||||
// unrealized = 27.00
|
||||
expect(cells[4]).toHaveTextContent('$27.00');
|
||||
// realized = 0.00
|
||||
expect(cells[5]).toHaveTextContent('$0.00');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 6. Empty state — no positions
|
||||
// -------------------------------------------------------------------------
|
||||
it('shows empty state when no positions exist', async () => {
|
||||
server.use(
|
||||
http.get('/api/positions', () => HttpResponse.json([])),
|
||||
);
|
||||
renderRoute('/positions');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No data')).toBeInTheDocument();
|
||||
});
|
||||
// Totals row should NOT render when empty
|
||||
expect(screen.queryByText('Totals')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 7. Multiple positions — totals sum correctly
|
||||
// -------------------------------------------------------------------------
|
||||
it('sums totals correctly with multiple positions', async () => {
|
||||
server.use(
|
||||
http.get('/api/positions', () => HttpResponse.json([
|
||||
{ id: 'p1', broker_account_id: null, ticker: 'AAPL', quantity: 10, avg_entry_price: 185.50, current_price: 188.20, unrealized_pnl: 27.00, realized_pnl: 0, updated_at: '2026-04-11T12:00:00Z' },
|
||||
{ id: 'p2', broker_account_id: null, ticker: 'MSFT', quantity: 5, avg_entry_price: 420.00, current_price: 415.00, unrealized_pnl: -25.00, realized_pnl: 50.00, updated_at: '2026-04-11T12:00:00Z' },
|
||||
])),
|
||||
);
|
||||
renderRoute('/positions');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Totals')).toBeInTheDocument();
|
||||
});
|
||||
const totalsRow = screen.getByText('Totals').closest('tr')!;
|
||||
const cells = within(totalsRow).getAllByRole('cell');
|
||||
// qty = 10 + 5 = 15
|
||||
expect(cells[1]).toHaveTextContent('15');
|
||||
// cost basis = (10*185.50) + (5*420.00) = 1855 + 2100 = 3955.00
|
||||
expect(cells[2]).toHaveTextContent('$3955.00');
|
||||
// market value = (10*188.20) + (5*415.00) = 1882 + 2075 = 3957.00
|
||||
expect(cells[3]).toHaveTextContent('$3957.00');
|
||||
// unrealized = 27 + (-25) = 2.00
|
||||
expect(cells[4]).toHaveTextContent('$2.00');
|
||||
// realized = 0 + 50 = 50.00
|
||||
expect(cells[5]).toHaveTextContent('$50.00');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 8. Negative PnL gets red color
|
||||
// -------------------------------------------------------------------------
|
||||
it('applies red color to negative unrealized PnL', async () => {
|
||||
server.use(
|
||||
http.get('/api/positions', () => HttpResponse.json([
|
||||
{ id: 'p1', broker_account_id: null, ticker: 'TSLA', quantity: 3, avg_entry_price: 250.00, current_price: 240.00, unrealized_pnl: -30.00, realized_pnl: 0, updated_at: '2026-04-11T12:00:00Z' },
|
||||
])),
|
||||
);
|
||||
renderRoute('/positions');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('TSLA')).toBeInTheDocument();
|
||||
});
|
||||
const row = screen.getByText('TSLA').closest('tr')!;
|
||||
const pnlEl = within(row).getByText('$-30.00');
|
||||
expect(pnlEl.className).toContain('text-red');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 9. Updated timestamp renders
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders the updated_at timestamp', async () => {
|
||||
renderRoute('/positions');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
// The component renders new Date(r.updated_at).toLocaleString()
|
||||
// Just verify something date-like appears in the row
|
||||
const row = screen.getByText('AAPL').closest('tr')!;
|
||||
expect(row).toHaveTextContent('2026');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 10. Page title renders
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders the page title', async () => {
|
||||
renderRoute('/positions');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Positions')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { renderRoute } from './render';
|
||||
import { server } from './mocks/server';
|
||||
|
||||
describe('Recommendations page', () => {
|
||||
// -------------------------------------------------------------------------
|
||||
// 1. Renders recommendation list with ticker
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders recommendation with ticker', async () => {
|
||||
renderRoute('/recommendations');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 2. Shows action badge
|
||||
// -------------------------------------------------------------------------
|
||||
it('shows action badge (buy)', async () => {
|
||||
renderRoute('/recommendations');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('buy')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 3. Shows mode badge
|
||||
// -------------------------------------------------------------------------
|
||||
it('shows mode badge (paper)', async () => {
|
||||
renderRoute('/recommendations');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('paper')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 4. Shows thesis text
|
||||
// -------------------------------------------------------------------------
|
||||
it('shows thesis text', async () => {
|
||||
renderRoute('/recommendations');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Strong earnings momentum')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 5. Risk tier filter renders with options
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders risk tier filter with options', async () => {
|
||||
renderRoute('/recommendations');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('Risk Tier')).toBeInTheDocument();
|
||||
});
|
||||
const select = screen.getByLabelText('Risk Tier');
|
||||
const options = within(select).getAllByRole('option');
|
||||
expect(options).toHaveLength(3);
|
||||
expect(options[0]).toHaveTextContent('Conservative');
|
||||
expect(options[1]).toHaveTextContent('Moderate');
|
||||
expect(options[2]).toHaveTextContent('Aggressive');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 6. Empty state
|
||||
// -------------------------------------------------------------------------
|
||||
it('shows empty state when no recommendations', async () => {
|
||||
server.use(
|
||||
http.get('/api/recommendations', () => HttpResponse.json([])),
|
||||
);
|
||||
renderRoute('/recommendations');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No data')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 7. Page title renders
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders page title', async () => {
|
||||
renderRoute('/recommendations');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('heading', { name: 'Recommendations' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 8. Multiple recommendations render
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders multiple recommendations', async () => {
|
||||
server.use(
|
||||
http.get('/api/recommendations', () => HttpResponse.json([
|
||||
{ id: 'r1', ticker: 'AAPL', action: 'buy', mode: 'paper', confidence: 0.78, time_horizon: '7d', thesis: 'Strong earnings', invalidation_conditions: null, portfolio_pct: 0.05, max_loss_pct: 0.02, model_version: 'v1', risk_classification: 'moderate', generated_at: '2026-04-10T19:00:00Z' },
|
||||
{ id: 'r2', ticker: 'MSFT', action: 'sell', mode: 'paper', confidence: 0.65, time_horizon: '30d', thesis: 'Weakening outlook', invalidation_conditions: null, portfolio_pct: 0.03, max_loss_pct: 0.01, model_version: 'v1', risk_classification: 'conservative', generated_at: '2026-04-11T10:00:00Z' },
|
||||
])),
|
||||
);
|
||||
renderRoute('/recommendations');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('MSFT')).toBeInTheDocument();
|
||||
expect(screen.getByText('sell')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 9. Risk tier filter changes selection
|
||||
// -------------------------------------------------------------------------
|
||||
it('allows changing risk tier', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderRoute('/recommendations');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('Risk Tier')).toBeInTheDocument();
|
||||
});
|
||||
const select = screen.getByLabelText('Risk Tier');
|
||||
await user.selectOptions(select, 'aggressive');
|
||||
expect(select).toHaveValue('aggressive');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 10. Ticker filter renders
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders ticker filter', async () => {
|
||||
renderRoute('/recommendations');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('Ticker…')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { renderRoute } from './render';
|
||||
import { server } from './mocks/server';
|
||||
|
||||
describe('Trends page', () => {
|
||||
// -------------------------------------------------------------------------
|
||||
// 1. Renders trend cards with entity ID
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders trend card with ticker', async () => {
|
||||
renderRoute('/trends');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 2. Shows trend direction arrow
|
||||
// -------------------------------------------------------------------------
|
||||
it('shows trend direction arrow', async () => {
|
||||
renderRoute('/trends');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
// TrendArrow renders an aria-label
|
||||
expect(screen.getByLabelText('Bullish')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 3. Shows window badge
|
||||
// -------------------------------------------------------------------------
|
||||
it('shows window badge on trend card', async () => {
|
||||
renderRoute('/trends');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
// The card has a window badge; the filter also has a 7d button
|
||||
// Verify at least two 7d elements exist (badge + filter button)
|
||||
const elements = screen.getAllByText('7d');
|
||||
expect(elements.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 4. Shows strength and confidence labels
|
||||
// -------------------------------------------------------------------------
|
||||
it('shows strength and confidence labels', async () => {
|
||||
renderRoute('/trends');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Strength')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Confidence')).toBeInTheDocument();
|
||||
expect(screen.getByText('Contradiction')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 5. Shows contradiction score
|
||||
// -------------------------------------------------------------------------
|
||||
it('displays contradiction score as percentage', async () => {
|
||||
renderRoute('/trends');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
// contradiction_score = 0.1 → "10%"
|
||||
expect(screen.getByText('10%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 6. Shows catalyst tags
|
||||
// -------------------------------------------------------------------------
|
||||
it('shows dominant catalyst tags', async () => {
|
||||
renderRoute('/trends');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('earnings')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 7. Evidence expand/collapse
|
||||
// -------------------------------------------------------------------------
|
||||
it('expands and collapses evidence on click', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderRoute('/trends');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
});
|
||||
// Mock has 1 supporting evidence item
|
||||
const showBtn = screen.getByText(/Show evidence/);
|
||||
expect(showBtn).toBeInTheDocument();
|
||||
await user.click(showBtn);
|
||||
// After expanding, should show "Hide evidence"
|
||||
expect(screen.getByText(/Hide evidence/)).toBeInTheDocument();
|
||||
// Click again to collapse
|
||||
await user.click(screen.getByText(/Hide evidence/));
|
||||
expect(screen.getByText(/Show evidence/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 8. Window filter buttons render
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders window filter buttons', async () => {
|
||||
renderRoute('/trends');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('All')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('intraday')).toBeInTheDocument();
|
||||
expect(screen.getByText('1d')).toBeInTheDocument();
|
||||
expect(screen.getByText('30d')).toBeInTheDocument();
|
||||
expect(screen.getByText('90d')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 9. Empty state
|
||||
// -------------------------------------------------------------------------
|
||||
it('shows empty state when no trends', async () => {
|
||||
server.use(
|
||||
http.get('/api/trends', () => HttpResponse.json([])),
|
||||
);
|
||||
renderRoute('/trends');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No trends found')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 10. Page title renders
|
||||
// -------------------------------------------------------------------------
|
||||
it('renders page title', async () => {
|
||||
renderRoute('/trends');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('heading', { name: 'Trends' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user