refactor: remove redundant page view models
Frontend verify / commits (pull_request) Successful in 29s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 8m35s

This commit is contained in:
2026-09-28 20:34:33 +03:00
parent 94adbafa34
commit fb3a5a6eaa
10 changed files with 109 additions and 313 deletions
@@ -18,7 +18,7 @@ import {
useLocation,
useNavigate,
} from "react-router";
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { configureAuthClient } from "@/shared/api";
import { createTestQueryClient } from "@/test/query";
@@ -47,13 +47,40 @@ const Navigation: FC = () => {
);
};
function renderEvents(accountId: string, searches: string[]) {
return render(
<QueryClientProvider client={createTestQueryClient()}>
<MantineProvider env="test">
<MemoryRouter
initialEntries={searches.map(search =>
`/accounts/${accountId}/events${search}`,
)}
initialIndex={0}
>
<Navigation />
<Routes>
<Route
element={<AccountEventsPage />}
path="/accounts/:accountId/events"
/>
</Routes>
</MemoryRouter>
</MantineProvider>
</QueryClientProvider>,
);
}
describe("Upcoming event date filters", () => {
it("keeps reads and sync disabled without an account scope", async () => {
beforeEach(() => {
configureAuthClient({
accessToken: () => "token",
clear: vi.fn(),
refresh: () => Promise.resolve(true),
});
});
it("keeps reads and sync disabled without an account scope", async () => {
const requests: string[] = [];
server.use(
http.all("*/api/v1/*", ({ request }) => {
@@ -87,11 +114,6 @@ describe("Upcoming event date filters", () => {
])(
"keeps event calendar dates in GET and source sync requests: %s",
async (search) => {
configureAuthClient({
accessToken: () => "token",
clear: vi.fn(),
refresh: () => Promise.resolve(true),
});
const requested: string[] = [];
const syncRequests: unknown[] = [];
server.use(
@@ -153,11 +175,6 @@ 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) => {
configureAuthClient({
accessToken: () => "token",
clear: vi.fn(),
refresh: () => Promise.resolve(true),
});
const searches: string[] = [];
server.use(
http.get(
@@ -169,31 +186,13 @@ describe("Upcoming event date filters", () => {
},
),
);
render(
<QueryClientProvider client={createTestQueryClient()}>
<MantineProvider env="test">
<MemoryRouter
initialEntries={[
"?from=bad&to=2099-02-28",
"?from=2099-02-30",
"?from=2099-03-01&to=2099-02-28",
"?from=2099-02-01",
"?to=2099-02-28",
].map(search => `/accounts/${accountId}/events${search}`)}
initialIndex={0}
>
<Navigation />
<Routes>
<Route
element={<AccountEventsPage />}
path="/accounts/:accountId/events"
/>
</Routes>
</MemoryRouter>
</MantineProvider>
</QueryClientProvider>,
);
renderEvents(accountId, [
"?from=bad&to=2099-02-28",
"?from=2099-02-30",
"?from=2099-03-01&to=2099-02-28",
"?from=2099-02-01",
"?to=2099-02-28",
]);
await screen.findByLabelText("Период");
for (const expected of ["", "", "?from=2099-02-01", "?to=2099-02-28"]) {
@@ -221,11 +220,6 @@ describe("Upcoming event date filters", () => {
it.each(["account-1", "all"])(
"lets users reset filters after a request failure for %s",
async (accountId) => {
configureAuthClient({
accessToken: () => "token",
clear: vi.fn(),
refresh: () => Promise.resolve(true),
});
server.use(
http.get(
"*/api/v1/brokerage-accounts/:accountId/events",
@@ -235,24 +229,7 @@ describe("Upcoming event date filters", () => {
: HttpResponse.json(resource({ events: [] })),
),
);
render(
<QueryClientProvider client={createTestQueryClient()}>
<MantineProvider env="test">
<MemoryRouter
initialEntries={[`/accounts/${accountId}/events?from=2099-02-01`]}
>
<Navigation />
<Routes>
<Route
element={<AccountEventsPage />}
path="/accounts/:accountId/events"
/>
</Routes>
</MemoryRouter>
</MantineProvider>
</QueryClientProvider>,
);
renderEvents(accountId, ["?from=2099-02-01"]);
await screen.findByRole("button", { name: "Повторить" });
fireEvent.click(screen.getByRole("button", { name: "Сбросить" }));
await screen.findByLabelText("Период");
@@ -263,11 +240,6 @@ 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) => {
configureAuthClient({
accessToken: () => "token",
clear: vi.fn(),
refresh: () => Promise.resolve(true),
});
const searches: string[] = [];
server.use(
http.get(
@@ -286,26 +258,7 @@ describe("Upcoming event date filters", () => {
},
),
);
render(
<QueryClientProvider client={createTestQueryClient()}>
<MantineProvider env="test">
<MemoryRouter
initialEntries={[
`/accounts/${accountId}/events?from=2099-02-01&to=2099-02-28`,
]}
>
<Navigation />
<Routes>
<Route
element={<AccountEventsPage />}
path="/accounts/:accountId/events"
/>
</Routes>
</MemoryRouter>
</MantineProvider>
</QueryClientProvider>,
);
renderEvents(accountId, ["?from=2099-02-01&to=2099-02-28"]);
const from = await screen.findByLabelText("Период");
expect(from).toHaveTextContent("01.02.2099 – 28.02.2099");
expect(screen.getByRole("button", { name: "Применить" })).toBeDisabled();
@@ -11,13 +11,11 @@ import { isValidPeriod } from "@/shared/lib";
type AccountEventsPageViewModel
= | {
canApply: boolean;
draftFrom: null | string;
draftTo: null | string;
draftPeriod: [null | string, null | string];
events: BrokerageEvent[];
lastSuccessfulAt?: null | string;
onApply: () => void;
onDraftFromChange: (value: null | string) => void;
onDraftToChange: (value: null | string) => void;
onPeriodChange: (value: [null | string, null | string]) => void;
onReset: () => void;
partial?: boolean;
refreshError: boolean;
@@ -47,8 +45,12 @@ function useAccountEventsPageViewModel(
? urlTo
: null;
const [draftFrom, setDraftFrom] = useState<null | string>(appliedFrom);
const [draftTo, setDraftTo] = useState<null | string>(appliedTo);
const [draftPeriod, setDraftPeriod] = useState<[
null | string,
null | string,
]>([appliedFrom, appliedTo]);
const [draftFrom, draftTo] = draftPeriod;
useEffect(() => {
if (isValidUrlPeriod) return;
const normalized = new URLSearchParams(searchParameters);
@@ -64,8 +66,7 @@ function useAccountEventsPageViewModel(
if (previousPeriod[0] !== appliedFrom || previousPeriod[1] !== appliedTo) {
setPreviousPeriod([appliedFrom, appliedTo]);
setDraftFrom(appliedFrom);
setDraftTo(appliedTo);
setDraftPeriod([appliedFrom, appliedTo]);
}
const events = useBrokerageEvents(accountId, {
@@ -86,8 +87,7 @@ function useAccountEventsPageViewModel(
if (events.isLoading) return { state: "loading" };
const onReset = () => {
setDraftFrom(null);
setDraftTo(null);
setDraftPeriod([null, null]);
setSearchParameters({});
};
@@ -105,8 +105,7 @@ function useAccountEventsPageViewModel(
canApply:
(draftFrom !== appliedFrom || draftTo !== appliedTo)
&& isValidPeriod(draftFrom, draftTo),
draftFrom,
draftTo,
draftPeriod,
events: events.data.events,
lastSuccessfulAt: events.meta?.lastSuccessfulAt,
onApply: () => {
@@ -116,8 +115,7 @@ function useAccountEventsPageViewModel(
...(draftTo && { to: draftTo }),
});
},
onDraftFromChange: setDraftFrom,
onDraftToChange: setDraftTo,
onPeriodChange: setDraftPeriod,
onReset,
partial: events.meta?.availability !== "complete",
refreshError: events.isError || sync.sourceError || sync.transportError,
@@ -10,6 +10,7 @@ import {
Title,
} from "@mantine/core";
import { DatePickerInput } from "@mantine/dates";
import { useTranslation } from "react-i18next";
import { BrokerageEventCard } from "@/entities/brokerage-event";
import { formatSnapshotTime } from "@/shared/lib";
@@ -17,7 +18,6 @@ import { formatSnapshotTime } from "@/shared/lib";
import type { AccountEventsPageViewModel } from "../AccountEventsPage.vm";
import { AccountEventsPageEmpty } from "./AccountEventsPageEmpty.view";
import { useAccountEventsPageLoadedVm } from "./AccountEventsPageLoaded.vm";
type AccountEventsPageLoadedProps = Extract<
AccountEventsPageViewModel,
@@ -56,7 +56,7 @@ const AccountEventsPageLoadedEvents: FC<AccountEventsPageLoadedProps> = (
};
const AccountEventsPageLoaded: FC<AccountEventsPageLoadedProps> = (props) => {
const { onPeriodChange, t } = useAccountEventsPageLoadedVm(props);
const { t } = useTranslation();
return (
<Stack gap="lg">
@@ -100,13 +100,13 @@ const AccountEventsPageLoaded: FC<AccountEventsPageLoadedProps> = (props) => {
clearable
clearButtonProps={{ "aria-label": t("ledger.clearPeriod") }}
label={t("ledger.period")}
onChange={onPeriodChange}
onChange={props.onPeriodChange}
popoverProps={{
middlewares: { shift: { crossAxis: true } },
returnFocus: true,
}}
type="range"
value={[props.draftFrom, props.draftTo]}
value={props.draftPeriod}
valueFormat="DD.MM.YYYY"
/>
</Group>
@@ -1,24 +0,0 @@
import { useTranslation } from "react-i18next";
import type { AccountEventsPageViewModel } from "../AccountEventsPage.vm";
type AccountEventsPageLoadedProps = Extract<
AccountEventsPageViewModel,
{ state: "loaded" }
>;
function useAccountEventsPageLoadedVm(props: AccountEventsPageLoadedProps) {
const { t } = useTranslation();
const onPeriodChange: (value: [null | string, null | string]) => void = ([
from,
to,
]) => {
props.onDraftFromChange(from);
props.onDraftToChange(to);
};
return { onPeriodChange, t };
}
export { useAccountEventsPageLoadedVm };