feat(pwa): экран штрафов с фотографиями нарушений
Список с вкладками «Неоплаченные / Все» и суммой долга в шапке; в карточке — фотография с камеры первой, до реквизитов: по ней водитель узнаёт свою машину и свою поездку. Дальше сумма, скидка с датой (то, ради чего заходят вовремя), скорость и разрешённый предел, постановление, камера, ссылка на PDF. Строка «Долг по штрафам» на главном экране теперь ведёт сюда, а не сразу к оплате: платить, не поняв за что, человек не станет. Аренда по-прежнему ведёт к оплате — за тем её и открывают. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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: 562.5, discount_amount: 281.25,
|
||||
discount_until: "2026-09-01", paid: false, cancelled: false, has_photo: true,
|
||||
};
|
||||
|
||||
vi.mock("@/api/driver", () => ({
|
||||
getFines: vi.fn(async (status: string) => ({
|
||||
items: status === "open" ? [FINE] : [FINE, { ...FINE, id: 13, paid: true, amount: 500 }],
|
||||
open_count: 1, open_sum: 562.5,
|
||||
})),
|
||||
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 () => {
|
||||
wrap();
|
||||
expect(await screen.findByText(/Не оплачено: 1 на 562,50 ₽/)).toBeInTheDocument();
|
||||
expect(vi.mocked(driver.getFines).mock.calls[0][0]).toBe("open");
|
||||
});
|
||||
|
||||
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(/63 км\/ч при разрешённых 40/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/281,25 ₽ со скидкой/)).toBeInTheDocument();
|
||||
expect(screen.getByText("18810525260813035846")).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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user