refactor: move PortfolioStructure to widgets layer
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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) => (
|
||||
<MantineProvider>
|
||||
<PortfolioStructure structure={structure} />
|
||||
</MantineProvider>
|
||||
);
|
||||
|
||||
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 %");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user