refactor: simplify errors and focus tests
This commit is contained in:
@@ -47,7 +47,7 @@ throwing route, безопасная ссылка и reset после navigation
|
|||||||
|
|
||||||
Примеры: [AuthForm test](../../src/features/auth/ui/AuthForm/AuthForm.test.tsx),
|
Примеры: [AuthForm test](../../src/features/auth/ui/AuthForm/AuthForm.test.tsx),
|
||||||
[client test](../../src/shared/api/client.test.ts),
|
[client test](../../src/shared/api/client.test.ts),
|
||||||
[usePortfolio test](../../src/entities/portfolio/api/usePortfolio.test.tsx).
|
[resource lifecycle test](../../src/entities/portfolio/api/resource-lifecycle.test.tsx).
|
||||||
Не копируй fixtures/сценарии целиком, если задача затрагивает один исход.
|
Не копируй fixtures/сценарии целиком, если задача затрагивает один исход.
|
||||||
|
|
||||||
## Команды
|
## Команды
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import { renderHook, waitFor } from "@testing-library/react";
|
|
||||||
import { http, HttpResponse } from "msw";
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import { configureAuthClient } from "@/shared/api";
|
|
||||||
import { createQueryWrapper } from "@/test/query";
|
|
||||||
import { resource } from "@/test/resource";
|
|
||||||
import { server } from "@/test/server";
|
|
||||||
|
|
||||||
import { useBrokerageAccounts } from "./useBrokerageAccounts";
|
|
||||||
|
|
||||||
describe("useBrokerageAccounts", () => {
|
|
||||||
it("loads accounts through the authenticated query seam", async () => {
|
|
||||||
configureAuthClient({
|
|
||||||
accessToken: () => "token",
|
|
||||||
clear: vi.fn(),
|
|
||||||
refresh: () => Promise.resolve(true),
|
|
||||||
});
|
|
||||||
server.use(
|
|
||||||
http.get("*/api/v1/brokerage-accounts", () =>
|
|
||||||
HttpResponse.json(
|
|
||||||
resource([
|
|
||||||
{
|
|
||||||
id: "account-1",
|
|
||||||
name: "Основной",
|
|
||||||
openedOn: "2024-08-13",
|
|
||||||
type: "BROKERAGE",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useBrokerageAccounts(), {
|
|
||||||
wrapper: createQueryWrapper().wrapper,
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
||||||
expect(result.current.data).toEqual([
|
|
||||||
{
|
|
||||||
id: "account-1",
|
|
||||||
name: "Основной",
|
|
||||||
openedOn: "2024-08-13",
|
|
||||||
type: "BROKERAGE",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps the current connection snapshot after a background refetch error", async () => {
|
|
||||||
configureAuthClient({
|
|
||||||
accessToken: () => "token",
|
|
||||||
clear: vi.fn(),
|
|
||||||
refresh: () => Promise.resolve(true),
|
|
||||||
});
|
|
||||||
let calls = 0;
|
|
||||||
server.use(
|
|
||||||
http.get("*/api/v1/brokerage-accounts", () => {
|
|
||||||
calls += 1;
|
|
||||||
|
|
||||||
return calls === 1
|
|
||||||
? HttpResponse.json(
|
|
||||||
resource([
|
|
||||||
{
|
|
||||||
id: "account-1",
|
|
||||||
name: "Основной",
|
|
||||||
openedOn: "2024-08-13",
|
|
||||||
type: "BROKERAGE",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
: HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useBrokerageAccounts(), {
|
|
||||||
wrapper: createQueryWrapper().wrapper,
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
||||||
void result.current.refetch();
|
|
||||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
||||||
|
|
||||||
expect(result.current.data?.[0].id).toBe("account-1");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { renderHook, waitFor } from "@testing-library/react";
|
|
||||||
import { http, HttpResponse } from "msw";
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import { configureAuthClient } from "@/shared/api";
|
|
||||||
import { createQueryWrapper } from "@/test/query";
|
|
||||||
import { resource } from "@/test/resource";
|
|
||||||
import { server } from "@/test/server";
|
|
||||||
|
|
||||||
import { usePortfolio } from "./usePortfolio";
|
|
||||||
|
|
||||||
describe("usePortfolio", () => {
|
|
||||||
it("loads the account portfolio", async () => {
|
|
||||||
configureAuthClient({
|
|
||||||
accessToken: () => "token",
|
|
||||||
clear: vi.fn(),
|
|
||||||
refresh: () => Promise.resolve(true),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { result } = renderHook(() => usePortfolio("account-1"), {
|
|
||||||
wrapper: createQueryWrapper().wrapper,
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
||||||
expect(result.current.data).toMatchObject({ positions: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps the current connection snapshot after a background refetch error", async () => {
|
|
||||||
configureAuthClient({
|
|
||||||
accessToken: () => "token",
|
|
||||||
clear: vi.fn(),
|
|
||||||
refresh: () => Promise.resolve(true),
|
|
||||||
});
|
|
||||||
let calls = 0;
|
|
||||||
server.use(
|
|
||||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => {
|
|
||||||
calls += 1;
|
|
||||||
|
|
||||||
return calls === 1
|
|
||||||
? HttpResponse.json(
|
|
||||||
resource({
|
|
||||||
cashBalances: [],
|
|
||||||
isStale: false,
|
|
||||||
lastSuccessfulAt: "2026-08-14T00:00:00Z",
|
|
||||||
positions: [],
|
|
||||||
structure: { currencies: [], unvaluedCategories: [] },
|
|
||||||
totalsByCurrency: [],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
: HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { result } = renderHook(() => usePortfolio("account-1"), {
|
|
||||||
wrapper: createQueryWrapper().wrapper,
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
||||||
void result.current.refetch();
|
|
||||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
||||||
|
|
||||||
expect(result.current.data).toMatchObject({
|
|
||||||
lastSuccessfulAt: "2026-08-14T00:00:00Z",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+31
-15
@@ -292,31 +292,47 @@ describe("TbankConnectionSection", () => {
|
|||||||
expect(client.getQueryData(operationsQueryKey)).toBeUndefined();
|
expect(client.getQueryData(operationsQueryKey)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves status and financial cache when a connection update fails", async () => {
|
it.each([
|
||||||
server.use(
|
["INVALID_TBANK_TOKEN", "Проверьте token и его права."],
|
||||||
http.put("*/api/v1/tbank-connection", () =>
|
["TBANK_UNAVAILABLE", "T-Bank временно недоступен. Повторите попытку позже."],
|
||||||
HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 }),
|
[undefined, "Не удалось выполнить запрос. Попробуйте ещё раз."],
|
||||||
),
|
])("preserves the draft and cache after %s, then retries successfully", async (code, message) => {
|
||||||
);
|
const { promise: response, resolve: finishUpdate }
|
||||||
|
= Promise.withResolvers<Response>();
|
||||||
|
|
||||||
|
const update = vi.fn()
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
HttpResponse.json({ code }, { status: 503 }),
|
||||||
|
)
|
||||||
|
.mockImplementationOnce(() => response);
|
||||||
|
|
||||||
|
server.use(http.put("*/api/v1/tbank-connection", update));
|
||||||
const client = seededClient();
|
const client = seededClient();
|
||||||
renderSection(client);
|
renderSection(client);
|
||||||
|
|
||||||
await screen.findByText("Не подключено");
|
await screen.findByText("Не подключено");
|
||||||
fireEvent.change(screen.getByLabelText("Token T-Bank"), {
|
const token = screen.getByLabelText("Token T-Bank");
|
||||||
target: { value: "replacement-token" },
|
fireEvent.change(token, { target: { value: "replacement-token" } });
|
||||||
});
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Подключить" }));
|
fireEvent.click(screen.getByRole("button", { name: "Подключить" }));
|
||||||
|
|
||||||
await screen.findByText(
|
await screen.findByText(message);
|
||||||
"T-Bank временно недоступен. Повторите попытку позже.",
|
expect(token).toHaveValue("replacement-token");
|
||||||
);
|
expect(screen.getByText("Не подключено")).toBeVisible();
|
||||||
expect(
|
|
||||||
screen.getByRole("button", { name: "Подключить" }),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(client.getQueryData(brokerageAccountsQueryKey)).toBeDefined();
|
expect(client.getQueryData(brokerageAccountsQueryKey)).toBeDefined();
|
||||||
expect(client.getQueryData(portfolioQueryKey)).toEqual({
|
expect(client.getQueryData(portfolioQueryKey)).toEqual({
|
||||||
source: "previous-connection",
|
source: "previous-connection",
|
||||||
});
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Подключить" }));
|
||||||
|
await waitFor(() => expect(update).toHaveBeenCalledTimes(2));
|
||||||
|
expect(screen.queryByText(message)).not.toBeInTheDocument();
|
||||||
|
expect(token).toBeDisabled();
|
||||||
|
expect(token).toHaveValue("replacement-token");
|
||||||
|
finishUpdate(HttpResponse.json({
|
||||||
|
connected: true, connectedAt: "2026-08-14T00:00:00Z",
|
||||||
|
}));
|
||||||
|
expect(await screen.findByText("Подключено с 14.08.2026")).toBeVisible();
|
||||||
|
expect(token).toHaveValue("");
|
||||||
|
expect(screen.queryByText(message)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps DELETE recovery inside the dialog and preserves cache until successful retry", async () => {
|
it("keeps DELETE recovery inside the dialog and preserves cache until successful retry", async () => {
|
||||||
|
|||||||
+2
-2
@@ -41,7 +41,7 @@ const TbankConnectionSectionLoaded: FC<TbankConnectionSectionLoadedProps> = (
|
|||||||
t,
|
t,
|
||||||
unavailableMessage,
|
unavailableMessage,
|
||||||
update,
|
update,
|
||||||
} = useTbankConnectionSectionLoadedVm(props);
|
} = useTbankConnectionSectionLoadedVm(props.status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack mt="xl">
|
<Stack mt="xl">
|
||||||
@@ -92,7 +92,7 @@ const TbankConnectionSectionLoaded: FC<TbankConnectionSectionLoadedProps> = (
|
|||||||
error={errors.token?.message}
|
error={errors.token?.message}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{requestError && (
|
{update.isError && (
|
||||||
<Alert color="red">
|
<Alert color="red">
|
||||||
{t(
|
{t(
|
||||||
isApiErrorCode(requestError, ApiErrorCode.InvalidTbankToken)
|
isApiErrorCode(requestError, ApiErrorCode.InvalidTbankToken)
|
||||||
|
|||||||
+3
-18
@@ -14,26 +14,12 @@ import {
|
|||||||
commitTbankConnectionBoundary,
|
commitTbankConnectionBoundary,
|
||||||
type TbankConnectionStatus,
|
type TbankConnectionStatus,
|
||||||
} from "../../model/connection-boundary";
|
} from "../../model/connection-boundary";
|
||||||
enum ConnectionErrorCode {
|
|
||||||
RequestFailed = "REQUEST_ERROR",
|
|
||||||
}
|
|
||||||
|
|
||||||
type TbankConnectionSectionLoadedProps = {
|
function useTbankConnectionSectionLoadedVm(status: TbankConnectionStatus) {
|
||||||
refreshError: boolean;
|
|
||||||
refreshing: boolean;
|
|
||||||
retry: () => void;
|
|
||||||
status: TbankConnectionStatus;
|
|
||||||
};
|
|
||||||
|
|
||||||
function useTbankConnectionSectionLoadedVm(
|
|
||||||
props: TbankConnectionSectionLoadedProps,
|
|
||||||
) {
|
|
||||||
const { status } = props;
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [isDeleteOpened, setIsDeleteOpened] = useState(false);
|
const [isDeleteOpened, setIsDeleteOpened] = useState(false);
|
||||||
const removalButtonRef = useRef<HTMLButtonElement>(null);
|
const removalButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
const [requestError, setRequestError] = useState<null | string>(null);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
@@ -43,8 +29,6 @@ function useTbankConnectionSectionLoadedVm(
|
|||||||
} = useForm<{ token: string }>();
|
} = useForm<{ token: string }>();
|
||||||
|
|
||||||
const update = apiQueryClient.useMutation("put", "/api/v1/tbank-connection", {
|
const update = apiQueryClient.useMutation("put", "/api/v1/tbank-connection", {
|
||||||
onError: error =>
|
|
||||||
setRequestError(apiErrorCode(error) ?? ConnectionErrorCode.RequestFailed),
|
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
commitTbankConnectionBoundary(queryClient, data);
|
commitTbankConnectionBoundary(queryClient, data);
|
||||||
reset();
|
reset();
|
||||||
@@ -66,6 +50,8 @@ function useTbankConnectionSectionLoadedVm(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const requestError = apiErrorCode(update.error);
|
||||||
|
|
||||||
const unavailableMessage = isApiErrorCode(
|
const unavailableMessage = isApiErrorCode(
|
||||||
requestError,
|
requestError,
|
||||||
ApiErrorCode.TbankUnavailable,
|
ApiErrorCode.TbankUnavailable,
|
||||||
@@ -80,7 +66,6 @@ function useTbankConnectionSectionLoadedVm(
|
|||||||
}, [isDeleteOpened, remove.isError]);
|
}, [isDeleteOpened, remove.isError]);
|
||||||
|
|
||||||
const onSubmit = handleSubmit(({ token }) => {
|
const onSubmit = handleSubmit(({ token }) => {
|
||||||
setRequestError(null);
|
|
||||||
update.mutate({ body: { token } });
|
update.mutate({ body: { token } });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -210,23 +210,9 @@ describe("useAccountPageViewModel", () => {
|
|||||||
expect(snapshot).toEqual(original);
|
expect(snapshot).toEqual(original);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prepares grouped position sections with derived display data", async () => {
|
it("prepares grouped position sections with derived display data", () => {
|
||||||
configureTestAuth();
|
|
||||||
mockLoadedAccountPage();
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useAccountPageViewModel(), {
|
|
||||||
wrapper: createWrapper(),
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => expect(result.current.state).toBe("loaded"));
|
|
||||||
|
|
||||||
const loaded = result.current as Extract<
|
|
||||||
typeof result.current,
|
|
||||||
{ state: "loaded" }
|
|
||||||
>;
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
loaded.positionSections.map(({ groups, kind }) => ({
|
toAccountPagePositionSections(portfolio).map(({ groups, kind }) => ({
|
||||||
kind,
|
kind,
|
||||||
positions: groups.flatMap(group => group.positions),
|
positions: groups.flatMap(group => group.positions),
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { CurrencyCards } from "./CurrencyCards.view";
|
import { CurrencyCards } from "./CurrencyCards.view";
|
||||||
|
|
||||||
describe("CurrencyCards currency presentation", () => {
|
describe("CurrencyCards currency presentation", () => {
|
||||||
it.each([0, 1, 3, 5])(
|
it.each([0, 5])(
|
||||||
"renders only the %i supplied totals in upstream order",
|
"renders only the %i supplied totals in upstream order",
|
||||||
(count) => {
|
(count) => {
|
||||||
const totals = [
|
const totals = [
|
||||||
@@ -74,20 +74,4 @@ describe("CurrencyCards currency presentation", () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it("keeps USD and CAD as distinct supplied values", () => {
|
|
||||||
render(
|
|
||||||
<MantineProvider>
|
|
||||||
<CurrencyCards
|
|
||||||
amounts={[
|
|
||||||
{ currency: "USD", value: "1.00" },
|
|
||||||
{ currency: "CAD", value: "2.00" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</MantineProvider>,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
screen.getAllByRole("definition").map(node => node.textContent),
|
|
||||||
).toEqual(["1,00 $", "2,00 CAD"]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { MantineProvider } from "@mantine/core";
|
import { MantineProvider } from "@mantine/core";
|
||||||
import { QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { http, HttpResponse } from "msw";
|
import { http, HttpResponse } from "msw";
|
||||||
import { MemoryRouter } from "react-router";
|
import { MemoryRouter } from "react-router";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { configureAuthClient } from "@/shared/api";
|
import { configureAuthClient } from "@/shared/api";
|
||||||
import { createTestQueryClient } from "@/test/query";
|
import { createTestQueryClient } from "@/test/query";
|
||||||
@@ -12,6 +12,8 @@ import { server } from "@/test/server";
|
|||||||
|
|
||||||
import { BondsPage } from "./BondsPage";
|
import { BondsPage } from "./BondsPage";
|
||||||
|
|
||||||
|
afterEach(() => vi.useRealTimers());
|
||||||
|
|
||||||
describe("bond search", () => {
|
describe("bond search", () => {
|
||||||
it("waits for a meaningful query and uses the server-verified ticker link", async () => {
|
it("waits for a meaningful query and uses the server-verified ticker link", async () => {
|
||||||
const query = vi.fn();
|
const query = vi.fn();
|
||||||
@@ -46,6 +48,7 @@ describe("bond search", () => {
|
|||||||
clear: vi.fn(),
|
clear: vi.fn(),
|
||||||
refresh: () => Promise.resolve(false),
|
refresh: () => Promise.resolve(false),
|
||||||
});
|
});
|
||||||
|
vi.useFakeTimers();
|
||||||
render(
|
render(
|
||||||
<QueryClientProvider client={createTestQueryClient()}>
|
<QueryClientProvider client={createTestQueryClient()}>
|
||||||
<MantineProvider>
|
<MantineProvider>
|
||||||
@@ -55,10 +58,16 @@ describe("bond search", () => {
|
|||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</QueryClientProvider>,
|
</QueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
const input = screen.getByLabelText("Поиск облигаций");
|
||||||
|
fireEvent.change(input, { target: { value: "S" } });
|
||||||
|
await act(() => vi.advanceTimersByTimeAsync(300));
|
||||||
expect(query).not.toHaveBeenCalled();
|
expect(query).not.toHaveBeenCalled();
|
||||||
fireEvent.change(screen.getByLabelText("Поиск облигаций"), {
|
fireEvent.change(input, { target: { value: "SB" } });
|
||||||
target: { value: "SU26238RMFS4" },
|
await act(() => vi.advanceTimersByTimeAsync(299));
|
||||||
});
|
expect(query).not.toHaveBeenCalled();
|
||||||
|
// The final input uses real timers for network and query notifications.
|
||||||
|
vi.useRealTimers();
|
||||||
|
fireEvent.change(input, { target: { value: "SU26238RMFS4" } });
|
||||||
expect(await screen.findByRole("link", { name: /ОФЗ/ })).toHaveAttribute(
|
expect(await screen.findByRole("link", { name: /ОФЗ/ })).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
"/bonds/SU26238RMFS4",
|
"/bonds/SU26238RMFS4",
|
||||||
@@ -67,6 +76,6 @@ describe("bond search", () => {
|
|||||||
expect(screen.getByText("Режим: TQOB")).toBeVisible();
|
expect(screen.getByText("Режим: TQOB")).toBeVisible();
|
||||||
expect(screen.getByText("ISIN: RU0009029540")).toBeVisible();
|
expect(screen.getByText("ISIN: RU0009029540")).toBeVisible();
|
||||||
expect(screen.getByText("Мосбиржа")).toBeVisible();
|
expect(screen.getByText("Мосбиржа")).toBeVisible();
|
||||||
await waitFor(() => expect(query).toHaveBeenCalledWith("SU26238RMFS4"));
|
await waitFor(() => expect(query).toHaveBeenCalledExactlyOnceWith("SU26238RMFS4"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { MantineProvider } from "@mantine/core";
|
import { MantineProvider } from "@mantine/core";
|
||||||
import { QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { http, HttpResponse } from "msw";
|
import { http, HttpResponse } from "msw";
|
||||||
import { MemoryRouter } from "react-router";
|
import { MemoryRouter } from "react-router";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { configureAuthClient } from "@/shared/api";
|
import { configureAuthClient } from "@/shared/api";
|
||||||
import { createTestQueryClient } from "@/test/query";
|
import { createTestQueryClient } from "@/test/query";
|
||||||
@@ -12,6 +12,8 @@ import { server } from "@/test/server";
|
|||||||
|
|
||||||
import { StocksPage } from "./StocksPage";
|
import { StocksPage } from "./StocksPage";
|
||||||
|
|
||||||
|
afterEach(() => vi.useRealTimers());
|
||||||
|
|
||||||
describe("stock search", () => {
|
describe("stock search", () => {
|
||||||
it("waits for a meaningful query and uses the server-verified ticker link", async () => {
|
it("waits for a meaningful query and uses the server-verified ticker link", async () => {
|
||||||
const query = vi.fn();
|
const query = vi.fn();
|
||||||
@@ -44,6 +46,7 @@ describe("stock search", () => {
|
|||||||
clear: vi.fn(),
|
clear: vi.fn(),
|
||||||
refresh: () => Promise.resolve(false),
|
refresh: () => Promise.resolve(false),
|
||||||
});
|
});
|
||||||
|
vi.useFakeTimers();
|
||||||
render(
|
render(
|
||||||
<QueryClientProvider client={createTestQueryClient()}>
|
<QueryClientProvider client={createTestQueryClient()}>
|
||||||
<MantineProvider>
|
<MantineProvider>
|
||||||
@@ -53,10 +56,16 @@ describe("stock search", () => {
|
|||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</QueryClientProvider>,
|
</QueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
const input = screen.getByLabelText("Поиск акций");
|
||||||
|
fireEvent.change(input, { target: { value: "S" } });
|
||||||
|
await act(() => vi.advanceTimersByTimeAsync(300));
|
||||||
expect(query).not.toHaveBeenCalled();
|
expect(query).not.toHaveBeenCalled();
|
||||||
fireEvent.change(screen.getByLabelText("Поиск акций"), {
|
fireEvent.change(input, { target: { value: "SB" } });
|
||||||
target: { value: "SBER" },
|
await act(() => vi.advanceTimersByTimeAsync(299));
|
||||||
});
|
expect(query).not.toHaveBeenCalled();
|
||||||
|
// The final input uses real timers for network and query notifications.
|
||||||
|
vi.useRealTimers();
|
||||||
|
fireEvent.change(input, { target: { value: "SBER" } });
|
||||||
expect(
|
expect(
|
||||||
await screen.findByRole("link", { name: /Сбербанк/ }),
|
await screen.findByRole("link", { name: /Сбербанк/ }),
|
||||||
).toHaveAttribute("href", "/stocks/SBER");
|
).toHaveAttribute("href", "/stocks/SBER");
|
||||||
@@ -64,6 +73,6 @@ describe("stock search", () => {
|
|||||||
expect(screen.getByText("Режим: TQBR")).toBeVisible();
|
expect(screen.getByText("Режим: TQBR")).toBeVisible();
|
||||||
expect(screen.getByText("ISIN: RU0009029540")).toBeVisible();
|
expect(screen.getByText("ISIN: RU0009029540")).toBeVisible();
|
||||||
expect(screen.getByText("Мосбиржа")).toBeVisible();
|
expect(screen.getByText("Мосбиржа")).toBeVisible();
|
||||||
await waitFor(() => expect(query).toHaveBeenCalledWith("SBER"));
|
await waitFor(() => expect(query).toHaveBeenCalledExactlyOnceWith("SBER"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user