import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { type ReactElement } from 'react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { describe, expect, it, vi } from 'vitest'; import * as accountHook from '../../hooks/useBrokerAccounts'; import * as operationsHook from '../../hooks/useBrokerOperations'; import * as portfolioHook from '../../hooks/useBrokerPortfolio'; import { BrokerAccountDetailPage } from './BrokerAccountDetailPage'; import { BrokerAccountsPage } from './BrokerAccountsPage'; function renderWithClient(ui: ReactElement, initialEntries = ['/broker']) { const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); return render( {ui} , ); } describe('Broker pages', () => { it('renders broker and IIS accounts', () => { vi.spyOn(accountHook, 'useBrokerAccounts').mockReturnValue({ data: [ { id: 'acc-1', type: 'brokerage', name: 'Broker', status: 'ACCOUNT_STATUS_OPEN', openedAt: null, accessLevel: null, }, { id: 'acc-2', type: 'iis', name: 'IIS', status: 'ACCOUNT_STATUS_OPEN', openedAt: null, accessLevel: null, }, ], isLoading: false, error: null, } as any); renderWithClient(); expect(screen.getByText('Broker')).toBeInTheDocument(); expect(screen.getByText('IIS')).toBeInTheDocument(); }); it('renders positions and operations for account detail', () => { vi.spyOn(portfolioHook, 'useBrokerPortfolio').mockReturnValue({ data: { account: { id: 'acc-1', type: 'brokerage', name: 'Broker', status: 'ACCOUNT_STATUS_OPEN', openedAt: null, accessLevel: null, }, totals: { portfolio: { currency: 'RUB', units: '1000', nano: 0, value: 1000 } }, yields: { expectedPercent: 5, daily: null, dailyPercent: null }, cash: [{ currency: 'RUB', units: '100', nano: 0, value: 100 }], blockedCash: [], positions: [ { figi: null, instrumentUid: 'uid-1', positionUid: null, ticker: 'SBER', classCode: 'TQBR', instrumentType: 'share', name: 'Sberbank', quantity: 10, blockedLots: null, currentPrice: null, currentValue: { currency: 'RUB', units: '1000', nano: 0, value: 1000 }, averagePositionPrice: null, expectedYieldPercent: null, dailyYield: null, }, ], asOf: '2026-06-16T00:00:00.000Z', }, isLoading: false, error: null, } as any); vi.spyOn(operationsHook, 'useBrokerOperations').mockReturnValue({ data: { accountId: 'acc-1', items: [ { cursor: 'cursor-1', accountId: 'acc-1', id: 'op-1', parentOperationId: null, date: '2026-06-16T00:00:00.000Z', category: 'trade', type: 'OPERATION_TYPE_BUY', description: 'Buy', state: 'OPERATION_STATE_EXECUTED', instrumentUid: 'uid-1', figi: null, ticker: 'SBER', classCode: 'TQBR', instrumentType: 'share', payment: { currency: 'RUB', units: '-1000', nano: 0, value: -1000 }, price: null, commission: null, yield: null, accruedInt: null, quantity: 10, quantityDone: 10, }, ], nextCursor: null, hasNext: false, asOf: '2026-06-16T00:00:00.000Z', }, isLoading: false, error: null, } as any); renderWithClient( } /> , ['/broker/acc-1'], ); expect(screen.getAllByText('SBER').length).toBeGreaterThan(0); expect(screen.getByText('Покупка')).toBeInTheDocument(); }); it('renders broker positions as separate linked stock and bond tables with current price', () => { vi.spyOn(portfolioHook, 'useBrokerPortfolio').mockReturnValue({ data: { account: { id: 'acc-1', type: 'brokerage', name: 'Broker', status: 'ACCOUNT_STATUS_OPEN', openedAt: null, accessLevel: null, }, totals: { portfolio: { currency: 'RUB', units: '10000', nano: 0, value: 10000 } }, yields: { expectedPercent: 5, daily: null, dailyPercent: null }, cash: [], blockedCash: [], positions: [ { figi: null, instrumentUid: 'share-uid', positionUid: null, ticker: 'SBER', classCode: 'TQBR', instrumentType: 'share', name: 'Sberbank', quantity: 10, blockedLots: null, currentPrice: { currency: 'RUB', units: '250', nano: 0, value: 250 }, currentValue: { currency: 'RUB', units: '2500', nano: 0, value: 2500 }, averagePositionPrice: null, expectedYieldPercent: 20, dailyYield: null, }, { figi: null, instrumentUid: 'bond-uid', positionUid: null, ticker: 'SU26238RMFS5', classCode: 'TQOB', instrumentType: 'bond', name: 'ОФЗ 26238', quantity: 2, blockedLots: null, currentPrice: { currency: 'RUB', units: '900', nano: 0, value: 900 }, currentValue: { currency: 'RUB', units: '1800', nano: 0, value: 1800 }, averagePositionPrice: null, expectedYieldPercent: 10, dailyYield: null, }, ], asOf: '2026-06-17T00:00:00.000Z', }, isLoading: false, error: null, } as any); vi.spyOn(operationsHook, 'useBrokerOperations').mockReturnValue({ data: { accountId: 'acc-1', items: [], nextCursor: null, hasNext: false, asOf: '2026-06-17T00:00:00.000Z', }, isLoading: false, error: null, } as any); renderWithClient( } /> , ['/broker/acc-1'], ); expect(screen.getByRole('heading', { name: 'Акции' })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: 'Облигации' })).toBeInTheDocument(); expect(screen.queryByRole('columnheader', { name: 'Доходность' })).not.toBeInTheDocument(); expect(screen.getAllByRole('columnheader', { name: 'Цена' })).toHaveLength(2); expect(screen.getByRole('link', { name: 'SBER' })).toHaveAttribute('href', '/stocks/SBER'); expect(screen.getByRole('link', { name: 'SU26238RMFS5' })).toHaveAttribute( 'href', '/bonds/SU26238RMFS5', ); expect(screen.getByText(/250,00/)).toBeInTheDocument(); expect(screen.getByText(/900,00/)).toBeInTheDocument(); }); it('renders broker operations with Russian labels, linked instruments and colored amounts', () => { vi.spyOn(portfolioHook, 'useBrokerPortfolio').mockReturnValue({ data: { account: { id: 'acc-1', type: 'brokerage', name: 'Broker', status: 'ACCOUNT_STATUS_OPEN', openedAt: null, accessLevel: null, }, totals: { portfolio: { currency: 'RUB', units: '10000', nano: 0, value: 10000 } }, yields: { expectedPercent: null, daily: null, dailyPercent: null }, cash: [], blockedCash: [], positions: [], asOf: '2026-06-17T00:00:00.000Z', }, isLoading: false, error: null, } as any); vi.spyOn(operationsHook, 'useBrokerOperations').mockReturnValue({ data: { accountId: 'acc-1', items: [ { cursor: 'op-1', accountId: 'acc-1', id: 'op-1', parentOperationId: null, date: '2026-06-17T10:00:00.000Z', category: 'income', type: 'OPERATION_TYPE_COUPON', description: 'Coupon', state: 'OPERATION_STATE_EXECUTED', instrumentUid: 'bond-uid', figi: null, ticker: 'SU26238RMFS5', classCode: 'TQOB', instrumentType: 'bond', payment: { currency: 'RUB', units: '120', nano: 0, value: 120 }, price: null, commission: null, yield: null, accruedInt: null, quantity: 2, quantityDone: 2, }, { cursor: 'op-2', accountId: 'acc-1', id: 'op-2', parentOperationId: null, date: '2026-06-17T11:00:00.000Z', category: 'tax', type: 'OPERATION_TYPE_TAX', description: 'Tax', state: 'OPERATION_STATE_EXECUTED', instrumentUid: null, figi: null, ticker: null, classCode: null, instrumentType: null, payment: { currency: 'RUB', units: '-13', nano: 0, value: -13 }, price: null, commission: null, yield: null, accruedInt: null, quantity: null, quantityDone: null, }, ], nextCursor: null, hasNext: false, asOf: '2026-06-17T00:00:00.000Z', }, isLoading: false, error: null, } as any); renderWithClient( } /> , ['/broker/acc-1'], ); expect(screen.getByText('Выплата купона')).toBeInTheDocument(); expect(screen.getByText('Налог')).toBeInTheDocument(); expect(screen.getByText(/\+120,00\s*₽/)).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'SU26238RMFS5' })).toHaveAttribute( 'href', '/bonds/SU26238RMFS5', ); }); it('requests broker operations by cursor with a page size of 10', async () => { const user = userEvent.setup(); vi.spyOn(portfolioHook, 'useBrokerPortfolio').mockReturnValue({ data: { account: { id: 'acc-1', type: 'brokerage', name: 'Broker', status: 'ACCOUNT_STATUS_OPEN', openedAt: null, accessLevel: null, }, totals: { portfolio: { currency: 'RUB', units: '10000', nano: 0, value: 10000 } }, yields: { expectedPercent: null, daily: null, dailyPercent: null }, cash: [], blockedCash: [], positions: [], asOf: '2026-06-17T00:00:00.000Z', }, isLoading: false, error: null, } as any); const operationsSpy = vi.spyOn(operationsHook, 'useBrokerOperations').mockImplementation( (_accountId, query) => ({ data: query.cursor === 'cursor-page-2' ? { accountId: 'acc-1', items: [ { cursor: 'op-page-2', accountId: 'acc-1', id: 'op-page-2', parentOperationId: null, date: '2026-06-17T12:00:00.000Z', category: 'trade', type: 'OPERATION_TYPE_SELL', description: 'Sell', state: 'OPERATION_STATE_EXECUTED', instrumentUid: 'share-uid', figi: null, ticker: 'SBER', classCode: 'TQBR', instrumentType: 'share', payment: { currency: 'RUB', units: '1000', nano: 0, value: 1000 }, price: null, commission: null, yield: null, accruedInt: null, quantity: 1, quantityDone: 1, }, ], nextCursor: null, hasNext: false, asOf: '2026-06-17T00:00:00.000Z', } : { accountId: 'acc-1', items: [ { cursor: 'op-page-1', accountId: 'acc-1', id: 'op-page-1', parentOperationId: null, date: '2026-06-17T10:00:00.000Z', category: 'trade', type: 'OPERATION_TYPE_BUY', description: 'Buy', state: 'OPERATION_STATE_EXECUTED', instrumentUid: 'share-uid', figi: null, ticker: 'SBER', classCode: 'TQBR', instrumentType: 'share', payment: { currency: 'RUB', units: '-1000', nano: 0, value: -1000 }, price: null, commission: null, yield: null, accruedInt: null, quantity: 1, quantityDone: 1, }, ], nextCursor: 'cursor-page-2', hasNext: true, asOf: '2026-06-17T00:00:00.000Z', }, isLoading: false, error: null, }) as any, ); renderWithClient( } /> , ['/broker/acc-1'], ); expect(operationsSpy).toHaveBeenLastCalledWith('acc-1', { limit: 10, cursor: undefined }); // Find pagination buttons by their text content (← and →) const nextButton = screen.getByRole('button', { name: '→' }); const prevButton = screen.getByRole('button', { name: '←' }); expect(prevButton).toBeDisabled(); expect(nextButton).not.toBeDisabled(); await user.click(nextButton); expect(operationsSpy).toHaveBeenLastCalledWith('acc-1', { limit: 10, cursor: 'cursor-page-2', }); // Page number is shown as just a number (without "Страница" label) expect(screen.getByText('2')).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: '←' })); expect(operationsSpy).toHaveBeenLastCalledWith('acc-1', { limit: 10, cursor: undefined }); expect(screen.getByText('1')).toBeInTheDocument(); }); });