import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter, Route, Routes } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; // Реальный случай: парк уже погасил постановление в ГИБДД со скидкой, а водитель // парку ещё должен. Именно на нём приложение показывало «неоплаченных нет». const FINE = { id: 12, number: "18810525260813035846", date: "2026-08-12", article: "12.09.2 - Превышение скорости", address: "Владивосток, Некрасовская, 90", car_number: "А947УТ125", amount: 618.75, settled: "owed" as const, cancelled: false, has_photo: true, }; vi.mock("@/api/driver", () => ({ getFines: vi.fn(async () => ({ items: [FINE, { ...FINE, id: 13, settled: "paid" as const, amount: 500 }], open_count: 1, debt: -5156, })), getFine: vi.fn(async () => ({ ...FINE, post_date: "2026-08-13", speed: 63, speed_limit: 40, speed_excess: 23, camera: "АвтоУраган-ВСМ2-М", photos: ["https://s3.pptaxi.ru/pp-fines/1/offense_photo-0.jpg"], document: "https://s3.pptaxi.ru/pp-fines/1/decision.pdf", })), })); import { FinesPage } from "./FinesPage"; import { FineDetailPage } from "./FineDetailPage"; import * as driver from "@/api/driver"; function wrap(path = "/fines") { const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); return render( } /> } /> экран пополнения

} />
); } describe("список штрафов", () => { beforeEach(() => vi.clearAllMocks()); it("нет непогашенных — открываем «Все», а не пустой экран", async () => { // Водитель оплатил штраф сам: долг нулевой, но штрафы у него есть. Раньше // список открывался на «Не погашены» и выглядел пустым (Морозов, 18.08.2026). vi.mocked(driver.getFines).mockResolvedValueOnce({ items: [{ ...FINE, settled: "paid" as const }], open_count: 0, debt: 0, } as never); wrap(); expect(await screen.findByText("погашен")).toBeInTheDocument(); expect(screen.queryByText(/Непогашенных штрафов нет/)).not.toBeInTheDocument(); }); it("долг берётся из баланса 1С, а не из сумм постановлений", async () => { wrap(); // Парк гасит ГИБДД авансом: если считать по «оплачен/не оплачен» из // постановлений, экран покажет «долгов нет» при −5 156 ₽ на главном. expect(await screen.findByText(/Долг по штрафам −5 156 ₽ · не погашено: 1/)).toBeInTheDocument(); }); it("закрытый в ГИБДД штраф остаётся непогашенным для водителя", async () => { wrap(); expect(await screen.findByText("не погашен")).toBeInTheDocument(); expect(screen.queryByText("оплачен")).not.toBeInTheDocument(); }); it("вкладка «Все» показывает и погашенные", async () => { wrap(); await screen.findByText(/Долг по штрафам/); await userEvent.click(screen.getByRole("button", { name: "Все" })); expect(await screen.findByText("погашен")).toBeInTheDocument(); }); it("штраф открывается в карточку", async () => { wrap(); await userEvent.click(await screen.findByRole("button", { name: /Превышение скорости/ })); expect(await screen.findByRole("heading", { name: /Превышение скорости/ })).toBeInTheDocument(); }); }); describe("карточка штрафа", () => { beforeEach(() => vi.clearAllMocks()); it("фотография нарушения стоит первой и открывается в полный размер", async () => { wrap("/fines/12"); const img = await screen.findByAltText("Фото нарушения 1"); expect(img).toHaveAttribute("src", expect.stringContaining("offense_photo")); expect(img.closest("a")).toHaveAttribute("target", "_blank"); }); it("показывает начисленную сумму, скорость и постановление", async () => { wrap("/fines/12"); expect(await screen.findByText("618,75 ₽")).toBeInTheDocument(); expect(screen.getByText(/63 км\/ч при разрешённых 40/)).toBeInTheDocument(); expect(screen.getByText("18810525260813035846")).toBeInTheDocument(); }); it("расчёты парка с ГИБДД водителю не показываются", async () => { // Решение владельца: без номинала, скидки и «парк уже оплатил». В начисление // заложена комиссия парка, и любая из этих строк рядом требует объяснений. wrap("/fines/12"); await screen.findByText("618,75 ₽"); expect(screen.queryByText(/750/)).not.toBeInTheDocument(); expect(screen.queryByText(/скидк/i)).not.toBeInTheDocument(); expect(screen.queryByText(/ГИБДД/)).not.toBeInTheDocument(); expect(screen.queryByText(/парк/i)).not.toBeInTheDocument(); }); it("неоплаченный штраф ведёт к оплате штрафов", async () => { wrap("/fines/12"); await userEvent.click(await screen.findByRole("button", { name: /Оплатить штрафы/ })); expect(await screen.findByText("экран пополнения")).toBeInTheDocument(); }); it("нет фото — экран объясняет это, а не показывает пустоту", async () => { vi.mocked(driver.getFine).mockResolvedValueOnce({ ...FINE, post_date: null, speed: null, speed_limit: null, speed_excess: null, camera: "", photos: [], document: null, } as never); wrap("/fines/12"); expect(await screen.findByText(/Фотографии по этому постановлению нет/)).toBeInTheDocument(); }); });