import { MantineProvider } from "@mantine/core"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { renderHook, waitFor } from "@testing-library/react"; import { HttpResponse, http } from "msw"; import type { FC, PropsWithChildren } from "react"; import { MemoryRouter, Route, Routes } from "react-router"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { Portfolio } from "@/entities/portfolio"; import { configureAuthClient } from "@/shared/api"; import { server } from "@/test/server"; import { useAccountPageViewModel } from "./AccountPage.vm"; const portfolio: Portfolio = { lastSuccessfulAt: "2026-08-13T10:00:00Z", isStale: true, totalsByCurrency: [{ currency: "RUB", value: "150" }], positions: [ { figi: "FIGI-STOCK", displayName: "Акция", type: "STOCK", quantity: "2", currentValue: { currency: "RUB", value: "100" }, averagePurchasePrice: null, absoluteReturn: { currency: "RUB", value: "10" }, isin: null, logoName: "akciya.png", }, { figi: "FIGI-BOND", displayName: "Облигация", type: "BOND", quantity: "1", currentValue: { currency: "RUB", value: "50" }, averagePurchasePrice: { currency: "RUB", value: "45" }, absoluteReturn: { currency: "RUB", value: "-5" }, isin: "RU000A0JX0J2", logoName: null, }, { figi: "FIGI-FUND", displayName: "Фонд", type: "FUND", quantity: "3", currentValue: null, averagePurchasePrice: null, absoluteReturn: null, isin: null, logoName: "fond.png", }, ], cashBalances: [{ currency: "USD", value: "1" }], }; function configureTestAuth() { configureAuthClient({ accessToken: () => "test-token", refresh: async () => true, clear: vi.fn(), }); } const wrapper: FC = (props) => { const { children } = props; return ( ); }; function mockLoadedAccountPage() { server.use( http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => HttpResponse.json(portfolio), ), http.get("*/api/v1/brokerage-accounts/:accountId/operations", () => HttpResponse.json({ accountId: "account-1", items: [ { id: "operation-1", occurredAt: "2026-08-13T10:00:00Z", type: "Buy", category: "trade", description: "Покупка акции", figi: "FIGI-STOCK", instrumentUid: null, ticker: "STK", instrumentType: "share", payment: { currency: "RUB", value: "-100" }, quantity: "1", price: null, accruedInterest: null, commission: null, }, ], nextCursor: null, hasNext: false, observedAt: "2026-08-13T10:00:00Z", }), ), http.get("*/api/v1/brokerage-accounts/:accountId/events", () => HttpResponse.json({ accountId: "account-1", calculatedAt: "2026-08-13T10:00:00Z", events: Array.from({ length: 6 }, (_, index) => ({ type: "coupon", date: `2026-09-${String(index + 1).padStart(2, "0")}`, dateKind: "event", figi: `FIGI-EVENT-${index}`, instrumentUid: null, ticker: `BND${index}`, displayName: `Купон ${index}`, projectedAmount: { currency: "RUB", value: String(index + 1) }, })), }), ), ); } describe("useAccountPageViewModel", () => { afterEach(() => configureAuthClient(null)); it("prepares grouped position sections with derived display data", async () => { configureTestAuth(); mockLoadedAccountPage(); const { result } = renderHook(() => useAccountPageViewModel(), { wrapper }); await waitFor(() => expect(result.current.state).toBe("loaded")); const loaded = result.current as Extract< typeof result.current, { state: "loaded" } >; expect(loaded.positionSections).toMatchObject([ { kind: "stocks", wide: true, positions: [ { displayName: "Акция", shareText: "66,7 %", returnText: "+10 ₽", returnTone: "positive", logoSrc: "https://invest-brands.cdn-tinkoff.ru/akciyax160.png", }, ], }, { kind: "bonds", wide: true, positions: [ { displayName: "Облигация", shareText: "33,3 %", returnText: "-5 ₽", returnTone: "negative", logoSrc: null, }, ], }, { kind: "other", wide: false, positions: [ { displayName: "Фонд", shareText: null, returnText: null, returnTone: undefined, logoSrc: null, }, ], }, ]); }); it("prepares recent operations and events state for presentation views", async () => { configureTestAuth(); mockLoadedAccountPage(); const { result } = renderHook(() => useAccountPageViewModel(), { wrapper }); 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({ state: "loaded", to: "/accounts/account-1/operations", items: [{ id: "operation-1", description: "Покупка акции" }], }); expect(loaded.recentEvents).toMatchObject({ state: "loaded", to: "/accounts/account-1/events", items: [ { ticker: "BND0" }, { ticker: "BND1" }, { ticker: "BND2" }, { ticker: "BND3" }, { ticker: "BND4" }, ], }); }); it("keeps recent errors isolated in the ViewModel presentation state", async () => { configureTestAuth(); server.use( http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => HttpResponse.json(portfolio), ), http.get("*/api/v1/brokerage-accounts/:accountId/operations", () => HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 }), ), http.get("*/api/v1/brokerage-accounts/:accountId/events", () => HttpResponse.json({ code: "MOEX_UNAVAILABLE" }, { status: 503 }), ), ); const { result } = renderHook(() => useAccountPageViewModel(), { wrapper }); await waitFor(() => expect(result.current).toMatchObject({ state: "loaded", recentOperations: { state: "error" }, recentEvents: { state: "error" }, }), ); }); });