feat: add quiet reading with rare confirmations

Part of the quiet-user-feedback change; docs and specs land in their own repositories.
This commit is contained in:
2026-10-06 07:01:58 +03:00
parent c096fc70ef
commit 5b30bb802a
77 changed files with 1352 additions and 1259 deletions
@@ -1,7 +1,6 @@
import { MantineProvider } from "@mantine/core";
import { type QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
fireEvent,
render,
screen,
waitFor,
@@ -261,7 +260,7 @@ describe("AccountPage states", () => {
it.each(["NOT_FOUND", "TBANK_NOT_CONNECTED", "INVALID_TBANK_TOKEN"])(
"does not offer retry without cached data for %s",
(code) => {
renderPage(<AccountPageError code={code} retry={vi.fn()} />);
renderPage(<AccountPageError code={code} />);
expect(
screen.queryByRole("button", { name: "Повторить" }),
).not.toBeInTheDocument();
@@ -274,12 +273,16 @@ describe("AccountPage states", () => {
},
);
it.each(["TBANK_UNAVAILABLE", "UNKNOWN"])("allows retry for %s", (code) => {
const retry = vi.fn();
renderPage(<AccountPageError code={code} retry={retry} />);
fireEvent.click(screen.getByRole("button", { name: "Повторить" }));
expect(retry).toHaveBeenCalledOnce();
});
it.each(["TBANK_UNAVAILABLE", "UNKNOWN"])(
"shows neutral unavailable without retry for %s",
(code) => {
renderPage(<AccountPageError code={code} />);
expect(screen.getByRole("heading", { name: "Пока недоступно" }))
.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Повторить" }))
.not.toBeInTheDocument();
},
);
it("shows supplied cash separately without adding currency instruments or recalculating totals", () => {
renderPage(
@@ -405,11 +408,9 @@ describe("AccountPage states", () => {
expect(
screen.getByRole("heading", { level: 1, name: "Портфель" }),
).toBeInTheDocument();
expect(
screen.getByText(
"T-Bank временно недоступен. Показан последний успешный снимок.",
),
).toBeInTheDocument();
expect(screen.queryByText(/T-Bank временно недоступен/))
.not.toBeInTheDocument();
expect(screen.queryByText(/Данные получены/)).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Акции" })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Облигации" }),
@@ -573,7 +574,7 @@ describe("AccountPage states", () => {
);
expect(
screen.getByText("Не удалось загрузить последние события."),
screen.getByText("Пока недоступно"),
).toBeInTheDocument();
expect(screen.getByText("Вторая")).toBeInTheDocument();
});
@@ -742,13 +743,13 @@ describe("AccountPage states", () => {
);
expect(
screen.getByText("Не удалось загрузить будущие события."),
screen.getByText("Данные появятся здесь, когда чтение восстановится."),
).toBeInTheDocument();
expect(screen.getByText("Вторая")).toBeInTheDocument();
});
it.each([503])(
"preserves totals and UTC snapshot through refresh error %i, retry and success",
"preserves totals quietly through refresh error %i and success",
async (status) => {
const initialPortfolio: Portfolio
= {
@@ -821,40 +822,30 @@ describe("AccountPage states", () => {
});
expect(structure).toHaveTextContent("10 ₽");
const snapshotText = screen.getByText(/Данные получены/).textContent;
expect(snapshotText).toContain("10:00");
expect(screen.queryByText(/Данные получены/)).not.toBeInTheDocument();
void queryClient.invalidateQueries({
queryKey: portfolioQueryOptions("account-1").queryKey,
});
expect(
await screen.findByText(
"Не удалось обновить портфель. Показан последний полученный снимок.",
),
).toBeInTheDocument();
expect(
screen.getByText(
"T-Bank временно недоступен. Показан последний успешный снимок.",
),
).toBeInTheDocument();
await waitFor(() => expect(requestCount).toBe(2));
expect(screen.queryByText(/Не удалось обновить портфель/))
.not.toBeInTheDocument();
expect(screen.queryByText(/T-Bank временно недоступен/))
.not.toBeInTheDocument();
expect(screen.getByText("Вторая")).toBeInTheDocument();
expect(
screen.getAllByRole("definition").map(node => node.textContent),
).toEqual(["1 234,5 ₽", "0,5 $"]);
// eslint-disable-next-line jest-dom/prefer-to-have-text-content -- The cached timestamp must remain exactly unchanged.
expect(screen.getByText(/Данные получены/).textContent).toBe(
snapshotText,
);
fireEvent.click(screen.getByRole("button", { name: "Повторить" }));
expect(screen.queryByRole("button", { name: "Повторить" }))
.not.toBeInTheDocument();
void queryClient.invalidateQueries({
queryKey: portfolioQueryOptions("account-1").queryKey,
});
await waitFor(() => expect(requestCount).toBe(3));
expect(structure).toHaveTextContent("10 ₽");
expect(
screen.getAllByRole("definition").map(node => node.textContent),
).toEqual(["1 234,5 ₽", "0,5 $"]);
// eslint-disable-next-line jest-dom/prefer-to-have-text-content -- The cached timestamp must remain exactly unchanged.
expect(screen.getByText(/Данные получены/).textContent).toBe(
snapshotText,
);
resolveRetry?.(
HttpResponse.json(
resource(
@@ -875,7 +866,7 @@ describe("AccountPage states", () => {
screen.getAllByRole("definition").map(node => node.textContent),
).toEqual(["-0,5 $", "2,50 €"]),
);
expect(screen.getByText(/Данные получены/)).toHaveTextContent("11:30");
expect(screen.queryByText(/Данные получены/)).not.toBeInTheDocument();
expect(structure).not.toHaveTextContent("10 ₽");
expect(structure).toHaveTextContent("Нет положительной стоимости");
expect(
@@ -916,13 +907,12 @@ describe("AccountPage states", () => {
expect(
screen.getAllByRole("definition").map(node => node.textContent),
).toEqual(["1 234,5 ₽", "0,5 $"]);
expect(screen.getByText(/Данные получены/)).toHaveTextContent("10:00");
expect(screen.queryByText(/Данные получены/)).not.toBeInTheDocument();
expect(
screen.queryByRole("status", { name: "Загрузка" }),
).not.toBeInTheDocument();
expect(
screen.getByRole("status", { name: "Обновляем портфель" }),
).toBeInTheDocument();
expect(screen.queryByRole("status", { name: "Обновляем портфель" }))
.not.toBeInTheDocument();
resolveRefresh?.(HttpResponse.json(resource(portfolio)));
});