test: await client settlement in late-response cases (WealthLedger/frontend#61) (#81)
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 10s
Frontend verify / verify (push) Successful in 7m35s

This commit was merged in pull request #81.
This commit is contained in:
2026-10-14 14:13:41 +03:00
parent edc7097bd7
commit f3725c202b
4 changed files with 39 additions and 14 deletions
+1 -6
View File
@@ -88,7 +88,6 @@ const quote
function setup( function setup(
options: { options: {
ambiguousTicker?: boolean; ambiguousTicker?: boolean;
failBond?: boolean;
failCoupons?: boolean; failCoupons?: boolean;
noPosition?: boolean; noPosition?: boolean;
path?: string; path?: string;
@@ -100,11 +99,7 @@ function setup(
const resolved = { ...bond, slug: options.slug ?? uid }; const resolved = { ...bond, slug: options.slug ?? uid };
let isFailQuote = false; let isFailQuote = false;
server.use( server.use(
http.get("*/api/v1/bonds/:uid", () => http.get("*/api/v1/bonds/:uid", () => HttpResponse.json(resource(resolved))),
options.failBond
? HttpResponse.json({ code: "NOT_FOUND" }, { status: 404 })
: HttpResponse.json(resource(resolved)),
),
http.get("*/api/v1/bonds/by-ticker/:ticker", ({ request }) => { http.get("*/api/v1/bonds/by-ticker/:ticker", ({ request }) => {
requests.push(new URL(request.url)); requests.push(new URL(request.url));
@@ -1,6 +1,6 @@
import { MantineProvider } from "@mantine/core"; import { MantineProvider } from "@mantine/core";
import { QueryClientProvider } from "@tanstack/react-query"; 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 { http, HttpResponse } from "msw";
import { MemoryRouter } from "react-router"; import { MemoryRouter } from "react-router";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
@@ -12,19 +12,37 @@ import { server } from "@/test/server";
import { StocksPage } from "./StocksPage"; import { StocksPage } from "./StocksPage";
import { useStocksPageVm } from "./StocksPage.vm"; import { useStocksPageVm } from "./StocksPage.vm";
const pageOperations: Promise<void>[] = [];
const StocksPageVmProbe = () => { const StocksPageVmProbe = () => {
const vm = useStocksPageVm(); const vm = useStocksPageVm();
return ( return (
<> <>
<button onClick={vm.onSearch}>search</button> <button
<button onClick={() => vm.onPage(2)}>page</button> onClick={() => {
pageOperations.push(vm.search());
}}
>
search
</button>
<button
onClick={() => {
pageOperations.push(vm.changePage(2));
}}
>
page
</button>
<output>{vm.items.map(item => item.name).join(", ")}</output> <output>{vm.items.map(item => item.name).join(", ")}</output>
</> </>
); );
}; };
afterEach(() => vi.useRealTimers()); afterEach(() => {
pageOperations.length = 0;
return vi.useRealTimers();
});
describe("stock search", () => { describe("stock search", () => {
it("waits for a meaningful query and uses the server-verified ticker link", async () => { 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: "page" }));
fireEvent.click(screen.getByRole("button", { name: "search" })); fireEvent.click(screen.getByRole("button", { name: "search" }));
await screen.findByText("Новый"); 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(); expect(screen.queryByText("Старый результат")).not.toBeInTheDocument();
}); });
@@ -101,6 +101,7 @@ function useStocksPageVm() {
}; };
return { return {
changePage,
criteria, criteria,
error, error,
items: page?.items ?? [], items: page?.items ?? [],
@@ -111,6 +112,8 @@ function useStocksPageVm() {
onSearch: () => void search(), onSearch: () => void search(),
page: page?.page, page: page?.page,
resultTime: page?.asOf, resultTime: page?.asOf,
// Test seam: settle the actual operation instead of the handler release.
search,
searched: page !== null, searched: page !== null,
totalPages: page ? page.page + Number(page.hasNext) : 0, totalPages: page ? page.page + Number(page.hasNext) : 0,
}; };
+8 -2
View File
@@ -164,10 +164,16 @@ describe("persistent resource query lifecycle", () => {
}); });
await waitFor(() => expect(isStarted).toBe(true)); 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<unknown> = Promise.resolve();
await act(async () => {
observation = result.current.observe() ?? Promise.resolve();
await observation;
});
await act(async () => { await act(async () => {
release(); release();
await blocked; await observation;
}); });
expect(result.current.data).toEqual(snapshot); expect(result.current.data).toEqual(snapshot);
}); });