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,81 +1,87 @@
|
||||
import type { FC, PropsWithChildren } from "react";
|
||||
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { HttpResponse, http } from "msw";
|
||||
import type { FC, PropsWithChildren } from "react";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { MemoryRouter, Route, Routes } from "react-router";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { Portfolio } from "@/entities/portfolio";
|
||||
|
||||
import { configureAuthClient } from "@/shared/api";
|
||||
import { createQueryWrapper } from "@/test/query";
|
||||
import { resource } from "@/test/resource";
|
||||
import { server } from "@/test/server";
|
||||
|
||||
import {
|
||||
toAccountPagePositionSections,
|
||||
useAccountPageViewModel,
|
||||
} from "./AccountPage.vm";
|
||||
|
||||
const portfolio: Portfolio = {
|
||||
lastSuccessfulAt: "2026-08-13T10:00:00Z",
|
||||
isStale: true,
|
||||
totalsByCurrency: [{ currency: "RUB", value: "150" }],
|
||||
positions: [
|
||||
{
|
||||
figi: "FIGI-STOCK",
|
||||
displayName: "Акция",
|
||||
type: "STOCK",
|
||||
quantity: "2",
|
||||
currentValue: { currency: "RUB", value: "100" },
|
||||
averagePurchasePrice: null,
|
||||
absoluteReturn: { currency: "RUB", value: "10" },
|
||||
isin: null,
|
||||
ticker: null,
|
||||
logoName: "akciya.png",
|
||||
},
|
||||
{
|
||||
figi: "FIGI-BOND",
|
||||
displayName: "Облигация",
|
||||
type: "BOND",
|
||||
quantity: "1",
|
||||
currentValue: { currency: "RUB", value: "50" },
|
||||
averagePurchasePrice: { currency: "RUB", value: "45" },
|
||||
absoluteReturn: { currency: "RUB", value: "-5" },
|
||||
isin: "RU000A0JX0J2",
|
||||
ticker: null,
|
||||
logoName: null,
|
||||
},
|
||||
{
|
||||
figi: "FIGI-FUND",
|
||||
displayName: "Фонд",
|
||||
type: "FUND",
|
||||
quantity: "3",
|
||||
currentValue: null,
|
||||
averagePurchasePrice: null,
|
||||
absoluteReturn: null,
|
||||
isin: null,
|
||||
ticker: null,
|
||||
logoName: "fond.png",
|
||||
},
|
||||
],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
cashBalances: [{ currency: "USD", value: "1" }],
|
||||
};
|
||||
const portfolio: Portfolio = (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
cashBalances: [{ currency: "USD", value: "1" }],
|
||||
isStale: true,
|
||||
lastSuccessfulAt: "2026-08-13T10:00:00Z",
|
||||
positions: [
|
||||
{
|
||||
absoluteReturn: { currency: "RUB", value: "10" },
|
||||
averagePurchasePrice: null,
|
||||
currentValue: { currency: "RUB", value: "100" },
|
||||
displayName: "Акция",
|
||||
figi: "FIGI-STOCK",
|
||||
isin: null,
|
||||
logoName: "akciya.png",
|
||||
quantity: "2",
|
||||
ticker: null,
|
||||
type: "STOCK",
|
||||
},
|
||||
{
|
||||
absoluteReturn: { currency: "RUB", value: "-5" },
|
||||
averagePurchasePrice: { currency: "RUB", value: "45" },
|
||||
currentValue: { currency: "RUB", value: "50" },
|
||||
displayName: "Облигация",
|
||||
figi: "FIGI-BOND",
|
||||
isin: "RU000A0JX0J2",
|
||||
logoName: null,
|
||||
quantity: "1",
|
||||
ticker: null,
|
||||
type: "BOND",
|
||||
},
|
||||
{
|
||||
absoluteReturn: null,
|
||||
averagePurchasePrice: null,
|
||||
currentValue: null,
|
||||
displayName: "Фонд",
|
||||
figi: "FIGI-FUND",
|
||||
isin: null,
|
||||
logoName: "fond.png",
|
||||
quantity: "3",
|
||||
ticker: null,
|
||||
type: "FUND",
|
||||
},
|
||||
],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
totalsByCurrency: [{ currency: "RUB", value: "150" }],
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */);
|
||||
|
||||
function configureTestAuth() {
|
||||
configureAuthClient({
|
||||
accessToken: () => "test-token",
|
||||
refresh: async () => true,
|
||||
clear: vi.fn(),
|
||||
refresh: () => Promise.resolve(true),
|
||||
});
|
||||
}
|
||||
|
||||
function createWrapper() {
|
||||
const { wrapper: QueryWrapper } = createQueryWrapper();
|
||||
const Wrapper: FC<PropsWithChildren> = (props) => (
|
||||
const Wrapper: FC<PropsWithChildren> = props => (
|
||||
<QueryWrapper>
|
||||
<MantineProvider>
|
||||
<MemoryRouter initialEntries={["/accounts/account-1"]}>
|
||||
<Routes>
|
||||
<Route path="/accounts/:accountId" element={props.children} />
|
||||
<Route element={props.children} path="/accounts/:accountId" />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</MantineProvider>
|
||||
@@ -91,30 +97,32 @@ function mockLoadedAccountPage() {
|
||||
),
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
|
||||
HttpResponse.json(
|
||||
resource({
|
||||
accountId: "account-1",
|
||||
items: [
|
||||
{
|
||||
id: "operation-1",
|
||||
occurredAt: "2026-08-13T10:00:00Z",
|
||||
type: "Buy",
|
||||
category: "trade",
|
||||
description: "Покупка акции",
|
||||
figi: "FIGI-STOCK",
|
||||
instrumentUid: null,
|
||||
ticker: "STK",
|
||||
instrumentType: "share",
|
||||
payment: { currency: "RUB", value: "-100" },
|
||||
quantity: "1",
|
||||
price: null,
|
||||
accruedInterest: null,
|
||||
commission: null,
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
hasNext: false,
|
||||
observedAt: "2026-08-13T10:00:00Z",
|
||||
}),
|
||||
resource((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
accountId: "account-1",
|
||||
hasNext: false,
|
||||
items: [
|
||||
{
|
||||
accruedInterest: null,
|
||||
category: "trade",
|
||||
commission: null,
|
||||
description: "Покупка акции",
|
||||
figi: "FIGI-STOCK",
|
||||
id: "operation-1",
|
||||
instrumentType: "share",
|
||||
instrumentUid: null,
|
||||
occurredAt: "2026-08-13T10:00:00Z",
|
||||
payment: { currency: "RUB", value: "-100" },
|
||||
price: null,
|
||||
quantity: "1",
|
||||
ticker: "STK",
|
||||
type: "Buy",
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
observedAt: "2026-08-13T10:00:00Z",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */)),
|
||||
),
|
||||
),
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/events", () =>
|
||||
@@ -122,16 +130,18 @@ function mockLoadedAccountPage() {
|
||||
resource({
|
||||
accountId: "account-1",
|
||||
calculatedAt: "2026-08-13T10:00:00Z",
|
||||
events: Array.from({ length: 6 }, (_, index) => ({
|
||||
type: "coupon",
|
||||
date: `2026-09-${String(index + 1).padStart(2, "0")}`,
|
||||
dateKind: "event",
|
||||
figi: `FIGI-EVENT-${index}`,
|
||||
instrumentUid: null,
|
||||
ticker: `BND${index}`,
|
||||
displayName: `Купон ${index}`,
|
||||
projectedAmount: { currency: "RUB", value: String(index + 1) },
|
||||
})),
|
||||
events: Array.from({ length: 6 }, (_, index) => ((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
date: `2026-09-${String(index + 1).padStart(2, "0")}`,
|
||||
dateKind: "event",
|
||||
displayName: `Купон ${index}`,
|
||||
figi: `FIGI-EVENT-${index}`,
|
||||
instrumentUid: null,
|
||||
projectedAmount: { currency: "RUB", value: String(index + 1) },
|
||||
ticker: `BND${index}`,
|
||||
type: "coupon",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */))),
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -150,38 +160,40 @@ describe("useAccountPageViewModel", () => {
|
||||
] as const;
|
||||
const snapshot: Portfolio = {
|
||||
...portfolio,
|
||||
totalsByCurrency: [{ currency: "RUB", value: "1250" }],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
cashBalances: [{ currency: "RUB", value: "1000" }],
|
||||
positions: [
|
||||
...portfolio.positions,
|
||||
{
|
||||
...portfolio.positions[0],
|
||||
currentValue: { currency: "RUB", value: "50" },
|
||||
figi: "FIGI-CURRENCY",
|
||||
type: "CURRENCY",
|
||||
currentValue: { currency: "RUB", value: "50" },
|
||||
},
|
||||
...otherTypes.map((type) => ({
|
||||
...portfolio.positions[2],
|
||||
figi: `FIGI-${type}-NEXT`,
|
||||
type,
|
||||
currentValue:
|
||||
...otherTypes.map(type => ((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
...portfolio.positions[2],
|
||||
currentValue:
|
||||
type === "OPTION" ? { currency: "RUB", value: "50" } : null,
|
||||
})),
|
||||
figi: `FIGI-${type}-NEXT`,
|
||||
type,
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */))),
|
||||
],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
totalsByCurrency: [{ currency: "RUB", value: "1250" }],
|
||||
};
|
||||
const original = structuredClone(snapshot);
|
||||
|
||||
const sections = toAccountPagePositionSections(snapshot);
|
||||
|
||||
expect(sections.map((section) => section.kind)).toEqual([
|
||||
expect(sections.map(section => section.kind)).toEqual([
|
||||
"stocks",
|
||||
"bonds",
|
||||
"other",
|
||||
]);
|
||||
expect(sections[2].groups.map((group) => group.positions[0].figi)).toEqual([
|
||||
expect(sections[2].groups.map(group => group.positions[0].figi)).toEqual([
|
||||
"FIGI-FUND",
|
||||
...otherTypes.map((type) => `FIGI-${type}-NEXT`),
|
||||
...otherTypes.map(type => `FIGI-${type}-NEXT`),
|
||||
]);
|
||||
expect(sections[0].groups[0].positions[0].shareText).toBe("40 %");
|
||||
expect(sections[1].groups[0].positions[0].shareText).toBe("20 %");
|
||||
@@ -203,9 +215,9 @@ describe("useAccountPageViewModel", () => {
|
||||
{ state: "loaded" }
|
||||
>;
|
||||
expect(
|
||||
loaded.positionSections.map(({ kind, groups }) => ({
|
||||
loaded.positionSections.map(({ groups, kind }) => ({
|
||||
kind,
|
||||
positions: groups.flatMap((group) => group.positions),
|
||||
positions: groups.flatMap(group => group.positions),
|
||||
})),
|
||||
).toMatchObject([
|
||||
{
|
||||
@@ -213,37 +225,41 @@ describe("useAccountPageViewModel", () => {
|
||||
positions: [
|
||||
{
|
||||
displayName: "Акция",
|
||||
shareText: "66,7 %",
|
||||
logoSrc: "https://invest-brands.cdn-tinkoff.ru/akciyax160.png",
|
||||
returnText: "+10 ₽",
|
||||
returnTone: "positive",
|
||||
logoSrc: "https://invest-brands.cdn-tinkoff.ru/akciyax160.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "bonds",
|
||||
positions: [
|
||||
{
|
||||
displayName: "Облигация",
|
||||
shareText: "33,3 %",
|
||||
returnText: "-5 ₽",
|
||||
returnTone: "negative",
|
||||
logoSrc: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "other",
|
||||
positions: [
|
||||
{
|
||||
displayName: "Фонд",
|
||||
shareText: null,
|
||||
returnText: null,
|
||||
returnTone: undefined,
|
||||
logoSrc: null,
|
||||
shareText: "66,7 %",
|
||||
},
|
||||
],
|
||||
},
|
||||
(/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
kind: "bonds",
|
||||
positions: [
|
||||
{
|
||||
displayName: "Облигация",
|
||||
logoSrc: null,
|
||||
returnText: "-5 ₽",
|
||||
returnTone: "negative",
|
||||
shareText: "33,3 %",
|
||||
},
|
||||
],
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */),
|
||||
(/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
kind: "other",
|
||||
positions: [
|
||||
{
|
||||
displayName: "Фонд",
|
||||
logoSrc: null,
|
||||
returnText: null,
|
||||
returnTone: undefined,
|
||||
shareText: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -265,13 +281,11 @@ describe("useAccountPageViewModel", () => {
|
||||
{ state: "loaded" }
|
||||
>;
|
||||
expect(loaded.recentOperations).toMatchObject({
|
||||
items: [{ description: "Покупка акции", id: "operation-1" }],
|
||||
state: "loaded",
|
||||
to: "/accounts/account-1/operations",
|
||||
items: [{ id: "operation-1", description: "Покупка акции" }],
|
||||
});
|
||||
expect(loaded.recentEvents).toMatchObject({
|
||||
state: "loaded",
|
||||
to: "/accounts/account-1/events",
|
||||
items: [
|
||||
{ ticker: "BND0" },
|
||||
{ ticker: "BND1" },
|
||||
@@ -279,6 +293,8 @@ describe("useAccountPageViewModel", () => {
|
||||
{ ticker: "BND3" },
|
||||
{ ticker: "BND4" },
|
||||
],
|
||||
state: "loaded",
|
||||
to: "/accounts/account-1/events",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -302,9 +318,9 @@ describe("useAccountPageViewModel", () => {
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current).toMatchObject({
|
||||
state: "loaded",
|
||||
recentOperations: { state: "error" },
|
||||
recentEvents: { state: "error" },
|
||||
recentOperations: { state: "error" },
|
||||
state: "loaded",
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -320,7 +336,9 @@ it("keeps overview sections independent when their source data is incomplete", a
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
|
||||
HttpResponse.json(
|
||||
resource(
|
||||
{ items: [], hasNext: false, nextCursor: null },
|
||||
(/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{ hasNext: false, items: [], nextCursor: null }
|
||||
/* eslint-enable unicorn/no-null */),
|
||||
{ availability: "partial" },
|
||||
),
|
||||
),
|
||||
@@ -341,20 +359,22 @@ it("keeps overview sections independent when their source data is incomplete", a
|
||||
await waitFor(() =>
|
||||
expect(result.current).toMatchObject({
|
||||
partial: true,
|
||||
recentOperations: { state: "partial", items: [] },
|
||||
recentEvents: { state: "partial", items: [] },
|
||||
recentEvents: { items: [], state: "partial" },
|
||||
recentOperations: { items: [], state: "partial" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("refreshes every overview section through one account descriptor and observational reads", async () => {
|
||||
configureTestAuth();
|
||||
const sections: Record<string, unknown> = {
|
||||
portfolio,
|
||||
operations: { items: [], hasNext: false, nextCursor: null },
|
||||
events: { events: [], incomeTotalsByCurrency: [] },
|
||||
"income-summary": { totalsByCurrency: [] },
|
||||
};
|
||||
const sections: Record<string, unknown> = (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
"events": { events: [], incomeTotalsByCurrency: [] },
|
||||
"income-summary": { totalsByCurrency: [] },
|
||||
"operations": { hasNext: false, items: [], nextCursor: null },
|
||||
portfolio,
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */);
|
||||
const observed: string[] = [];
|
||||
const scopes: unknown[] = [];
|
||||
let version = "1";
|
||||
@@ -377,25 +397,27 @@ it("refreshes every overview section through one account descriptor and observat
|
||||
return HttpResponse.json(
|
||||
{
|
||||
id: "overview",
|
||||
scope: { screen: "account", accountId: "account-1" },
|
||||
now: "2026-09-25T00:00:00Z",
|
||||
scope: { accountId: "account-1", screen: "account" },
|
||||
},
|
||||
{ status: 202 },
|
||||
);
|
||||
}),
|
||||
http.get("*/api/v1/tbank-sync/:id", () =>
|
||||
HttpResponse.json({
|
||||
id: "overview",
|
||||
scope: null,
|
||||
now: "2026-09-25T00:00:00Z",
|
||||
state: "succeeded",
|
||||
errorCode: null,
|
||||
progress: { total: 4, completed: 4, discovering: false },
|
||||
resources: Object.keys(sections).map(
|
||||
(resourceKey) =>
|
||||
resource(null, { resourceKey, stateVersion: version }).meta,
|
||||
),
|
||||
}),
|
||||
HttpResponse.json((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
errorCode: null,
|
||||
id: "overview",
|
||||
now: "2026-09-25T00:00:00Z",
|
||||
progress: { completed: 4, discovering: false, total: 4 },
|
||||
resources: Object.keys(sections).map(
|
||||
resourceKey =>
|
||||
resource(null, { resourceKey, stateVersion: version }).meta,
|
||||
),
|
||||
scope: null,
|
||||
state: "succeeded",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */)),
|
||||
),
|
||||
);
|
||||
const { result } = renderHook(() => useAccountPageViewModel(), {
|
||||
@@ -403,10 +425,10 @@ it("refreshes every overview section through one account descriptor and observat
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(result.current).toMatchObject({
|
||||
state: "loaded",
|
||||
recentOperations: { state: "empty" },
|
||||
recentEvents: { state: "empty" },
|
||||
incomeSummary: { state: "empty" },
|
||||
recentEvents: { state: "empty" },
|
||||
recentOperations: { state: "empty" },
|
||||
state: "loaded",
|
||||
}),
|
||||
);
|
||||
await act(async () => {
|
||||
@@ -415,5 +437,5 @@ it("refreshes every overview section through one account descriptor and observat
|
||||
await waitFor(() =>
|
||||
expect(new Set(observed)).toEqual(new Set(Object.keys(sections))),
|
||||
);
|
||||
expect(scopes).toEqual([{ screen: "account", accountId: "account-1" }]);
|
||||
expect(scopes).toEqual([{ accountId: "account-1", screen: "account" }]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user