dusk:find
Mint a re-resolvable q handle backed by one or more predicates (text, semanticsLabel, key). Mirrors Playwright's Locator semantics: every action call against a qN handle re-walks the live Semantics tree on each invocation, so the handle survives intermediate widget rebuilds without going stale.
qN and eN token spaces are disjoint. A handle minted by dusk:find is always qN; a handle harvested from dusk:snap is always eN. The dispatcher distinguishes by prefix, but action commands accept either shape via --ref=.
Table of contents
Synopsis
dart run fluttersdk_dusk dusk:find [--text=]
[--contains=]
[--semanticsLabel=]
[--key=]
dusk:find requires a running Flutter session (CommandBoot.connected). It dials the VM Service URI, calls ext.dusk.find with the supplied predicates, and prints the minted handle envelope as pretty-printed JSON.
At least one of the four options must be non-empty; an empty params map returns exit code 1 with a CLI-side error before the VM Service call.
Arguments
| Option | Type | Default | Description |
|---|---|---|---|
--text |
string | unset | Exact match against the widget's visible text label (the Semantics value or rendered Text content). Most common predicate; mirrors Playwright's getByText with exact-match semantics. |
--contains |
string | unset | Substring match against the visible text label or Semantics label (case-sensitive). Use when the label is dynamic (counters, timestamps, plurals) and exact --text is too brittle. |
--semanticsLabel |
string | unset | Exact match against the widget's accessibility label (the explicit Semantics(label: ...) value set by the widget tree). Use when the visible text and the a11y label diverge. |
--key |
string | unset | Match the widget's ValueKey identifier (the Key('signin-button') form). Most precise; survives label and copy changes. |
--within |
string (e) |
unset (whole tree) | Evaluate the predicates inside one subtree. The scope becomes part of the minted q handle, so it survives into every re-resolve; once the scope stops resolving the handle reports matched: false with a diagnostic naming it. Take the ref from a dusk:snap output: a ref minted by dusk:wait carries no semantics node, so it cannot bound a --semanticsLabel lookup and that combination is refused with a diagnostic rather than widened to the whole tree. |
The predicates compose AND: a dusk:find --text=Sign --key=signin-button call returns the widget that matches both. Use a single predicate when the agent only needs one axis.
The CLI guards an empty params map (Provide at least one of --text / --contains / --semanticsLabel / --key.) so the VM Service handler never sees a zero-predicate call.
Returns
dusk:find returns an integer exit code via Future:
| Exit code | Meaning |
|---|---|
0 |
Handle minted (or re-used; the registry is content-addressed). The handler emits the JSON envelope below. |
1 |
No predicate supplied. The CLI guard fires before the VM Service call. |
| non-zero | VM Service handler returned ServiceExtensionResponse.error. Typical cause: the predicate matched zero widgets (no matches surfaces as a structured failure so the agent knows to broaden the predicate). |
Success envelope (illustrative):
Single match:
{
"ref": "q1",
"matched": true,
"matchCount": 1
}
Multi-match (ambiguous predicate):
{
"ref": "q1",
"matched": true,
"matchCount": 2,
"diagnostic": "label 'Password' matched 2 nodes; refine with --key, --text, or --contains"
}
matchCount > 1 means the predicate is ambiguous: the handle still resolves to the FIRST match (backward-compatible), but the agent should narrow with an additional predicate before acting. Common disambiguation strategies:
- Add
--key=when the widget carries aValueKey. - Add
--text=when the accessibility label and the visible text differ. - Use
--contains=when only part of the label is unique.
Error envelope:
The VM Service handler propagates errors as ServiceExtensionResponse.error(extensionError, message). The CLI surfaces them via ArtisanContext.callExtension and exits non-zero. Common messages include No widget matched predicates: {}.
Re-resolution semantics
A qN handle stores the predicate map, not the matched widget. Every action call (dusk:tap --ref=q1, dusk:type --ref=q1, etc.) re-executes the query against the live Semantics tree. Three consequences:
- Widget rebuilds don't invalidate the handle. A
ListViewswap, a navigation transition, or a state-driven rebuild all leave the handle valid as long as the predicates still match something. - The query is re-run on every action. Cheap (a Semantics walk on each call), but emergent if the predicate is broad: prefer
--keyover--textfor hot paths. dusk:finditself is idempotent in the registry. Calling it twice with the same predicate map returns the sameqN; the registry is content-addressed.
eN handles minted by dusk:snap work the opposite way: they freeze the matched widget at snap time and go stale on the next rebuild. Use eN for one-shot reads, qN for any sequence that spans more than one frame.
Examples
1. Mint a handle by visible text
dart run fluttersdk_dusk dusk:find --text="Sign in"
Expected output (illustrative):
{
"ref": "q1",
"matchCount": 1,
"rect": [120, 400, 240, 48],
"role": "button",
"label": "Sign in"
}
Reuse q1 across subsequent action calls:
dart run fluttersdk_dusk dusk:tap --ref=q1
2. Mint a handle by accessibility label
dart run fluttersdk_dusk dusk:find --semanticsLabel="Submit form"
Use when the rendered button text is an icon and the only stable predicate is the a11y label.
3. Mint a handle by widget key (most precise)
dart run fluttersdk_dusk dusk:find --key="signin-submit"
Survives copy changes and a11y-label changes. Pair with a widget-side Key('signin-submit') declaration.
4. Mint a handle by substring (dynamic label)
dart run fluttersdk_dusk dusk:find --contains="pushed the button"
Useful when the visible label is dynamic, e.g. "You have pushed the button 5 times:" (counter changes per tap). --text would only match the exact string at the moment of capture; --contains survives the counter advancing.
5. Compose two predicates to disambiguate
dart run fluttersdk_dusk dusk:find --text="Save" --key="monitor-form-save"
The two predicates AND together; useful when the screen has multiple "Save" buttons but only one with the canonical key.
e-ref staleness and when to prefer q-handles
e tokens minted by dusk:snap are frozen to the Semantics node that was
live at snap time. They become defunct the moment the node leaves the tree, which
happens on any route push, list rebuild, or conditional widget swap. The
RefRegistry that backs e tokens does NOT re-resolve; calling an action
with a stale e returns a defunct (element no longer mounted) failure.
q handles minted by dusk:find store the predicate set instead of the
node, and re-walk the live tree on every action call. They survive navigations,
hot-reloads, and full widget rebuilds as long as the predicate still matches
something in the tree.
When to reach for dusk:find / q instead of using the e from a
snap:
- The page might rebuild between snap and action (e.g. Settings pages with dynamic sections, lists driven by async data).
- The agent will retry an action (gate failure, transient loading state).
- The flow spans more than one navigation hop; an
efrom the previous screen is always stale after the route change. - The agent holds a ref across a hot-reload.
The RefRegistry is intentionally frozen for e (it is a FIFO token store,
not a live observer). There is no mechanism to refresh a stale e in place;
the design intent is that dusk:snap re-mints the ref after every page change.
For rebuild-prone pages, prefer dusk:find / dusk:observe from the start.
Avoiding --semanticsLabel over-match
--semanticsLabel performs an exact case-sensitive match against
SemanticsNode.label and returns the FIRST node in tree order. When two or
more nodes carry the same label (e.g. two TextField widgets both labelled
Password on a sign-up form, or a list of repeated row controls), the handle
resolves to the first node in tree order, which may not be the intended target.
The matchCount field in the response tells the agent how many nodes matched.
A diagnostic key appears when matchCount > 1, e.g.:
label 'Password' matched 2 nodes; refine with --key, --text, or --contains
Disambiguation strategies (most to least precise):
- Add
--key=when the widget carries aValueKey. This is the most precise predicate and survives label changes. - Combine
--semanticsLabel=Password --text=Confirmwhen the second node has distinct visible text (some widgets expose both a label and a text value). - Use
--contains=when only part of the label is unique across the matching nodes. - Use
dusk:observewith a narrowintentand inspect the returned candidate list; each candidate includes role, bounds, and enricher fields that let the agent identify the correct target before minting the handle.
Interactive nodes win a mixed collision. When a label spans one interactive
node (a button, switch, or text field exposing SemanticsAction.tap) and one or
more inert nodes (a Text heading or a settings label that repeats the control's
copy), the handle resolves to the first interactive node rather than the first
node in tree order. A visible label naming an adjacent control, or a heading
repeating a button's text, otherwise sits first in tree order and the tap would
land on the inert label. When every match is interactive or every match is inert,
the handle falls back to the first node in tree order. matchCount and
diagnostic still report the full collision either way, so an agent that needs a
specific one of several interactive matches should still refine the predicate.
Each qN gesture then dispatches at the resolved node's own on-screen rect (the
RenderBox bounds of the widget that contributes the node), so off-centre
controls (a sidebar item, a submit button below the fold) receive the tap at
their real position rather than the viewport centre.
See also
- dusk:snap: produce
eNrefs for one-shot reads. - dusk:tap: consume the
qNref to synthesise a tap. - dusk:observe: structured candidate list of every interactive widget; useful when the agent doesn't know which predicate to query.