feat(portfolio): extend instrument logo rendering to bonds

- Rename presentation prop stock→wide; BOND section passes wide alongside STOCK
- Add bond test cases: CDN logo, fallback, signed return, single-column grid
- Add other-type regression test (no logo, labeled return preserved)
- Update bundle baseline (+4 bytes)
This commit is contained in:
2026-08-25 09:25:29 +03:00
parent a6d3051270
commit c04025a392
3 changed files with 104 additions and 12 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
"src/app/routes/ProtectedLayout.tsx": 3137, "src/app/routes/ProtectedLayout.tsx": 3137,
"src/pages/account-events/index.ts": 4468, "src/pages/account-events/index.ts": 4468,
"src/pages/account-operations/index.ts": 13861, "src/pages/account-operations/index.ts": 13861,
"src/pages/account/index.ts": 21868, "src/pages/account/index.ts": 21872,
"src/pages/accounts/index.ts": 6174, "src/pages/accounts/index.ts": 6174,
"src/pages/login/index.ts": 669, "src/pages/login/index.ts": 669,
"src/pages/not-found/index.ts": 850, "src/pages/not-found/index.ts": 850,
+92 -1
View File
@@ -123,7 +123,8 @@ describe("AccountPage states", () => {
.map((element) => element.textContent); .map((element) => element.textContent);
expect(positions).toEqual(["Вторая", "Первая"]); expect(positions).toEqual(["Вторая", "Первая"]);
expect(screen.getByText("10 ₽")).toBeInTheDocument(); expect(screen.getByText("10 ₽")).toBeInTheDocument();
expect(screen.getByText("Доход: -1 ₽")).toBeInTheDocument(); expect(screen.getByText("-1 ₽")).toBeInTheDocument();
expect(screen.queryByText(/Доход:/)).not.toBeInTheDocument();
expect(screen.getByText("Средняя цена: 11 ₽")).toBeInTheDocument(); expect(screen.getByText("Средняя цена: 11 ₽")).toBeInTheDocument();
expect(screen.getByText("Доля: 100 %")).toBeInTheDocument(); expect(screen.getByText("Доля: 100 %")).toBeInTheDocument();
expect(screen.getByText("RU000A0JX0J2")).toBeInTheDocument(); expect(screen.getByText("RU000A0JX0J2")).toBeInTheDocument();
@@ -173,6 +174,96 @@ describe("AccountPage states", () => {
expect(screen.queryByRole("img", { name: "Абра" })).not.toBeInTheDocument(); expect(screen.queryByRole("img", { name: "Абра" })).not.toBeInTheDocument();
}); });
it("renders bond cards with logo, fallback, and signed unlabeled return like stocks", () => {
renderPage(
<AccountPageLoaded
portfolio={{
...portfolio,
positions: [
{
figi: "FIGI-C",
displayName: "Долг",
type: "BOND",
quantity: "5",
currentValue: { currency: "RUB", value: "50" },
averagePurchasePrice: null,
absoluteReturn: { currency: "RUB", value: "2" },
isin: "RU000C0XY1Z3",
logoName: "dolg.png",
},
{
figi: "FIGI-D",
displayName: "Купон",
type: "BOND",
quantity: "1",
currentValue: null,
averagePurchasePrice: null,
absoluteReturn: { currency: "RUB", value: "0" },
isin: null,
logoName: null,
},
],
}}
/>,
);
expect(screen.getByRole("img", { name: "Долг" })).toHaveAttribute(
"src",
"https://invest-brands.cdn-tinkoff.ru/dolgx160.png",
);
expect(screen.getByText("К")).toBeInTheDocument();
expect(
screen.queryByRole("img", { name: "Купон" }),
).not.toBeInTheDocument();
expect(screen.getByText("+2 ₽")).toBeInTheDocument();
expect(screen.getByText("0 ₽")).toBeInTheDocument();
expect(screen.queryByText(/Доход:/)).not.toBeInTheDocument();
});
it("renders the bonds section as a single-column grid on any screen width", () => {
renderPage(
<AccountPageLoaded
portfolio={{
...portfolio,
positions: [portfolio.positions[1]],
}}
/>,
);
const singleColumnGrids = Array.from(document.querySelectorAll("style"))
.map((style) => (style.textContent ?? "").replace(/\s/g, ""))
.filter(
(css) =>
css.includes("--sg-cols:1") &&
!css.includes("@media") &&
!/--sg-cols:[23]/.test(css),
);
expect(singleColumnGrids).toHaveLength(1);
});
it("keeps other-position cards compact with labeled return and no logo", () => {
renderPage(
<AccountPageLoaded
portfolio={{
...portfolio,
positions: [
{
figi: "FIGI-E",
displayName: "Фонд",
type: "FUND",
quantity: "3",
currentValue: { currency: "RUB", value: "30" },
averagePurchasePrice: null,
absoluteReturn: { currency: "RUB", value: "-5" },
isin: null,
logoName: "fond.png",
},
],
}}
/>,
);
expect(screen.getByText("Доход: -5 ₽")).toBeInTheDocument();
expect(screen.queryByRole("img", { name: "Фонд" })).not.toBeInTheDocument();
});
it("uses the cached account title when available", () => { it("uses the cached account title when available", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
+11 -10
View File
@@ -191,7 +191,7 @@ export function AccountPageLoaded({
title={t("portfolio.stocks")} title={t("portfolio.stocks")}
positions={stocks} positions={stocks}
denominators={denominators} denominators={denominators}
stock wide
/> />
) : null} ) : null}
{bonds.length ? ( {bonds.length ? (
@@ -199,6 +199,7 @@ export function AccountPageLoaded({
title={t("portfolio.bonds")} title={t("portfolio.bonds")}
positions={bonds} positions={bonds}
denominators={denominators} denominators={denominators}
wide
/> />
) : null} ) : null}
{other.length ? ( {other.length ? (
@@ -353,22 +354,22 @@ function PositionSection({
title, title,
positions, positions,
denominators, denominators,
stock = false, wide = false,
}: { }: {
title: string; title: string;
positions: Portfolio["positions"]; positions: Portfolio["positions"];
denominators: Map<string, number>; denominators: Map<string, number>;
stock?: boolean; wide?: boolean;
}) { }) {
return ( return (
<Section title={title}> <Section title={title}>
<SimpleGrid cols={stock ? 1 : { base: 1, sm: 2, md: 3 }}> <SimpleGrid cols={wide ? 1 : { base: 1, sm: 2, md: 3 }}>
{positions.map((position) => ( {positions.map((position) => (
<PositionCard <PositionCard
key={`${position.figi}-${position.displayName}-${position.quantity}`} key={`${position.figi}-${position.displayName}-${position.quantity}`}
position={position} position={position}
denominators={denominators} denominators={denominators}
stock={stock} wide={wide}
/> />
))} ))}
</SimpleGrid> </SimpleGrid>
@@ -378,11 +379,11 @@ function PositionSection({
function PositionCard({ function PositionCard({
position, position,
denominators, denominators,
stock = false, wide = false,
}: { }: {
position: Portfolio["positions"][number]; position: Portfolio["positions"][number];
denominators: Map<string, number>; denominators: Map<string, number>;
stock?: boolean; wide?: boolean;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const share = position.currentValue?.currency const share = position.currentValue?.currency
@@ -399,17 +400,17 @@ function PositionCard({
: undefined : undefined
: undefined; : undefined;
const returnText = position.absoluteReturn const returnText = position.absoluteReturn
? stock ? wide
? `${Number(position.absoluteReturn.value) > 0 ? "+" : ""}${formatMoney(position.absoluteReturn)}` ? `${Number(position.absoluteReturn.value) > 0 ? "+" : ""}${formatMoney(position.absoluteReturn)}`
: t("portfolio.return", { : t("portfolio.return", {
value: formatMoney(position.absoluteReturn), value: formatMoney(position.absoluteReturn),
}) })
: null; : null;
const logoSrc = stock ? instrumentLogoUrl(position.logoName) : null; const logoSrc = wide ? instrumentLogoUrl(position.logoName) : null;
return ( return (
<Card className={classes.tile} withBorder> <Card className={classes.tile} withBorder>
<Group justify="space-between" align="flex-start" wrap="nowrap"> <Group justify="space-between" align="flex-start" wrap="nowrap">
{stock ? ( {wide ? (
<PositionLogo <PositionLogo
key={logoSrc ?? "no-logo"} key={logoSrc ?? "no-logo"}
src={logoSrc} src={logoSrc}