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