import { MantineProvider } from "@mantine/core"; import { render, screen, within } from "@testing-library/react"; import { MemoryRouter } from "react-router"; import { describe, expect, it } from "vitest"; import { resource } from "@/test/resource"; import type { BrokerageAccount } from "../../api/useBrokerageAccounts"; import { brokerageAccountCards } from "../../model/display"; import { BrokerageAccountList } from "./BrokerageAccountList"; type PortfolioSummary = NonNullable; const portfolioSummary = { bondPositionCount: 12, cashBalances: [ { currency: "RUB", value: "82500.5" }, { currency: "USD", value: "20" }, ], fundPositionCount: 0, stockPositionCount: 5, structure: { currencies: [{ categories: [ { sharePercent: "50.000000", type: "STOCK", value: "100" }, { sharePercent: "30.000000", type: "BOND", value: "60" }, { sharePercent: "0.000000", type: "OTHER", value: "0" }, { sharePercent: "20.000000", type: "CASH", value: "40" }, ], currency: "RUB", hasNegativeValues: false, totalValue: "200", }], unvaluedCategories: [], }, totalPositionCount: 17, totalsByCurrency: [ { currency: "RUB", value: "123456789012345678901.995" }, { currency: "USD", value: "20" }, { currency: "EUR", value: "5" }, ], unrealizedReturnByCurrency: [ { absolute: "136900", costBasis: "1000000", currency: "RUB", partial: true, percent: "12.40", }, { absolute: "-4.2", costBasis: "24.2", currency: "USD", partial: false, percent: "-3.05", }, { absolute: "0", costBasis: "5", currency: "EUR", partial: false, percent: null, }, ], } satisfies PortfolioSummary; type BrokerageAccountCardInput = Parameters[0][ "accounts" ]; function account( id: string, name: string, summary: null | PortfolioSummary, ): BrokerageAccount { return { id, name, openedOn: null, portfolioSummary: summary, type: "BROKERAGE", }; } function renderList(accounts: BrokerageAccountCardInput) { return render( , ); } describe("BrokerageAccountList", () => { it("renders returns, cash and structure with precise totals", () => { const nullReturnSummary: PortfolioSummary = { ...portfolioSummary, cashBalances: [], structure: { currencies: [], unvaluedCategories: [] }, totalsByCurrency: [{ currency: "EUR", value: "5" }], unrealizedReturnByCurrency: [{ absolute: "0", costBasis: "5", currency: "EUR", partial: false, percent: null, }], }; renderList(brokerageAccountCards([ account("account-1", "Основной", portfolioSummary), account("account-2", "Без доходности", nullReturnSummary), ])); const link = screen.getByRole("link", { name: /Основной/ }); expect(link).toHaveAttribute("href", "/accounts/account-1"); expect(within(link).getByText("123 456 789 012 345 678 902,00 ₽")) .toBeInTheDocument(); expect(within(link).getByText("Еще 1 валюта")).toBeInTheDocument(); expect(within(link).getByText("+12,40 %")).toBeInTheDocument(); expect(within(link).getByText("+136 900 ₽")).toBeInTheDocument(); expect(within(link).getByText("−3,05 %")).toBeInTheDocument(); expect(within(link).getByText("−4,2 USD")).toBeInTheDocument(); const nullReturnLink = screen.getByRole("link", { name: /Без доходности/ }); expect(within(nullReturnLink).getByText("—")).toBeInTheDocument(); expect(within(link).getByText("частично")).toBeInTheDocument(); expect(within(link).getByText("82 500,50 ₽")).toBeInTheDocument(); expect(within(link).getAllByText("20,00 USD")).not.toHaveLength(0); expect(within(link).getByText("17 позиций")).toBeInTheDocument(); expect(screen.getByRole("img", { name: "Структура в RUB: Акции 50 %; Облигации 30 %; " + "Другие активы 0 %; Свободные деньги 20 %", })).toBeInTheDocument(); const stockLegend = within(link).getByText(/Акции 50 %/); expect(stockLegend).toHaveTextContent("● Акции 50 %"); // eslint-disable-next-line testing-library/no-node-access -- The legend is aria-hidden, so only DOM traversal reaches its container. expect(stockLegend.parentElement).toHaveAttribute("aria-hidden", "true"); expect(within(link).queryByText(/Другие активы 0 %/)).not.toBeInTheDocument(); expect(link).toHaveTextContent("По данным брокера"); expect(link).toHaveTextContent("Структура RUB: Акции 100"); }); it("preserves empty, unavailable and stale states", () => { const emptySummary: PortfolioSummary = { ...portfolioSummary, cashBalances: [], structure: { currencies: [], unvaluedCategories: [] }, totalPositionCount: 0, totalsByCurrency: [], unrealizedReturnByCurrency: [], }; const cards = brokerageAccountCards([ account("empty", "Пустой", emptySummary), account("unavailable", "Недоступный", null), account("stale", "Устаревший", portfolioSummary), account("failed", "Ошибка обновления", null), ], resource([], { dependencies: [{ availability: "available", completeness: "complete", dataRevision: "stale-revision", freshness: "stale", lastAttempt: { finishedAt: null, problems: [], startedAt: null, state: "none", }, lastSuccessfulAt: "2026-08-13T00:00:00Z", resourceKey: "portfolio/stale", }, { availability: "available", completeness: "complete", dataRevision: "failed-revision", freshness: "stale", lastAttempt: { finishedAt: "2026-08-14T00:01:00Z", problems: [], startedAt: "2026-08-14T00:00:00Z", state: "failed", }, lastSuccessfulAt: "2026-08-13T00:00:00Z", resourceKey: "portfolio/failed", }], freshness: "stale", lastSuccessfulAt: "2026-08-14T00:00:00Z", }).meta.dependencies); renderList(cards); expect(screen.getByText("Нет текущих сумм")).toBeInTheDocument(); expect(screen.getAllByText("Сводка временно недоступна")) .toHaveLength(2); const staleLink = screen.getByRole("link", { name: /Устаревший/ }); expect(within(staleLink).getByText("Данные требуют обновления")) .toBeInTheDocument(); expect(within(staleLink).getByText(/Данные на/)).toBeInTheDocument(); const failedLink = screen.getByRole("link", { name: /Ошибка обновления/ }); expect(within(failedLink).getByText( "Не удалось обновить данные. Показаны последние полученные.", )).toBeInTheDocument(); }); });