Отдельный список штрафов удалён: два списка одного и того же в разных местах сбивали с толку. Фильтр «Штрафы» внизу главного экрана теперь показывает сами постановления, а не проводки по ним, и ведёт в карточку с фотографией. Разница между постановлением и проводкой здесь принципиальная: штраф, который водитель оплатил сам, парк не выставлял — проводки нет, и в выписке он бы не появился вовсе (Морозов, штраф от 16.08). Строка «Долг по штрафам» вверху теперь включает этот фильтр, а не уводит на другой экран. Период действует и на штрафы: календарь над списком не должен стоять, ничего не меняя. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
147 lines
7.4 KiB
TypeScript
147 lines
7.4 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
||
import { render, screen, within } 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";
|
||
|
||
// Ответ как у настоящего водителя: статьи 1С, итог отдельным числом.
|
||
vi.mock("@/api/driver", () => ({
|
||
getBalance: vi.fn(async () => ({
|
||
accounts: [
|
||
{ bucket: "Долг аренда", balance: -3050, payable: true },
|
||
{ bucket: "Долг по штрафам", balance: -5156, payable: true },
|
||
{ bucket: "Страховка", balance: 200, payable: false },
|
||
],
|
||
total: -8976.25,
|
||
synced_at: null,
|
||
pending_topup: 1500,
|
||
})),
|
||
getFines: vi.fn(async () => ({
|
||
items: [{
|
||
id: 12, number: "18810525260813035846", date: "2026-08-13",
|
||
article: "12.09.2 - Превышение скорости", address: "", car_number: "А947УТ125",
|
||
amount: 618.75, settled: "owed" as const, cancelled: false, has_photo: true,
|
||
}],
|
||
open_count: 1, debt: -5156,
|
||
})),
|
||
getStatement: vi.fn(async () => ({
|
||
items: [
|
||
{ date: "2026-08-17", title: "Аренда автомобиля", description: "ТС А947УТ125",
|
||
amount: -2600, kind: "charges" },
|
||
{ date: "2026-08-17", title: "Пополнение баланса", description: "", amount: 1000,
|
||
kind: "payments" },
|
||
],
|
||
from: "2026-08-11", to: "2026-08-18", kind: "all",
|
||
})),
|
||
}));
|
||
|
||
import { BalancePage } from "./BalancePage";
|
||
import * as driver from "@/api/driver";
|
||
|
||
function wrap() {
|
||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||
return render(
|
||
<QueryClientProvider client={qc}>
|
||
<MemoryRouter>
|
||
<Routes>
|
||
<Route path="/" element={<BalancePage />} />
|
||
<Route path="/topup" element={<p>экран пополнения</p>} />
|
||
</Routes>
|
||
</MemoryRouter>
|
||
</QueryClientProvider>
|
||
);
|
||
}
|
||
|
||
describe("главный экран", () => {
|
||
it("сверху деньги: итог с сервера и строка «зачисляется»", async () => {
|
||
wrap();
|
||
// Сумма статей дала бы −8 006, а в CRM у водителя −8 976,25. Показываем её.
|
||
expect(await screen.findByText("−8 976,25 ₽")).toBeInTheDocument();
|
||
expect(screen.getByText("Зачисляется")).toBeInTheDocument();
|
||
expect(screen.getByText("1 500 ₽")).toBeInTheDocument();
|
||
});
|
||
|
||
it("снизу выписка за период", async () => {
|
||
wrap();
|
||
expect(await screen.findByText("Аренда автомобиля")).toBeInTheDocument();
|
||
expect(screen.getAllByText(/17\.08\.2026/).length).toBe(2); // обе операции того дня
|
||
expect(screen.getByRole("button", { name: "Все операции" })).toBeInTheDocument();
|
||
expect(driver.getStatement).toHaveBeenCalled();
|
||
});
|
||
|
||
it("фильтр перезапрашивает операции выбранного вида", async () => {
|
||
wrap();
|
||
await userEvent.click(await screen.findByRole("button", { name: "Начисления" }));
|
||
const calls = vi.mocked(driver.getStatement).mock.calls;
|
||
expect(calls[calls.length - 1][2]).toBe("charges");
|
||
});
|
||
|
||
it("фильтр «Штрафы» показывает постановления, а не проводки", async () => {
|
||
// Штраф, оплаченный водителем самостоятельно, парк не выставлял — проводки по
|
||
// нему нет, и в выписке он бы не появился вовсе (Морозов, 18.08.2026).
|
||
wrap();
|
||
await userEvent.click(await screen.findByRole("button", { name: "Штрафы" }));
|
||
expect(await screen.findByText(/Превышение скорости/)).toBeInTheDocument();
|
||
expect(driver.getFines).toHaveBeenCalled();
|
||
});
|
||
|
||
it("аренда и штрафы видны сразу, остальные статьи — нет", async () => {
|
||
wrap();
|
||
await screen.findByText("−8 976,25 ₽");
|
||
// Эти две водитель проверяет каждый день — они на экране.
|
||
expect(screen.getByText("Долг аренда")).toBeInTheDocument();
|
||
expect(screen.getByText("Долг по штрафам")).toBeInTheDocument();
|
||
expect(screen.getByText("−3 050 ₽")).toBeInTheDocument();
|
||
// Редкие статьи на экран не лезут: для них есть шторка.
|
||
expect(screen.queryByText("Страховка")).not.toBeInTheDocument();
|
||
});
|
||
|
||
it("аренда ведёт к оплате, а штрафы — в свой список", async () => {
|
||
wrap();
|
||
await userEvent.click(await screen.findByRole("button", { name: /Долг аренда/ }));
|
||
expect(await screen.findByText("экран пополнения")).toBeInTheDocument();
|
||
});
|
||
|
||
it("строка штрафов включает фильтр внизу, а не уводит на другой экран", async () => {
|
||
wrap();
|
||
await userEvent.click(await screen.findByRole("button", { name: /Долг по штрафам/ }));
|
||
expect(await screen.findByText(/Превышение скорости/)).toBeInTheDocument();
|
||
expect(driver.getFines).toHaveBeenCalled();
|
||
});
|
||
|
||
it("категории для выбора живут в шторке «Пополнить»", async () => {
|
||
wrap();
|
||
await screen.findByText("−8 976,25 ₽");
|
||
await userEvent.click(screen.getByRole("button", { name: /^Пополнить$/ }));
|
||
const sheet = await screen.findByRole("dialog", { name: "Куда пополнить" });
|
||
const inSheet = within(sheet);
|
||
expect(inSheet.getByText("Долг аренда")).toBeInTheDocument();
|
||
expect(inSheet.getByText("Долг по штрафам")).toBeInTheDocument();
|
||
// «Страховка» платежом не закрывается — в списке выбора ей делать нечего.
|
||
expect(inSheet.queryByText("Страховка")).not.toBeInTheDocument();
|
||
});
|
||
|
||
it("шторка закрывается тапом мимо", async () => {
|
||
wrap();
|
||
await screen.findByText("−8 976,25 ₽");
|
||
await userEvent.click(screen.getByRole("button", { name: /^Пополнить$/ }));
|
||
await userEvent.click(await screen.findByRole("button", { name: "Закрыть" }));
|
||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||
});
|
||
|
||
it("отдельного раздела пополнений нет — они в выписке", async () => {
|
||
// Раздел «Мои пополнения» показывал наши платежи и у большинства водителей был
|
||
// пуст: 1С проводит пополнения сама, и они уже видны фильтром в выписке.
|
||
wrap();
|
||
await screen.findByText("−8 976,25 ₽");
|
||
expect(screen.queryByText(/Мои пополнения/)).not.toBeInTheDocument();
|
||
expect(screen.getByRole("button", { name: "Пополнения" })).toBeInTheDocument();
|
||
});
|
||
|
||
it("баланс не загрузился — экран говорит об этом, а не рисует ноль", async () => {
|
||
vi.mocked(driver.getBalance).mockRejectedValueOnce(new Error("boom"));
|
||
wrap();
|
||
expect(await screen.findByRole("button", { name: /Обновить баланс/ })).toBeInTheDocument();
|
||
});
|
||
});
|