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')
})
})