Skip to content

feat: add fdb native-tap + native in-process tap for fdb tap --at#63

Merged
andrzejchm merged 13 commits intomainfrom
feature/native-tap
Apr 28, 2026
Merged

feat: add fdb native-tap + native in-process tap for fdb tap --at#63
andrzejchm merged 13 commits intomainfrom
feature/native-tap

Conversation

@andrzejchm
Copy link
Copy Markdown
Owner

@andrzejchm andrzejchm commented Apr 27, 2026

Summary

Adds two complementary commands for tapping native UI that Flutter's GestureBinding cannot reach:

  1. fdb native-tap --at x,y — out-of-process tap via platform tools (Android: adb shell input tap; iOS sim: IndigoHID via SimulatorKit). Targets system-level UI that lives outside the app's window.
  2. fdb tap --at x,y (existing command, new behavior) — in-process tap via the new fdb_helper Pigeon NativeTapApi. Reaches UIAlertController, AlertDialog, in-process WebView content, and platform views.

Selector-based taps (--key, --text, --type) continue to use Flutter's GestureBinding for backwards compatibility.

The iOS 26 fix

The hardest part of this PR: iOS 26 broke the standard EarlGrey/KIF approach to UITouch + UIEvent injection. The fix mirrors KIF v3.12.2 (PR #1334) which was released in Oct 2025:

  • iOS 26 enforces stricter validation that the UIEvent's internal IOHIDEvent snapshot matches the current UITouch phase
  • Reusing a single UIEvent across Began → Ended phases (the pre-iOS-26 idiom) is silently dropped for non-UIControl responders
  • Fix: build a fresh UIEvent with a fresh IOHIDEvent per phase

Implementation lives in packages/fdb_helper/ios/Classes/FdbHelperNativeTap.{h,m} — ObjC because Swift's @convention(c) cannot represent IOKit's 17-argument digitizer event constructors with by-value struct args.

Verified platforms

Platform fdb tap --at (in-process) fdb native-tap (out-of-process)
Android (real device, Android 16) ✅ Tested — WebView HTML button ✅ Tested — adb shell input tap
iOS Simulator (iOS 26.2) ✅ Tested — UIAlertController dismissal ✅ Tested — IndigoHID Swift script
iOS Physical (iPhone 17 Pro, iOS 26.3.1) ✅ Tested — UIAlertController dismissal 🚫 Not supported — see follow-up fdb-6sz
macOS ✅ Tested — Flutter widget tap (counter increment) 🚫 Not supported (documented)

What's NOT supported (intentionally)

  • fdb native-tap on physical iOS — idb's ui tap is simulator-only (idb #853, open since Oct 2023). The de-facto standard for physical iOS tap injection in 2026 is WebDriverAgent (signed XCUITest runner installed on device), which has higher setup burden than the current zero-setup paths. Use fdb tap --at instead — it reaches in-app native overlays on physical iOS via fdb_helper. WDA-based replacement tracked in fdb-6sz.
  • fdb native-tap on macOS — cross-process click injection on macOS Sequoia/Tahoe requires Accessibility permission, which the system only grants to signed .app bundles. Homebrew CLIs (cliclick, opencode, tmux) cannot receive it. Use fdb tap --at instead.

Architecture

  • New Flutter plugin: fdb_helper is now a plugin (was a pure-Dart package), with platform-specific Swift/Kotlin/ObjC under packages/fdb_helper/{ios,android,macos}/.
  • Pigeon-generated NativeTapApi provides the typed Dart↔native bridge (packages/fdb_helper/pigeons/native_tap.dart).
  • Coordinate-based taps in tap_handler.dart route through dispatchNativeTapNativeTapApi.nativeTap. Selector-based taps unchanged.
  • Silent fallback to Flutter's GestureBinding on platforms without native impl (web, Linux, Windows). Clear warning + fallback when supported platform's native call fails.

Test app changes

  • Added NativeViewTestScreen with embedded WebViewWidget for verifying native injection on Android.
  • Added native UIAlertController test on iOS via MethodChannel("fdb_test/native_dialog").
  • Renamed bundle ID from com.fdb.test.testApp to dev.andrzejchm.fdb.testApp (matches flutter/packages convention; old ID was claimed by another developer team).
  • Removed hardcoded DEVELOPMENT_TEAM from project.pbxproj (also matches flutter/packages convention — contributors set their own team via Xcode automatic signing).

Smoke tests

task test:native-view-tap is now platform-aware:

  • Android: navigates to NativeViewTestScreen, taps WebView HTML button via --at, asserts WebView title becomes "TAPPED"
  • iOS sim/physical: shows UIAlertController, taps Confirm via --at 269,488, asserts result text becomes "CONFIRMED"
  • macOS: taps the FAB increment button via --at, asserts counter increases

task test:native-tap exercises the out-of-process path with sane error handling per platform (skips gracefully on iOS physical and macOS with clear messages).

Follow-up tickets (filed)

  • fdb-oru (P2) — fdb launch: surface actionable errors instead of generic "process exited unexpectedly"
  • fdb-9a8 (P3) — fdb launch: add --verbose flag passing through to flutter run --verbose
  • fdb-4e7 (P3) — fdb devices: handle wireless/stale device discovery
  • fdb-dty (P3) — fdb doctor: physical iOS prerequisite checks
  • fdb-6sz (P3) — fdb native-tap: implement physical iOS support via WebDriverAgent

Refs

Adds `fdb native-tap --at x,y` to tap native (non-Flutter) UI elements
such as iOS permission prompts, Android runtime-permission sheets, and
macOS native dialogs — bypassing Flutter's GestureBinding entirely.

Platform dispatch:
- Android: `adb shell input tap`
- iOS simulator: `cliclick` with CGWindowList-based window offset (no AX permission needed)
- iOS physical: `idb ui tap` (requires idb from Meta)
- macOS: `cliclick` with CGWindowList-based Flutter app window offset

Closes #57
- iOS simulator and physical now both use `idb ui tap` via IndigoHID
  (SimulatorKit), which correctly penetrates native OS dialogs and does
  not require fragile window-offset math or Accessibility permission
- Drop all Simulator window CGWindowList lookup and hardcoded chrome
  offset constants — replaced by idb's own coordinate handling
- macOS: detect missing Accessibility permission upfront via
  AXIsProcessTrusted() and print a clear, actionable error rather
  than silently dropping the CGEvent injection
- Update README, agents doc, and skill file to reflect idb for both
  iOS targets and document the macOS AX requirement
… injection

Replaces GestureBinding-based coordinate taps with platform-native injection
so that `fdb tap --at x,y` reaches native views overlaid on the Flutter
surface — UIAlertController, WKWebView, AlertDialog, platform views — not
just Flutter widgets.

Implementation:
- Pigeon (v26.3.4) defines the NativeTapApi interface, generating typed
  Dart/Swift/Kotlin bindings with zero hand-rolled codec code
- iOS:  UIApplication.sendEvent() with synthetic UITouch via UIKit private
  APIs (same pattern as EarlGrey/KIF); only used in debug builds
- macOS: NSApplication.sendEvent() with synthetic NSEvent mouseDown/Up
- Android: Activity.dispatchTouchEvent() with MotionEvent in physical pixels
  (Flutter logical → physical via displayMetrics.density); dispatched
  synchronously via CountDownLatch so the Pigeon reply is sent only after
  the tap completes
- Selector-based taps (--key, --text, --type) are unchanged and still use
  GestureBinding after resolving widget coordinates
- Unsupported platforms (web, Linux, Windows) fall back to GestureBinding
@andrzejchm andrzejchm changed the title feat: add fdb native-tap command feat: add fdb native-tap + native in-process tap for fdb tap --at Apr 27, 2026
… use IndigoHIDMessageForMouseNSEvent

Test app changes:
- Add NativeViewTestScreen with embedded WebView (WKWebView/Android WebView)
  showing a native HTML button not in the Flutter widget tree
- Wire to /native-view-test route and add navigation button
- Add task test:native-view-tap Taskfile task verifying fdb tap --at
  reaches native platform views unreachable by --key
- Add webview_flutter dependency to test app

iOS / macOS fdb_helper fix:
- Dispatch UIKit/AppKit sendEvent() on main thread via DispatchQueue.main.sync
  to prevent crash from UIKit access on Dart VM isolate thread
- Use Thread.isMainThread check to avoid deadlock when already on main thread

iOS native-tap fix (native_tap.dart):
- Replace manual IndigoMessage struct construction (which used wrong phase
  values causing app crashes) with IndigoHIDMessageForMouseNSEvent from
  SimulatorKit — the same private function idb uses internally
- This correctly constructs the Mach IPC message without corrupting
  uninitialized struct fields

Status:
- Android: fdb tap --at confirmed working via Activity.dispatchTouchEvent
- iOS sim: fdb native-tap confirmed not crashing, reaches UIKit native views;
  WKWebView requires SimDeviceScreen registration (out of scope for v1)
- macOS: fix applied, not yet tested end-to-end (no AX permission on this machine)
…S 26

iOS 26 enforces stricter validation that the UIEvent's IOHIDEvent snapshot
matches the current UITouch phase. The previous implementation reused the
same UIEvent across Began/Ended phases, which UIKit silently dropped for
non-UIControl responders (UIAlertController, UILabel, UIView).

Reimplements iOS in-process tap to mirror KIF v3.12.2 (PR #1334):
- Construct UITouch via [[alloc] init] then configure via private setters
- Build a fresh UIEvent for EACH phase via _touchesEvent + _clearTouches
- Attach a fresh IOHIDEvent (digitizer hand + finger sub-event) per phase

Implementation moved to ObjC (FdbHelperNativeTap.h/m) because Swift's
@convention(c) cannot represent the 17-argument C functions with by-value
struct args required by IOKit's IOHIDEventCreateDigitizerEvent.

Drops macOS support for fdb native-tap. cliclick path requires Accessibility
permission, which macOS Sequoia/Tahoe only grants to signed .app bundles.
Homebrew CLIs (cliclick, opencode, tmux) cannot receive it. Use fdb tap
--at instead — it works without any system permissions via in-process
fdb_helper.

Verified working:
- iOS 26 simulator: UIAlertController dismissal via fdb tap --at
- iOS 26.3.1 physical (iPhone 17 Pro): same
- Android (Android 16, real device): WebView HTML button via fdb tap --at
- macOS: Flutter widget tap via fdb tap --at (counter increment verified)

Renames test app bundle id from com.fdb.test.testApp to dev.andrzejchm.fdb.testApp
following the flutter/packages convention. Removes hardcoded DEVELOPMENT_TEAM
from project.pbxproj (also flutter/packages convention).

Refs KIF PR #1334: kif-framework/KIF#1334
…upported)

idb's 'ui tap' command is simulator-only — it errors on physical devices
with 'Target doesn't conform to FBSimulatorLifecycleCommands protocol'
(idb issue #853, open since Oct 2023, no fix planned). The physical iOS
code path was therefore never functional.

Removes the idb-based _tapIosPhysical implementation and replaces with a
clear 'not yet supported' error pointing users to:
  - fdb tap --at (in-process via fdb_helper, works on physical iOS for
    in-app overlays like UIAlertController)
  - beads ticket fdb-6sz which tracks the WebDriverAgent-based replacement
    (the de-facto standard for physical iOS tap injection in mid-2026)

Updates README, doc/README.agents.md, SKILL.md, and Taskfile to reflect
the reduced platform support matrix:
  - Android (real + emulator) — adb shell input tap
  - iOS simulator — IndigoHID via SimulatorKit
  - iOS physical — not supported (use fdb tap --at)
  - macOS — not supported (use fdb tap --at)
- Drop unused FdbConfigureTouch helper (was never called).
- Drop redundant setPhase/setTimestamp before Began event in
  FdbHelperNativeTapAtPoint — both are already set during touch setup,
  and FdbBuildEventForTouch updates the HID snapshot per phase.
- Drop unused DEVICE var and STORED_DEVICE local in test:native-tap.
- Rewrite stale comment in _simulatorScreenSize that promised a simctl
  io fallback that was never implemented.

Verified iOS sim 26.2 smoke test still passes:
  task test:native-view-tap  → UIAlertController dismissed via tap --at
  task test:native-tap       → IndigoHID tap dispatched at 200,400
Findings from PR #63 review (sub-agent):

1. Restore long-press semantics for coordinate longpress.
   When rawDuration is provided (i.e. fdb longpress --at), bypass the native
   tap path (which is quick-tap only via Pigeon NativeTapApi) and use
   dispatchTap with holdDuration. Native long-press on overlays will be
   added in a follow-up ticket.

2. Remove leftover hardcoded DEVELOPMENT_TEAM = FZX57HL4C9 from the iOS
   Debug build config (the previous edit only cleaned the Release config).
   Matches the flutter/packages convention referenced in the PR description.

3. Update packages/fdb_helper/AGENTS.md to acknowledge gesture_dispatcher.dart
   as the home for all gesture-dispatch helpers regardless of caller count.
   The native-tap Pigeon dispatch belongs at the gesture-dispatch boundary.

4. Surface native-tap fallback to callers via WARNING= token. When the
   in-process native injection fails on a supported platform and we fall
   back to Flutter's GestureBinding, agents need to know native overlays
   were NOT actually reached. dispatchNativeTap now returns NativeTapResult
   (native | unsupportedPlatform | nativeFailedFallback); the handler
   propagates this to the JSON response; the CLI emits 'WARNING=native_tap_fallback'.

6. Fix lying return type on _simulatorScreenSize. The function always
   returns a value (falls back to iPhone 17 Pro dimensions); change return
   type from Future<(double,double)?> to Future<(double,double)> and remove
   the now-unreachable null check at the call site.

7. Surface Android dispatchTouchEvent timeout. The 2-second latch wait was
   silently ignored; now throws DISPATCH_TIMEOUT FlutterError if the UI
   thread doesn't dispatch within 2s.

8. iOS impl now validates required private selectors upfront via
   FdbMissingRequiredSelector. Returns a clear NSError naming the missing
   selector instead of throwing unrecognized-selector exceptions mid-tap
   when a future iOS version drops a private API.
@andrzejchm andrzejchm merged commit f78d2a7 into main Apr 28, 2026
1 check passed
@andrzejchm andrzejchm deleted the feature/native-tap branch April 28, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant