feat: show persisted investment data and sync state

Consume the provided resource-envelope API snapshot. Preserve financial snapshots during
refresh, observe synchronization without importing, and fence late responses across
account and connection changes.
This commit is contained in:
2026-09-26 21:22:49 +03:00
parent b987fd8e78
commit 4e5f564fa1
81 changed files with 4714 additions and 1420 deletions
@@ -7,6 +7,7 @@ import { describe, expect, it, vi } from "vitest";
import { brokerageAccountsQueryKey } from "@/entities/brokerage-account";
import { configureAuthClient } from "@/shared/api";
import { createTestQueryClient } from "@/test/query";
import { resource } from "@/test/resource";
import { server } from "@/test/server";
import { AccountsPage } from "./AccountsPage";
import { AccountsPageEmpty } from "./components/AccountsPageEmpty.view";
@@ -80,7 +81,7 @@ describe("AccountsPage", () => {
server.use(
http.get("*/api/v1/brokerage-accounts", () => {
requestCount += 1;
if (requestCount === 1) return HttpResponse.json([account]);
if (requestCount === 1) return HttpResponse.json(resource([account]));
return new Promise<Response>((resolve) => {
resolveRefresh = resolve;
});
@@ -100,21 +101,31 @@ describe("AccountsPage", () => {
screen.getByRole("status", { name: "Обновляем данные" }),
).toBeInTheDocument();
resolveRefresh?.(HttpResponse.json([account]));
resolveRefresh?.(HttpResponse.json(resource([account])));
});
it("keeps the cached account list and offers retry after a refresh error", async () => {
const requests: unknown[] = [];
server.use(
http.post("*/api/v1/tbank-sync", async ({ request }) => {
requests.push(await request.json());
return HttpResponse.json(
{ code: "TBANK_UNAVAILABLE" },
{ status: 503 },
);
}),
);
let requestCount = 0;
server.use(
http.get("*/api/v1/brokerage-accounts", () => {
requestCount += 1;
if (requestCount === 1) return HttpResponse.json([account]);
if (requestCount === 1) return HttpResponse.json(resource([account]));
if (requestCount === 2)
return HttpResponse.json(
{ code: "TBANK_UNAVAILABLE" },
{ status: 503 },
);
return HttpResponse.json([account]);
return HttpResponse.json(resource([account]));
}),
);
const queryClient = createTestQueryClient();
@@ -132,7 +143,8 @@ describe("AccountsPage", () => {
expect(screen.getByText(account.name)).toBeInTheDocument();
screen.getByRole("button", { name: "Повторить" }).click();
await waitFor(() => expect(requestCount).toBe(3));
await waitFor(() => expect(requests).toEqual([{ screen: "accounts" }]));
expect(requestCount).toBe(2);
});
it("keeps a cached empty account list visible after a refresh error", async () => {
@@ -140,7 +152,7 @@ describe("AccountsPage", () => {
server.use(
http.get("*/api/v1/brokerage-accounts", () => {
requestCount += 1;
if (requestCount === 1) return HttpResponse.json([]);
if (requestCount === 1) return HttpResponse.json(resource([]));
return HttpResponse.json(
{ code: "TBANK_UNAVAILABLE" },
{ status: 503 },
@@ -163,3 +175,22 @@ describe("AccountsPage", () => {
expect(screen.getByText("Нет инвестиционных счетов")).toBeInTheDocument();
});
});
it("keeps unknown membership loading instead of declaring no accounts", async () => {
server.use(
http.get("*/api/v1/brokerage-accounts", () =>
HttpResponse.json(resource(null)),
),
);
const client = createTestQueryClient();
renderPage(client);
await waitFor(() =>
expect(client.getQueryData(brokerageAccountsQueryKey)).toMatchObject({
data: null,
}),
);
expect(
screen.queryByText("Нет инвестиционных счетов"),
).not.toBeInTheDocument();
expect(screen.getByRole("status")).toBeVisible();
});