Files
mechanic-pwa/driver-pwa/frontend/src/pages/FinesPage.test.tsx
T
tremble7681andClaude Opus 5 7204290e45 fix(pwa): штрафы не прячутся за вкладкой «Не погашены»
Водитель, оплативший штраф сам, видел пустой экран: список открывался на
«Не погашены», а там у него ноль (Морозов, штраф от 16.08 — оплачен им, парк его
не выставлял). Выглядело как «приложение потеряло мои штрафы».

Теперь вкладка выбирается по данным: есть непогашенные — открываем их, нет —
открываем «Все». Заодно один запрос вместо двух: список короткий, фильтруем на
месте, и переключение вкладок стало мгновенным.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:22:16 +10:00

132 lines
6.6 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 { 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();
});
});