271 lines
7.1 KiB
Markdown
271 lines
7.1 KiB
Markdown
|
|
# Frontend Testing Guide
|
||
|
|
|
||
|
|
This guide explains how to test the React frontend components and pages.
|
||
|
|
|
||
|
|
## Testing Setup
|
||
|
|
|
||
|
|
The frontend uses Vitest (recommended) or Jest for testing React components. To set up testing:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
cd frontend
|
||
|
|
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event vitest @vitest/ui jsdom
|
||
|
|
```
|
||
|
|
|
||
|
|
Add to `package.json`:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"scripts": {
|
||
|
|
"test": "vitest",
|
||
|
|
"test:ui": "vitest --ui",
|
||
|
|
"test:coverage": "vitest --coverage"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Testing Strategy
|
||
|
|
|
||
|
|
### Component Testing
|
||
|
|
|
||
|
|
Test individual components in isolation:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// frontend/src/components/__tests__/StatusIndicator.test.tsx
|
||
|
|
import { render, screen } from '@testing-library/react';
|
||
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
import StatusIndicator from '../StatusIndicator';
|
||
|
|
|
||
|
|
describe('StatusIndicator', () => {
|
||
|
|
it('renders connected status correctly', () => {
|
||
|
|
render(<StatusIndicator status="connected" label="WebSocket" />);
|
||
|
|
expect(screen.getByText('WebSocket')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows correct color for error status', () => {
|
||
|
|
render(<StatusIndicator status="error" label="Error" />);
|
||
|
|
const chip = screen.getByText('Error');
|
||
|
|
expect(chip).toHaveClass('MuiChip-colorError');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Page Testing
|
||
|
|
|
||
|
|
Test complete page workflows:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// frontend/src/pages/__tests__/StrategiesPage.test.tsx
|
||
|
|
import { render, screen, waitFor } from '@testing-library/react';
|
||
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||
|
|
import { describe, it, expect, vi } from 'vitest';
|
||
|
|
import StrategiesPage from '../StrategiesPage';
|
||
|
|
import * as strategiesApi from '../../api/strategies';
|
||
|
|
|
||
|
|
vi.mock('../../api/strategies');
|
||
|
|
|
||
|
|
describe('StrategiesPage', () => {
|
||
|
|
const queryClient = new QueryClient({
|
||
|
|
defaultOptions: { queries: { retry: false } }
|
||
|
|
});
|
||
|
|
|
||
|
|
it('displays list of strategies', async () => {
|
||
|
|
const mockStrategies = [
|
||
|
|
{
|
||
|
|
id: 1,
|
||
|
|
name: 'Test Strategy',
|
||
|
|
strategy_type: 'rsi',
|
||
|
|
enabled: false,
|
||
|
|
paper_trading: true,
|
||
|
|
// ... other fields
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
vi.mocked(strategiesApi.strategiesApi.listStrategies).mockResolvedValue(mockStrategies);
|
||
|
|
|
||
|
|
render(
|
||
|
|
<QueryClientProvider client={queryClient}>
|
||
|
|
<StrategiesPage />
|
||
|
|
</QueryClientProvider>
|
||
|
|
);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Test Strategy')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows create strategy button', () => {
|
||
|
|
render(
|
||
|
|
<QueryClientProvider client={queryClient}>
|
||
|
|
<StrategiesPage />
|
||
|
|
</QueryClientProvider>
|
||
|
|
);
|
||
|
|
expect(screen.getByText('Create Strategy')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### API Integration Testing
|
||
|
|
|
||
|
|
Test API client functions:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// frontend/src/api/__tests__/strategies.test.ts
|
||
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
|
|
import { strategiesApi } from '../strategies';
|
||
|
|
import { apiClient } from '../client';
|
||
|
|
|
||
|
|
vi.mock('../client');
|
||
|
|
|
||
|
|
describe('strategiesApi', () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('lists strategies', async () => {
|
||
|
|
const mockStrategies = [{ id: 1, name: 'Test' }];
|
||
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockStrategies });
|
||
|
|
|
||
|
|
const result = await strategiesApi.listStrategies();
|
||
|
|
expect(result).toEqual(mockStrategies);
|
||
|
|
expect(apiClient.get).toHaveBeenCalledWith('/api/strategies/');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('creates strategy', async () => {
|
||
|
|
const newStrategy = { name: 'New', strategy_type: 'rsi' };
|
||
|
|
const created = { id: 1, ...newStrategy };
|
||
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: created });
|
||
|
|
|
||
|
|
const result = await strategiesApi.createStrategy(newStrategy);
|
||
|
|
expect(result).toEqual(created);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Hook Testing
|
||
|
|
|
||
|
|
Test custom React hooks:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// frontend/src/hooks/__tests__/useWebSocket.test.ts
|
||
|
|
import { renderHook, waitFor } from '@testing-library/react';
|
||
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
|
|
import { useWebSocket } from '../useWebSocket';
|
||
|
|
|
||
|
|
// Mock WebSocket
|
||
|
|
global.WebSocket = vi.fn(() => ({
|
||
|
|
addEventListener: vi.fn(),
|
||
|
|
removeEventListener: vi.fn(),
|
||
|
|
send: vi.fn(),
|
||
|
|
close: vi.fn(),
|
||
|
|
readyState: WebSocket.OPEN,
|
||
|
|
})) as any;
|
||
|
|
|
||
|
|
describe('useWebSocket', () => {
|
||
|
|
it('connects to WebSocket', async () => {
|
||
|
|
const { result } = renderHook(() => useWebSocket('ws://localhost:8000/ws/'));
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(result.current.isConnected).toBe(true);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles messages', async () => {
|
||
|
|
const { result } = renderHook(() => useWebSocket('ws://localhost:8000/ws/'));
|
||
|
|
|
||
|
|
// Simulate message
|
||
|
|
const ws = (global.WebSocket as any).mock.results[0].value;
|
||
|
|
const messageEvent = new MessageEvent('message', {
|
||
|
|
data: JSON.stringify({ type: 'order_update', order_id: 1 })
|
||
|
|
});
|
||
|
|
ws.onmessage(messageEvent);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(result.current.lastMessage).toBeTruthy();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Test Coverage
|
||
|
|
|
||
|
|
### Components to Test
|
||
|
|
|
||
|
|
1. **Pages**:
|
||
|
|
- `StrategiesPage` - CRUD operations, start/stop
|
||
|
|
- `TradingPage` - Order placement, position closing
|
||
|
|
- `DashboardPage` - AutoPilot controls, system health
|
||
|
|
- `PortfolioPage` - Position management, charts
|
||
|
|
- `BacktestPage` - Backtest execution, results
|
||
|
|
- `SettingsPage` - All settings tabs
|
||
|
|
|
||
|
|
2. **Components**:
|
||
|
|
- `StrategyDialog` - Form validation, parameter configuration
|
||
|
|
- `OrderForm` - Order type handling, validation
|
||
|
|
- `PositionCard` - Position display, close functionality
|
||
|
|
- `StatusIndicator` - Status display
|
||
|
|
- `SystemHealth` - Health status aggregation
|
||
|
|
- `DataFreshness` - Timestamp calculations
|
||
|
|
- `OperationsPanel` - Operation display
|
||
|
|
|
||
|
|
3. **Hooks**:
|
||
|
|
- `useWebSocket` - Connection, messages, subscriptions
|
||
|
|
- `useRealtimeData` - Query invalidation
|
||
|
|
|
||
|
|
4. **Contexts**:
|
||
|
|
- `SnackbarContext` - Notification display
|
||
|
|
- `WebSocketProvider` - WebSocket management
|
||
|
|
|
||
|
|
## Running Tests
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Run all tests
|
||
|
|
npm test
|
||
|
|
|
||
|
|
# Run tests in watch mode
|
||
|
|
npm test -- --watch
|
||
|
|
|
||
|
|
# Run tests with UI
|
||
|
|
npm run test:ui
|
||
|
|
|
||
|
|
# Generate coverage report
|
||
|
|
npm run test:coverage
|
||
|
|
```
|
||
|
|
|
||
|
|
## Best Practices
|
||
|
|
|
||
|
|
1. **Mock External Dependencies**: Mock API calls, WebSocket connections
|
||
|
|
2. **Test User Interactions**: Use `@testing-library/user-event` for clicks, typing
|
||
|
|
3. **Test Error States**: Verify error handling and display
|
||
|
|
4. **Test Loading States**: Ensure loading indicators appear
|
||
|
|
5. **Test Accessibility**: Use `@testing-library/jest-dom` matchers
|
||
|
|
6. **Keep Tests Fast**: Mock expensive operations
|
||
|
|
7. **Test Edge Cases**: Empty states, error conditions, boundary values
|
||
|
|
|
||
|
|
## Example Test Suite Structure
|
||
|
|
|
||
|
|
```
|
||
|
|
frontend/src/
|
||
|
|
├── components/
|
||
|
|
│ ├── __tests__/
|
||
|
|
│ │ ├── StatusIndicator.test.tsx
|
||
|
|
│ │ ├── StrategyDialog.test.tsx
|
||
|
|
│ │ └── ...
|
||
|
|
│ └── ...
|
||
|
|
├── pages/
|
||
|
|
│ ├── __tests__/
|
||
|
|
│ │ ├── StrategiesPage.test.tsx
|
||
|
|
│ │ ├── TradingPage.test.tsx
|
||
|
|
│ │ └── ...
|
||
|
|
│ └── ...
|
||
|
|
├── hooks/
|
||
|
|
│ ├── __tests__/
|
||
|
|
│ │ ├── useWebSocket.test.ts
|
||
|
|
│ │ └── ...
|
||
|
|
│ └── ...
|
||
|
|
└── api/
|
||
|
|
├── __tests__/
|
||
|
|
│ ├── strategies.test.ts
|
||
|
|
│ └── ...
|
||
|
|
└── ...
|
||
|
|
```
|
||
|
|
|