diff --git a/src/platforms/android/__tests__/multitouch-helper.test.ts b/src/platforms/android/__tests__/multitouch-helper.test.ts index fc7d24a89..a6d589f4a 100644 --- a/src/platforms/android/__tests__/multitouch-helper.test.ts +++ b/src/platforms/android/__tests__/multitouch-helper.test.ts @@ -8,12 +8,10 @@ import { ANDROID_EMULATOR } from '../../../__tests__/test-utils/index.ts'; import { ensureAndroidMultiTouchHelper, parseAndroidMultiTouchHelperOutput, - pinchAndroid, resetAndroidMultiTouchHelperInstallCache, rotateGestureAndroid, runAndroidMultiTouchHelperGesture, swipeGestureAndroid, - transformGestureAndroid, } from '../multitouch-helper.ts'; import { withAndroidAdbProvider, @@ -109,9 +107,7 @@ test('runAndroidMultiTouchHelperGesture encodes one-finger swipe payloads', asyn capturedPayload = JSON.parse(Buffer.from(args[6]!, 'base64').toString('utf8')); return { exitCode: 0, - stdout: [resultRecord({ ok: 'true', kind: 'swipe' }), 'INSTRUMENTATION_CODE: 0'].join( - '\n', - ), + stdout: [resultRecord({ ok: 'true', kind: 'swipe' }), 'INSTRUMENTATION_CODE: 0'].join('\n'), stderr: '', }; }, @@ -166,66 +162,6 @@ test('runAndroidMultiTouchHelperGesture preserves helper failure messages', asyn ); }); -test('swipeGestureAndroid and multi-touch gestures prefer provider-native touch injection', async () => { - const calls: unknown[] = []; - await withAndroidAdbProvider( - { - exec: async () => { - throw new Error('adb should not run when native touch is available'); - }, - touch: async (request) => { - calls.push(request); - return { backendDetail: 'native' }; - }, - }, - { serial: ANDROID_EMULATOR.id }, - async () => { - const swipe = await swipeGestureAndroid(ANDROID_EMULATOR, { - x1: 340, - y1: 400, - x2: 60, - y2: 400, - durationMs: 300, - }); - const pinch = await pinchAndroid(ANDROID_EMULATOR, { scale: 2, x: 100, y: 200 }); - const rotate = await rotateGestureAndroid(ANDROID_EMULATOR, { - degrees: -215, - x: 100, - y: 200, - }); - const transform = await transformGestureAndroid(ANDROID_EMULATOR, { - x: 100, - y: 200, - dx: 30, - dy: -20, - scale: 1.5, - degrees: 35, - }); - - assert.equal(swipe?.backend, 'provider-native-touch'); - assert.equal(pinch.backend, 'provider-native-touch'); - assert.equal(rotate.backend, 'provider-native-touch'); - assert.equal(transform.backend, 'provider-native-touch'); - }, - ); - - assert.deepEqual(calls, [ - { kind: 'swipe', x1: 340, y1: 400, x2: 60, y2: 400, durationMs: 300 }, - { kind: 'pinch', x: 100, y: 200, scale: 2, durationMs: undefined }, - { kind: 'rotate', x: 100, y: 200, degrees: -215, durationMs: undefined }, - { - kind: 'transform', - x: 100, - y: 200, - dx: 30, - dy: -20, - scale: 1.5, - degrees: 35, - durationMs: undefined, - }, - ]); -}); - test('swipeGestureAndroid falls back to adb input swipe when helper path is unavailable', async () => { const adbCalls: string[][] = []; const result = await withAndroidAdbProvider( @@ -274,6 +210,37 @@ test('swipeGestureAndroid falls back to adb input swipe when helper path is unav assert.ok(adbCalls.some((args) => args.join(' ') === 'shell input swipe 340 400 60 400 300')); }); +test('swipeGestureAndroid propagates provider-native failures without adb fallback', async () => { + const adbCalls: string[][] = []; + await withAndroidAdbProvider( + { + exec: async (args) => { + adbCalls.push(args); + return { exitCode: 0, stdout: '', stderr: '' }; + }, + touch: async () => { + throw new Error('native touch failed'); + }, + }, + { serial: ANDROID_EMULATOR.id }, + async () => { + await assert.rejects( + () => + swipeGestureAndroid(ANDROID_EMULATOR, { + x1: 340, + y1: 400, + x2: 60, + y2: 400, + durationMs: 300, + }), + /native touch failed/, + ); + }, + ); + + assert.deepEqual(adbCalls, []); +}); + test('rotateGestureAndroid rejects zero velocity before provider dispatch', async () => { await withAndroidAdbProvider( { diff --git a/src/platforms/android/multitouch-helper.ts b/src/platforms/android/multitouch-helper.ts index da45bdba6..1e9393178 100644 --- a/src/platforms/android/multitouch-helper.ts +++ b/src/platforms/android/multitouch-helper.ts @@ -120,16 +120,14 @@ export async function swipeGestureAndroid( device: DeviceInfo, options: AndroidSwipeGestureOptions, ): Promise | void> { - const providerTouch = resolveAndroidTouchInjector(device); - if (providerTouch) { - return { - backend: 'provider-native-touch', - ...((await providerTouch({ kind: 'swipe', ...options })) ?? {}), - }; - } + const providerResult = await runAndroidTouchProviderGesture(device, { + kind: 'swipe', + ...options, + }); + if (providerResult) return providerResult; try { - return await runAndroidMultiTouchGesture(device, { kind: 'swipe', ...options }); + return await runAndroidMultiTouchHelperGestureForDevice(device, { kind: 'swipe', ...options }); } catch (error) { emitDiagnostic({ level: 'warn', @@ -151,7 +149,7 @@ export async function pinchAndroid( throw new AppError('INVALID_ARGS', 'gesture pinch requires scale > 0'); } const center = await resolveGestureCenter(device, options.x, options.y); - return await runAndroidMultiTouchGesture(device, { + return await performAndroidTouchGesture(device, { kind: 'pinch', x: center.x, y: center.y, @@ -175,7 +173,7 @@ export async function rotateGestureAndroid( } const center = await resolveGestureCenter(device, options.x, options.y); const degrees = options.degrees; - return await runAndroidMultiTouchGesture(device, { + return await performAndroidTouchGesture(device, { kind: 'rotate', x: center.x, y: center.y, @@ -197,7 +195,7 @@ export async function transformGestureAndroid( if (![options.x, options.y, options.dx, options.dy].every(Number.isFinite)) { throw new AppError('INVALID_ARGS', 'gesture transform requires finite x y dx dy'); } - return await runAndroidMultiTouchGesture(device, { + return await performAndroidTouchGesture(device, { kind: 'transform', x: options.x, y: options.y, @@ -219,16 +217,30 @@ async function resolveGestureCenter( return { x: Math.round(size.width / 2), y: Math.round(size.height / 2) }; } -async function runAndroidMultiTouchGesture( +async function performAndroidTouchGesture( device: DeviceInfo, request: AndroidTouchGestureRequest, ): Promise> { + const providerResult = await runAndroidTouchProviderGesture(device, request); + if (providerResult) return providerResult; + + return await runAndroidMultiTouchHelperGestureForDevice(device, request); +} + +async function runAndroidTouchProviderGesture( + device: DeviceInfo, + request: AndroidTouchGestureRequest, +): Promise | undefined> { const providerTouch = resolveAndroidTouchInjector(device); - if (providerTouch) { - const result = (await providerTouch(request)) ?? {}; - return { backend: 'provider-native-touch', ...result }; - } + if (!providerTouch) return undefined; + const result = (await providerTouch(request)) ?? {}; + return { backend: 'provider-native-touch', ...result }; +} +async function runAndroidMultiTouchHelperGestureForDevice( + device: DeviceInfo, + request: AndroidTouchGestureRequest, +): Promise> { const adb = resolveAndroidAdbExecutor(device); const artifact = await resolveAndroidMultiTouchHelperArtifact(); const adbProvider = resolveAndroidAdbProvider(device); diff --git a/test/integration/provider-scenarios/android-lifecycle.test.ts b/test/integration/provider-scenarios/android-lifecycle.test.ts index 327f57ae5..d2d0e376d 100644 --- a/test/integration/provider-scenarios/android-lifecycle.test.ts +++ b/test/integration/provider-scenarios/android-lifecycle.test.ts @@ -85,6 +85,13 @@ test('Provider-backed integration Android touch provider handles multi-touch ges const client = world.daemon.client(); await client.apps.open({ app: 'settings', ...world.selection }); + await client.interactions.swipe({ + from: { x: 340, y: 400 }, + to: { x: 60, y: 400 }, + durationMs: 300, + ...world.selection, + }); + const pinch = await client.interactions.pinch({ scale: 2, x: 195, @@ -118,6 +125,7 @@ test('Provider-backed integration Android touch provider handles multi-touch ges assert.equal(transform.backend, 'provider-native-touch'); assert.deepEqual(world.touchInjectionCalls, [ + { kind: 'swipe', x1: 340, y1: 400, x2: 60, y2: 400, durationMs: 300 }, { kind: 'pinch', x: 195, y: 320, scale: 2, durationMs: undefined }, { kind: 'rotate', x: 195, y: 320, degrees: 145, durationMs: undefined }, {