refactor: enforce frontend readability and UI boundaries #43

Merged
ksv741 merged 2 commits from codex/frontend-view-guardrails into develop 2026-09-28 19:40:24 +03:00
139 changed files with 1549 additions and 941 deletions
Showing only changes of commit a4046dbe87 - Show all commits
+3
View File
@@ -26,6 +26,9 @@ JSX siblings и вокруг многострочных declarations/control-flo
сложные union types. Generated artifacts не форматируй вручную. сложные union types. Generated artifacts не форматируй вручную.
URL, строковые/template/regex literals и комментарии исключены из `max-len`; URL, строковые/template/regex literals и комментарии исключены из `max-len`;
это исключение для содержимого, а не повод собирать длинные expressions. это исключение для содержимого, а не повод собирать длинные expressions.
Однострочный JSX не требует обрамляющих скобок; многострочный — требует.
`unicorn/no-null` отключён: API DTO, React refs и Mantine используют `null`.
Сохраняй различие `null`/`undefined` из контракта вместо массовых suppressions.
CSS и Markdown также держи в 80 колонках там, где это не ломает данные/ссылки. CSS и Markdown также держи в 80 колонках там, где это не ломает данные/ссылки.
Review проверяет то, чего синтаксис не доказывает: Review проверяет то, чего синтаксис не доказывает:
+15 -1
View File
@@ -13,7 +13,13 @@ Mantine component и общую тему. Для выпадающего выбо
Icons импортируй из `@wealthledger/icons`; геометрия, SVG assets и animation Icons импортируй из `@wealthledger/icons`; геометрия, SVG assets и animation
lifecycle принадлежат этому пакету. Domain/category lookup возвращает параметры lifecycle принадлежат этому пакету. Domain/category lookup возвращает параметры
иконки из model сущности. SVG декоративны по умолчанию; доступное имя action иконки из model сущности. Каждый SVG хранится цельным декларативным
компонентом: не разбивай path-строки и не собирай геометрию в runtime.
Русские category names из API — данные для нормализованного сопоставления,
не ключи реестра иконок; неизвестные/переименованные категории имеют fallback.
Известные названия банков принадлежат enum `BankName` в bank-card model;
свободный ввод пользовательского названия остаётся строкой.
SVG декоративны по умолчанию; доступное имя action
принадлежит кнопке. Meaningful icon получает `aria-hidden={false}`, `role="img"` принадлежит кнопке. Meaningful icon получает `aria-hidden={false}`, `role="img"`
и доступное имя. Приложение и Storybook подключают CSS пакета. и доступное имя. Приложение и Storybook подключают CSS пакета.
@@ -68,6 +74,14 @@ JSX props получают ссылки на функции, включая `onC
адаптации UI inputs выноси в `shared/lib`, передавая явные аргументы. Domain адаптации UI inputs выноси в `shared/lib`, передавая явные аргументы. Domain
преобразование остаётся в model своего owner; shared не импортирует entities/pages. преобразование остаётся в model своего owner; shared не импортирует entities/pages.
Closure над состоянием, mutations и refs является действием VM, а не shared helper. Closure над состоянием, mutations и refs является действием VM, а не shared helper.
VM собирает связные предметные hooks, когда внутри смешиваются независимые
сценарии. Например, `useOpeningDateField` владеет draft/date parsing и событиями
даты, `usePaymentSystemField` — выбором платёжной системы. Каждый такой hook
возвращает данные и действия своей ответственности, VM объединяет их для view.
Не группируй несвязанные функции только по префиксу `on*` и не создавай
passthrough hook над одним вызовом ради формального дробления.
Render-helper замени самостоятельным presentation component у того же owner. Render-helper замени самостоятельным presentation component у того же owner.
Отдельный state/section с самостоятельной responsibility выноси в Отдельный state/section с самостоятельной responsibility выноси в
+7 -2
View File
@@ -5,11 +5,16 @@ module.exports = {
forbidden: [ forbidden: [
...base.forbidden, ...base.forbidden,
{ {
from: { path: String.raw`\.stories\.tsx$|^\.storybook/preview\.tsx$` }, from: { path: "[.]stories[.]tsx$|^[.]storybook/preview[.]tsx$" },
name: "stories-must-not-reach-application-data", name: "stories-must-not-reach-application-data",
severity: "error", severity: "error",
to: { to: {
path: String.raw`^src/(shared/api|features/auth|app/routes|app/ui)/|^src/(entities|features)/[^/]+/api/|^src/pages/[^/]+/ui/.*Page\.tsx$|node_modules/(@tanstack/react-query|openapi-fetch|openapi-react-query)/`, path: [
Outdated
Review

можно ли упростить ? без использования String.raw ?

можно ли упростить ? без использования String.raw ?
"^src/(shared/api|features/auth|app/routes|app/ui)/",
"^src/(entities|features)/[^/]+/api/",
"^src/pages/[^/]+/ui/.*Page[.]tsx$",
"node_modules/(@tanstack/react-query|openapi-fetch|openapi-react-query)/",
],
reachable: true, reachable: true,
}, },
}, },
+2 -2
View File
@@ -126,8 +126,8 @@ export default defineConfig(
"error", "error",
{ replacements: { props: false, ref: false } }, { replacements: { props: false, ref: false } },
], ],
// React, Mantine and Fetch accept null at their documented call boundaries. // API DTOs, React refs and Mantine use null as part of their contracts.
"unicorn/no-null": ["error", { checkArguments: false }], "unicorn/no-null": "off",
"unicorn/prefer-ternary": ["error", "only-single-line"], "unicorn/prefer-ternary": ["error", "only-single-line"],
}, },
settings: { settings: {
@@ -1,4 +1,5 @@
.icon { .icon {
flex-shrink: 0;
overflow: visible; overflow: visible;
& path { & path {
@@ -1,86 +1,72 @@
import type { FC } from "react"; import type { FC } from "react";
import { bindIconAnimation } from "../bind-icon-animation"; import { BasketIcon } from "./svg/BasketIcon";
import classes from "./OperationIcon.module.css"; import { CarIcon } from "./svg/CarIcon";
import { CashWithdrawalIcon } from "./svg/CashWithdrawalIcon";
import { CoffeeIcon } from "./svg/CoffeeIcon";
import { DepositIcon } from "./svg/DepositIcon";
import { DiceIcon } from "./svg/DiceIcon";
import { DownloadIcon } from "./svg/DownloadIcon";
import { EducationIcon } from "./svg/EducationIcon";
import { GridIcon } from "./svg/GridIcon";
import { GrowthIcon } from "./svg/GrowthIcon";
import { HomeIcon } from "./svg/HomeIcon";
import { MedicalIcon } from "./svg/MedicalIcon";
import { MusicIcon } from "./svg/MusicIcon";
import { RefundIcon } from "./svg/RefundIcon";
import { RepaymentIcon } from "./svg/RepaymentIcon";
import { ServerIcon } from "./svg/ServerIcon";
import { ShieldIcon } from "./svg/ShieldIcon";
import { ShoppingIcon } from "./svg/ShoppingIcon";
import { SmokingIcon } from "./svg/SmokingIcon";
import { SubscriptionsIcon } from "./svg/SubscriptionsIcon";
import { TaxiIcon } from "./svg/TaxiIcon";
import { TopupIcon } from "./svg/TopupIcon";
import { TransferIcon } from "./svg/TransferIcon";
import { TravelIcon } from "./svg/TravelIcon";
import { WifiIcon } from "./svg/WifiIcon";
import { WineIcon } from "./svg/WineIcon";
const paths = { const icons = {
basket: basket: BasketIcon,
"M3 9h18l-2 11H5L3 9Z M8 9l4-6 4 6 M9 13v3 M15 13v3 M10 6a2 2 0 1 0 4 0a2 2 0 1 0-4 0", car: CarIcon,
car: cashWithdrawal: CashWithdrawalIcon,
"M4 16H2v-4l3-1 3-4h6l4 4 4 1v4h-2 M8 16h8 M6 11h12m-6-4v4 M4 16a2 2 0 1 0 4 0a2 2 0 1 0-4 0m2-2v4m-2-2h4 M16 16a2 2 0 1 0 4 0a2 2 0 1 0-4 0m2-2v4m-2-2h4", coffee: CoffeeIcon,
cashWithdrawal: "M4 10V3h16v7 M7 7h10v14H7V7Z M7 11h10 M10 16h4", deposit: DepositIcon,
coffee: dice: DiceIcon,
"M4 8h12v7a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V8Z M16 9h2a3 3 0 0 1 0 6h-2 M8 6c-2-1 2-2 0-4 M12 6c-2-1 2-2 0-4 M3 22h15", download: DownloadIcon,
deposit: education: EducationIcon,
"M6 9a6 6 0 1 0 12 0a6 6 0 1 0-12 0 M10.5 12V6h2a1.5 1.5 0 0 1 0 3h-3m0 1.5h3 M4 16c0 4 16 4 16 0 M4 19c0 4 16 4 16 0", grid: GridIcon,
dice: growth: GrowthIcon,
"M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z M7 7h.01 M17 7h.01 M12 12h.01 M7 17h.01 M17 17h.01", home: HomeIcon,
download: "M3 3h18v13H3V3Z M8 21h8 M12 16v5 M12 6v7 M9 10l3 3 3-3", medical: MedicalIcon,
education: "M2 8l10-5 10 5-10 5L2 8Z M6 10v7c4 3 8 3 12 0v-7 M22 8v9", music: MusicIcon,
grid: "M4 4h6v6H4V4Z M14 4h6v6h-6V4Z M4 14h6v6H4v-6Z M14 14h6v6h-6v-6Z", refund: RefundIcon,
growth: "M3 3v18h18 M6 15l5-5 4 3 6-8 M16 5h5v5", repayment: RepaymentIcon,
Outdated
Review

не нужно так, svg должны быть целостными, а не разбиты по кускам, чтобы собираться в рантайме

не нужно так, svg должны быть целостными, а не разбиты по кускам, чтобы собираться в рантайме
home: "M3 11l9-8 9 8 M5 10v11h14V10 M9 21v-8h6v8", server: ServerIcon,
medical: "M9 3h6v6h6v6h-6v6H9v-6H3V9h6V3Z", shield: ShieldIcon,
music: "M9 5v14a3 3 0 1 1-3-3h3 M9 5l11-2v14a3 3 0 1 1-3-3h3 M9 9l11-2", shopping: ShoppingIcon,
refund: "M8 4L3 9l5 5 M3 9h11a7 7 0 0 1 0 14", smoking: SmokingIcon,
repayment: "M3 5h18v14H3V5Z M3 9h18 M10 18v-7h3a2 2 0 0 1 0 4H9m0 1.5h4", subscriptions: SubscriptionsIcon,
server: taxi: TaxiIcon,
"M3 3h18v7H3V3Z M3 14h18v7H3v-7Z M7 6.5h.01 M7 17.5h.01 M15 6.5h3 M15 17.5h3", topup: TopupIcon,
shield: "M12 2l8 4v6c0 5-8 10-8 10S4 17 4 12V6l8-4Z M8 12l3 3 5-6", transfer: TransferIcon,
shopping: "M5 7h14l2 14H3L5 7Z M8 8V6a4 4 0 0 1 8 0v2", travel: TravelIcon,
smoking: wifi: WifiIcon,
"M3 14h18v5H3v-5Z M16 14v5 M8 10V8c0-2 3-2 3-4V2 M15 10V8c0-2 3-2 3-4", wine: WineIcon,
subscriptions: };
"M4 10a8 8 0 0 1 13-6l3 3 M20 3v4h-4 M20 14a8 8 0 0 1-13 6l-3-3 M4 21v-4h4",
taxi:
"M4 10l2-6h12l2 6 M3 10h18v8H3v-8Z M6 18v3 M18 18v3 M6 14h2 M16 14h2 M10 4V2h4v2",
topup:
"M20 8V5H5a2 2 0 0 1 0-4h13v4 M3 3v16a2 2 0 0 0 2 2h15V8H5 M16 12v5m-2.5-2.5h5",
transfer: "M3 7h18l-4-4 M21 17H3l4 4",
travel: "M10 3h4v7l7 5v3l-7-3v5l2 1v1l-4-1-4 1v-1l2-1v-5l-7 3v-3l7-5V3Z",
wifi:
"M3 7a14 14 0 0 1 18 0 M6 11a9 9 0 0 1 12 0 M9 15a4 4 0 0 1 6 0 M12 19h.01",
wine:
"M7 3h10v5a5 5 0 0 1-10 0V3Z M7 7h10v1a5 5 0 0 1-10 0V7Z M12 13v8 M8 21h8",
} as const;
type OperationIconProps = { type OperationIconProps = {
animation?: string;
color: string; color: string;
variant: keyof typeof paths; variant: keyof typeof icons;
}; };
const OperationIcon: FC<OperationIconProps> = (props) => { const OperationIcon: FC<OperationIconProps> = (props) => {
const { animation, color, variant } = props; const { color, variant } = props;
// eslint-disable-next-line security/detect-object-injection -- Animation keys select CSS module classes only. // eslint-disable-next-line security/detect-object-injection -- Variant is a closed key of the static icon registry.
const animationClass = animation ? classes[animation] : ""; const Icon = icons[variant];
// eslint-disable-next-line security/detect-object-injection -- Variant is a closed keyof the package geometry map. return <Icon style={{ color: `var(--mantine-color-${color}-7)` }} />;
const parts = paths[variant]
.split(" M")
.map((part, index) => ({ d: index ? `M${part}` : part, key: part }));
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${animationClass}`}
fill="none"
focusable="false"
height="24"
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
style={{ color: `var(--mantine-color-${color}-7)`, flexShrink: 0 }}
viewBox="0 0 24 24"
width="24"
>
{parts.map(part => (
<path d={part.d} key={part.key} pathLength={1} />
))}
</svg>
);
}; };
export { OperationIcon, type OperationIconProps }; export { OperationIcon, type OperationIconProps };
@@ -0,0 +1,40 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const BasketIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.basket}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 9h18l-2 11H5L3 9Z" pathLength={1} />
<path d="M8 9l4-6 4 6" pathLength={1} />
<path d="M9 13v3" pathLength={1} />
<path d="M15 13v3" pathLength={1} />
<path d="M10 6a2 2 0 1 0 4 0a2 2 0 1 0-4 0" pathLength={1} />
</svg>
);
};
export { BasketIcon };
@@ -0,0 +1,40 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const CarIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.car}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M4 16H2v-4l3-1 3-4h6l4 4 4 1v4h-2" pathLength={1} />
<path d="M8 16h8" pathLength={1} />
<path d="M6 11h12m-6-4v4" pathLength={1} />
<path d="M4 16a2 2 0 1 0 4 0a2 2 0 1 0-4 0m2-2v4m-2-2h4" pathLength={1} />
<path d="M16 16a2 2 0 1 0 4 0a2 2 0 1 0-4 0m2-2v4m-2-2h4" pathLength={1} />
</svg>
);
};
export { CarIcon };
@@ -0,0 +1,38 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const CashWithdrawalIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M4 10V3h16v7" pathLength={1} />
<path d="M7 7h10v14H7V7Z" pathLength={1} />
<path d="M7 11h10" pathLength={1} />
<path d="M10 16h4" pathLength={1} />
</svg>
);
};
export { CashWithdrawalIcon };
@@ -0,0 +1,40 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const CoffeeIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.coffee}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M4 8h12v7a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V8Z" pathLength={1} />
<path d="M16 9h2a3 3 0 0 1 0 6h-2" pathLength={1} />
<path d="M8 6c-2-1 2-2 0-4" pathLength={1} />
<path d="M12 6c-2-1 2-2 0-4" pathLength={1} />
<path d="M3 22h15" pathLength={1} />
</svg>
);
};
export { CoffeeIcon };
@@ -0,0 +1,38 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const DepositIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.deposit}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M6 9a6 6 0 1 0 12 0a6 6 0 1 0-12 0" pathLength={1} />
<path d="M10.5 12V6h2a1.5 1.5 0 0 1 0 3h-3m0 1.5h3" pathLength={1} />
<path d="M4 16c0 4 16 4 16 0" pathLength={1} />
<path d="M4 19c0 4 16 4 16 0" pathLength={1} />
</svg>
);
};
export { DepositIcon };
@@ -0,0 +1,42 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const DiceIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z" pathLength={1} />
<path d="M7 7h.01" pathLength={1} />
<path d="M17 7h.01" pathLength={1} />
<path d="M12 12h.01" pathLength={1} />
<path d="M7 17h.01" pathLength={1} />
<path d="M17 17h.01" pathLength={1} />
</svg>
);
};
export { DiceIcon };
@@ -0,0 +1,40 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const DownloadIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 3h18v13H3V3Z" pathLength={1} />
<path d="M8 21h8" pathLength={1} />
<path d="M12 16v5" pathLength={1} />
<path d="M12 6v7" pathLength={1} />
<path d="M9 10l3 3 3-3" pathLength={1} />
</svg>
);
};
export { DownloadIcon };
@@ -0,0 +1,36 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const EducationIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M2 8l10-5 10 5-10 5L2 8Z" pathLength={1} />
<path d="M6 10v7c4 3 8 3 12 0v-7" pathLength={1} />
<path d="M22 8v9" pathLength={1} />
</svg>
);
};
export { EducationIcon };
@@ -0,0 +1,38 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const GridIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.grid}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M4 4h6v6H4V4Z" pathLength={1} />
<path d="M14 4h6v6h-6V4Z" pathLength={1} />
<path d="M4 14h6v6H4v-6Z" pathLength={1} />
<path d="M14 14h6v6h-6v-6Z" pathLength={1} />
</svg>
);
};
export { GridIcon };
@@ -0,0 +1,36 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const GrowthIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.growth}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 3v18h18" pathLength={1} />
<path d="M6 15l5-5 4 3 6-8" pathLength={1} />
<path d="M16 5h5v5" pathLength={1} />
</svg>
);
};
export { GrowthIcon };
@@ -0,0 +1,36 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const HomeIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 11l9-8 9 8" pathLength={1} />
<path d="M5 10v11h14V10" pathLength={1} />
<path d="M9 21v-8h6v8" pathLength={1} />
</svg>
);
};
export { HomeIcon };
@@ -0,0 +1,32 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const MedicalIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M9 3h6v6h6v6h-6v6H9v-6H3V9h6V3Z" pathLength={1} />
</svg>
);
};
export { MedicalIcon };
@@ -0,0 +1,36 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const MusicIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M9 5v14a3 3 0 1 1-3-3h3" pathLength={1} />
<path d="M9 5l11-2v14a3 3 0 1 1-3-3h3" pathLength={1} />
<path d="M9 9l11-2" pathLength={1} />
</svg>
);
};
export { MusicIcon };
@@ -0,0 +1,34 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const RefundIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M8 4L3 9l5 5" pathLength={1} />
<path d="M3 9h11a7 7 0 0 1 0 14" pathLength={1} />
</svg>
);
};
export { RefundIcon };
@@ -0,0 +1,36 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const RepaymentIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.repayment}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 5h18v14H3V5Z" pathLength={1} />
<path d="M3 9h18" pathLength={1} />
<path d="M10 18v-7h3a2 2 0 0 1 0 4H9m0 1.5h4" pathLength={1} />
</svg>
);
};
export { RepaymentIcon };
@@ -0,0 +1,42 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const ServerIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.server}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 3h18v7H3V3Z" pathLength={1} />
<path d="M3 14h18v7H3v-7Z" pathLength={1} />
<path d="M7 6.5h.01" pathLength={1} />
<path d="M7 17.5h.01" pathLength={1} />
<path d="M15 6.5h3" pathLength={1} />
<path d="M15 17.5h3" pathLength={1} />
</svg>
);
};
export { ServerIcon };
@@ -0,0 +1,34 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const ShieldIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M12 2l8 4v6c0 5-8 10-8 10S4 17 4 12V6l8-4Z" pathLength={1} />
<path d="M8 12l3 3 5-6" pathLength={1} />
</svg>
);
};
export { ShieldIcon };
@@ -0,0 +1,34 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const ShoppingIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M5 7h14l2 14H3L5 7Z" pathLength={1} />
<path d="M8 8V6a4 4 0 0 1 8 0v2" pathLength={1} />
</svg>
);
};
export { ShoppingIcon };
@@ -0,0 +1,38 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const SmokingIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 14h18v5H3v-5Z" pathLength={1} />
<path d="M16 14v5" pathLength={1} />
<path d="M8 10V8c0-2 3-2 3-4V2" pathLength={1} />
<path d="M15 10V8c0-2 3-2 3-4" pathLength={1} />
</svg>
);
};
export { SmokingIcon };
@@ -0,0 +1,38 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const SubscriptionsIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M4 10a8 8 0 0 1 13-6l3 3" pathLength={1} />
<path d="M20 3v4h-4" pathLength={1} />
<path d="M20 14a8 8 0 0 1-13 6l-3-3" pathLength={1} />
<path d="M4 21v-4h4" pathLength={1} />
</svg>
);
};
export { SubscriptionsIcon };
@@ -0,0 +1,44 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const TaxiIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M4 10l2-6h12l2 6" pathLength={1} />
<path d="M3 10h18v8H3v-8Z" pathLength={1} />
<path d="M6 18v3" pathLength={1} />
<path d="M18 18v3" pathLength={1} />
<path d="M6 14h2" pathLength={1} />
<path d="M16 14h2" pathLength={1} />
<path d="M10 4V2h4v2" pathLength={1} />
</svg>
);
};
export { TaxiIcon };
@@ -0,0 +1,36 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const TopupIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.topup}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M20 8V5H5a2 2 0 0 1 0-4h13v4" pathLength={1} />
<path d="M3 3v16a2 2 0 0 0 2 2h15V8H5" pathLength={1} />
<path d="M16 12v5m-2.5-2.5h5" pathLength={1} />
</svg>
);
};
export { TopupIcon };
@@ -0,0 +1,34 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const TransferIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.transfer}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 7h18l-4-4" pathLength={1} />
<path d="M21 17H3l4 4" pathLength={1} />
</svg>
);
};
export { TransferIcon };
@@ -0,0 +1,32 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const TravelIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M10 3h4v7l7 5v3l-7-3v5l2 1v1l-4-1-4 1v-1l2-1v-5l-7 3v-3l7-5V3Z" pathLength={1} />
</svg>
);
};
export { TravelIcon };
@@ -0,0 +1,38 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const WifiIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.wifi}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 7a14 14 0 0 1 18 0" pathLength={1} />
<path d="M6 11a9 9 0 0 1 12 0" pathLength={1} />
<path d="M9 15a4 4 0 0 1 6 0" pathLength={1} />
<path d="M12 19h.01" pathLength={1} />
</svg>
);
};
export { WifiIcon };
@@ -0,0 +1,38 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const WineIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.wine}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M7 3h10v5a5 5 0 0 1-10 0V3Z" pathLength={1} />
<path d="M7 7h10v1a5 5 0 0 1-10 0V7Z" pathLength={1} />
<path d="M12 13v8" pathLength={1} />
<path d="M8 21h8" pathLength={1} />
</svg>
);
};
export { WineIcon };
+23 -1
View File
@@ -130,7 +130,7 @@ test("ESLint enforces the frontend profile and excludes generated artifacts", as
assert.equal(story.rules["react-refresh/only-export-components"], undefined); assert.equal(story.rules["react-refresh/only-export-components"], undefined);
}); });
test("all recommended plugin rules remain enabled in their intended scopes", async () => { test("recommended plugin rules retain the explicit null-contract exception", async () => {
const eslint = new ESLint(); const eslint = new ESLint();
const source = "src/shared/api/problem-details.ts"; const source = "src/shared/api/problem-details.ts";
const component = "packages/ui/src/EntryCard/EntryCard.tsx"; const component = "packages/ui/src/EntryCard/EntryCard.tsx";
@@ -175,6 +175,12 @@ test("all recommended plugin rules remain enabled in their intended scopes", asy
for (const [rule, setting] of Object.entries(expected)) { for (const [rule, setting] of Object.entries(expected)) {
const severity = Array.isArray(setting) ? setting[0] : setting; const severity = Array.isArray(setting) ? setting[0] : setting;
if (severity === "off" || severity === 0 || !rule.includes("/")) continue; if (severity === "off" || severity === 0 || !rule.includes("/")) continue;
if (rule === "unicorn/no-null") {
assert.equal(actual.get(rule)?.[0], 0);
continue;
}
assert.ok(actual.get(rule)?.[0] > 0, `${filePath}: ${rule}`); assert.ok(actual.get(rule)?.[0] > 0, `${filePath}: ${rule}`);
} }
} }
@@ -316,3 +322,19 @@ test("JSX spacing fixes preserve text interpolation and are stable", async () =>
assert.equal(again.output, undefined); assert.equal(again.output, undefined);
assert.equal(again.errorCount, 0); assert.equal(again.errorCount, 0);
}); });
test("compact JSX and contract nulls need no local suppressions", async () => {
const eslint = new ESLint({ fix: true });
const filePath = "packages/ui/src/EntryCard/EntryCard.tsx";
const code = "import type { FC } from 'react'; type Props = { active: boolean }; const Example: FC<Props> = (props) => props.active ? <span /> : <div />;\nexport { Example };";
const [fixed] = await eslint.lintText(code, { filePath });
assert.equal(fixed.errorCount, 0);
assert.match(fixed.output ?? code, /\? <span \/> : <div \/>/);
const [nullable] = await eslint.lintText(
"export const value: string | null = null;",
{ filePath: "src/shared/api/problem-details.ts" },
);
assert.equal(nullable.errorCount, 0);
});
+4 -17
View File
@@ -29,8 +29,6 @@ const accounts = [
{ id: "b", name: "ИИС" }, { id: "b", name: "ИИС" },
]; ];
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const position: Portfolio["positions"][number] const position: Portfolio["positions"][number]
= { = {
absoluteReturn: null, absoluteReturn: null,
@@ -45,7 +43,6 @@ const position: Portfolio["positions"][number]
type: "STOCK", type: "STOCK",
}; };
/* eslint-enable unicorn/no-null */
const portfolio: Portfolio = { const portfolio: Portfolio = {
cashBalances: [], cashBalances: [],
isStale: false, isStale: false,
@@ -55,8 +52,6 @@ const portfolio: Portfolio = {
totalsByCurrency: [{ currency: "RUB", value: "1200" }], totalsByCurrency: [{ currency: "RUB", value: "1200" }],
}; };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const receipt: BrokerageOperation const receipt: BrokerageOperation
= { = {
accruedInterest: null, accruedInterest: null,
@@ -76,11 +71,8 @@ const receipt: BrokerageOperation
type: "OPERATION_TYPE_COUPON", type: "OPERATION_TYPE_COUPON",
}; };
/* eslint-enable unicorn/no-null */
const items = accounts.map(account => ({ ...receipt, account })); const items = accounts.map(account => ({ ...receipt, account }));
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const analytics: IncomeAnalytics const analytics: IncomeAnalytics
= { = {
accountId: "all", accountId: "all",
@@ -118,7 +110,6 @@ const analytics: IncomeAnalytics
yearComparison: null, yearComparison: null,
}; };
/* eslint-enable unicorn/no-null */
const CurrentLocation: FC = () => { const CurrentLocation: FC = () => {
const location = useLocation(); const location = useLocation();
@@ -162,7 +153,7 @@ beforeEach(() => {
HttpResponse.json( HttpResponse.json(
resource( resource(
accounts.map( accounts.map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
account => account =>
({ ({
...account, ...account,
@@ -170,7 +161,6 @@ beforeEach(() => {
portfolioSummary: null, portfolioSummary: null,
type: "BROKERAGE", type: "BROKERAGE",
}), }),
/* eslint-enable unicorn/no-null */
), ),
), ),
), ),
@@ -182,7 +172,7 @@ beforeEach(() => {
"*/api/v1/brokerage-accounts/:accountId/operations", "*/api/v1/brokerage-accounts/:accountId/operations",
({ params }) => ({ params }) =>
HttpResponse.json( HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: params.accountId, accountId: params.accountId,
@@ -191,7 +181,6 @@ beforeEach(() => {
nextCursor: null, nextCursor: null,
observedAt: receipt.occurredAt, observedAt: receipt.occurredAt,
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
), ),
@@ -204,7 +193,7 @@ beforeEach(() => {
accountId: "all", accountId: "all",
calculatedAt: receipt.occurredAt, calculatedAt: receipt.occurredAt,
events: accounts.map( events: accounts.map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
account => account =>
({ ({
account, account,
@@ -219,7 +208,6 @@ beforeEach(() => {
ticker: "BOND", ticker: "BOND",
type: "coupon", type: "coupon",
}), }),
/* eslint-enable unicorn/no-null */
), ),
incomeTotalsByCurrency: [], incomeTotalsByCurrency: [],
}), }),
@@ -278,7 +266,7 @@ describe("All accounts routes", () => {
requests.push(url); requests.push(url);
return HttpResponse.json( return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: params.accountId, accountId: params.accountId,
@@ -294,7 +282,6 @@ describe("All accounts routes", () => {
nextCursor: url.searchParams.has("cursor") ? null : "next", nextCursor: url.searchParams.has("cursor") ? null : "next",
observedAt: receipt.occurredAt, observedAt: receipt.occurredAt,
}, },
/* eslint-enable unicorn/no-null */
), ),
); );
}, },
+1 -2
View File
@@ -104,7 +104,7 @@ describe("AppRoutes", () => {
), ),
http.get("*/api/v1/bank-cards", () => HttpResponse.json([])), http.get("*/api/v1/bank-cards", () => HttpResponse.json([])),
http.get("*/api/v1/bank-cards/summary", () => http.get("*/api/v1/bank-cards/summary", () =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json( HttpResponse.json(
{ {
cardId: null, cardId: null,
@@ -114,7 +114,6 @@ describe("AppRoutes", () => {
net: { currency: "RUB", value: "0" }, net: { currency: "RUB", value: "0" },
refunds: { currency: "RUB", value: "0" }, refunds: { currency: "RUB", value: "0" },
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
); );
@@ -106,12 +106,8 @@ const ProtectedDocumentTitle: FC = () => {
if (stocks) return <DocumentTitle title={t("navigation.stocks")} />; if (stocks) return <DocumentTitle title={t("navigation.stocks")} />;
return settings return settings
? ( ? <DocumentTitle title={t("navigation.settings")} />
<DocumentTitle title={t("navigation.settings")} /> : <DocumentTitle title={t("navigation.accounts")} />;
)
: (
<DocumentTitle title={t("navigation.accounts")} />
);
}; };
export { ProtectedDocumentTitle }; export { ProtectedDocumentTitle };
1
@@ -8,10 +8,10 @@ import { RouteErrorBoundary } from "./RouteErrorBoundary";
const session = { isAuthenticated: false }; const session = { isAuthenticated: false };
vi.mock("@/features/auth", () => ({ vi.mock("@/features/auth", () => ({
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
useAuth: () => useAuth: () =>
({ user: session.isAuthenticated ? {} : null }), ({ user: session.isAuthenticated ? {} : null }),
/* eslint-enable unicorn/no-null */
})); }));
const ThrowingRoute: FC = () => { const ThrowingRoute: FC = () => {
@@ -25,9 +25,7 @@ const HealthyRoute: FC = () => {
? ( ? (
"healthy route" "healthy route"
) )
: ( : <ThrowingRoute />}
<ThrowingRoute />
)}
</div> </div>
); );
}; };
@@ -34,9 +34,7 @@ class Boundary extends Component<BoundaryProps, State> {
render() { render() {
return this.state.hasError return this.state.hasError
? ( ? <RouteErrorFallback authenticated={this.props.authenticated} />
<RouteErrorFallback authenticated={this.props.authenticated} />
)
: ( : (
this.props.children this.props.children
); );
+1 -3
View File
@@ -79,9 +79,7 @@ const LazyPage: FC<LazyPageProps> = (props) => {
? ( ? (
children children
) )
: ( : <RouteTransition>{children}</RouteTransition>}
<RouteTransition>{children}</RouteTransition>
)}
</Suspense> </Suspense>
); );
}; };
+1 -3
View File
@@ -13,9 +13,7 @@ const Protected: FC<PropsWithChildren> = (props) => {
if (!ready) return <RouteSkeleton variant="shell" />; if (!ready) return <RouteSkeleton variant="shell" />;
return user return user
? ( ? <>{children}</>
<>{children}</>
)
: ( : (
<Navigate <Navigate
replace replace
+1 -3
View File
@@ -88,9 +88,7 @@ const AppBreadcrumbs: FC = () => {
return <BreadcrumbTrail items={[{ label: t("breadcrumbs.accounts") }]} />; return <BreadcrumbTrail items={[{ label: t("breadcrumbs.accounts") }]} />;
return settings return settings
? ( ? <BreadcrumbTrail items={[{ label: t("breadcrumbs.settings") }]} />
<BreadcrumbTrail items={[{ label: t("breadcrumbs.settings") }]} />
)
: undefined; : undefined;
}; };
@@ -19,8 +19,6 @@ import {
useBankSummary, useBankSummary,
} from "./queries"; } from "./queries";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const card: BankCard const card: BankCard
Review

слишком часто отключаем это правило, его нужно или совсем выключить или же правильно писать код, чтобы не дизейблить правило

слишком часто отключаем это правило, его нужно или совсем выключить или же правильно писать код, чтобы не дизейблить правило
= { = {
archivedAt: null, archivedAt: null,
@@ -35,7 +33,6 @@ const card: BankCard
version: 1, version: 1,
}; };
/* eslint-enable unicorn/no-null */
const transfer: CreateBankOperation = { const transfer: CreateBankOperation = {
amount: { currency: "RUB", value: "90071992547409.93" }, amount: { currency: "RUB", value: "90071992547409.93" },
cardId: "a", cardId: "a",
+8
View File
@@ -0,0 +1,8 @@
enum BankName {
AlfaBank = "Альфа-Банк",
Sberbank = "Сбербанк",
TBank = "Т-Банк",
Vtb = "ВТБ",
}
export { BankName };
@@ -32,7 +32,6 @@ function bankToday() {
function moneyCents(value: string): bigint | null { function moneyCents(value: string): bigint | null {
const normalized = value.trim().replace(",", "."); const normalized = value.trim().replace(",", ".");
if (!/^-?\d+$|^-?\d+\.\d{1,2}$/.test(normalized)) if (!/^-?\d+$|^-?\d+\.\d{1,2}$/.test(normalized))
// eslint-disable-next-line unicorn/no-null -- The money parser’s callers distinguish invalid input with null.
return null; return null;
const [whole, fraction = ""] = normalized const [whole, fraction = ""] = normalized
@@ -2,129 +2,68 @@ import type { OperationIconProps } from "@wealthledger/icons";
import type { BankOperation } from "../api/queries"; import type { BankOperation } from "../api/queries";
type OperationIcon = { const categoryIcons: {
animation?: string; appearance: OperationIconProps;
color: string; name: string;
variant: OperationIconProps["variant"]; }[] = [
}; { appearance: { color: "grape", variant: "wine" }, name: "Алкоголь" },
{ appearance: { color: "green", variant: "deposit" }, name: "Вклады" },
{ appearance: { color: "orange", variant: "home" }, name: "Жильё" },
Review

эту логику нужно переработать, потому что svg должны быть целостными, а не собираться в рантайме
также ключи в виде русских слов это порнография

эту логику нужно переработать, потому что svg должны быть целостными, а не собираться в рантайме также ключи в виде русских слов это порнография
{ appearance: { color: "red", variant: "medical" }, name: "Здоровье" },
{
appearance: { color: "green", variant: "growth" },
name: "Инвестиции и накопления",
},
{ appearance: { color: "orange", variant: "coffee" }, name: "Кафе" },
{ appearance: { color: "blue", variant: "education" }, name: "Образование" },
{ appearance: { color: "blue", variant: "transfer" }, name: "Переводы" },
{
appearance: { color: "orange", variant: "repayment" },
name: "Погашение кредита",
},
{
appearance: { color: "grape", variant: "subscriptions" },
name: "Подписки",
},
{ appearance: { color: "grape", variant: "shopping" }, name: "Покупки" },
{ appearance: { color: "green", variant: "basket" }, name: "Продукты" },
{ appearance: { color: "blue", variant: "travel" }, name: "Путешествия" },
{ appearance: { color: "grape", variant: "music" }, name: "Развлечения" },
{ appearance: { color: "blue", variant: "wifi" }, name: "Связь и интернет" },
{
appearance: { color: "green", variant: "cashWithdrawal" },
name: "Снятие наличных",
},
{ appearance: { color: "grape", variant: "dice" }, name: "Ставки" },
{ appearance: { color: "green", variant: "shield" }, name: "Страхование" },
{ appearance: { color: "orange", variant: "smoking" }, name: "Табак" },
{ appearance: { color: "yellow", variant: "taxi" }, name: "Такси" },
{ appearance: { color: "blue", variant: "car" }, name: "Транспорт" },
{
appearance: { color: "grape", variant: "server" },
name: "Хостинг и ИИ-сервисы",
},
{
appearance: { color: "blue", variant: "download" },
name: "Цифровые покупки",
},
];
const categoryIcons: Record<string, OperationIcon> = { const otherIcon: OperationIconProps = {
"алкоголь": {
animation: "wine",
color: "grape",
variant: "wine",
},
"вклады": {
animation: "deposit",
color: "green",
variant: "deposit",
},
"жилье": { color: "orange", variant: "home" },
"здоровье": { color: "red", variant: "medical" },
"инвестиции и накопления": {
animation: "growth",
color: "green",
variant: "growth",
},
"кафе": {
animation: "coffee",
color: "orange",
variant: "coffee",
},
"образование": {
color: "blue",
variant: "education",
},
"переводы": {
animation: "transfer",
color: "blue",
variant: "transfer",
},
"погашение кредита": {
animation: "repayment",
color: "orange",
variant: "repayment",
},
"подписки": {
color: "grape",
variant: "subscriptions",
},
"покупки": {
color: "grape",
variant: "shopping",
},
"продукты": {
animation: "basket",
color: "green",
variant: "basket",
},
"путешествия": {
color: "blue",
variant: "travel",
},
"развлечения": {
color: "grape",
variant: "music",
},
"связь и интернет": {
animation: "wifi",
color: "blue",
variant: "wifi",
},
"снятие наличных": {
color: "green",
variant: "cashWithdrawal",
},
"ставки": {
color: "grape",
variant: "dice",
},
"страхование": {
color: "green",
variant: "shield",
},
"табак": {
color: "orange",
variant: "smoking",
},
"такси": {
color: "yellow",
variant: "taxi",
},
"транспорт": {
animation: "car",
color: "blue",
variant: "car",
},
"хостинг и ии-сервисы": {
animation: "server",
color: "grape",
variant: "server",
},
"цифровые покупки": {
color: "blue",
variant: "download",
},
};
const otherIcon: OperationIcon = {
animation: "grid",
color: "gray", color: "gray",
variant: "grid", variant: "grid",
}; };
const operationIcons: Record< const operationIcons: Record<
Exclude<BankOperation["kind"], "expense">, Exclude<BankOperation["kind"], "expense">,
OperationIcon OperationIconProps
> = { > = {
refund: { color: "teal", variant: "refund" }, refund: { color: "teal", variant: "refund" },
topup: { topup: {
animation: "topup",
color: "green", color: "green",
variant: "topup", variant: "topup",
}, },
transfer: { transfer: {
animation: "transfer",
color: "blue", color: "blue",
variant: "transfer", variant: "transfer",
}, },
@@ -135,17 +74,20 @@ function bankOperationAppearance(
categoryName?: null | string, categoryName?: null | string,
) { ) {
if (kind !== "expense") if (kind !== "expense")
// eslint-disable-next-line security/detect-object-injection -- Operation kinds are finite; category names are guarded by Object.hasOwn. // eslint-disable-next-line security/detect-object-injection -- Operation kinds are a finite API union.
return operationIcons[kind]; return operationIcons[kind];
const name = categoryName const name = normalizeCategoryName(categoryName ?? "");
?.trim()
.toLocaleLowerCase("ru-RU")
.replaceAll("ё", "е");
return name && Object.hasOwn(categoryIcons, name) const category = categoryIcons.find(
? categoryIcons[name] // eslint-disable-line security/detect-object-injection -- Operation kinds are finite; category names are guarded by Object.hasOwn. item => normalizeCategoryName(item.name) === name,
: otherIcon; );
return category?.appearance ?? otherIcon;
}
function normalizeCategoryName(name: string) {
return name.trim().toLocaleLowerCase("ru-RU").replaceAll("ё", "е");
} }
export { bankOperationAppearance }; export { bankOperationAppearance };
@@ -2,17 +2,16 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
import { SimpleGrid } from "@mantine/core"; import { SimpleGrid } from "@mantine/core";
import { BankName } from "../../model/bank";
import { BankCardFace } from "./BankCardFace"; import { BankCardFace } from "./BankCardFace";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const meta const meta
= { = {
args: { args: {
card: { card: {
archivedAt: null, archivedAt: null,
balance: { currency: "RUB", value: "128450.75" }, balance: { currency: "RUB", value: "128450.75" },
bankName: "Сбербанк", bankName: BankName.Sberbank,
id: "fictional", id: "fictional",
lastFour: "4829", lastFour: "4829",
name: "Для повседневных покупок", name: "Для повседневных покупок",
@@ -25,7 +24,7 @@ const meta
component: BankCardFace, component: BankCardFace,
tags: ["autodocs"], tags: ["autodocs"],
title: "Product/BankCards/CardAppearance", title: "Product/BankCards/CardAppearance",
} satisfies /* eslint-enable unicorn/no-null */ Meta<typeof BankCardFace>; } satisfies Meta<typeof BankCardFace>;
export default meta; export default meta;
@@ -35,33 +34,28 @@ export const Banks: Story = {
render: arguments_ => ( render: arguments_ => (
<SimpleGrid cols={{ base: 1, sm: 2 }} maw={920}> <SimpleGrid cols={{ base: 1, sm: 2 }} maw={920}>
{[ {[
"Сбербанк", BankName.Sberbank,
"Т-Банк", BankName.TBank,
"Альфа-Банк", BankName.AlfaBank,
"ВТБ", BankName.Vtb,
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
Review

названия банков должны быть вынесены в отдельный enum

названия банков должны быть вынесены в отдельный enum
].map((bankName, index) => ( ].map((bankName, index) => (
<BankCardFace <BankCardFace
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */ card={{
card={ ...arguments_.card,
{ bankName,
...arguments_.card, paymentSystem:
bankName,
paymentSystem:
// eslint-disable-next-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key. // eslint-disable-next-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key.
(["mir", "mastercard", "visa", "mir", null] as const)[index] (["mir", "mastercard", "visa", "mir", null] as const)[index]
?? null, ?? null,
} }}
/* eslint-enable unicorn/no-null */
}
key={bankName ?? "unknown"} key={bankName ?? "unknown"}
/> />
))} ))}
</SimpleGrid> </SimpleGrid>
), ),
}; };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const MissingDetails: Story export const MissingDetails: Story
= { = {
args: { args: {
@@ -73,7 +67,7 @@ export const MissingDetails: Story
}, },
}, },
}; };
/* eslint-enable unicorn/no-null */
export const LongArchived: Story = { export const LongArchived: Story = {
args: { args: {
card: { card: {
@@ -22,9 +22,7 @@ const BankCardFace: FC<BankCardFaceProps> = (props) => {
<div className={classes.top}> <div className={classes.top}>
<span className={classes.bank}>{card.bankName || t("bank.card")}</span> <span className={classes.bank}>{card.bankName || t("bank.card")}</span>
{card.archivedAt && ( {card.archivedAt && <span className={classes.archive}>{t("bank.archived")}</span>}
<span className={classes.archive}>{t("bank.archived")}</span>
)}
</div> </div>
<div aria-hidden="true" className={classes.hardware}> <div aria-hidden="true" className={classes.hardware}>
@@ -46,9 +44,7 @@ const BankCardFace: FC<BankCardFaceProps> = (props) => {
{card.paymentSystem && ( {card.paymentSystem && (
<span className={classes.network} data-network={card.paymentSystem}> <span className={classes.network} data-network={card.paymentSystem}>
{card.paymentSystem === "mastercard" && ( {card.paymentSystem === "mastercard" && <span aria-hidden="true" className={classes.circles} />}
<span aria-hidden="true" className={classes.circles} />
)}
{t(`bank.paymentSystems.${card.paymentSystem}`)} {t(`bank.paymentSystems.${card.paymentSystem}`)}
</span> </span>
@@ -1,11 +1,10 @@
import type { ComponentProps } from "react"; import { useFormContext } from "react-hook-form";
import { DateInput } from "@mantine/dates";
import { useController, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { bankDateInputValue } from "../../model/form-validation"; import type { BankCardValues } from "../../model/form.schema";
import { type BankCardValues, bankDateSchema } from "../../model/form.schema";
import { useOpeningDateField } from "./useOpeningDateField";
import { usePaymentSystemField } from "./usePaymentSystemField";
function useBankCardFieldsVm() { function useBankCardFieldsVm() {
const { const {
Review

возможно лучше использовать внутри дополнительные хуки, в которых будет инкапсулировано поведение по смыслу, например все on* действия вынести в отдельный хук, который в хуке useBankCardFieldsVm будет просто отдавать наружу детали и т.д

возможно так будет понятнее логика + сделать в виде guardrails

возможно лучше использовать внутри дополнительные хуки, в которых будет инкапсулировано поведение по смыслу, например все on* действия вынести в отдельный хук, который в хуке useBankCardFieldsVm будет просто отдавать наружу детали и т.д возможно так будет понятнее логика + сделать в виде guardrails
@@ -15,62 +14,10 @@ function useBankCardFieldsVm() {
} = useFormContext<BankCardValues>(); } = useFormContext<BankCardValues>();
const { t } = useTranslation(); const { t } = useTranslation();
const openingDate = useOpeningDateField(control);
const paymentSystem = usePaymentSystemField(control);
const { field: paymentSystemField } = useController({ return { ...openingDate, ...paymentSystem, errors, register, t };
control,
name: "paymentSystem",
});
const { field: openingDateField } = useController({
control,
name: "openingDate",
});
const paymentSystems = ["mir", "visa", "mastercard"].map(value => ({
label: t(`bank.paymentSystems.${value}`),
value,
}));
const onPaymentSystemChange: (value: null | string) => void = value =>
paymentSystemField.onChange(value ?? "");
const dateParser: ComponentProps<typeof DateInput>["dateParser"] = value =>
bankDateSchema.safeParse(bankDateInputValue(value)).success
? bankDateInputValue(value)
: null; // eslint-disable-line unicorn/no-null -- Mantine represents an empty controlled openingDateField as null.
const onOpeningDateBlur: ComponentProps<typeof DateInput>["onBlur"] = (
event,
) => {
openingDateField.onChange(bankDateInputValue(event.currentTarget.value));
openingDateField.onBlur();
};
const onOpeningDateChange: (value: null | string) => void = value =>
openingDateField.onChange(value ?? "");
const onOpeningDateInput: ComponentProps<typeof DateInput>["onInput"] = event =>
openingDateField.onChange(bankDateInputValue(event.currentTarget.value));
const openingDateValue = bankDateSchema.safeParse(openingDateField.value)
.success
? openingDateField.value
: undefined;
return {
dateParser,
errors,
onOpeningDateBlur,
onOpeningDateChange,
onOpeningDateInput,
onPaymentSystemChange,
openingDateField,
openingDateValue,
paymentSystemField,
paymentSystems,
register,
t,
};
} }
export { useBankCardFieldsVm }; export { useBankCardFieldsVm };
@@ -12,8 +12,6 @@ import type { BankCard } from "../../api/queries";
import { BankCardForm } from "./BankCardForm"; import { BankCardForm } from "./BankCardForm";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const card: BankCard const card: BankCard
= { = {
archivedAt: null, archivedAt: null,
@@ -28,7 +26,6 @@ const card: BankCard
version: 1, version: 1,
}; };
/* eslint-enable unicorn/no-null */
function setup(value: BankCard) { function setup(value: BankCard) {
configureAuthClient({ configureAuthClient({
accessToken: () => "test", accessToken: () => "test",
@@ -75,7 +72,7 @@ describe("BankCardForm", () => {
); );
fireEvent.click(screen.getByRole("button", { name: "Сохранить" })); fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
await waitFor(() => expect(close).toHaveBeenCalled()); await waitFor(() => expect(close).toHaveBeenCalled());
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
expect(body).toEqual( expect(body).toEqual(
{ {
bankName: null, bankName: null,
@@ -84,7 +81,6 @@ describe("BankCardForm", () => {
paymentSystem: null, paymentSystem: null,
version: 1, version: 1,
}, },
/* eslint-enable unicorn/no-null */
); );
}); });
it("loads and saves the selected payment system", async () => { it("loads and saves the selected payment system", async () => {
@@ -57,7 +57,6 @@ const BankCardFields: FC<BankCardFieldsProps> = (props) => {
placeholder={t("bank.paymentSystemUnknown")} placeholder={t("bank.paymentSystemUnknown")}
value={ value={
paymentSystemField.value paymentSystemField.value
// eslint-disable-next-line unicorn/no-null -- Mantine represents an empty controlled paymentSystemField as null.
|| null || null
} }
/> />
@@ -49,16 +49,13 @@ function useBankCardFormVm(props: BankCardFormProps) {
const metadata = { const metadata = {
bankName: bankName:
values.bankName values.bankName
// eslint-disable-next-line unicorn/no-null -- PATCH uses null to clear these optional card fields.
|| null, || null,
lastFour: lastFour:
values.lastFour values.lastFour
// eslint-disable-next-line unicorn/no-null -- PATCH uses null to clear these optional card fields.
|| null, || null,
name: values.name, name: values.name,
paymentSystem: paymentSystem:
values.paymentSystem values.paymentSystem
// eslint-disable-next-line unicorn/no-null -- PATCH uses null to clear these optional card fields.
|| null, || null,
}; };
@@ -0,0 +1,49 @@
import type { ComponentProps } from "react";
import type { Control } from "react-hook-form";
import { DateInput } from "@mantine/dates";
import { useController } from "react-hook-form";
import { bankDateInputValue } from "../../model/form-validation";
import { type BankCardValues, bankDateSchema } from "../../model/form.schema";
function useOpeningDateField(control: Control<BankCardValues>) {
const { field: openingDateField } = useController({
control,
name: "openingDate",
});
const dateParser: ComponentProps<typeof DateInput>["dateParser"] = value =>
bankDateSchema.safeParse(bankDateInputValue(value)).success
? bankDateInputValue(value)
: null;
const onOpeningDateBlur: ComponentProps<typeof DateInput>["onBlur"] = (
event,
) => {
openingDateField.onChange(bankDateInputValue(event.currentTarget.value));
openingDateField.onBlur();
};
const onOpeningDateChange = (value: null | string) =>
openingDateField.onChange(value ?? "");
const onOpeningDateInput: ComponentProps<typeof DateInput>["onInput"] = event =>
openingDateField.onChange(bankDateInputValue(event.currentTarget.value));
const openingDateValue = bankDateSchema.safeParse(openingDateField.value)
.success
? openingDateField.value
: undefined;
return {
dateParser,
onOpeningDateBlur,
onOpeningDateChange,
onOpeningDateInput,
openingDateField,
openingDateValue,
};
}
export { useOpeningDateField };
@@ -0,0 +1,27 @@
import type { Control } from "react-hook-form";
import { useController } from "react-hook-form";
import { useTranslation } from "react-i18next";
import type { BankCardValues } from "../../model/form.schema";
function usePaymentSystemField(control: Control<BankCardValues>) {
const { t } = useTranslation();
const { field: paymentSystemField } = useController({
control,
name: "paymentSystem",
});
const paymentSystems = ["mir", "visa", "mastercard"].map(value => ({
label: t(`bank.paymentSystems.${value}`),
value,
}));
const onPaymentSystemChange = (value: null | string) =>
paymentSystemField.onChange(value ?? "");
return { onPaymentSystemChange, paymentSystemField, paymentSystems };
}
export { usePaymentSystemField };
@@ -10,7 +10,9 @@ const BankOperationIcon = (props: {
categoryName?: null | string; categoryName?: null | string;
kind: "expense" | "refund" | "topup" | "transfer"; kind: "expense" | "refund" | "topup" | "transfer";
}) => ( }) => (
<OperationIcon {...bankOperationAppearance(props.kind, props.categoryName)} /> <OperationIcon
{...bankOperationAppearance(props.kind, props.categoryName)}
/>
); );
const meta = { const meta = {
@@ -8,7 +8,9 @@ const BankOperationIcon = (props: {
categoryName?: null | string; categoryName?: null | string;
kind: "expense" | "refund" | "topup" | "transfer"; kind: "expense" | "refund" | "topup" | "transfer";
}) => ( }) => (
<OperationIcon {...bankOperationAppearance(props.kind, props.categoryName)} /> <OperationIcon
{...bankOperationAppearance(props.kind, props.categoryName)}
/>
); );
it("matches normalized category names, falls back after renaming and distinguishes operation kinds", () => { it("matches normalized category names, falls back after renaming and distinguishes operation kinds", () => {
@@ -28,7 +30,6 @@ it("matches normalized category names, falls back after renaming and distinguish
for (const categoryName of [ for (const categoryName of [
"Моя категория", "Моя категория",
"constructor", "constructor",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
]) { ]) {
rerender(<BankOperationIcon categoryName={categoryName} kind="expense" />); rerender(<BankOperationIcon categoryName={categoryName} kind="expense" />);
@@ -4,8 +4,6 @@ import { BankSummaryView } from "./BankSummary.view";
const money = (value: string) => ({ currency: "RUB", value }); const money = (value: string) => ({ currency: "RUB", value });
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const meta const meta
= { = {
args: { args: {
@@ -62,14 +60,14 @@ const meta
}, },
tags: ["autodocs"], tags: ["autodocs"],
title: "Product/BankCards/Summary", title: "Product/BankCards/Summary",
} satisfies /* eslint-enable unicorn/no-null */ Meta<typeof BankSummaryView>; } satisfies Meta<typeof BankSummaryView>;
export default meta; export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
export const Loaded: Story = { name: "Расходы по категориям" }; export const Loaded: Story = { name: "Расходы по категориям" };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const RefundMonth: Story export const RefundMonth: Story
= { = {
args: { args: {
@@ -93,8 +91,7 @@ export const RefundMonth: Story
}, },
name: "Возврат покупки прошлого месяца", name: "Возврат покупки прошлого месяца",
}; };
/* eslint-enable unicorn/no-null */
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const Empty: Story export const Empty: Story
= { = {
args: { args: {
@@ -109,7 +106,7 @@ export const Empty: Story
}, },
name: "Известные нули", name: "Известные нули",
}; };
/* eslint-enable unicorn/no-null */
export const Loading: Story = { export const Loading: Story = {
args: { data: undefined, loading: true }, args: { data: undefined, loading: true },
name: "Начальная загрузка", name: "Начальная загрузка",
@@ -122,7 +119,7 @@ export const CachedError: Story = {
args: { error: true }, args: { error: true },
name: "Сохранённые суммы при ошибке обновления", name: "Сохранённые суммы при ошибке обновления",
}; };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const LongAmounts: Story export const LongAmounts: Story
= { = {
args: { args: {
@@ -145,4 +142,3 @@ export const LongAmounts: Story
}, },
name: "Точный итог длиннее предела отдельного ввода", name: "Точный итог длиннее предела отдельного ввода",
}; };
/* eslint-enable unicorn/no-null */
@@ -109,9 +109,7 @@ const BankSummaryView: FC<BankSummaryViewProps> = (props) => {
</Group> </Group>
)} )}
{props.data && props.fetching && ( {props.data && props.fetching && <Text role="status">{t("bank.refreshing")}</Text>}
<Text role="status">{t("bank.refreshing")}</Text>
)}
</Stack> </Stack>
); );
}; };
@@ -88,9 +88,7 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
<Text>{category.name}</Text> <Text>{category.name}</Text>
{category.isDefault && ( {category.isDefault && <Badge>{t("bank.defaultCategory")}</Badge>}
<Badge>{t("bank.defaultCategory")}</Badge>
)}
</Group> </Group>
<Button onClick={category.onRename} variant="subtle"> <Button onClick={category.onRename} variant="subtle">
@@ -100,9 +98,7 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
); );
})} })}
{categories.isLoading && ( {categories.isLoading && <Text role="status">{t("bank.loading")}</Text>}
<Text role="status">{t("bank.loading")}</Text>
)}
{categories.isError && ( {categories.isError && (
<BankFeedback <BankFeedback
@@ -34,7 +34,7 @@ const LedgerEntryRow: FC<LedgerEntryRowProps> = (props) => {
t("ledger.occurredAt", { date: formatSnapshotTime(operation.occurredAt) }), t("ledger.occurredAt", { date: formatSnapshotTime(operation.occurredAt) }),
instrument, instrument,
props.withDetails props.withDetails
? null // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. ? null
: operation.account?.name, : operation.account?.name,
] ]
.filter(Boolean) .filter(Boolean)
@@ -45,7 +45,6 @@ function brokerageAccountCards(
return { return {
account, account,
positionCounts: positionCounts:
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
null, null,
source, source,
summary: { state: "unavailable" }, summary: { state: "unavailable" },
@@ -64,8 +64,6 @@ export const Available: Story = {
}, },
}; };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const Unavailable: Story export const Unavailable: Story
= { = {
args: { args: {
@@ -83,7 +81,6 @@ export const Unavailable: Story
}, },
}, },
}; };
/* eslint-enable unicorn/no-null */
export const Empty: Story = { export const Empty: Story = {
args: { args: {
@@ -193,8 +190,6 @@ export const LargeValues: Story = {
name: "Длинные суммы", name: "Длинные суммы",
}; };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const IndependentFreshness: Story export const IndependentFreshness: Story
= { = {
args: { args: {
@@ -236,4 +231,3 @@ export const IndependentFreshness: Story
}, },
name: "Независимая свежесть и ошибка сводок", name: "Независимая свежесть и ошибка сводок",
}; };
/* eslint-enable unicorn/no-null */
@@ -29,7 +29,7 @@ describe("BrokerageAccountList", () => {
], ],
}, },
type: "BROKERAGE", type: "BROKERAGE",
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
}, },
{ {
id: "account-2", id: "account-2",
@@ -38,10 +38,10 @@ describe("BrokerageAccountList", () => {
portfolioSummary: null, portfolioSummary: null,
type: "BROKERAGE", type: "BROKERAGE",
}, },
/* eslint-enable unicorn/no-null */ { {
id: "account-3", id: "account-3",
name: "Пустой", name: "Пустой",
openedOn: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture. openedOn: null,
portfolioSummary: { portfolioSummary: {
bondPositionCount: 0, bondPositionCount: 0,
stockPositionCount: 2, stockPositionCount: 2,
@@ -84,7 +84,7 @@ describe("BrokerageAccountList", () => {
it("shows portfolio summary freshness independently for each account", () => { it("shows portfolio summary freshness independently for each account", () => {
const accounts = brokerageAccountCards( const accounts = brokerageAccountCards(
["fresh", "stale"].map( ["fresh", "stale"].map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
id => id =>
({ ({
id, id,
@@ -97,7 +97,6 @@ it("shows portfolio summary freshness independently for each account", () => {
}, },
type: "BROKERAGE" as const, type: "BROKERAGE" as const,
}), }),
/* eslint-enable unicorn/no-null */
), ),
[ [
resource(null, { freshness: "fresh", resourceKey: "portfolio/fresh" }) resource(null, { freshness: "fresh", resourceKey: "portfolio/fresh" })
@@ -105,13 +105,9 @@ const BrokerageAccountList: FC<BrokerageAccountListProps> = (props) => {
</Text> </Text>
)} )}
{source?.sync.state === "failed" && ( {source?.sync.state === "failed" && <Text size="xs">{t("accounts.refreshError")}</Text>}
<Text size="xs">{t("accounts.refreshError")}</Text>
)}
{source?.freshness === "stale" && ( {source?.freshness === "stale" && <Text size="xs">{t("investment.stale")}</Text>}
<Text size="xs">{t("investment.stale")}</Text>
)}
</Stack> </Stack>
{!!positionCounts && ( {!!positionCounts && (
@@ -37,7 +37,7 @@ describe("useBrokerageEvents", () => {
server.use( server.use(
http.get("*/api/v1/brokerage-accounts/account-1/events", () => http.get("*/api/v1/brokerage-accounts/account-1/events", () =>
HttpResponse.json( HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: "account-1", accountId: "account-1",
@@ -56,7 +56,6 @@ describe("useBrokerageEvents", () => {
}, },
], ],
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
), ),
@@ -67,7 +66,7 @@ describe("useBrokerageEvents", () => {
}); });
await waitFor(() => expect(result.current.isSuccess).toBe(true)); await waitFor(() => expect(result.current.isSuccess).toBe(true));
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
expect(result.current.data?.events).toEqual([ expect(result.current.data?.events).toEqual([
{ {
date: "2026-09-01", date: "2026-09-01",
@@ -80,7 +79,7 @@ describe("useBrokerageEvents", () => {
ticker: "SBER", ticker: "SBER",
type: "dividend", type: "dividend",
}, },
/* eslint-enable unicorn/no-null */
]); ]);
}); });
}); });
@@ -19,7 +19,7 @@ const BrokerageEventCard: FC<BrokerageEventCardProps> = (props) => {
date, date,
event.dateKind === "registry" event.dateKind === "registry"
? t("events.dateKind.registry") ? t("events.dateKind.registry")
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. : null,
event.isin, event.isin,
event.account?.name, event.account?.name,
] ]
@@ -6,8 +6,6 @@ import type { BrokerageEvent } from "../../api/useBrokerageEvents";
import { BrokerageEventsPreview } from "./BrokerageEventsPreview"; import { BrokerageEventsPreview } from "./BrokerageEventsPreview";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const syntheticEvent const syntheticEvent
= { = {
date: "2026-10-15", date: "2026-10-15",
@@ -20,7 +18,7 @@ const syntheticEvent
projectedAmount: { currency: "RUB", value: "850.50" }, projectedAmount: { currency: "RUB", value: "850.50" },
ticker: "DEMO", ticker: "DEMO",
type: "coupon", type: "coupon",
} satisfies /* eslint-enable unicorn/no-null */ BrokerageEvent; } satisfies BrokerageEvent;
const eventsPath = "/accounts/storybook-account/events"; const eventsPath = "/accounts/storybook-account/events";
@@ -121,8 +119,6 @@ export const Empty: Story = {
}, },
}; };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const MissingAmount: Story export const MissingAmount: Story
= { = {
args: { args: {
@@ -142,9 +138,6 @@ export const MissingAmount: Story
}, },
}, },
}; };
/* eslint-enable unicorn/no-null */
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const LongEvent: Story export const LongEvent: Story
= { = {
@@ -165,4 +158,3 @@ export const LongEvent: Story
}, },
name: "Длинное название на узком экране", name: "Длинное название на узком экране",
}; };
/* eslint-enable unicorn/no-null */
@@ -46,31 +46,21 @@ const BrokerageEventsPreview: FC<BrokerageEventsPreviewProps> = (props) => {
{!!events.stale && <Text size="xs">{t("investment.stale")}</Text>} {!!events.stale && <Text size="xs">{t("investment.stale")}</Text>}
{!!events.refreshError && ( {!!events.refreshError && <Text role="status">{t("accounts.refreshError")}</Text>}
<Text role="status">{t("accounts.refreshError")}</Text>
)}
</> </>
)} )}
{events.state === "loading" && ( {events.state === "loading" && (
<LoadingSkeleton label={t("events.recentLoading")}> <LoadingSkeleton label={t("events.recentLoading")}>
{["first", "second", "third"].map(placeholder => ( {["first", "second", "third"].map(placeholder => <Skeleton height={48} key={placeholder} radius="sm" />)}
<Skeleton height={48} key={placeholder} radius="sm" />
))}
</LoadingSkeleton> </LoadingSkeleton>
)} )}
{events.state === "error" && ( {events.state === "error" && <Alert color="red">{t("events.recentError")}</Alert>}
<Alert color="red">{t("events.recentError")}</Alert>
)}
{events.state === "empty" && ( {events.state === "empty" && <Text c="dimmed">{t("events.recentEmpty")}</Text>}
<Text c="dimmed">{t("events.recentEmpty")}</Text>
)}
{events.state === "partial" && ( {events.state === "partial" && <Text role="status">{t("investment.partial")}</Text>}
<Text role="status">{t("investment.partial")}</Text>
)}
{(events.state === "loaded" || events.state === "partial") {(events.state === "loaded" || events.state === "partial")
&& events.items.map(event => ( && events.items.map(event => (
@@ -56,7 +56,7 @@ describe("persistent resource query lifecycle", () => {
await waitFor(() => expect(result.current.isSuccess).toBe(true)); await waitFor(() => expect(result.current.isSuccess).toBe(true));
await act(() => result.current.observe("new")); await act(() => result.current.observe("new"));
await waitFor(() => expect(result.current.meta?.stateVersion).toBe("new")); await waitFor(() => expect(result.current.meta?.stateVersion).toBe("new"));
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
expect(modes).toEqual([null, "observe", "observe"]); expect(modes).toEqual([null, "observe", "observe"]);
expect(queryClient.getQueryCache().getAll()).toHaveLength(1); expect(queryClient.getQueryCache().getAll()).toHaveLength(1);
expect( expect(
@@ -89,14 +89,13 @@ describe("persistent resource query lifecycle", () => {
expect(result.current.status).toBe("pending"); expect(result.current.status).toBe("pending");
expect(result.current.isPending).toBe(true); expect(result.current.isPending).toBe(true);
expect(result.current.isLoading).toBe(false); expect(result.current.isLoading).toBe(false);
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
expect(result.current.transport).toEqual( expect(result.current.transport).toEqual(
{ {
error: null, error: null,
fetchStatus: "idle", fetchStatus: "idle",
status: "success", status: "success",
}, },
/* eslint-enable unicorn/no-null */
); );
expect(result.current.meta?.availability).toBe("missing"); expect(result.current.meta?.availability).toBe("missing");
}); });
-3
View File
@@ -9,15 +9,12 @@ function formatShare(
denominator: Money | null, denominator: Money | null,
): null | string { ): null | string {
if (!numerator || !denominator) if (!numerator || !denominator)
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
return null; return null;
const total = Number(denominator.value); const total = Number(denominator.value);
if (!Number.isFinite(total) || total <= 0) if (!Number.isFinite(total) || total <= 0)
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
return null; return null;
const percent = (Number(numerator.value) / total) * PERCENT_SCALE; const percent = (Number(numerator.value) / total) * PERCENT_SCALE;
if (!Number.isFinite(percent)) if (!Number.isFinite(percent))
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
return null; return null;
const rounded = new Intl.NumberFormat("ru-RU", { const rounded = new Intl.NumberFormat("ru-RU", {
+3 -3
View File
@@ -30,7 +30,7 @@ function createAuthWrapper(
describe("AuthProvider", () => { describe("AuthProvider", () => {
it("authorizes requests after restoring the initial session", async () => { it("authorizes requests after restoring the initial session", async () => {
let authorization: null | string let authorization: null | string
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
= null; = null;
server.use( server.use(
@@ -234,12 +234,12 @@ describe("AuthProvider", () => {
}); });
expect( expect(
result.current.user?.id result.current.user?.id
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
?? null, ?? null,
).toBe( ).toBe(
loginAfterLogout loginAfterLogout
? "b" ? "b"
: null, // eslint-disable-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract. : null,
); );
expect(order).toEqual([ expect(order).toEqual([
"refresh-start", "refresh-start",
@@ -30,9 +30,7 @@ const InstrumentPosition: FC<InstrumentPositionProps> = (props) => {
<InstrumentSection state={vm.positionState} title={t("stock.position")}> <InstrumentSection state={vm.positionState} title={t("stock.position")}>
{!!vm.position && ( {!!vm.position && (
<Stack gap="md"> <Stack gap="md">
{!!vm.position.isStale && ( {!!vm.position.isStale && <Alert color="orange">{t("portfolio.staleWarning")}</Alert>}
<Alert color="orange">{t("portfolio.staleWarning")}</Alert>
)}
<div className={`${classes.metrics} ${classes.holding}`}> <div className={`${classes.metrics} ${classes.holding}`}>
<div> <div>
@@ -103,9 +101,7 @@ const InstrumentPosition: FC<InstrumentPositionProps> = (props) => {
{account.account.name} {account.account.name}
</Anchor> </Anchor>
) )
: ( : <Text>{t("stock.portfolio")}</Text>}
<Text>{t("stock.portfolio")}</Text>
)}
<div> <div>
<Text c="dimmed" size="xs"> <Text c="dimmed" size="xs">
@@ -82,9 +82,7 @@ const InstrumentSection: FC<InstrumentSectionProps> = (props) => {
{!!props.state.stale && <Text size="xs">{t("investment.stale")}</Text>} {!!props.state.stale && <Text size="xs">{t("investment.stale")}</Text>}
{!!props.state.partial && ( {!!props.state.partial && <Text role="status">{t("investment.partial")}</Text>}
<Text role="status">{t("investment.partial")}</Text>
)}
{!!props.state.lastSuccessfulAt && ( {!!props.state.lastSuccessfulAt && (
<Text c="dimmed" size="xs"> <Text c="dimmed" size="xs">
@@ -135,12 +135,11 @@ describe("page-owned screen synchronization", () => {
expect(modes).toEqual( expect(modes).toEqual(
cached cached
? [ ? [
null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture. null,
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
"observe", "observe",
] ]
: [null, "observe"], // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture. : [null, "observe"],
); );
expect(posts).toBe(0); expect(posts).toBe(0);
expect(result.current.disabled.data).toBeUndefined(); expect(result.current.disabled.data).toBeUndefined();
@@ -316,7 +315,7 @@ describe("page-owned screen synchronization", () => {
await waitFor(() => expect(result.current.accounts.isSuccess).toBe(true)); await waitFor(() => expect(result.current.accounts.isSuccess).toBe(true));
expect(result.current.accounts.data).toEqual([]); expect(result.current.accounts.data).toEqual([]);
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
expect(modes).toEqual([null, "observe"]); expect(modes).toEqual([null, "observe"]);
expect( expect(
queryClient queryClient
@@ -357,8 +356,6 @@ describe("page-owned screen synchronization", () => {
http.get("*/api/v1/tbank-sync/:id", () => { http.get("*/api/v1/tbank-sync/:id", () => {
polls++; polls++;
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
return HttpResponse.json( return HttpResponse.json(
{ {
errorCode: null, errorCode: null,
@@ -369,7 +366,6 @@ describe("page-owned screen synchronization", () => {
scope: { screen: "accounts" }, scope: { screen: "accounts" },
state: "succeeded", state: "succeeded",
}, },
/* eslint-enable unicorn/no-null */
); );
}), }),
); );
@@ -397,14 +393,14 @@ describe("page-owned screen synchronization", () => {
expect(result.current.accounts.meta?.stateVersion).toBe("2"), expect(result.current.accounts.meta?.stateVersion).toBe("2"),
); );
expect(bodies).toEqual([{ screen: "accounts" }]); expect(bodies).toEqual([{ screen: "accounts" }]);
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
expect(modes).toEqual([null, "observe"]); expect(modes).toEqual([null, "observe"]);
vi.useFakeTimers(); vi.useFakeTimers();
try { try {
await act(() => vi.advanceTimersByTimeAsync(120_000)); await act(() => vi.advanceTimersByTimeAsync(120_000));
expect(polls).toBe(1); expect(polls).toBe(1);
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
expect(modes).toEqual([null, "observe"]); expect(modes).toEqual([null, "observe"]);
unmount(); unmount();
await vi.advanceTimersByTimeAsync(120_000); await vi.advanceTimersByTimeAsync(120_000);
@@ -429,10 +425,10 @@ describe("page-owned screen synchronization", () => {
resource( resource(
isCompleted isCompleted
? [] ? []
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable API fixture. : null,
{ {
stateVersion: isCompleted ? "covered" : "initial", stateVersion: isCompleted ? "covered" : "initial",
sync: { errorCode: null, id: "history", state: "running" }, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture. sync: { errorCode: null, id: "history", state: "running" },
}, },
); );
@@ -445,8 +441,6 @@ describe("page-owned screen synchronization", () => {
http.get("*/api/v1/tbank-sync/:id", () => { http.get("*/api/v1/tbank-sync/:id", () => {
polls++; polls++;
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
return HttpResponse.json( return HttpResponse.json(
{ {
errorCode: null, errorCode: null,
@@ -461,7 +455,6 @@ describe("page-owned screen synchronization", () => {
scope: null, scope: null,
state: "running", state: "running",
}, },
/* eslint-enable unicorn/no-null */
); );
}), }),
); );
@@ -493,7 +486,7 @@ describe("page-owned screen synchronization", () => {
await act(async () => { await act(async () => {
await new Promise(resolve => setTimeout(resolve, 1100)); await new Promise(resolve => setTimeout(resolve, 1100));
}); });
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
expect(modes).toEqual([null, "observe"]); expect(modes).toEqual([null, "observe"]);
if (boundary === "leave") unmount(); if (boundary === "leave") unmount();
else act(() => configureAuthClient()); else act(() => configureAuthClient());
@@ -515,12 +508,11 @@ describe("page-owned screen synchronization", () => {
http.get("*/api/v1/brokerage-accounts", () => http.get("*/api/v1/brokerage-accounts", () =>
HttpResponse.json( HttpResponse.json(
resource( resource(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
[], [],
{ {
sync: { errorCode: null, id: "revoked", state: "running" }, sync: { errorCode: null, id: "revoked", state: "running" },
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
), ),
@@ -550,7 +542,7 @@ describe("page-owned screen synchronization", () => {
const modes: (null | string)[] = []; const modes: (null | string)[] = [];
const dependency = resource( const dependency = resource(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
[], [],
{ {
resourceKey: "operations", resourceKey: "operations",
@@ -561,7 +553,6 @@ describe("page-owned screen synchronization", () => {
state: isObserved ? "idle" : "running", state: isObserved ? "idle" : "running",
}, },
}, },
/* eslint-enable unicorn/no-null */
).meta; ).meta;
server.use( server.use(
@@ -574,7 +565,7 @@ describe("page-owned screen synchronization", () => {
resource( resource(
isObserved isObserved
? [] ? []
: null, // eslint-disable-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract. : null,
{ {
dependencies: [ dependencies: [
{ {
@@ -588,7 +579,7 @@ describe("page-owned screen synchronization", () => {
); );
}), }),
http.get("*/api/v1/tbank-sync/:id", () => http.get("*/api/v1/tbank-sync/:id", () =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json( HttpResponse.json(
{ {
errorCode: null, errorCode: null,
@@ -599,7 +590,6 @@ describe("page-owned screen synchronization", () => {
scope: null, scope: null,
state: "succeeded", state: "succeeded",
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
); );
@@ -615,7 +605,7 @@ describe("page-owned screen synchronization", () => {
); );
await waitFor(() => expect(result.current.data).toEqual([])); await waitFor(() => expect(result.current.data).toEqual([]));
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
expect(modes).toEqual([null, "observe"]); expect(modes).toEqual([null, "observe"]);
}); });
@@ -206,7 +206,7 @@ function useScreenSync(scope: InvestmentScreen, resources: ScreenResource[]) {
finally { finally {
if (requestInFlightRef.current === scopeKey) if (requestInFlightRef.current === scopeKey)
requestInFlightRef.current requestInFlightRef.current
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
= null; = null;
} }
}; };
@@ -48,8 +48,6 @@ describe("sync connection and session boundaries", () => {
started += 1; started += 1;
await blocked; await blocked;
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
return HttpResponse.json( return HttpResponse.json(
{ {
...accepted, ...accepted,
@@ -59,7 +57,6 @@ describe("sync connection and session boundaries", () => {
resources: [], resources: [],
state: "succeeded", state: "succeeded",
}, },
/* eslint-enable unicorn/no-null */
); );
}), }),
); );
@@ -103,13 +100,12 @@ describe("sync connection and session boundaries", () => {
if (boundary === "connection") if (boundary === "connection")
commitTbankConnectionBoundary( commitTbankConnectionBoundary(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
queryClient, queryClient,
{ {
connected: false, connected: false,
connectedAt: null, connectedAt: null,
}, },
/* eslint-enable unicorn/no-null */
); );
else { else {
generation += 1; generation += 1;
@@ -77,7 +77,7 @@ function renderSection(client: QueryClient) {
function seededClient() { function seededClient() {
const client = createTestQueryClient(); const client = createTestQueryClient();
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
client.setQueryData(brokerageAccountsQueryKey, [ client.setQueryData(brokerageAccountsQueryKey, [
{ {
id: "account-1", id: "account-1",
@@ -85,7 +85,7 @@ function seededClient() {
openedOn: null, openedOn: null,
type: "BROKERAGE", type: "BROKERAGE",
}, },
/* eslint-enable unicorn/no-null */
]); ]);
client.setQueryData(portfolioQueryKey, { source: "previous-connection" }); client.setQueryData(portfolioQueryKey, { source: "previous-connection" });
client.setQueryData(stockQueryKey, { source: "previous-connection" }); client.setQueryData(stockQueryKey, { source: "previous-connection" });
@@ -157,10 +157,9 @@ describe("TbankConnectionSection", () => {
return reads === 1 return reads === 1
? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 }) ? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 })
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
: HttpResponse.json( : HttpResponse.json(
{ connected: false, connectedAt: null }, { connected: false, connectedAt: null },
/* eslint-enable unicorn/no-null */
); );
}), }),
); );
@@ -12,12 +12,8 @@ const TbankConnectionSection: FC = () => {
if (viewModel.state === "loading") return <TbankConnectionLoading />; if (viewModel.state === "loading") return <TbankConnectionLoading />;
return viewModel.state === "error" return viewModel.state === "error"
? ( ? <TbankConnectionError retry={viewModel.retry} />
<TbankConnectionError retry={viewModel.retry} /> : <TbankConnectionSectionLoaded {...viewModel} />;
)
: (
<TbankConnectionSectionLoaded {...viewModel} />
);
}; };
export { TbankConnectionSection }; export { TbankConnectionSection };
@@ -129,9 +129,7 @@ const TbankConnectionSectionLoaded: FC<TbankConnectionSectionLoadedProps> = (
<Stack> <Stack>
<Text>{t("tbank.deleteText")}</Text> <Text>{t("tbank.deleteText")}</Text>
{remove.isError && ( {remove.isError && <Alert color="red">{t("tbank.requestError")}</Alert>}
<Alert color="red">{t("tbank.requestError")}</Alert>
)}
<Button <Button
color="red" color="red"
@@ -59,7 +59,6 @@ function useTbankConnectionSectionLoadedVm(
commitTbankConnectionBoundary(queryClient, { commitTbankConnectionBoundary(queryClient, {
connected: false, connected: false,
connectedAt: connectedAt:
// eslint-disable-next-line unicorn/no-null -- The disconnected API snapshot requires a null connection timestamp.
null, null,
}); });
setIsDeleteOpened(false); setIsDeleteOpened(false);
@@ -100,7 +100,6 @@ describe("Upcoming event date filters", () => {
return HttpResponse.json( return HttpResponse.json(
resource( resource(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
null, null,
{ {
sync: { sync: {
@@ -109,7 +108,6 @@ describe("Upcoming event date filters", () => {
state: "failed", state: "failed",
}, },
}, },
/* eslint-enable unicorn/no-null */
), ),
); );
}), }),
@@ -37,7 +37,7 @@ function renderPage() {
describe("AccountEventsPage", () => { describe("AccountEventsPage", () => {
beforeEach(() => { beforeEach(() => {
refresh.mockClear(); refresh.mockClear();
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
query.mockReturnValue( query.mockReturnValue(
{ {
data: { data: {
@@ -52,7 +52,7 @@ describe("AccountEventsPage", () => {
sync: { errorCode: null, id: null, state: "idle" }, sync: { errorCode: null, id: null, state: "idle" },
}, },
refetch: vi.fn(), 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", () => { 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( query.mockReturnValue(
{ {
data: undefined, data: undefined,
@@ -84,7 +83,7 @@ describe("AccountEventsPage", () => {
sync: { errorCode: null, id: null, state: "idle" }, sync: { errorCode: null, id: null, state: "idle" },
}, },
refetch: vi.fn(), refetch: vi.fn(),
} as /* eslint-enable unicorn/no-null */ never, } as never,
); );
renderPage(); renderPage();
expect(query).toHaveBeenCalledWith("", {}); expect(query).toHaveBeenCalledWith("", {});
@@ -95,7 +94,6 @@ describe("AccountEventsPage", () => {
}); });
it("renders events and marks an offer without an amount", () => { 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( query.mockReturnValue(
{ {
data: { data: {
@@ -135,7 +133,7 @@ describe("AccountEventsPage", () => {
sync: { errorCode: null, id: null, state: "idle" }, sync: { errorCode: null, id: null, state: "idle" },
}, },
refetch: vi.fn(), refetch: vi.fn(),
} as /* eslint-enable unicorn/no-null */ never, } as never,
); );
renderPage(); renderPage();
expect(screen.getByText("1 250 ₽")).toBeInTheDocument(); expect(screen.getByText("1 250 ₽")).toBeInTheDocument();
@@ -183,7 +181,6 @@ describe("AccountEventsPage", () => {
}); });
it("shows a structural busy state before events are available", () => { 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( query.mockReturnValue(
{ {
data: undefined, data: undefined,
@@ -194,7 +191,7 @@ describe("AccountEventsPage", () => {
sync: { errorCode: null, id: null, state: "idle" }, sync: { errorCode: null, id: null, state: "idle" },
}, },
refetch: vi.fn(), refetch: vi.fn(),
} as /* eslint-enable unicorn/no-null */ never, } as never,
); );
renderPage(); renderPage();
@@ -12,12 +12,8 @@ const AccountEventsPage: FC<AccountEventsPageProps> = (props) => {
if (viewModel.state === "loading") return <AccountEventsPageLoading />; if (viewModel.state === "loading") return <AccountEventsPageLoading />;
return viewModel.state === "error" return viewModel.state === "error"
? ( ? <AccountEventsPageError {...viewModel} />
<AccountEventsPageError {...viewModel} /> : <AccountEventsPageLoaded {...viewModel} />;
)
: (
<AccountEventsPageLoaded {...viewModel} />
);
}; };
export { AccountEventsPage }; export { AccountEventsPage };
@@ -41,11 +41,11 @@ function useAccountEventsPageViewModel(
const appliedFrom = isValidUrlPeriod const appliedFrom = isValidUrlPeriod
? urlFrom ? urlFrom
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. : null;
const appliedTo = isValidUrlPeriod const appliedTo = isValidUrlPeriod
? urlTo ? 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 [draftFrom, setDraftFrom] = useState<null | string>(appliedFrom);
const [draftTo, setDraftTo] = useState<null | string>(appliedTo); const [draftTo, setDraftTo] = useState<null | string>(appliedTo);
@@ -6,9 +6,7 @@ import { useTranslation } from "react-i18next";
const AccountEventsPageEmpty: FC = () => { const AccountEventsPageEmpty: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return <StateView text={t("events.emptyText")} title={t("events.emptyTitle")} />;
<StateView text={t("events.emptyText")} title={t("events.emptyTitle")} />
);
}; };
export { AccountEventsPageEmpty }; export { AccountEventsPageEmpty };
@@ -74,13 +74,9 @@ const AccountEventsPageLoaded: FC<AccountEventsPageLoadedProps> = (props) => {
{t("investment.refresh")} {t("investment.refresh")}
</Button> </Button>
{!!props.partial && ( {!!props.partial && <Alert role="status">{t("investment.partial")}</Alert>}
<Alert role="status">{t("investment.partial")}</Alert>
)}
{!!props.refreshing && ( {!!props.refreshing && <Progress animated aria-label={t("accounts.refreshing")} value={100} />}
<Progress animated aria-label={t("accounts.refreshing")} value={100} />
)}
{!!props.refreshError && ( {!!props.refreshError && (
<Alert aria-live="polite" color="yellow" role="status"> <Alert aria-live="polite" color="yellow" role="status">
@@ -27,9 +27,7 @@ const AccountEventsPageLoading: FC = () => {
<Skeleton height={36} width={108} /> <Skeleton height={36} width={108} />
</Group> </Group>
{skeletonRowIds.map(id => ( {skeletonRowIds.map(id => <Skeleton height={82} key={id} radius="md" />)}
<Skeleton height={82} key={id} radius="md" />
))}
</LoadingSkeleton> </LoadingSkeleton>
); );
}; };
+1 -2
View File
@@ -37,14 +37,13 @@ function moscowDate(value: Date): string {
function parseDraftDate(value: string): null | string { function parseDraftDate(value: string): null | string {
if (!/^\d{2}\.\d{2}\.\d{4}$/.test(value)) 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; return null;
const [day, month, year] = value.split(".", DATE_PART_COUNT); const [day, month, year] = value.split(".", DATE_PART_COUNT);
const date = `${year}-${month}-${day}`; const date = `${year}-${month}-${day}`;
return isDate(date) return isDate(date)
? date ? date
: null; // eslint-disable-line unicorn/no-null -- Mantine date fields use null for an incomplete or invalid date. : null;
} }
export { export {
@@ -38,8 +38,6 @@ import {
} from "./AccountIncomePage.vm"; } from "./AccountIncomePage.vm";
import { IncomePayments } from "./components/IncomePayments.view"; import { IncomePayments } from "./components/IncomePayments.view";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const analytics const analytics
= { = {
accountId: "account-1", accountId: "account-1",
@@ -81,7 +79,7 @@ const analytics
}, },
totalsByCurrency: [summary("RUB", "10", "2")], totalsByCurrency: [summary("RUB", "10", "2")],
yearComparison: null, yearComparison: null,
} satisfies /* eslint-enable unicorn/no-null */ IncomeAnalytics; } satisfies IncomeAnalytics;
function comparison(currency: string) { function comparison(currency: string) {
return { return {
@@ -93,28 +91,24 @@ function comparison(currency: string) {
} }
function entry(): IncomeAnalytics["entries"][number] { function entry(): IncomeAnalytics["entries"][number] {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */ return {
return ( accruedInterest: null,
{ category: "income",
accruedInterest: null, commission: null,
category: "income", description: "Купон",
commission: null, figi: "FIGI",
description: "Купон", id: "coupon-1",
figi: "FIGI", instrumentKey: "income-group-opaque-v2",
id: "coupon-1", instrumentType: "bond",
instrumentKey: "income-group-opaque-v2", instrumentUid: null,
instrumentType: "bond", logoName: null,
instrumentUid: null, occurredAt: "2026-08-01T12:00:00Z",
logoName: null, payment: money("RUB", "10"),
occurredAt: "2026-08-01T12:00:00Z", price: null,
payment: money("RUB", "10"), quantity: null,
price: null, ticker: "BOND",
quantity: null, type: "OPERATION_TYPE_COUPON",
ticker: "BOND", };
type: "OPERATION_TYPE_COUPON",
}
/* eslint-enable unicorn/no-null */
);
} }
function money(currency: string, value: string) { function money(currency: string, value: string) {
@@ -211,7 +205,6 @@ describe("AccountIncomePage", () => {
return HttpResponse.json( return HttpResponse.json(
resource( resource(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
null, null,
{ {
sync: { sync: {
@@ -220,7 +213,6 @@ describe("AccountIncomePage", () => {
state: "failed", 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 () => { 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 const response
= { = {
...analytics, ...analytics,
@@ -464,7 +455,6 @@ describe("AccountIncomePage", () => {
], ],
}; };
/* eslint-enable unicorn/no-null */
server.use( server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () => http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(resource(response)), HttpResponse.json(resource(response)),
@@ -607,37 +597,31 @@ describe("income comparison display", () => {
[ [
"10", "10",
"0", "0",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
], ],
[ [
"0", "0",
"0", "0",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
], ],
[ [
"10", "10",
"-0.00", "-0.00",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
], ],
[ [
"10", "10",
"-100", "-100",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
], ],
[ [
"9".repeat(400), "9".repeat(400),
"1", "1",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
], ],
[ [
"1", "1",
"9".repeat(400), "9".repeat(400),
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
], ],
])("formats change %s against base %s", (difference, base, expected) => { ])("formats change %s against base %s", (difference, base, expected) => {
@@ -656,7 +640,7 @@ describe("income comparison display", () => {
const result = display?.comparison?.values[0]; const result = display?.comparison?.values[0];
expect( expect(
result?.percentages.coupon?.replaceAll("\u{A0}", " ") 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, ?? null,
).toBe(expected); ).toBe(expected);
expect(result?.difference.coupon.value).toBe(difference); expect(result?.difference.coupon.value).toBe(difference);
@@ -863,7 +847,7 @@ describe("income period and snapshot boundary", () => {
), ),
), ),
http.get("*/api/v1/tbank-sync/:id", () => http.get("*/api/v1/tbank-sync/:id", () =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json( HttpResponse.json(
{ {
errorCode: null, errorCode: null,
@@ -874,7 +858,6 @@ describe("income period and snapshot boundary", () => {
scope: null, scope: null,
state: "succeeded", state: "succeeded",
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () => 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", () => { it("maps comparison notices by role regardless of dependency order or resource key", () => {
const selected = { ...resource(analytics).meta, role: "selected" as const }; 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 const previous
= { = {
...resource(null).meta, ...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 = { const year = {
...resource(null).meta, ...resource(null).meta,
resourceKey: "opaque/previous-looking-key", resourceKey: "opaque/previous-looking-key",
@@ -1041,10 +1021,10 @@ it.each([false, true])(
server.use( server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () => http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json( HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ ...analytics, previous: null, yearComparison: null }, { ...analytics, previous: null, yearComparison: null },
/* eslint-enable unicorn/no-null */ { {
dependencies: legacy dependencies: legacy
? [ ? [
resource(null).meta, resource(null).meta,
@@ -1073,22 +1053,21 @@ it.each([false, true])(
it("shows selected income before comparison coverage without inventing a zero baseline", async () => { it("shows selected income before comparison coverage without inventing a zero baseline", async () => {
const dependencies = (["year", "selected", "previous"] as const).map( const dependencies = (["year", "selected", "previous"] as const).map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
role => role =>
({ ({
...resource(role === "selected" ? analytics : null).meta, ...resource(role === "selected" ? analytics : null).meta,
role, role,
}), }),
/* eslint-enable unicorn/no-null */
); );
server.use( server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () => http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json( HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ ...analytics, previous: null, yearComparison: null }, { ...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, ...resource(analytics).meta,
role: "previous" as const, role: "previous" as const,
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
}, },
{ {
...resource(null).meta, ...resource(null).meta,
@@ -1127,7 +1106,7 @@ it("keeps complete previous comparison while year source failed without inventin
state: "failed" as const, state: "failed" as const,
}, },
}, },
/* eslint-enable unicorn/no-null */ { {
...resource(analytics).meta, ...resource(analytics).meta,
role: "selected" as const, role: "selected" as const,
}, },
@@ -12,12 +12,8 @@ const AccountIncomePage: FC<AccountIncomePageProps> = (props) => {
if (viewModel.state === "loading") return <AccountIncomePageLoading />; if (viewModel.state === "loading") return <AccountIncomePageLoading />;
return viewModel.state === "error" return viewModel.state === "error"
? ( ? <AccountIncomePageError {...viewModel} />
<AccountIncomePageError {...viewModel} /> : <AccountIncomePageLoaded {...viewModel} />;
)
: (
<AccountIncomePageLoaded {...viewModel} />
);
}; };
export { AccountIncomePage }; export { AccountIncomePage };
@@ -161,7 +161,6 @@ function comparisonDisplay(
comparison: IncomeAnalytics["yearComparison"], comparison: IncomeAnalytics["yearComparison"],
): IncomeDisplayModel["comparison"] { ): IncomeDisplayModel["comparison"] {
if (!comparison) if (!comparison)
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
return null; return null;
return { return {
@@ -220,11 +219,9 @@ function formatIncomeChange(
// Display-only ratio; server-provided Money values remain exact decimal strings. // Display-only ratio; server-provided Money values remain exact decimal strings.
const base = Number(compared); const base = Number(compared);
if (!Number.isFinite(base) || base <= 0) 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; return null;
const ratio = Number(difference) / base; const ratio = Number(difference) / base;
if (!Number.isFinite(ratio)) if (!Number.isFinite(ratio))
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
return null; return null;
return new Intl.NumberFormat("ru-RU", { return new Intl.NumberFormat("ru-RU", {
@@ -272,7 +269,7 @@ function toIncomeDisplayModel(
|| instruments.has(instrument.key) || instruments.has(instrument.key)
|| !currencies.has(instrument.total.currency) || !currencies.has(instrument.total.currency)
) )
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
return null; return null;
instruments.set(instrument.key, { instruments.set(instrument.key, {
currency: instrument.total.currency, currency: instrument.total.currency,
@@ -286,11 +283,9 @@ function toIncomeDisplayModel(
for (const entry of analytics.entries) { for (const entry of analytics.entries) {
if (typeof entry.instrumentKey !== "string" || !entry.instrumentKey.trim()) if (typeof entry.instrumentKey !== "string" || !entry.instrumentKey.trim())
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
return null; return null;
const instrument = instruments.get(entry.instrumentKey); const instrument = instruments.get(entry.instrumentKey);
if (!instrument || entry.payment?.currency !== instrument.currency) if (!instrument || entry.payment?.currency !== instrument.currency)
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
return null; return null;
const occurrenceKey = JSON.stringify([entry.instrumentKey, entry.id]); const occurrenceKey = JSON.stringify([entry.instrumentKey, entry.id]);
const occurrence = occurrences.get(occurrenceKey) ?? 0; const occurrence = occurrences.get(occurrenceKey) ?? 0;
@@ -309,7 +304,6 @@ function toIncomeDisplayModel(
for (const instrument of instruments.values()) { for (const instrument of instruments.values()) {
if (instrument.includedCount !== instrument.receiptCount) if (instrument.includedCount !== instrument.receiptCount)
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
return null; return null;
} }
@@ -35,12 +35,8 @@ const AccountIncomePageLoadedSummary: FC<LoadedProps> = (props) => {
} }
return analytics.totalsByCurrency.length > 0 return analytics.totalsByCurrency.length > 0
? ( ? <IncomeSummary totals={analytics.totalsByCurrency} />
<IncomeSummary totals={analytics.totalsByCurrency} /> : <StateView text={t("income.emptyText")} title={t("income.emptyTitle")} />;
)
: (
<StateView text={t("income.emptyText")} title={t("income.emptyTitle")} />
);
}; };
const AccountIncomePageLoaded: FC<LoadedProps> = (props) => { const AccountIncomePageLoaded: FC<LoadedProps> = (props) => {
@@ -34,9 +34,7 @@ const IncomeComparisons: FC<IncomeComparisonsProps> = (props) => {
<Title order={2}>{props.title}</Title> <Title order={2}>{props.title}</Title>
{props.comparison {props.comparison
? ( ? <Comparison {...props.comparison} title={props.title} />
<Comparison {...props.comparison} title={props.title} />
)
: ( : (
<Paper className={classes.comparisonNotice} p="sm" radius="md"> <Paper className={classes.comparisonNotice} p="sm" radius="md">
<Text aria-live="polite" c="gray.7" role="status" size="sm"> <Text aria-live="polite" c="gray.7" role="status" size="sm">
@@ -25,7 +25,7 @@ const PeriodExample: FC<PeriodExampleProps> = (props) => {
const submitted = isSubmitSuccessful const submitted = isSubmitSuccessful
? methods.getValues() ? methods.getValues()
: null; // eslint-disable-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract. : null;
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
@@ -24,8 +24,6 @@ import { server } from "@/test/server";
import { AccountOperationsPage } from "./AccountOperationsPage"; import { AccountOperationsPage } from "./AccountOperationsPage";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const operation: BrokerageOperation const operation: BrokerageOperation
= { = {
accruedInterest: null, accruedInterest: null,
@@ -44,7 +42,6 @@ const operation: BrokerageOperation
ticker: "SBER", ticker: "SBER",
type: "OPERATION_TYPE_BUY", type: "OPERATION_TYPE_BUY",
}; };
/* eslint-enable unicorn/no-null */
const Navigation: FC = () => { const Navigation: FC = () => {
const location = useLocation(); const location = useLocation();
@@ -322,10 +319,9 @@ describe("AccountOperationsPage", () => {
new URL(request.url).searchParams.has("from") new URL(request.url).searchParams.has("from")
? HttpResponse.json({ code: "VALIDATION_ERROR" }, { status: 400 }) ? HttpResponse.json({ code: "VALIDATION_ERROR" }, { status: 400 })
: HttpResponse.json( : HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ hasNext: false, items: [], nextCursor: null }, { hasNext: false, items: [], nextCursor: null },
/* eslint-enable unicorn/no-null */
), ),
), ),
), ),
@@ -368,7 +364,7 @@ describe("AccountOperationsPage", () => {
searches.push(url.search); searches.push(url.search);
return HttpResponse.json( return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: "account-1", accountId: "account-1",
@@ -377,7 +373,6 @@ describe("AccountOperationsPage", () => {
nextCursor: url.searchParams.get("cursor") ? null : "next-1", nextCursor: url.searchParams.get("cursor") ? null : "next-1",
observedAt: "2026-08-14T00:00:00Z", observedAt: "2026-08-14T00:00:00Z",
}, },
/* eslint-enable unicorn/no-null */
), ),
); );
}, },
@@ -411,7 +406,7 @@ describe("AccountOperationsPage", () => {
searches.push(url.search); searches.push(url.search);
return HttpResponse.json( return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: "account-1", accountId: "account-1",
@@ -420,7 +415,6 @@ describe("AccountOperationsPage", () => {
nextCursor: null, nextCursor: null,
observedAt: "2026-08-14T00:00:00Z", observedAt: "2026-08-14T00:00:00Z",
}, },
/* eslint-enable unicorn/no-null */
), ),
); );
}, },
@@ -451,7 +445,7 @@ describe("AccountOperationsPage", () => {
searches.push(url.search); searches.push(url.search);
return HttpResponse.json( return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: "account-1", accountId: "account-1",
@@ -460,7 +454,6 @@ describe("AccountOperationsPage", () => {
nextCursor: null, nextCursor: null,
observedAt: "2026-08-14T00:00:00Z", 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( return HttpResponse.json(
resource( resource(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
null, null,
{ {
sync: { sync: {
@@ -517,7 +509,6 @@ it("keeps UTC operation boundaries in GET and source sync requests", async () =>
state: "failed", 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( server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () => http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
HttpResponse.json( HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: "account-1", accountId: "account-1",
@@ -567,7 +558,7 @@ it("does not label incomplete empty operations as a confirmed empty period", asy
items: [], items: [],
nextCursor: null, nextCursor: null,
}, },
/* eslint-enable unicorn/no-null */ { availability: "partial" }, { availability: "partial" },
), ),
), ),
), ),
@@ -15,12 +15,8 @@ const AccountOperationsPage: FC<AccountOperationsPageProps> = (props) => {
} }
return viewModel.state === "error" return viewModel.state === "error"
? ( ? <AccountOperationsPageError {...viewModel} />
<AccountOperationsPageError {...viewModel} /> : <AccountOperationsPageLoaded {...viewModel} />;
)
: (
<AccountOperationsPageLoaded {...viewModel} />
);
}; };
export { AccountOperationsPage }; export { AccountOperationsPage };
@@ -54,11 +54,11 @@ const useAccountOperationsPageVm = (
const appliedFrom = isValidUrlPeriod const appliedFrom = isValidUrlPeriod
? urlFrom ? urlFrom
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. : null;
const appliedTo = isValidUrlPeriod const appliedTo = isValidUrlPeriod
? urlTo ? urlTo
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. : null;
const appliedCategories = operationCategories.filter(category => const appliedCategories = operationCategories.filter(category =>
(searchParameters.get("categories") ?? "").split(",").includes(category), (searchParameters.get("categories") ?? "").split(",").includes(category),
@@ -50,9 +50,7 @@ const AccountOperationsPageLoadedOperations: FC<
return; return;
} }
return ( return <StateView text={t("ledger.emptyText")} title={t("ledger.emptyTitle")} />;
<StateView text={t("ledger.emptyText")} title={t("ledger.emptyTitle")} />
);
}; };
const AccountOperationsPageLoaded: FC<AccountOperationsPageLoadedProps> = ( const AccountOperationsPageLoaded: FC<AccountOperationsPageLoadedProps> = (
@@ -91,15 +89,11 @@ const AccountOperationsPageLoaded: FC<AccountOperationsPageLoadedProps> = (
{t("investment.refresh")} {t("investment.refresh")}
</Button> </Button>
{!!props.partial && ( {!!props.partial && <Alert role="status">{t("investment.partial")}</Alert>}
<Alert role="status">{t("investment.partial")}</Alert>
)}
{!!props.historyLoading && <Alert>{t("investment.history")}</Alert>} {!!props.historyLoading && <Alert>{t("investment.history")}</Alert>}
{!!props.refreshing && ( {!!props.refreshing && <Progress animated aria-label={t("accounts.refreshing")} value={100} />}
<Progress animated aria-label={t("accounts.refreshing")} value={100} />
)}
{!!props.refreshError && ( {!!props.refreshError && (
<Alert aria-live="polite" color="yellow" role="status"> <Alert aria-live="polite" color="yellow" role="status">
@@ -28,9 +28,7 @@ const AccountOperationsPageLoading: FC = () => {
</Group> </Group>
<Stack gap="xs"> <Stack gap="xs">
{skeletonRowIds.map(id => ( {skeletonRowIds.map(id => <Skeleton height={64} key={id} radius="md" />)}
<Skeleton height={64} key={id} radius="md" />
))}
</Stack> </Stack>
</LoadingSkeleton> </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 const portfolio: Portfolio
= { = {
cashBalances: [{ currency: "USD", value: "0.5" }], cashBalances: [{ currency: "USD", value: "0.5" }],
@@ -103,7 +101,6 @@ const portfolio: Portfolio
structure: { currencies: [], unvaluedCategories: [] }, structure: { currencies: [], unvaluedCategories: [] },
totalsByCurrency: [{ currency: "RUB", value: "1234.5" }], totalsByCurrency: [{ currency: "RUB", value: "1234.5" }],
}; };
/* eslint-enable unicorn/no-null */
const emptyRecentOperations: AccountPageRecentOperations = { const emptyRecentOperations: AccountPageRecentOperations = {
state: "empty", state: "empty",
@@ -146,8 +143,6 @@ describe("AccountPage states", () => {
const uid = "c190ff1f-1447-4227-b543-316332699ca5"; const uid = "c190ff1f-1447-4227-b543-316332699ca5";
const requests = vi.fn(); const requests = vi.fn();
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const position const position
= { = {
...portfolio.positions[0], ...portfolio.positions[0],
@@ -158,7 +153,6 @@ describe("AccountPage states", () => {
type, type,
}; };
/* eslint-enable unicorn/no-null */
server.use( server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
HttpResponse.json( HttpResponse.json(
@@ -483,7 +477,6 @@ describe("AccountPage states", () => {
it("renders stock cards with logo fallback and signed unlabeled return", () => { it("renders stock cards with logo fallback and signed unlabeled return", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps( {...loadedProps(
{ {
...portfolio, ...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", () => { it("groups stock supporting metrics into quiet single lines", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps( {...loadedProps(
{ {
...portfolio, ...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", () => { it("uses the ticker only for stock cards and otherwise keeps the ISIN", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps( {...loadedProps(
{ {
...portfolio, ...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", () => { it("renders bond cards with logo, fallback, and signed unlabeled return like stocks", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps( {...loadedProps(
{ {
...portfolio, ...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", () => { it("renders bond cards in expanded layout with bond-specific classes", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps( {...loadedProps(
{ {
...portfolio, ...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", () => { it("keeps other-position cards compact with labeled return and no logo", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps( {...loadedProps(
{ {
...portfolio, ...portfolio,
@@ -726,7 +709,6 @@ describe("AccountPage states", () => {
}, },
], ],
}, },
/* eslint-enable unicorn/no-null */
)} )}
/>, />,
); );
@@ -738,17 +720,14 @@ describe("AccountPage states", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
{...loadedProps()} {...loadedProps()}
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
account={ account={{
{ id: "account-1",
id: "account-1", name: "Основной",
name: "Основной", openedOn: null,
openedOn: null, portfolioSummary: null,
portfolioSummary: null, type: "BROKERAGE",
type: "BROKERAGE", }}
}
/* eslint-enable unicorn/no-null */
}
/>, />,
); );
expect( expect(
@@ -777,33 +756,30 @@ describe("AccountPage states", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
{...loadedProps()} {...loadedProps()}
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
recentOperations={ recentOperations={{
{ items: [
items: [ {
{ accruedInterest: null,
accruedInterest: null, category: "trade",
category: "trade", commission: null,
commission: null, description: "Покупка акции",
description: "Покупка акции", figi: "FIGI-1",
figi: "FIGI-1", id: "operation-1",
id: "operation-1", instrumentType: "share",
instrumentType: "share", instrumentUid: "UID-1",
instrumentUid: "UID-1", logoName: null,
logoName: null, occurredAt: "2026-08-14T10:00:00Z",
occurredAt: "2026-08-14T10:00:00Z", payment: { currency: "RUB", value: "-12" },
payment: { currency: "RUB", value: "-12" }, price: null,
price: null, quantity: null,
quantity: null, ticker: "SBER",
ticker: "SBER", type: "OPERATION_TYPE_BUY",
type: "OPERATION_TYPE_BUY", },
}, ],
], state: "loaded",
state: "loaded", to: "/accounts/account-1/operations",
to: "/accounts/account-1/operations", }}
}
/* eslint-enable unicorn/no-null */
}
/>, />,
); );
@@ -812,7 +788,6 @@ describe("AccountPage states", () => {
}); });
it("keeps future events ordered with calendar dates and projected rather than posted amounts", () => { 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 const event: BrokerageEvent
= { = {
date: "2026-09-10", date: "2026-09-10",
@@ -827,41 +802,37 @@ describe("AccountPage states", () => {
type: "coupon", type: "coupon",
}; };
/* eslint-enable unicorn/no-null */
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
{...loadedProps()} {...loadedProps()}
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
recentEvents={ recentEvents={{
{ items: [
items: [ event,
event, {
{ ...event,
...event, date: "2026-09-11",
date: "2026-09-11", dateKind: "registry",
dateKind: "registry", displayName: "Акция",
displayName: "Акция", projectedAmount: { currency: "USD", value: "0" },
projectedAmount: { currency: "USD", value: "0" }, type: "dividend",
type: "dividend", },
}, {
{ ...event,
...event, date: "2026-09-12",
date: "2026-09-12", projectedAmount: { currency: "RUB", value: "1000" },
projectedAmount: { currency: "RUB", value: "1000" }, type: "maturity",
type: "maturity", },
}, {
{ ...event,
...event, date: "2026-09-13",
date: "2026-09-13", projectedAmount: null,
projectedAmount: null, type: "offer",
type: "offer", },
}, ],
], state: "loaded",
state: "loaded", to: "/accounts/account-1/events",
to: "/accounts/account-1/events", }}
}
/* eslint-enable unicorn/no-null */
}
/>, />,
); );
@@ -953,7 +924,6 @@ describe("AccountPage states", () => {
it.each([503])( it.each([503])(
"preserves totals and UTC snapshot through refresh error %i, retry and success", "preserves totals and UTC snapshot through refresh error %i, retry and success",
async (status) => { async (status) => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const initialPortfolio: Portfolio const initialPortfolio: Portfolio
= { = {
...portfolio, ...portfolio,
@@ -970,7 +940,6 @@ describe("AccountPage states", () => {
}, },
}; };
/* eslint-enable unicorn/no-null */
let requestCount = 0; let requestCount = 0;
let resolveRetry: ((response: Response) => void) | undefined; let resolveRetry: ((response: Response) => void) | undefined;
server.use( server.use(
@@ -985,7 +954,7 @@ describe("AccountPage states", () => {
), ),
), ),
http.get("*/api/v1/tbank-sync/:id", () => http.get("*/api/v1/tbank-sync/:id", () =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json( HttpResponse.json(
{ {
errorCode: null, errorCode: null,
@@ -999,7 +968,6 @@ describe("AccountPage states", () => {
scope: { accountId: "account-1", screen: "account" }, scope: { accountId: "account-1", screen: "account" },
state: "succeeded", state: "succeeded",
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => { http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => {
@@ -18,8 +18,6 @@ import {
useAccountPageViewModel, useAccountPageViewModel,
} from "./AccountPage.vm"; } from "./AccountPage.vm";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const portfolio: Portfolio const portfolio: Portfolio
= { = {
cashBalances: [{ currency: "USD", value: "1" }], cashBalances: [{ currency: "USD", value: "1" }],
@@ -66,7 +64,6 @@ const portfolio: Portfolio
structure: { currencies: [], unvaluedCategories: [] }, structure: { currencies: [], unvaluedCategories: [] },
totalsByCurrency: [{ currency: "RUB", value: "150" }], totalsByCurrency: [{ currency: "RUB", value: "150" }],
}; };
/* eslint-enable unicorn/no-null */
function configureTestAuth() { function configureTestAuth() {
configureAuthClient({ configureAuthClient({
@@ -101,7 +98,7 @@ function mockLoadedAccountPage() {
), ),
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () => http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
HttpResponse.json( HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: "account-1", accountId: "account-1",
@@ -127,7 +124,6 @@ function mockLoadedAccountPage() {
nextCursor: null, nextCursor: null,
observedAt: "2026-08-13T10:00:00Z", observedAt: "2026-08-13T10:00:00Z",
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
), ),
@@ -138,7 +134,7 @@ function mockLoadedAccountPage() {
calculatedAt: "2026-08-13T10:00:00Z", calculatedAt: "2026-08-13T10:00:00Z",
events: Array.from( events: Array.from(
{ length: 6 }, { length: 6 },
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
(_, index) => (_, index) =>
({ ({
date: `2026-09-${String(index + 1).padStart(2, "0")}`, date: `2026-09-${String(index + 1).padStart(2, "0")}`,
@@ -150,7 +146,6 @@ function mockLoadedAccountPage() {
ticker: `BND${index}`, ticker: `BND${index}`,
type: "coupon", type: "coupon",
}), }),
/* eslint-enable unicorn/no-null */
), ),
}), }),
), ),
@@ -181,7 +176,7 @@ describe("useAccountPageViewModel", () => {
type: "CURRENCY", type: "CURRENCY",
}, },
...otherTypes.map( ...otherTypes.map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
type => type =>
({ ({
...portfolio.positions[2], ...portfolio.positions[2],
@@ -190,7 +185,6 @@ describe("useAccountPageViewModel", () => {
figi: `FIGI-${type}-NEXT`, figi: `FIGI-${type}-NEXT`,
type, type,
}), }),
/* eslint-enable unicorn/no-null */
), ),
], ],
structure: { currencies: [], unvaluedCategories: [] }, structure: { currencies: [], unvaluedCategories: [] },
@@ -248,7 +242,7 @@ describe("useAccountPageViewModel", () => {
shareText: "66,7 %", shareText: "66,7 %",
}, },
], ],
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
}, },
{ {
kind: "bonds", kind: "bonds",
@@ -262,15 +256,15 @@ describe("useAccountPageViewModel", () => {
}, },
], ],
}, },
/* eslint-enable unicorn/no-null */ { {
kind: "other", kind: "other",
positions: [ positions: [
{ {
displayName: "Фонд", displayName: "Фонд",
logoSrc: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture. logoSrc: null,
returnText: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture. returnText: null,
returnTone: undefined, 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", () => http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
HttpResponse.json( HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ hasNext: false, items: [], nextCursor: null }, { 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 () => { it("refreshes every overview section through one account descriptor and observational reads", async () => {
configureTestAuth(); configureTestAuth();
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const sections: Record<string, unknown> const sections: Record<string, unknown>
= { = {
"events": { events: [], incomeTotalsByCurrency: [] }, "events": { events: [], incomeTotalsByCurrency: [] },
@@ -395,7 +387,6 @@ it("refreshes every overview section through one account descriptor and observat
portfolio, portfolio,
}; };
/* eslint-enable unicorn/no-null */
const observed: string[] = []; const observed: string[] = [];
const scopes: unknown[] = []; const scopes: unknown[] = [];
let version = "1"; 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", () => http.get("*/api/v1/tbank-sync/:id", () =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json( HttpResponse.json(
{ {
errorCode: null, errorCode: null,
@@ -441,7 +432,6 @@ it("refreshes every overview section through one account descriptor and observat
scope: null, scope: null,
state: "succeeded", state: "succeeded",
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
); );

Some files were not shown because too many files have changed in this diff Show More