Skip to content
Source
Sign inSign up

terminalFocus.test.ts

335 lines10.5 KB

1import { scopeThreadRef } from "@t3tools/client-runtime/environment";
2import { EnvironmentId, ThreadId } from "@t3tools/contracts";
3import { describe, expect, it, vi } from "vite-plus/test";
4​
5import { selectThreadRightPanelState, useRightPanelStore } from "../rightPanelStore";
6import { useTerminalUiStateStore } from "../terminalUiStateStore";
7import {
8 consumePendingTerminalFocus,
9 focusActiveDrawerTerminal,
10 focusActivePanelTerminal,
11 focusPanelTerminalAfterClose,
12 focusTerminal,
13 focusTerminalOnNavigation,
14 registerTerminalFocusTarget,
15 restoreFocusOnThreadEnter,
16} from "./terminalFocus";
17​
18const threadRef = (threadId: string) =>
19 scopeThreadRef(EnvironmentId.make("env"), ThreadId.make(threadId));
20​
21// Node has no DOM; entry only listens on the root's document for focus changes.
22const enterThread = (ref: ReturnType<typeof threadRef>) =>
23 restoreFocusOnThreadEnter(ref, {
24 root: {
25 ownerDocument: Object.assign(new EventTarget(), { activeElement: null }),
26 } as unknown as HTMLElement,
27 focusComposer: () => {},
28 });
29​
30describe("focusTerminal", () => {
31 it("focuses a mounted target right away", () => {
32 const ref = threadRef("mounted");
33 const focus = vi.fn(() => true);
34 const unregister = registerTerminalFocusTarget({ threadRef: ref, terminalId: "a", focus });
35​
36 focusTerminal(ref, "a");
37​
38 expect(focus).toHaveBeenCalledTimes(1);
39 unregister();
40 });
41​
42 it("waits for the requested terminal to mount, then consumes the request", () => {
43 const ref = threadRef("pending");
44 focusTerminal(ref, "b");
45​
46 const other = vi.fn(() => true);
47 const unregisterOther = registerTerminalFocusTarget({
48 threadRef: ref,
49 terminalId: "a",
50 focus: other,
51 });
52 const target = vi.fn(() => true);
53 const unregisterTarget = registerTerminalFocusTarget({
54 threadRef: ref,
55 terminalId: "b",
56 focus: target,
57 });
58 unregisterTarget();
59 const remount = vi.fn(() => true);
60 const unregisterRemount = registerTerminalFocusTarget({
61 threadRef: ref,
62 terminalId: "b",
63 focus: remount,
64 });
65​
66 expect(other).not.toHaveBeenCalled();
67 expect(target).toHaveBeenCalledTimes(1);
68 expect(remount).not.toHaveBeenCalled();
69 unregisterOther();
70 unregisterRemount();
71 });
72​
73 it("keeps only the newest request for a thread", () => {
74 const ref = threadRef("newest");
75 focusTerminal(ref, "a");
76 focusTerminal(ref, "b");
77​
78 const first = vi.fn(() => true);
79 const unregisterFirst = registerTerminalFocusTarget({
80 threadRef: ref,
81 terminalId: "a",
82 focus: first,
83 });
84 const second = vi.fn(() => true);
85 const unregisterSecond = registerTerminalFocusTarget({
86 threadRef: ref,
87 terminalId: "b",
88 focus: second,
89 });
90​
91 expect(first).not.toHaveBeenCalled();
92 expect(second).toHaveBeenCalledTimes(1);
93 unregisterFirst();
94 unregisterSecond();
95 });
96​
97 it("keeps the request when a mounted target cannot take focus", () => {
98 const ref = threadRef("hidden");
99 const hidden = vi.fn(() => false);
100 const unregisterHidden = registerTerminalFocusTarget({
101 threadRef: ref,
102 terminalId: "a",
103 focus: hidden,
104 });
105​
106 focusTerminal(ref, "a");
107 unregisterHidden();
108 const shown = vi.fn(() => true);
109 const unregisterShown = registerTerminalFocusTarget({
110 threadRef: ref,
111 terminalId: "a",
112 focus: shown,
113 });
114​
115 expect(hidden).toHaveBeenCalledTimes(1);
116 expect(shown).toHaveBeenCalledTimes(1);
117 unregisterShown();
118 });
119​
120 it("focuses a registered hidden target once it is shown", () => {
121 const ref = threadRef("shown-later");
122 let canFocus = false;
123 const focus = vi.fn(() => canFocus);
124 const unregister = registerTerminalFocusTarget({ threadRef: ref, terminalId: "a", focus });
125​
126 focusTerminal(ref, "a");
127 canFocus = true;
128 consumePendingTerminalFocus(ref, "a");
129 consumePendingTerminalFocus(ref, "a");
130​
131 expect(focus).toHaveBeenCalledTimes(2);
132 unregister();
133 });
134​
135 it("drops a parked request once a newer one targets another thread", () => {
136 const parked = threadRef("parked");
137 const hidden = vi.fn(() => false);
138 const unregisterHidden = registerTerminalFocusTarget({
139 threadRef: parked,
140 terminalId: "a",
141 focus: hidden,
142 });
143 focusTerminal(parked, "a");
144​
145 const other = threadRef("other");
146 const unregisterOther = registerTerminalFocusTarget({
147 threadRef: other,
148 terminalId: "b",
149 focus: () => true,
150 });
151 focusTerminal(other, "b");
152 unregisterHidden();
153 const remount = vi.fn(() => true);
154 const unregisterRemount = registerTerminalFocusTarget({
155 threadRef: parked,
156 terminalId: "a",
157 focus: remount,
158 });
159​
160 expect(remount).not.toHaveBeenCalled();
161 unregisterOther();
162 unregisterRemount();
163 });
164​
165 it("drops a parked request when the thread's drawer closes", () => {
166 const ref = threadRef("closed");
167 useTerminalUiStateStore.getState().ensureTerminal(ref, "a", { open: true, active: true });
168 focusActiveDrawerTerminal(ref);
169 useTerminalUiStateStore.getState().setTerminalOpen(ref, false);
170 focusActiveDrawerTerminal(ref);
171​
172 const focus = vi.fn(() => true);
173 const unregister = registerTerminalFocusTarget({ threadRef: ref, terminalId: "a", focus });
174​
175 expect(focus).not.toHaveBeenCalled();
176 unregister();
177 });
178​
179 it("entering a thread with its drawer open focuses the active drawer terminal", () => {
180 const ref = threadRef("entered");
181 useTerminalUiStateStore.getState().ensureTerminal(ref, "a", { open: true, active: true });
182 const focus = vi.fn(() => true);
183 const unregister = registerTerminalFocusTarget({ threadRef: ref, terminalId: "a", focus });
184​
185 const leave = enterThread(ref);
186​
187 expect(focus).toHaveBeenCalledTimes(1);
188 leave();
189 unregister();
190 });
191​
192 it("entering a thread keeps the request its navigating handler made", () => {
193 const ref = threadRef("revealed");
194 useTerminalUiStateStore.getState().ensureTerminal(ref, "drawer", { open: true, active: true });
195 focusTerminalOnNavigation(ref, "panel");
196​
197 // React commit order: the new thread's viewports attach and take the
198 // request before ChatView's thread-enter effect runs.
199 const drawer = vi.fn(() => true);
200 const unregisterDrawer = registerTerminalFocusTarget({
201 threadRef: ref,
202 terminalId: "drawer",
203 focus: drawer,
204 });
205 const panel = vi.fn(() => true);
206 const unregisterPanel = registerTerminalFocusTarget({
207 threadRef: ref,
208 terminalId: "panel",
209 focus: panel,
210 });
211 const leave = enterThread(ref);
212​
213 expect(drawer).not.toHaveBeenCalled();
214 expect(panel).toHaveBeenCalledTimes(1);
215 leave();
216 unregisterDrawer();
217 unregisterPanel();
218 });
219​
220 it("re-entering a thread after in-thread terminal actions focuses the drawer", () => {
221 const ref = threadRef("re-entered");
222 useTerminalUiStateStore.getState().ensureTerminal(ref, "drawer", { open: true, active: true });
223 const drawer = vi.fn(() => true);
224 const unregisterDrawer = registerTerminalFocusTarget({
225 threadRef: ref,
226 terminalId: "drawer",
227 focus: drawer,
228 });
229​
230 const leaveFirst = enterThread(ref);
231 focusTerminal(ref, "drawer");
232 focusTerminalOnNavigation(ref, "drawer");
233 leaveFirst();
234 drawer.mockClear();
235 const leaveSecond = enterThread(ref);
236​
237 expect(drawer).toHaveBeenCalledTimes(1);
238 leaveSecond();
239 unregisterDrawer();
240 });
241​
242 it("a navigating handler's request only outranks the next thread entry", () => {
243 const ref = threadRef("revisited");
244 useTerminalUiStateStore.getState().ensureTerminal(ref, "drawer", { open: true, active: true });
245 focusTerminalOnNavigation(ref, "panel");
246 enterThread(threadRef("elsewhere"))();
247​
248 const drawer = vi.fn(() => true);
249 const unregisterDrawer = registerTerminalFocusTarget({
250 threadRef: ref,
251 terminalId: "drawer",
252 focus: drawer,
253 });
254 const leave = enterThread(ref);
255​
256 expect(drawer).toHaveBeenCalledTimes(1);
257 leave();
258 unregisterDrawer();
259 });
260​
261 it("focuses the panel terminal a closed surface falls back to", () => {
262 const ref = threadRef("panel-fallback");
263 const panel = useRightPanelStore.getState();
264 panel.openTerminal(ref, "a");
265 panel.open(ref, "diff");
266 const { activeSurfaceId: diffSurfaceId } = selectThreadRightPanelState(
267 useRightPanelStore.getState().byThreadKey,
268 ref,
269 );
270 const focus = vi.fn(() => true);
271 const unregister = registerTerminalFocusTarget({ threadRef: ref, terminalId: "a", focus });
272 expect(diffSurfaceId).not.toBeNull();
273​
274 useRightPanelStore.getState().closeSurface(ref, diffSurfaceId!);
275 focusActivePanelTerminal(ref);
276​
277 expect(focus).toHaveBeenCalledTimes(1);
278 unregister();
279 });
280​
281 it("closing a surface's last terminal focuses the terminal surface it falls back to", () => {
282 const ref = threadRef("panel-terminal-fallback");
283 const panel = useRightPanelStore.getState();
284 panel.openTerminal(ref, "b");
285 panel.openTerminal(ref, "a");
286 const surfaceA = selectThreadRightPanelState(
287 useRightPanelStore.getState().byThreadKey,
288 ref,
289 ).activeSurfaceId!;
290 const focusB = vi.fn(() => true);
291 const unregister = registerTerminalFocusTarget({
292 threadRef: ref,
293 terminalId: "b",
294 focus: focusB,
295 });
296​
297 panel.closeTerminal(ref, surfaceA, "a");
298 focusActivePanelTerminal(ref);
299​
300 expect(focusB).toHaveBeenCalledTimes(1);
301 unregister();
302 });
303​
304 it("refocuses the panel terminal only when closing changed the active surface", () => {
305 const ref = threadRef("panel-after-close");
306 const panel = useRightPanelStore.getState();
307 panel.openTerminal(ref, "a");
308 const terminalSurfaceId = selectThreadRightPanelState(
309 useRightPanelStore.getState().byThreadKey,
310 ref,
311 ).activeSurfaceId!;
312 panel.open(ref, "diff");
313 const diffSurfaceId = selectThreadRightPanelState(
314 useRightPanelStore.getState().byThreadKey,
315 ref,
316 ).activeSurfaceId!;
317 const focus = vi.fn(() => true);
318 const unregister = registerTerminalFocusTarget({ threadRef: ref, terminalId: "a", focus });
319​
320 panel.activateSurface(ref, terminalSurfaceId);
321 panel.closeSurface(ref, diffSurfaceId);
322 focusPanelTerminalAfterClose(ref, terminalSurfaceId);
323 expect(focus).not.toHaveBeenCalled();
324​
325 panel.open(ref, "diff");
326 const reopenedDiffId = selectThreadRightPanelState(
327 useRightPanelStore.getState().byThreadKey,
328 ref,
329 ).activeSurfaceId!;
330 panel.closeSurface(ref, reopenedDiffId);
331 focusPanelTerminalAfterClose(ref, reopenedDiffId);
332 expect(focus).toHaveBeenCalledTimes(1);
333 unregister();
334 });
335});