refactor: enforce frontend view model boundaries
Frontend verify / commits (pull_request) Successful in 28s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 8m19s

This commit is contained in:
2026-09-28 18:48:23 +03:00
parent 5b69003516
commit ca0ad4f824
287 changed files with 8558 additions and 4538 deletions
+76 -42
View File
@@ -28,8 +28,11 @@ const accounts = [
{ id: "a", name: "Основной" },
{ id: "b", name: "ИИС" },
];
const position: Portfolio["positions"][number] = (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const position: Portfolio["positions"][number]
= {
absoluteReturn: null,
averagePurchasePrice: null,
currentValue: { currency: "RUB", value: "600" },
@@ -40,8 +43,9 @@ const position: Portfolio["positions"][number] = (/* eslint-disable unicorn/no-n
quantity: "2",
ticker: "SBER",
type: "STOCK",
}
/* eslint-enable unicorn/no-null */);
};
/* eslint-enable unicorn/no-null */
const portfolio: Portfolio = {
cashBalances: [],
isStale: false,
@@ -50,8 +54,11 @@ const portfolio: Portfolio = {
structure: { currencies: [], unvaluedCategories: [] },
totalsByCurrency: [{ currency: "RUB", value: "1200" }],
};
const receipt: BrokerageOperation = (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const receipt: BrokerageOperation
= {
accruedInterest: null,
category: "income",
commission: null,
@@ -67,11 +74,15 @@ const receipt: BrokerageOperation = (/* eslint-disable unicorn/no-null -- Fixtur
quantity: null,
ticker: "BOND",
type: "OPERATION_TYPE_COUPON",
}
/* eslint-enable unicorn/no-null */);
};
/* eslint-enable unicorn/no-null */
const items = accounts.map(account => ({ ...receipt, account }));
const analytics: IncomeAnalytics = (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const analytics: IncomeAnalytics
= {
accountId: "all",
bucketGranularity: "day",
buckets: [],
@@ -105,17 +116,21 @@ const analytics: IncomeAnalytics = (/* eslint-disable unicorn/no-null -- Fixture
},
],
yearComparison: null,
}
/* eslint-enable unicorn/no-null */);
};
/* eslint-enable unicorn/no-null */
const CurrentLocation: FC = () => {
const location = useLocation();
return (
<output aria-label="Текущий путь">
{location.pathname}
{location.search}
</output>
);
};
function renderPage(path: string) {
const client = createTestQueryClient();
render(
@@ -124,12 +139,14 @@ function renderPage(path: string) {
<MemoryRouter initialEntries={[path]}>
<AuthProvider>
<AppRoutes />
<CurrentLocation />
</AuthProvider>
</MemoryRouter>
</MantineProvider>
</QueryClientProvider>,
);
return client;
}
@@ -144,14 +161,17 @@ beforeEach(() => {
http.get("*/api/v1/brokerage-accounts", () =>
HttpResponse.json(
resource(
accounts.map(account => ((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
...account,
openedOn: null,
portfolioSummary: null,
type: "BROKERAGE",
}
/* eslint-enable unicorn/no-null */))),
accounts.map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
account =>
({
...account,
openedOn: null,
portfolioSummary: null,
type: "BROKERAGE",
}),
/* eslint-enable unicorn/no-null */
),
),
),
),
@@ -162,15 +182,17 @@ beforeEach(() => {
"*/api/v1/brokerage-accounts/:accountId/operations",
({ params }) =>
HttpResponse.json(
resource((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{
accountId: params.accountId,
hasNext: false,
items,
nextCursor: null,
observedAt: receipt.occurredAt,
}
/* eslint-enable unicorn/no-null */)),
},
/* eslint-enable unicorn/no-null */
),
),
),
http.get("*/api/v1/brokerage-accounts/:accountId/income-analytics", () =>
@@ -181,21 +203,24 @@ beforeEach(() => {
resource({
accountId: "all",
calculatedAt: receipt.occurredAt,
events: accounts.map(account => ((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
account,
date: "2026-09-15",
dateKind: "event",
displayName: "Облигация",
figi: null,
instrumentUid: null,
isin: null,
logoName: null,
projectedAmount: { currency: "RUB", value: "2" },
ticker: "BOND",
type: "coupon",
}
/* eslint-enable unicorn/no-null */))),
events: accounts.map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
account =>
({
account,
date: "2026-09-15",
dateKind: "event",
displayName: "Облигация",
figi: null,
instrumentUid: null,
isin: null,
logoName: null,
projectedAmount: { currency: "RUB", value: "2" },
ticker: "BOND",
type: "coupon",
}),
/* eslint-enable unicorn/no-null */
),
incomeTotalsByCurrency: [],
}),
),
@@ -235,7 +260,11 @@ describe("All accounts routes", () => {
);
for (const suffix of ["operations", "income", "events"])
expect(
screen.getAllByRole("link").find(link => link.getAttribute("href") === `/accounts/all/${suffix}`),
screen
.getAllByRole("link")
.find(
link => link.getAttribute("href") === `/accounts/all/${suffix}`,
),
).toBeInTheDocument();
});
@@ -247,8 +276,10 @@ describe("All accounts routes", () => {
({ params, request }) => {
const url = new URL(request.url);
requests.push(url);
return HttpResponse.json(
resource((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{
accountId: params.accountId,
hasNext: !url.searchParams.has("cursor"),
@@ -262,8 +293,9 @@ describe("All accounts routes", () => {
],
nextCursor: url.searchParams.has("cursor") ? null : "next",
observedAt: receipt.occurredAt,
}
/* eslint-enable unicorn/no-null */)),
},
/* eslint-enable unicorn/no-null */
),
);
},
),
@@ -281,9 +313,11 @@ describe("All accounts routes", () => {
requests.some(url => url.pathname.includes("/b/operations")),
).toBe(true),
);
const selected = requests.find(url =>
url.pathname.includes("/b/operations"),
);
expect(selected?.searchParams.get("from")).toBe("2026-08-01T00:00:00.000Z");
expect(selected?.searchParams.get("to")).toBe("2026-08-31T23:59:59.999Z");
expect(selected?.searchParams.get("categories")).toBe("income");
+6 -3
View File
@@ -62,6 +62,7 @@ describe("AppRoutes", () => {
server.use(
http.post("*/api/v1/auth/login", async ({ request }) => {
credentials = await request.json();
return HttpResponse.json({
accessToken: "login-token",
user: { email: "user@example.com", id: "user-1" },
@@ -103,7 +104,8 @@ describe("AppRoutes", () => {
),
http.get("*/api/v1/bank-cards", () => HttpResponse.json([])),
http.get("*/api/v1/bank-cards/summary", () =>
HttpResponse.json((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json(
{
cardId: null,
categories: [],
@@ -111,8 +113,9 @@ describe("AppRoutes", () => {
month: "2026-08",
net: { currency: "RUB", value: "0" },
refunds: { currency: "RUB", value: "0" },
}
/* eslint-enable unicorn/no-null */)),
},
/* eslint-enable unicorn/no-null */
),
),
);
renderRoutes("/cards?month=2026-08");
+50
View File
@@ -8,72 +8,104 @@ import { Protected, Public } from "../RouteGuards";
const CardPage = lazy(async () => {
const { CardPage } = await import("@/pages/card");
return { default: CardPage };
});
const CardsPage = lazy(async () => {
const { CardsPage } = await import("@/pages/cards");
return { default: CardsPage };
});
const BondPage = lazy(async () => {
const { BondPage } = await import("@/pages/bond");
return { default: BondPage };
});
const BondsPage = lazy(async () => {
const { BondsPage } = await import("@/pages/bonds");
return { default: BondsPage };
});
const StockPage = lazy(async () => {
const { StockPage } = await import("@/pages/stock");
return { default: StockPage };
});
const StocksPage = lazy(async () => {
const { StocksPage } = await import("@/pages/stocks");
return { default: StocksPage };
});
const AccountPage = lazy(async () => {
const { AccountPage } = await import("@/pages/account");
return { default: AccountPage };
});
const AccountEventsPage = lazy(async () => {
const { AccountEventsPage } = await import("@/pages/account-events");
return { default: AccountEventsPage };
});
const AccountOperationsPage = lazy(async () => {
const { AccountOperationsPage } = await import("@/pages/account-operations");
return { default: AccountOperationsPage };
});
const AccountIncomePage = lazy(async () => {
const { AccountIncomePage } = await import("@/pages/account-income");
return { default: AccountIncomePage };
});
const AccountsPage = lazy(async () => {
const { AccountsPage } = await import("@/pages/accounts");
return { default: AccountsPage };
});
const LoginPage = lazy(async () => {
const { LoginPage } = await import("@/pages/login");
return { default: LoginPage };
});
const NotFoundPage = lazy(async () => {
const { NotFoundPage } = await import("@/pages/not-found");
return { default: NotFoundPage };
});
const RegisterPage = lazy(async () => {
const { RegisterPage } = await import("@/pages/register");
return { default: RegisterPage };
});
const SettingsPage = lazy(async () => {
const { SettingsPage } = await import("@/pages/settings");
return { default: SettingsPage };
});
const ProtectedLayout = lazy(async () => {
const { ProtectedLayout } = await import("../ProtectedLayout");
return { default: ProtectedLayout };
});
const AppRoutes: FC = () => (
<Routes>
<Route element={<Navigate replace to="/accounts" />} path="/" />
<Route
element={(
<Public>
@@ -84,6 +116,7 @@ const AppRoutes: FC = () => (
)}
path="/login"
/>
<Route
element={(
<Public>
@@ -94,6 +127,7 @@ const AppRoutes: FC = () => (
)}
path="/register"
/>
<Route
element={(
<Protected>
@@ -111,6 +145,7 @@ const AppRoutes: FC = () => (
)}
path="/cards/:cardId"
/>
<Route
element={(
<LazyPage variant="content">
@@ -119,6 +154,7 @@ const AppRoutes: FC = () => (
)}
path="/cards"
/>
<Route
element={(
<LazyPage variant="content">
@@ -127,6 +163,7 @@ const AppRoutes: FC = () => (
)}
path="/bonds"
/>
<Route
element={(
<LazyPage variant="content">
@@ -135,6 +172,7 @@ const AppRoutes: FC = () => (
)}
path="/bonds/:bondId"
/>
<Route
element={(
<LazyPage variant="content">
@@ -143,6 +181,7 @@ const AppRoutes: FC = () => (
)}
path="/stocks"
/>
<Route
element={(
<LazyPage variant="content">
@@ -151,6 +190,7 @@ const AppRoutes: FC = () => (
)}
path="/stocks/:stockId"
/>
<Route
element={(
<LazyPage variant="content">
@@ -159,6 +199,7 @@ const AppRoutes: FC = () => (
)}
path="/accounts"
/>
<Route
element={(
<LazyPage variant="content">
@@ -167,6 +208,7 @@ const AppRoutes: FC = () => (
)}
path="/accounts/all"
/>
<Route
element={(
<LazyPage variant="content">
@@ -175,6 +217,7 @@ const AppRoutes: FC = () => (
)}
path="/accounts/all/operations"
/>
<Route
element={(
<LazyPage variant="content">
@@ -183,6 +226,7 @@ const AppRoutes: FC = () => (
)}
path="/accounts/all/income"
/>
<Route
element={(
<LazyPage variant="content">
@@ -191,6 +235,7 @@ const AppRoutes: FC = () => (
)}
path="/accounts/all/events"
/>
<Route
element={(
<LazyPage variant="content">
@@ -199,6 +244,7 @@ const AppRoutes: FC = () => (
)}
path="/accounts/:accountId"
/>
<Route
element={(
<LazyPage variant="content">
@@ -207,6 +253,7 @@ const AppRoutes: FC = () => (
)}
path="/accounts/:accountId/events"
/>
<Route
element={(
<LazyPage variant="content">
@@ -215,6 +262,7 @@ const AppRoutes: FC = () => (
)}
path="/accounts/:accountId/income"
/>
<Route
element={(
<LazyPage variant="content">
@@ -223,6 +271,7 @@ const AppRoutes: FC = () => (
)}
path="/accounts/:accountId/operations"
/>
<Route
element={(
<LazyPage variant="content">
@@ -232,6 +281,7 @@ const AppRoutes: FC = () => (
path="/settings"
/>
</Route>
<Route
element={(
<LazyPage variant="content">