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.
This commit is contained in:
Jinwoo-H
2026-09-24 01:14:21 -04:00
parent b85e051010
commit 2c2b84ce9e
2 changed files with 24 additions and 12 deletions
@@ -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<string>(), clickFailure: failure }
const reply: { current: unknown } = { current: {} }
return { sent: new Array<string>(), 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()
@@ -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,