diff --git a/src/features/tbank-connection/ui/TbankConnectionSection/TbankConnectionSection.view.test.tsx b/src/features/tbank-connection/ui/TbankConnectionSection/TbankConnectionSection.view.test.tsx
deleted file mode 100644
index ec68a16..0000000
--- a/src/features/tbank-connection/ui/TbankConnectionSection/TbankConnectionSection.view.test.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { MantineProvider } from "@mantine/core";
-import { fireEvent, render, screen } from "@testing-library/react";
-import { describe, expect, it, vi } from "vitest";
-
-import {
- TbankConnectionError,
- TbankConnectionLoading,
-} from "./TbankConnectionSection.view";
-
-describe("TbankConnectionSection state views", () => {
- it("renders loading and retries a status error", () => {
- const retry = vi.fn();
-
- const { rerender } = render(
-
-
- ,
- );
-
- expect(screen.getByRole("status", { name: "Загрузка" })).toHaveAttribute(
- "aria-busy",
- "true",
- );
- rerender(
-
-
- ,
- );
- fireEvent.click(screen.getByRole("button", { name: "Повторить" }));
- expect(retry).toHaveBeenCalledOnce();
- });
-});
diff --git a/src/pages/account-income/model/display.test.ts b/src/pages/account-income/model/display.test.ts
index 01ce9e7..60b7248 100644
--- a/src/pages/account-income/model/display.test.ts
+++ b/src/pages/account-income/model/display.test.ts
@@ -44,6 +44,71 @@ function summary(currency: string, coupon: string, dividend: string) {
};
}
+describe("income membership validation", () => {
+ const entry: IncomeAnalytics["entries"][number] = {
+ accruedInterest: null,
+ category: "income",
+ commission: null,
+ description: "Купон",
+ figi: "FIGI",
+ id: "coupon-1",
+ instrumentKey: "income-group",
+ instrumentType: "bond",
+ instrumentUid: null,
+ logoName: null,
+ occurredAt: "2026-08-01T12:00:00Z",
+ payment: money("RUB", "10"),
+ price: null,
+ quantity: null,
+ ticker: "BOND",
+ type: "OPERATION_TYPE_COUPON",
+ };
+
+ const instrument: IncomeAnalytics["instruments"][number] = {
+ ...summary("RUB", "10", "0"),
+ displayName: "BOND",
+ figi: "FIGI",
+ instrumentType: "bond",
+ instrumentUid: null,
+ key: entry.instrumentKey,
+ receiptCount: 1,
+ ticker: "BOND",
+ };
+
+ const valid: IncomeAnalytics = {
+ ...analytics,
+ entries: [entry],
+ instruments: [instrument],
+ totalsByCurrency: [summary("RUB", "10", "0")],
+ };
+
+ it.each([
+ ["missing key", undefined],
+ ["empty key", ""],
+ ["whitespace key", " "],
+ ["non-string key", 12],
+ ["unknown key", "not-in-breakdown"],
+ ])("rejects an entry with %s", (_name, instrumentKey) => {
+ // Deliberately malformed API data exercises the runtime validation boundary.
+ const response = {
+ ...valid,
+ entries: [{ ...entry, instrumentKey }],
+ } as unknown as IncomeAnalytics;
+
+ expect(toIncomeDisplayModel(response)).toBeNull();
+ });
+
+ it.each([
+ ["duplicate instrument keys", { instruments: [instrument, instrument] }],
+ ["currency mismatch", { entries: [{ ...entry, payment: money("USD", "10") }] }],
+ ["receipt count mismatch", { instruments: [{ ...instrument, receiptCount: 2 }] }],
+ ["missing entries", { entries: [] }],
+ ["empty instrument key", { instruments: [{ ...instrument, key: "" }] }],
+ ])("rejects %s", (_name, invalid) => {
+ expect(toIncomeDisplayModel({ ...valid, ...invalid })).toBeNull();
+ });
+});
+
describe("income comparison display", () => {
it.each([
["25", "100", "+25 %"],
diff --git a/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.test.tsx b/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.test.tsx
index 0ea45e7..0c12ea1 100644
--- a/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.test.tsx
+++ b/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.test.tsx
@@ -324,17 +324,11 @@ describe("AccountIncomePage", () => {
expect(within(panel).getAllByRole("listitem")).toHaveLength(2);
});
- it.each([
- ["missing key", undefined],
- ["empty key", ""],
- ["whitespace key", " "],
- ["non-string key", 12],
- ["unknown key", "not-in-breakdown"],
- ])("rejects %s and recovers through retry", async (_name, instrumentKey) => {
+ it("rejects inconsistent analytics and recovers through retry", async () => {
let response = {
...analytics,
entries: [
- { ...analytics.entries[0], instrumentKey },
+ { ...analytics.entries[0], instrumentKey: "not-in-breakdown" },
analytics.entries[1],
],
};
@@ -356,51 +350,6 @@ describe("AccountIncomePage", () => {
).toBeInTheDocument();
});
- it.each([
- [
- "duplicate instrument keys",
- {
- ...analytics,
- instruments: [...analytics.instruments, analytics.instruments[0]],
- },
- ],
- [
- "currency mismatch",
- {
- ...analytics,
- entries: [
- { ...analytics.entries[0], payment: money("USD", "10") },
- analytics.entries[1],
- ],
- },
- ],
- [
- "receipt count mismatch",
- {
- ...analytics,
- instruments: [{ ...analytics.instruments[0], receiptCount: 3 }],
- },
- ],
- ["missing entries", { ...analytics, entries: [] }],
- [
- "empty instrument key",
- { ...analytics, instruments: [{ ...analytics.instruments[0], key: "" }] },
- ],
- ])("shows a local error for %s", async (_name, response) => {
- server.use(
- http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
- HttpResponse.json(resource(response)),
- ),
- );
- renderPage();
- expect(
- await screen.findByRole("button", { name: "Повторить" }),
- ).toBeInTheDocument();
- expect(
- screen.queryByRole("region", { name: "Выплаты" }),
- ).not.toBeInTheDocument();
- });
-
it("accepts an empty analytics response", async () => {
server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
@@ -870,8 +819,6 @@ describe("income period and snapshot boundary", () => {
expect(
screen.getByRole("heading", { name: "Выплаты" }),
).toBeInTheDocument();
- expect(screen.queryByRole("heading", { level: 3 })).not.toBeInTheDocument();
- expect(screen.queryByRole("heading", { level: 4 })).not.toBeInTheDocument();
expect(screen.queryByText("RUB")).not.toBeInTheDocument();
expect(screen.queryByText("USD")).not.toBeInTheDocument();
const cards = screen.getAllByRole("listitem");
@@ -881,7 +828,6 @@ describe("income period and snapshot boundary", () => {
expect(cards[2]).toHaveTextContent("−3 ₽");
expect(screen.queryByText(/Итого:|Выплат:/)).not.toBeInTheDocument();
expect(screen.queryByRole("button")).not.toBeInTheDocument();
- expect(screen.getAllByText("Купон")).toHaveLength(6);
});
});
diff --git a/src/pages/account/ui/AccountPage/AccountPage.test.tsx b/src/pages/account/ui/AccountPage/AccountPage.test.tsx
index 1492a67..7b89b18 100644
--- a/src/pages/account/ui/AccountPage/AccountPage.test.tsx
+++ b/src/pages/account/ui/AccountPage/AccountPage.test.tsx
@@ -302,7 +302,9 @@ describe("AccountPage states", () => {
expect(
screen.queryByRole("button", { name: "Повторить" }),
).not.toBeInTheDocument();
- expect(screen.getByRole("link")).toHaveAttribute(
+ expect(screen.getByRole("link", {
+ name: code === "NOT_FOUND" ? "К счетам" : "Открыть настройки T-Bank",
+ })).toHaveAttribute(
"href",
code === "NOT_FOUND" ? "/accounts" : "/settings",
);
@@ -427,34 +429,13 @@ describe("AccountPage states", () => {
},
);
- it("renders loading and empty states", () => {
- renderPage();
- expect(screen.getByRole("status")).toBeInTheDocument();
-
+ it("renders the empty state", () => {
renderPage();
expect(
screen.getByRole("heading", { name: "Портфель пока пуст" }),
).toBeInTheDocument();
});
- it("renders allowed error actions", () => {
- const retry = vi.fn();
- renderPage();
- expect(screen.getByRole("link", { name: "К счетам" })).toHaveAttribute(
- "href",
- "/accounts",
- );
-
- renderPage();
- expect(
- screen.getByRole("link", { name: "Открыть настройки T-Bank" }),
- ).toHaveAttribute("href", "/settings");
-
- renderPage();
- fireEvent.click(screen.getByRole("button", { name: "Повторить" }));
- expect(retry).toHaveBeenCalledOnce();
- });
-
it("renders a stale portfolio in upstream order with nullable values hidden", () => {
renderPage();
diff --git a/src/test/storybook.test.tsx b/src/test/storybook.test.tsx
index 2feed41..9a39756 100644
--- a/src/test/storybook.test.tsx
+++ b/src/test/storybook.test.tsx
@@ -1,7 +1,6 @@
import { composeStories } from "@storybook/react-vite";
// eslint-disable-next-line testing-library/no-manual-cleanup -- Vitest globals are disabled, so RTL cannot register its automatic afterEach cleanup.
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
-import { fn } from "storybook/test";
import { afterEach, describe, expect, it, vi } from "vitest";
import preview from "../../.storybook/preview";
@@ -56,18 +55,4 @@ describe("Storybook isolated product examples", () => {
for (const spy of spies) expect(spy).not.toHaveBeenCalled();
});
-
- it("keeps synthetic totals visible while the refresh example calls a local retry", () => {
- const { RefreshError } = composeStories(RefreshStories, preview);
- const retry = fn();
- const view = render();
- expect(screen.getByText(/125\s*000,50/)).toBeVisible();
- fireEvent.click(screen.getByRole("button"));
- expect(retry).toHaveBeenCalledOnce();
- view.rerender(
- ,
- );
- expect(screen.getByText(/125\s*000,50/)).toBeVisible();
- expect(screen.getByRole("status")).toBeVisible();
- });
});