Frontend verify / commits (pull_request) Successful in 29s
Frontend verify / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 8m54s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 12s
Frontend verify / verify (push) Successful in 9m59s
151 lines
3.9 KiB
TypeScript
151 lines
3.9 KiB
TypeScript
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 type { Portfolio } from "@/entities/portfolio";
|
|
|
|
import { toAccountPagePositionSections } from "./AccountPage.vm";
|
|
import { PositionSection } from "./components/PositionSection.view";
|
|
|
|
const position: Portfolio["positions"][number]
|
|
= {
|
|
absoluteReturn: { currency: "RUB", value: "25" },
|
|
account: { id: "a", name: "Основной" },
|
|
averagePurchasePrice: { currency: "RUB", value: "280" },
|
|
currentValue: { currency: "RUB", value: "375" },
|
|
displayName: "Сбербанк",
|
|
figi: "FIGI-SBER",
|
|
isin: "RU0009029540",
|
|
logoName: null,
|
|
quantity: "1.25",
|
|
ticker: "SBER",
|
|
type: "STOCK",
|
|
};
|
|
|
|
const second
|
|
= {
|
|
...position,
|
|
absoluteReturn: null,
|
|
account: { id: "b", name: "ИИС" },
|
|
averagePurchasePrice: null,
|
|
currentValue: null,
|
|
quantity: "-2",
|
|
};
|
|
|
|
function sections(positions: Portfolio["positions"]) {
|
|
return toAccountPagePositionSections({
|
|
cashBalances: [],
|
|
isStale: false,
|
|
lastSuccessfulAt: "2026-09-09T12:00:00Z",
|
|
positions,
|
|
structure: { currencies: [], unvaluedCategories: [] },
|
|
totalsByCurrency: [],
|
|
});
|
|
}
|
|
|
|
describe("Positions grouped by security", () => {
|
|
it("shows one security with exact per-account quantities and valuations", () => {
|
|
const section = sections([position, second])[0];
|
|
render(
|
|
<MantineProvider>
|
|
<MemoryRouter>
|
|
<PositionSection section={section} />
|
|
</MemoryRouter>
|
|
</MantineProvider>,
|
|
);
|
|
expect(screen.getAllByText("Сбербанк")).toHaveLength(1);
|
|
|
|
const rows = within(
|
|
screen.getByRole("list", { name: "Позиции по счетам" }),
|
|
).getAllByRole("listitem");
|
|
|
|
expect(rows).toHaveLength(2);
|
|
expect(
|
|
within(rows[0]).getByRole("link", { name: "Основной" }),
|
|
).toHaveAttribute("href", "/accounts/a");
|
|
expect(within(rows[0]).getByText("Количество: 1.25")).toBeInTheDocument();
|
|
expect(within(rows[0]).getByText("375 ₽")).toBeInTheDocument();
|
|
expect(
|
|
within(rows[0]).getByText("Средняя цена: 280 ₽"),
|
|
).toBeInTheDocument();
|
|
expect(within(rows[1]).getByRole("link", { name: "ИИС" })).toHaveAttribute(
|
|
"href",
|
|
"/accounts/b",
|
|
);
|
|
expect(within(rows[1]).getByText("Количество: -2")).toBeInTheDocument();
|
|
expect(rows[1]).not.toHaveTextContent("₽");
|
|
});
|
|
|
|
it("uses ISIN only when FIGI is absent, never merging by name or ticker", () => {
|
|
expect(
|
|
|
|
sections([
|
|
{
|
|
...position,
|
|
figi: null,
|
|
},
|
|
{ ...second, figi: null },
|
|
])[0].groups,
|
|
).toHaveLength(1);
|
|
expect(
|
|
sections([position, { ...second, figi: "OTHER-FIGI" }])[0].groups,
|
|
).toHaveLength(2);
|
|
expect(
|
|
|
|
sections([
|
|
{
|
|
...position,
|
|
figi: null,
|
|
isin: null,
|
|
},
|
|
{
|
|
...second,
|
|
figi: null,
|
|
isin: null,
|
|
},
|
|
])[0].groups,
|
|
).toHaveLength(2);
|
|
});
|
|
|
|
it("preserves one-account cards and does not merge repeated rows on the same account", () => {
|
|
expect(
|
|
sections([position, { ...second, account: position.account }])[0].groups,
|
|
).toHaveLength(2);
|
|
expect(
|
|
sections([
|
|
{ ...position, account: undefined },
|
|
{ ...second, account: undefined },
|
|
])[0].groups,
|
|
).toHaveLength(2);
|
|
});
|
|
|
|
it("keeps currencies and purchase prices attached to their source positions", () => {
|
|
const source = [
|
|
position,
|
|
{
|
|
...second,
|
|
averagePurchasePrice: { currency: "USD", value: "0.99" },
|
|
currentValue: { currency: "USD", value: "1.01" },
|
|
},
|
|
];
|
|
|
|
const before = structuredClone(source);
|
|
const [group] = sections(source)[0].groups;
|
|
expect(
|
|
group.positions.map(item => [
|
|
item.quantity,
|
|
item.currentValue,
|
|
item.averagePurchasePrice,
|
|
]),
|
|
).toEqual(
|
|
source.map(item => [
|
|
item.quantity,
|
|
item.currentValue,
|
|
item.averagePurchasePrice,
|
|
]),
|
|
);
|
|
expect(source).toEqual(before);
|
|
});
|
|
});
|