Driving real apps: gotchas for agents
A long real-app E2E session surfaces a recurring set of traps that every agent
re-discovers the hard way. This page collects them, with the workaround each one
now has built into fluttersdk_dusk. Read it once before driving a non-trivial
app; it will save you a dozen dead-end round trips.
Table of contents
- 1. Refs go stale on rebuild
- 2. Text fields may snapshot nested
- 3. dusk:console needs (less than) you think
- 4. dusk:exceptions is cumulative
- 5. restart preserves the CDP port
- 6. Overlays get stuck
1. Refs go stale on rebuild
e refs from dusk:snap freeze the widget at snapshot time. A navigation,
a modal open/close, a setState, or any significant rebuild invalidates them:
the next action on a stale e fails with a not-found or stale envelope.
Workaround:
- Re-snap (
dusk:snap) after any action that changes the screen, and use the fresh refs. - For a target that survives re-renders (a stable
Text, accessibility label, orKey), preferdusk:find/dusk:observe, which mint a re-resolvableqhandle that re-walks the live tree on every action call.qhandles survive rebuild, route push, and snapshot disposal as long as the predicates still match. - Pointer verbs (
dusk:tap,dusk:hover,dusk:drag, ...) now dispatch at the element's LIVE rect, not the cached snapshot rect, so a target that merely shifted slots (sameElement/RenderObject) is still hit correctly. The false-success class (gate passes, pointer lands on the stale position,onTapnever fires) is gone for slot shifts; genuine rebuilds still need a re-snap.
2. Text fields may snapshot nested
A wind WInput (and any TextField wrapped in Semantics(textField: true))
historically snapshotted as TWO nested textbox nodes, because RenderEditable
unconditionally owns its own textField Semantics node and MergeSemantics
cannot absorb it. Agents naturally targeted the inner leaf, where dusk:type
threw a -32000.
Workaround:
dusk:snapnow collapses the nested pair (by render-object containment, never label/value equality, so two sibling fields sharing a label stay distinct) and emits a single ref for the outer node, markedtypeable: true. Target the node carryingtypeable: true.- Better: use
dusk:fill --ref= --text=, which focuses, clears, types, and settles in one call (and retries once on a stale handle). It resolves the right editable for you and composes the gated focus/clear/type handlers, so you do not re-build the focus + clear + type + settle dance.
3. dusk:console captures debugPrint in-package now
dusk:console historically surfaced full structured logs only when
fluttersdk_telescope was installed and wired.
Workaround:
DuskPlugin.install()now chains adebugPrintoverride that records everydebugPrint(...)/print(...)call into a bounded in-package ring buffer, so those entries appear indusk:consoleeven without telescope.- When telescope IS installed it enriches the output with
Logger.root.onRecordentries and its other watchers. Directdart:developer log()calls that bypassdebugPrintstill require telescope'sLogWatcher.
4. dusk:exceptions is cumulative
dusk:exceptions returns the full exception history by default, so a single
pre-existing error keeps re-appearing after every action and produces false
positives when you are checking whether YOUR action raised something new.
Workaround:
- Record the current time before the action, then pass
dusk:exceptions --since=afterwards to get only exceptions raised strictly after that timestamp. Unparseablesincevalues are treated as absent (full list).
5. restart preserves the CDP port
When artisan was started with --cdp-port (the web path that backs
dusk:screenshot CDP fallback, dusk:resize, dusk:device), a naive restart
used to drop the port, breaking those CDP-routed tools until you restarted with
the flag again.
Workaround:
artisan restart(and barestart) now re-read the priorcdpPortfrom~/.artisan/state.jsonand reuse it as the default, so CDP stays wired across restarts. An explicit--cdp-portstill wins.
6. Overlays get stuck
A left-over dialog, bottom sheet, dropdown menu, or barrier modal blocks the
next screen from rendering, and a single dusk:modal (dismiss-modals) does not
clear overlays that are not PopupRoutes.
Workaround:
- Use
dusk:reset_overlays, which runs three idempotent layers: dismiss everyPopupRoute, pressEscape(for shortcut-driven overlays), then tap a Cancel/Dismiss/Close/OK/Done affordance as a last resort. Safe to call speculatively between flows; the response (popped,escaped,dismissTapped) tells you which layer cleared the screen.
See also
- dusk:fill, dusk:reset_overlays
- dusk:tap (
--verify,--until) - dusk:exceptions, dusk:find
- Reference: Actionability gate