Frontend security / security (push) Successful in 9s
Frontend verify / verify (push) Successful in 5m10s
Conventional commits / commits (pull_request) Successful in 25s
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 5m10s
152 lines
5.5 KiB
TypeScript
152 lines
5.5 KiB
TypeScript
import { MantineProvider } from "@mantine/core";
|
|
import { QueryClientProvider } from "@tanstack/react-query";
|
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { HttpResponse, http } from "msw";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { configureAuthClient } from "@/shared/api";
|
|
import { createTestQueryClient } from "@/test/query";
|
|
import { server } from "@/test/server";
|
|
import type { BankCard } from "../../api/queries";
|
|
import { BankCardForm } from "./BankCardForm";
|
|
|
|
const card: BankCard = {
|
|
id: "a",
|
|
name: "Основная",
|
|
bankName: "Банк",
|
|
lastFour: "1234",
|
|
paymentSystem: null,
|
|
openingBalance: { currency: "RUB", value: "100" },
|
|
balance: { currency: "RUB", value: "50" },
|
|
openingDate: "2026-01-01",
|
|
archivedAt: null,
|
|
version: 1,
|
|
};
|
|
function setup(value: BankCard) {
|
|
configureAuthClient({
|
|
accessToken: () => "test",
|
|
refresh: async () => false,
|
|
clear: () => {},
|
|
});
|
|
server.use(http.get("*/api/v1/bank-cards/a", () => HttpResponse.json(value)));
|
|
const close = vi.fn();
|
|
render(
|
|
<QueryClientProvider client={createTestQueryClient()}>
|
|
<MantineProvider env="test">
|
|
<BankCardForm card={value} onClose={close} />
|
|
</MantineProvider>
|
|
</QueryClientProvider>,
|
|
);
|
|
return close;
|
|
}
|
|
describe("BankCardForm", () => {
|
|
it("waits for the exit transition before notifying the parent", async () => {
|
|
const close = setup(card);
|
|
fireEvent.click(screen.getByRole("button", { name: "Отмена" }));
|
|
expect(close).not.toHaveBeenCalled();
|
|
await waitFor(() => expect(close).toHaveBeenCalledTimes(1));
|
|
});
|
|
it("clears optional metadata on archive while omitting protected financial fields", async () => {
|
|
const close = setup({ ...card, archivedAt: "2026-09-01" });
|
|
expect(screen.getByLabelText(/Начальный остаток/)).toHaveAttribute(
|
|
"readonly",
|
|
);
|
|
expect(screen.getByLabelText(/Дата начала/)).toHaveAttribute("readonly");
|
|
fireEvent.change(screen.getByLabelText(/Банк/), { target: { value: "" } });
|
|
fireEvent.change(screen.getByLabelText(/Последние четыре/), {
|
|
target: { value: "" },
|
|
});
|
|
let body: unknown;
|
|
server.use(
|
|
http.patch("*/api/v1/bank-cards/a", async ({ request }) => {
|
|
body = await request.json();
|
|
return HttpResponse.json(card);
|
|
}),
|
|
);
|
|
fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
|
|
await waitFor(() => expect(close).toHaveBeenCalled());
|
|
expect(body).toEqual({
|
|
version: 1,
|
|
name: "Основная",
|
|
bankName: null,
|
|
lastFour: null,
|
|
paymentSystem: null,
|
|
});
|
|
});
|
|
it("loads and saves the selected payment system", async () => {
|
|
const close = setup({ ...card, paymentSystem: "mir" });
|
|
expect(
|
|
screen.getByRole("combobox", { name: /Платёжная система/ }),
|
|
).toHaveValue("МИР");
|
|
fireEvent.click(
|
|
screen.getByRole("combobox", { name: /Платёжная система/ }),
|
|
);
|
|
fireEvent.click(await screen.findByRole("option", { name: "Visa" }));
|
|
let body: unknown;
|
|
server.use(
|
|
http.patch("*/api/v1/bank-cards/a", async ({ request }) => {
|
|
body = await request.json();
|
|
return HttpResponse.json({ ...card, paymentSystem: "visa" });
|
|
}),
|
|
);
|
|
fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
|
|
await waitFor(() => expect(close).toHaveBeenCalled());
|
|
expect(body).toMatchObject({ paymentSystem: "visa" });
|
|
});
|
|
|
|
it("retains an invalid opening date draft and explains conflict with saved history", async () => {
|
|
const close = setup(card);
|
|
fireEvent.change(screen.getByLabelText(/Дата начала/), {
|
|
target: { value: "01.08.2026" },
|
|
});
|
|
server.use(
|
|
http.patch("*/api/v1/bank-cards/a", () =>
|
|
HttpResponse.json(
|
|
{ code: "BANK_OPENING_DATE_CONFLICT" },
|
|
{ status: 400 },
|
|
),
|
|
),
|
|
);
|
|
fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
|
|
await screen.findByText(
|
|
"Дата начала противоречит сохранённой истории карты.",
|
|
);
|
|
expect(screen.getByLabelText(/Дата начала/)).toHaveValue("01.08.2026");
|
|
expect(close).not.toHaveBeenCalled();
|
|
});
|
|
it("keeps card edit version until explicit refresh adoption", async () => {
|
|
const close = setup(card);
|
|
let version = 1;
|
|
const bodies: Record<string, unknown>[] = [];
|
|
server.use(
|
|
http.get("*/api/v1/bank-cards/a", () =>
|
|
HttpResponse.json({ ...card, version }),
|
|
),
|
|
http.patch("*/api/v1/bank-cards/a", async ({ request }) => {
|
|
const body = (await request.json()) as Record<string, unknown>;
|
|
bodies.push(body);
|
|
version = 2;
|
|
return body.version === 1
|
|
? HttpResponse.json(
|
|
{ code: "BANK_VERSION_CONFLICT" },
|
|
{ status: 409 },
|
|
)
|
|
: HttpResponse.json(card);
|
|
}),
|
|
);
|
|
fireEvent.change(screen.getByLabelText(/Название карты/), {
|
|
target: { value: "Мой черновик" },
|
|
});
|
|
fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
|
|
await screen.findByText(/Запись уже изменена/);
|
|
fireEvent.click(screen.getByRole("button", { name: "Перечитать запись" }));
|
|
const adopt = await screen.findByRole("button", {
|
|
name: "Использовать актуальную версию 2",
|
|
});
|
|
expect(screen.getByLabelText(/Название карты/)).toHaveValue("Мой черновик");
|
|
fireEvent.click(adopt);
|
|
fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
|
|
await waitFor(() => expect(close).toHaveBeenCalled());
|
|
expect(bodies.map((b) => b.version)).toEqual([1, 2]);
|
|
});
|
|
});
|