chore: finish frontend ESLint migration #18
Enable every plugin recommended preset and replace Biome with ESLint formatting. Preserve API contracts and JSX whitespace; verify source, tests, builds and tooling.
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { HttpResponse, http } from "msw";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { MemoryRouter, Route, Routes, useLocation } from "react-router";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { configureAuthClient } from "@/shared/api";
|
||||
import { createTestQueryClient } from "@/test/query";
|
||||
import { resource } from "@/test/resource";
|
||||
import { server } from "@/test/server";
|
||||
|
||||
import { BondPage } from "./BondPage";
|
||||
|
||||
const LocationProbe = () => {
|
||||
@@ -21,74 +23,78 @@ const LocationProbe = () => {
|
||||
};
|
||||
|
||||
const uid = "e6123145-9665-43e0-8413-cd61b8aa9b13";
|
||||
const money = (value: string) => ({ value, currency: "RUB" });
|
||||
const bond = {
|
||||
instrumentUid: uid,
|
||||
name: "Тестовая облигация",
|
||||
ticker: "TEST",
|
||||
nominal: money("500"),
|
||||
initialNominal: money("1000"),
|
||||
aci: money("12"),
|
||||
maturityDate: "2030-05-15",
|
||||
callDate: null,
|
||||
couponQuantityPerYear: 4,
|
||||
floatingCoupon: true,
|
||||
amortization: true,
|
||||
perpetual: false,
|
||||
subordinated: false,
|
||||
isin: "TESTISIN",
|
||||
currency: "RUB",
|
||||
sector: "financial",
|
||||
lot: 10,
|
||||
logoName: null,
|
||||
historyFrom: null,
|
||||
};
|
||||
const money = (value: string) => ({ currency: "RUB", value });
|
||||
const bond = (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
aci: money("12"),
|
||||
amortization: true,
|
||||
callDate: null,
|
||||
couponQuantityPerYear: 4,
|
||||
currency: "RUB",
|
||||
floatingCoupon: true,
|
||||
historyFrom: null,
|
||||
initialNominal: money("1000"),
|
||||
instrumentUid: uid,
|
||||
isin: "TESTISIN",
|
||||
logoName: null,
|
||||
lot: 10,
|
||||
maturityDate: "2030-05-15",
|
||||
name: "Тестовая облигация",
|
||||
nominal: money("500"),
|
||||
perpetual: false,
|
||||
sector: "financial",
|
||||
subordinated: false,
|
||||
ticker: "TEST",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */);
|
||||
const position = {
|
||||
quantity: "10",
|
||||
currentValue: money("3000"),
|
||||
averagePurchasePrice: money("200"),
|
||||
absoluteReturn: money("1000"),
|
||||
returnPercent: "50",
|
||||
accounts: [
|
||||
{
|
||||
absoluteReturn: money("1000"),
|
||||
account: { id: "main", name: "Основной" },
|
||||
averagePurchasePrice: money("200"),
|
||||
currentValue: money("3000"),
|
||||
instrumentUid: uid,
|
||||
quantity: "10",
|
||||
currentValue: money("3000"),
|
||||
averagePurchasePrice: money("200"),
|
||||
absoluteReturn: money("1000"),
|
||||
returnPercent: "50",
|
||||
account: { id: "main", name: "Основной" },
|
||||
},
|
||||
],
|
||||
averagePurchasePrice: money("200"),
|
||||
currentValue: money("3000"),
|
||||
isStale: false,
|
||||
observedAt: "2026-09-10T10:00:00Z",
|
||||
quantity: "10",
|
||||
returnPercent: "50",
|
||||
};
|
||||
const quote = {
|
||||
pricePercent: "98.125",
|
||||
cleanPrice: money("490.625"),
|
||||
aci: money("12"),
|
||||
dirtyPrice: money("502.625"),
|
||||
priceAt: "2026-09-10T10:00:00Z",
|
||||
tradingStatus: null,
|
||||
changePercentagePoints: "-0.875",
|
||||
previousCloseDate: "2026-09-09",
|
||||
unavailable: [],
|
||||
observedAt: "2026-09-10T10:00:00Z",
|
||||
};
|
||||
const quote = (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
aci: money("12"),
|
||||
changePercentagePoints: "-0.875",
|
||||
cleanPrice: money("490.625"),
|
||||
dirtyPrice: money("502.625"),
|
||||
observedAt: "2026-09-10T10:00:00Z",
|
||||
previousCloseDate: "2026-09-09",
|
||||
priceAt: "2026-09-10T10:00:00Z",
|
||||
pricePercent: "98.125",
|
||||
tradingStatus: null,
|
||||
unavailable: [],
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */);
|
||||
function setup(
|
||||
options: {
|
||||
noPosition?: boolean;
|
||||
failCoupons?: boolean;
|
||||
ratingState?: "active" | "withdrawn" | "missing" | "unavailable";
|
||||
failBond?: boolean;
|
||||
path?: string;
|
||||
slug?: string;
|
||||
ambiguousTicker?: boolean;
|
||||
failBond?: boolean;
|
||||
failCoupons?: boolean;
|
||||
noPosition?: boolean;
|
||||
path?: string;
|
||||
ratingState?: "active" | "missing" | "unavailable" | "withdrawn";
|
||||
slug?: string;
|
||||
} = {},
|
||||
) {
|
||||
const requests: URL[] = [];
|
||||
const resolved = { ...bond, slug: options.slug ?? uid };
|
||||
let failQuote = false;
|
||||
let isFailQuote = false;
|
||||
server.use(
|
||||
http.get("*/api/v1/bonds/:uid", () =>
|
||||
options.failBond
|
||||
@@ -101,71 +107,78 @@ function setup(
|
||||
? HttpResponse.json({ code: "BOND_TICKER_AMBIGUOUS" }, { status: 409 })
|
||||
: HttpResponse.json(resource(resolved));
|
||||
}),
|
||||
http.get("*/api/v1/bonds/:uid/:section", ({ params, request }) => {
|
||||
http.get("*/api/v1/bonds/:uid/metrics", ({ request }) => {
|
||||
requests.push(new URL(request.url));
|
||||
if (params.section === "metrics")
|
||||
return HttpResponse.json(
|
||||
resource({
|
||||
return HttpResponse.json(
|
||||
resource((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
coupon: money("32.88"),
|
||||
couponDate: "2026-10-27",
|
||||
couponPeriodDays: 90,
|
||||
couponType: "COUPON_TYPE_FLOATING",
|
||||
couponYieldPercent: "27.18",
|
||||
couponYieldPriceAt: quote.priceAt,
|
||||
creditRating:
|
||||
options.ratingState === "missing" ||
|
||||
options.ratingState === "unavailable"
|
||||
options.ratingState === "missing"
|
||||
|| options.ratingState === "unavailable"
|
||||
? null
|
||||
: {
|
||||
agency: "NCR",
|
||||
value:
|
||||
options.ratingState === "withdrawn" ? null : "AAA.ru",
|
||||
date: "2026-04-17",
|
||||
observedAt: quote.observedAt,
|
||||
sourceUrl: "https://ratings.ru/ratings/issues/123/",
|
||||
status:
|
||||
options.ratingState === "withdrawn"
|
||||
? "withdrawn"
|
||||
: "active",
|
||||
date: "2026-04-17",
|
||||
sourceUrl: "https://ratings.ru/ratings/issues/123/",
|
||||
observedAt: quote.observedAt,
|
||||
value:
|
||||
options.ratingState === "withdrawn" ? null : "AAA.ru",
|
||||
},
|
||||
coupon: money("32.88"),
|
||||
couponDate: "2026-10-27",
|
||||
couponType: "COUPON_TYPE_FLOATING",
|
||||
couponPeriodDays: 90,
|
||||
couponYieldPercent: "27.18",
|
||||
couponYieldPriceAt: quote.priceAt,
|
||||
effectiveYield: {
|
||||
value: "15.6262",
|
||||
pricePercent: "53.384",
|
||||
date: "2030-05-15",
|
||||
dateType: "MATDATE",
|
||||
pricePercent: "53.384",
|
||||
tradeAt: quote.priceAt,
|
||||
value: "15.6262",
|
||||
},
|
||||
observedAt: quote.observedAt,
|
||||
unavailable:
|
||||
options.ratingState === "unavailable" ? ["rating"] : [],
|
||||
observedAt: quote.observedAt,
|
||||
}),
|
||||
);
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */)),
|
||||
);
|
||||
}),
|
||||
http.get("*/api/v1/bonds/:uid/:section", ({ params, request }) => {
|
||||
requests.push(new URL(request.url));
|
||||
|
||||
if (params.section === "quote")
|
||||
return failQuote
|
||||
return isFailQuote
|
||||
? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 })
|
||||
: HttpResponse.json(resource(quote));
|
||||
if (params.section === "position")
|
||||
return HttpResponse.json(
|
||||
resource(
|
||||
options.noPosition
|
||||
? { ...position, quantity: "0", accounts: [] }
|
||||
? { ...position, accounts: [], quantity: "0" }
|
||||
: position,
|
||||
),
|
||||
);
|
||||
if (params.section === "candles")
|
||||
return HttpResponse.json(
|
||||
resource({
|
||||
candles: [
|
||||
{
|
||||
time: "2026-09-09T00:00:00Z",
|
||||
close: "99.25",
|
||||
isComplete: true,
|
||||
},
|
||||
],
|
||||
currency: null,
|
||||
historyFrom: null,
|
||||
observedAt: quote.observedAt,
|
||||
}),
|
||||
resource((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
candles: [
|
||||
{
|
||||
close: "99.25",
|
||||
isComplete: true,
|
||||
time: "2026-09-09T00:00:00Z",
|
||||
},
|
||||
],
|
||||
currency: null,
|
||||
historyFrom: null,
|
||||
observedAt: quote.observedAt,
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */)),
|
||||
);
|
||||
if (options.failCoupons)
|
||||
return HttpResponse.json(
|
||||
@@ -173,71 +186,75 @@ function setup(
|
||||
{ status: 503 },
|
||||
);
|
||||
return HttpResponse.json(
|
||||
resource({
|
||||
coupons: [
|
||||
{
|
||||
perBond: money("32.88"),
|
||||
recordDate: "2026-10-13",
|
||||
number: "1",
|
||||
couponType: "COUPON_TYPE_FLOATING",
|
||||
periodDays: 90,
|
||||
paymentDate: "2026-10-27",
|
||||
timing: "upcoming",
|
||||
estimatedAmount: options.noPosition ? null : money("328.8"),
|
||||
},
|
||||
{
|
||||
perBond: money("20"),
|
||||
recordDate: "2025-10-13",
|
||||
paymentDate: null,
|
||||
number: "2",
|
||||
couponType: null,
|
||||
periodDays: null,
|
||||
timing: "past",
|
||||
estimatedAmount: null,
|
||||
},
|
||||
],
|
||||
from: "2021-09-10",
|
||||
to: "2027-09-10",
|
||||
observedAt: quote.observedAt,
|
||||
positionAt: null,
|
||||
estimateUnavailable: false,
|
||||
positionIsStale: false,
|
||||
}),
|
||||
resource((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
coupons: [
|
||||
{
|
||||
couponType: "COUPON_TYPE_FLOATING",
|
||||
estimatedAmount: options.noPosition ? null : money("328.8"),
|
||||
number: "1",
|
||||
paymentDate: "2026-10-27",
|
||||
perBond: money("32.88"),
|
||||
periodDays: 90,
|
||||
recordDate: "2026-10-13",
|
||||
timing: "upcoming",
|
||||
},
|
||||
{
|
||||
couponType: null,
|
||||
estimatedAmount: null,
|
||||
number: "2",
|
||||
paymentDate: null,
|
||||
perBond: money("20"),
|
||||
periodDays: null,
|
||||
recordDate: "2025-10-13",
|
||||
timing: "past",
|
||||
},
|
||||
],
|
||||
estimateUnavailable: false,
|
||||
from: "2021-09-10",
|
||||
observedAt: quote.observedAt,
|
||||
positionAt: null,
|
||||
positionIsStale: false,
|
||||
to: "2027-09-10",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */)),
|
||||
);
|
||||
}),
|
||||
http.get("*/api/v1/brokerage-accounts/all/operations", ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
requests.push(url);
|
||||
const next = url.searchParams.has("cursor");
|
||||
const isNext = url.searchParams.has("cursor");
|
||||
return HttpResponse.json(
|
||||
resource({
|
||||
accountId: "all",
|
||||
observedAt: quote.observedAt,
|
||||
hasNext: !next,
|
||||
nextCursor: next ? null : "next",
|
||||
items: [
|
||||
{
|
||||
id: next ? "sold" : "dividend",
|
||||
occurredAt: next
|
||||
? "2022-01-02T10:00:00Z"
|
||||
: "2025-01-02T10:00:00Z",
|
||||
type: next ? "OPERATION_TYPE_SELL" : "OPERATION_TYPE_COUPON",
|
||||
category: next ? "trade" : "income",
|
||||
description: "",
|
||||
account: { id: "main", name: "Основной" },
|
||||
payment: money("200"),
|
||||
quantity: next ? "2" : null,
|
||||
price: next ? money("100") : null,
|
||||
},
|
||||
],
|
||||
}),
|
||||
resource((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
accountId: "all",
|
||||
hasNext: !isNext,
|
||||
items: [
|
||||
{
|
||||
account: { id: "main", name: "Основной" },
|
||||
category: isNext ? "trade" : "income",
|
||||
description: "",
|
||||
id: isNext ? "sold" : "dividend",
|
||||
occurredAt: isNext
|
||||
? "2022-01-02T10:00:00Z"
|
||||
: "2025-01-02T10:00:00Z",
|
||||
payment: money("200"),
|
||||
price: isNext ? money("100") : null,
|
||||
quantity: isNext ? "2" : null,
|
||||
type: isNext ? "OPERATION_TYPE_SELL" : "OPERATION_TYPE_COUPON",
|
||||
},
|
||||
],
|
||||
nextCursor: isNext ? null : "next",
|
||||
observedAt: quote.observedAt,
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
configureAuthClient({
|
||||
accessToken: () => "synthetic",
|
||||
refresh: async () => false,
|
||||
clear: vi.fn(),
|
||||
refresh: () => Promise.resolve(false),
|
||||
});
|
||||
const client = createTestQueryClient();
|
||||
render(
|
||||
@@ -246,17 +263,17 @@ function setup(
|
||||
<MemoryRouter initialEntries={[options.path ?? `/bonds/${uid}`]}>
|
||||
<LocationProbe />
|
||||
<Routes>
|
||||
<Route path="/bonds/:bondId" element={<BondPage />} />
|
||||
<Route element={<BondPage />} path="/bonds/:bondId" />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</MantineProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
return {
|
||||
requests,
|
||||
failQuote: () => {
|
||||
failQuote = true;
|
||||
isFailQuote = true;
|
||||
},
|
||||
requests,
|
||||
};
|
||||
}
|
||||
describe("bond page", () => {
|
||||
@@ -280,11 +297,11 @@ describe("bond page", () => {
|
||||
expect(await screen.findByText("328,8 ₽")).toBeVisible();
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
requests.some((url) => url.searchParams.get("instrumentUid") === uid),
|
||||
requests.some(url => url.searchParams.get("instrumentUid") === uid),
|
||||
).toBe(true),
|
||||
);
|
||||
expect(
|
||||
requests.some((url) => url.pathname === `/api/v1/bonds/${uid}/quote`),
|
||||
requests.some(url => url.pathname === `/api/v1/bonds/${uid}/quote`),
|
||||
).toBe(true);
|
||||
fireEvent.click(screen.getByRole("button", { name: /Цены в таблице/ }));
|
||||
await waitFor(() => expect(screen.getByText("99,25 %")).toBeVisible());
|
||||
@@ -317,11 +334,13 @@ describe("bond page", () => {
|
||||
);
|
||||
});
|
||||
it("keeps market content and sold-position history when coupons fail", async () => {
|
||||
setup({ noPosition: true, failCoupons: true });
|
||||
setup({ failCoupons: true, noPosition: true });
|
||||
await screen.findByText("98,125 %");
|
||||
const message = await screen.findByText(/Не удалось загрузить этот раздел/);
|
||||
expect(
|
||||
(await screen.findByText(/Не удалось загрузить этот раздел/)).closest(
|
||||
'[role="status"]',
|
||||
// eslint-disable-next-line testing-library/no-node-access -- This assertion checks DOM structure, decoration or focus behavior that semantic queries cannot establish.
|
||||
message.closest(
|
||||
"[role=\"status\"]",
|
||||
),
|
||||
).toHaveAttribute("aria-live", "polite");
|
||||
expect(
|
||||
@@ -346,32 +365,34 @@ describe("bond page", () => {
|
||||
await screen.findByText("98,125 %");
|
||||
controls.failQuote();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Обновить" }));
|
||||
const message = await screen.findByText(/последние полученные/);
|
||||
expect(
|
||||
(await screen.findByText(/последние полученные/)).closest(
|
||||
'[role="status"]',
|
||||
// eslint-disable-next-line testing-library/no-node-access -- This assertion checks DOM structure, decoration or focus behavior that semantic queries cannot establish.
|
||||
message.closest(
|
||||
"[role=\"status\"]",
|
||||
),
|
||||
).toHaveAttribute("aria-live", "polite");
|
||||
expect(screen.getByText("98,125 %")).toBeVisible();
|
||||
expect(scopes).toEqual([
|
||||
{
|
||||
screen: "bond",
|
||||
instrumentUid: uid,
|
||||
accountId: "all",
|
||||
candlePeriod: "year",
|
||||
categories: ["trade"],
|
||||
instrumentUid: uid,
|
||||
limit: 20,
|
||||
screen: "bond",
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("offers search for an ambiguous or missing instrument without loading personal data", async () => {
|
||||
const { requests } = setup({ path: "/bonds/SAME", ambiguousTicker: true });
|
||||
const { requests } = setup({ ambiguousTicker: true, path: "/bonds/SAME" });
|
||||
expect(
|
||||
await screen.findByText(/Тикер соответствует нескольким выпускам/),
|
||||
).toBeVisible();
|
||||
expect(
|
||||
screen.getByRole("link", { name: "К поиску облигаций" }),
|
||||
).toHaveAttribute("href", "/bonds");
|
||||
expect(requests.some((url) => url.pathname.endsWith("/position"))).toBe(
|
||||
expect(requests.some(url => url.pathname.endsWith("/position"))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
@@ -384,16 +405,16 @@ describe("bond page", () => {
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
requests.some(
|
||||
(url) =>
|
||||
url.searchParams.get("categories") === "income" &&
|
||||
!url.searchParams.has("cursor"),
|
||||
url =>
|
||||
url.searchParams.get("categories") === "income"
|
||||
&& !url.searchParams.has("cursor"),
|
||||
),
|
||||
).toBe(true),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("radio", { name: "Год" }));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
requests.some((url) => url.searchParams.get("period") === "year"),
|
||||
requests.some(url => url.searchParams.get("period") === "year"),
|
||||
).toBe(true),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user