refactor: address frontend review feedback
Frontend verify / commits (pull_request) Successful in 29s
Frontend verify / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 8m54s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 12s
Frontend verify / verify (push) Successful in 9m59s
Frontend verify / commits (pull_request) Successful in 29s
Frontend verify / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 8m54s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 12s
Frontend verify / verify (push) Successful in 9m59s
This commit was merged in pull request #43.
This commit is contained in:
@@ -67,8 +67,6 @@ function renderPage(ui: React.ReactNode) {
|
||||
);
|
||||
}
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const portfolio: Portfolio
|
||||
= {
|
||||
cashBalances: [{ currency: "USD", value: "0.5" }],
|
||||
@@ -103,7 +101,6 @@ const portfolio: Portfolio
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
totalsByCurrency: [{ currency: "RUB", value: "1234.5" }],
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
const emptyRecentOperations: AccountPageRecentOperations = {
|
||||
state: "empty",
|
||||
@@ -146,8 +143,6 @@ describe("AccountPage states", () => {
|
||||
const uid = "c190ff1f-1447-4227-b543-316332699ca5";
|
||||
const requests = vi.fn();
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const position
|
||||
= {
|
||||
...portfolio.positions[0],
|
||||
@@ -158,7 +153,6 @@ describe("AccountPage states", () => {
|
||||
type,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
|
||||
HttpResponse.json(
|
||||
@@ -483,7 +477,6 @@ describe("AccountPage states", () => {
|
||||
it("renders stock cards with logo fallback and signed unlabeled return", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{...loadedProps(
|
||||
{
|
||||
...portfolio,
|
||||
@@ -514,7 +507,6 @@ describe("AccountPage states", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
@@ -528,7 +520,6 @@ describe("AccountPage states", () => {
|
||||
it("groups stock supporting metrics into quiet single lines", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{...loadedProps(
|
||||
{
|
||||
...portfolio,
|
||||
@@ -547,7 +538,6 @@ describe("AccountPage states", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
@@ -563,7 +553,6 @@ describe("AccountPage states", () => {
|
||||
it("uses the ticker only for stock cards and otherwise keeps the ISIN", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{...loadedProps(
|
||||
{
|
||||
...portfolio,
|
||||
@@ -606,7 +595,6 @@ describe("AccountPage states", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
@@ -623,7 +611,6 @@ describe("AccountPage states", () => {
|
||||
it("renders bond cards with logo, fallback, and signed unlabeled return like stocks", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{...loadedProps(
|
||||
{
|
||||
...portfolio,
|
||||
@@ -654,7 +641,6 @@ describe("AccountPage states", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
@@ -674,7 +660,6 @@ describe("AccountPage states", () => {
|
||||
it("renders bond cards in expanded layout with bond-specific classes", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{...loadedProps(
|
||||
{
|
||||
...portfolio,
|
||||
@@ -693,7 +678,6 @@ describe("AccountPage states", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
@@ -707,7 +691,6 @@ describe("AccountPage states", () => {
|
||||
it("keeps other-position cards compact with labeled return and no logo", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{...loadedProps(
|
||||
{
|
||||
...portfolio,
|
||||
@@ -726,7 +709,6 @@ describe("AccountPage states", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
@@ -738,17 +720,14 @@ describe("AccountPage states", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
{...loadedProps()}
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
account={
|
||||
{
|
||||
id: "account-1",
|
||||
name: "Основной",
|
||||
openedOn: null,
|
||||
portfolioSummary: null,
|
||||
type: "BROKERAGE",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */
|
||||
}
|
||||
|
||||
account={{
|
||||
id: "account-1",
|
||||
name: "Основной",
|
||||
openedOn: null,
|
||||
portfolioSummary: null,
|
||||
type: "BROKERAGE",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
@@ -777,33 +756,30 @@ describe("AccountPage states", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
{...loadedProps()}
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
recentOperations={
|
||||
{
|
||||
items: [
|
||||
{
|
||||
accruedInterest: null,
|
||||
category: "trade",
|
||||
commission: null,
|
||||
description: "Покупка акции",
|
||||
figi: "FIGI-1",
|
||||
id: "operation-1",
|
||||
instrumentType: "share",
|
||||
instrumentUid: "UID-1",
|
||||
logoName: null,
|
||||
occurredAt: "2026-08-14T10:00:00Z",
|
||||
payment: { currency: "RUB", value: "-12" },
|
||||
price: null,
|
||||
quantity: null,
|
||||
ticker: "SBER",
|
||||
type: "OPERATION_TYPE_BUY",
|
||||
},
|
||||
],
|
||||
state: "loaded",
|
||||
to: "/accounts/account-1/operations",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */
|
||||
}
|
||||
|
||||
recentOperations={{
|
||||
items: [
|
||||
{
|
||||
accruedInterest: null,
|
||||
category: "trade",
|
||||
commission: null,
|
||||
description: "Покупка акции",
|
||||
figi: "FIGI-1",
|
||||
id: "operation-1",
|
||||
instrumentType: "share",
|
||||
instrumentUid: "UID-1",
|
||||
logoName: null,
|
||||
occurredAt: "2026-08-14T10:00:00Z",
|
||||
payment: { currency: "RUB", value: "-12" },
|
||||
price: null,
|
||||
quantity: null,
|
||||
ticker: "SBER",
|
||||
type: "OPERATION_TYPE_BUY",
|
||||
},
|
||||
],
|
||||
state: "loaded",
|
||||
to: "/accounts/account-1/operations",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -812,7 +788,6 @@ describe("AccountPage states", () => {
|
||||
});
|
||||
|
||||
it("keeps future events ordered with calendar dates and projected rather than posted amounts", () => {
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
const event: BrokerageEvent
|
||||
= {
|
||||
date: "2026-09-10",
|
||||
@@ -827,41 +802,37 @@ describe("AccountPage states", () => {
|
||||
type: "coupon",
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
{...loadedProps()}
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
recentEvents={
|
||||
{
|
||||
items: [
|
||||
event,
|
||||
{
|
||||
...event,
|
||||
date: "2026-09-11",
|
||||
dateKind: "registry",
|
||||
displayName: "Акция",
|
||||
projectedAmount: { currency: "USD", value: "0" },
|
||||
type: "dividend",
|
||||
},
|
||||
{
|
||||
...event,
|
||||
date: "2026-09-12",
|
||||
projectedAmount: { currency: "RUB", value: "1000" },
|
||||
type: "maturity",
|
||||
},
|
||||
{
|
||||
...event,
|
||||
date: "2026-09-13",
|
||||
projectedAmount: null,
|
||||
type: "offer",
|
||||
},
|
||||
],
|
||||
state: "loaded",
|
||||
to: "/accounts/account-1/events",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */
|
||||
}
|
||||
|
||||
recentEvents={{
|
||||
items: [
|
||||
event,
|
||||
{
|
||||
...event,
|
||||
date: "2026-09-11",
|
||||
dateKind: "registry",
|
||||
displayName: "Акция",
|
||||
projectedAmount: { currency: "USD", value: "0" },
|
||||
type: "dividend",
|
||||
},
|
||||
{
|
||||
...event,
|
||||
date: "2026-09-12",
|
||||
projectedAmount: { currency: "RUB", value: "1000" },
|
||||
type: "maturity",
|
||||
},
|
||||
{
|
||||
...event,
|
||||
date: "2026-09-13",
|
||||
projectedAmount: null,
|
||||
type: "offer",
|
||||
},
|
||||
],
|
||||
state: "loaded",
|
||||
to: "/accounts/account-1/events",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -953,7 +924,6 @@ describe("AccountPage states", () => {
|
||||
it.each([503])(
|
||||
"preserves totals and UTC snapshot through refresh error %i, retry and success",
|
||||
async (status) => {
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
const initialPortfolio: Portfolio
|
||||
= {
|
||||
...portfolio,
|
||||
@@ -970,7 +940,6 @@ describe("AccountPage states", () => {
|
||||
},
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
let requestCount = 0;
|
||||
let resolveRetry: ((response: Response) => void) | undefined;
|
||||
server.use(
|
||||
@@ -985,7 +954,7 @@ describe("AccountPage states", () => {
|
||||
),
|
||||
),
|
||||
http.get("*/api/v1/tbank-sync/:id", () =>
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
HttpResponse.json(
|
||||
{
|
||||
errorCode: null,
|
||||
@@ -999,7 +968,6 @@ describe("AccountPage states", () => {
|
||||
scope: { accountId: "account-1", screen: "account" },
|
||||
state: "succeeded",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => {
|
||||
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
useAccountPageViewModel,
|
||||
} from "./AccountPage.vm";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const portfolio: Portfolio
|
||||
= {
|
||||
cashBalances: [{ currency: "USD", value: "1" }],
|
||||
@@ -66,7 +64,6 @@ const portfolio: Portfolio
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
totalsByCurrency: [{ currency: "RUB", value: "150" }],
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
function configureTestAuth() {
|
||||
configureAuthClient({
|
||||
@@ -101,7 +98,7 @@ function mockLoadedAccountPage() {
|
||||
),
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
|
||||
HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
accountId: "account-1",
|
||||
@@ -127,7 +124,6 @@ function mockLoadedAccountPage() {
|
||||
nextCursor: null,
|
||||
observedAt: "2026-08-13T10:00:00Z",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -138,7 +134,7 @@ function mockLoadedAccountPage() {
|
||||
calculatedAt: "2026-08-13T10:00:00Z",
|
||||
events: Array.from(
|
||||
{ length: 6 },
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
(_, index) =>
|
||||
({
|
||||
date: `2026-09-${String(index + 1).padStart(2, "0")}`,
|
||||
@@ -150,7 +146,6 @@ function mockLoadedAccountPage() {
|
||||
ticker: `BND${index}`,
|
||||
type: "coupon",
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
}),
|
||||
),
|
||||
@@ -181,7 +176,7 @@ describe("useAccountPageViewModel", () => {
|
||||
type: "CURRENCY",
|
||||
},
|
||||
...otherTypes.map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
type =>
|
||||
({
|
||||
...portfolio.positions[2],
|
||||
@@ -190,7 +185,6 @@ describe("useAccountPageViewModel", () => {
|
||||
figi: `FIGI-${type}-NEXT`,
|
||||
type,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
@@ -248,7 +242,7 @@ describe("useAccountPageViewModel", () => {
|
||||
shareText: "66,7 %",
|
||||
},
|
||||
],
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
},
|
||||
{
|
||||
kind: "bonds",
|
||||
@@ -262,15 +256,15 @@ describe("useAccountPageViewModel", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */ {
|
||||
{
|
||||
kind: "other",
|
||||
positions: [
|
||||
{
|
||||
displayName: "Фонд",
|
||||
logoSrc: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
returnText: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
logoSrc: null,
|
||||
returnText: null,
|
||||
returnTone: undefined,
|
||||
shareText: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
shareText: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -351,10 +345,10 @@ it("keeps overview sections independent when their source data is incomplete", a
|
||||
),
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
|
||||
HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{ hasNext: false, items: [], nextCursor: null },
|
||||
/* eslint-enable unicorn/no-null */ { availability: "partial" },
|
||||
{ availability: "partial" },
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -385,8 +379,6 @@ it("keeps overview sections independent when their source data is incomplete", a
|
||||
it("refreshes every overview section through one account descriptor and observational reads", async () => {
|
||||
configureTestAuth();
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const sections: Record<string, unknown>
|
||||
= {
|
||||
"events": { events: [], incomeTotalsByCurrency: [] },
|
||||
@@ -395,7 +387,6 @@ it("refreshes every overview section through one account descriptor and observat
|
||||
portfolio,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
const observed: string[] = [];
|
||||
const scopes: unknown[] = [];
|
||||
let version = "1";
|
||||
@@ -427,7 +418,7 @@ it("refreshes every overview section through one account descriptor and observat
|
||||
);
|
||||
}),
|
||||
http.get("*/api/v1/tbank-sync/:id", () =>
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
HttpResponse.json(
|
||||
{
|
||||
errorCode: null,
|
||||
@@ -441,7 +432,6 @@ it("refreshes every overview section through one account descriptor and observat
|
||||
scope: null,
|
||||
state: "succeeded",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -209,7 +209,7 @@ function toPosition(
|
||||
|
||||
const returnValue = position.absoluteReturn
|
||||
? Number(position.absoluteReturn.value)
|
||||
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null;
|
||||
|
||||
let returnTone: AccountPageReturnTone | undefined;
|
||||
if (returnValue !== null && returnValue !== 0)
|
||||
@@ -226,17 +226,17 @@ function toPosition(
|
||||
: undefined,
|
||||
logoSrc: isExpanded
|
||||
? instrumentLogoUrl(position.logoName)
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null,
|
||||
returnText: position.absoluteReturn
|
||||
? `${returnPrefix}${formatMoney(position.absoluteReturn)}`
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null,
|
||||
returnTone,
|
||||
shareText: position.currentValue?.currency
|
||||
? formatShare(position.currentValue, {
|
||||
currency: position.currentValue.currency,
|
||||
value: String(denominators.get(position.currentValue.currency) ?? 0),
|
||||
})
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -62,9 +62,7 @@ const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
|
||||
sourceError={props.sourceError}
|
||||
/>
|
||||
|
||||
{!!props.portfolio.isStale && (
|
||||
<Alert color="blue">{t("portfolio.staleWarning")}</Alert>
|
||||
)}
|
||||
{!!props.portfolio.isStale && <Alert color="blue">{t("portfolio.staleWarning")}</Alert>}
|
||||
|
||||
<Text c="dimmed" size="sm">
|
||||
{t(
|
||||
@@ -78,12 +76,8 @@ const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
|
||||
</Text>
|
||||
|
||||
{props.partial
|
||||
? (
|
||||
<Alert role="status">{t("investment.partial")}</Alert>
|
||||
)
|
||||
: (
|
||||
<CurrencyCards amounts={props.portfolio.totalsByCurrency} />
|
||||
)}
|
||||
? <Alert role="status">{t("investment.partial")}</Alert>
|
||||
: <CurrencyCards amounts={props.portfolio.totalsByCurrency} />}
|
||||
|
||||
{!props.partial && (
|
||||
<PortfolioStructure
|
||||
|
||||
+1
-3
@@ -34,9 +34,7 @@ const AccountPageLoading: FC = () => {
|
||||
|
||||
<Skeleton height={260} radius="sm" />
|
||||
|
||||
{["first", "second", "third"].map(placeholder => (
|
||||
<Skeleton height={52} key={placeholder} radius="sm" />
|
||||
))}
|
||||
{["first", "second", "third"].map(placeholder => <Skeleton height={52} key={placeholder} radius="sm" />)}
|
||||
</LoadingSkeleton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -69,9 +69,7 @@ const IncomeSummary: FC<IncomeSummaryProps> = (props) => {
|
||||
|
||||
{!!props.stale && <Text size="xs">{t("investment.stale")}</Text>}
|
||||
|
||||
{!!props.refreshError && (
|
||||
<Text role="status">{t("income.refreshError")}</Text>
|
||||
)}
|
||||
{!!props.refreshError && <Text role="status">{t("income.refreshError")}</Text>}
|
||||
|
||||
{props.summary.totalsByCurrency.map(total => (
|
||||
<Paper key={total.currency} p="sm" withBorder>
|
||||
|
||||
+2
-4
@@ -81,13 +81,12 @@ export const Incomplete: Story = {
|
||||
{
|
||||
...rub,
|
||||
categories: rub.categories.map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
row =>
|
||||
({
|
||||
...row,
|
||||
sharePercent: null,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -110,14 +109,13 @@ export const Negative: Story = {
|
||||
{
|
||||
...rub,
|
||||
categories: rub.categories.map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
row =>
|
||||
({
|
||||
...row,
|
||||
sharePercent: null,
|
||||
value: row.type === "OTHER" ? "25000" : row.value,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
hasNegativeValues: true,
|
||||
totalValue: "1150000",
|
||||
|
||||
+2
-5
@@ -27,7 +27,6 @@ const ui = (structure: null | Portfolio["structure"] | undefined) => (
|
||||
describe("portfolio structure", () => {
|
||||
it.each([
|
||||
undefined,
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
null,
|
||||
])(
|
||||
"keeps an older snapshot without structure available (%s) and recovers on refresh",
|
||||
@@ -74,13 +73,12 @@ describe("portfolio structure", () => {
|
||||
{
|
||||
...rub,
|
||||
categories: rub.categories.map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
row =>
|
||||
({
|
||||
...row,
|
||||
sharePercent: null,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -106,14 +104,13 @@ describe("portfolio structure", () => {
|
||||
{
|
||||
...rub,
|
||||
categories: rub.categories.map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
row =>
|
||||
({
|
||||
...row,
|
||||
sharePercent: null,
|
||||
value: row.type === "STOCK" ? "-60" : row.value,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
hasNegativeValues: true,
|
||||
},
|
||||
|
||||
+1
-3
@@ -97,9 +97,7 @@ const PortfolioStructureView: FC<PortfolioStructureViewProps> = (props) => {
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{!!props.negative && (
|
||||
<Text size="sm">{t("portfolio.structure.negative")}</Text>
|
||||
)}
|
||||
{!!props.negative && <Text size="sm">{t("portfolio.structure.negative")}</Text>}
|
||||
|
||||
{!!(
|
||||
!props.unavailable
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ const usePortfolioStructureVm = (props: PortfolioStructureProps) => {
|
||||
return {
|
||||
chartData,
|
||||
currencies: structure?.currencies.map(item => item.currency) ?? [],
|
||||
// eslint-disable-next-line unicorn/no-null -- Mantine clears the controlled currency with null.
|
||||
|
||||
currency: selected?.currency ?? null,
|
||||
incomplete: isIncomplete,
|
||||
negative: selected?.hasNegativeValues ?? false,
|
||||
|
||||
@@ -4,8 +4,6 @@ import type { AccountPagePosition } from "../../AccountPage.vm";
|
||||
|
||||
import { PositionCard } from "./PositionCard.view";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const syntheticPosition
|
||||
= {
|
||||
absoluteReturn: { currency: "RUB", value: "5000" },
|
||||
@@ -22,7 +20,7 @@ const syntheticPosition
|
||||
shareText: "20%",
|
||||
ticker: "DEMO",
|
||||
type: "STOCK",
|
||||
} satisfies /* eslint-enable unicorn/no-null */ AccountPagePosition;
|
||||
} satisfies AccountPagePosition;
|
||||
|
||||
const meta = {
|
||||
args: { kind: "stocks", position: syntheticPosition },
|
||||
@@ -62,8 +60,6 @@ export const StockGain: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const BondLoss: Story
|
||||
= {
|
||||
args: {
|
||||
@@ -94,9 +90,6 @@ export const BondLoss: Story
|
||||
},
|
||||
},
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const Other: Story
|
||||
= {
|
||||
@@ -120,9 +113,6 @@ export const Other: Story
|
||||
},
|
||||
},
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const MissingValuation: Story
|
||||
= {
|
||||
@@ -149,4 +139,3 @@ export const MissingValuation: Story
|
||||
},
|
||||
},
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
@@ -60,10 +60,10 @@ const PositionCard: FC<PositionCardProps> = (props) => {
|
||||
? t("portfolio.avgPrice", {
|
||||
value: formatMoney(position.averagePurchasePrice),
|
||||
})
|
||||
: null, // eslint-disable-line unicorn/no-null -- Mantine represents an empty controlled field as null.
|
||||
: null,
|
||||
position.shareText
|
||||
? t("portfolio.share", { value: position.shareText })
|
||||
: null, // eslint-disable-line unicorn/no-null -- Mantine represents an empty controlled field as null.
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
|
||||
@@ -44,9 +44,7 @@ const PositionGroupCard: FC<PositionGroupCardProps> = (props) => {
|
||||
{identity.displayName}
|
||||
</Anchor>
|
||||
)
|
||||
: (
|
||||
<Text fw={600}>{identity.displayName}</Text>
|
||||
)}
|
||||
: <Text fw={600}>{identity.displayName}</Text>}
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{identity.ticker ?? identity.isin ?? identity.figi}
|
||||
@@ -90,9 +88,7 @@ const PositionGroupCard: FC<PositionGroupCardProps> = (props) => {
|
||||
</Group>
|
||||
|
||||
<Group gap="xs" justify="space-between">
|
||||
{!!position.currentValue && (
|
||||
<Text size="sm">{formatMoney(position.currentValue)}</Text>
|
||||
)}
|
||||
{!!position.currentValue && <Text size="sm">{formatMoney(position.currentValue)}</Text>}
|
||||
|
||||
{!!position.returnText && (
|
||||
<Text className={appearanceClass} size="sm">
|
||||
|
||||
@@ -17,9 +17,7 @@ const PositionLogo: FC<PositionLogoProps> = (props) => {
|
||||
|
||||
const logo
|
||||
= src && !failed
|
||||
? (
|
||||
<Avatar alt={label} onError={onError} radius="md" size={40} src={src} />
|
||||
)
|
||||
? <Avatar alt={label} onError={onError} radius="md" size={40} src={src} />
|
||||
: (
|
||||
<Avatar radius="md" size={40}>
|
||||
{(label.trim()[0] ?? "?").toUpperCase()}
|
||||
|
||||
@@ -32,31 +32,21 @@ const RecentOperations: FC<RecentOperationsProps> = (props) => {
|
||||
|
||||
{!!operations.stale && <Text size="xs">{t("investment.stale")}</Text>}
|
||||
|
||||
{!!operations.refreshError && (
|
||||
<Text role="status">{t("accounts.refreshError")}</Text>
|
||||
)}
|
||||
{!!operations.refreshError && <Text role="status">{t("accounts.refreshError")}</Text>}
|
||||
</>
|
||||
)}
|
||||
|
||||
{operations.state === "loading" && (
|
||||
<LoadingSkeleton label={t("ledger.recentLoading")}>
|
||||
{["first", "second", "third"].map(placeholder => (
|
||||
<Skeleton height={48} key={placeholder} radius="sm" />
|
||||
))}
|
||||
{["first", "second", "third"].map(placeholder => <Skeleton height={48} key={placeholder} radius="sm" />)}
|
||||
</LoadingSkeleton>
|
||||
)}
|
||||
|
||||
{operations.state === "error" && (
|
||||
<Alert color="red">{t("ledger.recentError")}</Alert>
|
||||
)}
|
||||
{operations.state === "error" && <Alert color="red">{t("ledger.recentError")}</Alert>}
|
||||
|
||||
{operations.state === "empty" && (
|
||||
<Text c="dimmed">{t("ledger.recentEmpty")}</Text>
|
||||
)}
|
||||
{operations.state === "empty" && <Text c="dimmed">{t("ledger.recentEmpty")}</Text>}
|
||||
|
||||
{operations.state === "partial" && (
|
||||
<Text role="status">{t("investment.partial")}</Text>
|
||||
)}
|
||||
{operations.state === "partial" && <Text role="status">{t("investment.partial")}</Text>}
|
||||
|
||||
{(operations.state === "loaded" || operations.state === "partial")
|
||||
&& operations.items.map(operation => (
|
||||
|
||||
@@ -8,8 +8,6 @@ import type { Portfolio } from "@/entities/portfolio";
|
||||
import { toAccountPagePositionSections } from "./AccountPage.vm";
|
||||
import { PositionSection } from "./components/PositionSection.view";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const position: Portfolio["positions"][number]
|
||||
= {
|
||||
absoluteReturn: { currency: "RUB", value: "25" },
|
||||
@@ -25,8 +23,6 @@ const position: Portfolio["positions"][number]
|
||||
type: "STOCK",
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
const second
|
||||
= {
|
||||
...position,
|
||||
@@ -37,7 +33,6 @@ const second
|
||||
quantity: "-2",
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
function sections(positions: Portfolio["positions"]) {
|
||||
return toAccountPagePositionSections({
|
||||
cashBalances: [],
|
||||
@@ -84,30 +79,30 @@ describe("Positions grouped by security", () => {
|
||||
|
||||
it("uses ISIN only when FIGI is absent, never merging by name or ticker", () => {
|
||||
expect(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
sections([
|
||||
{
|
||||
...position,
|
||||
figi: null,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */ { ...second, figi: null }, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
{ ...second, figi: null },
|
||||
])[0].groups,
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
sections([position, { ...second, figi: "OTHER-FIGI" }])[0].groups,
|
||||
).toHaveLength(2);
|
||||
expect(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
sections([
|
||||
{
|
||||
...position,
|
||||
figi: null,
|
||||
isin: null,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */ {
|
||||
{
|
||||
...second,
|
||||
figi: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
isin: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
figi: null,
|
||||
isin: null,
|
||||
},
|
||||
])[0].groups,
|
||||
).toHaveLength(2);
|
||||
|
||||
@@ -31,7 +31,7 @@ it("keeps expanded positions vertical while other positions adapt to the viewpor
|
||||
const otherType = kind === "bonds" ? "BOND" : "FUND";
|
||||
|
||||
const positions: AccountPagePosition[] = [1, 2, 3].map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
index =>
|
||||
({
|
||||
absoluteReturn: null,
|
||||
@@ -48,7 +48,6 @@ it("keeps expanded positions vertical while other positions adapt to the viewpor
|
||||
ticker: null,
|
||||
type: kind === "stocks" ? "STOCK" : otherType,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
|
||||
const { unmount } = render(
|
||||
|
||||
Reference in New Issue
Block a user