refactor: enforce frontend view model boundaries
This commit is contained in:
+13
-9
@@ -64,8 +64,10 @@ export const Available: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Unavailable: Story = (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const Unavailable: Story
|
||||
= {
|
||||
args: {
|
||||
accounts: brokerageAccountCards([
|
||||
{ ...syntheticAccount, openedOn: null, portfolioSummary: null },
|
||||
@@ -76,12 +78,12 @@ export const Unavailable: Story = (/* eslint-disable unicorn/no-null -- Fixture
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Счёт остаётся доступен для открытия, даже если сводка временно недоступна. Неизвестные суммы не заменяются нулём; отсутствующая дата открытия не отображается.",
|
||||
"Счёт остаётся доступен для открытия, даже если сводка временно недоступна. Неизвестные суммы не заменяются нулём; отсутствующая дата открытия не отображается.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */);
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
@@ -191,8 +193,10 @@ export const LargeValues: Story = {
|
||||
name: "Длинные суммы",
|
||||
};
|
||||
|
||||
export const IndependentFreshness: Story = (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const IndependentFreshness: Story
|
||||
= {
|
||||
args: {
|
||||
accounts: brokerageAccountCards(
|
||||
[
|
||||
@@ -231,5 +235,5 @@ export const IndependentFreshness: Story = (/* eslint-disable unicorn/no-null --
|
||||
),
|
||||
},
|
||||
name: "Независимая свежесть и ошибка сводок",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */);
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
+40
-47
@@ -29,29 +29,26 @@ describe("BrokerageAccountList", () => {
|
||||
],
|
||||
},
|
||||
type: "BROKERAGE",
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
},
|
||||
{
|
||||
id: "account-2",
|
||||
name: "Недоступный",
|
||||
openedOn: null,
|
||||
portfolioSummary: null,
|
||||
type: "BROKERAGE",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */ {
|
||||
id: "account-3",
|
||||
name: "Пустой",
|
||||
openedOn: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
portfolioSummary: {
|
||||
bondPositionCount: 0,
|
||||
stockPositionCount: 2,
|
||||
totalsByCurrency: [],
|
||||
},
|
||||
type: "BROKERAGE",
|
||||
},
|
||||
(/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
id: "account-2",
|
||||
name: "Недоступный",
|
||||
openedOn: null,
|
||||
portfolioSummary: null,
|
||||
type: "BROKERAGE",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */),
|
||||
(/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
id: "account-3",
|
||||
name: "Пустой",
|
||||
openedOn: null,
|
||||
portfolioSummary: {
|
||||
bondPositionCount: 0,
|
||||
stockPositionCount: 2,
|
||||
totalsByCurrency: [],
|
||||
},
|
||||
type: "BROKERAGE",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */),
|
||||
])}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
@@ -71,19 +68,11 @@ describe("BrokerageAccountList", () => {
|
||||
expect(screen.getByText("Нет текущих сумм")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Открыть счет")).toHaveLength(3);
|
||||
const available = within(screen.getByRole("link", { name: /Основной/ }));
|
||||
expect(available.getAllByRole("definition")[0]).toHaveTextContent(
|
||||
"5",
|
||||
);
|
||||
expect(
|
||||
available.getAllByRole("definition")[1],
|
||||
).toHaveTextContent("12");
|
||||
expect(available.getAllByRole("definition")[0]).toHaveTextContent("5");
|
||||
expect(available.getAllByRole("definition")[1]).toHaveTextContent("12");
|
||||
const unvalued = within(screen.getByRole("link", { name: /Пустой/ }));
|
||||
expect(unvalued.getAllByRole("definition")[0]).toHaveTextContent(
|
||||
"2",
|
||||
);
|
||||
expect(
|
||||
unvalued.getAllByRole("definition")[1],
|
||||
).toHaveTextContent("0");
|
||||
expect(unvalued.getAllByRole("definition")[0]).toHaveTextContent("2");
|
||||
expect(unvalued.getAllByRole("definition")[1]).toHaveTextContent("0");
|
||||
expect(
|
||||
within(screen.getByRole("link", { name: /Недоступный/ })).queryByText(
|
||||
"Акции",
|
||||
@@ -94,19 +83,22 @@ describe("BrokerageAccountList", () => {
|
||||
|
||||
it("shows portfolio summary freshness independently for each account", () => {
|
||||
const accounts = brokerageAccountCards(
|
||||
["fresh", "stale"].map(id => ((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
id,
|
||||
name: id,
|
||||
openedOn: null,
|
||||
portfolioSummary: {
|
||||
bondPositionCount: 0,
|
||||
stockPositionCount: 1,
|
||||
totalsByCurrency: [{ currency: "RUB", value: "10" }],
|
||||
},
|
||||
type: "BROKERAGE" as const,
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */))),
|
||||
["fresh", "stale"].map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
id =>
|
||||
({
|
||||
id,
|
||||
name: id,
|
||||
openedOn: null,
|
||||
portfolioSummary: {
|
||||
bondPositionCount: 0,
|
||||
stockPositionCount: 1,
|
||||
totalsByCurrency: [{ currency: "RUB", value: "10" }],
|
||||
},
|
||||
type: "BROKERAGE" as const,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
[
|
||||
resource(null, { freshness: "fresh", resourceKey: "portfolio/fresh" })
|
||||
.meta,
|
||||
@@ -114,6 +106,7 @@ it("shows portfolio summary freshness independently for each account", () => {
|
||||
.meta,
|
||||
],
|
||||
);
|
||||
|
||||
render(
|
||||
<MantineProvider>
|
||||
<MemoryRouter>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { Card, Group, SimpleGrid, Stack, Text } from "@mantine/core";
|
||||
import { WalletIcon } from "@wealthledger/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
|
||||
@@ -40,13 +41,14 @@ const BrokerageAccountSummary: FC<BrokerageAccountSummaryProps> = (props) => {
|
||||
{formatMoney(total)}
|
||||
</Text>
|
||||
))}
|
||||
{!!(summary.remainingCurrencyCount) && ((
|
||||
|
||||
{!!summary.remainingCurrencyCount && (
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("accounts.remainingCurrencies", {
|
||||
count: summary.remainingCurrencyCount,
|
||||
})}
|
||||
</Text>
|
||||
))}
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -72,76 +74,78 @@ const BrokerageAccountList: FC<BrokerageAccountListProps> = (props) => {
|
||||
<Text className={classes.kicker} fw={700} size="xs">
|
||||
{t(accountTypeKey(account.type))}
|
||||
</Text>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={classes.accountIcon}
|
||||
fill="none"
|
||||
focusable="false"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<rect height="15" rx="3" width="18" x="3" y="5" />
|
||||
<path d="M3 9h18M7 15h3M14 5V3H7a4 4 0 0 0-4 4" />
|
||||
</svg>
|
||||
|
||||
<WalletIcon className={classes.accountIcon} />
|
||||
</Group>
|
||||
|
||||
<Text className={classes.name} component="h2" fw={700}>
|
||||
{account.name}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack className={classes.balance} gap="xs">
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("accounts.currentValue")}
|
||||
</Text>
|
||||
|
||||
<BrokerageAccountSummary summary={summary} />
|
||||
{!!(source?.lastSuccessfulAt) && ((
|
||||
|
||||
{!!source?.lastSuccessfulAt && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("investment.sourceTime", {
|
||||
date: formatSnapshotTime(source.lastSuccessfulAt),
|
||||
})}
|
||||
</Text>
|
||||
))}
|
||||
)}
|
||||
|
||||
{(source?.sync.state === "running"
|
||||
|| source?.sync.state === "queued") && ((
|
||||
|| source?.sync.state === "queued") && (
|
||||
<Text role="status" size="xs">
|
||||
{t("accounts.refreshing")}
|
||||
</Text>
|
||||
))}
|
||||
{(source?.sync.state === "failed") && ((
|
||||
)}
|
||||
|
||||
{source?.sync.state === "failed" && (
|
||||
<Text size="xs">{t("accounts.refreshError")}</Text>
|
||||
))}
|
||||
{(source?.freshness === "stale") && ((
|
||||
)}
|
||||
|
||||
{source?.freshness === "stale" && (
|
||||
<Text size="xs">{t("investment.stale")}</Text>
|
||||
))}
|
||||
)}
|
||||
</Stack>
|
||||
{!!(positionCounts) && ((
|
||||
|
||||
{!!positionCounts && (
|
||||
<dl className={classes.positions}>
|
||||
<div className={classes.stocks}>
|
||||
<dt>{t("accounts.stocks")}</dt>
|
||||
|
||||
<dd>
|
||||
{new Intl.NumberFormat("ru-RU").format(positionCounts.stocks)}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div className={classes.bonds}>
|
||||
<dt>{t("accounts.bonds")}</dt>
|
||||
|
||||
<dd>
|
||||
{new Intl.NumberFormat("ru-RU").format(positionCounts.bonds)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
))}
|
||||
)}
|
||||
|
||||
<Group className={classes.footer} gap="sm" justify="space-between">
|
||||
{!!(account.openedOn) && ((
|
||||
{!!account.openedOn && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("accounts.openedOn", {
|
||||
date: formatDate(account.openedOn),
|
||||
})}
|
||||
</Text>
|
||||
))}
|
||||
)}
|
||||
|
||||
<Text className={classes.open} component="span" fw={600} size="sm">
|
||||
{t("accounts.openAccount")}
|
||||
|
||||
<span aria-hidden="true" className={classes.arrow}>
|
||||
→
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user