refactor!: support RUB only (WealthLedger/main#55) (#168)
This commit was merged in pull request #168.
This commit is contained in:
@@ -96,7 +96,7 @@ function renderPage(ui: React.ReactNode) {
|
||||
|
||||
const portfolio
|
||||
= {
|
||||
cashBalances: [{ currency: "USD", value: "0.5" }],
|
||||
cash: { currency: "RUB", value: "0.5" },
|
||||
isStale: true,
|
||||
lastSuccessfulAt: "2026-08-13T10:00:00Z",
|
||||
positions: [
|
||||
@@ -129,8 +129,8 @@ const portfolio
|
||||
type: "BOND",
|
||||
},
|
||||
] satisfies [Portfolio["positions"][number], ...Portfolio["positions"]],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
totalsByCurrency: [{ currency: "RUB", value: "1234.5" }],
|
||||
structure: { categories: [], hasNegativeValues: false, totalValue: "0", unvaluedCategories: [] },
|
||||
totals: { currency: "RUB", value: "1234.5" },
|
||||
} satisfies Portfolio;
|
||||
|
||||
function position(
|
||||
@@ -201,8 +201,6 @@ const LoadedHarness = (
|
||||
function loadedProps(nextPortfolio: Portfolio = portfolio) {
|
||||
return {
|
||||
concentration: null,
|
||||
currency: null,
|
||||
onCurrencyChange: vi.fn(),
|
||||
onSliceChange: vi.fn(),
|
||||
portfolio: nextPortfolio,
|
||||
positionSections: toAccountPagePositionSections(nextPortfolio),
|
||||
@@ -242,10 +240,10 @@ describe("AccountPage states", () => {
|
||||
HttpResponse.json(resource(
|
||||
{
|
||||
...portfolio,
|
||||
cashBalances: [],
|
||||
cash: { currency: "RUB", value: "0" },
|
||||
isStale: false,
|
||||
positions: [],
|
||||
totalsByCurrency: [],
|
||||
totals: { currency: "RUB", value: "0" },
|
||||
},
|
||||
{
|
||||
lastAttempt: {
|
||||
@@ -278,7 +276,7 @@ describe("AccountPage states", () => {
|
||||
const data = { ...portfolio, positions };
|
||||
|
||||
const model = toConcentration(
|
||||
data, "stocks", "RUB", concentrationLabels(),
|
||||
data, "stocks", concentrationLabels(),
|
||||
);
|
||||
|
||||
const props = loadedProps(data);
|
||||
@@ -287,7 +285,6 @@ describe("AccountPage states", () => {
|
||||
{...props}
|
||||
accountId="a"
|
||||
concentration={model}
|
||||
currency="RUB"
|
||||
tab="concentration"
|
||||
/>,
|
||||
);
|
||||
@@ -326,10 +323,8 @@ describe("AccountPage states", () => {
|
||||
concentration={toConcentration(
|
||||
{ ...portfolio, positions },
|
||||
"stocks",
|
||||
"RUB",
|
||||
concentrationLabels(),
|
||||
)}
|
||||
currency="RUB"
|
||||
tab="concentration"
|
||||
/>,
|
||||
);
|
||||
@@ -354,9 +349,8 @@ describe("AccountPage states", () => {
|
||||
{...loadedProps(data)}
|
||||
accountId="a"
|
||||
concentration={toConcentration(
|
||||
data, "stocks", "RUB", concentrationLabels(),
|
||||
data, "stocks", concentrationLabels(),
|
||||
)}
|
||||
currency="RUB"
|
||||
tab="concentration"
|
||||
/>,
|
||||
);
|
||||
@@ -370,9 +364,8 @@ describe("AccountPage states", () => {
|
||||
{...loadedProps(data)}
|
||||
accountId="a"
|
||||
concentration={toConcentration(
|
||||
data, "issuers", "RUB", concentrationLabels(),
|
||||
data, "issuers", concentrationLabels(),
|
||||
)}
|
||||
currency="RUB"
|
||||
slice="issuers"
|
||||
tab="concentration"
|
||||
/>,
|
||||
@@ -392,9 +385,8 @@ describe("AccountPage states", () => {
|
||||
{...loadedProps(data)}
|
||||
accountId="a"
|
||||
concentration={toConcentration(
|
||||
data, "stocks", "RUB", concentrationLabels(),
|
||||
data, "stocks", concentrationLabels(),
|
||||
)}
|
||||
currency="RUB"
|
||||
tab="concentration"
|
||||
/>,
|
||||
);
|
||||
@@ -424,9 +416,8 @@ describe("AccountPage states", () => {
|
||||
{...loadedProps(data)}
|
||||
accountId="a"
|
||||
concentration={toConcentration(
|
||||
data, "sectors", "RUB", concentrationLabels(),
|
||||
data, "sectors", concentrationLabels(),
|
||||
)}
|
||||
currency="RUB"
|
||||
slice="sectors"
|
||||
tab="concentration"
|
||||
/>,
|
||||
@@ -457,9 +448,8 @@ describe("AccountPage states", () => {
|
||||
{...loadedProps(data)}
|
||||
accountId="a"
|
||||
concentration={toConcentration(
|
||||
data, "stocks", "RUB", concentrationLabels(),
|
||||
data, "stocks", concentrationLabels(),
|
||||
)}
|
||||
currency="RUB"
|
||||
tab="concentration"
|
||||
/>,
|
||||
);
|
||||
@@ -484,9 +474,8 @@ describe("AccountPage states", () => {
|
||||
{...loadedProps(data)}
|
||||
accountId="a"
|
||||
concentration={toConcentration(
|
||||
data, "stocks", "RUB", concentrationLabels(),
|
||||
data, "stocks", concentrationLabels(),
|
||||
)}
|
||||
currency="RUB"
|
||||
partial
|
||||
tab="concentration"
|
||||
/>,
|
||||
@@ -496,16 +485,14 @@ describe("AccountPage states", () => {
|
||||
.toHaveLength(1);
|
||||
});
|
||||
|
||||
it("keeps slice and currency while switching page tabs and resets on account change", async () => {
|
||||
it("keeps the slice while switching page tabs and resets on account change", async () => {
|
||||
const multiCurrency = { ...portfolio, positions:
|
||||
["RUB", "USD"].flatMap(currency =>
|
||||
[400, 250, 150, 100, 50, 30, 20].map((value, index) =>
|
||||
position({
|
||||
currentValue: { currency, value: String(value) },
|
||||
displayName: `${currency} акция ${index + 1}`,
|
||||
sector: "energy", type: "STOCK",
|
||||
}),
|
||||
),
|
||||
[400, 250, 150, 100, 50, 30, 20].map((value, index) =>
|
||||
position({
|
||||
currentValue: { currency: "RUB", value: String(value) },
|
||||
displayName: `Акция ${index + 1}`,
|
||||
sector: "energy", type: "STOCK",
|
||||
}),
|
||||
) };
|
||||
|
||||
server.use(
|
||||
@@ -520,37 +507,27 @@ describe("AccountPage states", () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: /Прочее/ }));
|
||||
expect(screen.getByRole("button", { name: /Прочее/ }))
|
||||
.toHaveAttribute("aria-expanded", "true");
|
||||
fireEvent.click(screen.getByRole("combobox", { name: "Валюта концентрации" }));
|
||||
fireEvent.click(screen.getByRole("option", { name: "USD" }));
|
||||
expect(screen.getByRole("button", { name: /Прочее/ }))
|
||||
.toHaveAttribute("aria-expanded", "false");
|
||||
fireEvent.click(screen.getByRole("button", { name: /Прочее/ }));
|
||||
fireEvent.click(screen.getByRole("radio", { name: "Секторы" }));
|
||||
fireEvent.click(screen.getByRole("radio", { name: "Акции" }));
|
||||
expect(screen.getByRole("button", { name: /Прочее/ }))
|
||||
.toHaveAttribute("aria-expanded", "false");
|
||||
fireEvent.click(screen.getByRole("radio", { name: "Секторы" }));
|
||||
expect(screen.getByRole("combobox", { name: "Валюта концентрации" }))
|
||||
.toHaveValue("USD");
|
||||
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Позиции" }));
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Концентрация" }));
|
||||
|
||||
expect(screen.getByRole("radio", { name: "Секторы" }))
|
||||
.toBeChecked();
|
||||
expect(screen.getByRole("combobox", { name: "Валюта концентрации" }))
|
||||
.toHaveValue("USD");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Другой счёт" }));
|
||||
fireEvent.click(await screen.findByRole("tab", { name: "Концентрация" }));
|
||||
expect(screen.getByRole("radio", { name: "Акции" })).toBeChecked();
|
||||
expect(screen.getByRole("combobox", { name: "Валюта концентрации" }))
|
||||
.toHaveValue("RUB");
|
||||
});
|
||||
|
||||
it("falls back to the first currency when the selected one disappears", () => {
|
||||
it("includes all ruble positions in concentration", () => {
|
||||
const data = { ...portfolio, positions: [
|
||||
position({
|
||||
currentValue: { currency: "USD", value: "200" },
|
||||
currentValue: { currency: "RUB", value: "200" },
|
||||
}),
|
||||
position({
|
||||
currentValue: { currency: "RUB", value: "100" },
|
||||
@@ -558,12 +535,11 @@ describe("AccountPage states", () => {
|
||||
] };
|
||||
|
||||
const model = toConcentration(
|
||||
data, "stocks", "EUR", concentrationLabels(),
|
||||
data, "stocks", concentrationLabels(),
|
||||
);
|
||||
|
||||
expect(model.currencies).toEqual(["RUB", "USD"]);
|
||||
expect(model.total).toBe("100");
|
||||
expect(model.items.map(item => item.amount)).toEqual(["100"]);
|
||||
expect(model.total).toBe("300");
|
||||
expect(model.items.map(item => item.amount)).toEqual(["200", "100"]);
|
||||
});
|
||||
it("shows the loaded portfolio source sync status", async () => {
|
||||
server.use(
|
||||
@@ -679,7 +655,7 @@ describe("AccountPage states", () => {
|
||||
).toHaveTextContent("Данные о структуре портфеля пока недоступны.");
|
||||
expect(
|
||||
screen.getAllByRole("definition").map(node => node.textContent),
|
||||
).toEqual(["1 234,5 ₽"]);
|
||||
).toEqual(["1 234,50 ₽"]);
|
||||
});
|
||||
it("keeps initial placeholders decorative without amounts or focus targets", () => {
|
||||
const { container } = renderPage(<AccountPageLoading />);
|
||||
@@ -722,11 +698,7 @@ describe("AccountPage states", () => {
|
||||
<LoadedHarness
|
||||
{...loadedProps({
|
||||
...portfolio,
|
||||
cashBalances: [
|
||||
{ currency: "RUB", value: "234.500000000000000001" },
|
||||
{ currency: "USD", value: "-0.5" },
|
||||
{ currency: "XYZ", value: "3.00" },
|
||||
],
|
||||
cash: { currency: "RUB", value: "234.500000000000000001" },
|
||||
positions: [
|
||||
...portfolio.positions,
|
||||
{
|
||||
@@ -737,7 +709,7 @@ describe("AccountPage states", () => {
|
||||
type: "CURRENCY",
|
||||
},
|
||||
],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
structure: { categories: [], hasNegativeValues: false, totalValue: "0", unvaluedCategories: [] },
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
@@ -750,14 +722,12 @@ describe("AccountPage states", () => {
|
||||
|
||||
expect(
|
||||
balances.getAllByRole("definition").map(node => node.textContent),
|
||||
).toEqual(["234,500000000000000001 ₽", "-0,5 $", "3,00 XYZ"]);
|
||||
).toEqual(["234,50 ₽"]);
|
||||
expect(
|
||||
screen.getAllByRole("definition").map(node => node.textContent),
|
||||
).toEqual([
|
||||
"1 234,5 ₽",
|
||||
"234,500000000000000001 ₽",
|
||||
"-0,5 $",
|
||||
"3,00 XYZ",
|
||||
"1 234,50 ₽",
|
||||
"234,50 ₽",
|
||||
]);
|
||||
expect(screen.queryByText("Торгуемая валюта")).not.toBeInTheDocument();
|
||||
expect(
|
||||
@@ -774,7 +744,7 @@ describe("AccountPage states", () => {
|
||||
resource({
|
||||
...portfolio,
|
||||
positions: hasPositions ? portfolio.positions : [],
|
||||
totalsByCurrency: [],
|
||||
totals: { currency: "RUB", value: "0" },
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -782,20 +752,20 @@ describe("AccountPage states", () => {
|
||||
renderAccountRoute(createTestQueryClient());
|
||||
await screen.findByRole("heading", { level: 1, name: "Портфель" });
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Деньги" }));
|
||||
expect(screen.getByRole("definition")).toHaveTextContent("0,5 $");
|
||||
expect(screen.getByText("0,50 ₽")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Портфель пока пуст")).not.toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ totalsByCurrency: [] },
|
||||
{ totalsByCurrency: [{ currency: "RUB", value: "100" }] },
|
||||
{ empty: true, totals: { currency: "RUB", value: "0" } },
|
||||
{ empty: false, totals: { currency: "RUB", value: "100" } },
|
||||
])(
|
||||
"hides the empty other section without emptying a currency-only portfolio (totals: $totalsByCurrency)",
|
||||
async ({ totalsByCurrency }) => {
|
||||
"hides the empty other section without emptying a currency-only portfolio (totals: $totals.value)",
|
||||
async ({ empty, totals }) => {
|
||||
const snapshot: Portfolio = {
|
||||
...portfolio,
|
||||
cashBalances: [],
|
||||
cash: { currency: "RUB", value: "0" },
|
||||
positions: [
|
||||
{
|
||||
...portfolio.positions[0],
|
||||
@@ -804,8 +774,8 @@ describe("AccountPage states", () => {
|
||||
type: "CURRENCY",
|
||||
},
|
||||
],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
totalsByCurrency,
|
||||
structure: { categories: [], hasNegativeValues: false, totalValue: "0", unvaluedCategories: [] },
|
||||
totals,
|
||||
};
|
||||
|
||||
server.use(
|
||||
@@ -826,11 +796,11 @@ describe("AccountPage states", () => {
|
||||
expect(
|
||||
within(
|
||||
screen.getByRole("tabpanel", { name: "Деньги" }),
|
||||
).queryByRole("definition"),
|
||||
).not.toBeInTheDocument();
|
||||
).getByRole("definition"),
|
||||
).toHaveTextContent("0,00 ₽");
|
||||
expect(
|
||||
screen.queryAllByRole("definition").map(node => node.textContent),
|
||||
).toEqual(totalsByCurrency.length > 0 ? ["100 ₽"] : []);
|
||||
).toEqual([empty ? "0,00 ₽" : "100,00 ₽", "0,00 ₽"]);
|
||||
expect(
|
||||
queryClient.getQueryData(portfolioQueryOptions("account-1").queryKey),
|
||||
).toEqual(resource(snapshot));
|
||||
@@ -1147,7 +1117,7 @@ describe("AccountPage states", () => {
|
||||
date: "2026-09-11",
|
||||
dateKind: "registry",
|
||||
displayName: "Акция",
|
||||
projectedAmount: { currency: "USD", value: "0" },
|
||||
projectedAmount: { currency: "RUB", value: "0" },
|
||||
type: "dividend",
|
||||
},
|
||||
{
|
||||
@@ -1191,7 +1161,7 @@ describe("AccountPage states", () => {
|
||||
expect(
|
||||
content.getByText("11.09.2026 · Дата реестра · RU000A0JX0J2"),
|
||||
).toBeVisible();
|
||||
for (const amount of ["850,50 ₽", "0 $", "1 000 ₽", "Сумма неизвестна"])
|
||||
for (const amount of ["850,50 ₽", "0 ₽", "1 000 ₽", "Сумма неизвестна"])
|
||||
expect(content.getByText(amount)).toBeVisible();
|
||||
expect(content.getAllByText("Прогноз")).toHaveLength(4);
|
||||
expect(content.queryByText("Без движения денег")).not.toBeInTheDocument();
|
||||
@@ -1277,7 +1247,7 @@ describe("AccountPage states", () => {
|
||||
return HttpResponse.json(resource(
|
||||
requests === 1
|
||||
? portfolio
|
||||
: { ...portfolio, totalsByCurrency: [{ currency: "RUB", value: "999" }] },
|
||||
: { ...portfolio, totals: { currency: "RUB", value: "999" } },
|
||||
{ resourceKey: "portfolio" },
|
||||
));
|
||||
}),
|
||||
@@ -1301,14 +1271,14 @@ describe("AccountPage states", () => {
|
||||
|
||||
expect(screen.getByText("Не удалось обновить данные")).toBeInTheDocument();
|
||||
expect(screen.getByText("Вторая")).toBeInTheDocument();
|
||||
expect(screen.getByText("1 234,5 ₽")).toBeInTheDocument();
|
||||
expect(screen.getByText("1 234,50 ₽")).toBeInTheDocument();
|
||||
// One cached read plus the three automatic attempts.
|
||||
expect(requests).toBe(4);
|
||||
|
||||
isFailing = false;
|
||||
fireEvent.click(retry);
|
||||
|
||||
expect(await screen.findByText("999 ₽")).toBeInTheDocument();
|
||||
expect(await screen.findByText("999,00 ₽")).toBeInTheDocument();
|
||||
expect(requests).toBe(5);
|
||||
expect(screen.queryByText("Не удалось обновить данные"))
|
||||
.not.toBeInTheDocument();
|
||||
@@ -1370,14 +1340,9 @@ describe("AccountPage states", () => {
|
||||
= {
|
||||
...portfolio,
|
||||
structure: {
|
||||
currencies: [
|
||||
{
|
||||
categories: [{ sharePercent: null, type: "BOND", value: "10" }],
|
||||
currency: "RUB",
|
||||
hasNegativeValues: false,
|
||||
totalValue: "10",
|
||||
},
|
||||
],
|
||||
categories: [{ sharePercent: null, type: "BOND", value: "10" }],
|
||||
hasNegativeValues: false,
|
||||
totalValue: "10",
|
||||
unvaluedCategories: [{ count: 1, type: "STOCK" }],
|
||||
},
|
||||
};
|
||||
@@ -1428,7 +1393,7 @@ describe("AccountPage states", () => {
|
||||
expect(screen.getByText("Вторая")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getAllByRole("definition").map(node => node.textContent),
|
||||
).toEqual(["1 234,5 ₽"]);
|
||||
).toEqual(["1 234,50 ₽"]);
|
||||
expect(screen.queryByRole("button", { name: "Повторить" }))
|
||||
.not.toBeInTheDocument();
|
||||
void queryClient.invalidateQueries({
|
||||
@@ -1438,17 +1403,17 @@ describe("AccountPage states", () => {
|
||||
expect(structure).toHaveTextContent("10 ₽");
|
||||
expect(
|
||||
screen.getAllByRole("definition").map(node => node.textContent),
|
||||
).toEqual(["1 234,5 ₽"]);
|
||||
).toEqual(["1 234,50 ₽"]);
|
||||
resolveRetry?.(
|
||||
HttpResponse.json(
|
||||
resource(
|
||||
{
|
||||
...portfolio,
|
||||
cashBalances: [{ currency: "EUR", value: "2.50" }],
|
||||
cash: { currency: "RUB", value: "2.50" },
|
||||
isStale: false,
|
||||
lastSuccessfulAt: "2026-08-14T11:30:00Z",
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
totalsByCurrency: [{ currency: "USD", value: "-0.5" }],
|
||||
structure: { categories: [], hasNegativeValues: false, totalValue: "0", unvaluedCategories: [] },
|
||||
totals: { currency: "RUB", value: "-0.5" },
|
||||
},
|
||||
{ dataRevision: "2", resourceKey: "portfolio" },
|
||||
),
|
||||
@@ -1457,7 +1422,7 @@ describe("AccountPage states", () => {
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getAllByRole("definition").map(node => node.textContent),
|
||||
).toEqual(["-0,5 $"]),
|
||||
).toEqual(["-0,50 ₽"]),
|
||||
);
|
||||
expect(screen.getByText(/^Данные на /)).toHaveTextContent("Данные на");
|
||||
expect(structure).not.toHaveTextContent("10 ₽");
|
||||
@@ -1499,7 +1464,7 @@ describe("AccountPage states", () => {
|
||||
expect(screen.getByText("Вторая")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getAllByRole("definition").map(node => node.textContent),
|
||||
).toEqual(["1 234,5 ₽"]);
|
||||
).toEqual(["1 234,50 ₽"]);
|
||||
expect(screen.getByText(/^Данные на /)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("status", { name: "Загрузка" }),
|
||||
|
||||
Reference in New Issue
Block a user