import { MantineProvider } from "@mantine/core"; import { type QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { render, screen, waitFor } from "@testing-library/react"; import { http, HttpResponse } from "msw"; import { MemoryRouter } from "react-router"; import { describe, expect, it, vi } from "vitest"; import { brokerageAccountsQueryKey } from "@/entities/brokerage-account"; import { configureAuthClient } from "@/shared/api"; import { createTestQueryClient } from "@/test/query"; import { resource } from "@/test/resource"; import { server } from "@/test/server"; import { AccountsPage } from "./AccountsPage"; import { AccountsPageEmpty } from "./components/AccountsPageEmpty.view"; import { AccountsPageError } from "./components/AccountsPageError.view"; import { AccountsPageLoading } from "./components/AccountsPageLoading.view"; const account = { id: "account-1", name: "Основной", openedOn: "2026-01-01", type: "BROKERAGE", } as const; function configureTestAuth() { configureAuthClient({ accessToken: () => "test-token", clear: vi.fn(), refresh: () => Promise.resolve(true), }); } function renderPage(queryClient: QueryClient) { configureTestAuth(); return render( , ); } describe("AccountsPage", () => { it("loads accounts after retrying an initial HTTP failure", async () => { const modes: (null | string)[] = []; server.use( http.get("*/api/v1/brokerage-accounts", ({ request }) => { modes.push(new URL(request.url).searchParams.get("sync")); return modes.length === 1 ? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 }) : HttpResponse.json(resource([account])); }), ); renderPage(createTestQueryClient()); expect(await screen.findByText(account.name, undefined, { timeout: 4000, })).toBeVisible(); expect(modes).toEqual([null, null]); }); it.each([ ["loading", AccountsPageLoading, "status"], ["empty", AccountsPageEmpty, undefined], ] as const)("renders only the %s state", (_name, State, role) => { render( , ); // eslint-disable-next-line vitest/no-conditional-expect -- The fixed table case selects the expected outcome; every case asserts its own branch. if (role) expect(screen.getByRole(role)).toBeInTheDocument(); else // eslint-disable-next-line vitest/no-conditional-expect -- The fixed table case selects the expected outcome; every case asserts its own branch. expect( screen.getByRole("heading", { name: "Нет инвестиционных счетов" }), ).toBeInTheDocument(); }); it("routes connection errors to settings without technical retry", () => { render( , ); expect(screen.queryByRole("button", { name: "Повторить" })) .not.toBeInTheDocument(); expect( screen.getByRole("link", { name: "Открыть настройки T-Bank" }), ).toHaveAttribute("href", "/settings"); }); it("keeps the cached account list visible during a background refresh", async () => { let requestCount = 0; let resolveRefresh: ((response: Response) => void) | undefined; server.use( http.get("*/api/v1/brokerage-accounts", () => { requestCount += 1; if (requestCount === 1) return HttpResponse.json(resource([account])); { const deferred = Promise.withResolvers(); resolveRefresh = deferred.resolve; return deferred.promise; } }), ); const queryClient = createTestQueryClient(); renderPage(queryClient); await screen.findByText(account.name); void queryClient.invalidateQueries({ queryKey: brokerageAccountsQueryKey, }); await waitFor(() => expect(requestCount).toBe(2)); expect(screen.getByText(account.name)).toBeInTheDocument(); expect(screen.queryByRole("status", { name: "Обновляем данные" })) .not.toBeInTheDocument(); resolveRefresh?.(HttpResponse.json(resource([account]))); }); it("keeps the cached account list quietly after a refresh error", async () => { let requestCount = 0; server.use( http.get("*/api/v1/brokerage-accounts", () => { requestCount += 1; if (requestCount === 1) return HttpResponse.json(resource([account])); return requestCount === 2 ? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 }) : HttpResponse.json(resource([account])); }), ); const queryClient = createTestQueryClient(); renderPage(queryClient); await screen.findByText(account.name); void queryClient.invalidateQueries({ queryKey: brokerageAccountsQueryKey, }); await waitFor(() => expect(requestCount).toBe(2)); expect( screen.queryByText( "Не удалось обновить данные. Показаны последние полученные.", ), ).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Повторить" })) .not.toBeInTheDocument(); expect(screen.getByText(account.name)).toBeInTheDocument(); }); it("keeps a cached empty account list visible after a refresh error", async () => { let requestCount = 0; server.use( http.get("*/api/v1/brokerage-accounts", () => { requestCount += 1; return requestCount === 1 ? HttpResponse.json(resource([])) : HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 }); }), ); const queryClient = createTestQueryClient(); renderPage(queryClient); await screen.findByText("Нет инвестиционных счетов"); void queryClient.invalidateQueries({ queryKey: brokerageAccountsQueryKey, }); await waitFor(() => expect(requestCount).toBe(2)); expect( screen.queryByText( "Не удалось обновить данные. Показаны последние полученные.", ), ).not.toBeInTheDocument(); expect(screen.getByText("Нет инвестиционных счетов")).toBeInTheDocument(); }); }); it.each(["none", "failed"] as const)( "keeps incomplete empty membership distinct from no accounts (attempt=%s)", async (state) => { server.use( http.get("*/api/v1/brokerage-accounts", () => HttpResponse.json(resource([], { completeness: "incomplete", lastAttempt: { finishedAt: state === "failed" ? "2026-08-14T00:01:00Z" : null, problems: [], startedAt: state === "none" ? null : "2026-08-14T00:00:00Z", state, }, })), ), ); const client = createTestQueryClient(); renderPage(client); await waitFor(() => expect(client.getQueryData(brokerageAccountsQueryKey)).toMatchObject({ data: [], meta: { completeness: "incomplete" }, }), ); expect(screen.queryByText("Нет инвестиционных счетов")) .not.toBeInTheDocument(); if (state === "failed") // eslint-disable-next-line vitest/no-conditional-expect -- Each fixed state has a different observable recovery contract. expect(await screen.findByRole("heading", { name: "Пока недоступно" })) .toBeVisible(); else // eslint-disable-next-line vitest/no-conditional-expect -- Each fixed state has a different observable recovery contract. expect(screen.getByRole("status")).toBeVisible(); }, ); it("keeps unknown membership loading instead of declaring no accounts", async () => { server.use( http.get("*/api/v1/brokerage-accounts", () => HttpResponse.json(resource(null)), ), ); const client = createTestQueryClient(); renderPage(client); await waitFor(() => expect(client.getQueryData(brokerageAccountsQueryKey)).toMatchObject( { data: null, }, ), ); expect( screen.queryByText("Нет инвестиционных счетов"), ).not.toBeInTheDocument(); expect(screen.getByRole("status")).toBeVisible(); });