diff --git a/apps/web/src/components/AppSidebarLayout.browser.test.tsx b/apps/web/src/components/AppSidebarLayout.browser.test.tsx index c97ea5e6412aa..138fbda2dd16d 100644 --- a/apps/web/src/components/AppSidebarLayout.browser.test.tsx +++ b/apps/web/src/components/AppSidebarLayout.browser.test.tsx @@ -1,7 +1,14 @@ import { page, userEvent } from "vite-plus/test/browser"; +import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; import { describe, expect, it, vi } from "vite-plus/test"; -import { render } from "../../test/render"; +import { act, render } from "../../test/render"; +import { + __resetClientSettingsPersistenceForTests, + __setClientSettingsForTests, + updateClientSettings, +} from "../hooks/useSettings"; +import { AppSidebarLayout } from "./AppSidebarLayout"; vi.mock(import("../state/server"), async (original) => { const actual = await original(); @@ -12,10 +19,63 @@ vi.mock(import("../state/server"), async (original) => { ]); return { ...actual, primaryServerKeybindingsAtom: Atom.make(keybindings) }; }); -// The thread list needs live environments; the shortcut lives in the shell around it. -vi.mock(import("./Sidebar"), () => ({ default: (() => null) as never })); +vi.mock("../env", () => ({ isElectron: true })); +vi.mock("./Sidebar", () => ({ default: () => null })); +vi.mock("./BackgroundThreadAnnouncer", () => ({ BackgroundThreadAnnouncer: () => null })); +vi.mock("./sidebar/SidebarChrome", () => ({ SidebarBrandWidthProbe: () => null })); +vi.mock("../localApi", () => ({ + ensureLocalApi: () => ({ + persistence: { setClientSettings: async () => undefined }, + }), +})); -import { AppSidebarLayout } from "./AppSidebarLayout"; +function setup(historySwipeNavigation: boolean) { + __resetClientSettingsPersistenceForTests(); + __setClientSettingsForTests({ ...DEFAULT_CLIENT_SETTINGS, historySwipeNavigation }); +} + +function wheel(deltaX: number) { + window.dispatchEvent(new WheelEvent("wheel", { deltaX, clientY: 200 })); +} + +describe("desktop history swipe navigation", () => { + it.each(["back", "forward"] as const)( + "disables %s swipes and can re-enable them without reloading", + async (direction) => { + setup(false); + using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("MacIntel"); + using _available = vi + .spyOn(window.navigation, direction === "back" ? "canGoBack" : "canGoForward", "get") + .mockReturnValue(true); + await using screen = await render(Thread); + using navigate = vi.spyOn(window.history, direction).mockImplementation(() => undefined); + const deltaX = direction === "back" ? -140 : 140; + + await act(() => { + wheel(deltaX); + wheel(0); + }); + expect(navigate).not.toHaveBeenCalled(); + await expect.element(screen.getByTestId("history-swipe-indicator")).not.toBeInTheDocument(); + + await act(() => updateClientSettings({ historySwipeNavigation: true })); + await act(() => wheel(deltaX)); + await expect.element(screen.getByTestId("history-swipe-indicator")).toBeInTheDocument(); + + await act(() => updateClientSettings({ historySwipeNavigation: false })); + await expect.element(screen.getByTestId("history-swipe-indicator")).not.toBeInTheDocument(); + await act(() => wheel(0)); + expect(navigate).not.toHaveBeenCalled(); + + await act(() => updateClientSettings({ historySwipeNavigation: true })); + await act(() => { + wheel(deltaX); + wheel(0); + }); + expect(navigate).toHaveBeenCalledOnce(); + }, + ); +}); describe("sidebar toggle shortcut", () => { it("leaves the key to a focused terminal when its rule excludes terminalFocus", async () => { diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 8e4baf20451c8..7d21017541a80 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -31,7 +31,7 @@ import { selectActiveRightPanel, useRightPanelStore } from "../rightPanelStore"; import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore"; import { cn, isMacPlatform } from "../lib/utils"; import { primaryServerKeybindingsAtom } from "../state/server"; -import { useEnvironmentIdentificationMode } from "../hooks/useSettings"; +import { useClientSettings, useEnvironmentIdentificationMode } from "../hooks/useSettings"; import { PanelAnimationSuppressionProvider, usePanelAnimationSettings, @@ -418,6 +418,7 @@ export function AppSidebarLayout({ children: ReactNode; showSidebar?: boolean; }) { + const historySwipeNavigation = useClientSettings((settings) => settings.historySwipeNavigation); const sidebarArea = workspaceAreaProps("sidebar", 0, "Threads"); const [sidebarRouteReveal] = useState(() => ({ observedKey: null, @@ -495,7 +496,7 @@ export function AppSidebarLayout({ {children} - {isElectron ? : null} + {isElectron && historySwipeNavigation ? : null} ); diff --git a/apps/web/src/components/HistorySwipeNavigation.tsx b/apps/web/src/components/HistorySwipeNavigation.tsx index 2924f82cd3136..d38d0a0bf211e 100644 --- a/apps/web/src/components/HistorySwipeNavigation.tsx +++ b/apps/web/src/components/HistorySwipeNavigation.tsx @@ -187,6 +187,7 @@ function SwipeIndicator({ direction, progress, y, bounds, tracking }: SwipeIndic return (