Files
frontend/src/pages/account-income/ui/AccountIncomePage.test.tsx
T
ksv741 561222e468 feat: add stock pages and ticker navigation
Add stock search and detail pages with charts, dividends and shared event rows.

Use verified ticker links for portfolio names and logos, numeric dates and refreshed account cards.

Update the generated API contract.
2026-09-10 21:23:41 +03:00

800 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import "dayjs/locale/ru";
import { MantineProvider } from "@mantine/core";
import { DatesProvider } from "@mantine/dates";
import {
act,
fireEvent,
render,
screen,
waitFor,
within,
} from "@testing-library/react";
import { HttpResponse, http } from "msw";
import type { FC } from "react";
import {
MemoryRouter,
Route,
Routes,
useLocation,
useNavigate,
} from "react-router";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { IncomeAnalytics } from "@/entities/income";
import { configureAuthClient } from "@/shared/api";
import { createQueryWrapper } from "@/test/query";
import { server } from "@/test/server";
import { AccountIncomePage } from "./AccountIncomePage";
import { isDate, toIncomeDisplayModel } from "./AccountIncomePage.vm";
import { IncomePayments } from "./components/IncomePayments.view";
const analytics = {
accountId: "account-1",
period: { from: "2026-08-01", to: "2026-08-27" },
calculatedAt: "2026-08-27T00:00:00Z",
totalsByCurrency: [summary("RUB", "10", "2")],
previous: {
period: { from: "2026-07-01", to: "2026-07-31" },
totalsByCurrency: [comparison("RUB")],
},
yearComparison: null,
bucketGranularity: "day",
buckets: [
{
period: { from: "2026-08-01", to: "2026-08-01" },
totalsByCurrency: [summary("RUB", "10", "2")],
},
],
instruments: [
{
key: "income-group-opaque-v2",
figi: "FIGI",
instrumentUid: null,
ticker: "BOND",
instrumentType: "bond",
displayName: "BOND",
coupon: money("RUB", "10"),
dividend: money("RUB", "2"),
total: money("RUB", "12"),
receiptCount: 2,
},
],
entries: [
entry(),
{
...entry(),
id: "dividend-1",
type: "OPERATION_TYPE_DIVIDEND",
payment: money("RUB", "2"),
},
],
} satisfies IncomeAnalytics;
function money(currency: string, value: string) {
return { currency, value };
}
function summary(currency: string, coupon: string, dividend: string) {
return {
currency,
coupon: money(currency, coupon),
dividend: money(currency, dividend),
total: money(currency, String(Number(coupon) + Number(dividend))),
};
}
function comparison(currency: string) {
return {
currency,
selected: summary(currency, "10", "2"),
compared: summary(currency, "1", "0"),
difference: summary(currency, "9", "2"),
};
}
function entry(): IncomeAnalytics["entries"][number] {
return {
id: "coupon-1",
instrumentKey: "income-group-opaque-v2",
occurredAt: "2026-08-01T12:00:00Z",
type: "OPERATION_TYPE_COUPON",
category: "income",
description: "Купон",
figi: "FIGI",
instrumentUid: null,
ticker: "BOND",
instrumentType: "bond",
logoName: null,
payment: money("RUB", "10"),
quantity: null,
price: null,
accruedInterest: null,
commission: null,
};
}
function renderPage(search = "from=2026-08-01&to=2026-08-27") {
configureAuthClient({
accessToken: () => "token",
refresh: async () => true,
clear: vi.fn(),
});
const { wrapper: QueryWrapper, queryClient } = createQueryWrapper();
const result = render(
<MantineProvider env="test">
<DatesProvider settings={{ locale: "ru" }}>
<QueryWrapper>
<MemoryRouter
initialEntries={[`/accounts/account-1/income?${search}`]}
>
<LocationProbe />
<Routes>
<Route
path="/accounts/:accountId/income"
element={<AccountIncomePage />}
/>
<Route
path="/accounts/:accountId/events"
element={<h1>Events</h1>}
/>
</Routes>
</MemoryRouter>
</QueryWrapper>
</DatesProvider>
</MantineProvider>,
);
return { ...result, queryClient };
}
describe("AccountIncomePage", () => {
it("requests a multi-year period from the URL without resetting it", async () => {
const requested: string[] = [];
server.use(
http.get(
"*/api/v1/brokerage-accounts/:accountId/income-analytics",
({ request }) => {
requested.push(new URL(request.url).search);
return HttpResponse.json(analytics);
},
),
);
renderPage("from=2020-01-01&to=2025-12-31");
await waitFor(() =>
expect(requested).toContain("?from=2020-01-01&to=2025-12-31"),
);
});
it("preserves every repeated-ID payment when an open card updates", async () => {
const first = { ...entry(), id: "same-id", description: "Выплата A" };
const second = {
...entry(),
id: "same-id",
description: "Выплата B",
payment: money("RUB", "-2"),
};
const third = {
...entry(),
id: "other-id",
description: "Выплата C",
payment: money("RUB", "1"),
};
function breakdown(entries: IncomeAnalytics["entries"], total: string) {
const display = toIncomeDisplayModel({
...analytics,
instruments: [
{
...analytics.instruments[0],
coupon: money("RUB", total),
dividend: money("RUB", "0"),
total: money("RUB", total),
receiptCount: entries.length,
},
],
entries,
});
expect(display).not.toBeNull();
return (
<MantineProvider env="test">
<IncomePayments entries={display?.entries ?? []} />
</MantineProvider>
);
}
const { rerender } = render(breakdown([first, second, third], "9"));
const panel = await screen.findByRole("region", { name: "Выплаты" });
const payments = () =>
within(panel)
.getAllByText(/^Выплата [ABC]$/)
.map((element) => element.textContent);
expect(payments()).toEqual(["Выплата A", "Выплата B", "Выплата C"]);
rerender(breakdown([third, first, second], "9"));
expect(payments()).toEqual(["Выплата C", "Выплата A", "Выплата B"]);
expect(within(panel).getByText("−2 ₽")).toBeInTheDocument();
rerender(breakdown([second, second, first], "6"));
expect(payments()).toEqual(["Выплата B", "Выплата B", "Выплата A"]);
expect(within(panel).getAllByText("−2 ₽")).toHaveLength(2);
rerender(breakdown([second, first], "8"));
expect(payments()).toEqual(["Выплата B", "Выплата A"]);
expect(within(panel).getAllByRole("listitem")).toHaveLength(2);
});
it.each([
["missing key", undefined],
["empty key", ""],
["whitespace key", " "],
["non-string key", 12],
["unknown key", "not-in-breakdown"],
])("rejects %s and recovers through retry", async (_name, instrumentKey) => {
let response = {
...analytics,
entries: [
{ ...analytics.entries[0], instrumentKey },
analytics.entries[1],
],
};
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(response),
),
);
renderPage();
const retry = await screen.findByRole("button", { name: "Повторить" });
expect(
screen.queryByRole("region", { name: "Выплаты" }),
).not.toBeInTheDocument();
response = analytics;
fireEvent.click(retry);
expect(
await screen.findByRole("region", { name: "Выплаты" }),
).toBeInTheDocument();
});
it.each([
[
"duplicate instrument keys",
{
...analytics,
instruments: [...analytics.instruments, analytics.instruments[0]],
},
],
[
"currency mismatch",
{
...analytics,
entries: [
{ ...analytics.entries[0], payment: money("USD", "10") },
analytics.entries[1],
],
},
],
[
"receipt count mismatch",
{
...analytics,
instruments: [{ ...analytics.instruments[0], receiptCount: 3 }],
},
],
["missing entries", { ...analytics, entries: [] }],
[
"empty instrument key",
{ ...analytics, instruments: [{ ...analytics.instruments[0], key: "" }] },
],
])("shows a local error for %s", async (_name, response) => {
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(response),
),
);
renderPage();
expect(
await screen.findByRole("button", { name: "Повторить" }),
).toBeInTheDocument();
expect(
screen.queryByRole("region", { name: "Выплаты" }),
).not.toBeInTheDocument();
});
it("accepts an empty analytics response", async () => {
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json({
...analytics,
totalsByCurrency: [],
buckets: [],
instruments: [],
entries: [],
}),
),
);
renderPage();
expect(
await screen.findByRole("heading", { name: "Пассивный доход" }),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Повторить" }),
).not.toBeInTheDocument();
});
it("renders the signed drill-down using opaque membership rather than instrument identity", async () => {
const response = {
...analytics,
instruments: [
{
...analytics.instruments[0],
key: "server-only-key",
total: money("RUB", "8"),
dividend: money("RUB", "-2"),
},
],
entries: [
{
...analytics.entries[0],
instrumentKey: "server-only-key",
figi: null,
ticker: null,
description: "Исходная выплата",
},
{
...analytics.entries[1],
instrumentKey: "server-only-key",
instrumentUid: "unrelated-uid",
payment: money("RUB", "-2"),
description: "Отрицательная корректировка",
},
],
};
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(response),
),
);
renderPage();
const panel = await screen.findByRole("region", { name: "Выплаты" });
expect(within(panel).getByText("Исходная выплата")).toBeInTheDocument();
expect(
within(panel).getByText("Отрицательная корректировка"),
).toBeInTheDocument();
expect(within(panel).getByText("−2 ₽")).toBeInTheDocument();
});
it("rejects malformed calendar dates without throwing", () => {
expect(isDate("2026-99-99")).toBe(false);
expect(isDate("2026-02-29")).toBe(false);
expect(isDate("2024-02-29")).toBe(true);
});
it("renders realized text and comparisons without subscribing to events", async () => {
const eventsRequest = vi.fn(() => new HttpResponse(null, { status: 503 }));
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json({
...analytics,
yearComparison: {
period: { from: "2025-08-01", to: "2025-08-27" },
totalsByCurrency: [comparison("RUB")],
},
}),
),
http.get("*/api/v1/brokerage-accounts/account-1/events", eventsRequest),
);
renderPage();
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "Пассивный доход" }),
).toBeInTheDocument(),
);
expect(
screen.getByText("Налоговые операции не включены в этот результат."),
).toBeInTheDocument();
expect(screen.queryByText("Ближайший прогноз")).not.toBeInTheDocument();
expect(screen.queryByText("Динамика")).not.toBeInTheDocument();
expect(eventsRequest).not.toHaveBeenCalled();
expect(screen.getByRole("region", { name: "Выплаты" })).toBeInTheDocument();
const selected = screen.getByRole("region", {
name: "01.08.2026 – 27.08.2026",
});
expect(
screen.getByRole("region", { name: "01.08.2025 – 27.08.2025" }),
).toBeInTheDocument();
expect(screen.queryByText(/Предыдущий период/)).not.toBeInTheDocument();
expect(screen.queryByText("Выбранный период")).not.toBeInTheDocument();
expect(screen.queryByText("Сравниваемый период")).not.toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Годом ранее", level: 2 }),
).toBeInTheDocument();
expect(screen.queryByText("Сравнения")).not.toBeInTheDocument();
expect(screen.queryByText("RUB", { exact: true })).not.toBeInTheDocument();
for (const amount of ["10 ₽", "2 ₽", "12 ₽"]) {
expect(within(selected).getByText(amount)).toBeInTheDocument();
}
const difference = screen.getByRole("region", { name: "Разница" });
for (const amount of ["+9 ₽", "+2 ₽", "+11 ₽"]) {
expect(within(difference).getByText(amount)).toBeInTheDocument();
}
expect(within(difference).getByText("+900 %")).toBeInTheDocument();
expect(within(difference).getByText("+1 100 %")).toBeInTheDocument();
expect(within(difference).queryByText("нет базы")).not.toBeInTheDocument();
expect(within(difference).getAllByText(/%$/)).toHaveLength(2);
expect(
within(difference).getByText("+2 ₽").closest("dd"),
).toHaveTextContent(/^\+2 ₽$/);
});
it("shows the unavailable year state without falling back to the previous period", async () => {
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(analytics),
),
);
renderPage();
expect(
await screen.findByText("Сравнение с годом ранее недоступно."),
).toBeInTheDocument();
expect(
screen.queryByRole("region", { name: "Разница" }),
).not.toBeInTheDocument();
expect(screen.queryByText(/Предыдущий период/)).not.toBeInTheDocument();
});
it("shows a recoverable realized error", async () => {
server.use(
http.get(
"*/api/v1/brokerage-accounts/account-1/income-analytics",
() => new HttpResponse(null, { status: 503 }),
),
);
renderPage();
await waitFor(() =>
expect(
screen.getByRole("button", { name: "Повторить" }),
).toBeInTheDocument(),
);
});
});
describe("income comparison display", () => {
it.each([
["25", "100", "+25 %"],
["-25", "100", "-25 %"],
["0", "100", "0 %"],
["-0.000", "100", "0 %"],
["0.0001", "100", "0 %"],
["1", "3", "+33,3 %"],
["0.01", "0.04", "+25 %"],
["123456789012345678.123456789", "123456789012345678.123456789", "+100 %"],
["10", "0", null],
["0", "0", null],
["10", "-0.00", null],
["10", "-100", null],
["9".repeat(400), "1", null],
["1", "9".repeat(400), null],
])("formats change %s against base %s", (difference, base, expected) => {
const value = comparison("RUB");
value.difference.coupon = money("RUB", difference);
value.compared.coupon = money("RUB", base);
const display = toIncomeDisplayModel({
...analytics,
yearComparison: {
period: { from: "2025-08-01", to: "2025-08-27" },
totalsByCurrency: [value],
},
});
const result = display?.comparison?.values[0];
expect(result?.percentages.coupon?.replaceAll("\u00a0", " ") ?? null).toBe(
expected,
);
expect(result?.difference.coupon.value).toBe(difference);
expect(result?.compared.coupon.value).toBe(base);
});
it("formats calendar ranges across years and preserves separate currencies", () => {
const display = toIncomeDisplayModel({
...analytics,
period: { from: "2025-12-01", to: "2026-01-09" },
yearComparison: {
period: { from: "2024-12-01", to: "2025-01-09" },
totalsByCurrency: [comparison("RUB"), comparison("USD")],
},
});
expect(display?.comparison?.selectedPeriod.replaceAll(/\s/g, " ")).toBe(
"01.12.2025 – 09.01.2026",
);
expect(display?.comparison?.comparedPeriod.replaceAll(/\s/g, " ")).toBe(
"01.12.2024 – 09.01.2025",
);
expect(display?.comparison?.values.map((value) => value.currency)).toEqual([
"RUB",
"USD",
]);
});
});
const LocationProbe: FC = () => {
const location = useLocation();
const navigate = useNavigate();
return (
<>
<output data-testid="location">{location.search}</output>
<button type="button" onClick={() => navigate(-1)}>
Back
</button>
</>
);
};
function inputDate(label: string, value: string) {
const field = screen.getByLabelText(label);
fireEvent.focus(field);
fireEvent.input(field, { target: { value } });
fireEvent.blur(field);
return field;
}
function respondWithRequestedPeriod(requests: string[]) {
server.use(
http.get(
"*/api/v1/brokerage-accounts/account-1/income-analytics",
({ request }) => {
const url = new URL(request.url);
requests.push(url.search);
return HttpResponse.json({
...analytics,
period: {
from: url.searchParams.get("from"),
to: url.searchParams.get("to"),
},
});
},
),
);
}
describe("income period and snapshot boundary", () => {
afterEach(() => vi.useRealTimers());
it("restores formatted URL dates, keeps draft unapplied, submits ISO and follows browser history", async () => {
const requests: string[] = [];
respondWithRequestedPeriod(requests);
renderPage();
expect(await screen.findByLabelText("С")).toHaveValue("01.08.2026");
expect(screen.getByLabelText("По")).toHaveValue("27.08.2026");
inputDate("С", "02.08.2026");
expect(requests).toHaveLength(1);
expect(
screen.queryByText("2026-08-01 — 2026-08-27"),
).not.toBeInTheDocument();
expect(screen.getByTestId("location")).toHaveTextContent(
"from=2026-08-01&to=2026-08-27",
);
fireEvent.submit(
screen
.getByRole("button", { name: "Применить" })
.closest("form") as HTMLFormElement,
);
await waitFor(() => expect(requests).toHaveLength(2));
expect(await screen.findByLabelText("С")).toHaveValue("02.08.2026");
expect(screen.getByTestId("location")).toHaveTextContent(
"from=2026-08-02&to=2026-08-27",
);
fireEvent.click(screen.getByRole("button", { name: "Back" }));
await waitFor(() =>
expect(screen.getByLabelText("С")).toHaveValue("01.08.2026"),
);
});
it.each([
["С", "", "Введите существующую дату"],
["С", "31.02.2026", "Введите существующую дату"],
["По", "29.02.2026", "Введите существующую дату"],
["По", "2026-08-02", "Введите существующую дату"],
["С", "01.01.2099", "Дата не может быть позже"],
["По", "01.01.2099", "Дата не может быть позже"],
["По", "31.07.2026", "Укажите корректный период"],
])(
"rejects visible %s=%s without replacing data or URL",
async (label, value, error) => {
const requests: string[] = [];
respondWithRequestedPeriod(requests);
renderPage();
await screen.findByLabelText("С");
const field = inputDate(label, value);
fireEvent.click(screen.getByRole("button", { name: "Применить" }));
expect(await screen.findByText(new RegExp(error))).toBeInTheDocument();
expect(field).toHaveValue(value);
expect(field).toHaveAttribute("aria-invalid", "true");
await waitFor(() => expect(field).toHaveFocus());
expect(requests).toHaveLength(1);
expect(screen.getByTestId("location")).toHaveTextContent(
"from=2026-08-01&to=2026-08-27",
);
expect(
screen.queryByText("2026-08-01 — 2026-08-27"),
).not.toBeInTheDocument();
expect(screen.getByRole("region", { name: "Выплаты" })).toBeVisible();
},
);
it("replaces invalid raw text with a calendar choice and closes the calendar with Escape", async () => {
respondWithRequestedPeriod([]);
renderPage();
const from = await screen.findByLabelText("С");
act(() => from.focus());
fireEvent.input(from, { target: { value: "invalid" } });
// The selected month stays available even when the visible draft is invalid.
const day = await screen.findByRole("button", {
name: /^2 августа 2026$/i,
});
fireEvent.click(day);
expect(from).toHaveValue("02.08.2026");
act(() => from.focus());
fireEvent.keyDown(from, { key: "Escape" });
await waitFor(() =>
expect(
screen.queryByRole("button", { name: /^2 августа 2026$/i }),
).not.toBeInTheDocument(),
);
});
it("resets a dirty default period and errors using Moscow's day across a UTC month boundary", async () => {
vi.useFakeTimers({ toFake: ["Date"] });
vi.setSystemTime(new Date("2026-08-31T21:30:00Z"));
const requests: string[] = [];
respondWithRequestedPeriod(requests);
renderPage("");
expect(await screen.findByLabelText("С")).toHaveValue("01.09.2026");
expect(screen.getByLabelText("По")).toHaveValue("01.09.2026");
inputDate("С", "wrong");
fireEvent.click(screen.getByRole("button", { name: "Применить" }));
await screen.findByText(/Введите существующую дату/);
fireEvent.click(screen.getByRole("button", { name: "Сбросить" }));
await waitFor(() =>
expect(screen.getByLabelText("С")).toHaveValue("01.09.2026"),
);
expect(
screen.queryByText(/Введите существующую дату/),
).not.toBeInTheDocument();
expect(screen.getByLabelText("По")).toHaveValue("01.09.2026");
expect(screen.getByTestId("location")).toHaveTextContent(
"from=2026-09-01&to=2026-09-01",
);
expect(requests).toEqual(["?from=2026-09-01&to=2026-09-01"]);
});
it("keeps valid data and draft during refetch, warns on failure and retries", async () => {
respondWithRequestedPeriod([]);
const { queryClient } = renderPage();
await screen.findByLabelText("С");
inputDate("С", "02.08.2026");
let release!: () => void;
const pending = new Promise<void>((resolve) => {
release = resolve;
});
server.use(
http.get(
"*/api/v1/brokerage-accounts/account-1/income-analytics",
async () => {
await pending;
return new HttpResponse(null, { status: 503 });
},
),
);
act(() => {
void queryClient.invalidateQueries();
});
expect(
await screen.findByRole("status", { name: "Обновляем пассивный доход" }),
).toBeInTheDocument();
expect(screen.getByRole("region", { name: "Выплаты" })).toBeVisible();
expect(screen.getByLabelText("С")).toHaveValue("02.08.2026");
release();
expect(
await screen.findByText(/Показан последний загруженный результат/),
).toBeInTheDocument();
expect(screen.getByRole("region", { name: "Выплаты" })).toBeVisible();
respondWithRequestedPeriod([]);
fireEvent.click(screen.getByRole("button", { name: "Повторить" }));
await waitFor(() =>
expect(
screen.queryByText(/Показан последний загруженный результат/),
).not.toBeInTheDocument(),
);
expect(screen.getByLabelText("С")).toHaveValue("02.08.2026");
});
it("shows busy skeleton for a new uncached period without relabeling the previous result", async () => {
respondWithRequestedPeriod([]);
renderPage();
await screen.findByLabelText("С");
let release!: () => void;
const pending = new Promise<void>((resolve) => {
release = resolve;
});
server.use(
http.get(
"*/api/v1/brokerage-accounts/account-1/income-analytics",
async () => {
await pending;
return HttpResponse.json({
...analytics,
period: { from: "2026-08-02", to: "2026-08-27" },
});
},
),
);
inputDate("С", "02.08.2026");
fireEvent.click(screen.getByRole("button", { name: "Применить" }));
expect(
await screen.findByRole("status", { name: "Загрузка" }),
).toHaveAttribute("aria-busy", "true");
expect(
screen.queryByRole("region", { name: "Выплаты" }),
).not.toBeInTheDocument();
expect(
screen.queryByText("2026-08-02 — 2026-08-27"),
).not.toBeInTheDocument();
release();
expect(await screen.findByLabelText("С")).toHaveValue("02.08.2026");
});
it("shows one flat payment card per entry in server order without currency or instrument groups", () => {
const instruments = [
{
...analytics.instruments[0],
key: "usd",
total: money("USD", "10"),
coupon: money("USD", "10"),
dividend: money("USD", "0"),
receiptCount: 1,
},
{
...analytics.instruments[0],
key: "rub",
total: money("RUB", "-2"),
coupon: money("RUB", "-2"),
dividend: money("RUB", "0"),
receiptCount: 1,
},
{
...analytics.instruments[0],
key: "rub-negative",
displayName: "Другой инструмент",
total: money("RUB", "-3"),
coupon: money("RUB", "-3"),
dividend: money("RUB", "0"),
receiptCount: 1,
},
];
const display = toIncomeDisplayModel({
...analytics,
totalsByCurrency: [summary("RUB", "-5", "0"), summary("USD", "10", "0")],
instruments,
entries: instruments.map((instrument) => ({
...entry(),
instrumentKey: instrument.key,
payment: instrument.total,
})),
});
render(
<MantineProvider env="test">
<IncomePayments entries={display?.entries ?? []} />
</MantineProvider>,
);
expect(
screen.getByRole("heading", { name: "Выплаты" }),
).toBeInTheDocument();
expect(screen.queryByRole("heading", { level: 3 })).not.toBeInTheDocument();
expect(screen.queryByRole("heading", { level: 4 })).not.toBeInTheDocument();
expect(screen.queryByText("RUB")).not.toBeInTheDocument();
expect(screen.queryByText("USD")).not.toBeInTheDocument();
const cards = screen.getAllByRole("listitem");
expect(cards).toHaveLength(3);
expect(cards[0]).toHaveTextContent("+10 USD");
expect(cards[1]).toHaveTextContent("−2 ₽");
expect(cards[2]).toHaveTextContent("−3 ₽");
expect(screen.queryByText(/Итого:|Выплат:/)).not.toBeInTheDocument();
expect(screen.queryByRole("button")).not.toBeInTheDocument();
expect(screen.getAllByText("Купон")).toHaveLength(6);
});
});