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,9 +1,11 @@
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { HttpResponse, http } from "msw";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { apiClient, configureAuthClient } from "@/shared/api";
|
||||
import { createQueryWrapper } from "@/test/query";
|
||||
import { server } from "@/test/server";
|
||||
|
||||
import { useBankMutations } from "./mutations";
|
||||
import {
|
||||
type BankCard,
|
||||
@@ -17,32 +19,34 @@ import {
|
||||
useBankSummary,
|
||||
} from "./queries";
|
||||
|
||||
const card: BankCard = {
|
||||
id: "a",
|
||||
name: "Daily",
|
||||
bankName: null,
|
||||
lastFour: null,
|
||||
paymentSystem: null,
|
||||
openingBalance: { currency: "RUB", value: "100" },
|
||||
openingDate: "2026-09-01",
|
||||
balance: { currency: "RUB", value: "100" },
|
||||
archivedAt: null,
|
||||
version: 1,
|
||||
};
|
||||
const card: BankCard = (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
archivedAt: null,
|
||||
balance: { currency: "RUB", value: "100" },
|
||||
bankName: null,
|
||||
id: "a",
|
||||
lastFour: null,
|
||||
name: "Daily",
|
||||
openingBalance: { currency: "RUB", value: "100" },
|
||||
openingDate: "2026-09-01",
|
||||
paymentSystem: null,
|
||||
version: 1,
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */);
|
||||
const transfer: CreateBankOperation = {
|
||||
amount: { currency: "RUB", value: "90071992547409.93" },
|
||||
cardId: "a",
|
||||
date: "2026-09-12",
|
||||
id: "4e6caa65-dd52-4629-81e2-ce8a139a0612",
|
||||
kind: "transfer",
|
||||
cardId: "a",
|
||||
targetCardId: "b",
|
||||
amount: { currency: "RUB", value: "90071992547409.93" },
|
||||
date: "2026-09-12",
|
||||
};
|
||||
|
||||
function authenticate() {
|
||||
configureAuthClient({
|
||||
accessToken: () => "token",
|
||||
refresh: async () => true,
|
||||
clear: vi.fn(),
|
||||
refresh: () => Promise.resolve(true),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,30 +63,28 @@ function readHandlers(counts: Map<string, number>, categoryName = "Food") {
|
||||
counts.set(key, (counts.get(key) ?? 0) + 1);
|
||||
if (url.pathname.endsWith("/categories"))
|
||||
return HttpResponse.json([
|
||||
{ id: "food", name: categoryName, isDefault: true, version: 1 },
|
||||
{ id: "food", isDefault: true, name: categoryName, version: 1 },
|
||||
]);
|
||||
if (url.pathname.endsWith("/summary"))
|
||||
return HttpResponse.json({
|
||||
month: url.searchParams.get("month"),
|
||||
cardId: url.searchParams.get("cardId"),
|
||||
expenses: { currency: "RUB", value: "0" },
|
||||
refunds: { currency: "RUB", value: "0" },
|
||||
net: { currency: "RUB", value: "0" },
|
||||
categories: [],
|
||||
expenses: { currency: "RUB", value: "0" },
|
||||
month: url.searchParams.get("month"),
|
||||
net: { currency: "RUB", value: "0" },
|
||||
refunds: { currency: "RUB", value: "0" },
|
||||
});
|
||||
if (
|
||||
url.pathname.endsWith("/operations") ||
|
||||
url.pathname.endsWith("/expenses")
|
||||
url.pathname.endsWith("/operations")
|
||||
|| url.pathname.endsWith("/expenses")
|
||||
)
|
||||
return HttpResponse.json({
|
||||
hasNext: false,
|
||||
items: [],
|
||||
page: 1,
|
||||
total: 0,
|
||||
hasNext: false,
|
||||
});
|
||||
if (url.pathname.includes("/operations/"))
|
||||
return HttpResponse.json({ ...transfer, version: 1 });
|
||||
return HttpResponse.json({ ...card, id: url.pathname.split("/").at(-1) });
|
||||
return url.pathname.includes("/operations/") ? HttpResponse.json({ ...transfer, version: 1 }) : HttpResponse.json({ ...card, id: url.pathname.split("/").at(-1) });
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -93,15 +95,16 @@ function useReadModels() {
|
||||
const b = useBankCard("b");
|
||||
const categories = useBankCategories();
|
||||
const history = useBankOperations("a", {
|
||||
limit: 20,
|
||||
month: "2026-09",
|
||||
page: 2,
|
||||
limit: 20,
|
||||
});
|
||||
const expenses = useBankExpenses("a", { page: 1 });
|
||||
const september = useBankSummary("2026-09");
|
||||
const august = useBankSummary("2026-08", "b");
|
||||
const original = useBankOperation("original");
|
||||
return {
|
||||
mutations: useBankMutations(),
|
||||
reads: [
|
||||
cards,
|
||||
a,
|
||||
@@ -113,7 +116,6 @@ function useReadModels() {
|
||||
august,
|
||||
original,
|
||||
],
|
||||
mutations: useBankMutations(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -122,7 +124,7 @@ describe("banking API", () => {
|
||||
|
||||
it("loads every page for all mode and fails without exposing partial results", async () => {
|
||||
const pages: number[] = [];
|
||||
let fail = false;
|
||||
let isFail = false;
|
||||
server.use(
|
||||
http.get("*/api/v1/bank-cards/a/operations", ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
@@ -130,33 +132,33 @@ describe("banking API", () => {
|
||||
pages.push(page);
|
||||
expect(url.searchParams.get("limit")).toBe("100");
|
||||
expect(url.searchParams.get("month")).toBe("2026-09");
|
||||
if (fail && page === 2)
|
||||
if (isFail && page === 2)
|
||||
return HttpResponse.json({ code: "FAILED" }, { status: 500 });
|
||||
return HttpResponse.json({
|
||||
items: [{ ...transfer, id: String(page) }],
|
||||
total: 2,
|
||||
page,
|
||||
hasNext: page === 1,
|
||||
items: [{ ...transfer, id: String(page) }],
|
||||
page,
|
||||
total: 2,
|
||||
});
|
||||
}),
|
||||
);
|
||||
const { result } = renderHook(
|
||||
() => useBankOperations("a", { month: "2026-09", page: 1, limit: "all" }),
|
||||
() => useBankOperations("a", { limit: "all", month: "2026-09", page: 1 }),
|
||||
{ wrapper: createQueryWrapper().wrapper },
|
||||
);
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(pages).toEqual([1, 2]);
|
||||
expect(result.current.data?.items.map((item) => item.id)).toEqual([
|
||||
expect(result.current.data?.items.map(item => item.id)).toEqual([
|
||||
"1",
|
||||
"2",
|
||||
]);
|
||||
fail = true;
|
||||
const failed = renderHook(
|
||||
() => useBankOperations("a", { month: "2026-09", page: 1, limit: "all" }),
|
||||
isFail = true;
|
||||
const { result: failedResult } = renderHook(
|
||||
() => useBankOperations("a", { limit: "all", month: "2026-09", page: 1 }),
|
||||
{ wrapper: createQueryWrapper().wrapper },
|
||||
);
|
||||
await waitFor(() => expect(failed.result.current.isError).toBe(true));
|
||||
expect(failed.result.current.data).toBeUndefined();
|
||||
await waitFor(() => expect(failedResult.current.isError).toBe(true));
|
||||
expect(failedResult.current.data).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses session-aware banking endpoints and explicit month/page without T-Bank", async () => {
|
||||
@@ -166,29 +168,29 @@ describe("banking API", () => {
|
||||
wrapper: createQueryWrapper().wrapper,
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(result.current.reads.every((read) => read.isSuccess)).toBe(true),
|
||||
expect(result.current.reads.every(read => read.isSuccess)).toBe(true),
|
||||
);
|
||||
expect([...counts.keys()]).toContain(
|
||||
expect(counts.keys()).toContain(
|
||||
"/api/v1/bank-cards?includeArchived=true",
|
||||
);
|
||||
expect([...counts.keys()]).toContain(
|
||||
"/api/v1/bank-cards/a/operations?month=2026-09&page=2&limit=20",
|
||||
expect(counts.keys()).toContain(
|
||||
"/api/v1/bank-cards/a/operations?limit=20&month=2026-09&page=2",
|
||||
);
|
||||
expect([...counts.keys()]).toContain(
|
||||
"/api/v1/bank-cards/summary?month=2026-08&cardId=b",
|
||||
expect(counts.keys()).toContain(
|
||||
"/api/v1/bank-cards/summary?cardId=b&month=2026-08",
|
||||
);
|
||||
expect(counts.size).toBe(9);
|
||||
});
|
||||
|
||||
it("replays the exact UUID and large Money string once across shared auth refresh", async () => {
|
||||
let token = "expired";
|
||||
const refresh = vi.fn(async () => {
|
||||
const refresh = vi.fn(() => {
|
||||
token = "renewed";
|
||||
return true;
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
configureAuthClient({ accessToken: () => token, refresh, clear: vi.fn() });
|
||||
configureAuthClient({ accessToken: () => token, clear: vi.fn(), refresh });
|
||||
const bodies: unknown[] = [];
|
||||
const headers: (string | null)[] = [];
|
||||
const headers: (null | string)[] = [];
|
||||
server.use(
|
||||
http.post("*/api/v1/bank-cards/operations", async ({ request }) => {
|
||||
bodies.push(await request.json());
|
||||
@@ -232,10 +234,8 @@ describe("banking API", () => {
|
||||
|
||||
it("keeps the confirmed balance while a mutation is pending or fails, and on refetch failure", async () => {
|
||||
server.use(...readHandlers(new Map()));
|
||||
let finish: () => void = () => {};
|
||||
const pending = new Promise<void>((resolve) => {
|
||||
finish = resolve;
|
||||
});
|
||||
|
||||
const { promise: pending, resolve: finish } = Promise.withResolvers<void>();
|
||||
server.use(
|
||||
http.post("*/api/v1/bank-cards/operations", async () => {
|
||||
await pending;
|
||||
@@ -243,7 +243,7 @@ describe("banking API", () => {
|
||||
}),
|
||||
);
|
||||
const { result } = renderHook(
|
||||
() => ({ query: useBankCard("a"), mutations: useBankMutations() }),
|
||||
() => ({ mutations: useBankMutations(), query: useBankCard("a") }),
|
||||
{ wrapper: createQueryWrapper().wrapper },
|
||||
);
|
||||
await waitFor(() => expect(result.current.query.isSuccess).toBe(true));
|
||||
@@ -251,7 +251,8 @@ describe("banking API", () => {
|
||||
act(() => {
|
||||
saving = result.current.mutations.createOperation
|
||||
.mutateAsync(transfer)
|
||||
.catch(() => undefined);
|
||||
// eslint-disable-next-line unicorn/prefer-await -- Attach the handler immediately and retain the pending promise for queue/race ordering.
|
||||
.catch(() => {});
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(result.current.mutations.createOperation.isPending).toBe(true),
|
||||
@@ -280,17 +281,18 @@ describe("banking API", () => {
|
||||
HttpResponse.json({ ...transfer, version: 1 }),
|
||||
),
|
||||
);
|
||||
const { wrapper, queryClient } = createQueryWrapper();
|
||||
const { queryClient, wrapper } = createQueryWrapper();
|
||||
const brokerKey = ["get", "/api/v1/brokerage-accounts"];
|
||||
queryClient.setQueryData(brokerKey, []);
|
||||
const { result } = renderHook(useReadModels, { wrapper });
|
||||
await waitFor(() =>
|
||||
expect(result.current.reads.every((read) => read.isSuccess)).toBe(true),
|
||||
expect(result.current.reads.every(read => read.isSuccess)).toBe(true),
|
||||
);
|
||||
await act(() =>
|
||||
result.current.mutations.createOperation.mutateAsync(transfer),
|
||||
);
|
||||
expect([...counts.values()]).toEqual(Array(9).fill(2));
|
||||
expect(counts.size).toBe(9);
|
||||
for (const count of counts.values()) expect(count).toBe(2);
|
||||
expect(queryClient.getQueryState(brokerKey)?.isInvalidated).toBe(false);
|
||||
});
|
||||
|
||||
@@ -316,8 +318,8 @@ describe("banking API", () => {
|
||||
await act(async () => {
|
||||
await expect(
|
||||
result.current.mutations.updateCard.mutateAsync({
|
||||
id: "a",
|
||||
body: { name: "changed", version: 1 },
|
||||
id: "a",
|
||||
}),
|
||||
).rejects.toEqual({ code });
|
||||
});
|
||||
@@ -329,7 +331,7 @@ describe("banking API", () => {
|
||||
let name = "Food";
|
||||
server.use(
|
||||
http.get("*/api/v1/bank-cards/categories", () =>
|
||||
HttpResponse.json([{ id: "food", name, isDefault: true, version: 1 }]),
|
||||
HttpResponse.json([{ id: "food", isDefault: true, name, version: 1 }]),
|
||||
),
|
||||
http.patch("*/api/v1/bank-cards/categories/food", async ({ request }) => {
|
||||
const body = await request.json();
|
||||
@@ -337,8 +339,8 @@ describe("banking API", () => {
|
||||
name = "Groceries";
|
||||
return HttpResponse.json({
|
||||
id: "food",
|
||||
name,
|
||||
isDefault: true,
|
||||
name,
|
||||
version: 2,
|
||||
});
|
||||
}),
|
||||
@@ -366,8 +368,8 @@ describe("banking API", () => {
|
||||
);
|
||||
await act(() =>
|
||||
result.current.mutations.updateCategory.mutateAsync({
|
||||
id: "food",
|
||||
body: { name: "Groceries", version: 1 },
|
||||
id: "food",
|
||||
}),
|
||||
);
|
||||
await waitFor(() =>
|
||||
@@ -375,8 +377,8 @@ describe("banking API", () => {
|
||||
);
|
||||
await act(() =>
|
||||
result.current.mutations.updateOperation.mutateAsync({
|
||||
id: "original",
|
||||
body: { description: "corrected", version: 1 },
|
||||
id: "original",
|
||||
}),
|
||||
);
|
||||
await act(() =>
|
||||
@@ -403,8 +405,8 @@ describe("banking API", () => {
|
||||
}),
|
||||
),
|
||||
);
|
||||
const { wrapper, queryClient } = createQueryWrapper();
|
||||
const { result, rerender } = renderHook(() => useBankCard("a"), {
|
||||
const { queryClient, wrapper } = createQueryWrapper();
|
||||
const { rerender, result } = renderHook(() => useBankCard("a"), {
|
||||
wrapper,
|
||||
});
|
||||
await waitFor(() => expect(result.current.data?.balance.value).toBe("100"));
|
||||
@@ -413,7 +415,7 @@ describe("banking API", () => {
|
||||
});
|
||||
expect(result.current.data?.balance.value).toBe("100");
|
||||
act(() => {
|
||||
configureAuthClient(null);
|
||||
configureAuthClient();
|
||||
queryClient.clear();
|
||||
});
|
||||
rerender();
|
||||
|
||||
Reference in New Issue
Block a user