From a37edcf8a9b0f61d196370fc787738ef36c754e8 Mon Sep 17 00:00:00 2001 From: Renfei Zhou Date: Sun, 15 Nov 2026 15:24:58 +0000 Subject: [PATCH 1/2] test(fork): a right-panel tab switches on a press anywhere on it A click on a right-panel tab's padding or its top and bottom edges does nothing, a click with a few pixels of hand movement is swallowed by the tab drag sensor, and a working click switches only after mouse-up. This browser test presses a tab's edges and its label with jitter and checks that the tab is active before the button is released, that focus stays where the switch put it, that touch and keyboard switch on click, and that the close button closes without switching. It fails on main. Co-Authored-By: Claude Opus 5.5 --- .../rightPanelSplit/tabPress.browser.test.tsx | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 apps/web/src/fork/rightPanelSplit/tabPress.browser.test.tsx diff --git a/apps/web/src/fork/rightPanelSplit/tabPress.browser.test.tsx b/apps/web/src/fork/rightPanelSplit/tabPress.browser.test.tsx new file mode 100644 index 0000000000000..8ae6aade06361 --- /dev/null +++ b/apps/web/src/fork/rightPanelSplit/tabPress.browser.test.tsx @@ -0,0 +1,157 @@ +import "~/index.css"; + +import { EnvironmentId } from "@t3tools/contracts"; +import { describe, expect, it, vi } from "vite-plus/test"; +import { userEvent } from "vite-plus/test/browser"; +import type { ReactNode } from "react"; + +import { RightPanelTabs, type RightPanelTabsProps } from "~/components/RightPanelTabs"; +import type { RightPanelSurface } from "~/rightPanelStore"; +import { render } from "../../../test/render"; + +const BROWSER_SURFACE: RightPanelSurface = { + id: "browser:tab-1", + kind: "preview", + resourceId: "tab-1", +}; +const DIFF_SURFACE: RightPanelSurface = { id: "diff", kind: "diff" }; + +function tabs( + overrides: Pick & + Partial>, + children: ReactNode = null, +) { + const noop = () => {}; + const props: RightPanelTabsProps = { + mode: "inline", + keybindings: [], + getShortcutContext: () => ({ + terminalFocus: false, + terminalOpen: false, + previewFocus: false, + previewOpen: true, + isWeb: true, + isDesktop: false, + }), + surfaces: [BROWSER_SURFACE, DIFF_SURFACE], + environmentId: EnvironmentId.make("environment-local"), + activeSurfaceId: DIFF_SURFACE.id, + pendingSurfaceIds: new Set(), + previewSessions: {}, + desktopByTabId: {}, + terminalLabelsById: new Map(), + onCloseSurface: noop, + onCloseOtherSurfaces: noop, + onCloseSurfacesToRight: noop, + onCloseAllSurfaces: noop, + onCopyFilePath: noop, + onAddBrowser: noop, + onAddBrowserInProfile: noop, + onAddTerminal: noop, + onAddDiff: noop, + onAddFiles: noop, + onAddPullRequest: noop, + onAddPullRequests: noop, + onAddDevice: noop, + browserAvailable: true, + terminalAvailable: true, + diffAvailable: true, + filesAvailable: true, + pullRequestAvailable: true, + pullRequestsAvailable: true, + deviceAvailable: true, + ...overrides, + children, + }; + return ; +} + +function pointer( + target: EventTarget, + type: string, + x: number, + y: number, + pointerType: "mouse" | "touch" = "mouse", +) { + target.dispatchEvent( + new PointerEvent(type, { + bubbles: true, + cancelable: true, + composed: true, + pointerId: 1, + pointerType, + isPrimary: true, + button: type === "pointermove" ? -1 : 0, + buttons: type === "pointerup" ? 0 : 1, + clientX: x, + clientY: y, + }), + ); +} + +describe("tab activation", () => { + it("activates a tab on press anywhere on it, through hand jitter, but not from its close button", async () => { + const onActivate = vi.fn(); + const onCloseSurface = vi.fn(); + await using screen = await render(tabs({ onActivate, onCloseSurface })); + const tab = screen.container.querySelector('[data-active-tab="false"]')!; + const box = tab.getBoundingClientRect(); + const midY = box.top + box.height / 2; + + for (const [x, y] of [ + [box.left + 2, midY], + [box.right - 2, midY], + [box.left + box.width / 2, box.top + 1], + ] as const) { + onActivate.mockClear(); + pointer(document.elementFromPoint(x, y)!, "pointerdown", x, y); + expect(onActivate).toHaveBeenCalledWith(BROWSER_SURFACE); + pointer(document, "pointerup", x, y); + } + + onActivate.mockClear(); + const label = screen.getByRole("button", { name: "Browser", exact: true }).element(); + const start = label.getBoundingClientRect(); + pointer(label, "pointerdown", start.left + 4, start.top + 4); + pointer(document, "pointermove", start.left + 14, start.top + 10); + pointer(document, "pointerup", start.left + 14, start.top + 10); + expect(onActivate).toHaveBeenCalledWith(BROWSER_SURFACE); + + onActivate.mockClear(); + const close = screen.getByRole("button", { name: "Close Browser" }).element(); + const closeBox = close.getBoundingClientRect(); + pointer(close, "pointerdown", closeBox.left + 4, closeBox.top + 4); + pointer(document, "pointerup", closeBox.left + 4, closeBox.top + 4); + await userEvent.click(close); + expect(onCloseSurface).toHaveBeenCalledWith(BROWSER_SURFACE); + expect(onActivate).not.toHaveBeenCalled(); + }); + + it("leaves focus where activation put it, activates once per click, and waits for the tap on touch", async () => { + const onActivate = vi.fn((_surface: RightPanelSurface) => + document.querySelector('[aria-label="Terminal input"]')?.focus(), + ); + await using screen = await render( + tabs({ onActivate },