From f3725c202bd8cef4ba711085df82a921a2da6b7d Mon Sep 17 00:00:00 2001 From: codex-bot Date: Wed, 14 Oct 2026 14:13:41 +0300 Subject: [PATCH] test: await client settlement in late-response cases (WealthLedger/frontend#61) (#81) --- src/pages/bond/ui/BondPage/BondPage.test.tsx | 7 +--- .../stocks/ui/StocksPage/StocksPage.test.tsx | 33 +++++++++++++++---- .../stocks/ui/StocksPage/StocksPage.vm.ts | 3 ++ src/shared/api/resource-query.test.tsx | 10 ++++-- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/pages/bond/ui/BondPage/BondPage.test.tsx b/src/pages/bond/ui/BondPage/BondPage.test.tsx index fada25a..3c5fa52 100644 --- a/src/pages/bond/ui/BondPage/BondPage.test.tsx +++ b/src/pages/bond/ui/BondPage/BondPage.test.tsx @@ -88,7 +88,6 @@ const quote function setup( options: { ambiguousTicker?: boolean; - failBond?: boolean; failCoupons?: boolean; noPosition?: boolean; path?: string; @@ -100,11 +99,7 @@ function setup( const resolved = { ...bond, slug: options.slug ?? uid }; let isFailQuote = false; server.use( - http.get("*/api/v1/bonds/:uid", () => - options.failBond - ? HttpResponse.json({ code: "NOT_FOUND" }, { status: 404 }) - : HttpResponse.json(resource(resolved)), - ), + http.get("*/api/v1/bonds/:uid", () => HttpResponse.json(resource(resolved))), http.get("*/api/v1/bonds/by-ticker/:ticker", ({ request }) => { requests.push(new URL(request.url)); diff --git a/src/pages/stocks/ui/StocksPage/StocksPage.test.tsx b/src/pages/stocks/ui/StocksPage/StocksPage.test.tsx index d36d5b0..76718f6 100644 --- a/src/pages/stocks/ui/StocksPage/StocksPage.test.tsx +++ b/src/pages/stocks/ui/StocksPage/StocksPage.test.tsx @@ -1,6 +1,6 @@ import { MantineProvider } from "@mantine/core"; import { QueryClientProvider } from "@tanstack/react-query"; -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { http, HttpResponse } from "msw"; import { MemoryRouter } from "react-router"; import { afterEach, describe, expect, it, vi } from "vitest"; @@ -12,19 +12,37 @@ import { server } from "@/test/server"; import { StocksPage } from "./StocksPage"; import { useStocksPageVm } from "./StocksPage.vm"; +const pageOperations: Promise[] = []; + const StocksPageVmProbe = () => { const vm = useStocksPageVm(); return ( <> - - + + {vm.items.map(item => item.name).join(", ")} ); }; -afterEach(() => vi.useRealTimers()); +afterEach(() => { + pageOperations.length = 0; + + return vi.useRealTimers(); +}); describe("stock search", () => { it("waits for a meaningful query and uses the server-verified ticker link", async () => { @@ -230,9 +248,12 @@ describe("stock search", () => { fireEvent.click(screen.getByRole("button", { name: "page" })); fireEvent.click(screen.getByRole("button", { name: "search" })); await screen.findByText("Новый"); - oldPage.resolve(); + await act(async () => { + oldPage.resolve(); + await pageOperations[0]; + }); - await waitFor(() => expect(screen.getByText("Новый")).toBeVisible()); + expect(screen.getByText("Новый")).toBeVisible(); expect(screen.queryByText("Старый результат")).not.toBeInTheDocument(); }); diff --git a/src/pages/stocks/ui/StocksPage/StocksPage.vm.ts b/src/pages/stocks/ui/StocksPage/StocksPage.vm.ts index 8ce87d7..81f6986 100644 --- a/src/pages/stocks/ui/StocksPage/StocksPage.vm.ts +++ b/src/pages/stocks/ui/StocksPage/StocksPage.vm.ts @@ -101,6 +101,7 @@ function useStocksPageVm() { }; return { + changePage, criteria, error, items: page?.items ?? [], @@ -111,6 +112,8 @@ function useStocksPageVm() { onSearch: () => void search(), page: page?.page, resultTime: page?.asOf, + // Test seam: settle the actual operation instead of the handler release. + search, searched: page !== null, totalPages: page ? page.page + Number(page.hasNext) : 0, }; diff --git a/src/shared/api/resource-query.test.tsx b/src/shared/api/resource-query.test.tsx index 5bb2f07..a6ddf93 100644 --- a/src/shared/api/resource-query.test.tsx +++ b/src/shared/api/resource-query.test.tsx @@ -164,10 +164,16 @@ describe("persistent resource query lifecycle", () => { }); await waitFor(() => expect(isStarted).toBe(true)); - await act(() => result.current.observe()); + // Capture the observation promise before releasing the old handler; the + // final assertion must follow its settlement, not the handler release. + let observation: Promise = Promise.resolve(); + await act(async () => { + observation = result.current.observe() ?? Promise.resolve(); + await observation; + }); await act(async () => { release(); - await blocked; + await observation; }); expect(result.current.data).toEqual(snapshot); });