Водитель, оплативший штраф сам, видел пустой экран: список открывался на «Не погашены», а там у него ноль (Морозов, штраф от 16.08 — оплачен им, парк его не выставлял). Выглядело как «приложение потеряло мои штрафы». Теперь вкладка выбирается по данным: есть непогашенные — открываем их, нет — открываем «Все». Заодно один запрос вместо двух: список короткий, фильтруем на месте, и переключение вкладок стало мгновенным. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
132 lines
6.6 KiB
TypeScript
132 lines
6.6 KiB
TypeScript
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(
|
||
<QueryClientProvider client={qc}>
|
||
<MemoryRouter initialEntries={[path]}>
|
||
<Routes>
|
||
<Route path="/fines" element={<FinesPage />} />
|
||
<Route path="/fines/:fineId" element={<FineDetailPage />} />
|
||
<Route path="/topup" element={<p>экран пополнения</p>} />
|
||
</Routes>
|
||
</MemoryRouter>
|
||
</QueryClientProvider>
|
||
);
|
||
}
|
||
|
||
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();
|
||
});
|
||
});
|