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:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user