From 2c2b84ce9e830db478615d41771fc1f94f8b12b0 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 24 Sep 2026 01:14:21 -0400 Subject: [PATCH] fix(mobile): never replay a tap the host answered A fulfilled browser.mouseClick ran on the host, but a null result still replayed it as move/down/up. The native bridge always answers { clicked }, while the external-Chromium provider returns agent-browser's `data` as is, which can be null, so a right-click there was a double tap. Only a refusal that is not delivery-unknown now replays. --- ...e-browser-commands-click-fallback.test.tsx | 25 ++++++++++++++++--- .../browser/use-mobile-browser-commands.ts | 11 +++----- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/mobile/src/browser/use-mobile-browser-commands-click-fallback.test.tsx b/mobile/src/browser/use-mobile-browser-commands-click-fallback.test.tsx index 1159c529400..2fec0fb223c 100644 --- a/mobile/src/browser/use-mobile-browser-commands-click-fallback.test.tsx +++ b/mobile/src/browser/use-mobile-browser-commands-click-fallback.test.tsx @@ -11,17 +11,21 @@ import type { RpcClient } from '../transport/rpc-client' import { useMobileBrowserCommands } from './use-mobile-browser-commands' import { useMobileBrowserRequest } from './use-mobile-browser-request' -const { sent, clickFailure } = vi.hoisted(() => { +const { sent, clickFailure, clickReply } = vi.hoisted(() => { const failure: { current: Error | null } = { current: null } - return { sent: new Array(), clickFailure: failure } + const reply: { current: unknown } = { current: {} } + return { sent: new Array(), clickFailure: failure, clickReply: reply } }) vi.mock('./mobile-browser-command-operations', () => { const command = (method: string) => ({ request: vi.fn(async () => { sent.push(method) - if (method === 'browser.mouseClick' && clickFailure.current) { - throw clickFailure.current + if (method === 'browser.mouseClick') { + if (clickFailure.current) { + throw clickFailure.current + } + return clickReply.current } return {} }), @@ -85,6 +89,7 @@ describe('tap fallback', () => { beforeEach(() => { sent.length = 0 clickFailure.current = null + clickReply.current = {} }) it('does not replay a click whose delivery is unknown', async () => { @@ -100,6 +105,18 @@ describe('tap fallback', () => { expect(sent).toEqual(['browser.mouseClick']) }) + // The external-Chromium provider answers a delivered click with agent-browser's `data`, which can be null. + it('does not replay a click the host answered, whatever the answer', async () => { + clickReply.current = null + const commands = mountCommands() + + await act(async () => { + await commands.sendPointerClick({ x: 10, y: 20 }, 'right') + }) + + expect(sent).toEqual(['browser.mouseClick']) + }) + it('replays a click the host refused as move, down and up', async () => { clickFailure.current = new Error('Unknown method: browser.mouseClick') const commands = mountCommands() diff --git a/mobile/src/browser/use-mobile-browser-commands.ts b/mobile/src/browser/use-mobile-browser-commands.ts index 46a7b0d42f5..0881688622d 100644 --- a/mobile/src/browser/use-mobile-browser-commands.ts +++ b/mobile/src/browser/use-mobile-browser-commands.ts @@ -119,7 +119,7 @@ export function useMobileBrowserCommands(args: MobileBrowserCommandArgs) { return } try { - const reply = browserPointerClick.interpret( + browserPointerClick.interpret( await browserPointerClick.request( client, { @@ -143,18 +143,13 @@ export function useMobileBrowserCommands(args: MobileBrowserCommandArgs) { ) ) setError(null) - if (reply !== null) { - return - } + return } catch (error) { // Why: a timed-out click may still run on the host; replaying it as move/down/up double-taps. - if (isRpcDeliveryUnknown(error)) { + if (isRpcDeliveryUnknown(error) || pointerModifiers.length > 0) { return } } - if (pointerModifiers.length > 0) { - return - } try { const moveReply = await browserPointerMove.request(client, { ...base,