feat: default journal period to last 7 days
Frontend verify / commits (pull_request) Successful in 50s
Frontend verify / security (pull_request) Failing after 13s
Frontend verify / verify (pull_request) Skipped

This commit is contained in:
2026-10-06 22:05:41 +03:00
parent c25f6bd85b
commit 0ea8d7b62d
11 changed files with 121 additions and 34 deletions
@@ -20,6 +20,7 @@ import {
import { beforeEach, describe, expect, it, vi } from "vitest";
import { configureAuthClient } from "@/shared/api";
import { defaultPeriod, formatDate } from "@/shared/lib";
import { createTestQueryClient } from "@/test/query";
import { resource } from "@/test/resource";
import { server } from "@/test/server";
@@ -157,6 +158,7 @@ describe("Upcoming event date filters", () => {
it.each(["account-1", "all"])(
"normalizes invalid links and history while preserving one-sided future dates for %s",
async (accountId) => {
const [defaultFrom, defaultTo] = defaultPeriod();
const searches: string[] = [];
server.use(
http.get(
@@ -195,20 +197,24 @@ describe("Upcoming event date filters", () => {
),
);
expect(screen.getByLabelText("Период")).toHaveTextContent("01.02.2099");
expect(searches).toEqual(["", "?from=2099-02-01", "?to=2099-02-28"]);
expect(searches[0]).toBe("");
expect(searches).toContain(`?from=${defaultFrom}&to=${defaultTo}`);
expect(searches).toContain("?from=2099-02-01");
expect(searches).toContain("?to=2099-02-28");
},
);
it.each(["account-1", "all"])(
"lets users reset filters after a request failure for %s",
async (accountId) => {
const [defaultFrom] = defaultPeriod();
server.use(
http.get(
"*/api/v1/brokerage-accounts/:accountId/events",
({ request }) =>
new URL(request.url).searchParams.has("from")
? HttpResponse.json({ code: "VALIDATION_ERROR" }, { status: 400 })
: HttpResponse.json(resource({ events: [] })),
new URL(request.url).searchParams.get("from") === defaultFrom
? HttpResponse.json(resource({ events: [] }))
: HttpResponse.json({ code: "VALIDATION_ERROR" }, { status: 400 }),
),
);
renderEvents(accountId, ["?from=2099-02-01"]);
@@ -222,6 +228,7 @@ describe("Upcoming event date filters", () => {
it.each(["account-1", "all"])(
"applies dates only on submit, restores URL dates, and resets from empty results for %s",
async (accountId) => {
const [defaultFrom, defaultTo] = defaultPeriod();
const searches: string[] = [];
server.use(
http.get(
@@ -240,38 +247,42 @@ describe("Upcoming event date filters", () => {
},
),
);
renderEvents(accountId, ["?from=2099-02-01&to=2099-02-28"]);
renderEvents(accountId, ["?from=2026-02-01&to=2026-02-28"]);
const from = await screen.findByLabelText("Период");
expect(from).toHaveTextContent("01.02.2099 – 28.02.2099");
expect(from).toHaveTextContent("01.02.2026 – 28.02.2026");
expect(screen.getByRole("button", { name: "Применить" })).toBeDisabled();
expect(searches).toEqual(["?from=2099-02-01&to=2099-02-28"]);
expect(searches).toEqual(["?from=2026-02-01&to=2026-02-28"]);
fireEvent.click(from);
fireEvent.click(
await screen.findByRole("button", { name: /10 February 2099/i }),
await screen.findByRole("button", { name: /10 February 2026/i }),
);
fireEvent.click(
await screen.findByRole("button", { name: /28 February 2099/i }),
await screen.findByRole("button", { name: /28 February 2026/i }),
);
expect(searches).toHaveLength(1);
fireEvent.click(screen.getByRole("button", { name: "Применить" }));
await waitFor(() =>
expect(searches.at(-1)).toBe("?from=2099-02-10&to=2099-02-28"),
expect(searches.at(-1)).toBe("?from=2026-02-10&to=2026-02-28"),
);
expect(await screen.findByLabelText("Период")).toHaveTextContent(
"10.02.2099 – 28.02.2099",
"10.02.2026 – 28.02.2026",
);
fireEvent.click(screen.getByRole("button", { name: "History back" }));
await waitFor(() =>
expect(screen.getByLabelText("Период")).toHaveTextContent(
"01.02.2099 – 28.02.2099",
"01.02.2026 – 28.02.2026",
),
);
expect(screen.getByLabelText("URL")).toHaveTextContent(
"?from=2099-02-01&to=2099-02-28",
"?from=2026-02-01&to=2026-02-28",
);
fireEvent.click(screen.getByRole("button", { name: "Сбросить" }));
await waitFor(() => expect(searches.at(-1)).toBe(""));
expect(await screen.findByLabelText("Период")).toHaveTextContent("");
await waitFor(() =>
expect(searches.at(-1)).toBe(`?from=${defaultFrom}&to=${defaultTo}`),
);
expect(await screen.findByLabelText("Период")).toHaveTextContent(
`${formatDate(defaultFrom)} – ${formatDate(defaultTo)}`,
);
expect(screen.getByRole("button", { name: "Применить" })).toBeDisabled();
expect(
screen.getByText("За выбранный период событий не найдено"),
@@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import type * as BrokerageEvents from "@/entities/brokerage-event";
import { useBrokerageEvents } from "@/entities/brokerage-event";
import { defaultPeriod } from "@/shared/lib";
import { AccountEventsPage } from "./AccountEventsPage";
@@ -71,7 +72,10 @@ describe("AccountEventsPage", () => {
</MemoryRouter>
</MantineProvider>,
);
expect(query).toHaveBeenCalledWith("selected-account", {});
expect(query).toHaveBeenCalledWith("selected-account", {
from: defaultPeriod()[0],
to: defaultPeriod()[1],
});
});
it("keeps the loading fallback when no result or account is available", () => {
@@ -88,7 +92,10 @@ describe("AccountEventsPage", () => {
} as never,
);
renderPage();
expect(query).toHaveBeenCalledWith("", {});
expect(query).toHaveBeenCalledWith("", {
from: defaultPeriod()[0],
to: defaultPeriod()[1],
});
expect(screen.getByRole("status", { name: "Загрузка" })).toHaveAttribute(
"aria-busy",
"true",
@@ -6,15 +6,23 @@ import {
useBrokerageEvents,
} from "@/entities/brokerage-event";
import { useScreenSync } from "@/features/tbank-connection";
import { isValidPeriod, useInitialAvailability, useUrlPeriod } from "@/shared/lib";
import {
defaultPeriod,
isValidPeriod,
moscowDate,
useInitialAvailability,
useUrlPeriod,
} from "@/shared/lib";
type AccountEventsPageViewModel
= | {
canApply: boolean;
draftPeriod: [null | string, null | string];
events: BrokerageEvent[];
maxDate: string;
onApply: () => void;
onPeriodChange: (value: [null | string, null | string]) => void;
onPeriodPopoverOpen: () => void;
onReset: () => void;
partial?: boolean;
state: "loaded";
@@ -38,6 +46,8 @@ function useAccountEventsPageViewModel(): AccountEventsPageViewModel {
const [draftFrom, draftTo] = draftPeriod;
const [maxDate, setMaxDate] = useState(() => moscowDate(new Date()));
const scope = JSON.stringify([accountId, appliedFrom, appliedTo]);
const [previousScope, setPreviousScope] = useState(scope);
@@ -72,7 +82,7 @@ function useAccountEventsPageViewModel(): AccountEventsPageViewModel {
return { state: "loading" };
const onReset = () => {
setDraftPeriod([null, null]);
setDraftPeriod([appliedFrom, appliedTo]);
setSearchParameters({});
};
@@ -92,6 +102,7 @@ function useAccountEventsPageViewModel(): AccountEventsPageViewModel {
&& isValidPeriod(draftFrom, draftTo),
draftPeriod,
events: events.data.events,
maxDate,
onApply: () => {
if (!isValidPeriod(draftFrom, draftTo)) return;
setSearchParameters({
@@ -99,7 +110,10 @@ function useAccountEventsPageViewModel(): AccountEventsPageViewModel {
...(draftTo && { to: draftTo }),
});
},
onPeriodChange: setDraftPeriod,
onPeriodChange: value => setDraftPeriod(
value[0] === null && value[1] === null ? defaultPeriod() : value,
),
onPeriodPopoverOpen: () => setMaxDate(moscowDate(new Date())),
onReset,
partial: events.meta?.availability !== "complete",
state: "loaded",
@@ -65,9 +65,11 @@ const AccountEventsPageLoaded: FC<AccountEventsPageLoadedProps> = (props) => {
clearable
clearButtonProps={{ "aria-label": t("ledger.clearPeriod") }}
label={t("ledger.period")}
maxDate={props.maxDate}
onChange={props.onPeriodChange}
popoverProps={{
middlewares: { shift: { crossAxis: true } },
onOpen: props.onPeriodPopoverOpen,
returnFocus: true,
}}
type="range"