feat: add portfolio

This commit is contained in:
2026-08-13 22:45:00 +03:00
parent 6e89c1a8f7
commit 9156f29707
11 changed files with 594 additions and 12 deletions
+117
View File
@@ -0,0 +1,117 @@
import { MantineProvider } from "@mantine/core";
import { cleanup, render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { Portfolio } from "../../../entities/portfolio";
import {
AccountPageEmpty,
AccountPageError,
AccountPageLoaded,
AccountPageLoading,
} from "./AccountPage";
function renderPage(ui: React.ReactNode) {
return render(
<MantineProvider>
<MemoryRouter>{ui}</MemoryRouter>
</MantineProvider>,
);
}
const portfolio: Portfolio = {
lastSuccessfulAt: "2026-08-13T10:00:00Z",
isStale: true,
totalsByCurrency: [{ currency: "RUB", value: "1234.5" }],
positions: [
{
figi: "FIGI-2",
displayName: "Вторая",
type: "STOCK",
quantity: "2",
currentValue: null,
averagePurchasePrice: null,
absoluteReturn: null,
},
{
figi: "FIGI-1",
displayName: "Первая",
type: "BOND",
quantity: "1",
currentValue: { currency: "RUB", value: "10" },
averagePurchasePrice: null,
absoluteReturn: { currency: "RUB", value: "-1" },
},
],
cashBalances: [{ currency: "USD", value: "0.5" }],
};
describe("AccountPage states", () => {
afterEach(cleanup);
it("renders loading and empty states", () => {
renderPage(<AccountPageLoading />);
expect(screen.getByRole("status")).toBeInTheDocument();
renderPage(<AccountPageEmpty />);
expect(
screen.getByRole("heading", { name: "Портфель пока пуст" }),
).toBeInTheDocument();
});
it("renders allowed error actions", () => {
const retry = vi.fn();
renderPage(<AccountPageError code="NOT_FOUND" retry={retry} />);
expect(screen.getByRole("link", { name: "К счетам" })).toHaveAttribute(
"href",
"/accounts",
);
renderPage(<AccountPageError code="TBANK_NOT_CONNECTED" retry={retry} />);
expect(
screen.getByRole("link", { name: "Открыть настройки T-Bank" }),
).toHaveAttribute("href", "/settings");
renderPage(<AccountPageError code="TBANK_UNAVAILABLE" retry={retry} />);
screen.getByRole("button", { name: "Повторить" }).click();
expect(retry).toHaveBeenCalledOnce();
});
it("renders a stale portfolio in upstream order with nullable values", () => {
renderPage(<AccountPageLoaded portfolio={portfolio} />);
expect(
screen.getByRole("heading", { level: 1, name: "Портфель" }),
).toBeInTheDocument();
expect(
screen.getByText(
"T-Bank временно недоступен. Показан последний успешный снимок.",
),
).toBeInTheDocument();
expect(screen.getByText("—")).toBeInTheDocument();
expect(screen.getByText("Доход: —")).toBeInTheDocument();
const positions = screen
.getAllByText(/Вторая|Первая/)
.map((element) => element.textContent);
expect(positions).toEqual(["Вторая", "Первая"]);
expect(screen.getByRole("heading", { name: "Акции" })).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Облигации" }),
).toBeInTheDocument();
});
it("uses the cached account title when available", () => {
renderPage(
<AccountPageLoaded
portfolio={portfolio}
account={{
id: "account-1",
name: "Основной",
type: "BROKERAGE",
openedOn: null,
}}
/>,
);
expect(
screen.getByRole("heading", { level: 1, name: "Основной" }),
).toBeInTheDocument();
});
});