From 95a7b5c7d035db9fbe49df1b58669560a215959d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 9 Jun 2026 15:50:47 +0200 Subject: [PATCH 1/2] fix: use XCTest drag for iOS swipes --- .../RunnerTests+CommandExecution.swift | 8 +++++-- .../__tests__/dispatch-interactions.test.ts | 3 +-- src/core/__tests__/dispatch-series.test.ts | 18 --------------- src/core/dispatch-interactions.ts | 2 -- src/core/dispatch-series.ts | 4 ---- src/platforms/ios/interactions.ts | 23 ++++++------------- 6 files changed, 14 insertions(+), 44 deletions(-) diff --git a/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift b/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift index ede603478..ebe461aee 100644 --- a/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +++ b/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift @@ -17,6 +17,10 @@ extension RunnerTests { min(max((durationMs / 5.0) / 1000.0, 0.016), 0.120) } + private func coordinateDragHoldDuration() -> TimeInterval { + 0.050 + } + func unsupportedResponse(for outcome: RunnerInteractionOutcome) -> Response? { switch outcome { case .performed: @@ -657,7 +661,7 @@ extension RunnerTests { } let holdDuration = command.synthesized == true ? synthesizedSwipeFallbackHoldDuration(durationMs: command.durationMs ?? 250) - : min(max((command.durationMs ?? 60) / 1000.0, 0.016), 10.0) + : coordinateDragHoldDuration() let (timing, outcome) = performGesture(activeApp) { dragAt( app: activeApp, @@ -715,7 +719,7 @@ extension RunnerTests { } let holdDuration = command.synthesized == true ? synthesizedSwipeFallbackHoldDuration(durationMs: command.durationMs ?? 250) - : min(max((command.durationMs ?? 60) / 1000.0, 0.016), 10.0) + : coordinateDragHoldDuration() let (timing, outcome) = performGesture(activeApp) { performDragSeries( count: count, diff --git a/src/core/__tests__/dispatch-interactions.test.ts b/src/core/__tests__/dispatch-interactions.test.ts index 5975479eb..1fdab0489 100644 --- a/src/core/__tests__/dispatch-interactions.test.ts +++ b/src/core/__tests__/dispatch-interactions.test.ts @@ -165,7 +165,7 @@ test('handleSwipeCommand preserves iOS swipe duration through dispatch', async ( }); }); -test('handleSwipeCommand uses synthesized iOS runner drag series for repeated swipes', async () => { +test('handleSwipeCommand uses XCTest iOS runner drag series for repeated swipes', async () => { mockRunIosRunnerCommand.mockResolvedValueOnce({ gestureStartUptimeMs: 100, gestureEndUptimeMs: 720, @@ -194,7 +194,6 @@ test('handleSwipeCommand uses synthesized iOS runner drag series for repeated sw count: 2, pauseMs: 50, pattern: 'ping-pong', - synthesized: true, appBundleId: 'com.example.App', }); assert.equal(result.timingMode, 'runner-series'); diff --git a/src/core/__tests__/dispatch-series.test.ts b/src/core/__tests__/dispatch-series.test.ts index a72f166ef..96adabf44 100644 --- a/src/core/__tests__/dispatch-series.test.ts +++ b/src/core/__tests__/dispatch-series.test.ts @@ -4,7 +4,6 @@ import { requireIntInRange, shouldUseIosTapSeries, shouldUseIosDragSeries, - shouldUseSynthesizedIosDrag, } from '../dispatch-series.ts'; import { AppError } from '../../utils/errors.ts'; import type { DeviceInfo } from '../../utils/device.ts'; @@ -66,23 +65,6 @@ test('shouldUseIosDragSeries returns false when count is 1', () => { assert.equal(shouldUseIosDragSeries(iosDevice, 1), false); }); -// --- shouldUseSynthesizedIosDrag --- - -test('shouldUseSynthesizedIosDrag returns true only for non-tvOS iOS targets', () => { - assert.equal(shouldUseSynthesizedIosDrag(iosDevice), true); - assert.equal(shouldUseSynthesizedIosDrag({ ...iosDevice, target: 'tv' }), false); - assert.equal( - shouldUseSynthesizedIosDrag({ - platform: 'macos', - id: 'mac', - name: 'Mac', - kind: 'device', - target: 'desktop', - }), - false, - ); -}); - // --- computeDeterministicJitter --- // --- runRepeatedSeries --- diff --git a/src/core/dispatch-interactions.ts b/src/core/dispatch-interactions.ts index 5264538d2..109bac27b 100644 --- a/src/core/dispatch-interactions.ts +++ b/src/core/dispatch-interactions.ts @@ -25,7 +25,6 @@ import { requireIntInRange, shouldUseIosTapSeries, shouldUseIosDragSeries, - shouldUseSynthesizedIosDrag, computeDeterministicJitter, runRepeatedSeries, } from './dispatch-series.ts'; @@ -488,7 +487,6 @@ async function runSwipeCoordinates(params: { count, pauseMs, pattern, - ...(shouldUseSynthesizedIosDrag(device) ? { synthesized: true } : {}), appBundleId: context?.appBundleId, }, { diff --git a/src/core/dispatch-series.ts b/src/core/dispatch-series.ts index 655d74c4c..f43d431b4 100644 --- a/src/core/dispatch-series.ts +++ b/src/core/dispatch-series.ts @@ -27,10 +27,6 @@ export function shouldUseIosDragSeries(device: DeviceInfo, count: number): boole return isApplePlatform(device.platform) && count > 1; } -export function shouldUseSynthesizedIosDrag(device: DeviceInfo): boolean { - return device.platform === 'ios' && device.target !== 'tv'; -} - export function computeDeterministicJitter(index: number, jitterPx: number): [number, number] { if (jitterPx <= 0) return [0, 0]; const [dx, dy] = DETERMINISTIC_JITTER_PATTERN[index % DETERMINISTIC_JITTER_PATTERN.length]!; diff --git a/src/platforms/ios/interactions.ts b/src/platforms/ios/interactions.ts index bd254ef65..71e658f60 100644 --- a/src/platforms/ios/interactions.ts +++ b/src/platforms/ios/interactions.ts @@ -1,6 +1,5 @@ import { AppError } from '../../utils/errors.ts'; import type { DeviceInfo } from '../../utils/device.ts'; -import { shouldUseSynthesizedIosDrag } from '../../core/dispatch-series.ts'; import { buildScrollGesturePlan, type ScrollDirection } from '../../core/scroll-gesture.ts'; import { runIosRunnerCommand } from './runner-client.ts'; import type { RunnerCommand } from './runner-contract.ts'; @@ -107,8 +106,8 @@ export function iosRunnerOverrides( swipe: async (x1, y1, x2, y2, durationMs) => { return await runIosRunnerCommand( device, - iosDragCommand(device, ctx, x1, y1, x2, y2, durationMs, { - synthesizedDefaultDurationMs: IOS_SWIPE_DEFAULT_DURATION_MS, + iosDragCommand(ctx, x1, y1, x2, y2, durationMs, { + defaultDurationMs: IOS_SWIPE_DEFAULT_DURATION_MS, }), runnerOpts, ); @@ -116,9 +115,8 @@ export function iosRunnerOverrides( pan: async (x1, y1, x2, y2, durationMs) => { return await runIosRunnerCommand( device, - iosDragCommand(device, ctx, x1, y1, x2, y2, durationMs, { - synthesizedDefaultDurationMs: 500, - legacyDefaultDurationMs: 500, + iosDragCommand(ctx, x1, y1, x2, y2, durationMs, { + defaultDurationMs: 500, }), runnerOpts, ); @@ -266,7 +264,6 @@ function iosTapCommand( } function iosDragCommand( - device: DeviceInfo, ctx: RunnerContext, x: number, y: number, @@ -274,14 +271,10 @@ function iosDragCommand( y2: number, durationMs: number | undefined, options: { - synthesizedDefaultDurationMs: number; - legacyDefaultDurationMs?: number; + defaultDurationMs: number; }, ): RunnerCommand { - const useSynthesizedDrag = shouldUseSynthesizedIosDrag(device); - const normalizedDurationMs = useSynthesizedDrag - ? iosGestureDurationMs(durationMs, options.synthesizedDefaultDurationMs) - : (durationMs ?? options.legacyDefaultDurationMs); + const normalizedDurationMs = iosGestureDurationMs(durationMs, options.defaultDurationMs); return { command: 'drag', x, @@ -289,7 +282,6 @@ function iosDragCommand( x2, y2, ...(normalizedDurationMs !== undefined ? { durationMs: normalizedDurationMs } : {}), - ...(useSynthesizedDrag ? { synthesized: true } : {}), appBundleId: ctx.appBundleId, }; } @@ -347,14 +339,13 @@ async function runAppleScroll( const runnerResult = await runRunnerCommand( device, iosDragCommand( - device, ctx, frame.originX + plan.x1, frame.originY + plan.y1, frame.originX + plan.x2, frame.originY + plan.y2, undefined, - { synthesizedDefaultDurationMs: IOS_SWIPE_DEFAULT_DURATION_MS }, + { defaultDurationMs: IOS_SWIPE_DEFAULT_DURATION_MS }, ), runnerOpts, ); From a39cf1bc03ad03e35824ff396535c3c460fd4c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 9 Jun 2026 16:06:55 +0200 Subject: [PATCH 2/2] fix: update iOS drag CI expectations --- src/platforms/ios/__tests__/index.test.ts | 9 +++------ src/platforms/ios/interactions.ts | 13 ++++++++++--- test/integration/provider-scenarios/ios-world.ts | 1 - 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/platforms/ios/__tests__/index.test.ts b/src/platforms/ios/__tests__/index.test.ts index 1adf7de73..28a6426b1 100644 --- a/src/platforms/ios/__tests__/index.test.ts +++ b/src/platforms/ios/__tests__/index.test.ts @@ -217,7 +217,7 @@ for (const [name, device] of [ }); } -test('iosRunnerOverrides maps swipe to synthesized iOS drag duration', async () => { +test('iosRunnerOverrides maps swipe to XCTest iOS drag duration', async () => { mockRunIosRunnerCommand.mockResolvedValue({}); const { overrides } = iosRunnerOverrides(IOS_TEST_SIMULATOR, { @@ -235,7 +235,6 @@ test('iosRunnerOverrides maps swipe to synthesized iOS drag duration', async () x2: 180, y2: 200, durationMs: 300, - synthesized: true, appBundleId: 'com.example.App', }); assert.deepEqual(mockRunIosRunnerCommand.mock.calls[1]?.[1], { @@ -245,7 +244,6 @@ test('iosRunnerOverrides maps swipe to synthesized iOS drag duration', async () x2: 180, y2: 200, durationMs: 250, - synthesized: true, appBundleId: 'com.example.App', }); assert.deepEqual(mockRunIosRunnerCommand.mock.calls[2]?.[1], { @@ -255,7 +253,6 @@ test('iosRunnerOverrides maps swipe to synthesized iOS drag duration', async () x2: 180, y2: 200, durationMs: 300, - synthesized: true, appBundleId: 'com.example.App', }); }); @@ -286,7 +283,7 @@ for (const [name, device] of [ } for (const [name, device, expectedGestureFields] of [ - ['iOS', IOS_TEST_SIMULATOR, { durationMs: 250, synthesized: true }], + ['iOS', IOS_TEST_SIMULATOR, { durationMs: 250 }], ['macOS', MACOS_TEST_DEVICE, {}], ] as const) { test(`iosRunnerOverrides maps ${name} scroll to the expected drag path`, async () => { @@ -938,7 +935,7 @@ test('screenshotIos retries simulator capture timeouts and eventually succeeds', else process.env.AGENT_DEVICE_TEST_SCREENSHOT_COUNT_FILE = previousScreenshotCountFile; await fs.rm(tmpDir, { recursive: true, force: true }); } -}); +}, 10_000); test('openIosApp web URL on iOS device without app falls back to Safari', async () => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-device-ios-safari-test-')); diff --git a/src/platforms/ios/interactions.ts b/src/platforms/ios/interactions.ts index 71e658f60..cf240704d 100644 --- a/src/platforms/ios/interactions.ts +++ b/src/platforms/ios/interactions.ts @@ -106,7 +106,7 @@ export function iosRunnerOverrides( swipe: async (x1, y1, x2, y2, durationMs) => { return await runIosRunnerCommand( device, - iosDragCommand(ctx, x1, y1, x2, y2, durationMs, { + iosDragCommand(device, ctx, x1, y1, x2, y2, durationMs, { defaultDurationMs: IOS_SWIPE_DEFAULT_DURATION_MS, }), runnerOpts, @@ -115,8 +115,9 @@ export function iosRunnerOverrides( pan: async (x1, y1, x2, y2, durationMs) => { return await runIosRunnerCommand( device, - iosDragCommand(ctx, x1, y1, x2, y2, durationMs, { + iosDragCommand(device, ctx, x1, y1, x2, y2, durationMs, { defaultDurationMs: 500, + legacyDefaultDurationMs: 500, }), runnerOpts, ); @@ -264,6 +265,7 @@ function iosTapCommand( } function iosDragCommand( + device: DeviceInfo, ctx: RunnerContext, x: number, y: number, @@ -272,9 +274,13 @@ function iosDragCommand( durationMs: number | undefined, options: { defaultDurationMs: number; + legacyDefaultDurationMs?: number; }, ): RunnerCommand { - const normalizedDurationMs = iosGestureDurationMs(durationMs, options.defaultDurationMs); + const normalizedDurationMs = + device.platform === 'ios' && device.target !== 'tv' + ? iosGestureDurationMs(durationMs, options.defaultDurationMs) + : (durationMs ?? options.legacyDefaultDurationMs); return { command: 'drag', x, @@ -339,6 +345,7 @@ async function runAppleScroll( const runnerResult = await runRunnerCommand( device, iosDragCommand( + device, ctx, frame.originX + plan.x1, frame.originY + plan.y1, diff --git a/test/integration/provider-scenarios/ios-world.ts b/test/integration/provider-scenarios/ios-world.ts index 1e2ec346a..7c859f7f3 100644 --- a/test/integration/provider-scenarios/ios-world.ts +++ b/test/integration/provider-scenarios/ios-world.ts @@ -76,7 +76,6 @@ export async function createIosSettingsWorld(): Promise { x2: 276, y2: 122, durationMs: 500, - synthesized: true, appBundleId: 'com.apple.Preferences', }, result: { dragged: true },