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,10 +1,12 @@
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { HttpResponse, http } from "msw";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { apiClient, configureAuthClient } from "@/shared/api";
|
||||
import { createQueryWrapper } from "@/test/query";
|
||||
import { resource } from "@/test/resource";
|
||||
import { server } from "@/test/server";
|
||||
|
||||
import {
|
||||
type Portfolio,
|
||||
portfolioQueryOptions,
|
||||
@@ -12,25 +14,25 @@ import {
|
||||
} from "./usePortfolio";
|
||||
|
||||
const snapshot: Portfolio = {
|
||||
positions: [],
|
||||
totalsByCurrency: [{ currency: "RUB", value: "123.000000001" }],
|
||||
cashBalances: [],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
lastSuccessfulAt: "2026-08-14T00:00:00Z",
|
||||
isStale: false,
|
||||
lastSuccessfulAt: "2026-08-14T00:00:00Z",
|
||||
positions: [],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
totalsByCurrency: [{ currency: "RUB", value: "123.000000001" }],
|
||||
};
|
||||
function auth() {
|
||||
configureAuthClient({
|
||||
accessToken: () => "synthetic",
|
||||
refresh: async () => true,
|
||||
clear: vi.fn(),
|
||||
refresh: () => Promise.resolve(true),
|
||||
});
|
||||
}
|
||||
|
||||
describe("persistent resource query lifecycle", () => {
|
||||
it("observes into the canonical cache and rejects an old notified state before accepting the current one", async () => {
|
||||
auth();
|
||||
const modes: (string | null)[] = [];
|
||||
const modes: (null | string)[] = [];
|
||||
let observations = 0;
|
||||
server.use(
|
||||
http.get(
|
||||
@@ -47,16 +49,17 @@ describe("persistent resource query lifecycle", () => {
|
||||
},
|
||||
),
|
||||
);
|
||||
const { wrapper, queryClient } = createQueryWrapper();
|
||||
const { queryClient, wrapper } = createQueryWrapper();
|
||||
const { result } = renderHook(() => usePortfolio("a"), { wrapper });
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
await act(() => result.current.observe("new"));
|
||||
await waitFor(() => expect(result.current.meta?.stateVersion).toBe("new"));
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
|
||||
expect(modes).toEqual([null, "observe", "observe"]);
|
||||
expect(queryClient.getQueryCache().getAll()).toHaveLength(1);
|
||||
expect(
|
||||
queryClient.getQueryData(portfolioQueryOptions("a").queryKey),
|
||||
).toMatchObject({ meta: { stateVersion: "new" }, data: snapshot });
|
||||
).toMatchObject({ data: snapshot, meta: { stateVersion: "new" } });
|
||||
});
|
||||
it("does not convert missing or failed source data to empty financial success", async () => {
|
||||
auth();
|
||||
@@ -65,9 +68,9 @@ describe("persistent resource query lifecycle", () => {
|
||||
HttpResponse.json(
|
||||
resource(null, {
|
||||
sync: {
|
||||
errorCode: "TBANK_UNAVAILABLE",
|
||||
id: "work",
|
||||
state: "failed",
|
||||
errorCode: "TBANK_UNAVAILABLE",
|
||||
},
|
||||
}),
|
||||
),
|
||||
@@ -82,20 +85,22 @@ describe("persistent resource query lifecycle", () => {
|
||||
expect(result.current.status).toBe("pending");
|
||||
expect(result.current.isPending).toBe(true);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.transport).toEqual({
|
||||
status: "success",
|
||||
fetchStatus: "idle",
|
||||
error: null,
|
||||
});
|
||||
expect(result.current.transport).toEqual((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
error: null,
|
||||
fetchStatus: "idle",
|
||||
status: "success",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */));
|
||||
expect(result.current.meta?.availability).toBe("missing");
|
||||
});
|
||||
it("keeps the refetch envelope while exposing the refreshed resource payload", async () => {
|
||||
auth();
|
||||
let ready = false;
|
||||
let isReady = false;
|
||||
const refreshed = resource(snapshot, { stateVersion: "ready" });
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
|
||||
HttpResponse.json(ready ? refreshed : resource(null)),
|
||||
HttpResponse.json(isReady ? refreshed : resource(null)),
|
||||
),
|
||||
);
|
||||
const { result } = renderHook(() => usePortfolio("a"), {
|
||||
@@ -109,7 +114,7 @@ describe("persistent resource query lifecycle", () => {
|
||||
expect(missing.status).toBe("success");
|
||||
expect(missing.data).toEqual(resource(null));
|
||||
expect(missing.data?.data).toBeNull();
|
||||
ready = true;
|
||||
isReady = true;
|
||||
const loaded = await result.current.refetch();
|
||||
expect(loaded.data).toEqual(refreshed);
|
||||
expect(loaded.data?.data?.totalsByCurrency).toEqual(
|
||||
@@ -124,10 +129,8 @@ describe("persistent resource query lifecycle", () => {
|
||||
});
|
||||
it("rejects a late ordinary response when an observe response for that resource arrived first", async () => {
|
||||
auth();
|
||||
let release!: () => void;
|
||||
const blocked = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
|
||||
const { promise: blocked, resolve: release } = Promise.withResolvers<void>();
|
||||
server.use(
|
||||
http.get(
|
||||
"*/api/v1/brokerage-accounts/:accountId/portfolio",
|
||||
@@ -151,19 +154,15 @@ describe("persistent resource query lifecycle", () => {
|
||||
});
|
||||
it("clears private snapshots on confirmed access loss and fences another pending success", async () => {
|
||||
auth();
|
||||
let denied = false;
|
||||
let release!: () => void;
|
||||
const blocked = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
let isDenied = false;
|
||||
|
||||
const { promise: blocked, resolve: release } = Promise.withResolvers<void>();
|
||||
server.use(
|
||||
http.get(
|
||||
"*/api/v1/brokerage-accounts/:accountId/portfolio",
|
||||
async ({ params }) => {
|
||||
if (params.accountId === "b") await blocked;
|
||||
if (params.accountId === "a" && denied)
|
||||
return HttpResponse.json({ code: "NOT_FOUND" }, { status: 404 });
|
||||
return HttpResponse.json(resource(snapshot));
|
||||
return isDenied && params.accountId === "a" ? HttpResponse.json({ code: "NOT_FOUND" }, { status: 404 }) : HttpResponse.json(resource(snapshot));
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -173,7 +172,7 @@ describe("persistent resource query lifecycle", () => {
|
||||
{ wrapper },
|
||||
);
|
||||
await waitFor(() => expect(result.current.a.data).toEqual(snapshot));
|
||||
denied = true;
|
||||
isDenied = true;
|
||||
await act(() => result.current.a.refetch());
|
||||
await waitFor(() => expect(result.current.a.data).toBeUndefined());
|
||||
await act(async () => {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { HttpResponse, http } from "msw";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { configureAuthClient } from "@/shared/api";
|
||||
import { createQueryWrapper } from "@/test/query";
|
||||
import { resource } from "@/test/resource";
|
||||
import { server } from "@/test/server";
|
||||
|
||||
import { usePortfolio } from "./usePortfolio";
|
||||
|
||||
describe("usePortfolio", () => {
|
||||
it("loads the account portfolio", async () => {
|
||||
configureAuthClient({
|
||||
accessToken: () => "token",
|
||||
refresh: async () => true,
|
||||
clear: vi.fn(),
|
||||
refresh: () => Promise.resolve(true),
|
||||
});
|
||||
const { result } = renderHook(() => usePortfolio("account-1"), {
|
||||
wrapper: createQueryWrapper().wrapper,
|
||||
@@ -25,8 +27,8 @@ describe("usePortfolio", () => {
|
||||
it("keeps the current connection snapshot after a background refetch error", async () => {
|
||||
configureAuthClient({
|
||||
accessToken: () => "token",
|
||||
refresh: async () => true,
|
||||
clear: vi.fn(),
|
||||
refresh: () => Promise.resolve(true),
|
||||
});
|
||||
let calls = 0;
|
||||
server.use(
|
||||
@@ -35,12 +37,12 @@ describe("usePortfolio", () => {
|
||||
return calls === 1
|
||||
? HttpResponse.json(
|
||||
resource({
|
||||
lastSuccessfulAt: "2026-08-14T00:00:00Z",
|
||||
cashBalances: [],
|
||||
isStale: false,
|
||||
totalsByCurrency: [],
|
||||
lastSuccessfulAt: "2026-08-14T00:00:00Z",
|
||||
positions: [],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
cashBalances: [],
|
||||
totalsByCurrency: [],
|
||||
}),
|
||||
)
|
||||
: HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { formatShare } from "./display";
|
||||
|
||||
describe("portfolio display helpers", () => {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import type { Portfolio } from "../api/usePortfolio";
|
||||
|
||||
const PERCENT_SCALE = 100;
|
||||
|
||||
type Money = Portfolio["totalsByCurrency"][number];
|
||||
|
||||
function formatShare(
|
||||
numerator: Money | null,
|
||||
denominator: Money | null,
|
||||
): string | null {
|
||||
if (!numerator || !denominator) return null;
|
||||
): null | string {
|
||||
if (!numerator || !denominator) return /* eslint-disable-line unicorn/no-null -- The existing formatter contract uses null for unavailable data. */ null;
|
||||
const total = Number(denominator.value);
|
||||
if (!Number.isFinite(total) || total <= 0) return null;
|
||||
// biome-ignore lint/style/noMagicNumbers: obvious conversion of a ratio to percent.
|
||||
const percent = (Number(numerator.value) / total) * 100;
|
||||
if (!Number.isFinite(percent)) return null;
|
||||
if (!Number.isFinite(total) || total <= 0) return /* eslint-disable-line unicorn/no-null -- The existing formatter contract uses null for unavailable data. */ null;
|
||||
const percent = (Number(numerator.value) / total) * PERCENT_SCALE;
|
||||
if (!Number.isFinite(percent)) return /* eslint-disable-line unicorn/no-null -- The existing formatter contract uses null for unavailable data. */ null;
|
||||
const rounded = new Intl.NumberFormat("ru-RU", {
|
||||
maximumFractionDigits: 1,
|
||||
}).format(percent);
|
||||
|
||||
Reference in New Issue
Block a user