Skip to content
Source
Sign inSign up

Sidebar.logic.browser.test.tsx

155 lines6.9 KB

1import { EnvironmentId, ThreadId, type ResolvedKeybindingsConfig } from "@t3tools/contracts";
2import { compileResolvedKeybindingsConfig, isMacPlatform } from "@t3tools/shared/keybindings";
3import { describe, expect, it, vi } from "vite-plus/test";
4import { userEvent } from "vite-plus/test/browser";
5​
6import { act, fakeTimers, render } from "../../test/render";
7import { registerModelPickerContent } from "../modelPickerVisibility";
8import { useTerminalUiStateStore } from "../terminalUiStateStore";
9import {
10 THREAD_JUMP_HINT_SHOW_DELAY_MS,
11 useSnoozeWakeClock,
12 useThreadJumpHintsVisible,
13} from "./Sidebar.logic";
14​
15const MOUNTED_AT_MS = Date.parse("2026-10-24T08:00:00.000Z");
16​
17function WakeClock(props: { readonly threads: ReadonlyArray<{ readonly snoozedUntil: string }> }) {
18 return <output>{useSnoozeWakeClock(props.threads)}</output>;
20​
21describe("useSnoozeWakeClock", () => {
22 it("settles in one step when many distinct wakes passed during a sleep", async () => {
23 using now = vi.spyOn(Date, "now").mockReturnValue(MOUNTED_AT_MS);
24 // More distinct wakes than React's 25 render-phase re-render limit.
25 const threads = Array.from({ length: 40 }, (_, index) => ({
26 snoozedUntil: new Date(MOUNTED_AT_MS + 1 + index * 60_000).toISOString(),
27 }));
28 await using screen = await render(<WakeClock threads={threads} />, {
29 // A bare hook host; nothing here links or navigates.
30 router: false,
31 });
32 await expect.element(screen.getByRole("status")).toHaveTextContent(String(MOUNTED_AT_MS));
33​
34 // The machine resumes after every wake has passed; the first wake's timer fires late.
35 const resumedAtMs = MOUNTED_AT_MS + 24 * 60 * 60_000;
36 now.mockReturnValue(resumedAtMs);
37​
38 // The clock lands on the last elapsed wake, so every woken thread has left the shelf.
39 const lastWakeMs = MOUNTED_AT_MS + 1 + 39 * 60_000;
40 await expect.element(screen.getByRole("status")).toHaveTextContent(String(lastWakeMs));
41 });
42});
43​
44const ROUTE_THREAD_REF = {
45 environmentId: EnvironmentId.make("local"),
46 threadId: ThreadId.make("thread-1"),
47};
48​
49function JumpHints(props: { readonly keybindings: ResolvedKeybindingsConfig }) {
50 const shown = useThreadJumpHintsVisible(props.keybindings, ROUTE_THREAD_REF);
51 return <output>{shown ? "shown" : "hidden"}</output>;
53​
54describe("useThreadJumpHintsVisible", () => {
55 it("waits for the delay again on a second hold", async () => {
56 const keybindings = compileResolvedKeybindingsConfig([
57 { key: "mod+1", command: "thread.jump.1" },
58 ]);
59 const modifier = isMacPlatform(navigator.platform) ? "Meta" : "Control";
60 await using screen = await render(<JumpHints keybindings={keybindings} />, {
61 // A bare hook host; nothing here links or navigates.
62 router: false,
63 });
64 using _timers = fakeTimers();
65 const output = screen.getByRole("status");
66​
67 await userEvent.keyboard(`{${modifier}>}`);
68 await expect.element(output).toHaveTextContent("hidden");
69 await act(() => vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS));
70 await expect.element(output).toHaveTextContent("shown");
71​
72 await userEvent.keyboard(`{/${modifier}}`);
73 await expect.element(output).toHaveTextContent("hidden");
74 await act(() => vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS));
75​
76 await userEvent.keyboard(`{${modifier}>}`);
77 await act(() => vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS - 1));
78 await expect.element(output).toHaveTextContent("hidden");
79 await act(() => vi.advanceTimersByTime(1));
80 await expect.element(output).toHaveTextContent("shown");
81 await userEvent.keyboard(`{/${modifier}}`);
82 });
83​
84 it("follows terminal focus and the route thread's terminal while the modifier is held", async () => {
85 using stack = new DisposableStack();
86 const terminal = document.createElement("div");
87 terminal.dataset.terminalOwner = "drawer";
88 const terminalInput = document.createElement("textarea");
89 terminal.append(terminalInput);
90 document.body.append(terminal);
91 stack.defer(() => terminal.remove());
92 stack.defer(() => useTerminalUiStateStore.getState().setTerminalOpen(ROUTE_THREAD_REF, false));
93 const keybindings = compileResolvedKeybindingsConfig([
94 { key: "mod+1", command: "thread.jump.1", when: "terminalOpen" },
95 ]);
96 const modifier = isMacPlatform(navigator.platform) ? "Meta" : "Control";
97 await using screen = await render(<JumpHints keybindings={keybindings} />, {
98 // A bare hook host; nothing here links or navigates.
99 router: false,
100 });
101 using _timers = fakeTimers();
102 const output = screen.getByRole("status");
103​
104 await userEvent.keyboard(`{${modifier}>}`);
105 await act(() => vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS));
106 await expect.element(output).toHaveTextContent("hidden");
107 await act(() => useTerminalUiStateStore.getState().setTerminalOpen(ROUTE_THREAD_REF, true));
108 await expect.element(output).toHaveTextContent("shown");
109 await act(() => terminalInput.focus());
110 await expect.element(output).toHaveTextContent("hidden");
111 await act(() => terminalInput.blur());
112 await expect.element(output).toHaveTextContent("shown");
113 await userEvent.keyboard(`{/${modifier}}`);
114 await expect.element(output).toHaveTextContent("hidden");
115 });
116​
117 it("returns when the model picker closes while the modifier is still held", async () => {
118 const keybindings = compileResolvedKeybindingsConfig([
119 { key: "mod+1", command: "thread.jump.1", when: "!modelPickerOpen" },
120 ]);
121 const modifier = isMacPlatform(navigator.platform) ? "Meta" : "Control";
122 await using screen = await render(<JumpHints keybindings={keybindings} />, {
123 // A bare hook host; nothing here links or navigates.
124 router: false,
125 });
126 using _timers = fakeTimers();
127 const output = screen.getByRole("status");
128​
129 await userEvent.keyboard(`{${modifier}>}`);
130 await act(() => vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS));
131 await expect.element(output).toHaveTextContent("shown");
132 let unregisterModelPicker = () => {};
133 await act(() => {
134 unregisterModelPicker = registerModelPickerContent();
135 });
136 await expect.element(output).toHaveTextContent("hidden");
137 await act(() => unregisterModelPicker());
138 await expect.element(output).toHaveTextContent("shown");
139 await userEvent.keyboard(`{/${modifier}}`);
140 });
141​
142 it("shows after the delay for a jump binding without a modifier", async () => {
143 const keybindings = compileResolvedKeybindingsConfig([{ key: "f1", command: "thread.jump.1" }]);
144 // Mounting starts the hold delay.
145 using _timers = fakeTimers();
146 await using screen = await render(<JumpHints keybindings={keybindings} />, {
147 // A bare hook host; nothing here links or navigates.
148 router: false,
149 });
150 const output = screen.getByRole("status");
151​
152 await act(() => vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS));
153 await expect.element(output).toHaveTextContent("shown");
154 });
155});