import { MantineProvider } from "@mantine/core"; import { QueryClientProvider } from "@tanstack/react-query"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { http, HttpResponse } from "msw"; import { MemoryRouter } from "react-router"; import { afterEach, describe, expect, it, vi } from "vitest"; import { configureAuthClient } from "@/shared/api"; import { createTestQueryClient } from "@/test/query"; import { server } from "@/test/server"; import { StocksPage } from "./StocksPage"; import { useStocksPageVm } from "./StocksPage.vm"; const pageOperations: Promise[] = []; const StocksPageVmProbe = () => { const vm = useStocksPageVm(); return ( <> {vm.items.map(item => item.name).join(", ")} ); }; afterEach(() => { pageOperations.length = 0; return vi.useRealTimers(); }); describe("stock search", () => { it("waits for a meaningful query and uses the server-verified ticker link", async () => { const query = vi.fn(); server.use( http.post("*/api/v1/stocks/screener/search", async ({ request }) => { query(await request.json()); return HttpResponse.json({ asOf: "2026-10-10T10:00:00Z", hasNext: false, items: [{ countryOfRisk: "RU", currency: "RUB", instrumentUid: "e6123145-9665-43e0-8413-cd61b8aa9b13", isin: "RU0009029540", logoName: "sber.png", lot: 10, name: "Сбербанк", realExchange: "REAL_EXCHANGE_MOEX", sector: "financial", shareType: "SHARE_TYPE_COMMON", ticker: "SBER", }], page: 1, searchId: "search-id", }); }), ); configureAuthClient({ accessToken: () => "synthetic", clear: vi.fn(), refresh: () => Promise.resolve(false), }); render( , ); const input = screen.getByLabelText("Название или тикер"); fireEvent.change(input, { target: { value: "SBER" } }); expect(query).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole("button", { name: "Найти" })); expect( await screen.findByRole("link", { name: /Сбербанк/ }), ).toHaveAttribute("href", "/stocks/e6123145-9665-43e0-8413-cd61b8aa9b13"); expect(screen.getByText("Список получен 10.10.2026, 10:00")).toBeVisible(); expect(screen.getByText("Лот: 10 шт.")).toBeVisible(); expect(screen.getByText("ISIN: RU0009029540")).toBeVisible(); expect(screen.getAllByText("Мосбиржа")[0]).toBeVisible(); expect(screen.getByAltText("")).toHaveAttribute( "src", "https://invest-brands.cdn-tinkoff.ru/sberx160.png", ); await waitFor(() => expect(query).toHaveBeenCalledExactlyOnceWith({ query: "SBER" })); }); it("searches by the selected sector without a text query", async () => { const query = vi.fn(); server.use( http.post("*/api/v1/stocks/screener/search", async ({ request }) => { query(await request.json()); return HttpResponse.json({ asOf: "2026-10-10T10:00:00Z", hasNext: false, items: [], page: 1, searchId: "sector-search", }); }), ); configureAuthClient({ accessToken: () => "synthetic", clear: vi.fn(), refresh: () => Promise.resolve(false), }); render( , ); fireEvent.click(screen.getByRole("combobox", { name: "Сектор" })); fireEvent.click(await screen.findByRole("option", { name: "Энергетика" })); fireEvent.click(screen.getByRole("button", { name: "Найти" })); await waitFor(() => expect(query).toHaveBeenCalledExactlyOnceWith({ sector: "energy", })); }); it("shows known result pages and reads the selected page", async () => { const requestedPages = vi.fn(); server.use( http.post("*/api/v1/stocks/screener/search", () => HttpResponse.json({ asOf: "2026-10-10T10:00:00Z", hasNext: true, items: [], page: 1, searchId: "paged-search", })), http.get("*/api/v1/stocks/screener/search/paged-search", ({ request }) => { const page = Number(new URL(request.url).searchParams.get("page")); requestedPages(page); return HttpResponse.json({ asOf: "2026-10-10T10:00:00Z", hasNext: false, items: [], page, searchId: "paged-search", }); }), ); configureAuthClient({ accessToken: () => "synthetic", clear: vi.fn(), refresh: () => Promise.resolve(false), }); render( , ); fireEvent.click(screen.getByRole("button", { name: "Найти" })); fireEvent.click(await screen.findByRole( "button", { name: "Страница 2" }, )); await waitFor(() => expect(requestedPages).toHaveBeenCalledExactlyOnceWith(2), ); }); it("keeps a newer search when an earlier page request finishes late", async () => { const oldPage = Promise.withResolvers(); let searches = 0; server.use( http.post("*/api/v1/stocks/screener/search", () => { searches += 1; return HttpResponse.json({ asOf: "2026-10-10T10:00:00Z", hasNext: searches === 1, items: [{ countryOfRisk: "RU", currency: "RUB", instrumentUid: searches === 1 ? "old" : "new", isin: null, logoName: null, lot: 1, name: searches === 1 ? "Старый" : "Новый", realExchange: "REAL_EXCHANGE_MOEX", sector: "financial", shareType: "SHARE_TYPE_COMMON", ticker: searches === 1 ? "OLD" : "NEW", }], page: 1, searchId: searches === 1 ? "old-search" : "new-search", }); }), http.get("*/api/v1/stocks/screener/search/old-search", async () => { await oldPage.promise; return HttpResponse.json({ asOf: "2026-10-10T10:00:00Z", hasNext: false, items: [{ countryOfRisk: "RU", currency: "RUB", instrumentUid: "old-page", isin: null, logoName: null, lot: 1, name: "Старый результат", realExchange: "REAL_EXCHANGE_MOEX", sector: "financial", shareType: "SHARE_TYPE_COMMON", ticker: "OLD", }], page: 2, searchId: "old-search", }); }), ); render(); fireEvent.click(screen.getByRole("button", { name: "search" })); await screen.findByText("Старый"); fireEvent.click(screen.getByRole("button", { name: "page" })); fireEvent.click(screen.getByRole("button", { name: "search" })); await screen.findByText("Новый"); await act(async () => { oldPage.resolve(); await pageOperations[0]; }); expect(screen.getByText("Новый")).toBeVisible(); expect(screen.queryByText("Старый результат")).not.toBeInTheDocument(); }); it("disables filters and shows loading while searching", async () => { const { promise, resolve } = Promise.withResolvers(); server.use( http.post("*/api/v1/stocks/screener/search", async () => { await promise; return HttpResponse.json({ asOf: "2026-10-10T10:00:00Z", hasNext: false, items: [], page: 1, searchId: "pending-search", }); }), ); configureAuthClient({ accessToken: () => "synthetic", clear: vi.fn(), refresh: () => Promise.resolve(false), }); render( , ); fireEvent.click(screen.getByRole("button", { name: "Найти" })); await waitFor(() => expect( screen.getByRole("combobox", { name: "Сектор" }), ).toBeDisabled()); expect(screen.getByRole("button", { name: "Найти" })).toHaveAttribute( "data-loading", "true", ); resolve(); }); });