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:
@@ -14,6 +14,7 @@ import type { BrokerageEvent } from "@/entities/brokerage-event";
|
||||
import { type Portfolio, portfolioQueryOptions } from "@/entities/portfolio";
|
||||
import { configureAuthClient } from "@/shared/api";
|
||||
import { createTestQueryClient } from "@/test/query";
|
||||
import { resource } from "@/test/resource";
|
||||
import { server } from "@/test/server";
|
||||
import { AccountPage } from "./AccountPage";
|
||||
import type {
|
||||
@@ -144,27 +145,31 @@ describe("AccountPage states", () => {
|
||||
};
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
|
||||
HttpResponse.json({
|
||||
...portfolio,
|
||||
positions:
|
||||
accountId === "all"
|
||||
? ["account-1", "account-2"].map((id) => ({
|
||||
...position,
|
||||
account: { id, name: id },
|
||||
}))
|
||||
: [position],
|
||||
}),
|
||||
HttpResponse.json(
|
||||
resource({
|
||||
...portfolio,
|
||||
positions:
|
||||
accountId === "all"
|
||||
? ["account-1", "account-2"].map((id) => ({
|
||||
...position,
|
||||
account: { id, name: id },
|
||||
}))
|
||||
: [position],
|
||||
}),
|
||||
),
|
||||
),
|
||||
http.get(`*/api/v1/${collection}/:uid`, () => {
|
||||
requests();
|
||||
return HttpResponse.json({
|
||||
instrumentUid: uid,
|
||||
slug,
|
||||
name: "Название из карточки инструмента",
|
||||
ticker: "OTHER-TICKER",
|
||||
isin: "OTHER-ISIN",
|
||||
logoName: "other.png",
|
||||
});
|
||||
return HttpResponse.json(
|
||||
resource({
|
||||
instrumentUid: uid,
|
||||
slug,
|
||||
name: "Название из карточки инструмента",
|
||||
ticker: "OTHER-TICKER",
|
||||
isin: "OTHER-ISIN",
|
||||
logoName: "other.png",
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
configureTestAuth();
|
||||
@@ -204,22 +209,24 @@ describe("AccountPage states", () => {
|
||||
const requests = vi.fn();
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
|
||||
HttpResponse.json({
|
||||
...portfolio,
|
||||
positions: [
|
||||
{
|
||||
...portfolio.positions[0],
|
||||
instrumentUid: uid,
|
||||
ticker: "SBERP",
|
||||
},
|
||||
],
|
||||
}),
|
||||
HttpResponse.json(
|
||||
resource({
|
||||
...portfolio,
|
||||
positions: [
|
||||
{
|
||||
...portfolio.positions[0],
|
||||
instrumentUid: uid,
|
||||
ticker: "SBERP",
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
http.get("*/api/v1/stocks/:uid", () => {
|
||||
requests();
|
||||
return scenario === "unavailable"
|
||||
? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 })
|
||||
: HttpResponse.json({ instrumentUid: uid, slug: uid });
|
||||
: HttpResponse.json(resource({ instrumentUid: uid, slug: uid }));
|
||||
}),
|
||||
);
|
||||
renderAccountRoute(createTestQueryClient());
|
||||
@@ -234,7 +241,7 @@ describe("AccountPage states", () => {
|
||||
it("keeps the account and positions visible for an older HTTP response without structure", async () => {
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
|
||||
HttpResponse.json({ ...portfolio, structure: undefined }),
|
||||
HttpResponse.json(resource({ ...portfolio, structure: undefined })),
|
||||
),
|
||||
);
|
||||
renderAccountRoute(createTestQueryClient());
|
||||
@@ -322,11 +329,13 @@ describe("AccountPage states", () => {
|
||||
async (hasPositions) => {
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
|
||||
HttpResponse.json({
|
||||
...portfolio,
|
||||
totalsByCurrency: [],
|
||||
positions: hasPositions ? portfolio.positions : [],
|
||||
}),
|
||||
HttpResponse.json(
|
||||
resource({
|
||||
...portfolio,
|
||||
totalsByCurrency: [],
|
||||
positions: hasPositions ? portfolio.positions : [],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
renderAccountRoute(createTestQueryClient());
|
||||
@@ -358,7 +367,7 @@ describe("AccountPage states", () => {
|
||||
};
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
|
||||
HttpResponse.json(snapshot),
|
||||
HttpResponse.json(resource(snapshot)),
|
||||
),
|
||||
);
|
||||
const queryClient = createTestQueryClient();
|
||||
@@ -380,7 +389,7 @@ describe("AccountPage states", () => {
|
||||
).toEqual(totalsByCurrency.length ? ["100 ₽"] : []);
|
||||
expect(
|
||||
queryClient.getQueryData(portfolioQueryOptions("account-1").queryKey),
|
||||
).toEqual(snapshot);
|
||||
).toEqual(resource(snapshot));
|
||||
},
|
||||
);
|
||||
|
||||
@@ -887,7 +896,7 @@ describe("AccountPage states", () => {
|
||||
expect(screen.getByText("Вторая")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([503, 404])(
|
||||
it.each([503])(
|
||||
"preserves totals and UTC snapshot through refresh error %i, retry and success",
|
||||
async (status) => {
|
||||
const initialPortfolio: Portfolio = {
|
||||
@@ -907,9 +916,36 @@ describe("AccountPage states", () => {
|
||||
let requestCount = 0;
|
||||
let resolveRetry: ((response: Response) => void) | undefined;
|
||||
server.use(
|
||||
http.post("*/api/v1/tbank-sync", () =>
|
||||
HttpResponse.json(
|
||||
{
|
||||
id: "overview",
|
||||
scope: { screen: "account", accountId: "account-1" },
|
||||
now: "2026-09-25T00:00:00Z",
|
||||
},
|
||||
{ status: 202 },
|
||||
),
|
||||
),
|
||||
http.get("*/api/v1/tbank-sync/:id", () =>
|
||||
HttpResponse.json({
|
||||
id: "overview",
|
||||
scope: { screen: "account", accountId: "account-1" },
|
||||
now: "2026-09-25T00:00:00Z",
|
||||
state: "succeeded",
|
||||
errorCode: null,
|
||||
progress: { total: 1, completed: 1, discovering: false },
|
||||
resources: [
|
||||
resource(null, { resourceKey: "portfolio", stateVersion: "2" })
|
||||
.meta,
|
||||
],
|
||||
}),
|
||||
),
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => {
|
||||
requestCount += 1;
|
||||
if (requestCount === 1) return HttpResponse.json(initialPortfolio);
|
||||
if (requestCount === 1)
|
||||
return HttpResponse.json(
|
||||
resource(initialPortfolio, { resourceKey: "portfolio" }),
|
||||
);
|
||||
if (requestCount === 3)
|
||||
return new Promise<Response>((resolve) => {
|
||||
resolveRetry = resolve;
|
||||
@@ -961,14 +997,19 @@ describe("AccountPage states", () => {
|
||||
snapshotText,
|
||||
);
|
||||
resolveRetry?.(
|
||||
HttpResponse.json({
|
||||
...portfolio,
|
||||
isStale: false,
|
||||
lastSuccessfulAt: "2026-08-14T11:30:00Z",
|
||||
totalsByCurrency: [{ currency: "USD", value: "-0.5" }],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
cashBalances: [{ currency: "EUR", value: "2.50" }],
|
||||
}),
|
||||
HttpResponse.json(
|
||||
resource(
|
||||
{
|
||||
...portfolio,
|
||||
isStale: false,
|
||||
lastSuccessfulAt: "2026-08-14T11:30:00Z",
|
||||
totalsByCurrency: [{ currency: "USD", value: "-0.5" }],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
cashBalances: [{ currency: "EUR", value: "2.50" }],
|
||||
},
|
||||
{ resourceKey: "portfolio", stateVersion: "2" },
|
||||
),
|
||||
),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
@@ -993,7 +1034,7 @@ describe("AccountPage states", () => {
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => {
|
||||
requestCount += 1;
|
||||
if (requestCount === 1) return HttpResponse.json(portfolio);
|
||||
if (requestCount === 1) return HttpResponse.json(resource(portfolio));
|
||||
return new Promise<Response>((resolve) => {
|
||||
resolveRefresh = resolve;
|
||||
});
|
||||
@@ -1020,6 +1061,6 @@ describe("AccountPage states", () => {
|
||||
screen.getByRole("status", { name: "Обновляем портфель" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
resolveRefresh?.(HttpResponse.json(portfolio));
|
||||
resolveRefresh?.(HttpResponse.json(resource(portfolio)));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user