Files
stonks-oracle/frontend/src/test/override.test.tsx
T
Celes Renata 913fe8b0b3 feat: override trade tab — manual order entry with auto-registration
Backend:
- OverrideOrderRequest/Response Pydantic models with ticker, quantity, price validators
- POST /api/trading/override/order endpoint (enqueue to Redis broker queue)
- auto_register_symbol() module for untracked ticker registration via Symbol Registry
- Unit tests (17) and property-based tests (3 x 100 examples)

Frontend:
- OverrideTradePanel component (order form + positions display)
- Override tab in TradingEngine page with URL search param navigation
- Override Trade button on Trading Controls page
- useSubmitOverrideOrder mutation hook
- MSW handler and 13 component/integration tests

Steering:
- Updated steering docs for Ubuntu dev machine with nvm/Node 24
2026-04-17 07:02:30 +00:00

315 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 110 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<string, unknown>;
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');
});
});