Files
frontend/src/pages/account/ui/AccountPage/AccountPage.vm.test.tsx
T
ksv741 3c226f8c29
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 12s
Frontend verify / verify (push) Successful in 7m41s
feat: render hourly investment states
2026-10-09 21:13:34 +03:00

249 lines
6.5 KiB
TypeScript

import type { FC, PropsWithChildren } from "react";
import { MantineProvider } from "@mantine/core";
import { renderHook, waitFor } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { MemoryRouter, Route, Routes } from "react-router";
import { describe, expect, it, vi } from "vitest";
import { configureAuthClient } from "@/shared/api";
import { createQueryWrapper } from "@/test/query";
import { resource } from "@/test/resource";
import { server } from "@/test/server";
import { portfolio } from "../../model/positions.fixtures";
import { useAccountPageViewModel } from "./AccountPage.vm";
function configureTestAuth() {
configureAuthClient({
accessToken: () => "test-token",
clear: vi.fn(),
refresh: () => Promise.resolve(true),
});
}
function createWrapper() {
const { wrapper: QueryWrapper } = createQueryWrapper();
const Wrapper: FC<PropsWithChildren> = props => (
<QueryWrapper>
<MantineProvider>
<MemoryRouter initialEntries={["/accounts/account-1"]}>
<Routes>
<Route element={props.children} path="/accounts/:accountId" />
</Routes>
</MemoryRouter>
</MantineProvider>
</QueryWrapper>
);
return Wrapper;
}
function mockLoadedAccountPage() {
server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
HttpResponse.json(resource(portfolio)),
),
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
HttpResponse.json(
resource(
{
accountId: "account-1",
hasNext: false,
items: [
{
accruedInterest: null,
category: "trade",
commission: null,
description: "Покупка акции",
figi: "FIGI-STOCK",
id: "operation-1",
instrumentType: "share",
instrumentUid: null,
occurredAt: "2026-08-13T10:00:00Z",
payment: { currency: "RUB", value: "-100" },
price: null,
quantity: "1",
ticker: "STK",
type: "Buy",
},
],
nextCursor: null,
observedAt: "2026-08-13T10:00:00Z",
},
),
),
),
http.get("*/api/v1/brokerage-accounts/:accountId/events", () =>
HttpResponse.json(
resource({
accountId: "account-1",
calculatedAt: "2026-08-13T10:00:00Z",
events: Array.from(
{ length: 6 },
(_, index) =>
({
date: `2026-09-${String(index + 1).padStart(2, "0")}`,
dateKind: "event",
displayName: `Купон ${index}`,
figi: `FIGI-EVENT-${index}`,
instrumentUid: null,
projectedAmount: { currency: "RUB", value: String(index + 1) },
ticker: `BND${index}`,
type: "coupon",
}),
),
}),
),
),
);
}
describe("useAccountPageViewModel", () => {
it("prepares recent operations and events state for presentation views", async () => {
configureTestAuth();
mockLoadedAccountPage();
const { result } = renderHook(() => useAccountPageViewModel(), {
wrapper: createWrapper(),
});
await waitFor(() =>
expect(
(result.current as { recentEvents?: { state: string } }).recentEvents,
)?.toMatchObject({ state: "loaded" }),
);
const loaded = result.current as Extract<
typeof result.current,
{ state: "loaded" }
>;
expect(loaded.recentOperations).toMatchObject({
items: [{ description: "Покупка акции", id: "operation-1" }],
state: "loaded",
to: "/accounts/account-1/operations",
});
expect(loaded.recentEvents).toMatchObject({
items: [
{ ticker: "BND0" },
{ ticker: "BND1" },
{ ticker: "BND2" },
{ ticker: "BND3" },
{ ticker: "BND4" },
],
state: "loaded",
to: "/accounts/account-1/events",
});
});
it("keeps recent errors isolated in the ViewModel presentation state", async () => {
configureTestAuth();
server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
HttpResponse.json(resource(portfolio)),
),
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
HttpResponse.json({ code: "DEMO_FAILURE" }, { status: 400 }),
),
http.get("*/api/v1/brokerage-accounts/:accountId/events", () =>
HttpResponse.json({ code: "DEMO_FAILURE" }, { status: 400 }),
),
);
const { result } = renderHook(() => useAccountPageViewModel(), {
wrapper: createWrapper(),
});
await waitFor(() =>
expect(result.current).toMatchObject({
recentEvents: { state: "error" },
recentOperations: { state: "error" },
state: "loaded",
}),
);
});
});
it("keeps overview sections independent when their source data is incomplete", async () => {
configureTestAuth();
mockLoadedAccountPage();
server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
HttpResponse.json(resource(portfolio, { completeness: "incomplete" })),
),
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
HttpResponse.json(
resource(
{ hasNext: false, items: [], nextCursor: null },
{ completeness: "incomplete" },
),
),
),
http.get("*/api/v1/brokerage-accounts/:accountId/events", () =>
HttpResponse.json(
resource(
{ events: [], incomeTotalsByCurrency: [] },
{ completeness: "incomplete" },
),
),
),
);
const { result } = renderHook(() => useAccountPageViewModel(), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.state).toBe("loaded"));
await waitFor(() =>
expect(result.current).toMatchObject({
partial: true,
recentEvents: { items: [], state: "partial" },
recentOperations: { items: [], state: "partial" },
}),
);
});
it("keeps overview sections loaded without exposing technical retry", async () => {
configureTestAuth();
const sections: Record<string, unknown>
= {
"events": { events: [], incomeTotalsByCurrency: [] },
"income-summary": { totalsByCurrency: [] },
"operations": { hasNext: false, items: [], nextCursor: null },
portfolio,
};
server.use(
...Object.entries(sections).map(([path, data]) =>
http.get(
`*/api/v1/brokerage-accounts/:accountId/${path}`,
() => {
return HttpResponse.json(
resource(data, { dataRevision: "1", resourceKey: path }),
);
},
),
),
);
const { result } = renderHook(() => useAccountPageViewModel(), {
wrapper: createWrapper(),
});
await waitFor(() =>
expect(result.current).toMatchObject({
incomeSummary: { state: "empty" },
recentEvents: { state: "empty" },
recentOperations: { state: "empty" },
state: "loaded",
}),
);
expect(result.current).not.toHaveProperty("retry");
});