Files
frontend/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.test.tsx
T
codex-bot 2b94a16ae5
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 11s
Frontend verify / verify (push) Failing after 9m0s
refactor!: support RUB only (WealthLedger/main#55) (#168)
2026-11-11 09:36:29 +03:00

899 lines
26 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 type { FC } from "react";
import { MantineProvider } from "@mantine/core";
import { DatesProvider } from "@mantine/dates";
import { QueryClientProvider } from "@tanstack/react-query";
import {
act,
fireEvent,
render,
renderHook,
screen,
waitFor,
within,
} from "@testing-library/react";
import "dayjs/locale/ru";
import { http, HttpResponse } from "msw";
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 { formatSnapshotTime } from "@/shared/lib";
import { createTestQueryClient } from "@/test/query";
import { resource } from "@/test/resource";
import { server } from "@/test/server";
import { toIncomeDisplayModel } from "../../model/display";
import { AccountIncomePage } from "./AccountIncomePage";
import {
useAccountIncomePageVm,
} from "./AccountIncomePage.vm";
import { IncomePayments } from "./components/IncomePayments.view";
// Automatic read retries wait 1s + 2s before the failure is final.
const RETRIES_EXHAUSTED_MS = 6000;
const TEST_MS = 10_000;
const analytics
= {
accountId: "account-1",
bucketGranularity: "day",
buckets: [
{
period: { from: "2026-08-01", to: "2026-08-01" },
total: summary("10", "2"),
},
],
calculatedAt: "2026-08-27T00:00:00Z",
entries: [
entry(),
{
...entry(),
id: "dividend-1",
payment: money("RUB", "2"),
type: "OPERATION_TYPE_DIVIDEND",
},
],
instruments: [
{
coupon: money("RUB", "10"),
displayName: "BOND",
dividend: money("RUB", "2"),
figi: "FIGI",
instrumentType: "bond",
instrumentUid: null,
key: "income-group-opaque-v2",
receiptCount: 2,
ticker: "BOND",
total: money("RUB", "12"),
},
],
period: { from: "2026-08-01", to: "2026-08-27" },
total: summary("10", "2"),
} satisfies IncomeAnalytics & { instruments: [unknown] };
function entry(): IncomeAnalytics["entries"][number] {
return {
accruedInterest: null,
category: "income",
commission: null,
description: "Купон",
figi: "FIGI",
id: "coupon-1",
instrumentKey: "income-group-opaque-v2",
instrumentType: "bond",
instrumentUid: null,
logoName: null,
occurredAt: "2026-08-01T12:00:00Z",
payment: money("RUB", "10"),
price: null,
quantity: null,
ticker: "BOND",
type: "OPERATION_TYPE_COUPON",
};
}
function money(currency: string, value: string) {
return { currency, value };
}
function renderPage(
search = "from=2026-08-01&to=2026-08-27",
accountId = "account-1",
) {
configureAuthClient({
accessToken: () => "token",
clear: vi.fn(),
refresh: () => Promise.resolve(true),
});
const queryClient = createTestQueryClient();
const view = render(
<MantineProvider env="test">
<DatesProvider settings={{ locale: "ru" }}>
<QueryClientProvider client={queryClient}>
<MemoryRouter
initialEntries={[`/accounts/${accountId}/income?${search}`]}
>
<LocationProbe />
<Routes>
<Route
element={<AccountIncomePage />}
path="/accounts/:accountId/income"
/>
<Route
element={<h1>Events</h1>}
path="/accounts/:accountId/events"
/>
</Routes>
</MemoryRouter>
</QueryClientProvider>
</DatesProvider>
</MantineProvider>,
);
return { ...view, queryClient };
}
function summary(coupon: string, dividend: string) {
return {
coupon: money("RUB", coupon),
dividend: money("RUB", dividend),
total: money("RUB", String(Number(coupon) + Number(dividend))),
};
}
describe("AccountIncomePage", () => {
it("keeps reads and sync disabled without an account scope", async () => {
configureAuthClient({
accessToken: () => "token",
clear: vi.fn(),
refresh: () => Promise.resolve(true),
});
const requests: string[] = [];
server.use(
http.all("*/api/v1/*", ({ request }) => {
requests.push(request.url);
return new HttpResponse(null, { status: 503 });
}),
);
const queryClient = createTestQueryClient();
const { result } = renderHook(() => useAccountIncomePageVm(), {
wrapper: ({ children }) => (
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={["?from=2024-02-29&to=2024-03-01"]}>
{children}
</MemoryRouter>
</QueryClientProvider>
),
});
await new Promise(resolve => setTimeout(resolve, 0));
expect(result.current).toEqual({ state: "loading" });
expect(queryClient.isFetching()).toBe(0);
expect(requests).toEqual([]);
});
it("keeps income calendar dates in GET and source sync requests", 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(
resource(
null,
{
lastAttempt: {
finishedAt: "2026-08-14T00:01:00Z",
problems: [],
startedAt: "2026-08-14T00:00:00Z",
state: "failed",
},
},
),
);
},
),
);
renderPage("from=2024-02-29&to=2024-03-01");
await screen.findByText("Не удалось рассчитать пассивный доход");
expect(requested).toEqual(["?from=2024-02-29&to=2024-03-01"]);
expect(screen.queryByRole("button", { name: "Повторить" })).not.toBeInTheDocument();
});
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(resource(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(), description: "Выплата A", id: "same-id" };
const second = {
...entry(),
description: "Выплата B",
id: "same-id",
payment: money("RUB", "-2"),
};
const third = {
...entry(),
description: "Выплата C",
id: "other-id",
payment: money("RUB", "1"),
};
function breakdown(entries: IncomeAnalytics["entries"], total: string) {
const display = toIncomeDisplayModel({
...analytics,
entries,
instruments: [
{
...analytics.instruments[0],
coupon: money("RUB", total),
dividend: money("RUB", "0"),
receiptCount: entries.length,
total: money("RUB", total),
},
],
});
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("accepts an empty analytics response", async () => {
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(
resource({
...analytics,
buckets: [],
entries: [],
instruments: [],
total: summary("0", "0"),
}),
),
),
);
renderPage();
expect(
await screen.findByRole("heading", { name: "Пассивный доход" }),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Повторить" }),
).not.toBeInTheDocument();
});
it("keeps a null analytics envelope loading instead of an empty page", async () => {
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(resource(null)),
),
);
renderPage();
expect(
await screen.findByRole("status", { name: "Загрузка" }, { timeout: 4000 }),
).toBeVisible();
expect(
screen.queryByRole("heading", { name: "Пассивный доход" }),
).not.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,
entries: [
{
...analytics.entries[0],
description: "Исходная выплата",
figi: null,
instrumentKey: "server-only-key",
ticker: null,
},
{
...analytics.entries[1],
description: "Отрицательная корректировка",
instrumentKey: "server-only-key",
instrumentUid: "unrelated-uid",
payment: money("RUB", "-2"),
},
],
instruments: [
{
...analytics.instruments[0],
dividend: money("RUB", "-2"),
key: "server-only-key",
total: money("RUB", "8"),
},
],
};
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(resource(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("renders realized income without comparison blocks or event requests", async () => {
const eventsRequest = vi.fn(() => new HttpResponse(null, { status: 503 }));
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(resource(analytics)),
),
http.get("*/api/v1/brokerage-accounts/account-1/events", eventsRequest),
);
renderPage();
expect(
await screen.findByRole("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();
expect(screen.queryByText("Предыдущий период")).not.toBeInTheDocument();
expect(screen.queryByText("Годом ранее")).not.toBeInTheDocument();
expect(screen.queryByText(/Сравнение с/)).not.toBeInTheDocument();
});
it.each([
["full", analytics],
["empty", { ...analytics, buckets: [], entries: [], instruments: [], total: summary("0", "0") }],
])("shows the server source time and last attempt for a %s result", async (_name, data) => {
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(
resource(data, {
freshness: "stale",
lastAttempt: {
finishedAt: "2026-08-27T01:00:00Z",
problems: [],
startedAt: "2026-08-27T00:59:00Z",
state: "failed",
},
lastSuccessfulAt: "2026-08-26T12:00:00Z",
}),
),
),
);
renderPage();
expect(await screen.findByText(/^Данные на /)).toHaveTextContent(
formatSnapshotTime("2026-08-26T12:00:00Z"),
);
expect(screen.getByText("Обновление не удалось")).toBeInTheDocument();
expect(screen.getByText("Данные требуют обновления")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Повторить" })).not.toBeInTheDocument();
});
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/account-1/income-analytics",
() => {
requests += 1;
return isFailing
? new HttpResponse(null, { status: 503 })
: HttpResponse.json(resource(analytics));
},
),
);
renderPage();
expect(
await screen.findByText(
"Не удалось рассчитать пассивный доход",
undefined,
{ timeout: RETRIES_EXHAUSTED_MS },
),
).toBeInTheDocument();
const retry = screen.getByRole("button", { name: "Повторить" });
expect(requests).toBe(3);
isFailing = false;
fireEvent.click(retry);
expect(
await screen.findByRole("region", { name: "Выплаты" }),
).toBeInTheDocument();
expect(requests).toBe(4);
},
);
it(
"keeps cached income, warns and retries the same GET",
{ timeout: TEST_MS },
async () => {
let isFailing = false;
let requests = 0;
server.use(
http.get(
"*/api/v1/brokerage-accounts/account-1/income-analytics",
() => {
requests += 1;
return isFailing
? HttpResponse.json(
{ code: "TBANK_UNAVAILABLE" },
{ status: 503 },
)
: HttpResponse.json(resource(analytics));
},
),
);
const { queryClient } = renderPage();
await screen.findByRole("region", { name: "Выплаты" });
expect(screen.queryByRole("button", { name: "Повторить" }))
.not.toBeInTheDocument();
isFailing = true;
void queryClient.invalidateQueries({
queryKey: ["get", "/api/v1/brokerage-accounts/{accountId}/income-analytics"],
});
const retry = await screen.findByRole(
"button",
{ name: "Повторить" },
{ timeout: RETRIES_EXHAUSTED_MS },
);
expect(screen.getByText("Не удалось обновить данные")).toBeInTheDocument();
expect(screen.getByRole("region", { name: "Выплаты" })).toBeInTheDocument();
expect(requests).toBe(4);
isFailing = false;
fireEvent.click(retry);
await waitFor(() =>
expect(screen.queryByText("Не удалось обновить данные"))
.not.toBeInTheDocument());
expect(requests).toBe(5);
},
);
});
const LocationProbe: FC = () => {
const location = useLocation();
const navigate = useNavigate();
return (
<>
<output data-testid="location">{location.search}</output>
<button onClick={() => navigate(-1)} type="button">
Back
</button>
</>
);
};
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(
resource({
...analytics,
period: {
from: url.searchParams.get("from"),
to: url.searchParams.get("to"),
},
}),
);
},
),
);
}
function selectPeriod() {
fireEvent.click(screen.getByLabelText("Период"));
fireEvent.click(screen.getByRole("button", { name: /^2 августа 2026$/i }));
fireEvent.click(screen.getByRole("button", { name: /^27 августа 2026$/i }));
}
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("Период")).toHaveTextContent(
"01.08.2026 – 27.08.2026",
);
selectPeriod();
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: "Применить" })
// eslint-disable-next-line testing-library/no-node-access -- This assertion checks DOM structure, decoration or focus behavior that semantic queries cannot establish.
.closest("form") as HTMLFormElement,
);
await waitFor(() => expect(requests).toHaveLength(2));
expect(await screen.findByLabelText("Период")).toHaveTextContent(
"02.08.2026 – 27.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("Период")).toHaveTextContent(
"01.08.2026 – 27.08.2026",
),
);
});
it("rejects an incomplete range without replacing the URL and allows a single day", async () => {
const requests: string[] = [];
respondWithRequestedPeriod(requests);
renderPage();
const period = await screen.findByLabelText("Период");
fireEvent.click(period);
fireEvent.click(screen.getByRole("button", { name: /^2 августа 2026$/i }));
fireEvent.keyDown(period, { key: "Escape" });
fireEvent.click(screen.getByRole("button", { name: "Применить" }));
expect(await screen.findByText(/Введите существующую дату/)).toBeVisible();
expect(requests).toHaveLength(1);
if (period.getAttribute("aria-expanded") !== "true")
fireEvent.click(period);
fireEvent.click(screen.getByRole("button", { name: /^2 августа 2026$/i }));
fireEvent.click(screen.getByRole("button", { name: "Применить" }));
await waitFor(() =>
expect(requests.at(-1)).toBe("?from=2026-08-02&to=2026-08-02"),
);
});
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("Период")).toHaveTextContent(
"01.09.2026 – 01.09.2026",
);
fireEvent.click(screen.getByLabelText("Период"));
fireEvent.click(screen.getByRole("button", { name: /^1 сентября 2026$/i }));
fireEvent.keyDown(screen.getByLabelText("Период"), { key: "Escape" });
fireEvent.click(screen.getByRole("button", { name: "Применить" }));
await screen.findByText(/Введите существующую дату/);
fireEvent.click(screen.getByRole("button", { name: "Сбросить" }));
await waitFor(() =>
expect(screen.getByLabelText("Период")).toHaveTextContent(
"01.09.2026 – 01.09.2026",
),
);
expect(
screen.queryByText(/Введите существующую дату/),
).not.toBeInTheDocument();
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 failure", async () => {
respondWithRequestedPeriod([]);
const { queryClient } = renderPage();
await screen.findByLabelText("Период");
selectPeriod();
const { promise: pending, resolve: release }
= Promise.withResolvers<void>();
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(screen.getByRole("region", { name: "Выплаты" })).toBeVisible();
expect(screen.getByLabelText("Период")).toHaveTextContent(
"02.08.2026 – 27.08.2026",
);
release();
expect(screen.getByRole("region", { name: "Выплаты" })).toBeVisible();
expect(screen.queryByText(/Показан последний загруженный результат/)).not.toBeInTheDocument();
expect(screen.getByLabelText("Период")).toHaveTextContent(
"02.08.2026 – 27.08.2026",
);
});
it("shows busy skeleton for a new uncached period without relabeling the previous result", async () => {
respondWithRequestedPeriod([]);
renderPage();
await screen.findByLabelText("Период");
const { promise: pending, resolve: release }
= Promise.withResolvers<void>();
server.use(
http.get(
"*/api/v1/brokerage-accounts/account-1/income-analytics",
async () => {
await pending;
return HttpResponse.json(
resource({
...analytics,
period: { from: "2026-08-02", to: "2026-08-27" },
}),
);
},
),
);
selectPeriod();
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("Период")).toHaveTextContent(
"02.08.2026 – 27.08.2026",
);
});
it("shows one flat payment card per entry in server order without currency or instrument groups", () => {
const instruments = [
{
...analytics.instruments[0],
coupon: money("RUB", "10"),
dividend: money("RUB", "0"),
key: "rub-positive",
receiptCount: 1,
total: money("RUB", "10"),
},
{
...analytics.instruments[0],
coupon: money("RUB", "-2"),
dividend: money("RUB", "0"),
key: "rub",
receiptCount: 1,
total: money("RUB", "-2"),
},
{
...analytics.instruments[0],
coupon: money("RUB", "-3"),
displayName: "Другой инструмент",
dividend: money("RUB", "0"),
key: "rub-negative",
receiptCount: 1,
total: money("RUB", "-3"),
},
];
const display = toIncomeDisplayModel({
...analytics,
entries: instruments.map(instrument => ({
...entry(),
instrumentKey: instrument.key,
payment: instrument.total,
})),
instruments,
total: summary("-5", "0"),
});
render(
<MantineProvider env="test">
<IncomePayments entries={display?.entries ?? []} />
</MantineProvider>,
);
expect(
screen.getByRole("heading", { name: "Выплаты" }),
).toBeInTheDocument();
expect(screen.queryByText("RUB")).not.toBeInTheDocument();
const cards = screen.getAllByRole("listitem");
expect(cards).toHaveLength(3);
expect(cards[0]).toHaveTextContent("+10 ₽");
expect(cards[1]).toHaveTextContent("−2 ₽");
expect(cards[2]).toHaveTextContent("−3 ₽");
expect(screen.queryByText(/Итого:|Выплат:/)).not.toBeInTheDocument();
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});
it("groups all-account payouts by instrument with an exact total", async () => {
server.use(
http.get(
"*/api/v1/brokerage-accounts/:accountId/income-analytics",
() => HttpResponse.json(resource({
...analytics,
accountId: "all",
entries: [
{
...entry(),
account: { id: "broker", name: "Брокерский" },
payment: money("RUB", "10"),
},
{
...entry(),
account: { id: "iis", name: "ИИС" },
id: "coupon-2",
payment: money("RUB", "20"),
},
],
})),
),
);
renderPage(undefined, "all");
expect(await screen.findByText("Выплата купонов BOND")).toBeInTheDocument();
expect(screen.getByText("+30 ₽")).toBeInTheDocument();
const accounts = within(screen.getByRole("list", {
name: /Выплата купонов BOND, Исполнено/,
}));
expect(accounts.getByText("Брокерский")).toBeInTheDocument();
expect(accounts.getByText("ИИС")).toBeInTheDocument();
expect(accounts.queryByRole("link")).not.toBeInTheDocument();
});
it("keeps repeated payout ids inside one group as separate cards", async () => {
server.use(
http.get(
"*/api/v1/brokerage-accounts/:accountId/income-analytics",
() => HttpResponse.json(resource({
...analytics,
accountId: "all",
entries: [
{
...entry(),
account: { id: "broker", name: "Брокерский" },
payment: money("RUB", "10"),
},
{
...entry(),
account: { id: "iis", name: "ИИС" },
payment: money("RUB", "20"),
},
],
})),
),
);
renderPage(undefined, "all");
expect(await screen.findByText("+30 ₽")).toBeInTheDocument();
const accounts = within(screen.getByRole("list", {
name: /Выплата купонов BOND/,
}));
expect(accounts.getAllByRole("listitem")).toHaveLength(2);
});
it("shows the group total after completeness changes to complete", async () => {
let completeness: "complete" | "incomplete" = "incomplete";
const entries = [
{
...entry(),
account: { id: "broker", name: "Брокерский" },
payment: money("RUB", "10"),
},
{
...entry(),
account: { id: "iis", name: "ИИС" },
id: "coupon-2",
payment: money("RUB", "20"),
},
];
server.use(
http.get(
"*/api/v1/brokerage-accounts/:accountId/income-analytics",
() => HttpResponse.json(
resource({ ...analytics, accountId: "all", entries }, { completeness }),
),
),
);
const view = renderPage(undefined, "all");
await screen.findByText("Выплата купонов BOND");
expect(screen.queryByText("+30 ₽")).not.toBeInTheDocument();
view.unmount();
completeness = "complete";
renderPage(undefined, "all");
expect(await screen.findByText("+30 ₽")).toBeInTheDocument();
});
});