168 lines
5.8 KiB
TypeScript
168 lines
5.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { HistoryTab } from './HistoryTab';
|
|
|
|
const {
|
|
authState,
|
|
fetchTradeProfilesMock,
|
|
fetchTradeHistoryMock,
|
|
fetchPositionsBootstrapMock
|
|
} = vi.hoisted(() => ({
|
|
authState: {
|
|
user: { id: 'u1' },
|
|
profile: { role: 'user' },
|
|
refreshProfile: vi.fn(),
|
|
session: { access_token: 'session-token' }
|
|
},
|
|
fetchTradeProfilesMock: vi.fn(),
|
|
fetchTradeHistoryMock: vi.fn(),
|
|
fetchPositionsBootstrapMock: vi.fn()
|
|
}));
|
|
|
|
vi.mock('../components/AuthContext', () => ({
|
|
useAuth: () => authState
|
|
}));
|
|
|
|
vi.mock('../lib/profileApi', () => ({
|
|
fetchTradeProfiles: fetchTradeProfilesMock
|
|
}));
|
|
|
|
vi.mock('../lib/tradeHistoryApi', () => ({
|
|
fetchTradeHistory: fetchTradeHistoryMock
|
|
}));
|
|
|
|
vi.mock('../lib/positionsApi', () => ({
|
|
fetchPositionsBootstrap: fetchPositionsBootstrapMock
|
|
}));
|
|
|
|
vi.mock('../hooks/useCanonicalLifecycle', () => ({
|
|
useCanonicalLifecycle: () => ({
|
|
snapshot: {
|
|
lifecycleRows: [{ tradeId: 'TRD-1' }],
|
|
realizedTrades: [],
|
|
openPositions: [],
|
|
diagnostics: { truncated: false }
|
|
},
|
|
loading: false,
|
|
error: null
|
|
})
|
|
}));
|
|
|
|
describe('HistoryTab Master Suite', () => {
|
|
const historyData = [
|
|
{ id: 'h1', symbol: 'BTC', entry_price: 50000, exit_price: 51000, pnl: 1000, pnl_percent: 2, created_at: '2024-01-01T10:00:00Z', side: 'BUY', source: 'BOT', profile_id: 'p1', size: 1, reason: 'Trend' }
|
|
];
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
authState.user = { id: 'u1' };
|
|
authState.profile = { role: 'user' };
|
|
authState.session = { access_token: 'session-token' };
|
|
fetchTradeProfilesMock.mockResolvedValue([{ id: 'p1', name: 'Alpha', allocated_capital: 1000 }]);
|
|
fetchTradeHistoryMock.mockResolvedValue(historyData);
|
|
fetchPositionsBootstrapMock.mockResolvedValue({
|
|
entries: [],
|
|
orders: [{
|
|
id: 'o1',
|
|
order_id: 'o1',
|
|
profile_id: 'p1',
|
|
symbol: 'BTC',
|
|
side: 'BUY',
|
|
qty: 1,
|
|
price: 50000,
|
|
status: 'filled',
|
|
timestamp: '2024-01-01T09:00:00Z',
|
|
trade_id: 'TRD-1',
|
|
action: 'ENTRY',
|
|
source: 'BOT'
|
|
}],
|
|
historyTradeKeys: [{ trade_id: 'TRD-1', profile_id: 'p1' }],
|
|
profiles: [{ id: 'p1', name: 'Alpha' }]
|
|
});
|
|
});
|
|
|
|
it('processes metrics and handles sort toggles', async () => {
|
|
const user = userEvent.setup();
|
|
render(<HistoryTab />);
|
|
|
|
await waitFor(() => expect(screen.getByText(/Total Trades/i)).toBeInTheDocument());
|
|
await waitFor(() => expect(screen.getByText('BTC')).toBeInTheDocument());
|
|
|
|
const sortBtn = await screen.findByRole('button', { name: /Newest/i });
|
|
await user.click(sortBtn);
|
|
await waitFor(() => expect(screen.queryByText(/Oldest/i)).toBeInTheDocument());
|
|
|
|
await user.click(screen.getByText(/Oldest/i));
|
|
await waitFor(() => expect(screen.queryByText(/Newest/i)).toBeInTheDocument());
|
|
});
|
|
|
|
it('covers date filtering and pagination', async () => {
|
|
const user = userEvent.setup();
|
|
const manyRecords = Array.from({ length: 25 }, (_, i) => ({
|
|
id: `h${i}`,
|
|
symbol: 'BTC',
|
|
entry_price: 50000,
|
|
exit_price: 51000,
|
|
pnl: 1000,
|
|
pnl_percent: 2,
|
|
created_at: `2024-01-01T12:${String(i % 60).padStart(2, '0')}:00`,
|
|
side: 'BUY',
|
|
source: 'BOT',
|
|
profile_id: 'p1',
|
|
size: 1,
|
|
reason: 'Trend',
|
|
trade_id: `TRD-${i}`
|
|
}));
|
|
|
|
fetchTradeHistoryMock.mockResolvedValue(manyRecords);
|
|
fetchPositionsBootstrapMock.mockResolvedValue({
|
|
entries: [],
|
|
orders: manyRecords.map((row) => ({
|
|
id: row.id,
|
|
order_id: row.id,
|
|
profile_id: 'p1',
|
|
symbol: row.symbol,
|
|
side: row.side,
|
|
qty: row.size,
|
|
price: row.entry_price,
|
|
status: 'filled',
|
|
timestamp: row.created_at,
|
|
trade_id: row.trade_id,
|
|
action: 'ENTRY',
|
|
source: 'BOT'
|
|
})),
|
|
historyTradeKeys: manyRecords.map((row) => ({ trade_id: row.trade_id, profile_id: 'p1' })),
|
|
profiles: [{ id: 'p1', name: 'Alpha' }]
|
|
});
|
|
|
|
const { container } = render(<HistoryTab />);
|
|
await screen.findByText(/Page 1 \/ 2/i, {}, { timeout: 5000 });
|
|
|
|
const dateInputs = container.querySelectorAll('input[type="date"]');
|
|
if (dateInputs.length >= 2) {
|
|
fireEvent.change(dateInputs[0], { target: { value: '2024-01-01' } });
|
|
fireEvent.change(dateInputs[1], { target: { value: '2024-01-02' } });
|
|
}
|
|
|
|
const nextBtn = await screen.findByRole('button', { name: /Next/i });
|
|
await user.click(nextBtn);
|
|
await screen.findByText(/Page 2 \/ 2/i);
|
|
|
|
const prevBtn = await screen.findByRole('button', { name: /Prev/i });
|
|
await user.click(prevBtn);
|
|
await screen.findByText(/Page 1 \/ 2/i);
|
|
|
|
await user.click(screen.getByText(/Clear/i));
|
|
});
|
|
|
|
it('covers empty fallback and source mapping', async () => {
|
|
fetchTradeHistoryMock.mockResolvedValue([
|
|
{ ...historyData[0], source: 'MANUAL', profile_id: null }
|
|
]);
|
|
render(<HistoryTab />);
|
|
await waitFor(() => expect(screen.queryByText(/MANUAL/i)).toBeInTheDocument());
|
|
});
|
|
});
|