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( , ); 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); }); });