215 lines
7.0 KiB
TypeScript
215 lines
7.0 KiB
TypeScript
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 }) => (
|
||
<a href={to}>{children}</a>
|
||
),
|
||
}))
|
||
|
||
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(<LocalizationProvider dateAdapter={AdapterDayjs}>{ui}</LocalizationProvider>)
|
||
}
|
||
|
||
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(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||
|
||
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(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||
|
||
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(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||
|
||
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(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||
|
||
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(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||
|
||
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(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||
|
||
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(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||
|
||
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(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||
|
||
const skeletons = screen.getAllByTestId('dashboard-table-skeleton')
|
||
expect(skeletons.length).toBeGreaterThanOrEqual(1)
|
||
})
|
||
|
||
it('does not show skeleton when data is loaded', () => {
|
||
renderWithProviders(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||
|
||
expect(screen.queryByTestId('dashboard-table-skeleton')).not.toBeInTheDocument()
|
||
})
|
||
})
|