1import type { EnvironmentId, ScopedThreadRef, ThreadId } 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 { useActiveThreadRef } from "./useActiveThreadRef";
7
8type ThreadLike = { environmentId: EnvironmentId; id: ThreadId; status: string } | null;
9
10let renderer: ReactTestRenderer | null = null;
11let observed: Array<ScopedThreadRef | null> = [];
12
13function Probe({ thread }: { thread: ThreadLike }) {
14 observed.push(useActiveThreadRef(thread));
15 return null;
16}
17
18const thread = (id: string, status = "idle"): ThreadLike => ({
19 environmentId: "env-1" as EnvironmentId,
20 id: id as ThreadId,
21 status,
22});
23
24async function render(value: ThreadLike) {
25 await act(() => {
26 if (renderer) renderer.update(<Probe thread={value} />);
27 else renderer = create(<Probe thread={value} />);
28 });
29 return observed.at(-1);
30}
31
32beforeEach(() => {
33 observed = [];
34 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
35});
36
37afterEach(async () => {
38 await act(() => renderer?.unmount());
39 renderer = null;
40 vi.unstubAllGlobals();
41});
42
43describe("useActiveThreadRef", () => {
44 it("keeps the same ref while the thread object is replaced during a run", async () => {
45 const first = await render(thread("t1", "running"));
46 const afterUpdate = await render(thread("t1", "waiting"));
47
48 expect(first).toEqual({ environmentId: "env-1", threadId: "t1" });
49 expect(afterUpdate).toBe(first);
50 });
51
52 it("returns a new ref when the thread changes or goes away", async () => {
53 const first = await render(thread("t1"));
54 const switched = await render(thread("t2"));
55 const gone = await render(null);
56
57 expect(switched).not.toBe(first);
58 expect(switched).toEqual({ environmentId: "env-1", threadId: "t2" });
59 expect(gone).toBeNull();
60 });
61});