import { MantineProvider } from "@mantine/core"; import { QueryClientProvider } from "@tanstack/react-query"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { HttpResponse, http } from "msw"; import { MemoryRouter, Route, Routes } from "react-router"; import { describe, expect, it, vi } from "vitest"; import { type BrokerageOperation, LedgerEntryRow, } from "@/entities/broker-operation"; import { configureAuthClient } from "@/shared/api"; import { createTestQueryClient } from "@/test/query"; import { server } from "@/test/server"; import { AccountOperationsPage } from "./AccountOperationsPage"; const operation: BrokerageOperation = { id: "operation-1", occurredAt: "2026-08-14T10:00:00Z", type: "OPERATION_TYPE_BUY", category: "trade", description: "Покупка акций", figi: null, instrumentUid: null, ticker: "SBER", instrumentType: "share", logoName: null, payment: null, quantity: "2", price: { currency: "RUB", value: "100" }, accruedInterest: null, commission: { currency: "RUB", value: "1" }, }; function configureTestAuth() { configureAuthClient({ accessToken: () => "token", refresh: async () => true, clear: vi.fn(), }); } function renderPage() { return render( } /> , ); } describe("LedgerEntryRow", () => { it("marks cashless operations without rendering operation details", () => { render( , ); expect(screen.getByText("Без движения денег")).toBeInTheDocument(); expect( screen.queryByRole("button", { name: "Показать детали" }), ).toBeNull(); expect(screen.queryByText("Цена: 100 ₽")).toBeNull(); expect(screen.queryByText("Количество: 2")).toBeNull(); }); it("labels a positive payment with its category type", () => { render( , ); expect(screen.getByText("Сделка")).toBeInTheDocument(); expect(screen.getByText("+15 ₽")).toBeInTheDocument(); }); it("labels a coupon income as Купон", () => { render( , ); expect(screen.getByText("Купон")).toBeInTheDocument(); expect(screen.getByText("+8 ₽")).toBeInTheDocument(); }); it("labels a dividend income as Дивиденд", () => { render( , ); expect(screen.getByText("Дивиденд")).toBeInTheDocument(); expect(screen.getByText("+20 ₽")).toBeInTheDocument(); }); it("labels a fee as Комиссия", () => { render( , ); expect(screen.getByText("Комиссия")).toBeInTheDocument(); expect(screen.getByText("−3 ₽")).toBeInTheDocument(); }); it("keeps a zero payment neutral", () => { render( , ); expect(screen.getByText("Без движения денег")).toBeInTheDocument(); expect(screen.queryByText("Сделка")).toBeNull(); }); it("uses an instrument initial when the ledger entry has no logo", () => { render( , ); expect(screen.getByText("S")).toBeInTheDocument(); }); }); describe("AccountOperationsPage", () => { it("resets cursor pagination when filters change", async () => { configureTestAuth(); const searches: string[] = []; server.use( http.get( "*/api/v1/brokerage-accounts/:accountId/operations", ({ request }) => { const url = new URL(request.url); searches.push(url.search); return HttpResponse.json({ accountId: "account-1", items: [operation], nextCursor: url.searchParams.get("cursor") ? null : "next-1", hasNext: !url.searchParams.get("cursor"), observedAt: "2026-08-14T00:00:00Z", }); }, ), ); renderPage(); expect( await screen.findByRole("heading", { name: "Журнал операций" }), ).toBeInTheDocument(); expect( screen.queryByRole("link", { name: "К счету" }), ).not.toBeInTheDocument(); fireEvent.click(await screen.findByRole("button", { name: "Далее" })); await waitFor(() => expect(searches.at(-1)).toContain("cursor=next-1")); fireEvent.click(screen.getByRole("combobox", { name: "Категории" })); fireEvent.click(await screen.findByText("Налог")); fireEvent.click(screen.getByRole("button", { name: "Применить" })); await waitFor(() => expect(searches.at(-1)).toContain("categories=tax")); expect(searches.at(-1)).not.toContain("cursor="); }); it("does not refetch when draft changes without apply", async () => { configureTestAuth(); const searches: string[] = []; server.use( http.get( "*/api/v1/brokerage-accounts/:accountId/operations", ({ request }) => { const url = new URL(request.url); searches.push(url.search); return HttpResponse.json({ accountId: "account-1", items: [operation], nextCursor: null, hasNext: false, observedAt: "2026-08-14T00:00:00Z", }); }, ), ); renderPage(); expect( await screen.findByRole("heading", { name: "Журнал операций" }), ).toBeInTheDocument(); await waitFor(() => expect(searches.length).toBeGreaterThan(0)); const countBefore = searches.length; fireEvent.click(screen.getByRole("combobox", { name: "Категории" })); fireEvent.click(await screen.findByText("Налог")); await new Promise((resolve) => setTimeout(resolve, 100)); expect(searches.length).toBe(countBefore); expect(searches.some((s) => s.includes("categories=tax"))).toBe(false); }); it("reset clears filters and refetches unfiltered", async () => { configureTestAuth(); const searches: string[] = []; server.use( http.get( "*/api/v1/brokerage-accounts/:accountId/operations", ({ request }) => { const url = new URL(request.url); searches.push(url.search); return HttpResponse.json({ accountId: "account-1", items: [operation], nextCursor: null, hasNext: false, observedAt: "2026-08-14T00:00:00Z", }); }, ), ); renderPage(); expect( await screen.findByRole("heading", { name: "Журнал операций" }), ).toBeInTheDocument(); fireEvent.click(screen.getByRole("combobox", { name: "Категории" })); fireEvent.click(await screen.findByText("Налог")); fireEvent.click(screen.getByRole("button", { name: "Применить" })); await waitFor(() => expect(searches.some((s) => s.includes("categories=tax"))).toBe(true), ); fireEvent.click(screen.getByRole("button", { name: "Сбросить" })); await waitFor(() => expect(searches.at(-1)).not.toContain("categories=")); expect(screen.getByRole("button", { name: "Применить" })).toBeDisabled(); }); it("renders an error state", async () => { configureTestAuth(); server.use( http.get("*/api/v1/brokerage-accounts/:accountId/operations", () => HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 }), ), ); renderPage(); expect( await screen.findByText("Не удалось загрузить журнал"), ).toBeInTheDocument(); }); });