Skip to content
Source
Sign inSign up

threadContextDrag.browser.test.tsx

84 lines3.0 KB

1import { memo, useRef } from "react";
2import { describe, expect, it } from "vite-plus/test";
3import type { EnvironmentId, ScopedThreadRef, ThreadId } from "@t3tools/contracts";
4​
5import { act, render } from "../../../test/render";
6import { THREAD_CONTEXT_DROP_EVENT, useThreadContextDrop } from "./threadContextDrag";
7​
8const ENVIRONMENT_ID = "environment-local" as EnvironmentId;
9​
10// ChatComposer is memoized and reused across thread navigation; each render's
11// drop callback must write into the draft of the thread it last rendered.
12const ComposerDropTarget = memo(function ComposerDropTarget({
13 draftKey,
14 drafts,
15}: {
16 draftKey: string;
17 drafts: Map<string, Array<ThreadId>>;
18}) {
19 const targetRef = useRef<HTMLDivElement | null>(null);
20 useThreadContextDrop(targetRef, ENVIRONMENT_ID, draftKey, (refs, isActive) => {
21 if (!isActive()) return;
22 drafts.get(draftKey)?.push(...refs.map((ref) => ref.threadId));
23 });
24 return <div data-drop-target ref={targetRef} />;
25});
26​
27// Keep the key stable to isolate effect-event updates; React 19.2 retained initial props.
28const StableKeyDropTarget = memo(function StableKeyDropTarget({
29 received,
30}: {
31 received: Array<ThreadId>;
32}) {
33 const targetRef = useRef<HTMLDivElement | null>(null);
34 useThreadContextDrop(targetRef, ENVIRONMENT_ID, "thread-a", (refs) => {
35 received.push(...refs.map((ref) => ref.threadId));
36 });
37 return <div data-drop-target ref={targetRef} />;
38});
39​
40describe("useThreadContextDrop", () => {
41 it("drops into the last rendered thread's draft when the memoized target is reused", async () => {
42 const drafts = new Map<string, Array<ThreadId>>([
43 ["thread-a", []],
44 ["thread-b", []],
45 ]);
46​
47 await using screen = await render(<ComposerDropTarget draftKey="thread-a" drafts={drafts} />);
48 await screen.rerender(<ComposerDropTarget draftKey="thread-b" drafts={drafts} />);
49​
50 const dropped: ScopedThreadRef = {
51 environmentId: ENVIRONMENT_ID,
52 threadId: "thread-dropped" as ThreadId,
53 };
54 const target = screen.container.querySelector("[data-drop-target]");
55 expect(target).not.toBeNull();
56 await act(() => {
57 target!.dispatchEvent(new CustomEvent(THREAD_CONTEXT_DROP_EVENT, { detail: [dropped] }));
58 });
59​
60 expect(drafts.get("thread-b")).toEqual(["thread-dropped"]);
61 expect(drafts.get("thread-a")).toEqual([]);
62 });
63​
64 it("reads the latest props in a memoized target whose listener stays attached", async () => {
65 const first: Array<ThreadId> = [];
66 const latest: Array<ThreadId> = [];
67​
68 await using screen = await render(<StableKeyDropTarget received={first} />);
69 await screen.rerender(<StableKeyDropTarget received={latest} />);
70​
71 const target = screen.container.querySelector("[data-drop-target]");
72 expect(target).not.toBeNull();
73 await act(() => {
74 target!.dispatchEvent(
75 new CustomEvent(THREAD_CONTEXT_DROP_EVENT, {
76 detail: [{ environmentId: ENVIRONMENT_ID, threadId: "thread-dropped" as ThreadId }],
77 }),
78 );
79 });
80​
81 expect(latest).toEqual(["thread-dropped"]);
82 expect(first).toEqual([]);
83 });
84});