fix(ios): open the sole-screen gesture route on a full-screen iPad app

TH
43m ago
+798-126

Summary

Taps, long presses, scroll/swipe/gesture-viewport, and the back fallback fail on an iPad simulator when the app's accessibility tree is too deep for XCTest: the sole-screen synthesized-gesture route accepted only an iPhone, though iPad hosts run the same hostile trees. RunnerFullScreenEvidenceFromForeground now decides the proof in-process by comparing the AXRuntime primary app's frame against the system app's frame — the same rule the snapshot bridge refuses a windowed capture on (app-window-not-full-screen, ADR 0004); neither read walks the app's tree. The comparison, tolerance, foreground-element protocol, primary-pid check, and the whole frame-usability test live in one canonical header, apple/snapshot-bridge/ForegroundApplicationGeometry.h, which the bridge includes directly and the runner's UITests target compiles through its HEADER_SEARCH_PATHS (resolved in the checkout and in the installed package; check:packaged-runner-swift guards the packaged search path, and the header is fingerprinted by both the bridge cache identity and the runner source fingerprint). RunnerResolveSoleScreen keeps one owner of the rule, early-returns on the phone idiom before probing, and opens on iPad only on fills-screen; windowed/Stage Manager or unknown keeps requiring a window. The Swift side reads the same single owner (RunnerResolveSoleScreen in soleScreenReferenceFrame), reusing #3920's soleDisplayFrame for the frame itself, so screen-count and screen-space rotation stay #3920's and the device rule stays the bridge's. The back fallback point derives from the screen's top safe-area inset via one formula for every route. The nightly workflow gains an iPad matrix leg inside the existing job (no parallel job).

Validation

Head 36028a1f (includes origin/main at 45125d7, incl. #3920).

  • Shared-header round: the two duplicated rule blocks and the byte-parity guard are deleted; both consumers compile the canonical header. The installed-package build is proven: xcodebuild build-for-testing of the packaged tree in a scratch install layout succeeds, and fails with 'ForegroundApplicationGeometry.h' file not found when the shipped header is removed. The header enters both build identities (bridge source list + runner fingerprint, with a packaged-layout test).
  • Runner unit tests (unit-test variant, own derived path): the nightly iPad selection passes on a dedicated simulator, log shows AGENT_DEVICE_RUNNER_FULL_SCREEN_EVIDENCE result=fills-screen and the SpringBoard negative result=not-primary-app. The testSoleScreenDisplayIDIsNamedOnlyForAnAppThatFillsItsScreen device test asserts the windowed/foreign-owner refusal (not-primary-app → no sole screen) executably.
  • Post-merge live re-check at 36028a1f (Release fixture artifact + JS repack, own sims/state dirs): iPad heavy feed — press id="heavy-post-0-like" → Liked posts: 1, press 417 640, longpress 200 560 800 → Last action: held post 1, scroll down --pixels 500, swipe 417 900 417 400 → Flung, back; the session runner.log carries 10× SYNTHESIZED_SOLE_SCREEN_ROUTE resolved={{0, 0}, {834, 1210}}. Landscape-left: the same synthesis completes and the route resolves the rotated frame {{0, 0}, {1210, 834}}; rotating back to portrait recovers {834, 1210} without relaunch. iPhone heavy feed — press id="heavy-post-0-like" → Liked posts: 1 and back work; AGENT_DEVICE_RUNNER_BACK_FALLBACK_TOP_INSET read=62.0 windows=1; zero probe lines on the phone.
  • pnpm check:xctest-selection (501 declared; host 358, PR 5, nightly 493, nightly-iPad 3, 0 dark) and the darwin -Werror bridge compile pass. pnpm check:affected --run --head 36028a1f passed all runnable checks with exactCommit: true. PR CI: 19/19 pass.
  • XCTest Nightly dispatched on the final head — run https://github.com/callstack/agent-device/actions/runs/40242364443: both legs success; the iPad leg runs the 3-test selection on iPad Pro 11-inch (M5) (log quotes in the linked comment above), and the reporter asserts Executed 3 test(s); the source reaches 3 on this lane.

Base/head comparison for the live table is the pre-PR behavior measured at f4944b5 (see the 2026-10-25 review replies): every heavy-feed gesture timed out or refused; at head each completes in ~1s.