refactor: address frontend review feedback
Frontend verify / commits (pull_request) Successful in 29s
Frontend verify / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 8m54s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 12s
Frontend verify / verify (push) Successful in 9m59s

This commit was merged in pull request #43.
This commit is contained in:
2026-09-28 19:20:32 +03:00
parent ca0ad4f824
commit a4046dbe87
139 changed files with 1549 additions and 941 deletions
@@ -100,7 +100,6 @@ describe("Upcoming event date filters", () => {
return HttpResponse.json(
resource(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
null,
{
sync: {
@@ -109,7 +108,6 @@ describe("Upcoming event date filters", () => {
state: "failed",
},
},
/* eslint-enable unicorn/no-null */
),
);
}),
@@ -37,7 +37,7 @@ function renderPage() {
describe("AccountEventsPage", () => {
beforeEach(() => {
refresh.mockClear();
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
query.mockReturnValue(
{
data: {
@@ -52,7 +52,7 @@ describe("AccountEventsPage", () => {
sync: { errorCode: null, id: null, state: "idle" },
},
refetch: vi.fn(),
} as /* eslint-enable unicorn/no-null */ never,
} as never,
);
});
@@ -73,7 +73,6 @@ describe("AccountEventsPage", () => {
});
it("keeps the loading fallback when no result or account is available", () => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
query.mockReturnValue(
{
data: undefined,
@@ -84,7 +83,7 @@ describe("AccountEventsPage", () => {
sync: { errorCode: null, id: null, state: "idle" },
},
refetch: vi.fn(),
} as /* eslint-enable unicorn/no-null */ never,
} as never,
);
renderPage();
expect(query).toHaveBeenCalledWith("", {});
@@ -95,7 +94,6 @@ describe("AccountEventsPage", () => {
});
it("renders events and marks an offer without an amount", () => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
query.mockReturnValue(
{
data: {
@@ -135,7 +133,7 @@ describe("AccountEventsPage", () => {
sync: { errorCode: null, id: null, state: "idle" },
},
refetch: vi.fn(),
} as /* eslint-enable unicorn/no-null */ never,
} as never,
);
renderPage();
expect(screen.getByText("1 250 ₽")).toBeInTheDocument();
@@ -183,7 +181,6 @@ describe("AccountEventsPage", () => {
});
it("shows a structural busy state before events are available", () => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
query.mockReturnValue(
{
data: undefined,
@@ -194,7 +191,7 @@ describe("AccountEventsPage", () => {
sync: { errorCode: null, id: null, state: "idle" },
},
refetch: vi.fn(),
} as /* eslint-enable unicorn/no-null */ never,
} as never,
);
renderPage();
@@ -12,12 +12,8 @@ const AccountEventsPage: FC<AccountEventsPageProps> = (props) => {
if (viewModel.state === "loading") return <AccountEventsPageLoading />;
return viewModel.state === "error"
? (
<AccountEventsPageError {...viewModel} />
)
: (
<AccountEventsPageLoaded {...viewModel} />
);
? <AccountEventsPageError {...viewModel} />
: <AccountEventsPageLoaded {...viewModel} />;
};
export { AccountEventsPage };
@@ -41,11 +41,11 @@ function useAccountEventsPageViewModel(
const appliedFrom = isValidUrlPeriod
? urlFrom
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null;
const appliedTo = isValidUrlPeriod
? urlTo
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null;
const [draftFrom, setDraftFrom] = useState<null | string>(appliedFrom);
const [draftTo, setDraftTo] = useState<null | string>(appliedTo);
@@ -6,9 +6,7 @@ import { useTranslation } from "react-i18next";
const AccountEventsPageEmpty: FC = () => {
const { t } = useTranslation();
return (
<StateView text={t("events.emptyText")} title={t("events.emptyTitle")} />
);
return <StateView text={t("events.emptyText")} title={t("events.emptyTitle")} />;
};
export { AccountEventsPageEmpty };
@@ -74,13 +74,9 @@ const AccountEventsPageLoaded: FC<AccountEventsPageLoadedProps> = (props) => {
{t("investment.refresh")}
</Button>
{!!props.partial && (
<Alert role="status">{t("investment.partial")}</Alert>
)}
{!!props.partial && <Alert role="status">{t("investment.partial")}</Alert>}
{!!props.refreshing && (
<Progress animated aria-label={t("accounts.refreshing")} value={100} />
)}
{!!props.refreshing && <Progress animated aria-label={t("accounts.refreshing")} value={100} />}
{!!props.refreshError && (
<Alert aria-live="polite" color="yellow" role="status">
@@ -27,9 +27,7 @@ const AccountEventsPageLoading: FC = () => {
<Skeleton height={36} width={108} />
</Group>
{skeletonRowIds.map(id => (
<Skeleton height={82} key={id} radius="md" />
))}
{skeletonRowIds.map(id => <Skeleton height={82} key={id} radius="md" />)}
</LoadingSkeleton>
);
};
+1 -2
View File
@@ -37,14 +37,13 @@ function moscowDate(value: Date): string {
function parseDraftDate(value: string): null | string {
if (!/^\d{2}\.\d{2}\.\d{4}$/.test(value))
// eslint-disable-next-line unicorn/no-null -- Mantine date fields use null for an incomplete or invalid date.
return null;
const [day, month, year] = value.split(".", DATE_PART_COUNT);
const date = `${year}-${month}-${day}`;
return isDate(date)
? date
: null; // eslint-disable-line unicorn/no-null -- Mantine date fields use null for an incomplete or invalid date.
: null;
}
export {
@@ -38,8 +38,6 @@ import {
} from "./AccountIncomePage.vm";
import { IncomePayments } from "./components/IncomePayments.view";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const analytics
= {
accountId: "account-1",
@@ -81,7 +79,7 @@ const analytics
},
totalsByCurrency: [summary("RUB", "10", "2")],
yearComparison: null,
} satisfies /* eslint-enable unicorn/no-null */ IncomeAnalytics;
} satisfies IncomeAnalytics;
function comparison(currency: string) {
return {
@@ -93,28 +91,24 @@ function comparison(currency: string) {
}
function entry(): IncomeAnalytics["entries"][number] {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
return (
{
accruedInterest: null,
category: "income",
commission: null,
description: "Купон",
figi: "FIGI",
id: "coupon-1",
instrumentKey: "income-group-opaque-v2",
instrumentType: "bond",
instrumentUid: null,
logoName: null,
occurredAt: "2026-08-01T12:00:00Z",
payment: money("RUB", "10"),
price: null,
quantity: null,
ticker: "BOND",
type: "OPERATION_TYPE_COUPON",
}
/* eslint-enable unicorn/no-null */
);
return {
accruedInterest: null,
category: "income",
commission: null,
description: "Купон",
figi: "FIGI",
id: "coupon-1",
instrumentKey: "income-group-opaque-v2",
instrumentType: "bond",
instrumentUid: null,
logoName: null,
occurredAt: "2026-08-01T12:00:00Z",
payment: money("RUB", "10"),
price: null,
quantity: null,
ticker: "BOND",
type: "OPERATION_TYPE_COUPON",
};
}
function money(currency: string, value: string) {
@@ -211,7 +205,6 @@ describe("AccountIncomePage", () => {
return HttpResponse.json(
resource(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
null,
{
sync: {
@@ -220,7 +213,6 @@ describe("AccountIncomePage", () => {
state: "failed",
},
},
/* eslint-enable unicorn/no-null */
),
);
},
@@ -434,7 +426,6 @@ describe("AccountIncomePage", () => {
});
it("renders the signed drill-down using opaque membership rather than instrument identity", async () => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const response
= {
...analytics,
@@ -464,7 +455,6 @@ describe("AccountIncomePage", () => {
],
};
/* eslint-enable unicorn/no-null */
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(resource(response)),
@@ -607,37 +597,31 @@ describe("income comparison display", () => {
[
"10",
"0",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null,
],
[
"0",
"0",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null,
],
[
"10",
"-0.00",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null,
],
[
"10",
"-100",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null,
],
[
"9".repeat(400),
"1",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null,
],
[
"1",
"9".repeat(400),
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null,
],
])("formats change %s against base %s", (difference, base, expected) => {
@@ -656,7 +640,7 @@ describe("income comparison display", () => {
const result = display?.comparison?.values[0];
expect(
result?.percentages.coupon?.replaceAll("\u{A0}", " ")
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
?? null,
).toBe(expected);
expect(result?.difference.coupon.value).toBe(difference);
@@ -863,7 +847,7 @@ describe("income period and snapshot boundary", () => {
),
),
http.get("*/api/v1/tbank-sync/:id", () =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json(
{
errorCode: null,
@@ -874,7 +858,6 @@ describe("income period and snapshot boundary", () => {
scope: null,
state: "succeeded",
},
/* eslint-enable unicorn/no-null */
),
),
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
@@ -998,8 +981,6 @@ describe("income period and snapshot boundary", () => {
it("maps comparison notices by role regardless of dependency order or resource key", () => {
const selected = { ...resource(analytics).meta, role: "selected" as const };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const previous
= {
...resource(null).meta,
@@ -1012,7 +993,6 @@ it("maps comparison notices by role regardless of dependency order or resource k
},
};
/* eslint-enable unicorn/no-null */
const year = {
...resource(null).meta,
resourceKey: "opaque/previous-looking-key",
@@ -1041,10 +1021,10 @@ it.each([false, true])(
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{ ...analytics, previous: null, yearComparison: null },
/* eslint-enable unicorn/no-null */ {
{
dependencies: legacy
? [
resource(null).meta,
@@ -1073,22 +1053,21 @@ it.each([false, true])(
it("shows selected income before comparison coverage without inventing a zero baseline", async () => {
const dependencies = (["year", "selected", "previous"] as const).map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
role =>
({
...resource(role === "selected" ? analytics : null).meta,
role,
}),
/* eslint-enable unicorn/no-null */
);
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{ ...analytics, previous: null, yearComparison: null },
/* eslint-enable unicorn/no-null */ { dependencies },
{ dependencies },
),
),
),
@@ -1116,7 +1095,7 @@ it("keeps complete previous comparison while year source failed without inventin
{
...resource(analytics).meta,
role: "previous" as const,
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
},
{
...resource(null).meta,
@@ -1127,7 +1106,7 @@ it("keeps complete previous comparison while year source failed without inventin
state: "failed" as const,
},
},
/* eslint-enable unicorn/no-null */ {
{
...resource(analytics).meta,
role: "selected" as const,
},
@@ -12,12 +12,8 @@ const AccountIncomePage: FC<AccountIncomePageProps> = (props) => {
if (viewModel.state === "loading") return <AccountIncomePageLoading />;
return viewModel.state === "error"
? (
<AccountIncomePageError {...viewModel} />
)
: (
<AccountIncomePageLoaded {...viewModel} />
);
? <AccountIncomePageError {...viewModel} />
: <AccountIncomePageLoaded {...viewModel} />;
};
export { AccountIncomePage };
@@ -161,7 +161,6 @@ function comparisonDisplay(
comparison: IncomeAnalytics["yearComparison"],
): IncomeDisplayModel["comparison"] {
if (!comparison)
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
return null;
return {
@@ -220,11 +219,9 @@ function formatIncomeChange(
// Display-only ratio; server-provided Money values remain exact decimal strings.
const base = Number(compared);
if (!Number.isFinite(base) || base <= 0)
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
return null;
const ratio = Number(difference) / base;
if (!Number.isFinite(ratio))
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
return null;
return new Intl.NumberFormat("ru-RU", {
@@ -272,7 +269,7 @@ function toIncomeDisplayModel(
|| instruments.has(instrument.key)
|| !currencies.has(instrument.total.currency)
)
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
return null;
instruments.set(instrument.key, {
currency: instrument.total.currency,
@@ -286,11 +283,9 @@ function toIncomeDisplayModel(
for (const entry of analytics.entries) {
if (typeof entry.instrumentKey !== "string" || !entry.instrumentKey.trim())
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
return null;
const instrument = instruments.get(entry.instrumentKey);
if (!instrument || entry.payment?.currency !== instrument.currency)
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
return null;
const occurrenceKey = JSON.stringify([entry.instrumentKey, entry.id]);
const occurrence = occurrences.get(occurrenceKey) ?? 0;
@@ -309,7 +304,6 @@ function toIncomeDisplayModel(
for (const instrument of instruments.values()) {
if (instrument.includedCount !== instrument.receiptCount)
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
return null;
}
@@ -35,12 +35,8 @@ const AccountIncomePageLoadedSummary: FC<LoadedProps> = (props) => {
}
return analytics.totalsByCurrency.length > 0
? (
<IncomeSummary totals={analytics.totalsByCurrency} />
)
: (
<StateView text={t("income.emptyText")} title={t("income.emptyTitle")} />
);
? <IncomeSummary totals={analytics.totalsByCurrency} />
: <StateView text={t("income.emptyText")} title={t("income.emptyTitle")} />;
};
const AccountIncomePageLoaded: FC<LoadedProps> = (props) => {
@@ -34,9 +34,7 @@ const IncomeComparisons: FC<IncomeComparisonsProps> = (props) => {
<Title order={2}>{props.title}</Title>
{props.comparison
? (
<Comparison {...props.comparison} title={props.title} />
)
? <Comparison {...props.comparison} title={props.title} />
: (
<Paper className={classes.comparisonNotice} p="sm" radius="md">
<Text aria-live="polite" c="gray.7" role="status" size="sm">
@@ -25,7 +25,7 @@ const PeriodExample: FC<PeriodExampleProps> = (props) => {
const submitted = isSubmitSuccessful
? methods.getValues()
: null; // eslint-disable-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
: null;
return (
<FormProvider {...methods}>
@@ -24,8 +24,6 @@ import { server } from "@/test/server";
import { AccountOperationsPage } from "./AccountOperationsPage";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const operation: BrokerageOperation
= {
accruedInterest: null,
@@ -44,7 +42,6 @@ const operation: BrokerageOperation
ticker: "SBER",
type: "OPERATION_TYPE_BUY",
};
/* eslint-enable unicorn/no-null */
const Navigation: FC = () => {
const location = useLocation();
@@ -322,10 +319,9 @@ describe("AccountOperationsPage", () => {
new URL(request.url).searchParams.has("from")
? HttpResponse.json({ code: "VALIDATION_ERROR" }, { status: 400 })
: HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{ hasNext: false, items: [], nextCursor: null },
/* eslint-enable unicorn/no-null */
),
),
),
@@ -368,7 +364,7 @@ describe("AccountOperationsPage", () => {
searches.push(url.search);
return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{
accountId: "account-1",
@@ -377,7 +373,6 @@ describe("AccountOperationsPage", () => {
nextCursor: url.searchParams.get("cursor") ? null : "next-1",
observedAt: "2026-08-14T00:00:00Z",
},
/* eslint-enable unicorn/no-null */
),
);
},
@@ -411,7 +406,7 @@ describe("AccountOperationsPage", () => {
searches.push(url.search);
return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{
accountId: "account-1",
@@ -420,7 +415,6 @@ describe("AccountOperationsPage", () => {
nextCursor: null,
observedAt: "2026-08-14T00:00:00Z",
},
/* eslint-enable unicorn/no-null */
),
);
},
@@ -451,7 +445,7 @@ describe("AccountOperationsPage", () => {
searches.push(url.search);
return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{
accountId: "account-1",
@@ -460,7 +454,6 @@ describe("AccountOperationsPage", () => {
nextCursor: null,
observedAt: "2026-08-14T00:00:00Z",
},
/* eslint-enable unicorn/no-null */
),
);
},
@@ -508,7 +501,6 @@ it("keeps UTC operation boundaries in GET and source sync requests", async () =>
return HttpResponse.json(
resource(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
null,
{
sync: {
@@ -517,7 +509,6 @@ it("keeps UTC operation boundaries in GET and source sync requests", async () =>
state: "failed",
},
},
/* eslint-enable unicorn/no-null */
),
);
},
@@ -559,7 +550,7 @@ it("does not label incomplete empty operations as a confirmed empty period", asy
server.use(
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(
{
accountId: "account-1",
@@ -567,7 +558,7 @@ it("does not label incomplete empty operations as a confirmed empty period", asy
items: [],
nextCursor: null,
},
/* eslint-enable unicorn/no-null */ { availability: "partial" },
{ availability: "partial" },
),
),
),
@@ -15,12 +15,8 @@ const AccountOperationsPage: FC<AccountOperationsPageProps> = (props) => {
}
return viewModel.state === "error"
? (
<AccountOperationsPageError {...viewModel} />
)
: (
<AccountOperationsPageLoaded {...viewModel} />
);
? <AccountOperationsPageError {...viewModel} />
: <AccountOperationsPageLoaded {...viewModel} />;
};
export { AccountOperationsPage };
@@ -54,11 +54,11 @@ const useAccountOperationsPageVm = (
const appliedFrom = isValidUrlPeriod
? urlFrom
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null;
const appliedTo = isValidUrlPeriod
? urlTo
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null;
const appliedCategories = operationCategories.filter(category =>
(searchParameters.get("categories") ?? "").split(",").includes(category),
@@ -50,9 +50,7 @@ const AccountOperationsPageLoadedOperations: FC<
return;
}
return (
<StateView text={t("ledger.emptyText")} title={t("ledger.emptyTitle")} />
);
return <StateView text={t("ledger.emptyText")} title={t("ledger.emptyTitle")} />;
};
const AccountOperationsPageLoaded: FC<AccountOperationsPageLoadedProps> = (
@@ -91,15 +89,11 @@ const AccountOperationsPageLoaded: FC<AccountOperationsPageLoadedProps> = (
{t("investment.refresh")}
</Button>
{!!props.partial && (
<Alert role="status">{t("investment.partial")}</Alert>
)}
{!!props.partial && <Alert role="status">{t("investment.partial")}</Alert>}
{!!props.historyLoading && <Alert>{t("investment.history")}</Alert>}
{!!props.refreshing && (
<Progress animated aria-label={t("accounts.refreshing")} value={100} />
)}
{!!props.refreshing && <Progress animated aria-label={t("accounts.refreshing")} value={100} />}
{!!props.refreshError && (
<Alert aria-live="polite" color="yellow" role="status">
@@ -28,9 +28,7 @@ const AccountOperationsPageLoading: FC = () => {
</Group>
<Stack gap="xs">
{skeletonRowIds.map(id => (
<Skeleton height={64} key={id} radius="md" />
))}
{skeletonRowIds.map(id => <Skeleton height={64} key={id} radius="md" />)}
</Stack>
</LoadingSkeleton>
);
@@ -67,8 +67,6 @@ function renderPage(ui: React.ReactNode) {
);
}
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const portfolio: Portfolio
= {
cashBalances: [{ currency: "USD", value: "0.5" }],
@@ -103,7 +101,6 @@ const portfolio: Portfolio
structure: { currencies: [], unvaluedCategories: [] },
totalsByCurrency: [{ currency: "RUB", value: "1234.5" }],
};
/* eslint-enable unicorn/no-null */
const emptyRecentOperations: AccountPageRecentOperations = {
state: "empty",
@@ -146,8 +143,6 @@ describe("AccountPage states", () => {
const uid = "c190ff1f-1447-4227-b543-316332699ca5";
const requests = vi.fn();
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const position
= {
...portfolio.positions[0],
@@ -158,7 +153,6 @@ describe("AccountPage states", () => {
type,
};
/* eslint-enable unicorn/no-null */
server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
HttpResponse.json(
@@ -483,7 +477,6 @@ describe("AccountPage states", () => {
it("renders stock cards with logo fallback and signed unlabeled return", () => {
renderPage(
<AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps(
{
...portfolio,
@@ -514,7 +507,6 @@ describe("AccountPage states", () => {
},
],
},
/* eslint-enable unicorn/no-null */
)}
/>,
);
@@ -528,7 +520,6 @@ describe("AccountPage states", () => {
it("groups stock supporting metrics into quiet single lines", () => {
renderPage(
<AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps(
{
...portfolio,
@@ -547,7 +538,6 @@ describe("AccountPage states", () => {
},
],
},
/* eslint-enable unicorn/no-null */
)}
/>,
);
@@ -563,7 +553,6 @@ describe("AccountPage states", () => {
it("uses the ticker only for stock cards and otherwise keeps the ISIN", () => {
renderPage(
<AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps(
{
...portfolio,
@@ -606,7 +595,6 @@ describe("AccountPage states", () => {
},
],
},
/* eslint-enable unicorn/no-null */
)}
/>,
);
@@ -623,7 +611,6 @@ describe("AccountPage states", () => {
it("renders bond cards with logo, fallback, and signed unlabeled return like stocks", () => {
renderPage(
<AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps(
{
...portfolio,
@@ -654,7 +641,6 @@ describe("AccountPage states", () => {
},
],
},
/* eslint-enable unicorn/no-null */
)}
/>,
);
@@ -674,7 +660,6 @@ describe("AccountPage states", () => {
it("renders bond cards in expanded layout with bond-specific classes", () => {
renderPage(
<AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps(
{
...portfolio,
@@ -693,7 +678,6 @@ describe("AccountPage states", () => {
},
],
},
/* eslint-enable unicorn/no-null */
)}
/>,
);
@@ -707,7 +691,6 @@ describe("AccountPage states", () => {
it("keeps other-position cards compact with labeled return and no logo", () => {
renderPage(
<AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps(
{
...portfolio,
@@ -726,7 +709,6 @@ describe("AccountPage states", () => {
},
],
},
/* eslint-enable unicorn/no-null */
)}
/>,
);
@@ -738,17 +720,14 @@ describe("AccountPage states", () => {
renderPage(
<AccountPageLoaded
{...loadedProps()}
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
account={
{
id: "account-1",
name: "Основной",
openedOn: null,
portfolioSummary: null,
type: "BROKERAGE",
}
/* eslint-enable unicorn/no-null */
}
account={{
id: "account-1",
name: "Основной",
openedOn: null,
portfolioSummary: null,
type: "BROKERAGE",
}}
/>,
);
expect(
@@ -777,33 +756,30 @@ describe("AccountPage states", () => {
renderPage(
<AccountPageLoaded
{...loadedProps()}
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
recentOperations={
{
items: [
{
accruedInterest: null,
category: "trade",
commission: null,
description: "Покупка акции",
figi: "FIGI-1",
id: "operation-1",
instrumentType: "share",
instrumentUid: "UID-1",
logoName: null,
occurredAt: "2026-08-14T10:00:00Z",
payment: { currency: "RUB", value: "-12" },
price: null,
quantity: null,
ticker: "SBER",
type: "OPERATION_TYPE_BUY",
},
],
state: "loaded",
to: "/accounts/account-1/operations",
}
/* eslint-enable unicorn/no-null */
}
recentOperations={{
items: [
{
accruedInterest: null,
category: "trade",
commission: null,
description: "Покупка акции",
figi: "FIGI-1",
id: "operation-1",
instrumentType: "share",
instrumentUid: "UID-1",
logoName: null,
occurredAt: "2026-08-14T10:00:00Z",
payment: { currency: "RUB", value: "-12" },
price: null,
quantity: null,
ticker: "SBER",
type: "OPERATION_TYPE_BUY",
},
],
state: "loaded",
to: "/accounts/account-1/operations",
}}
/>,
);
@@ -812,7 +788,6 @@ describe("AccountPage states", () => {
});
it("keeps future events ordered with calendar dates and projected rather than posted amounts", () => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const event: BrokerageEvent
= {
date: "2026-09-10",
@@ -827,41 +802,37 @@ describe("AccountPage states", () => {
type: "coupon",
};
/* eslint-enable unicorn/no-null */
renderPage(
<AccountPageLoaded
{...loadedProps()}
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
recentEvents={
{
items: [
event,
{
...event,
date: "2026-09-11",
dateKind: "registry",
displayName: "Акция",
projectedAmount: { currency: "USD", value: "0" },
type: "dividend",
},
{
...event,
date: "2026-09-12",
projectedAmount: { currency: "RUB", value: "1000" },
type: "maturity",
},
{
...event,
date: "2026-09-13",
projectedAmount: null,
type: "offer",
},
],
state: "loaded",
to: "/accounts/account-1/events",
}
/* eslint-enable unicorn/no-null */
}
recentEvents={{
items: [
event,
{
...event,
date: "2026-09-11",
dateKind: "registry",
displayName: "Акция",
projectedAmount: { currency: "USD", value: "0" },
type: "dividend",
},
{
...event,
date: "2026-09-12",
projectedAmount: { currency: "RUB", value: "1000" },
type: "maturity",
},
{
...event,
date: "2026-09-13",
projectedAmount: null,
type: "offer",
},
],
state: "loaded",
to: "/accounts/account-1/events",
}}
/>,
);
@@ -953,7 +924,6 @@ describe("AccountPage states", () => {
it.each([503])(
"preserves totals and UTC snapshot through refresh error %i, retry and success",
async (status) => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const initialPortfolio: Portfolio
= {
...portfolio,
@@ -970,7 +940,6 @@ describe("AccountPage states", () => {
},
};
/* eslint-enable unicorn/no-null */
let requestCount = 0;
let resolveRetry: ((response: Response) => void) | undefined;
server.use(
@@ -985,7 +954,7 @@ describe("AccountPage states", () => {
),
),
http.get("*/api/v1/tbank-sync/:id", () =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json(
{
errorCode: null,
@@ -999,7 +968,6 @@ describe("AccountPage states", () => {
scope: { accountId: "account-1", screen: "account" },
state: "succeeded",
},
/* eslint-enable unicorn/no-null */
),
),
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => {
@@ -18,8 +18,6 @@ import {
useAccountPageViewModel,
} from "./AccountPage.vm";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const portfolio: Portfolio
= {
cashBalances: [{ currency: "USD", value: "1" }],
@@ -66,7 +64,6 @@ const portfolio: Portfolio
structure: { currencies: [], unvaluedCategories: [] },
totalsByCurrency: [{ currency: "RUB", value: "150" }],
};
/* eslint-enable unicorn/no-null */
function configureTestAuth() {
configureAuthClient({
@@ -101,7 +98,7 @@ function mockLoadedAccountPage() {
),
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(
{
accountId: "account-1",
@@ -127,7 +124,6 @@ function mockLoadedAccountPage() {
nextCursor: null,
observedAt: "2026-08-13T10:00:00Z",
},
/* eslint-enable unicorn/no-null */
),
),
),
@@ -138,7 +134,7 @@ function mockLoadedAccountPage() {
calculatedAt: "2026-08-13T10:00:00Z",
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")}`,
@@ -150,7 +146,6 @@ function mockLoadedAccountPage() {
ticker: `BND${index}`,
type: "coupon",
}),
/* eslint-enable unicorn/no-null */
),
}),
),
@@ -181,7 +176,7 @@ describe("useAccountPageViewModel", () => {
type: "CURRENCY",
},
...otherTypes.map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
type =>
({
...portfolio.positions[2],
@@ -190,7 +185,6 @@ describe("useAccountPageViewModel", () => {
figi: `FIGI-${type}-NEXT`,
type,
}),
/* eslint-enable unicorn/no-null */
),
],
structure: { currencies: [], unvaluedCategories: [] },
@@ -248,7 +242,7 @@ describe("useAccountPageViewModel", () => {
shareText: "66,7 %",
},
],
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
},
{
kind: "bonds",
@@ -262,15 +256,15 @@ describe("useAccountPageViewModel", () => {
},
],
},
/* 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.
logoSrc: null,
returnText: null,
returnTone: undefined,
shareText: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
shareText: null,
},
],
},
@@ -351,10 +345,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(
{ hasNext: false, items: [], nextCursor: null },
/* eslint-enable unicorn/no-null */ { availability: "partial" },
{ availability: "partial" },
),
),
),
@@ -385,8 +379,6 @@ 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();
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const sections: Record<string, unknown>
= {
"events": { events: [], incomeTotalsByCurrency: [] },
@@ -395,7 +387,6 @@ it("refreshes every overview section through one account descriptor and observat
portfolio,
};
/* eslint-enable unicorn/no-null */
const observed: string[] = [];
const scopes: unknown[] = [];
let version = "1";
@@ -427,7 +418,7 @@ it("refreshes every overview section through one account descriptor and observat
);
}),
http.get("*/api/v1/tbank-sync/:id", () =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json(
{
errorCode: null,
@@ -441,7 +432,6 @@ it("refreshes every overview section through one account descriptor and observat
scope: null,
state: "succeeded",
},
/* eslint-enable unicorn/no-null */
),
),
);
@@ -209,7 +209,7 @@ function toPosition(
const returnValue = position.absoluteReturn
? Number(position.absoluteReturn.value)
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null;
let returnTone: AccountPageReturnTone | undefined;
if (returnValue !== null && returnValue !== 0)
@@ -226,17 +226,17 @@ function toPosition(
: undefined,
logoSrc: isExpanded
? instrumentLogoUrl(position.logoName)
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null,
returnText: position.absoluteReturn
? `${returnPrefix}${formatMoney(position.absoluteReturn)}`
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null,
returnTone,
shareText: position.currentValue?.currency
? formatShare(position.currentValue, {
currency: position.currentValue.currency,
value: String(denominators.get(position.currentValue.currency) ?? 0),
})
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null,
};
}
@@ -62,9 +62,7 @@ const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
sourceError={props.sourceError}
/>
{!!props.portfolio.isStale && (
<Alert color="blue">{t("portfolio.staleWarning")}</Alert>
)}
{!!props.portfolio.isStale && <Alert color="blue">{t("portfolio.staleWarning")}</Alert>}
<Text c="dimmed" size="sm">
{t(
@@ -78,12 +76,8 @@ const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
</Text>
{props.partial
? (
<Alert role="status">{t("investment.partial")}</Alert>
)
: (
<CurrencyCards amounts={props.portfolio.totalsByCurrency} />
)}
? <Alert role="status">{t("investment.partial")}</Alert>
: <CurrencyCards amounts={props.portfolio.totalsByCurrency} />}
{!props.partial && (
<PortfolioStructure
@@ -34,9 +34,7 @@ const AccountPageLoading: FC = () => {
<Skeleton height={260} radius="sm" />
{["first", "second", "third"].map(placeholder => (
<Skeleton height={52} key={placeholder} radius="sm" />
))}
{["first", "second", "third"].map(placeholder => <Skeleton height={52} key={placeholder} radius="sm" />)}
</LoadingSkeleton>
);
};
@@ -69,9 +69,7 @@ const IncomeSummary: FC<IncomeSummaryProps> = (props) => {
{!!props.stale && <Text size="xs">{t("investment.stale")}</Text>}
{!!props.refreshError && (
<Text role="status">{t("income.refreshError")}</Text>
)}
{!!props.refreshError && <Text role="status">{t("income.refreshError")}</Text>}
{props.summary.totalsByCurrency.map(total => (
<Paper key={total.currency} p="sm" withBorder>
@@ -81,13 +81,12 @@ export const Incomplete: Story = {
{
...rub,
categories: rub.categories.map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
row =>
({
...row,
sharePercent: null,
}),
/* eslint-enable unicorn/no-null */
),
},
],
@@ -110,14 +109,13 @@ export const Negative: Story = {
{
...rub,
categories: rub.categories.map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
row =>
({
...row,
sharePercent: null,
value: row.type === "OTHER" ? "25000" : row.value,
}),
/* eslint-enable unicorn/no-null */
),
hasNegativeValues: true,
totalValue: "1150000",
@@ -27,7 +27,6 @@ const ui = (structure: null | Portfolio["structure"] | undefined) => (
describe("portfolio structure", () => {
it.each([
undefined,
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null,
])(
"keeps an older snapshot without structure available (%s) and recovers on refresh",
@@ -74,13 +73,12 @@ describe("portfolio structure", () => {
{
...rub,
categories: rub.categories.map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
row =>
({
...row,
sharePercent: null,
}),
/* eslint-enable unicorn/no-null */
),
},
],
@@ -106,14 +104,13 @@ describe("portfolio structure", () => {
{
...rub,
categories: rub.categories.map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
row =>
({
...row,
sharePercent: null,
value: row.type === "STOCK" ? "-60" : row.value,
}),
/* eslint-enable unicorn/no-null */
),
hasNegativeValues: true,
},
@@ -97,9 +97,7 @@ const PortfolioStructureView: FC<PortfolioStructureViewProps> = (props) => {
</Stack>
)}
{!!props.negative && (
<Text size="sm">{t("portfolio.structure.negative")}</Text>
)}
{!!props.negative && <Text size="sm">{t("portfolio.structure.negative")}</Text>}
{!!(
!props.unavailable
@@ -35,7 +35,7 @@ const usePortfolioStructureVm = (props: PortfolioStructureProps) => {
return {
chartData,
currencies: structure?.currencies.map(item => item.currency) ?? [],
// eslint-disable-next-line unicorn/no-null -- Mantine clears the controlled currency with null.
currency: selected?.currency ?? null,
incomplete: isIncomplete,
negative: selected?.hasNegativeValues ?? false,
@@ -4,8 +4,6 @@ import type { AccountPagePosition } from "../../AccountPage.vm";
import { PositionCard } from "./PositionCard.view";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const syntheticPosition
= {
absoluteReturn: { currency: "RUB", value: "5000" },
@@ -22,7 +20,7 @@ const syntheticPosition
shareText: "20%",
ticker: "DEMO",
type: "STOCK",
} satisfies /* eslint-enable unicorn/no-null */ AccountPagePosition;
} satisfies AccountPagePosition;
const meta = {
args: { kind: "stocks", position: syntheticPosition },
@@ -62,8 +60,6 @@ export const StockGain: Story = {
},
};
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const BondLoss: Story
= {
args: {
@@ -94,9 +90,6 @@ export const BondLoss: Story
},
},
};
/* eslint-enable unicorn/no-null */
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const Other: Story
= {
@@ -120,9 +113,6 @@ export const Other: Story
},
},
};
/* eslint-enable unicorn/no-null */
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const MissingValuation: Story
= {
@@ -149,4 +139,3 @@ export const MissingValuation: Story
},
},
};
/* eslint-enable unicorn/no-null */
@@ -60,10 +60,10 @@ const PositionCard: FC<PositionCardProps> = (props) => {
? t("portfolio.avgPrice", {
value: formatMoney(position.averagePurchasePrice),
})
: null, // eslint-disable-line unicorn/no-null -- Mantine represents an empty controlled field as null.
: null,
position.shareText
? t("portfolio.share", { value: position.shareText })
: null, // eslint-disable-line unicorn/no-null -- Mantine represents an empty controlled field as null.
: null,
]
.filter(Boolean)
.join(" · ");
@@ -44,9 +44,7 @@ const PositionGroupCard: FC<PositionGroupCardProps> = (props) => {
{identity.displayName}
</Anchor>
)
: (
<Text fw={600}>{identity.displayName}</Text>
)}
: <Text fw={600}>{identity.displayName}</Text>}
<Text c="dimmed" size="xs">
{identity.ticker ?? identity.isin ?? identity.figi}
@@ -90,9 +88,7 @@ const PositionGroupCard: FC<PositionGroupCardProps> = (props) => {
</Group>
<Group gap="xs" justify="space-between">
{!!position.currentValue && (
<Text size="sm">{formatMoney(position.currentValue)}</Text>
)}
{!!position.currentValue && <Text size="sm">{formatMoney(position.currentValue)}</Text>}
{!!position.returnText && (
<Text className={appearanceClass} size="sm">
@@ -17,9 +17,7 @@ const PositionLogo: FC<PositionLogoProps> = (props) => {
const logo
= src && !failed
? (
<Avatar alt={label} onError={onError} radius="md" size={40} src={src} />
)
? <Avatar alt={label} onError={onError} radius="md" size={40} src={src} />
: (
<Avatar radius="md" size={40}>
{(label.trim()[0] ?? "?").toUpperCase()}
@@ -32,31 +32,21 @@ const RecentOperations: FC<RecentOperationsProps> = (props) => {
{!!operations.stale && <Text size="xs">{t("investment.stale")}</Text>}
{!!operations.refreshError && (
<Text role="status">{t("accounts.refreshError")}</Text>
)}
{!!operations.refreshError && <Text role="status">{t("accounts.refreshError")}</Text>}
</>
)}
{operations.state === "loading" && (
<LoadingSkeleton label={t("ledger.recentLoading")}>
{["first", "second", "third"].map(placeholder => (
<Skeleton height={48} key={placeholder} radius="sm" />
))}
{["first", "second", "third"].map(placeholder => <Skeleton height={48} key={placeholder} radius="sm" />)}
</LoadingSkeleton>
)}
{operations.state === "error" && (
<Alert color="red">{t("ledger.recentError")}</Alert>
)}
{operations.state === "error" && <Alert color="red">{t("ledger.recentError")}</Alert>}
{operations.state === "empty" && (
<Text c="dimmed">{t("ledger.recentEmpty")}</Text>
)}
{operations.state === "empty" && <Text c="dimmed">{t("ledger.recentEmpty")}</Text>}
{operations.state === "partial" && (
<Text role="status">{t("investment.partial")}</Text>
)}
{operations.state === "partial" && <Text role="status">{t("investment.partial")}</Text>}
{(operations.state === "loaded" || operations.state === "partial")
&& operations.items.map(operation => (
@@ -8,8 +8,6 @@ import type { Portfolio } from "@/entities/portfolio";
import { toAccountPagePositionSections } from "./AccountPage.vm";
import { PositionSection } from "./components/PositionSection.view";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const position: Portfolio["positions"][number]
= {
absoluteReturn: { currency: "RUB", value: "25" },
@@ -25,8 +23,6 @@ const position: Portfolio["positions"][number]
type: "STOCK",
};
/* eslint-enable unicorn/no-null */
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const second
= {
...position,
@@ -37,7 +33,6 @@ const second
quantity: "-2",
};
/* eslint-enable unicorn/no-null */
function sections(positions: Portfolio["positions"]) {
return toAccountPagePositionSections({
cashBalances: [],
@@ -84,30 +79,30 @@ describe("Positions grouped by security", () => {
it("uses ISIN only when FIGI is absent, never merging by name or ticker", () => {
expect(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
sections([
{
...position,
figi: null,
},
/* eslint-enable unicorn/no-null */ { ...second, figi: null }, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
{ ...second, figi: null },
])[0].groups,
).toHaveLength(1);
expect(
sections([position, { ...second, figi: "OTHER-FIGI" }])[0].groups,
).toHaveLength(2);
expect(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
sections([
{
...position,
figi: null,
isin: null,
},
/* eslint-enable unicorn/no-null */ {
{
...second,
figi: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
isin: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
figi: null,
isin: null,
},
])[0].groups,
).toHaveLength(2);
@@ -31,7 +31,7 @@ it("keeps expanded positions vertical while other positions adapt to the viewpor
const otherType = kind === "bonds" ? "BOND" : "FUND";
const positions: AccountPagePosition[] = [1, 2, 3].map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
index =>
({
absoluteReturn: null,
@@ -48,7 +48,6 @@ it("keeps expanded positions vertical while other positions adapt to the viewpor
ticker: null,
type: kind === "stocks" ? "STOCK" : otherType,
}),
/* eslint-enable unicorn/no-null */
);
const { unmount } = render(
@@ -60,7 +60,7 @@ describe("AccountsPage", () => {
renderPage(createTestQueryClient());
fireEvent.click(await screen.findByRole("button", { name: "Повторить" }));
expect(await screen.findByText(account.name)).toBeVisible();
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
expect(modes).toEqual([null, "observe"]);
});
it.each([
@@ -212,12 +212,11 @@ it("keeps unknown membership loading instead of declaring no accounts", async ()
const client = createTestQueryClient();
renderPage(client);
await waitFor(() =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
expect(client.getQueryData(brokerageAccountsQueryKey)).toMatchObject(
{
data: null,
},
/* eslint-enable unicorn/no-null */
),
);
expect(
+4 -14
View File
@@ -27,8 +27,6 @@ const LocationProbe = () => {
const uid = "e6123145-9665-43e0-8413-cd61b8aa9b13";
const money = (value: string) => ({ currency: "RUB", value });
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const bond
= {
aci: money("12"),
@@ -52,7 +50,6 @@ const bond
ticker: "TEST",
};
/* eslint-enable unicorn/no-null */
const position = {
absoluteReturn: money("1000"),
accounts: [
@@ -74,8 +71,6 @@ const position = {
returnPercent: "50",
};
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const quote
= {
aci: money("12"),
@@ -90,7 +85,6 @@ const quote
unavailable: [],
};
/* eslint-enable unicorn/no-null */
function setup(
options: {
ambiguousTicker?: boolean;
@@ -122,7 +116,7 @@ function setup(
requests.push(new URL(request.url));
return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{
coupon: money("32.88"),
@@ -158,7 +152,6 @@ function setup(
unavailable:
options.ratingState === "unavailable" ? ["rating"] : [],
},
/* eslint-enable unicorn/no-null */
),
);
}),
@@ -179,7 +172,7 @@ function setup(
);
if (params.section === "candles")
return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{
candles: [
@@ -193,7 +186,6 @@ function setup(
historyFrom: null,
observedAt: quote.observedAt,
},
/* eslint-enable unicorn/no-null */
),
);
if (options.failCoupons)
@@ -203,7 +195,7 @@ function setup(
);
return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{
coupons: [
@@ -235,7 +227,6 @@ function setup(
positionIsStale: false,
to: "2027-09-10",
},
/* eslint-enable unicorn/no-null */
),
);
}),
@@ -245,7 +236,7 @@ function setup(
const isNext = url.searchParams.has("cursor");
return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{
accountId: "all",
@@ -268,7 +259,6 @@ function setup(
nextCursor: isNext ? null : "next",
observedAt: quote.observedAt,
},
/* eslint-enable unicorn/no-null */
),
);
}),
+5 -15
View File
@@ -172,17 +172,11 @@ const BondPageView: FC<BondPageViewProps> = (props) => {
</Group>
<Group gap="xs" mt="md">
{!!bond.floatingCoupon && (
<Badge variant="light">{t("bond.floatingCoupon")}</Badge>
)}
{!!bond.floatingCoupon && <Badge variant="light">{t("bond.floatingCoupon")}</Badge>}
{!!bond.amortization && (
<Badge variant="light">{t("bond.amortization")}</Badge>
)}
{!!bond.amortization && <Badge variant="light">{t("bond.amortization")}</Badge>}
{!!bond.perpetual && (
<Badge variant="light">{t("bond.perpetual")}</Badge>
)}
{!!bond.perpetual && <Badge variant="light">{t("bond.perpetual")}</Badge>}
{!!bond.subordinated && (
<Badge color="orange" variant="light">
@@ -414,9 +408,7 @@ const BondCoupons: FC<BondPageViewProps> = (props) => {
/>
))
)
: (
<Text c="dimmed">{t("bond.noCoupons")}</Text>
)}
: <Text c="dimmed">{t("bond.noCoupons")}</Text>}
<Text c="dimmed" size="xs">
{t("bond.couponWindow", {
@@ -452,9 +444,7 @@ const BondCoupons: FC<BondPageViewProps> = (props) => {
/>
))
)
: (
<Text c="dimmed">{t("bond.noCoupons")}</Text>
)}
: <Text c="dimmed">{t("bond.noCoupons")}</Text>}
</Stack>
</Accordion.Panel>
</Accordion.Item>
+3 -4
View File
@@ -91,7 +91,6 @@ function useBondPageVm() {
return {
averageLine:
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
null,
bond: bond.data,
bondState: section(bond, uid ? sync.retry : () => bond.refetch()),
@@ -100,7 +99,7 @@ function useBondPageVm() {
canonicalTo:
bond.data?.slug && bond.data.slug !== bondId
? `/bonds/${encodeURIComponent(bond.data.slug)}${search}`
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null,
chartData,
code: apiErrorCode(bond.error),
coupons: coupons.data,
@@ -111,12 +110,12 @@ function useBondPageVm() {
logoName:
item.logoName
?? bond.data?.logoName
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
?? null,
ticker:
item.ticker
?? bond.data?.ticker
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
?? null,
})) ?? [],
filter,
+2 -7
View File
@@ -19,8 +19,6 @@ import { CardPage } from "./CardPage";
const money = (value: string) => ({ currency: "RUB", value });
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const card
= {
archivedAt: null,
@@ -34,8 +32,6 @@ const card
version: 1,
};
/* eslint-enable unicorn/no-null */
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const operation
= {
amount: money("50.01"),
@@ -55,7 +51,6 @@ const operation
version: 1,
};
/* eslint-enable unicorn/no-null */
function setup(
options: {
cardsError?: boolean;
@@ -342,13 +337,13 @@ describe("CardPage", () => {
};
bodies.push(body);
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
current
= {
...current,
archivedAt: body.archived ? "2026-09-01" : null,
version: current.version + 1,
} as /* eslint-enable unicorn/no-null */ typeof card;
} as typeof card;
return HttpResponse.json(current);
}),
+2 -6
View File
@@ -227,12 +227,8 @@ const CardPageView: FC<CardPageViewProps> = (props) => {
variant="subtle"
>
{card.archivedAt
? (
<RestoreIcon size={18} />
)
: (
<DeleteIcon size={18} strokeWidth={1.5} />
)}
? <RestoreIcon size={18} />
: <DeleteIcon size={18} strokeWidth={1.5} />}
</ActionIcon>
</Group>
</div>
+1 -1
View File
@@ -84,7 +84,7 @@ function useCardPageVm() {
otherCard:
item.kind === "transfer"
? otherCardId
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null,
title,
tone: isOutgoing ? ("negative" as const) : ("positive" as const),
value: `${isOutgoing ? "−" : "+"}${formatMoney(item.amount)}`,
@@ -25,7 +25,7 @@ function useCardPagePresentationVm(vm: CardPageVm) {
if (pending.scope !== scope || vm.history.isError) {
pendingPageRef.current
// eslint-disable-next-line unicorn/no-null -- Reset the pending-pagination ref to its initial null sentinel.
= null;
return;
@@ -40,7 +40,6 @@ function useCardPagePresentationVm(vm: CardPageVm) {
if (delta) window.scrollBy({ behavior: "instant", top: delta });
}
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
pendingPageRef.current = null;
}, [
scope,
@@ -12,8 +12,6 @@ import { server } from "@/test/server";
import { DeleteOperation } from "./DeleteOperation";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const transfer: BankOperation
= {
amount: { currency: "RUB", value: "100" },
@@ -33,7 +31,6 @@ const transfer: BankOperation
version: 1,
};
/* eslint-enable unicorn/no-null */
describe("DeleteOperation", () => {
it("names both sides and requires a deliberate fresh version after conflict", async () => {
configureAuthClient({
@@ -59,7 +59,7 @@ function useOperationFieldsVm(props: OperationFieldsProps) {
const dateParser: ComponentProps<typeof DateInput>["dateParser"] = value =>
props.validDate(props.dateInputValue(value))
? props.dateInputValue(value)
: null; // eslint-disable-line unicorn/no-null -- Mantine represents an empty controlled dateField as null.
: null;
const onDateBlur: ComponentProps<typeof DateInput>["onBlur"] = (event) => {
dateField.onChange(props.dateInputValue(event.currentTarget.value));
@@ -20,8 +20,6 @@ import { OperationForm } from "./OperationForm";
const money = (value: string) => ({ currency: "RUB", value });
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const card: BankCard
= {
archivedAt: null,
@@ -36,7 +34,6 @@ const card: BankCard
version: 1,
};
/* eslint-enable unicorn/no-null */
async function select(label: RegExp, value: RegExp | string) {
fireEvent.click(screen.getByRole("combobox", { name: label }));
fireEvent.click(await screen.findByRole("option", { name: value }));
@@ -229,7 +226,6 @@ describe("OperationForm", () => {
expect(body).not.toHaveProperty("categoryId");
});
it("finds a previous-month purchase across pages, checks remaining refund and retains the complete-refund intent", async () => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const purchase: BankOperation
= {
amount: money("500"),
@@ -249,7 +245,6 @@ describe("OperationForm", () => {
version: 1,
};
/* eslint-enable unicorn/no-null */
server.use(
http.get("*/api/v1/bank-cards/a/expenses", ({ request }) => {
return HttpResponse.json({
@@ -313,7 +308,6 @@ describe("OperationForm", () => {
});
});
it("edits a full refund with its own amount excluded from the limit and immutable purchase", async () => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const refund: BankOperation
= {
amount: money("500"),
@@ -333,7 +327,6 @@ describe("OperationForm", () => {
version: 2,
};
/* eslint-enable unicorn/no-null */
const purchase = {
...refund,
availableRefund: money("0"),
@@ -394,7 +387,6 @@ describe("OperationForm", () => {
});
it("keeps edit draft and stale version until explicit adoption after conflict", async () => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const operation: BankOperation
= {
amount: money("100"),
@@ -414,7 +406,6 @@ describe("OperationForm", () => {
version: 1,
};
/* eslint-enable unicorn/no-null */
let readVersion = 1;
const bodies: Record<string, unknown>[] = [];
server.use(
@@ -228,9 +228,7 @@ const OperationFormView: FC<OperationFormViewProps> = (props) => {
<>
<RefundFields {...props.refund} />
{props.loadingExpenses && (
<Text role="status">{t("bank.loadingHistory")}</Text>
)}
{props.loadingExpenses && <Text role="status">{t("bank.loadingHistory")}</Text>}
{props.emptyExpenses && <Text>{t("bank.noPurchases")}</Text>}
@@ -165,13 +165,13 @@ const useOperationFormVm = (props: OperationFormProps) => {
return {
categoriesError: categories.isError
? categories.error
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null,
categoriesOpen,
closeCategories: () => setCategoriesOpen(false),
error,
expensesError: expenses.isError
? expenses.error
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null,
methods,
onCategoryCreated: (id: string) => {
methods.setValue("categoryId", id);
@@ -182,7 +182,7 @@ const useOperationFormVm = (props: OperationFormProps) => {
retrySource: () => void sourceQuery.refetch(),
sourceError: sourceQuery.isError
? sourceQuery.error
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null,
view: {
categoriesOpen,
editing: Boolean(props.operation),
@@ -33,7 +33,7 @@ function renderCards(isFail = false, hasCategoryFailure = false) {
isFail ? HttpResponse.error() : HttpResponse.json([]),
),
http.get("*/api/v1/bank-cards/summary", () =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json(
{
cardId: null,
@@ -43,7 +43,6 @@ function renderCards(isFail = false, hasCategoryFailure = false) {
net: money("0"),
refunds: money("0"),
},
/* eslint-enable unicorn/no-null */
),
),
);
@@ -119,7 +118,7 @@ describe("CardsPage", () => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(),
);
expect(submitted[0]).toEqual(submitted[1]);
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
expect(submitted[0]).toMatchObject(
{
bankName: null,
@@ -127,7 +126,6 @@ describe("CardsPage", () => {
name: "Повседневная",
openingBalance: money("-92233720368547758.08"),
},
/* eslint-enable unicorn/no-null */
);
});
it("uses archive only for card list and keeps all-card summary including negative net", async () => {
@@ -137,8 +135,6 @@ describe("CardsPage", () => {
http.get("*/api/v1/bank-cards", ({ request }) => {
requests.push(new URL(request.url).search);
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
return HttpResponse.json([
{
archivedAt: "2026-09-01",
@@ -151,7 +147,7 @@ describe("CardsPage", () => {
openingDate: "2026-01-01",
version: 1,
},
/* eslint-enable unicorn/no-null */
]);
}),
);
@@ -175,8 +171,6 @@ describe("CardsPage", () => {
http.get("*/api/v1/bank-cards/summary", ({ request }) => {
requests.push(new URL(request.url).search);
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
return HttpResponse.json(
{
cardId: null,
@@ -185,7 +179,6 @@ describe("CardsPage", () => {
net: money("0"),
refunds: money("0"),
},
/* eslint-enable unicorn/no-null */
);
}),
);
@@ -125,9 +125,7 @@ const CardsPageView: FC<CardsPageViewProps> = (props) => {
</Group>
)}
{vm.cards.data && vm.cards.isFetching && (
<Text role="status">{t("bank.refreshing")}</Text>
)}
{vm.cards.data && vm.cards.isFetching && <Text role="status">{t("bank.refreshing")}</Text>}
<Group align="flex-end" justify="space-between">
<BankPeriodPicker onChange={vm.setPeriod} period={vm.period} />
@@ -29,8 +29,6 @@ const LocationProbe = () => {
const uid = "e6123145-9665-43e0-8413-cd61b8aa9b13";
const money = (value: string) => ({ currency: "RUB", value });
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const stock
= {
currency: "RUB",
@@ -44,7 +42,6 @@ const stock
ticker: "TEST",
};
/* eslint-enable unicorn/no-null */
const position = {
absoluteReturn: money("1000"),
accounts: [
@@ -124,7 +121,7 @@ function setup(
);
if (params.section === "candles")
return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{
candles: [],
@@ -132,14 +129,13 @@ function setup(
historyFrom: null,
observedAt: quote.observedAt,
},
/* eslint-enable unicorn/no-null */
),
);
if (params.section === "company")
return options.failCompany
? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 })
: HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{
description: "Описание компании",
@@ -155,12 +151,11 @@ function setup(
observedAt: quote.observedAt,
unavailable: [],
},
/* eslint-enable unicorn/no-null */
),
);
return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{
dividends: [
@@ -188,7 +183,6 @@ function setup(
positionIsStale: false,
to: "2027-09-10",
},
/* eslint-enable unicorn/no-null */
),
);
}),
@@ -200,7 +194,7 @@ function setup(
return HttpResponse.json({ code: cursorError }, { status: 400 });
return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource(
{
accountId: "all",
@@ -225,7 +219,6 @@ function setup(
nextCursor: isNext ? null : "next",
observedAt: quote.observedAt,
},
/* eslint-enable unicorn/no-null */
),
);
}),
@@ -264,7 +257,6 @@ function setup(
describe("stock page", () => {
it("preserves unknown and unambiguous currencies with compact, ratio and percent formatting", async () => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
setup(
{
metrics: [
@@ -312,7 +304,6 @@ describe("stock page", () => {
},
],
},
/* eslint-enable unicorn/no-null */
);
expect(await screen.findByText("1,23 млн валюта неизвестна")).toBeVisible();
for (const value of [
@@ -468,9 +459,8 @@ describe("stock page", () => {
it.each([
[
"INVALID_OPERATIONS_CURSOR",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null,
null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
null,
],
["TBANK_UNAVAILABLE", "next", "observe"],
])(
@@ -146,9 +146,7 @@ const StockPageView: FC<StockPageViewProps> = (props) => {
);
}
else {
companyMetricsContent = (
<Text c="dimmed">{t("stock.noFundamentals")}</Text>
);
companyMetricsContent = <Text c="dimmed">{t("stock.noFundamentals")}</Text>;
}
}
@@ -219,9 +217,7 @@ const StockPageView: FC<StockPageViewProps> = (props) => {
</Text>
)}
{stock.lot !== null && (
<Text size="sm">{t("stock.lot", { value: stock.lot })}</Text>
)}
{stock.lot !== null && <Text size="sm">{t("stock.lot", { value: stock.lot })}</Text>}
</Group>
{!!vm.company && (
+5 -5
View File
@@ -92,7 +92,7 @@ function useStockPageVm() {
const averageLine
= average && average.currency === candles.data?.currency
? Number(average.value)
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null;
const companyData = company.data;
const search = parameters.size > 0 ? `?${parameters.toString()}` : "";
@@ -101,13 +101,13 @@ function useStockPageVm() {
averageLine:
averageLine !== null && Number.isFinite(averageLine)
? averageLine
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null,
candles: candles.data,
candlesState: section(candles, sync.retry),
canonicalTo:
stock.data?.slug && stock.data.slug !== stockId
? `/stocks/${encodeURIComponent(stock.data.slug)}${search}`
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: null,
chartData,
code: apiErrorCode(stock.error),
company: companyData,
@@ -120,12 +120,12 @@ function useStockPageVm() {
logoName:
item.logoName
?? stock.data?.logoName
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
?? null,
ticker:
item.ticker
?? stock.data?.ticker
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
?? null,
})) ?? [],
filter,
@@ -162,9 +162,7 @@ const StockDividends: FC<StockEventsProps> = (props) => {
);
}
else {
pastDividendsContent = (
<Text c="dimmed">{t("stock.noDividends")}</Text>
);
pastDividendsContent = <Text c="dimmed">{t("stock.noDividends")}</Text>;
}
return (