Files
frontend/src/pages/cards/ui/CardsPage/CardsPage.test.tsx
T
ksv741 5b30bb802a feat: add quiet reading with rare confirmations
Part of the quiet-user-feedback change; docs and specs land in their own repositories.
2026-10-06 07:01:58 +03:00

236 lines
7.4 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 { MantineProvider } from "@mantine/core";
import { QueryClientProvider } from "@tanstack/react-query";
import {
fireEvent,
render,
screen,
waitFor,
within,
} from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { MemoryRouter } from "react-router";
import { assert, describe, expect, it } from "vitest";
import { configureAuthClient } from "@/shared/api";
import { createTestQueryClient } from "@/test/query";
import { server } from "@/test/server";
import { CardsPage } from "./CardsPage";
const money = (value: string) => ({ currency: "RUB", value });
function renderCards(isFail = false, hasCategoryFailure = false) {
configureAuthClient({
accessToken: () => "test",
clear: () => {},
refresh: () => Promise.resolve(false),
});
server.use(
http.get("*/api/v1/bank-cards/categories", () =>
hasCategoryFailure ? HttpResponse.error() : HttpResponse.json([]),
),
http.get("*/api/v1/bank-cards", () =>
isFail ? HttpResponse.error() : HttpResponse.json([]),
),
http.get("*/api/v1/bank-cards/summary", () =>
HttpResponse.json(
{
cardId: null,
categories: [],
expenses: money("0"),
month: "2026-09",
net: money("0"),
refunds: money("0"),
},
),
),
);
return render(
<QueryClientProvider client={createTestQueryClient()}>
<MantineProvider env="test">
<MemoryRouter initialEntries={["/cards?month=2026-09"]}>
<CardsPage />
</MemoryRouter>
</MantineProvider>
</QueryClientProvider>,
);
}
describe("CardsPage", () => {
it("preserves exact negative opening amount and the creation intent through unknown failure", async () => {
renderCards();
await screen.findByRole("heading", { name: "Добавьте первую карту" });
const creationButton = screen.getAllByRole("button", { name: "Добавить карту" })[0];
assert.isDefined(creationButton);
fireEvent.click(creationButton);
const dialog = screen.getByRole("dialog");
const submitted: Record<string, unknown>[] = [];
let resolve: (() => void) | undefined;
server.use(
http.post("*/api/v1/bank-cards", async ({ request }) => {
submitted.push((await request.json()) as Record<string, unknown>);
if (submitted.length === 1) {
const deferred = Promise.withResolvers<void>();
resolve = deferred.resolve;
await deferred.promise;
return HttpResponse.error();
}
return HttpResponse.json({ id: submitted[0]?.id }, { status: 201 });
}),
);
fireEvent.change(within(dialog).getByLabelText(/Название карты/), {
target: { value: " Повседневная " },
});
fireEvent.change(within(dialog).getByLabelText(/Начальный остаток/), {
target: { value: "1,001" },
});
fireEvent.click(within(dialog).getByRole("button", { name: "Сохранить" }));
expect(
await screen.findByText(/Введите сумму в допустимом диапазоне/),
).toBeInTheDocument();
expect(submitted).toHaveLength(0);
fireEvent.change(within(dialog).getByLabelText(/Начальный остаток/), {
target: { value: "-92233720368547758,08" },
});
fireEvent.change(within(dialog).getByLabelText(/Дата начала учёта/), {
target: { value: "2026-01-01" },
});
fireEvent.click(within(dialog).getByRole("button", { name: "Сохранить" }));
await waitFor(() => expect(submitted).toHaveLength(1));
expect(
within(dialog).getByRole("button", { name: "Отмена" }),
).toBeDisabled();
expect(
within(dialog).getByRole("button", { name: "Закрыть" }),
).toBeDisabled();
resolve?.();
await screen.findByText(/Не удалось подтвердить сохранение/);
expect(within(dialog).getByLabelText(/Начальный остаток/)).toHaveValue(
"-92233720368547758,08",
);
fireEvent.click(within(dialog).getByRole("button", { name: "Сохранить" }));
await waitFor(() =>
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(),
);
expect(submitted[0]).toEqual(submitted[1]);
expect(submitted[0]).toMatchObject(
{
bankName: null,
lastFour: null,
name: "Повседневная",
openingBalance: money("-92233720368547758.08"),
},
);
});
it("uses archive only for card list and keeps all-card summary including negative net", async () => {
renderCards();
const requests: string[] = [];
server.use(
http.get("*/api/v1/bank-cards", ({ request }) => {
requests.push(new URL(request.url).search);
return HttpResponse.json([
{
archivedAt: "2026-09-01",
balance: money("0"),
bankName: null,
id: "demo",
lastFour: null,
name: "Запасная",
openingBalance: money("0"),
openingDate: "2026-01-01",
version: 1,
},
]);
}),
);
fireEvent.click(screen.getByLabelText("Показать архив"));
await screen.findByText("Запасная");
expect(requests).toContain("?includeArchived=true");
expect(screen.getByLabelText("Период")).toHaveTextContent(
"01.09.2026 – 30.09.2026",
);
expect(screen.getByRole("link", { name: /Запасная/ })).toHaveAttribute(
"href",
"/cards/demo?from=2026-09-01&to=2026-09-30",
);
});
it("applies a completed custom period to the summary", async () => {
renderCards();
await screen.findByText("За этот период операций по расходам нет.");
await screen.findByRole("heading", { name: "Добавьте первую карту" });
const requests: string[] = [];
server.use(
http.get("*/api/v1/bank-cards/summary", ({ request }) => {
requests.push(new URL(request.url).search);
return HttpResponse.json(
{
cardId: null,
categories: [],
expenses: money("0"),
net: money("0"),
refunds: money("0"),
},
);
}),
);
fireEvent.click(screen.getByLabelText("Период"));
fireEvent.click(
await screen.findByRole("button", { name: "10 September 2026" }),
);
expect(requests).toHaveLength(0);
fireEvent.click(
await screen.findByRole("button", { name: "20 September 2026" }),
);
await waitFor(() =>
expect(requests.at(-1)).toBe("?from=2026-09-10&to=2026-09-20"),
);
});
it("shows retry after failure instead of presenting an empty list", async () => {
renderCards(true);
// Mount starts asynchronously; this handler owns the request.
expect(
screen.queryByRole("alert"),
).not.toBeInTheDocument();
expect(
screen.queryByRole("heading", { name: "Добавьте первую карту" }),
).not.toBeInTheDocument();
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
server.use(http.get("*/api/v1/bank-cards", () => HttpResponse.json([])));
await waitFor(() => {
expect(
screen.queryByRole("alert"),
).not.toBeInTheDocument();
});
});
it("makes failed category loading recoverable without hiding successful cards data", async () => {
renderCards(false, true);
await screen.findByRole("heading", { name: "Добавьте первую карту" });
expect(
screen.queryByRole("alert"),
).not.toBeInTheDocument();
server.use(
http.get("*/api/v1/bank-cards/categories", () =>
HttpResponse.json([
{ id: "default", isDefault: true, name: "Другое", version: 1 },
]),
),
);
await waitFor(() => {
expect(
screen.getByRole("button", { name: "Категории" }),
).toBeInTheDocument();
});
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});
});