import { describe, it, expect } from 'vitest'; import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { http, HttpResponse } from 'msw'; import { renderRoute } from './render'; import { server } from './mocks/server'; // --------------------------------------------------------------------------- // 1. Override tab renders in tab bar // --------------------------------------------------------------------------- describe('Override tab in Trading Engine', () => { it('renders Override tab in the tab bar', async () => { renderRoute('/trading/engine?tab=override'); await waitFor(() => { const tab = screen.getByRole('tab', { name: 'Override' }); expect(tab).toBeInTheDocument(); expect(tab).toHaveAttribute('aria-selected', 'true'); }); }); // --------------------------------------------------------------------------- // 2. Override tab shows form and positions sections // --------------------------------------------------------------------------- it('shows order form and positions sections', async () => { renderRoute('/trading/engine?tab=override'); await waitFor(() => { expect(screen.getByText('Submit Override Order')).toBeInTheDocument(); }); expect(screen.getByText('Current Positions')).toBeInTheDocument(); }); // --------------------------------------------------------------------------- // 3. Override tab accessible via URL param ?tab=override // --------------------------------------------------------------------------- it('is accessible via URL param ?tab=override', async () => { renderRoute('/trading/engine?tab=override'); await waitFor(() => { expect(screen.getByText('Submit Override Order')).toBeInTheDocument(); }); // The Override tab should be selected const tab = screen.getByRole('tab', { name: 'Override' }); expect(tab).toHaveAttribute('aria-selected', 'true'); }); // --------------------------------------------------------------------------- // 4. Order form fields are present // --------------------------------------------------------------------------- it('renders all order form fields (ticker, side, quantity, order type)', async () => { renderRoute('/trading/engine?tab=override'); await waitFor(() => { expect(screen.getByTestId('ticker-input')).toBeInTheDocument(); }); expect(screen.getByTestId('side-buy')).toBeInTheDocument(); expect(screen.getByTestId('side-sell')).toBeInTheDocument(); expect(screen.getByTestId('quantity-input')).toBeInTheDocument(); expect(screen.getByTestId('order-type-select')).toBeInTheDocument(); expect(screen.getByTestId('submit-order')).toBeInTheDocument(); }); // --------------------------------------------------------------------------- // 5. Conditional price fields show/hide based on order type // --------------------------------------------------------------------------- it('shows limit price field when order type is limit', async () => { const user = userEvent.setup(); renderRoute('/trading/engine?tab=override'); await waitFor(() => { expect(screen.getByTestId('order-type-select')).toBeInTheDocument(); }); // Market order — no price fields expect(screen.queryByTestId('limit-price-input')).not.toBeInTheDocument(); expect(screen.queryByTestId('stop-price-input')).not.toBeInTheDocument(); // Switch to limit await user.selectOptions(screen.getByTestId('order-type-select'), 'limit'); expect(screen.getByTestId('limit-price-input')).toBeInTheDocument(); expect(screen.queryByTestId('stop-price-input')).not.toBeInTheDocument(); // Switch to stop await user.selectOptions(screen.getByTestId('order-type-select'), 'stop'); expect(screen.queryByTestId('limit-price-input')).not.toBeInTheDocument(); expect(screen.getByTestId('stop-price-input')).toBeInTheDocument(); // Switch to stop_limit await user.selectOptions(screen.getByTestId('order-type-select'), 'stop_limit'); expect(screen.getByTestId('limit-price-input')).toBeInTheDocument(); expect(screen.getByTestId('stop-price-input')).toBeInTheDocument(); // Switch back to market await user.selectOptions(screen.getByTestId('order-type-select'), 'market'); expect(screen.queryByTestId('limit-price-input')).not.toBeInTheDocument(); expect(screen.queryByTestId('stop-price-input')).not.toBeInTheDocument(); }); // --------------------------------------------------------------------------- // 6. Form validation errors for invalid inputs // --------------------------------------------------------------------------- it('shows validation errors for invalid inputs', async () => { const user = userEvent.setup(); renderRoute('/trading/engine?tab=override'); await waitFor(() => { expect(screen.getByTestId('submit-order')).toBeInTheDocument(); }); // Submit empty form await user.click(screen.getByTestId('submit-order')); await waitFor(() => { // Ticker and quantity should show errors const alerts = screen.getAllByRole('alert'); expect(alerts.length).toBeGreaterThanOrEqual(2); }); expect(screen.getByText(/Ticker must be 1–10 alphabetic characters/)).toBeInTheDocument(); expect(screen.getByText(/Quantity must be a positive number/)).toBeInTheDocument(); }); it('shows validation error for missing limit price on limit order', async () => { const user = userEvent.setup(); renderRoute('/trading/engine?tab=override'); await waitFor(() => { expect(screen.getByTestId('order-type-select')).toBeInTheDocument(); }); await user.type(screen.getByTestId('ticker-input'), 'AAPL'); await user.type(screen.getByTestId('quantity-input'), '10'); await user.selectOptions(screen.getByTestId('order-type-select'), 'limit'); // Submit without limit price await user.click(screen.getByTestId('submit-order')); await waitFor(() => { expect(screen.getByText(/Limit price is required/)).toBeInTheDocument(); }); }); // --------------------------------------------------------------------------- // 7. Successful order submission shows success message and resets form // --------------------------------------------------------------------------- it('shows success message and resets form on successful submission', async () => { const user = userEvent.setup(); renderRoute('/trading/engine?tab=override'); await waitFor(() => { expect(screen.getByTestId('ticker-input')).toBeInTheDocument(); }); await user.type(screen.getByTestId('ticker-input'), 'AAPL'); await user.type(screen.getByTestId('quantity-input'), '10'); await user.click(screen.getByTestId('submit-order')); await waitFor(() => { expect(screen.getByTestId('success-banner')).toBeInTheDocument(); }); expect(screen.getByText(/Order queued — Job ID: job-test-123/)).toBeInTheDocument(); expect(screen.getByText(/Status: queued/)).toBeInTheDocument(); // Form should be reset expect(screen.getByTestId('ticker-input')).toHaveValue(''); expect(screen.getByTestId('quantity-input')).toHaveValue(null); }); // --------------------------------------------------------------------------- // 8. 422 error display // --------------------------------------------------------------------------- it('displays 422 validation errors from the server', async () => { // Override the handler to return 422 server.use( http.post('/trading/api/trading/override/order', () => { return HttpResponse.json( { detail: [{ msg: 'Ticker not recognized by broker' }] }, { status: 422 }, ); }), ); const user = userEvent.setup(); renderRoute('/trading/engine?tab=override'); await waitFor(() => { expect(screen.getByTestId('ticker-input')).toBeInTheDocument(); }); await user.type(screen.getByTestId('ticker-input'), 'AAPL'); await user.type(screen.getByTestId('quantity-input'), '10'); await user.click(screen.getByTestId('submit-order')); await waitFor(() => { expect(screen.getByTestId('validation-errors')).toBeInTheDocument(); }); expect(screen.getByText('Ticker not recognized by broker')).toBeInTheDocument(); }); // --------------------------------------------------------------------------- // 9. Submit button loading state during submission // --------------------------------------------------------------------------- it('disables submit button and shows loading state during submission', async () => { // Use a delayed handler to observe loading state server.use( http.post('/trading/api/trading/override/order', async ({ request }) => { await new Promise((resolve) => setTimeout(resolve, 200)); const body = await request.json() as Record; return HttpResponse.json( { job_id: 'job-test-123', status: 'queued', ticker: body.ticker, side: body.side, quantity: body.quantity, auto_registered: false }, { status: 202 }, ); }), ); const user = userEvent.setup(); renderRoute('/trading/engine?tab=override'); await waitFor(() => { expect(screen.getByTestId('ticker-input')).toBeInTheDocument(); }); await user.type(screen.getByTestId('ticker-input'), 'AAPL'); await user.type(screen.getByTestId('quantity-input'), '10'); await user.click(screen.getByTestId('submit-order')); // Button should be disabled and show loading text await waitFor(() => { const btn = screen.getByTestId('submit-order'); expect(btn).toBeDisabled(); expect(btn).toHaveTextContent('Submitting…'); }); // Eventually resolves to success await waitFor(() => { expect(screen.getByTestId('success-banner')).toBeInTheDocument(); }); }); // --------------------------------------------------------------------------- // 10. Positions table renders with mock data // --------------------------------------------------------------------------- it('renders positions table with mock data', async () => { renderRoute('/trading/engine?tab=override'); await waitFor(() => { expect(screen.getByTestId('positions-table')).toBeInTheDocument(); }); const table = screen.getByTestId('positions-table'); expect(within(table).getByText('AAPL')).toBeInTheDocument(); expect(within(table).getByText('10')).toBeInTheDocument(); }); // --------------------------------------------------------------------------- // 11. Positions loading state // --------------------------------------------------------------------------- it('shows loading state while positions are loading', async () => { // Delay the positions response to observe loading state server.use( http.get('/api/positions', async () => { await new Promise((resolve) => setTimeout(resolve, 500)); return HttpResponse.json([]); }), ); renderRoute('/trading/engine?tab=override'); // The form should render while positions are loading await waitFor(() => { expect(screen.getByText('Submit Override Order')).toBeInTheDocument(); }); // The positions section should show a loading indicator (LoadingSpinner renders a role="status" element) expect(screen.getByText('Current Positions')).toBeInTheDocument(); }); // --------------------------------------------------------------------------- // 12. Positions empty state // --------------------------------------------------------------------------- it('shows empty state when no positions exist', async () => { server.use( http.get('/api/positions', () => HttpResponse.json([])), ); renderRoute('/trading/engine?tab=override'); await waitFor(() => { expect(screen.getByTestId('no-positions')).toBeInTheDocument(); }); expect(screen.getByText('No current positions')).toBeInTheDocument(); }); }); // --------------------------------------------------------------------------- // 13. "Override Trade" button on Trading page exists and links correctly // --------------------------------------------------------------------------- describe('Override Trade button on Trading page', () => { it('renders Override Trade button that links to override tab', async () => { renderRoute('/trading'); await waitFor(() => { expect(screen.getByTestId('override-trade-button')).toBeInTheDocument(); }); const link = screen.getByTestId('override-trade-button'); expect(link).toHaveTextContent('Override Trade'); expect(link).toHaveAttribute('href', '/trading/engine?tab=override'); }); });