Skip to content

fix: keep iOS synthesized drags off AX#1148

Merged
thymikee merged 2 commits into
mainfrom
fix/ios-ax-free-synthesized-drag
Jul 8, 2026
Merged

fix: keep iOS synthesized drags off AX#1148
thymikee merged 2 commits into
mainfrom
fix/ios-ax-free-synthesized-drag

Conversation

@thymikee

@thymikee thymikee commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

Make iOS scroll and synthesized coordinate drags avoid XCTest AX/window/keyboard lookup so they keep working when the app accessibility tree is unhealthy.

Default iOS scroll to the synthesized drag lane, add finite screenshot-size frame fallback/validation, and reuse the AX-free path for synthesized sequence drags while keeping macOS/tvOS behavior unchanged.

Validation

  • pnpm build:xcuitest:ios
  • pnpm build:xcuitest:macos
  • pnpm check:quick
  • pnpm exec vitest run src/platforms/apple/core/tests/index.test.ts
  • pnpm build
  • Live iPhone 17 Pro simulator C25DBB5B-9254-4293-A8D5-2785C78DE03A: opened Bluesky, confirmed sparse/AX-rejected snapshot, then verified scroll down --pixels 400, gesture pan, gesture fling, press, double-tap, swipe, and screenshot capture on the same session.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.6 MB 1.6 MB +280 B
JS gzip 502.9 kB 503.0 kB +66 B
npm tarball 601.5 kB 602.5 kB +1.0 kB
npm unpacked 2.1 MB 2.1 MB +4.0 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.7 ms 27.0 ms +0.3 ms
CLI --help 54.5 ms 54.0 ms -0.5 ms

Top changed chunks: no changes in the largest emitted chunks.

@thymikee

thymikee commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Swift Runner Unit Compile is failing with a real compile error:

  • apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift:1455:50: ambiguous use of .infinity
  • The failing line is fallbackScreenshotSize: { CGSize(width: .infinity, height: 932) }

Please make the type explicit there, for example CGFloat.infinity or another finite/typed value that matches the test intent, then rerun pnpm build:xcuitest:ios / the Swift runner unit compile gate.

@thymikee

thymikee commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Review — keep iOS synthesized drags off AX

Solid direction: routing iOS scroll and synthesized coordinate drags through an AX-free lane (no window/keyboard/element lookup) is the right way to keep interaction working when the accessibility tree is unhealthy, and the frame-resolution ladder is well unit-tested. A few consequences of going AX-free are worth a conscious look before merge — the biggest is a hot-path perf regression, plus a couple of behaviors that silently changed.

Findings (most-severe first)

1. Every synthesized gesture now takes redundant full-screen screenshots (hot-path regression).
RunnerTests+Interaction.swift:1064synthesizedGestureReferenceFrame(app _:) ignores the app and calls finiteSynthesizedReferenceFrame(appFrame: .zero, fallbackBounds: .zero, …). isUsableReferenceFrame(.zero) is always false, so production always falls through to XCUIScreen.main.screenshot().image.size (:1069) — the well-tested appFrame/fallbackBounds branches are dead in production. Worse, each layer re-resolves the frame independently: a single scroll captures 3 screenshots (scrollReferenceFrameaxFreeSynthesizedDragPlansynthesizedDragAt), a synthesized drag 2, a synthesized tap 1 — where the old path used the free app.frame. On the hottest gesture (tap) this replaces a cheap property read with a synchronous main-thread screen capture (tens of ms), which cuts against the perf goal and compounds under settle/record loops.
Fix: resolve the frame once and thread plan.referenceFrame into synthesizedDragAt instead of recomputing; and/or wire a cheap AX-free source (UIScreen.main.bounds, points) into fallbackBounds so the screenshot is only a last resort.

2. iOS synthesized drag/scroll dropped keyboard avoidance.
RunnerTests+CommandExecution.swift:1230 / :1338 — the iOS lane bypasses both keyboardAvoidingDragPoints and resolvedTouchReferenceFrameframeAvoidingKeyboard. With a text field focused (healthy tree), a downward scroll now starts at ~85% of full screen height — inside the keyboard region — so the synthesized swipe lands on the keys instead of scrolling the list underneath. This is an intentional cost of staying AX-free, but it silently regresses the common focused-textfield case; worth confirming it's acceptable.

3. scroll/drag lost the XCUICoordinate fallback; scroll is now unconditionally synthesized.
RunnerTests+CommandExecution.swift:857 / :1291executeSynthesizedDragGesture returns non-nil for every iOS outcome, and scroll passes synthesized: shouldUseSynthesizedScrollPath() (always true), where it previously synthesized only when durationMs != nil. If the private XCTest event-synthesis symbol ever goes unavailable (Xcode drift / API removal), every iOS scroll and synthesized drag hard-fails UNSUPPORTED_OPERATION where the old default path degraded to a coordinate dragAt. Reasonable tradeoff given the new hint text, but it removes the safety net for a whole feature.

4. Preflight-skip now bypasses the foreground guard + interaction stabilization for scroll/drag/sequence.
RunnerTests+CommandExecution.swift:1388 + :541 — extending shouldSkipAppActivationPreflight to scroll/drag/sequence takes the early resolveAppWithoutActivation branch, skipping interaction_foreground_guard re-activation (:582) and applyInteractionStabilizationIfNeeded() (:599). A scroll issued after the target app was backgrounded injects the swipe into whatever is foreground (SpringBoard/another app) instead of re-activating it, and gestures can fire mid-transition-animation. This was already true for tap, so it's consistent — but scroll/drag previously always ran the activation path, so it's a behavior change worth an explicit sign-off.

5. Landscape coordinate mapping — low confidence, worth a device check.
RunnerTests+Interaction.swift:1064nativeSynthesizedPoint assumes frame is interface-oriented (as app.frame was). XCUIScreen.main.screenshot().image.size orientation/points-vs-pixels semantics differ from app.frame; portrait was validated live, but in landscape the height - localY rotation could place touches at the wrong native coordinate. Recommend a quick landscape drag/tap check on device.

Minor cleanups

  • The min(max(… ?? 250, 16), 10000) duration clamp is duplicated across executeDragGesture, executeSynthesizedDragGesture, and performSequenceStep — a shared helper avoids drift.
  • The finite-coordinate guard runs up to 3× per drag (executeDragGestureaxFreeSynthesizedDragPlansynthesizedDragAt) with three different error messages; the inner two are unreachable once the outer passes.

Automated review — please sanity-check findings 2–5 against intended behavior; several are deliberate AX-free tradeoffs rather than outright bugs.

@thymikee

thymikee commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Addressed the review/CI blockers in ab81d9d.

Changes:

  • Fixed the Swift unit compile failure by using a typed CGFloat.infinity in the finite-frame test.
  • Removed repeated frame resolution on synthesized drags: scroll computes the screenshot frame once and passes it through to synthesized drag; sequence drag does the same.
  • Tried a cheap AX-free UIKit bounds source, but live verification showed it reports 320x480 on the iPhone 17 Pro runner while the actual interaction viewport is 402x874, so I removed it rather than ship bad coordinates. Screenshot sizing remains the fallback source, but now it is resolved once per gesture path instead of redundantly.
  • Restored foreground guard/stabilization for drag, scroll, and sequence by limiting activation-preflight skip back to synthesized coordinate taps only.
  • Added best-effort keyboard avoidance for synthesized drag/scroll when the last snapshot did not report AX rejection; after AX-rejected snapshots, the runner avoids keyboard AX probes to preserve the unhealthy-tree coordinate lane.
  • Restored XCUICoordinate fallback for explicit synthesized drag when private synthesis is unavailable. Default iOS scroll intentionally still returns unsupported instead of falling back to AX-backed coordinate drag after private synthesis failure, because that fallback is the failing path on AX-broken screens.

Validation:

  • pnpm build:xcuitest:ios
  • pnpm build:xcuitest:macos
  • pnpm check:quick
  • pnpm exec vitest run src/platforms/apple/core/tests/index.test.ts
  • pnpm build
  • Live iPhone 17 Pro simulator C25DBB5B-9254-4293-A8D5-2785C78DE03A: Bluesky portrait scroll down --pixels 400 succeeded with referenceWidth 402/referenceHeight 874; landscape-left pan succeeded with referenceWidth 874/referenceHeight 402; screenshot artifact captured at /private/tmp/ios-ax-drag-rereview-landscape.png; session closed and verified no sessions remain.

Residual note:

  • A full interactive snapshot on the current Bluesky screen still hit the severe AX runner recycle budget path during smoke. That is the pre-existing unhealthy-tree condition this PR routes coordinate scroll/drag around; coordinate scroll still succeeded afterward.

@thymikee thymikee force-pushed the fix/ios-ax-free-synthesized-drag branch from ab81d9d to 47f0063 Compare July 7, 2026 16:34
@thymikee

thymikee commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Re-review after 47f0063: no actionable blockers found. The prior Swift compile failure is fixed, synthesized scroll/drag now reuses the resolved reference frame instead of recomputing screenshots, drag/scroll/sequence stay on the foreground guard/stabilization path, keyboard avoidance is gated away from AX-rejected states, and explicit synthesized drag keeps a coordinate fallback. CI is green across 21 checks, and the PR includes live iPhone 17 Pro simulator evidence for portrait scroll and landscape pan with session cleanup noted. Labeling ready-for-human for maintainer judgment.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Jul 7, 2026
@thymikee thymikee merged commit 694266d into main Jul 8, 2026
21 checks passed
@thymikee thymikee deleted the fix/ios-ax-free-synthesized-drag branch July 8, 2026 09:06
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-08 09:07 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant