feat: add stock screener
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { 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 { resource } from "@/test/resource";
|
||||
import { server } from "@/test/server";
|
||||
|
||||
import { StocksPage } from "./StocksPage";
|
||||
@@ -18,27 +17,28 @@ describe("stock search", () => {
|
||||
it("waits for a meaningful query and uses the server-verified ticker link", async () => {
|
||||
const query = vi.fn();
|
||||
server.use(
|
||||
http.get("*/api/v1/stocks", ({ request }) => {
|
||||
query(new URL(request.url).searchParams.get("query"));
|
||||
http.post("*/api/v1/stocks/screener/search", async ({ request }) => {
|
||||
query(await request.json());
|
||||
|
||||
return HttpResponse.json(
|
||||
resource([
|
||||
{
|
||||
apiTradeAvailable: true,
|
||||
classCode: "TQBR",
|
||||
instrumentUid: "unique-stock",
|
||||
isin: "RU0009029540",
|
||||
logoName: "sber.png",
|
||||
name: "Сбербанк",
|
||||
price: { currency: "RUB", value: "306.12" },
|
||||
priceAt: "2026-09-10T10:00:00Z",
|
||||
realExchange: "REAL_EXCHANGE_MOEX",
|
||||
shareType: "SHARE_TYPE_COMMON",
|
||||
slug: "SBER",
|
||||
ticker: "SBER",
|
||||
},
|
||||
]),
|
||||
);
|
||||
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({
|
||||
@@ -46,7 +46,6 @@ describe("stock search", () => {
|
||||
clear: vi.fn(),
|
||||
refresh: () => Promise.resolve(false),
|
||||
});
|
||||
vi.useFakeTimers();
|
||||
render(
|
||||
<QueryClientProvider client={createTestQueryClient()}>
|
||||
<MantineProvider>
|
||||
@@ -56,23 +55,148 @@ describe("stock search", () => {
|
||||
</MantineProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
const input = screen.getByLabelText("Поиск акций");
|
||||
fireEvent.change(input, { target: { value: "S" } });
|
||||
await act(() => vi.advanceTimersByTimeAsync(300));
|
||||
expect(query).not.toHaveBeenCalled();
|
||||
fireEvent.change(input, { target: { value: "SB" } });
|
||||
await act(() => vi.advanceTimersByTimeAsync(299));
|
||||
expect(query).not.toHaveBeenCalled();
|
||||
// The final input uses real timers for network and query notifications.
|
||||
vi.useRealTimers();
|
||||
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/SBER");
|
||||
expect(screen.getByText("306,12 ₽")).toBeVisible();
|
||||
expect(screen.getByText("Режим: TQBR")).toBeVisible();
|
||||
).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.getByText("Мосбиржа")).toBeVisible();
|
||||
await waitFor(() => expect(query).toHaveBeenCalledExactlyOnceWith("SBER"));
|
||||
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(
|
||||
<QueryClientProvider client={createTestQueryClient()}>
|
||||
<MantineProvider>
|
||||
<MemoryRouter>
|
||||
<StocksPage />
|
||||
</MemoryRouter>
|
||||
</MantineProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
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(
|
||||
<QueryClientProvider client={createTestQueryClient()}>
|
||||
<MantineProvider>
|
||||
<MemoryRouter>
|
||||
<StocksPage />
|
||||
</MemoryRouter>
|
||||
</MantineProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Найти" }));
|
||||
fireEvent.click(await screen.findByRole(
|
||||
"button",
|
||||
{ name: "Страница 2" },
|
||||
));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(requestedPages).toHaveBeenCalledExactlyOnceWith(2),
|
||||
);
|
||||
});
|
||||
|
||||
it("disables filters and shows loading while searching", async () => {
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
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(
|
||||
<QueryClientProvider client={createTestQueryClient()}>
|
||||
<MantineProvider>
|
||||
<MemoryRouter>
|
||||
<StocksPage />
|
||||
</MemoryRouter>
|
||||
</MantineProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Найти" }));
|
||||
|
||||
await waitFor(() => expect(
|
||||
screen.getByRole("combobox", { name: "Сектор" }),
|
||||
).toBeDisabled());
|
||||
expect(screen.getByRole("button", { name: "Найти" })).toHaveAttribute(
|
||||
"data-loading",
|
||||
"true",
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user