refactor: enforce account page view model boundary

This commit is contained in:
2026-08-25 13:17:45 +03:00
parent fd72fb2239
commit fff36f252a
20 changed files with 912 additions and 428 deletions
+60 -39
View File
@@ -13,6 +13,11 @@ import {
AccountPageLoaded,
AccountPageLoading,
} from "./AccountPage";
import type {
AccountPageRecentEvents,
AccountPageRecentOperations,
} from "./AccountPage.vm";
import { toAccountPagePositionSections } from "./AccountPage.vm";
vi.mock("@/features/auth", () => ({
useAuth: () => ({ accessToken: "test-token" }),
@@ -73,13 +78,39 @@ const portfolio: Portfolio = {
cashBalances: [{ currency: "USD", value: "0.5" }],
};
const emptyRecentOperations: AccountPageRecentOperations = {
state: "empty",
to: "/accounts/account-1/operations",
};
const emptyRecentEvents: AccountPageRecentEvents = {
state: "empty",
to: "/accounts/account-1/events",
};
function loadedProps(nextPortfolio: Portfolio = portfolio) {
return {
portfolio: nextPortfolio,
positionSections: toAccountPagePositionSections(nextPortfolio),
recentOperations: emptyRecentOperations,
recentEvents: emptyRecentEvents,
};
}
function emptyProps() {
return {
recentOperations: emptyRecentOperations,
recentEvents: emptyRecentEvents,
};
}
describe("AccountPage states", () => {
afterEach(cleanup);
it("renders loading and empty states", () => {
renderPage(<AccountPageLoading />);
expect(screen.getByRole("status")).toBeInTheDocument();
renderPage(<AccountPageEmpty />);
renderPage(<AccountPageEmpty {...emptyProps()} />);
expect(
screen.getByRole("heading", { name: "Портфель пока пуст" }),
).toBeInTheDocument();
@@ -104,7 +135,7 @@ describe("AccountPage states", () => {
});
it("renders a stale portfolio in upstream order with nullable values hidden", () => {
renderPage(<AccountPageLoaded portfolio={portfolio} />);
renderPage(<AccountPageLoaded {...loadedProps()} />);
expect(
screen.getByRole("heading", { level: 1, name: "Портфель" }),
@@ -138,7 +169,7 @@ describe("AccountPage states", () => {
it("renders stock cards with logo fallback and signed unlabeled return", () => {
renderPage(
<AccountPageLoaded
portfolio={{
{...loadedProps({
...portfolio,
positions: [
{
@@ -164,7 +195,7 @@ describe("AccountPage states", () => {
logoName: null,
},
],
}}
})}
/>,
);
expect(screen.getByText("А")).toBeInTheDocument();
@@ -177,7 +208,7 @@ describe("AccountPage states", () => {
it("renders bond cards with logo, fallback, and signed unlabeled return like stocks", () => {
renderPage(
<AccountPageLoaded
portfolio={{
{...loadedProps({
...portfolio,
positions: [
{
@@ -203,7 +234,7 @@ describe("AccountPage states", () => {
logoName: null,
},
],
}}
})}
/>,
);
expect(screen.getByRole("img", { name: "Долг" })).toHaveAttribute(
@@ -222,10 +253,10 @@ describe("AccountPage states", () => {
it("renders the bonds section as a single-column grid on any screen width", () => {
renderPage(
<AccountPageLoaded
portfolio={{
{...loadedProps({
...portfolio,
positions: [portfolio.positions[1]],
}}
})}
/>,
);
const singleColumnGrids = Array.from(document.querySelectorAll("style"))
@@ -242,7 +273,7 @@ describe("AccountPage states", () => {
it("keeps other-position cards compact with labeled return and no logo", () => {
renderPage(
<AccountPageLoaded
portfolio={{
{...loadedProps({
...portfolio,
positions: [
{
@@ -257,7 +288,7 @@ describe("AccountPage states", () => {
logoName: "fond.png",
},
],
}}
})}
/>,
);
expect(screen.getByText("Доход: -5 ₽")).toBeInTheDocument();
@@ -267,7 +298,7 @@ describe("AccountPage states", () => {
it("uses the cached account title when available", () => {
renderPage(
<AccountPageLoaded
portfolio={portfolio}
{...loadedProps()}
account={{
id: "account-1",
name: "Основной",
@@ -281,47 +312,37 @@ describe("AccountPage states", () => {
).toBeInTheDocument();
});
it("keeps the portfolio visible when recent events fail", async () => {
server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 }),
),
);
it("keeps the portfolio visible when recent operations fail", () => {
renderPage(
<AccountPageLoaded
accountId="account-1"
accessToken="token"
portfolio={portfolio}
{...loadedProps()}
recentOperations={{
state: "error",
to: "/accounts/account-1/operations",
}}
/>,
);
await waitFor(() =>
expect(
screen.getByText("Не удалось загрузить последние события."),
).toBeInTheDocument(),
);
expect(
screen.getByText("Не удалось загрузить последние события."),
).toBeInTheDocument();
expect(screen.getByText("Вторая")).toBeInTheDocument();
});
it("keeps the portfolio visible when upcoming events fail", async () => {
server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/events", () =>
HttpResponse.json({ code: "MOEX_UNAVAILABLE" }, { status: 503 }),
),
);
it("keeps the portfolio visible when upcoming events fail", () => {
renderPage(
<AccountPageLoaded
accountId="account-1"
accessToken="token"
portfolio={portfolio}
{...loadedProps()}
recentEvents={{
state: "error",
to: "/accounts/account-1/events",
}}
/>,
);
await waitFor(() =>
expect(
screen.getByText("Не удалось загрузить будущие события."),
).toBeInTheDocument(),
);
expect(
screen.getByText("Не удалось загрузить будущие события."),
).toBeInTheDocument();
expect(screen.getByText("Вторая")).toBeInTheDocument();
});