fdemon_driver

In-process driver for debug-mode Flutter apps. It registers ext.fdemon.driver.* Dart VM service extensions so fdemon can find widgets and synthesize taps, scrolls, long-presses, and text entry — the same in-process mechanism integration_test uses.

Setup

import 'package:fdemon_driver/fdemon_driver.dart';

void main() {
  enableFdemonDriver(); // No-op in release builds.
  runApp(const MyApp());
}

That's it. When the app runs in debug mode, fdemon connects to the app's Dart VM service and drives it through the registered extensions.

Protocol

Every extension takes a single args parameter holding a JSON-encoded object (individual string parameters are accepted as a fallback) and returns JSON. Failures are returned as {"error": "...", ...} payloads — for example with a candidates list when a target matches zero or multiple widgets — never as RPC faults.

Extension Args Result
ext.fdemon.driver.ping {"ok":true,"version":"0.6.2"}
ext.fdemon.driver.find by (key|text|type|semanticsLabel), value, limit=10 {"matches":[...],"truncated":bool}
ext.fdemon.driver.tap by+value (unique match) or x+y (global logical px), settle=true, settleTimeoutMs=3000 {"ok":true,"settled"?:bool,...}
ext.fdemon.driver.longPress same targeting (incl. settle), durationMs=600 {"ok":true,"settled"?:bool,...}
ext.fdemon.driver.scroll same targeting, dx, dy (pointer movement), durationMs=300, steps=20 {"ok":true,...}
ext.fdemon.driver.enterText by, value, text {"ok":true,...}
ext.fdemon.driver.semanticsAction by (semanticsLabel), value, action (tap|longPress|scrollUp|scrollDown) {"ok":true,...}

find match entries have the shape {"description","type","key"?,"text"?,"rect":{"x","y","w","h"},"hitTestable"} with rect in global logical pixels.

Settling (tap / longPress)

Tapping a widget inside a scrollable that is still fling-decelerating is standard Flutter behavior gone wrong for automation: the pointer-down is consumed as "stop scrolling" instead of activating the item, and find can report mid-fling rects (including hitTestable: false for moving targets).

tap and longPress therefore settle widget targets by default (settle, default true; coordinate x+y gestures never settle): the handler re-resolves the widget fresh (it may rebuild while scrolling) and samples its global rect every ~80 ms until two consecutive samples agree within 0.5 px on all four edges, then computes the gesture point. If the rect has not stabilized after settleTimeoutMs (default 3000) of sampling, the gesture proceeds anyway at the last observed position — a target inside a looping animation must not fail the action — and the response carries "settled": false plus "motion": {"dx","dy","samples"} (the center movement observed over the sampling window). A successful settle adds "settled": true; pass "settle": false to act immediately with no settled field.

Semantics labels

semanticsAction matches labels exactly first; when nothing matches exactly it falls back to a whitespace-normalized, case-insensitive substring match, so labels merged from several widgets (for example by MergeSemantics, which joins the parts with newlines) are still targetable by any one part. No-match errors include a candidates list of the labeled semantics nodes currently on screen so the caller can pick a valid label.

Security note

The driver is debug-mode only: enableFdemonDriver() runs entirely inside an assert, so it compiles to nothing in release builds. Even so, treat the Dart VM service as a privileged surface — anyone who can reach the VM service port can drive the app, read memory, and execute code. Keep the VM service bound to localhost (the Flutter default) and never expose it on untrusted networks.

Libraries

fdemon_driver
In-process driver for debug-mode Flutter apps.