From d910d624d6583ae0a6ef66133eb7a2bb8eea3e60 Mon Sep 17 00:00:00 2001 From: codex-bot Date: Sat, 7 Nov 2026 18:44:44 +0300 Subject: [PATCH] test: remove timing races from screen-sync and route transition tests (#138) (#166) --- .../model/screen-sync.test.tsx | 31 +++++++++++++++---- src/test/setup.ts | 4 +++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/features/tbank-connection/model/screen-sync.test.tsx b/src/features/tbank-connection/model/screen-sync.test.tsx index d30379b..1a4e2c0 100644 --- a/src/features/tbank-connection/model/screen-sync.test.tsx +++ b/src/features/tbank-connection/model/screen-sync.test.tsx @@ -10,6 +10,9 @@ import { server } from "@/test/server"; import { useScreenSync } from "../index"; +const TRANSPORT_ATTEMPTS = 3; +const TRANSPORT_RECOVERY_MS = 3000; + function auth() { configureAuthClient({ accessToken: () => "synthetic", @@ -28,11 +31,13 @@ describe("read-only screen synchronization", () => { gets += 1; syncModes.push(new URL(request.url).searchParams.get("sync")); - return gets === 1 + // The transport retries a transient read twice (1 s, 2 s) on its own. + return gets <= TRANSPORT_ATTEMPTS ? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 }) : HttpResponse.json(resource([])); }), ); + vi.useFakeTimers(); const { result } = renderHook( () => { @@ -46,12 +51,26 @@ describe("read-only screen synchronization", () => { { wrapper: createQueryWrapper().wrapper }, ); - await waitFor(() => expect(gets).toBe(1)); - await act(async () => result.current.sync.retry()); - await waitFor(() => expect(result.current.accounts.data).toEqual([])); + try { + // Exhaust the transport back-off so the query settles in error. + await vi.waitFor(async () => { + await act(() => vi.advanceTimersByTimeAsync(TRANSPORT_RECOVERY_MS)); + expect(result.current.sync.transportError).toBe(true); + }); + expect(gets).toBe(TRANSPORT_ATTEMPTS); - expect(gets).toBe(2); - expect(syncModes).toEqual([null, null]); + await act(async () => result.current.sync.retry()); + await vi.waitFor(async () => { + await act(() => vi.advanceTimersByTimeAsync(0)); + expect(result.current.accounts.data).toEqual([]); + }); + } + finally { + vi.useRealTimers(); + } + + expect(gets).toBe(TRANSPORT_ATTEMPTS + 1); + expect(syncModes).toEqual(Array.from({ length: gets }, () => null)); }); it("refreshes the currently open resources together", async () => { diff --git a/src/test/setup.ts b/src/test/setup.ts index da99809..2ceb5ca 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -1,5 +1,6 @@ import "@testing-library/jest-dom/vitest"; import { cleanup, configure } from "@testing-library/react"; +import { MotionGlobalConfig } from "motion/react"; import { afterAll, afterEach, beforeAll, vi } from "vitest"; import { configureAuthClient } from "@/shared/api"; @@ -10,6 +11,9 @@ import { server } from "./server"; // Allow async UI updates on the slower CI runner; local waits stay unchanged. if (process.env.CI) configure({ asyncUtilTimeout: 5000 }); +// Route fades start at opacity 0; under load toBeVisible can catch that frame. +MotionGlobalConfig.skipAnimations = true; + beforeAll(() => server.listen({ onUnhandledRequest: "error" })); afterEach(() => { cleanup();