From 2cbf66893e4dd325479d23ce4e7efc3ad0027711 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Tue, 10 Nov 2026 12:42:25 -0600 Subject: [PATCH] fix(web): preserve search result titles in drag previews --- .../components/Sidebar.drag.browser.test.tsx | 83 +++++++++++++++++++ apps/web/src/components/Sidebar.tsx | 4 +- 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/Sidebar.drag.browser.test.tsx diff --git a/apps/web/src/components/Sidebar.drag.browser.test.tsx b/apps/web/src/components/Sidebar.drag.browser.test.tsx new file mode 100644 index 0000000000000..b6494d089ae1c --- /dev/null +++ b/apps/web/src/components/Sidebar.drag.browser.test.tsx @@ -0,0 +1,83 @@ +import { createMemoryHistory } from "@tanstack/react-router"; +import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; +import type * as Context from "effect/Context"; +import { expect, it, vi } from "vite-plus/test"; +import { page, userEvent } from "vite-plus/test/browser"; + +import { AppRoot } from "~/AppRoot"; +// The thread route loads ChatView lazily; its first transform would eat the test's timeout. +import "~/components/ChatView"; +import { __setClientSettingsForTests } from "~/hooks/useSettings"; +import { getRouter } from "~/router"; +import { + ENVIRONMENT_ID, + installFakeEnvironment, + threadWithHistory, + type FakeDriver, +} from "../../test/fakeEnvironment"; +import { act, render } from "../../test/render"; + +// The socket is the only fake layer; see test/appScenarios.renders.tsx. +vi.mock("../../../../packages/client-runtime/src/connection/driver.ts", async (importOriginal) => { + const [actual, { fakeDriver }, Layer] = await Promise.all([ + importOriginal<{ readonly ConnectionDriver: Context.Key }>(), + import("../../test/fakeEnvironment"), + import("effect/Layer"), + ]); + return { ...actual, layer: Layer.succeed(actual.ConnectionDriver, fakeDriver) }; +}); + +vi.mock(import("~/rpc/requestLatencyState"), async (original) => ({ + ...(await original()), + trackRpcRequestSent: () => {}, +})); + +it("keeps the title when dragging a search result from a collapsed folder", async () => { + await page.viewport(1400, 900); + const [first, second] = [threadWithHistory("visible", 1), threadWithHistory("search-target", 1)]; + using _environment = installFakeEnvironment([first, second]); + __setClientSettingsForTests({ + ...DEFAULT_CLIENT_SETTINGS, + boringSidebarEnabled: true, + onboardingCompletedAt: "2026-01-01T00:00:00.000Z", + }); + const router = getRouter( + createMemoryHistory({ initialEntries: [`/${ENVIRONMENT_ID}/${first.id}`] }), + ); + await router.load(); + await using _app = await render(, { + // AppRoot brings its own router. + router: false, + }); + const folder = page.getByRole("button", { name: /^render/ }); + await expect.element(folder).toHaveAttribute("aria-expanded", "true"); + await userEvent.click(folder); + await expect.element(page.getByText(second.title, { exact: true })).not.toBeInTheDocument(); + await userEvent.fill(page.getByRole("combobox", { name: "Search threads" }), "search-target"); + const result = page.getByRole("option", { name: /Thread search-target/ }); + await expect.element(result).toBeVisible(); + const bounds = result.element().getBoundingClientRect(); + const pointer = (type: string, x: number, y: number) => + new PointerEvent(type, { + bubbles: true, + cancelable: true, + pointerId: 1, + isPrimary: true, + button: 0, + buttons: type === "pointerup" ? 0 : 1, + clientX: x, + clientY: y, + }); + using _release = { + [Symbol.dispose]: () => document.dispatchEvent(pointer("pointerup", -100, 100)), + }; + await act(() => { + result.element().dispatchEvent(pointer("pointerdown", bounds.x + 10, bounds.y + 10)); + document.dispatchEvent(pointer("pointermove", -100, 100)); + }); + await vi.waitFor(() => { + expect( + document.querySelector('body > div[aria-hidden="true"].pointer-events-none')?.textContent, + ).toBe(second.title); + }); +}); diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 113b84c6c09f5..2b1a8403935c6 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1765,8 +1765,8 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve } restoreDragScroll(); const threads = contextDragThreads(); - const title = - threadByKeyRef.current.get(contextDragKeyRef.current ?? "")?.title.trim() || "Thread"; + const threadRef = parseScopedThreadKey(contextDragKeyRef.current ?? ""); + const title = (threadRef && readThreadShell(threadRef)?.title.trim()) || "Thread"; moveThreadContextDragGhost(point, { title, count: threads.length }); return true; };