import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import type { ReactNode } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { BrokerEventsPage } from './BrokerEventsPage' const mockSetSearchParams = vi.fn() const mockSearchParams = new URLSearchParams() vi.mock('@/entities/broker-event', () => ({ useBrokerEvents: vi.fn(), })) vi.mock('@/widgets/broker-account-layout', () => ({ useBrokerAccountContext: () => ({ accountId: 'acc-1', portfolio: null }), })) vi.mock('@/shared/lib/router/useSearchParams', () => ({ useSearchParamsCompat: () => [mockSearchParams, mockSetSearchParams], })) vi.mock('@tanstack/react-router', async () => { const actual = await vi.importActual('@tanstack/react-router') return { ...actual, useNavigate: () => vi.fn(), Link: actual.Link, Outlet: actual.Outlet, } }) vi.mock('@moex-vibe/design-system', () => ({ Button: ({ children, onClick, disabled }: any) => ( ), Checkbox: ({ label, checked, onChange }: any) => ( ), Chip: ({ label }: any) => {label}, Heading: ({ children }: { children: ReactNode }) =>

{children}

, Text: ({ children }: { children: ReactNode }) => {children}, TextField: ({ label, value, onChange, ...props }: any) => ( {})} type={props.type || 'text'} /> ), })) import { useBrokerEvents } from '@/entities/broker-event' function createWrapper() { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) return function Wrapper({ children }: { children: ReactNode }) { return {children} } } const mockData = { summary: { eventCount: 3, totalEstimatedCashflow: 450, actualCashflow: 0, forecastEstimatedCashflow: 450, nearestEventDate: '2026-06-25', dividendsTotal: 150, couponsTotal: 36.9, principalRepaymentTotal: 1000, actualDividendsTotal: 0, actualCouponsTotal: 0, actualPrincipalRepaymentTotal: 0, }, items: [ { id: 'ev-1', type: 'dividend', source: 'forecast', category: 'cashflow', ticker: 'SBER', name: 'Сбер Банк', eventDate: '2026-06-25', paymentDate: null, instrumentUid: 'uid-sber', instrumentType: 'share', quantitySnapshot: 10, payoutPerUnit: 15, estimatedAmount: 150, actualAmount: null, currency: 'RUB' as const, estimateMode: 'current_position', }, { id: 'ev-2', type: 'coupon', source: 'forecast', category: 'cashflow', ticker: 'SU26238RMFS5', name: 'ОФЗ 26238', eventDate: '2026-06-27', paymentDate: null, instrumentUid: 'uid-bond', instrumentType: 'bond', quantitySnapshot: 1, payoutPerUnit: 36.9, estimatedAmount: 36.9, actualAmount: null, currency: 'RUB' as const, estimateMode: 'current_position', }, { id: 'ev-3', type: 'maturity', source: 'actual', category: 'cashflow', ticker: 'VTBR', name: 'ВТБ', eventDate: '2026-06-30', paymentDate: '2026-06-30', instrumentUid: 'uid-vtbr', instrumentType: 'bond', quantitySnapshot: null, payoutPerUnit: null, estimatedAmount: null, actualAmount: 1000, currency: 'RUB' as const, estimateMode: null, }, ], } describe('BrokerEventsPage', () => { beforeEach(() => { vi.clearAllMocks() mockSearchParams.delete('from') mockSearchParams.delete('to') mockSearchParams.delete('types') }) it('renders loading state', () => { vi.mocked(useBrokerEvents).mockReturnValue({ data: undefined, isLoading: true, isError: false, error: null, isSuccess: false, isPending: true, dataUpdatedAt: 0, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isFetched: false, isFetchedAfterMount: false, isFetching: true, isInitialLoading: true, isPaused: false, isLoadingError: false, isRefetchError: false, isPlaceholderData: false, isStale: false, refetch: vi.fn(), promise: new Promise(() => {}), status: 'pending', fetchStatus: 'fetching', } as unknown as ReturnType) render(, { wrapper: createWrapper() }) expect(screen.getByText('Загрузка событий…')).toBeInTheDocument() }) it('renders error state', () => { vi.mocked(useBrokerEvents).mockReturnValue({ data: undefined, isLoading: false, isError: true, error: new Error('fail'), isSuccess: false, isPending: false, dataUpdatedAt: 0, errorUpdatedAt: 0, failureCount: 1, failureReason: null, errorUpdateCount: 1, isFetched: true, isFetchedAfterMount: true, isFetching: false, isInitialLoading: false, isPaused: false, isLoadingError: true, isRefetchError: false, isPlaceholderData: false, isStale: false, refetch: vi.fn(), promise: new Promise(() => {}), status: 'error', fetchStatus: 'idle', } as unknown as ReturnType) render(, { wrapper: createWrapper() }) expect(screen.getByText('Не удалось загрузить календарь событий')).toBeInTheDocument() }) it('renders empty state', () => { vi.mocked(useBrokerEvents).mockReturnValue({ data: { summary: { eventCount: 0, totalEstimatedCashflow: 0, actualCashflow: 0, forecastEstimatedCashflow: 0, nearestEventDate: null, dividendsTotal: 0, couponsTotal: 0, principalRepaymentTotal: 0, actualDividendsTotal: 0, actualCouponsTotal: 0, actualPrincipalRepaymentTotal: 0, }, items: [], }, isLoading: false, isError: false, error: null, isSuccess: true, isPending: false, dataUpdatedAt: Date.now(), errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isFetched: true, isFetchedAfterMount: true, isFetching: false, isInitialLoading: false, isPaused: false, isLoadingError: false, isRefetchError: false, isPlaceholderData: false, isStale: false, refetch: vi.fn(), promise: Promise.resolve({ summary: { eventCount: 0, totalEstimatedCashflow: 0, actualCashflow: 0, forecastEstimatedCashflow: 0, nearestEventDate: null, dividendsTotal: 0, couponsTotal: 0, principalRepaymentTotal: 0, actualDividendsTotal: 0, actualCouponsTotal: 0, actualPrincipalRepaymentTotal: 0, }, items: [], }), status: 'success', fetchStatus: 'idle', } as unknown as ReturnType) render(, { wrapper: createWrapper() }) expect(screen.getByText('В выбранном диапазоне событий нет')).toBeInTheDocument() }) it('renders date range inputs', () => { vi.mocked(useBrokerEvents).mockReturnValue({ data: mockData, isLoading: false, isError: false, error: null, isSuccess: true, isPending: false, dataUpdatedAt: Date.now(), errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isFetched: true, isFetchedAfterMount: true, isFetching: false, isInitialLoading: false, isPaused: false, isLoadingError: false, isRefetchError: false, isPlaceholderData: false, isStale: false, refetch: vi.fn(), promise: Promise.resolve(mockData), status: 'success', fetchStatus: 'idle', } as unknown as ReturnType) render(, { wrapper: createWrapper() }) expect(screen.getByLabelText('С')).toBeInTheDocument() expect(screen.getByLabelText('По')).toBeInTheDocument() }) it('renders events heading, summary and table', () => { vi.mocked(useBrokerEvents).mockReturnValue({ data: mockData, isLoading: false, isError: false, error: null, isSuccess: true, isPending: false, dataUpdatedAt: Date.now(), errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isFetched: true, isFetchedAfterMount: true, isFetching: false, isInitialLoading: false, isPaused: false, isLoadingError: false, isRefetchError: false, isPlaceholderData: false, isStale: false, refetch: vi.fn(), promise: Promise.resolve(mockData), status: 'success', fetchStatus: 'idle', } as unknown as ReturnType) render(, { wrapper: createWrapper() }) expect(screen.getByText('События')).toBeInTheDocument() expect(screen.getByText('Событий')).toBeInTheDocument() expect(screen.getByText('3')).toBeInTheDocument() expect(screen.getByText('Ближайшее')).toBeInTheDocument() expect(screen.getByText('Прогноз выплат')).toBeInTheDocument() expect(screen.getByText('Дивиденд')).toBeInTheDocument() expect(screen.getByText('Купон')).toBeInTheDocument() expect(screen.getByText('Погашение')).toBeInTheDocument() expect(screen.getByText('SBER')).toBeInTheDocument() expect(screen.getByText('SU26238RMFS5')).toBeInTheDocument() expect(screen.getByText('VTBR')).toBeInTheDocument() expect(screen.getByText('Прогноз выплат')).toBeInTheDocument() expect(screen.getAllByText('Поступило').length).toBeGreaterThan(0) expect(screen.getByText('Факт')).toBeInTheDocument() expect(screen.getAllByText('Прогноз').length).toBeGreaterThan(0) }) it('renders corporate events in a separate muted section', () => { const dataWithOffer = { ...mockData, summary: { ...mockData.summary, eventCount: 4 }, items: [ ...mockData.items, { id: 'ev-4', type: 'offer', source: 'forecast', category: 'corporate', ticker: 'SU26238RMFS5', name: 'ОФЗ 26238', eventDate: '2026-07-05', paymentDate: null, instrumentUid: 'uid-bond-2', instrumentType: 'bond', quantitySnapshot: 1, payoutPerUnit: null, estimatedAmount: null, actualAmount: null, currency: null, estimateMode: null, }, ], } vi.mocked(useBrokerEvents).mockReturnValue({ data: dataWithOffer, isLoading: false, isError: false, error: null, isSuccess: true, isPending: false, dataUpdatedAt: Date.now(), errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isFetched: true, isFetchedAfterMount: true, isFetching: false, isInitialLoading: false, isPaused: false, isLoadingError: false, isRefetchError: false, isPlaceholderData: false, isStale: false, refetch: vi.fn(), promise: Promise.resolve(dataWithOffer), status: 'success', fetchStatus: 'idle', } as unknown as ReturnType) render(, { wrapper: createWrapper() }) expect(screen.getByText('Корпоративные события')).toBeInTheDocument() expect(screen.getByText('Оферта')).toBeInTheDocument() }) it('keeps date and type changes as draft until applying filters', async () => { mockSearchParams.set('from', '2026-06-15') mockSearchParams.set('to', '2026-06-29') mockSearchParams.set('types', 'dividend,coupon') vi.mocked(useBrokerEvents).mockReturnValue({ data: mockData, isLoading: false, isError: false, error: null, isSuccess: true, isPending: false, dataUpdatedAt: Date.now(), errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isFetched: true, isFetchedAfterMount: true, isFetching: false, isInitialLoading: false, isPaused: false, isLoadingError: false, isRefetchError: false, isPlaceholderData: false, isStale: false, refetch: vi.fn(), promise: Promise.resolve(mockData), status: 'success', fetchStatus: 'idle', } as unknown as ReturnType) render(, { wrapper: createWrapper() }) await userEvent.clear(screen.getByLabelText('С')) await userEvent.type(screen.getByLabelText('С'), '2026-06-10') await userEvent.click(screen.getByLabelText('Купоны')) expect(mockSetSearchParams).not.toHaveBeenCalled() await userEvent.click(screen.getByRole('button', { name: 'Показать' })) const applied = mockSetSearchParams.mock.calls[0][0] as URLSearchParams expect(applied.get('from')).toBe('2026-06-10') expect(applied.get('to')).toBe('2026-06-29') expect(applied.get('types')).toBe('dividend') }) })