refactor: simplify resource queries and layout tests
Frontend security / security (push) Successful in 10s
Frontend verify / verify (push) Successful in 5m51s

This commit was merged in pull request #40.
This commit is contained in:
2026-09-27 18:53:12 +03:00
parent 5e076470aa
commit c1b08735e3
18 changed files with 508 additions and 122 deletions
@@ -70,6 +70,7 @@ function setup(
noPosition?: boolean;
failCompany?: boolean;
failStock?: boolean;
cursorError?: string;
path?: string;
slug?: string;
ambiguousTicker?: boolean;
@@ -79,6 +80,8 @@ function setup(
const requests: URL[] = [];
const resolved = { ...stock, slug: options.slug ?? uid };
let failQuote = false;
let cursorError = options.cursorError;
let operationPayment = "200";
server.use(
http.get("*/api/v1/stocks/:uid", () =>
options.failStock
@@ -166,6 +169,8 @@ function setup(
const url = new URL(request.url);
requests.push(url);
const next = url.searchParams.has("cursor");
if (next && cursorError)
return HttpResponse.json({ code: cursorError }, { status: 400 });
return HttpResponse.json(
resource({
accountId: "all",
@@ -182,7 +187,7 @@ function setup(
category: next ? "trade" : "income",
description: "",
account: { id: "main", name: "Основной" },
payment: money("200"),
payment: money(operationPayment),
quantity: next ? "2" : null,
price: next ? money("100") : null,
},
@@ -211,6 +216,10 @@ function setup(
);
return {
requests,
recoverOperations: () => {
cursorError = undefined;
operationPayment = "201";
},
failQuote: () => {
failQuote = true;
},
@@ -374,7 +383,7 @@ describe("stock page", () => {
expect(requests).toHaveLength(previousRequests);
expect(modes).toEqual(["observe"]);
});
it("sends instrument-filtered continuation, resets it on filter changes and changes chart period", async () => {
it("sends instrument-filtered continuation, restores it on returning to the filter and changes chart period", async () => {
const { requests } = setup();
await screen.findByText("Зачисление дивидендов");
fireEvent.click(screen.getByRole("button", { name: "Далее" }));
@@ -399,6 +408,14 @@ describe("stock page", () => {
),
).toBe(true),
);
expect(await screen.findByText("Зачисление дивидендов")).toBeVisible();
expect(screen.getByRole("button", { name: "Назад" })).toBeDisabled();
fireEvent.click(screen.getByRole("radio", { name: "Все" }));
expect(await screen.findByText("Продажа")).toBeVisible();
expect(screen.getByRole("button", { name: "Назад" })).toBeEnabled();
fireEvent.click(screen.getByRole("button", { name: "Назад" }));
expect(await screen.findByText("Зачисление дивидендов")).toBeVisible();
expect(screen.getByRole("button", { name: "Назад" })).toBeDisabled();
fireEvent.click(screen.getByRole("radio", { name: "Год" }));
await waitFor(() =>
expect(
@@ -406,6 +423,34 @@ describe("stock page", () => {
).toBe(true),
);
});
it.each([
["INVALID_OPERATIONS_CURSOR", null, null],
["TBANK_UNAVAILABLE", "next", "observe"],
])(
"recovers history after %s with fresh data and the correct cursor",
async (code, cursor, sync) => {
const controls = setup({ cursorError: code });
await screen.findByText("Зачисление дивидендов");
fireEvent.click(screen.getByRole("button", { name: "Далее" }));
const retry = await screen.findByRole("button", { name: "Повторить" });
controls.recoverOperations();
const previousRequests = controls.requests.length;
fireEvent.click(retry);
expect(await screen.findByText("+201 ₽")).toBeVisible();
const recovered = controls.requests.slice(previousRequests);
expect(recovered).toHaveLength(1);
expect(recovered[0]?.pathname).toBe(
"/api/v1/brokerage-accounts/all/operations",
);
expect(recovered[0]?.searchParams.get("instrumentUid")).toBe(uid);
expect(recovered[0]?.searchParams.get("limit")).toBe("20");
expect(recovered[0]?.searchParams.get("cursor")).toBe(cursor);
expect(recovered[0]?.searchParams.get("sync")).toBe(sync);
if (cursor)
expect(screen.getByRole("button", { name: "Назад" })).toBeEnabled();
else expect(screen.getByRole("button", { name: "Назад" })).toBeDisabled();
},
);
it("keeps the last quote on a refresh failure", async () => {
const scopes: unknown[] = [];
server.use(
+11 -43
View File
@@ -1,11 +1,5 @@
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { useParams, useSearchParams } from "react-router";
import {
brokerageOperationsQueryOptions,
type OperationCategory,
useBrokerageOperations,
} from "@/entities/broker-operation";
import type { OperationCategory } from "@/entities/broker-operation";
import {
type StockPeriod,
useStock,
@@ -15,7 +9,10 @@ import {
useStockPosition,
useStockQuote,
} from "@/entities/stock";
import { section } from "@/features/instrument-details";
import {
section,
useInstrumentOperations,
} from "@/features/instrument-details";
import { useScreenSync } from "@/features/tbank-connection";
import { apiErrorCode } from "@/shared/api";
import { formatDate, instrumentLogoUrl } from "@/shared/lib";
@@ -38,27 +35,13 @@ function useStockPageVm() {
periods.find((value) => value === params.get("period")) ?? "month";
const filter =
filters.find((value) => value === params.get("events")) ?? "all";
const scope = `${uid}:${filter}`;
const [pagination, setPagination] = useState<{
scope: string;
cursors: string[];
}>({ scope, cursors: [] });
const cursors = pagination.scope === scope ? pagination.cursors : [];
const client = useQueryClient();
const quote = useStockQuote(uid);
const candles = useStockCandles(uid, period);
const position = useStockPosition(uid);
const company = useStockCompany(uid);
const dividends = useStockDividends(uid);
const firstPage = {
instrumentUid: uid,
categories: categories[filter],
limit: 20,
};
const operations = useBrokerageOperations(uid ? "all" : undefined, {
...firstPage,
cursor: cursors.at(-1),
});
const history = useInstrumentOperations(uid, filter, categories[filter]);
const { operations } = history;
const sync = useScreenSync(
{
screen: "stock",
@@ -75,15 +58,6 @@ function useStockPageVm() {
next.set(key, value);
setParams(next, { replace: true });
};
const retryOperations = () => {
if (apiErrorCode(operations.error) === "INVALID_OPERATIONS_CURSOR") {
setPagination({ scope, cursors: [] });
void client.invalidateQueries({
queryKey: brokerageOperationsQueryOptions("all", firstPage).queryKey,
exact: true,
});
} else void sync.retry();
};
const chartData =
candles.data?.candles.map((candle) => ({
...candle,
@@ -145,7 +119,7 @@ function useStockPageVm() {
operations: operations.data,
operationsState: {
...section(operations, sync.retry),
retry: retryOperations,
retry: () => history.retry(sync.retry),
},
eventRows:
operations.data?.items.map((item) => ({
@@ -155,15 +129,9 @@ function useStockPageVm() {
})) ?? [],
filter,
setFilter: (value: string) => setParam("events", value),
hasPrevious: cursors.length > 0,
onPrevious: () => setPagination({ scope, cursors: cursors.slice(0, -1) }),
onNext: () => {
if (operations.data?.nextCursor)
setPagination({
scope,
cursors: [...cursors, operations.data.nextCursor],
});
},
hasPrevious: history.hasPrevious,
onPrevious: history.onPrevious,
onNext: history.onNext,
};
}
type StockPageVm = ReturnType<typeof useStockPageVm>;