// @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(); 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(); 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(); await waitFor(() => expect(screen.queryByText(/MANUAL/i)).toBeInTheDocument()); }); });