feat: redesign year-over-year income comparison

This commit is contained in:
2026-09-09 18:28:15 +03:00
parent 451400b63b
commit 91eaeb5661
6 changed files with 398 additions and 59 deletions
@@ -368,7 +368,13 @@ describe("AccountIncomePage", () => {
const eventsRequest = vi.fn(() => new HttpResponse(null, { status: 503 }));
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(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),
);
@@ -385,14 +391,50 @@ describe("AccountIncomePage", () => {
expect(screen.queryByText("Динамика")).not.toBeInTheDocument();
expect(eventsRequest).not.toHaveBeenCalled();
expect(screen.getByRole("region", { name: "Выплаты" })).toBeInTheDocument();
const selected = screen.getByRole("region", {
name: /^1–27 авг\. 2026\sг\.$/,
});
expect(
screen.getByText(
"Выбранный период: Купоны 10 ₽ · Дивиденды 2 ₽ · Итого 12 ₽",
screen.getByRole("region", { name: /^1–27 авг\. 2025\sг\.$/ }),
).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.getByText("Разница: Купоны 9 ₽ · Дивиденды 2 ₽ · Итого 11 ₽"),
).toBeInTheDocument();
screen.queryByRole("region", { name: "Разница" }),
).not.toBeInTheDocument();
expect(screen.queryByText(/Предыдущий период/)).not.toBeInTheDocument();
});
it("shows a recoverable realized error", async () => {
@@ -411,6 +453,63 @@ describe("AccountIncomePage", () => {
});
});
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(
"1 дек. 2025 г. – 9 янв. 2026 г.",
);
expect(display?.comparison?.comparedPeriod.replaceAll(/\s/g, " ")).toBe(
"1 дек. 2024 г. – 9 янв. 2025 г.",
);
expect(display?.comparison?.values.map((value) => value.currency)).toEqual([
"RUB",
"USD",
]);
});
});
const LocationProbe: FC = () => {
const location = useLocation();
const navigate = useNavigate();