import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'
import { render, screen, waitFor } 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 type { BrokerPortfolio } from '@/shared/api'
import { BrokerDashboard } from './BrokerDashboard'
const hookMocks = vi.hoisted(() => ({
useBrokerEvents: vi.fn(),
useBrokerOperations: vi.fn(),
}))
vi.mock('@tanstack/react-router', () => ({
Link: ({ children, to }: { children: React.ReactNode; to: string }) => (
{children}
),
}))
vi.mock('@/entities/broker-analytics', () => ({
useBrokerAnalytics: () => ({
data: {
totalDeposits: 1000,
totalWithdrawn: 100,
netInvested: 900,
totalDividends: 25,
totalCoupons: 15,
totalReceived: 40,
totalReturnPercent: 4.44,
currency: 'RUB',
},
isLoading: false,
isError: false,
}),
}))
vi.mock('@/entities/broker-event', () => ({
useBrokerEvents: hookMocks.useBrokerEvents,
}))
vi.mock('@/entities/broker-operation', () => ({
useBrokerOperations: hookMocks.useBrokerOperations,
}))
const portfolio: BrokerPortfolio = {
account: {
id: 'acc-1',
name: 'Основной счёт',
type: 'brokerage',
status: 'open',
openedAt: null,
accessLevel: null,
},
positionCounts: { shares: 2, bonds: 1, etf: 0, other: 0 },
totals: {
shares: null,
bonds: null,
etf: null,
currencies: null,
futures: null,
options: null,
structuredProducts: null,
dfa: null,
portfolio: { currency: 'RUB', units: '1000', nano: 0, value: 1000 },
},
yields: { expectedPercent: null, daily: null, dailyPercent: null },
cash: [],
blockedCash: [],
asOf: '2026-06-26T00:00:00.000Z',
}
function renderWithProviders(ui: ReactNode) {
return render({ui})
}
describe('BrokerDashboard', () => {
beforeEach(() => {
hookMocks.useBrokerEvents.mockReturnValue({
data: { items: [], summary: {}, asOf: '2026-06-26T00:00:00.000Z' },
isLoading: false,
isError: false,
})
hookMocks.useBrokerOperations.mockReturnValue({
data: {
accountId: 'acc-1',
items: [],
nextCursor: null,
hasNext: false,
asOf: '2026-06-26T00:00:00.000Z',
},
isLoading: false,
isError: false,
})
})
it('renders the dashboard sections', () => {
renderWithProviders()
expect(screen.getByText('Основной счёт')).toBeInTheDocument()
expect(screen.getByText('События')).toBeInTheDocument()
expect(screen.getByText('Доходы')).toBeInTheDocument()
expect(screen.getByText('Аналитика доходности')).toBeInTheDocument()
expect(screen.getByText('Аллокация')).toBeInTheDocument()
})
it('applies event type chips immediately to useBrokerEvents', async () => {
const user = userEvent.setup()
renderWithProviders()
const couponChips = screen.getAllByRole('button', { name: 'Купоны' })
await user.click(couponChips[0])
await waitFor(() => {
expect(hookMocks.useBrokerEvents).toHaveBeenLastCalledWith(
'acc-1',
expect.objectContaining({ types: 'dividend,maturity,offer' }),
{ enabled: true },
)
})
})
it('applies income type chips immediately to useBrokerOperations', async () => {
const user = userEvent.setup()
renderWithProviders()
const couponChips = screen.getAllByRole('button', { name: 'Купоны' })
await user.click(couponChips[1])
await waitFor(() => {
expect(hookMocks.useBrokerOperations).toHaveBeenLastCalledWith(
'acc-1',
expect.objectContaining({
operationTypes: 'OPERATION_TYPE_DIVIDEND,OPERATION_TYPE_DIV_EXT',
}),
{ enabled: true },
)
})
})
it('shows date filter toggle button with apply action and accessible name', async () => {
const user = userEvent.setup()
renderWithProviders()
const applyButtons = screen.getAllByRole('button', { name: /Применить период/ })
expect(applyButtons).toHaveLength(2)
const toggleButtons = screen.getAllByRole('button', { name: /^Период/ })
expect(toggleButtons).toHaveLength(2)
await user.click(toggleButtons[0])
expect(screen.getByText('7д')).toBeInTheDocument()
expect(screen.getByText('30д')).toBeInTheDocument()
expect(screen.getByText('90д')).toBeInTheDocument()
expect(screen.getByText('1г')).toBeInTheDocument()
expect(screen.getByText('Всё')).toBeInTheDocument()
expect(screen.getByText('Сбросить')).toBeInTheDocument()
})
it('does not render a text chevron glyph inside the period toggle button', () => {
renderWithProviders()
const toggleButtons = screen.getAllByRole('button', { name: /^Период/ })
expect(toggleButtons).toHaveLength(2)
for (const button of toggleButtons) {
const text = button.textContent ?? ''
expect(text).not.toMatch(/[▼▲vV]/)
expect(button.querySelector('svg')).not.toBeNull()
}
})
it('renders hero "Всего доходов" with the ₽ symbol and no "RUB" code', () => {
renderWithProviders()
expect(screen.getByText('Всего доходов')).toBeInTheDocument()
const hero = screen.getByLabelText('Ключевые показатели брокерского счёта')
const heroText = hero.textContent ?? ''
expect(heroText).toContain('₽')
expect(heroText).not.toContain('RUB')
})
it('shows skeleton table while events are loading', () => {
hookMocks.useBrokerEvents.mockReturnValue({
data: undefined,
isLoading: true,
isError: false,
})
renderWithProviders()
const skeletons = screen.getAllByTestId('dashboard-table-skeleton')
expect(skeletons.length).toBeGreaterThanOrEqual(1)
})
it('shows skeleton table while income operations are loading', () => {
hookMocks.useBrokerOperations.mockReturnValue({
data: undefined,
isLoading: true,
isError: false,
})
renderWithProviders()
const skeletons = screen.getAllByTestId('dashboard-table-skeleton')
expect(skeletons.length).toBeGreaterThanOrEqual(1)
})
it('does not show skeleton when data is loaded', () => {
renderWithProviders()
expect(screen.queryByTestId('dashboard-table-skeleton')).not.toBeInTheDocument()
})
})