From e8445822fe66329619e26e3b9ff44b6ad0682548 Mon Sep 17 00:00:00 2001 From: Cameron Chisholm Date: Mon, 19 Oct 2026 22:41:14 +0100 Subject: [PATCH 1/2] feat(apple): admit two-finger gestures on physical iOS devices --- docs/adr/0013-unified-gesture-plans.md | 2 +- packages/contracts/src/gesture-admission.ts | 14 +++--- packages/contracts/src/gesture-runtime.ts | 2 +- .../core/gesture-runtime-admission.test.ts | 43 +++++++++++-------- packages/platform-android/src/runtime.test.ts | 2 +- .../platform-apple/src/gesture-facts.test.ts | 15 +++---- packages/platform-apple/src/gesture-facts.ts | 12 +----- .../platform-apple/src/interactions.test.ts | 31 ++++++++++--- .../platform-apple/src/multitouch-support.ts | 15 ++----- .../src/platform-runtime.test.ts | 10 +++-- .../src/platform-runtime.ts | 16 +++---- .../schema/cli-help-first-screen.test.ts | 2 +- src/commands/schema/cli-help-topics.test.ts | 2 +- src/commands/schema/cli-help.ts | 2 +- ...-runtime-gateway-gesture-admission.test.ts | 7 +-- website/docs/docs/commands.md | 4 +- 16 files changed, 87 insertions(+), 92 deletions(-) diff --git a/docs/adr/0013-unified-gesture-plans.md b/docs/adr/0013-unified-gesture-plans.md index c0547d2d30..29217a29cc 100644 --- a/docs/adr/0013-unified-gesture-plans.md +++ b/docs/adr/0013-unified-gesture-plans.md @@ -142,7 +142,7 @@ Platform adapters consume the canonical plan: macOS lowers a one-contact plan to its drag executor. tvOS lowers a one-contact plan to remote direction. Core admission and the Apple adapter both consume the same shared multi-touch support policy. - Multi-touch remains capability-gated to iOS simulators. + Multi-touch is capability-gated to iOS and iPadOS, on simulators and physical devices, through the XCTest runner. iOS deliberately retains three one-contact motion schedules behind the shared private-XCTest event bridge. Apple's public drag API models the phases independently as an [initial hold, movement velocity, and destination hold](https://developer.apple.com/documentation/xcuiautomation/xcuicoordinate/press%28forduration%3Athendragto%3Awithvelocity%3Athenholdforduration%3A%29). diff --git a/packages/contracts/src/gesture-admission.ts b/packages/contracts/src/gesture-admission.ts index d45e166135..44388cf6b7 100644 --- a/packages/contracts/src/gesture-admission.ts +++ b/packages/contracts/src/gesture-admission.ts @@ -29,16 +29,12 @@ export const TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT = /** The hint the Android owner states for a TV target, which has no touch input at all. */ export const ANDROID_TV_MULTI_TOUCH_UNSUPPORTED_HINT = - 'Android TV has no touch input — this gesture is supported on Android phones, tablets, and the iOS simulator only.'; - -/** The hint the Apple owner states for a physical iOS/iPadOS device. */ -export const PHYSICAL_IOS_MULTI_TOUCH_UNSUPPORTED_HINT = - 'Two-finger gesture synthesis is iOS-simulator only — not available on physical iOS devices.'; + 'Android TV has no touch input — this gesture is supported on Android phones, tablets, iOS, and iPadOS.'; /** * How a refused cell names itself, reproducing every subject the retired admission produced. * - * Four owner-specific subjects, then the plain platform name. The special cases resolve the + * Three owner-specific subjects, then the plain platform name. The special cases resolve the * Apple OS the way `assertAppleMultiTouchSupported` does; the default reads `appleOs` raw, the way * the retired `gesturePlatformMessage` did — an Apple device with no declared OS therefore still * reports `apple`, exactly as before. @@ -54,14 +50,14 @@ function gestureRefusalSubject(device: DeviceInfo, tier: GestureRuntimeTier): st } /** - * The three owner-specific subjects two-contact synthesis produced, or `undefined` where the - * retired admission fell through to the plain platform name. + * The owner-specific subjects two-contact synthesis refusals use, or `undefined` for the plain + * platform name. iOS and iPadOS have none: their owner admits multi-touch on simulators and + * devices, so only a provider can refuse it there, and its hint says why. */ function multiTouchRefusalSubject(device: DeviceInfo): string | undefined { if (device.platform === 'android') return device.target === 'tv' ? 'Android TV' : undefined; if (!isApplePlatform(device.platform)) return undefined; const appleOs = resolveDeviceAppleOs(device); - if (appleOs === 'ios' || appleOs === 'ipados') return 'physical iOS devices'; if (appleOs === 'macos' || appleOs === 'tvos' || appleOs === 'visionos') { return APPLE_OS_DISPLAY_NAMES[appleOs]; } diff --git a/packages/contracts/src/gesture-runtime.ts b/packages/contracts/src/gesture-runtime.ts index 589cf9b0f9..07653bccd9 100644 --- a/packages/contracts/src/gesture-runtime.ts +++ b/packages/contracts/src/gesture-runtime.ts @@ -46,7 +46,7 @@ export type GestureViewportInput = Readonly<{ * - `performDirectionalFlingPlan` — `gesture fling --direction`, whose speed semantics Linux * cannot honor even though it executes coordinate flings through its drag primitive. * - `performMultiTouchGesturePlan` — pinch/rotate/transform and two-pointer pan. On Apple this is - * the two-finger XCTest synthesis, which is iOS/iPadOS **simulator** only. + * the two-finger XCTest synthesis, which runs on iOS/iPadOS simulators and physical devices. * - `performTargetAuthoredDrag` — `gesture drag`, which needs an adapter preserving source hold, * timed movement, and destination hold. */ diff --git a/packages/daemon/src/core/gesture-runtime-admission.test.ts b/packages/daemon/src/core/gesture-runtime-admission.test.ts index d8cd0e01a2..69a8967d75 100644 --- a/packages/daemon/src/core/gesture-runtime-admission.test.ts +++ b/packages/daemon/src/core/gesture-runtime-admission.test.ts @@ -20,26 +20,10 @@ const refusalCases = [ fact: { available: false, reason: 'unsupported-platform-leaf', - hint: 'Android TV has no touch input — this gesture is supported on Android phones, tablets, and the iOS simulator only.', + hint: 'Android TV has no touch input — this gesture is supported on Android phones, tablets, iOS, and iPadOS.', }, hint: /Android TV has no touch input/, - }, - { - name: 'physical iOS', - device: { - platform: 'apple', - appleOs: 'ios', - id: 'iphone-device', - name: 'iPhone', - kind: 'device', - target: 'mobile', - }, - fact: { - available: false, - reason: 'unsupported-device-kind', - hint: 'Two-finger gesture synthesis is iOS-simulator only — not available on physical iOS devices.', - }, - hint: /physical iOS devices/, + message: 'gesture pinch is not supported on Android TV', }, { name: 'tvOS', @@ -57,12 +41,32 @@ const refusalCases = [ hint: 'tvOS does not support multi-touch gesture plans.', }, hint: /tvOS/, + message: 'gesture pinch is not supported on tvOS', + }, + { + // The Apple owner admits physical iOS; a provider without the XCTest runner can still refuse. + name: 'provider-refused physical iOS', + device: { + platform: 'apple', + appleOs: 'ios', + id: 'ios-device', + name: 'iPhone', + kind: 'device', + target: 'mobile', + }, + fact: { + available: false, + reason: 'unsupported-provider-mode', + hint: 'Two-finger gesture synthesis is not available on iOS devices reached through a WebDriver provider.', + }, + hint: /WebDriver provider/, + message: 'gesture pinch is not supported on ios', }, ] as const; test.each(refusalCases)( '$name preserves multi-touch admission details without binding', - async ({ device, fact, hint }) => { + async ({ device, fact, hint, message }) => { const target = device as DeviceInfo; const owner = localRuntimeOwner(target.platform); const base = createUnavailableRuntimeFactsForTest(target, owner); @@ -89,6 +93,7 @@ test.each(refusalCases)( expect(resolved.ok).toBe(false); if (!resolved.ok) { expect(resolved.response.error.code).toBe('UNSUPPORTED_OPERATION'); + expect(resolved.response.error.message).toBe(message); expect(resolved.response.error.hint).toMatch(hint); } expect(inspectFacts).toHaveBeenCalledOnce(); diff --git a/packages/platform-android/src/runtime.test.ts b/packages/platform-android/src/runtime.test.ts index 66bbfab378..b0eb155957 100644 --- a/packages/platform-android/src/runtime.test.ts +++ b/packages/platform-android/src/runtime.test.ts @@ -483,7 +483,7 @@ test('carries the retired Android TV hints verbatim', async () => { expect(facts.operations.performMultiTouchGesturePlan).toEqual({ available: false, reason: 'unsupported-platform-leaf', - hint: 'Android TV has no touch input — this gesture is supported on Android phones, tablets, and the iOS simulator only.', + hint: 'Android TV has no touch input — this gesture is supported on Android phones, tablets, iOS, and iPadOS.', }); expect(facts.operations.performTargetAuthoredDrag).toMatchObject({ available: false, diff --git a/packages/platform-apple/src/gesture-facts.test.ts b/packages/platform-apple/src/gesture-facts.test.ts index 69694ae447..e1eacdfb06 100644 --- a/packages/platform-apple/src/gesture-facts.test.ts +++ b/packages/platform-apple/src/gesture-facts.test.ts @@ -39,7 +39,7 @@ test.each([ appleDevice({ kind: 'device', iosPhysicalDeviceBackend: 'coredevice' }), true, true, - false, + true, true, true, true, @@ -64,14 +64,8 @@ test.each([ }, ); -test('carries the retired multi-touch hints verbatim on every Apple leaf that refused', async () => { +test('carries the retired multi-touch hints on every Apple leaf that refused', async () => { const runtime = createApplePlatformRuntime(platformRuntimeHostFixture()); - const physical = await runtime.inspectFacts(appleDevice({ kind: 'device' })); - expect(physical.operations.performMultiTouchGesturePlan).toEqual({ - available: false, - reason: 'unsupported-device-kind', - hint: 'Two-finger gesture synthesis is iOS-simulator only — not available on physical iOS devices.', - }); const macos = await runtime.inspectFacts(leaves.macos); expect(macos.operations.performMultiTouchGesturePlan).toMatchObject({ available: false, @@ -107,5 +101,8 @@ test('binds only the gesture tiers the leaf admitted', async () => { expect(simulator.operations.scrollDirection).toBeTypeOf('function'); const physical = await bind(appleDevice({ kind: 'device' })); expect(physical.operations.performGesturePlan).toBeTypeOf('function'); - expect(physical.operations.performMultiTouchGesturePlan).toBeUndefined(); + expect(physical.operations.performMultiTouchGesturePlan).toBeTypeOf('function'); + const macos = await bind(leaves.macos); + expect(macos.operations.performGesturePlan).toBeTypeOf('function'); + expect(macos.operations.performMultiTouchGesturePlan).toBeUndefined(); }); diff --git a/packages/platform-apple/src/gesture-facts.ts b/packages/platform-apple/src/gesture-facts.ts index 93e651f196..09f1e6a68d 100644 --- a/packages/platform-apple/src/gesture-facts.ts +++ b/packages/platform-apple/src/gesture-facts.ts @@ -1,8 +1,5 @@ import { APPLE_MULTI_TOUCH_UNSUPPORTED_HINTS } from './multitouch-support.ts'; -import { - PHYSICAL_IOS_MULTI_TOUCH_UNSUPPORTED_HINT, - TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT, -} from '@agent-device/contracts/gesture-admission'; +import { TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT } from '@agent-device/contracts/gesture-admission'; import { gestureRuntimeOperationFacts } from '@agent-device/contracts/gesture-runtime'; import type { RuntimeOperationFact, @@ -19,11 +16,6 @@ const gestureLeafUnavailable = Object.freeze({ const gestureKindUnavailable = unsupportedAppleDeviceKind( 'Gestures are supported only for Apple simulators and devices.', ); -const physicalIosMultiTouchUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: PHYSICAL_IOS_MULTI_TOUCH_UNSUPPORTED_HINT, -} as const); const targetAuthoredDragUnavailable = Object.freeze({ available: false, reason: 'unsupported-platform-leaf', @@ -76,7 +68,7 @@ function appleMultiTouchGestureFact(device: DeviceInfo): RuntimeOperationFact { ...(hint === undefined ? {} : { hint }), } as const); } - return device.kind === 'simulator' ? available : physicalIosMultiTouchUnavailable; + return available; } function appleTargetAuthoredDragFact(device: DeviceInfo): RuntimeOperationFact { diff --git a/packages/platform-apple/src/interactions.test.ts b/packages/platform-apple/src/interactions.test.ts index ef184b9bcd..2bd6a14cf0 100644 --- a/packages/platform-apple/src/interactions.test.ts +++ b/packages/platform-apple/src/interactions.test.ts @@ -9,10 +9,8 @@ import { } from './interactions.ts'; import { runAppleRunnerCommand } from './core/runner-client.ts'; import { AppError } from '@agent-device/kernel/errors'; -import { - gestureRefusalMessage, - PHYSICAL_IOS_MULTI_TOUCH_UNSUPPORTED_HINT, -} from '@agent-device/contracts/gesture-admission'; +import { gestureRefusalMessage } from '@agent-device/contracts/gesture-admission'; +import { APPLE_MULTI_TOUCH_UNSUPPORTED_HINTS } from './multitouch-support.ts'; import type { GesturePlan } from '@agent-device/contracts/gesture-plan-types'; import type { RunnerCommand } from './runner/index.ts'; import { TEXT_ENTRY_ROUTES } from '@agent-device/contracts/interactor-types'; @@ -415,19 +413,38 @@ test('performGestureApple sends exact two-pointer pan samples through gesture', }); }); +test('performGestureApple sends two-pointer samples to a physical iOS device', async () => { + mockRunAppleRunnerCommand.mockResolvedValue({ transformed: true }); + const plan = twoFingerPanPlan(); + + const result = await performGestureApple( + IOS_TEST_DEVICE, + { appBundleId: 'com.example.App' }, + {}, + plan, + ); + + assert.deepEqual(result, { transformed: true }); + assert.deepEqual(mockRunAppleRunnerCommand.mock.calls[0]?.[1], { + command: 'gesture', + gesturePlan: plan, + appBundleId: 'com.example.App', + }); +}); + test('Apple admission and execution share the same multi-touch refusal', async () => { // Admission is now the Apple owner's `performMultiTouchGesturePlan` fact composed with the // shared refusal wording (R42); execution keeps its defensive adapter check. The two must // still say the same thing, which is what this pins. - const admissionMessage = gestureRefusalMessage(IOS_TEST_DEVICE, 'multi-touch', 'pan'); + const admissionMessage = gestureRefusalMessage(MACOS_TEST_DEVICE, 'multi-touch', 'pan'); await assert.rejects( - () => performGestureApple(IOS_TEST_DEVICE, {}, {}, twoFingerPanPlan()), + () => performGestureApple(MACOS_TEST_DEVICE, {}, {}, twoFingerPanPlan()), (error: unknown) => error instanceof AppError && error.code === 'UNSUPPORTED_OPERATION' && error.message === admissionMessage && - error.details?.hint === PHYSICAL_IOS_MULTI_TOUCH_UNSUPPORTED_HINT, + error.details?.hint === APPLE_MULTI_TOUCH_UNSUPPORTED_HINTS.macos, ); assert.equal(mockRunAppleRunnerCommand.mock.calls.length, 0); }); diff --git a/packages/platform-apple/src/multitouch-support.ts b/packages/platform-apple/src/multitouch-support.ts index 10644b5202..3dc47cb8f2 100644 --- a/packages/platform-apple/src/multitouch-support.ts +++ b/packages/platform-apple/src/multitouch-support.ts @@ -10,9 +10,9 @@ import type { GesturePlan } from '@agent-device/contracts/gesture-plan-types'; export const APPLE_MULTI_TOUCH_UNSUPPORTED_HINTS: Partial> = { visionos: 'visionOS uses spatial input and does not support two-finger touch synthesis.', - tvos: 'tvOS has no touch input — this gesture is supported on Android and the iOS simulator only.', + tvos: 'tvOS has no touch input — this gesture is supported on Android, iOS, and iPadOS.', macos: - 'macOS automation has no multi-touch input — this gesture is supported on Android and the iOS simulator only.', + 'macOS automation has no multi-touch input — this gesture is supported on Android, iOS, and iPadOS.', }; /** One policy shared by command admission and the defensive Apple adapter check. */ @@ -24,16 +24,7 @@ export function assertAppleMultiTouchSupported( throw unsupported(intent, device.platform); } const appleOs = resolveDeviceAppleOs(device); - if ((appleOs === 'ios' || appleOs === 'ipados') && device.kind === 'simulator') return; - if (appleOs === 'ios' || appleOs === 'ipados') { - throw new AppError( - 'UNSUPPORTED_OPERATION', - `gesture ${intent} is not supported on physical iOS devices`, - { - hint: 'Two-finger gesture synthesis is iOS-simulator only — not available on physical iOS devices.', - }, - ); - } + if (appleOs === 'ios' || appleOs === 'ipados') return; throw unsupported( intent, APPLE_OS_DISPLAY_NAMES[appleOs], diff --git a/packages/provider-webdriver/src/platform-runtime.test.ts b/packages/provider-webdriver/src/platform-runtime.test.ts index 110a885879..70132f51af 100644 --- a/packages/provider-webdriver/src/platform-runtime.test.ts +++ b/packages/provider-webdriver/src/platform-runtime.test.ts @@ -513,8 +513,8 @@ function host(run: PlatformRuntimeHost['commands']['run']): PlatformRuntimeHost } // R52/R53: gestures and scrolling ride the same reachability gate the captures do. The one extra -// gate is the retired multi-touch policy — this provider owns physical devices only, and -// two-finger synthesis on a physical iOS device was refused before this migration too. +// gate is multi-touch: this provider reaches iOS through its own WebDriver interactor, which has no +// two-finger synthesis. test.each([ ['Android physical', device, true], ['iOS physical', { ...device, platform: 'apple' as const, appleOs: 'ios' as const }, false], @@ -534,8 +534,10 @@ test.each([ expect(facts.operations.scrollDirection).toEqual({ available: true }); expect(facts.operations.performMultiTouchGesturePlan.available).toBe(multiTouch); if (!multiTouch) { - expect(facts.operations.performMultiTouchGesturePlan).toMatchObject({ - hint: 'Two-finger gesture synthesis is iOS-simulator only — not available on physical iOS devices.', + expect(facts.operations.performMultiTouchGesturePlan).toEqual({ + available: false, + reason: 'unsupported-provider-mode', + hint: 'Two-finger gesture synthesis is not available on iOS devices reached through a WebDriver provider.', }); } }); diff --git a/packages/provider-webdriver/src/platform-runtime.ts b/packages/provider-webdriver/src/platform-runtime.ts index b00a6df9f2..26cdea38a9 100644 --- a/packages/provider-webdriver/src/platform-runtime.ts +++ b/packages/provider-webdriver/src/platform-runtime.ts @@ -14,7 +14,6 @@ import { bindProviderFocusInteractor, focusRuntimeOperationFacts, } from '@agent-device/contracts/focus-runtime'; -import { PHYSICAL_IOS_MULTI_TOUCH_UNSUPPORTED_HINT } from '@agent-device/contracts/gesture-admission'; import { bindProviderGestureInteractor, gestureRuntimeOperationFacts, @@ -147,9 +146,9 @@ const scrollUnavailable = unavailableFact( 'unsupported-provider-mode', 'This WebDriver provider runtime does not expose scrolling for this device.', ); -const physicalIosMultiTouchUnavailable = unavailableFact( - 'unsupported-device-kind', - PHYSICAL_IOS_MULTI_TOUCH_UNSUPPORTED_HINT, +const iosMultiTouchUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Two-finger gesture synthesis is not available on iOS devices reached through a WebDriver provider.', ); const elementTextUnavailable = unavailableFact( 'unsupported-provider-mode', @@ -574,9 +573,8 @@ function webDriverFacts( fillRef: typeUnavailable, }), // Gestures and scrolling ride the same provider interactor the captures do, so they need the - // same reachability. The one extra gate is the retired multi-touch policy: this provider only - // ever owns physical devices, and two-finger synthesis on a physical iOS device was refused - // before this migration exactly as it is refused here. + // same reachability. The one extra gate is multi-touch: this provider reaches iOS only through + // its own WebDriver interactor, which has no two-finger synthesis, not the Apple runner. ...gestureRuntimeOperationFacts({ unsupported: gestureUnavailable, plan: interactorCell(reachable, gestureUnavailable), @@ -662,10 +660,10 @@ function webDriverFacts( }); } -/** Two-finger synthesis is iOS-simulator only, and this provider owns no simulators. */ +/** Two-finger synthesis is an Apple runner capability, and this provider never uses the runner. */ function webDriverMultiTouchCell(device: DeviceInfo, reachable: boolean): RuntimeOperationFact { if (!reachable) return gestureUnavailable; - return device.platform === 'apple' ? physicalIosMultiTouchUnavailable : available; + return device.platform === 'apple' ? iosMultiTouchUnavailable : available; } /** The device shapes this provider can reach at all through its own WebDriver interactor. */ diff --git a/src/commands/schema/cli-help-first-screen.test.ts b/src/commands/schema/cli-help-first-screen.test.ts index 21aeb61feb..f4ab3f28ec 100644 --- a/src/commands/schema/cli-help-first-screen.test.ts +++ b/src/commands/schema/cli-help-first-screen.test.ts @@ -52,7 +52,7 @@ const FIRST_SCREEN_RULES: Record = { ], gestures: [ 'longpress accepts coordinates, @refs, or selectors; prefer @ref/selector, coordinates only as a fallback', - 'iOS simulator transform/pinch/rotate use private XCTest synthesis for a continuous two-finger pan/scale/rotation path; verify app metrics instead of assuming requested values map exactly to recognizer output', + 'iOS transform/pinch/rotate use private XCTest synthesis for a continuous two-finger pan/scale/rotation path; verify app metrics instead of assuming requested values map exactly to recognizer output', 'Rare iOS accessibility gap: a row shown disabled/hittable:false where press reports success but no UI change, or a collapsed composite control with no child refs -- run snapshot -i --json, compute the target center from rects, press x y, then diff snapshot -i. Coordinates are fallback-only; document why you used them.', ], tv: [ diff --git a/src/commands/schema/cli-help-topics.test.ts b/src/commands/schema/cli-help-topics.test.ts index 783a564486..d7a8a5df71 100644 --- a/src/commands/schema/cli-help-topics.test.ts +++ b/src/commands/schema/cli-help-topics.test.ts @@ -329,7 +329,7 @@ test('usageForCommand resolves gestures help topic', async () => { assert.match(help, /press --count --jitter-px for tap series/); assert.match( help, - /iOS simulator transform\/pinch\/rotate use private XCTest synthesis for a continuous two-finger/, + /iOS transform\/pinch\/rotate use private XCTest synthesis for a continuous two-finger/, ); assert.match(help, /Android transform injects a geometric two-finger path/); assert.match(help, /verify semantic app state or coarse per-component effects/); diff --git a/src/commands/schema/cli-help.ts b/src/commands/schema/cli-help.ts index f8136ade2c..3db6ce0fce 100644 --- a/src/commands/schema/cli-help.ts +++ b/src/commands/schema/cli-help.ts @@ -213,7 +213,7 @@ Shapes: For repeated iOS smoke checks: press --count --jitter-px for tap series, swipe --count for drag series. Platform quirks: - iOS simulator transform/pinch/rotate use private XCTest synthesis for a continuous two-finger pan/scale/rotation path; verify app metrics instead of assuming requested values map exactly to recognizer output. + iOS transform/pinch/rotate use private XCTest synthesis for a continuous two-finger pan/scale/rotation path; verify app metrics instead of assuming requested values map exactly to recognizer output. Android transform injects a geometric two-finger path; app recognizers may report non-exact pan/scale/rotation -- verify semantic app state or coarse per-component effects instead of exact numeric deltas unless the app exposes stable metrics. If Android needs exact values, prefer isolated gesture pan --pointer-count 2, gesture pinch, or gesture rotate over one combined transform: agent-device gesture transform 200 420 80 -40 2 35 700 --platform android agent-device wait text "pan changed yes" 3000 --platform android diff --git a/src/platform-runtime-gateway-gesture-admission.test.ts b/src/platform-runtime-gateway-gesture-admission.test.ts index 05a1e6b0bb..c963d304a8 100644 --- a/src/platform-runtime-gateway-gesture-admission.test.ts +++ b/src/platform-runtime-gateway-gesture-admission.test.ts @@ -62,7 +62,7 @@ test('target-authored drag is admitted only where adapters preserve every author } }); -test('iOS and iPadOS simulators admit multi-touch while physical devices do not', async () => { +test('iOS and iPadOS simulators and physical devices admit multi-touch', async () => { for (const appleOs of ['ios', 'ipados'] as const) { const simulator = await gateway.inspectFacts( device({ platform: 'apple', appleOs, kind: 'simulator' }), @@ -73,10 +73,7 @@ test('iOS and iPadOS simulators admit multi-touch while physical devices do not' expect(simulator.operations.performGesturePlan.available).toBe(true); expect(simulator.operations.performMultiTouchGesturePlan.available).toBe(true); expect(physical.operations.performGesturePlan.available).toBe(true); - expect(physical.operations.performMultiTouchGesturePlan).toMatchObject({ - available: false, - hint: expect.stringMatching(/iOS-simulator only/), - }); + expect(physical.operations.performMultiTouchGesturePlan.available).toBe(true); } }); diff --git a/website/docs/docs/commands.md b/website/docs/docs/commands.md index c1c2b438e5..af5462556c 100644 --- a/website/docs/docs/commands.md +++ b/website/docs/docs/commands.md @@ -301,7 +301,7 @@ vega virtual-device stop - TV targets include Apple TV and Android TV simulators, emulators, and connected devices. Vega OS support is limited to the Vega Virtual Device (VVD). - TV targets are focus-first: move focus with `tv-remote` before selecting a control. Don't use raw `adb shell input keyevent` in command plans. - Android TV: app launch and app listing use `LEANBACK_LAUNCHER` activities. `tv-remote` sends ADB keyevents; `longpress