import { MantineProvider } from "@mantine/core"; import { fireEvent, render, screen, within } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import type { Portfolio } from "@/entities/portfolio"; import { PortfolioStructure } from "./PortfolioStructure"; const rub: Portfolio["structure"]["currencies"][number] = { categories: [ { sharePercent: "60", type: "STOCK", value: "60" }, { sharePercent: "20", type: "BOND", value: "20" }, { sharePercent: "0", type: "OTHER", value: "0" }, { sharePercent: "20", type: "CASH", value: "20" }, ], currency: "RUB", hasNegativeValues: false, totalValue: "100", }; const ui = (structure: null | Portfolio["structure"] | undefined) => ( ); describe("portfolio structure", () => { it.each([ undefined, null, ])( "keeps an older snapshot without structure available (%s) and recovers on refresh", (structure) => { const view = render(ui(structure)); expect( screen.getByRole("region", { name: "Структура портфеля" }), ).toHaveTextContent("Данные о структуре портфеля пока недоступны."); expect( screen.queryByText(/Нет положительной стоимости/), ).not.toBeInTheDocument(); expect(screen.queryByRole("list")).not.toBeInTheDocument(); view.rerender(ui({ currencies: [rub], unvaluedCategories: [] })); expect(screen.getByRole("list")).toHaveTextContent("Акции60 ₽60 %"); expect( screen.queryByText("Данные о структуре портфеля пока недоступны."), ).not.toBeInTheDocument(); }, ); it("shows exact server amounts and cash-inclusive shares without hovering", () => { render(ui({ currencies: [rub], unvaluedCategories: [] })); const list = screen.getByRole("list", { name: "Категории активов" }); expect(list).toHaveTextContent("Акции60 ₽60 %"); expect(list).toHaveTextContent("Свободные деньги20 ₽20 %"); expect(screen.queryByRole("combobox")).not.toBeInTheDocument(); }); it("switches currencies and falls back when a refresh removes the selection", () => { const usd = { ...rub, currency: "USD" }; const view = render(ui({ currencies: [rub, usd], unvaluedCategories: [] })); fireEvent.click( screen.getByRole("combobox", { name: "Валюта структуры портфеля" }), ); fireEvent.click(screen.getByRole("option", { name: "USD" })); expect(screen.getByRole("list")).toHaveTextContent("60 USD"); expect(screen.getByRole("list")).not.toHaveTextContent("₽"); view.rerender(ui({ currencies: [rub], unvaluedCategories: [] })); expect(screen.getByRole("list")).toHaveTextContent("60 ₽"); expect(screen.queryByRole("combobox")).not.toBeInTheDocument(); }); it("retains known amounts and names unvalued categories without inventing a zero", () => { render( ui({ currencies: [ { ...rub, categories: rub.categories.map( row => ({ ...row, sharePercent: null, }), ), }, ], unvaluedCategories: [ { count: 2, type: "OTHER" }, { count: 1, type: "STOCK" }, ], }), ); expect(screen.getByText(/Оценка неполная/)).toBeInTheDocument(); expect(screen.getByRole("list")).toHaveTextContent("Известно: 60 ₽"); expect(screen.getByRole("list")).toHaveTextContent( "Другие активыНет оценки", ); expect( screen.queryByLabelText(/Структура по типам/), ).not.toBeInTheDocument(); }); it("keeps negative amounts visible without a donut or invented positive shares", () => { render( ui({ currencies: [ { ...rub, categories: rub.categories.map( row => ({ ...row, sharePercent: null, value: row.type === "STOCK" ? "-60" : row.value, }), ), hasNegativeValues: true, }, ], unvaluedCategories: [], }), ); expect(screen.getByText(/Есть отрицательные/)).toBeInTheDocument(); expect(screen.getByRole("list")).toHaveTextContent("-60 ₽"); expect( within(screen.getByRole("list")).getAllByText("Доля недоступна"), ).toHaveLength(4); expect( screen.queryByLabelText(/Структура по типам/), ).not.toBeInTheDocument(); }); it("handles unknown-only and empty compositions", () => { const view = render( ui({ currencies: [], unvaluedCategories: [{ count: 1, type: "BOND" }] }), ); expect(screen.getByText("Облигации: без оценки — 1")).toBeInTheDocument(); expect(screen.queryByRole("list")).not.toBeInTheDocument(); view.rerender(ui({ currencies: [], unvaluedCategories: [] })); expect(screen.getByText(/Нет положительной стоимости/)).toBeInTheDocument(); }); it("keeps full decimal amounts and describes tiny shares", () => { render( ui({ currencies: [ { ...rub, categories: [ { sharePercent: "0.000001", type: "CASH", value: "9007199254740993.000000001", }, ], }, ], unvaluedCategories: [], }), ); expect(screen.getByRole("list")).toHaveTextContent( "9 007 199 254 740 993,000000001 ₽", ); expect(screen.getByRole("list")).toHaveTextContent("< 0,01 %"); }); });