chore: add typed ESLint checks #18
Frontend security / security (push) Successful in 9s
Frontend verify / verify (push) Failing after 17m37s
Conventional commits / commits (pull_request) Successful in 26s
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Failing after 22m42s

Refs WealthLedger/main#18. Keep Biome; add no Prettier dependencies.
This commit is contained in:
2026-09-28 05:52:29 +03:00
parent c1b08735e3
commit 851457e03c
38 changed files with 2914 additions and 155 deletions
@@ -18,6 +18,57 @@ function auth() {
});
}
describe("page-owned screen synchronization", () => {
it("ignores a late sync acceptance after the account scope changes", async () => {
auth();
let release!: () => void;
const blocked = new Promise<void>((resolve) => {
release = resolve;
});
const posted: string[] = [];
const polled: string[] = [];
server.use(
http.post("*/api/v1/tbank-sync", async ({ request }) => {
const scope = (await request.json()) as {
screen: "account";
accountId: string;
};
posted.push(scope.accountId);
await blocked;
return HttpResponse.json(
{ id: scope.accountId, scope, now: "2026-09-25T00:00:00Z" },
{ status: 202 },
);
}),
http.get("*/api/v1/tbank-sync/:id", ({ params }) => {
polled.push(String(params.id));
return HttpResponse.json({
id: params.id,
state: "succeeded",
resources: [],
});
}),
);
const { result, rerender } = renderHook(
({ accountId }) => useScreenSync({ screen: "account", accountId }, []),
{
initialProps: { accountId: "old" },
wrapper: createQueryWrapper().wrapper,
},
);
let pending!: Promise<void>;
act(() => {
pending = result.current.refresh();
});
await waitFor(() => expect(posted).toEqual(["old"]));
rerender({ accountId: "new" });
await act(async () => {
release();
await pending;
});
await act(() => result.current.refresh());
await waitFor(() => expect(polled).toEqual(["new"]));
expect(posted).toEqual(["old", "new"]);
});
it.each([false, true])(
"retries a failed GET without forcing the source (cached=%s)",
async (cached) => {
@@ -155,7 +206,7 @@ describe("page-owned screen synchronization", () => {
);
expect(published).not.toContainEqual(
expect.objectContaining({
meta: expect.objectContaining({ stateVersion: "old" }),
meta: expect.objectContaining({ stateVersion: "old" }) as unknown,
}),
);
unsubscribe();
@@ -354,7 +405,7 @@ describe("page-owned screen synchronization", () => {
});
expect(modes).toEqual([null, "observe"]);
if (boundary === "leave") unmount();
else await act(() => configureAuthClient(null));
else act(() => configureAuthClient(null));
const stopped = polls;
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 1100));
@@ -1,5 +1,5 @@
import { type QueryKey, useQueryClient } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import {
apiErrorCode,
apiQueryClient,
@@ -33,7 +33,9 @@ function useScreenSync(scope: InvestmentScreen, resources: ScreenResource[]) {
} | null>(null);
const requestInFlight = useRef<string | null>(null);
const currentScope = useRef(scopeKey);
currentScope.current = scopeKey;
useLayoutEffect(() => {
currentScope.current = scopeKey;
}, [scopeKey]);
const currentMutation = JSON.stringify(mutation.variables?.body) === scopeKey;
const observed = useRef(new Map<string, string>());
const ids = [
@@ -56,7 +56,10 @@ describe("sync connection and session boundaries", () => {
const { wrapper, queryClient } = createQueryWrapper();
const published: string[] = [];
const unsubscribe = queryClient.getQueryCache().subscribe((event) => {
if (event.query.queryKey[1] === "/api/v1/tbank-sync/{id}") {
if (
(event.query.queryKey as readonly unknown[])[1] ===
"/api/v1/tbank-sync/{id}"
) {
const data = event.query.state.data as { now?: string } | undefined;
if (data?.now) published.push(data.now);
}
@@ -73,6 +76,7 @@ describe("sync connection and session boundaries", () => {
act(() => {
mutation = result.current.mutation.mutateAsync({ body: screen });
});
// eslint-disable-next-line vitest/valid-expect -- Attach rejection handling before releasing the request; the assertion is awaited below.
const discarded = expect(mutation).rejects.toMatchObject({
name: "AbortError",
});
@@ -163,11 +163,9 @@ describe("TbankConnectionSection", () => {
});
fireEvent.click(screen.getByRole("button", { name: "Подключить" }));
await waitFor(() =>
expect(
screen.getByRole("button", { name: "Заменить token" }),
).toBeInTheDocument(),
);
expect(
await screen.findByRole("button", { name: "Заменить token" }),
).toBeInTheDocument();
expect(client.getQueryData(brokerageAccountsQueryKey)).toBeUndefined();
expect(client.getQueryData(bankCardsQueryKey)).toEqual([
{ id: "bank-card-1" },
@@ -224,11 +222,9 @@ describe("TbankConnectionSection", () => {
);
fireEvent.click(await screen.findByRole("button", { name: /^Удалить$/ }));
await waitFor(() =>
expect(
screen.getByRole("button", { name: "Подключить" }),
).toBeInTheDocument(),
);
expect(
await screen.findByRole("button", { name: "Подключить" }),
).toBeInTheDocument();
expect(client.getQueryData(brokerageAccountsQueryKey)).toBeUndefined();
expect(client.getQueryData(bankCardsQueryKey)).toEqual([
{ id: "bank-card-1" },