168 lines
4.7 KiB
TypeScript
168 lines
4.7 KiB
TypeScript
import { MantineProvider } from "@mantine/core";
|
||
import { QueryClientProvider } from "@tanstack/react-query";
|
||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||
import { http, HttpResponse } from "msw";
|
||
import { MemoryRouter, Route, Routes } from "react-router";
|
||
import { 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 { AccountEventsPage } from "./AccountEventsPage";
|
||
|
||
// Automatic read retries wait 1s + 2s before the failure is final.
|
||
const RETRIES_EXHAUSTED_MS = 6000;
|
||
const TEST_MS = 10_000;
|
||
|
||
const snapshot = resource({
|
||
accountId: "account-1",
|
||
calculatedAt: "2026-08-14T00:00:00Z",
|
||
events: [
|
||
{
|
||
date: "2026-10-01",
|
||
dateKind: "event",
|
||
displayName: "Synthetic coupon",
|
||
figi: null,
|
||
instrumentUid: null,
|
||
isin: null,
|
||
logoName: null,
|
||
projectedAmount: { currency: "RUB", value: "123.45" },
|
||
ticker: "SYN",
|
||
type: "coupon",
|
||
},
|
||
],
|
||
incomeTotalsByCurrency: [{ currency: "RUB", value: "123.45" }],
|
||
window: { from: "2026-08-14", to: "2026-09-13" },
|
||
});
|
||
|
||
function renderEvents() {
|
||
configureAuthClient({
|
||
accessToken: () => "token",
|
||
clear: vi.fn(),
|
||
refresh: () => Promise.resolve(true),
|
||
});
|
||
const queryClient = createTestQueryClient();
|
||
|
||
render(
|
||
<QueryClientProvider client={queryClient}>
|
||
<MantineProvider env="test">
|
||
<MemoryRouter initialEntries={["/accounts/account-1/events"]}>
|
||
<Routes>
|
||
<Route
|
||
element={<AccountEventsPage />}
|
||
path="/accounts/:accountId/events"
|
||
/>
|
||
</Routes>
|
||
</MemoryRouter>
|
||
</MantineProvider>
|
||
</QueryClientProvider>,
|
||
);
|
||
|
||
return { queryClient };
|
||
}
|
||
|
||
describe("AccountEventsPage read retry", () => {
|
||
it(
|
||
"offers retry after an initial failure and recovers in place",
|
||
{ timeout: TEST_MS },
|
||
async () => {
|
||
let isFailing = true;
|
||
let requests = 0;
|
||
server.use(
|
||
http.get("*/api/v1/brokerage-accounts/:accountId/events", () => {
|
||
requests += 1;
|
||
|
||
return isFailing
|
||
? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 })
|
||
: HttpResponse.json(snapshot);
|
||
}),
|
||
);
|
||
renderEvents();
|
||
|
||
const retry = await screen.findByRole(
|
||
"button",
|
||
{ name: "Повторить" },
|
||
{ timeout: RETRIES_EXHAUSTED_MS },
|
||
);
|
||
|
||
expect(requests).toBe(3);
|
||
|
||
isFailing = false;
|
||
fireEvent.click(retry);
|
||
|
||
expect(await screen.findByText("Купон · Synthetic coupon")).toBeInTheDocument();
|
||
expect(requests).toBe(4);
|
||
},
|
||
);
|
||
|
||
it(
|
||
"keeps cached events, warns and retries the same GET",
|
||
{ timeout: TEST_MS },
|
||
async () => {
|
||
let isFailing = false;
|
||
let requests = 0;
|
||
let imports = 0;
|
||
server.use(
|
||
http.post("*/api/v1/tbank-sync", () => {
|
||
imports += 1;
|
||
|
||
return HttpResponse.json({}, { status: 202 });
|
||
}),
|
||
http.get("*/api/v1/brokerage-accounts/:accountId/events", () => {
|
||
requests += 1;
|
||
|
||
return isFailing
|
||
? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 })
|
||
: HttpResponse.json(snapshot);
|
||
}),
|
||
);
|
||
const { queryClient } = renderEvents();
|
||
await screen.findByText("Купон · Synthetic coupon");
|
||
expect(screen.getByText(/Прогноз от .* на 14\.08\.2026 – 13\.09\.2026/)).toBeVisible();
|
||
expect(screen.queryByRole("button", { name: "Повторить" }))
|
||
.not.toBeInTheDocument();
|
||
|
||
isFailing = true;
|
||
void queryClient.invalidateQueries({
|
||
queryKey: ["get", "/api/v1/brokerage-accounts/{accountId}/events"],
|
||
});
|
||
|
||
const retry = await screen.findByRole(
|
||
"button",
|
||
{ name: "Повторить" },
|
||
{ timeout: RETRIES_EXHAUSTED_MS },
|
||
);
|
||
|
||
expect(screen.getByText("Не удалось обновить данные")).toBeInTheDocument();
|
||
expect(screen.getByText("Купон · Synthetic coupon")).toBeInTheDocument();
|
||
expect(screen.getByText(/Прогноз от .* на 14\.08\.2026 – 13\.09\.2026/)).toBeVisible();
|
||
expect(requests).toBe(4);
|
||
|
||
isFailing = false;
|
||
fireEvent.click(retry);
|
||
|
||
await waitFor(() =>
|
||
expect(screen.queryByText("Не удалось обновить данные"))
|
||
.not.toBeInTheDocument());
|
||
expect(requests).toBe(5);
|
||
expect(imports).toBe(0);
|
||
},
|
||
);
|
||
|
||
it("does not offer a plain retry when the account is gone", async () => {
|
||
server.use(
|
||
http.get("*/api/v1/brokerage-accounts/:accountId/events", () =>
|
||
HttpResponse.json({ code: "NOT_FOUND" }, { status: 404 })),
|
||
);
|
||
renderEvents();
|
||
|
||
await waitFor(() =>
|
||
expect(screen.queryByRole("heading", { name: "Загрузка" }))
|
||
.not.toBeInTheDocument());
|
||
expect(screen.queryByRole("button", { name: "Повторить" }))
|
||
.not.toBeInTheDocument();
|
||
});
|
||
});
|