fix: resolve frontend audit findings
Frontend security / security (push) Successful in 10s
Frontend verify / verify (push) Successful in 4m57s
Conventional commits / commits (pull_request) Successful in 28s
Frontend security / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 5m10s
Frontend security / security (push) Successful in 10s
Frontend verify / verify (push) Successful in 4m57s
Conventional commits / commits (pull_request) Successful in 28s
Frontend security / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 5m10s
Repair investment recovery, draft preservation, date validation, currency presentation, accessibility, and route error recovery. Correct Docker and dependency checks and consume explicit income roles.
This commit is contained in:
@@ -9,6 +9,7 @@ import { createTestQueryClient } from "@/test/query";
|
||||
import { resource } from "@/test/resource";
|
||||
import { server } from "@/test/server";
|
||||
import { StockPage } from "./StockPage";
|
||||
import type { StockPageVm } from "./StockPage.vm";
|
||||
|
||||
const LocationProbe = () => {
|
||||
const location = useLocation();
|
||||
@@ -72,6 +73,7 @@ function setup(
|
||||
path?: string;
|
||||
slug?: string;
|
||||
ambiguousTicker?: boolean;
|
||||
metrics?: NonNullable<StockPageVm["company"]>["metrics"];
|
||||
} = {},
|
||||
) {
|
||||
const requests: URL[] = [];
|
||||
@@ -118,7 +120,7 @@ function setup(
|
||||
: HttpResponse.json(
|
||||
resource({
|
||||
description: "Описание компании",
|
||||
metrics: [
|
||||
metrics: options.metrics ?? [
|
||||
{
|
||||
key: "peRatioTtm",
|
||||
value: 5.6,
|
||||
@@ -215,6 +217,63 @@ function setup(
|
||||
};
|
||||
}
|
||||
describe("stock page", () => {
|
||||
it("preserves unknown and unambiguous currencies with compact, ratio and percent formatting", async () => {
|
||||
setup({
|
||||
metrics: [
|
||||
{
|
||||
key: "marketCapitalization",
|
||||
value: 1234567.89,
|
||||
unit: "money",
|
||||
currency: null,
|
||||
period: null,
|
||||
},
|
||||
{
|
||||
key: "revenueTtm",
|
||||
value: 1234567.89,
|
||||
unit: "money",
|
||||
currency: "RUB",
|
||||
period: "TTM",
|
||||
},
|
||||
{
|
||||
key: "ebitdaTtm",
|
||||
value: 1234567.89,
|
||||
unit: "money",
|
||||
currency: "USD",
|
||||
period: "TTM",
|
||||
},
|
||||
{
|
||||
key: "netIncomeTtm",
|
||||
value: 1234567.89,
|
||||
unit: "money",
|
||||
currency: "CAD",
|
||||
period: "TTM",
|
||||
},
|
||||
{
|
||||
key: "peRatioTtm",
|
||||
value: 5.678,
|
||||
unit: "ratio",
|
||||
currency: null,
|
||||
period: "TTM",
|
||||
},
|
||||
{
|
||||
key: "roe",
|
||||
value: 12.3456,
|
||||
unit: "percent",
|
||||
currency: null,
|
||||
period: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(await screen.findByText("1,23 млн валюта неизвестна")).toBeVisible();
|
||||
for (const value of [
|
||||
"1,23 млн ₽",
|
||||
"1,23 млн USD",
|
||||
"1,23 млн CAD",
|
||||
"5,68 ×",
|
||||
"12,35 %",
|
||||
])
|
||||
expect(screen.getByText(value)).toBeVisible();
|
||||
});
|
||||
it("opens a direct ticker URL and uses its resolved UID for market and personal requests", async () => {
|
||||
const { requests } = setup({ path: "/stocks/SBERP", slug: "SBERP" });
|
||||
await screen.findByText("Зачисление дивидендов");
|
||||
@@ -290,6 +349,31 @@ describe("stock page", () => {
|
||||
),
|
||||
).toHaveAttribute("aria-live", "polite");
|
||||
});
|
||||
it("recovers an initially missing company section without reloading successful sections", async () => {
|
||||
const { requests } = setup({ failCompany: true });
|
||||
const retry = await screen.findByRole("button", { name: "Повторить" });
|
||||
await screen.findByText("Зачисление дивидендов");
|
||||
const modes: (string | null)[] = [];
|
||||
server.use(
|
||||
http.get("*/api/v1/stocks/:uid/company", ({ request }) => {
|
||||
modes.push(new URL(request.url).searchParams.get("sync"));
|
||||
return HttpResponse.json(
|
||||
resource({
|
||||
description: "Восстановленное описание",
|
||||
metrics: [],
|
||||
unavailable: [],
|
||||
observedAt: quote.observedAt,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
const previousRequests = requests.length;
|
||||
fireEvent.click(retry);
|
||||
expect(await screen.findByText("Восстановленное описание")).toBeVisible();
|
||||
expect(screen.getByText("Зачисление дивидендов")).toBeVisible();
|
||||
expect(requests).toHaveLength(previousRequests);
|
||||
expect(modes).toEqual(["observe"]);
|
||||
});
|
||||
it("sends instrument-filtered continuation, resets it on filter changes and changes chart period", async () => {
|
||||
const { requests } = setup();
|
||||
await screen.findByText("Зачисление дивидендов");
|
||||
|
||||
@@ -190,13 +190,9 @@ const StockPageView: FC<StockPageViewProps> = (props) => {
|
||||
metric.unit === "money" ? "compact" : "standard",
|
||||
}).format(metric.value)}{" "}
|
||||
{metric.unit === "money"
|
||||
? new Intl.NumberFormat("ru-RU", {
|
||||
style: "currency",
|
||||
currency: metric.currency ?? "RUB",
|
||||
currencyDisplay: "narrowSymbol",
|
||||
})
|
||||
.formatToParts(0)
|
||||
.find((part) => part.type === "currency")?.value
|
||||
? metric.currency === "RUB"
|
||||
? "₽"
|
||||
: (metric.currency ?? t("stock.currencyUnknown"))
|
||||
: metric.unit === "percent"
|
||||
? "%"
|
||||
: "×"}
|
||||
|
||||
@@ -82,7 +82,7 @@ function useStockPageVm() {
|
||||
queryKey: brokerageOperationsQueryOptions("all", firstPage).queryKey,
|
||||
exact: true,
|
||||
});
|
||||
} else void sync.refresh();
|
||||
} else void sync.retry();
|
||||
};
|
||||
const chartData =
|
||||
candles.data?.candles.map((candle) => ({
|
||||
@@ -105,15 +105,15 @@ function useStockPageVm() {
|
||||
? `/stocks/${encodeURIComponent(stock.data.slug)}${params.size ? `?${params.toString()}` : ""}`
|
||||
: null,
|
||||
stock: stock.data,
|
||||
stockState: section(stock, uid ? sync.refresh : () => stock.refetch()),
|
||||
stockState: section(stock, uid ? sync.retry : () => stock.refetch()),
|
||||
code: apiErrorCode(stock.error),
|
||||
logoSrc: instrumentLogoUrl(stock.data?.logoName),
|
||||
quote: quote.data,
|
||||
quoteState: section(quote, sync.refresh),
|
||||
quoteState: section(quote, sync.retry),
|
||||
candles: candles.data,
|
||||
chartData,
|
||||
priceRows: chartData.toReversed(),
|
||||
candlesState: section(candles, sync.refresh),
|
||||
candlesState: section(candles, sync.retry),
|
||||
period,
|
||||
setPeriod: (value: string) => setParam("period", value),
|
||||
averageLine:
|
||||
@@ -121,11 +121,11 @@ function useStockPageVm() {
|
||||
hasIncompleteCandle:
|
||||
candles.data?.candles.some((candle) => !candle.isComplete) ?? false,
|
||||
position: position.data,
|
||||
positionState: section(position, sync.refresh),
|
||||
positionState: section(position, sync.retry),
|
||||
company: companyData,
|
||||
companyState: section(company, sync.refresh),
|
||||
companyState: section(company, sync.retry),
|
||||
dividends: dividends.data,
|
||||
dividendsState: section(dividends, sync.refresh),
|
||||
dividendsState: section(dividends, sync.retry),
|
||||
upcoming:
|
||||
dividends.data?.dividends
|
||||
.filter((event) => event.timing === "upcoming")
|
||||
@@ -144,7 +144,7 @@ function useStockPageVm() {
|
||||
) ?? [],
|
||||
operations: operations.data,
|
||||
operationsState: {
|
||||
...section(operations, sync.refresh),
|
||||
...section(operations, sync.retry),
|
||||
retry: retryOperations,
|
||||
},
|
||||
eventRows:
|
||||
|
||||
Reference in New Issue
Block a user