Skip to content
Source
Sign inSign up
Open History to see the last commit

Sidebar.drag.browser.test.tsx

142 lines5.6 KB

1import { createMemoryHistory } from "@tanstack/react-router";
2import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings";
3import type * as Context from "effect/Context";
4import { expect, it, vi } from "vite-plus/test";
5import { page, userEvent } from "vite-plus/test/browser";
6​
7import { AppRoot } from "~/AppRoot";
8// The thread route loads ChatView lazily; its first transform would eat the test's timeout.
9import "~/components/ChatView";
10import { __setClientSettingsForTests } from "~/hooks/useSettings";
11import { getRouter } from "~/router";
12import {
13 ENVIRONMENT_ID,
14 installFakeEnvironment,
15 threadWithHistory,
16 type FakeDriver,
17} from "../../test/fakeEnvironment";
18import { act, render } from "../../test/render";
19import { makeThreadFixture } from "../test-fixtures";
20import { SidebarSearchResultRow } from "./sidebar/SidebarThreadRow";
21​
22// The socket is the only fake layer; see test/appScenarios.renders.tsx.
23vi.mock("../../../../packages/client-runtime/src/connection/driver.ts", async (importOriginal) => {
24 const [actual, { fakeDriver }, Layer] = await Promise.all([
25 importOriginal<{ readonly ConnectionDriver: Context.Key<unknown, FakeDriver> }>(),
26 import("../../test/fakeEnvironment"),
27 import("effect/Layer"),
28 ]);
29 return { ...actual, layer: Layer.succeed(actual.ConnectionDriver, fakeDriver) };
30});
31​
32vi.mock(import("~/rpc/requestLatencyState"), async (original) => ({
33 ...(await original()),
34 trackRpcRequestSent: () => {},
35}));
36​
37it("shows the titled preview inside and outside search results from a collapsed folder", async () => {
38 await page.viewport(1400, 900);
39 const [first, second] = [threadWithHistory("visible", 1), threadWithHistory("search-target", 1)];
40 using _environment = installFakeEnvironment([first, second]);
41 __setClientSettingsForTests({
42 ...DEFAULT_CLIENT_SETTINGS,
43 boringSidebarEnabled: true,
44 onboardingCompletedAt: "2026-01-01T00:00:00.000Z",
45 });
46 const router = getRouter(
47 createMemoryHistory({ initialEntries: [`/${ENVIRONMENT_ID}/${first.id}`] }),
48 );
49 await router.load();
50 await using _app = await render(<AppRoot router={router} />, {
51 // AppRoot brings its own router.
52 router: false,
53 });
54 const folder = page.getByRole("button", { name: /^render/ });
55 await expect.element(folder).toHaveAttribute("aria-expanded", "true");
56 // A removed row fades out as a clone without its test id, so match the live row only.
57 const secondRow = page.getByTestId("sidebar-row-folder").filter({ hasText: second.title });
58 await expect.element(secondRow).toBeInTheDocument();
59 // The shift guard drops pointer presses on a row that appeared under 500ms ago; keys pass.
60 folder.element().focus();
61 await userEvent.keyboard("{Enter}");
62 await expect.element(secondRow).not.toBeInTheDocument();
63 await userEvent.fill(page.getByRole("combobox", { name: "Search threads" }), "search-target");
64 const result = page.getByRole("option", { name: /Thread search-target/ });
65 await expect.element(result).toBeVisible();
66 const bounds = result.element().getBoundingClientRect();
67 const pointer = (type: string, x: number, y: number) =>
68 new PointerEvent(type, {
69 bubbles: true,
70 cancelable: true,
71 pointerId: 1,
72 isPrimary: true,
73 button: 0,
74 buttons: type === "pointerup" ? 0 : 1,
75 clientX: x,
76 clientY: y,
77 });
78 using _release = {
79 [Symbol.dispose]: () => document.dispatchEvent(pointer("pointerup", -100, 100)),
80 };
81 await act(() => {
82 result.element().dispatchEvent(pointer("pointerdown", bounds.x + 10, bounds.y + 10));
83 document.dispatchEvent(pointer("pointermove", bounds.x + 30, bounds.y + 10));
84 });
85 await vi.waitFor(() => {
86 expect(
87 document.querySelector('body > div[aria-hidden="true"].pointer-events-none')?.textContent,
88 ).toBe(second.title);
89 });
90 await act(() => document.dispatchEvent(pointer("pointermove", -100, 100)));
91 await vi.waitFor(() => {
92 expect(
93 document.querySelector('body > div[aria-hidden="true"].pointer-events-none')?.textContent,
94 ).toBe(second.title);
95 });
96 await act(() => document.dispatchEvent(pointer("pointermove", bounds.x + 30, bounds.y + 10)));
97 await vi.waitFor(() => {
98 expect(
99 document.querySelector('body > div[aria-hidden="true"].pointer-events-none')?.textContent,
100 ).toBe(second.title);
101 });
102 await act(() =>
103 document.dispatchEvent(new KeyboardEvent("keydown", { code: "Escape", bubbles: true })),
104 );
105 await vi.waitFor(() => {
106 expect(document.querySelector('body > div[aria-hidden="true"].pointer-events-none')).toBeNull();
107 });
108});
109​
110it("keeps focus in the search box when a click picks a search result", async () => {
111 const onSelect = vi.fn();
112 await using screen = await render(
113 <>
114 <input aria-label="Search threads" />
115 <ul role="listbox">
116 <SidebarSearchResultRow
117 isSettled={false}
118 thread={makeThreadFixture({ title: "Invoice export" })}
119 project={null}
120 projectDisplayName={null}
121 environmentLabel={null}
122 environmentMachine="server"
123 providerEntryByInstanceId={new Map()}
124 isHighlighted={false}
125 isRouteActive={false}
126 resultId="result-0"
127 searchMatch={null}
128 searchQuery="invoice"
129 onHighlight={() => {}}
130 onSelect={onSelect}
131 onFileDropThreads={() => {}}
132 />
133 </ul>
134 </>,
135 );
136 const search = screen.getByRole("textbox", { name: "Search threads" });
137 await userEvent.click(search);
138 await userEvent.click(screen.getByRole("option"));
139 expect(onSelect).toHaveBeenCalledOnce();
140 // Picking clears the results; a focused result would drop focus, and its recovery scrolls the list.
141 await expect.element(search).toHaveFocus();
142});