1import { EnvironmentId } from "@t3tools/contracts";
2import { act } from "react";
3import { create, type ReactTestRenderer } from "react-test-renderer";
4import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
5
6import { useThreadSearch } from "./queries";
7
8const searchResults = vi.hoisted(() => ({
9 matches: [{ threadId: "thread-1", source: "user", snippet: "a link" }],
10 isLoading: false,
11}));
12vi.mock("@effect/atom-react", async (importOriginal) => ({
13 ...(await importOriginal<typeof import("@effect/atom-react")>()),
14 useAtomValue: (atom: { label?: ReadonlyArray<unknown> }) =>
15 String(atom.label?.[0]).includes("empty") ? { matches: [], isLoading: false } : searchResults,
16}));
17
18const environmentIds = [EnvironmentId.make("local")];
19type ThreadSearch = ReturnType<typeof useThreadSearch>;
20
21function Probe(props: { query: string; onResult: (result: ThreadSearch) => void }) {
22 props.onResult(useThreadSearch(environmentIds, props.query));
23 return null;
24}
25
26describe("useThreadSearch", () => {
27 let renderer: ReactTestRenderer | null = null;
28 beforeEach(() => {
29 vi.useFakeTimers();
30 vi.stubGlobal("window", globalThis);
31 });
32 afterEach(() => {
33 act(() => renderer?.unmount());
34 renderer = null;
35 vi.unstubAllGlobals();
36 vi.useRealTimers();
37 });
38
39 it("keeps the settled query and matches stable while typing, then reports the settled query", () => {
40 const results: ThreadSearch[] = [];
41 const render = (query: string) => {
42 const element = <Probe query={query} onResult={(result) => results.push(result)} />;
43 act(() => {
44 if (renderer) renderer.update(element);
45 else renderer = create(element);
46 });
47 return results.at(-1)!;
48 };
49
50 render("");
51 const typing = ["li", "lin", "link "].map(render);
52 expect(typing.map((result) => result.query)).toEqual(["", "", ""]);
53 expect(new Set(typing.map((result) => result.matches)).size).toBe(1);
54 expect(typing.every((result) => result.isPending)).toBe(true);
55
56 act(() => {
57 vi.advanceTimersByTime(200);
58 });
59 const settled = results.at(-1)!;
60 expect(settled.query).toBe("link");
61 expect(settled.matches).toBe(searchResults.matches);
62 expect(settled.isPending).toBe(false);
63 });
64});