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
@@ -18,8 +18,10 @@ import {
useAccountPageViewModel,
} from "./AccountPage.vm";
const portfolio: Portfolio = (/* 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 portfolio: Portfolio
= {
cashBalances: [{ currency: "USD", value: "1" }],
isStale: true,
lastSuccessfulAt: "2026-08-13T10:00:00Z",
@@ -63,8 +65,8 @@ const portfolio: Portfolio = (/* eslint-disable unicorn/no-null -- Fixture prese
],
structure: { currencies: [], unvaluedCategories: [] },
totalsByCurrency: [{ currency: "RUB", value: "150" }],
}
/* eslint-enable unicorn/no-null */);
};
/* eslint-enable unicorn/no-null */
function configureTestAuth() {
configureAuthClient({
@@ -76,6 +78,7 @@ function configureTestAuth() {
function createWrapper() {
const { wrapper: QueryWrapper } = createQueryWrapper();
const Wrapper: FC<PropsWithChildren> = props => (
<QueryWrapper>
<MantineProvider>
@@ -87,6 +90,7 @@ function createWrapper() {
</MantineProvider>
</QueryWrapper>
);
return Wrapper;
}
@@ -97,7 +101,8 @@ function mockLoadedAccountPage() {
),
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
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: "account-1",
hasNext: false,
@@ -121,8 +126,9 @@ function mockLoadedAccountPage() {
],
nextCursor: null,
observedAt: "2026-08-13T10:00:00Z",
}
/* eslint-enable unicorn/no-null */)),
},
/* eslint-enable unicorn/no-null */
),
),
),
http.get("*/api/v1/brokerage-accounts/:accountId/events", () =>
@@ -130,18 +136,22 @@ function mockLoadedAccountPage() {
resource({
accountId: "account-1",
calculatedAt: "2026-08-13T10:00:00Z",
events: Array.from({ length: 6 }, (_, index) => ((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
date: `2026-09-${String(index + 1).padStart(2, "0")}`,
dateKind: "event",
displayName: `Купон ${index}`,
figi: `FIGI-EVENT-${index}`,
instrumentUid: null,
projectedAmount: { currency: "RUB", value: String(index + 1) },
ticker: `BND${index}`,
type: "coupon",
}
/* eslint-enable unicorn/no-null */))),
events: Array.from(
{ length: 6 },
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
(_, index) =>
({
date: `2026-09-${String(index + 1).padStart(2, "0")}`,
dateKind: "event",
displayName: `Купон ${index}`,
figi: `FIGI-EVENT-${index}`,
instrumentUid: null,
projectedAmount: { currency: "RUB", value: String(index + 1) },
ticker: `BND${index}`,
type: "coupon",
}),
/* eslint-enable unicorn/no-null */
),
}),
),
),
@@ -158,6 +168,7 @@ describe("useAccountPageViewModel", () => {
"DIGITAL_FINANCIAL_ASSET",
"FUND",
] as const;
const snapshot: Portfolio = {
...portfolio,
cashBalances: [{ currency: "RUB", value: "1000" }],
@@ -169,19 +180,23 @@ describe("useAccountPageViewModel", () => {
figi: "FIGI-CURRENCY",
type: "CURRENCY",
},
...otherTypes.map(type => ((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
...portfolio.positions[2],
currentValue:
type === "OPTION" ? { currency: "RUB", value: "50" } : null,
figi: `FIGI-${type}-NEXT`,
type,
}
/* eslint-enable unicorn/no-null */))),
...otherTypes.map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
type =>
({
...portfolio.positions[2],
currentValue:
type === "OPTION" ? { currency: "RUB", value: "50" } : null,
figi: `FIGI-${type}-NEXT`,
type,
}),
/* eslint-enable unicorn/no-null */
),
],
structure: { currencies: [], unvaluedCategories: [] },
totalsByCurrency: [{ currency: "RUB", value: "1250" }],
};
const original = structuredClone(snapshot);
const sections = toAccountPagePositionSections(snapshot);
@@ -210,10 +225,12 @@ describe("useAccountPageViewModel", () => {
});
await waitFor(() => expect(result.current.state).toBe("loaded"));
const loaded = result.current as Extract<
typeof result.current,
{ state: "loaded" }
>;
expect(
loaded.positionSections.map(({ groups, kind }) => ({
kind,
@@ -231,35 +248,32 @@ describe("useAccountPageViewModel", () => {
shareText: "66,7 %",
},
],
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
},
{
kind: "bonds",
positions: [
{
displayName: "Облигация",
logoSrc: null,
returnText: "-5 ₽",
returnTone: "negative",
shareText: "33,3 %",
},
],
},
/* eslint-enable unicorn/no-null */ {
kind: "other",
positions: [
{
displayName: "Фонд",
logoSrc: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
returnText: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
returnTone: undefined,
shareText: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
},
],
},
(/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
kind: "bonds",
positions: [
{
displayName: "Облигация",
logoSrc: null,
returnText: "-5 ₽",
returnTone: "negative",
shareText: "33,3 %",
},
],
}
/* eslint-enable unicorn/no-null */),
(/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
kind: "other",
positions: [
{
displayName: "Фонд",
logoSrc: null,
returnText: null,
returnTone: undefined,
shareText: null,
},
],
}
/* eslint-enable unicorn/no-null */),
]);
});
@@ -276,10 +290,12 @@ describe("useAccountPageViewModel", () => {
(result.current as { recentEvents?: { state: string } }).recentEvents,
)?.toMatchObject({ state: "loaded" }),
);
const loaded = result.current as Extract<
typeof result.current,
{ state: "loaded" }
>;
expect(loaded.recentOperations).toMatchObject({
items: [{ description: "Покупка акции", id: "operation-1" }],
state: "loaded",
@@ -335,11 +351,10 @@ it("keeps overview sections independent when their source data is incomplete", a
),
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
(/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{ hasNext: false, items: [], nextCursor: null }
/* eslint-enable unicorn/no-null */),
{ availability: "partial" },
{ hasNext: false, items: [], nextCursor: null },
/* eslint-enable unicorn/no-null */ { availability: "partial" },
),
),
),
@@ -352,9 +367,11 @@ it("keeps overview sections independent when their source data is incomplete", a
),
),
);
const { result } = renderHook(() => useAccountPageViewModel(), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.state).toBe("loaded"));
await waitFor(() =>
expect(result.current).toMatchObject({
@@ -367,14 +384,18 @@ it("keeps overview sections independent when their source data is incomplete", a
it("refreshes every overview section through one account descriptor and observational reads", async () => {
configureTestAuth();
const sections: Record<string, unknown> = (/* 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 sections: Record<string, unknown>
= {
"events": { events: [], incomeTotalsByCurrency: [] },
"income-summary": { totalsByCurrency: [] },
"operations": { hasNext: false, items: [], nextCursor: null },
portfolio,
}
/* eslint-enable unicorn/no-null */);
};
/* eslint-enable unicorn/no-null */
const observed: string[] = [];
const scopes: unknown[] = [];
let version = "1";
@@ -385,6 +406,7 @@ it("refreshes every overview section through one account descriptor and observat
({ request }) => {
if (new URL(request.url).searchParams.get("sync") === "observe")
observed.push(path);
return HttpResponse.json(
resource(data, { resourceKey: path, stateVersion: version }),
);
@@ -394,6 +416,7 @@ it("refreshes every overview section through one account descriptor and observat
http.post("*/api/v1/tbank-sync", async ({ request }) => {
scopes.push(await request.json());
version = "2";
return HttpResponse.json(
{
id: "overview",
@@ -404,7 +427,8 @@ it("refreshes every overview section through one account descriptor and observat
);
}),
http.get("*/api/v1/tbank-sync/:id", () =>
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(
{
errorCode: null,
id: "overview",
@@ -416,13 +440,16 @@ it("refreshes every overview section through one account descriptor and observat
),
scope: null,
state: "succeeded",
}
/* eslint-enable unicorn/no-null */)),
},
/* eslint-enable unicorn/no-null */
),
),
);
const { result } = renderHook(() => useAccountPageViewModel(), {
wrapper: createWrapper(),
});
await waitFor(() =>
expect(result.current).toMatchObject({
incomeSummary: { state: "empty" },