Releases: AlphaWaveSystems/flutter-probe
Releases · AlphaWaveSystems/flutter-probe
Release list
v0.10.4
Fixed
- A hung reconnect attempt could leave the CLI silently stuck with no way
to recover other than manually interrupting it (part of PT-25). The
auto-reconnect retry loop passed the outer, per-run context into each
reconnect attempt — that context has no deadline of its own (only
Ctrl-C/SIGTERM cancels it), unlike the per-step timeout the CLI otherwise
honors. If a device call during reconnect (e.g. anadbcommand) ever
hung, there was nothing to force it to give up. Each reconnect attempt is
now bounded by the same step timeout the CLI already uses everywhere
else. This addresses one confirmed, independently-reproducible
contributing factor reported alongside PT-25's WebSocket-drop
investigation; the drop itself is still open — see PT-25 in
IMPROVEMENT_TASKS.mdfor what's resolved vs. still outstanding. tap 1st #id(an ordinal modifier combined with an id-selector) always
misparsed (PT-26). The parser's ordinal-selector branch only checked for
quoted text or a bare identifier after the ordinal, never an#idtoken —
sotap 1st #post_list_cardleft#post_list_cardcompletely unconsumed,
and it misparsed as a second, stray, unknown recipe call. Fixed by
accepting an id-selector after an ordinal too. Since Flutter itself
enforces uniqueKeys among direct siblings, disambiguating repeated
same-id rows by position also required a matching fix on the Dart agent
side: theordinalselector kind was hardcoded to always match by
displayed text, never by id — now it checks for the#prefix (matching
the plainidselector kind's existing convention) and matches by key
instead when present.
v0.10.3
Fixed
- A recipe named starting with the word "open" (e.g.
open most recent post) always misparsed (PT-23).openis a reserved keyword for the
built-inopen the app/open linkverbs, and the parser claimed any
step starting with it unconditionally — an undocumented fallback then
swallowed the next bare word as a selector and left the rest of the line
to misparse as a second, stray, unknown recipe call. Fixed by only
treatingopenas the built-in verb when what follows actually matches
one of its two documented forms (the app/app, or a link) via a
backtracking lookahead; anything else now falls through to a normal
recipe call, preserving the whole phrase as the call's name. - A recipe with a hyphenated name (e.g.
create looking-for post)
always misparsed (PT-24). The lexer tokenized a word-internal hyphen
as its own standalone token unconditionally — fine for the recipe
definition (a quoted string, hyphen preserved as-is), but at the
call site the recipe name is built by rejoining bare word tokens with
spaces, so the hyphen came back out padded ("looking - for"), and the
two representations could never match. Fixed by keeping a hyphen that's
directly between two word characters part of the same identifier token.
A standalone hyphen (e.g. the negative sign inset location -33.8, 151.2) is unaffected, since it's only ever reached preceded by a space,
never mid-identifier.
v0.10.2
A small release with no functional changes — a reported regression was
re-investigated and re-closed, with a regression test added to lock in the
verified-correct behavior going forward.
Testing
- Locked in sequential tap+type behavior when one field auto-focuses on
page load (PT-21, reopened and re-closed). A report claimed sequential
multi-field text entry breaks specifically when a field requests focus
duringinitState/first frame (e.g. a "remember last email" convenience
feature) — text supposedly keeps landing in that auto-focused field
regardless of which field is tapped afterward. Re-investigated with the
exact discriminator described: a widget test with one field auto-focusing
via a post-framerequestFocus(), plus real-device verification against
the actual login screen with the same pattern temporarily added, in both
tap orders. Both pass cleanly — focus correctly shifts to whichever field
is tapped, and each field retains only its own typed value. Added a
regression test locking this in.
v0.10.1
A quick patch release for regressions surfaced during v0.10.0's own
verification pass, plus an unrelated CI-automation fix found along the way.
Fixed
- The daily Dependabot auto-merge workflow never actually merged anything,
for months. Its skip condition treatedmergeStateStatus == "UNKNOWN"
as "already in the merge queue" — butUNKNOWNjust means GitHub hasn't
finished computing mergeability yet (the common case on a fresh poll), not
"already queued." Every eligible PR was silently skipped every single day
since at least mid-May, despite the workflow itself reporting "success."
Separately, even a PR that had passed that check would have hit a second,
compounding bug: the merge command itself (gh pr merge --squash, no
--auto) doesn't work for a merge-queue-gated branch — it just prints a
warning and does nothing, the same issue found and fixed earlier this
release for this repo's own CI (see PT-19-adjacent commit history). Fixed
both: only skip onDIRTY/BLOCKED(states that definitively block a
merge), and let a merge attempt happen and fail on its own terms
otherwise; added--autoto the merge command. Verified by manually
invoking the corrected logic against the real backlog — 9 previously
stuck PRs (some open since May) are now genuinely in the merge queue. wait for network idle/wait for the page to load/wait for page to loadalways misparsed, silently splitting into a no-op plus a stray,
broken statement (PT-20)."for"(and"the") are both global filler
words thatparseWait's initial filler-strip consumes immediately after
"wait"— by the time the code checked for"for"specifically to
distinguish this verb family, it was already gone, so that check could
never match. The parser fell through to a default case that produced a
WaitPageLoadstep (the right kind, by coincidence) but never consumed
"network idle"/"page to load" — those leftover words became a second,
separate statement that then failed at runtime as an unknown recipe
call. Invisible toprobe lint, since both halves parse as individually
valid syntax; only surfaces when the test actually runs. This bug has
existed since the project's very first commit — not a regression from
any recent release, despite surfacing during this release's own
verification pass. Fixed by detecting the network/page-load cases
directly (mirroring the patternwait for animations to endalready
used for the same underlying issue) and consuming the full phrase before
the next token is checked..github/workflows/e2e.yml's CI E2E suite failedflutter pub getoutright
on every run ("name" field doesn't match expected name "probe_agent").
The workflow'sdependency_overridesblock still referenced the package's
old pre-rename name (probe_agent) instead offlutter_probe_agent— a
stale reference to a rename that happened well before this release. Found
when thev0.10.0tag push triggered this workflow directly (it isn't
gated on regular PRs, only tag pushes, which is why it went unnoticed
through several prior releases). Unrelated to this release's own changes;
fixed by updating the override key to the current package name.
v0.10.0
A hardening release working through a backlog of real E2E test issues
surfaced by driver projects (IMPROVEMENT_TASKS.md, PT-01 through PT-19).
Every fix was reproduced and verified against a real device before shipping,
and several turned out to have a different (or larger) root cause than
originally reported — noted inline below.
Added
agent.launch_timeoutconfig option (and--launch-timeoutflag)
(PT-10).restart the app/clear app dataused to be bounded by a
hardcoded, unconfigurable 90s step timeout — no amount of raising
dial_timeout/token_read_timeoutcould help an app whose actual
cold-launch path does non-trivial async work (e.g. Firebase App Check
re-initialization costing 90-100s) before becoming interactive. Defaults
to 120s; raise it for apps with an expensive startup path.- Verbose connect diagnostics (PT-01).
-v/--verboseonprobe testnow
traces every step of the connect handshake — ADB setup, port forward
setup/teardown, each Android token-read source attempted (run-as,
/data/local/tmp, logcat) with hit/miss detail, WebSocket dial attempts
(including transient retries), and handshake accept/reject. Connect
failures were previously a total black box with zero diagnostic output;
see PT-01 inIMPROVEMENT_TASKS.md. - CLI↔agent version handshake (PT-07).
probe.pingnow carries
client_version(CLI → agent) andagent_version(agent → CLI) alongside
the existing{"ok":true}response. Every connect path (WebSocket dial,
HTTP dial, relay, and reconnect-after-restart) now logs a warning when the
CLI and agent versions differ, and hard-fails with a clear error when they
have different major versions. Older CLIs/agents that don't send or
recognize these fields degrade gracefully — an empty/missing version is
always treated as "unknown," never as a mismatch. Addresses PT-07 in
IMPROVEMENT_TASKS.md— CLI/agent version drift was a standing suspect in
unexplained connection failures with zero signal from the tool itself
until now.
Changed
- ProbeScript now errors loudly instead of silently no-oping on several
classes of malformed script (PT-02):- An unknown recipe call (typo, or the recipe was never defined/isn't
loaded fromrecipes_folder/use) is now a runtime error, not a silent
skip. This was the single highest-leverage fix inIMPROVEMENT_TASKS.md
— a silently-skipped call could mask a completely broken flow (e.g. a
sign-in recipe that never actually signs anyone in) indefinitely, with
every downstream test still reporting green. - An unquoted
<placeholder>(e.g.type <email>instead of
type "<email>") is now a parse error. Angle brackets have no meaning
outside a quoted string in ProbeScript's grammar; unquoted, both brackets
were silently dropped by the lexer, leaving a bare identifier that gets
typed/matched as literal text with no indication anything was wrong. elseis now accepted as an alias forotherwise. Previouslyelse
lexed as a plain identifier, was silently treated as an unknown recipe
call (a sibling step of theif, not nested inside it), and its body ran
unconditionally on every run regardless of theifcondition —
exactly the opposite of what the test author intended, with no error
anywhere.resolve()'s placeholder-substitution loop is now bounded. A variable
bound to a value containing its own placeholder marker (e.g. passing the
unquoted literal<email>as a recipe argument) previously looped
forever substituting the same text for itself, hanging the CLI with no
error; it now terminates, leaving the placeholder unresolved. (Full
positional/named argument forwarding into nested recipe calls — a
larger, separate redesign of how recipe calls are matched against
definitions — is intentionally not part of this change; see PT-02's
"Update" note inIMPROVEMENT_TASKS.md.)
- An unknown recipe call (typo, or the recipe was never defined/isn't
Fixed
drag <selector> to <selector>— the documented syntax — always failed
to parse (PT-19)."to"was lexed as its own token but was never
actually consumable anywhere: it was missing from the parser's
filler-word list and used nowhere else in the grammar. The parser choked
ontowhere it expected the second selector to start, and the rest of
the line got misparsed as an unrelated recipe call. Found during a final
regression pass across the full e2e suite before this release — this bug
has always existed; PT-02's error-loudly fix (above, same release) is what
first surfaced it, since it previously failed silently instead.kill the appfollowed by any step other thanopen the app/
restart the app(await,tap,see, etc.) still hung and then
permanently failed (PT-18, follow-on from PT-09'sopen the appfix).
The generic step-level auto-reconnect only re-dials, assuming the app
process is already running — after a genuinekill the app, nothing is
listening at all, so re-dialing could never succeed regardless of
remaining attempts. Detects a connection-refused dial failure
specifically (as opposed to a timeout/reset on a still-alive process,
which a transient network drop would produce) and relaunches the app
before retrying, the same way theopen the appfix does, but from the
generic reconnect path so it now covers every verb, not justopen.open the appafterkill the appnever actually relaunched the app
(PT-09). It always sent an RPC over the (now-closed) connection, which
failed; the generic step-level auto-reconnect that kicked in afterward
only re-dials assuming the app process is already running — it never
relaunches one that was genuinely force-stopped. In practice this meant
the documentedkill the app→open the apppattern hung through the
full reconnect-retry window and then failed with a connection-refused
error.open the appnow detects a dead connection and relaunches the
app the same wayrestart the appdoes, before reconnecting. Found
while documenting cross-test-block state behavior (see Documentation
below) — not what that investigation originally set out to find, but
blocking anyone who'd try to usekill the app/open the appfor
exactly the per-test isolation those docs describe as the supported way
to opt out of the default shared-state behavior.take screenshotcould capture stale content from the previous route
instead of the current screen (PT-16). Found while investigating a
scroll bug (PT-03): a screenshot taken right after navigating sometimes
looked unchanged, even thoughsee/don't seeassertions confirmed real
navigation had happened. Two independent causes:- Unlike every other verb,
screenshotnever called
_sync.waitForSettled()before capturing — a capture taken right after
navigation could land mid-route-transition instead of waiting for the
push/pop animation to finish. _captureViaRepaintBoundarypicked the largestRenderRepaintBoundary
in the entire element tree with no route-awareness — sinceNavigator
keeps the previous route mounted underneath the current one, and both
routes typically produce a same-size, screen-sized boundary, the strict
area > bestAreacomparison kept whichever one was visited first (the
previous route, inOverlayinsertion order), silently capturing the
old screen instead. Same class of bug as PT-03/PT-15, just in the
screenshot path instead ofProbeFinder/scroll. Fixed by skipping
boundaries belonging to a non-current route, mirroringProbeFinder's
existing fix.
- Unlike every other verb,
scrollcould lose the gesture arena toDismissible-wrapped list rows
and never actually scroll (PT-15).scrollwas a thin delegate to
swipe's pointer-gesture simulation, which has to win the gesture arena
against any competing recognizer along the way — aDismissiblerow's own
HorizontalDragGestureRecognizercould still intercept it. Reproduced
against a real iOS simulator: a 50-item list withDismissiblerows never
scrolled past the first screen, while the identical verb worked fine on a
plain list.scroll's job is "reveal more content," unlikeswipe(which
tests a real gesture interaction like swipe-to-dismiss) — it doesn't need
to enter the gesture arena at all, so it now drives the nearest
Scrollable's ownScrollPositiondirectly instead, sidestepping the
competition entirely. When no selector is given, picks the Scrollable with
the largest viewport (aTextField's own internal cursor-scrolling
Scrollablecould otherwise be found first in tree order and silently
"scroll" nothing visible).scroll/swipecould report success while producing zero visible
movement (PT-03). Root-caused two independent bugs by reproducing
against a real iOS simulator:- The synthetic drag gesture sent a single
PointerMoveEventcovering the
entire distance in one jump. Real touches (and Flutter's own gesture
arena / scroll physics) expect a sequence of incremental moves — a
single jump could fail to register as a scroll at all. Now split into
10 incremental steps, matching a real drag. - The widget-tree finder had no concept of "which mounted route is
actually the current one." Flutter'sNavigatorkeeps previous routes
mounted underneath the current one by default (noOffstagewrapper),
so a screen reached via a stacked push could have several live
Scrollables (and other matching widgets) simultaneously — one per
mounted route — and every selector-based verb (not just scroll/swipe)
could silently resolve to a widget on a route the user can no longer
see. Fixed by checkingModalRoute.of(element)?.isCurrentin the
finder's core visibility check, so this also fixes false-positive
see/wait untilmatches against stale conte...
- The synthetic drag gesture sent a single
v0.9.9
Added
deliver signal "name" ["value"]— new ProbeScript step that resolves
a pendingawaitSignal(name)call in the Flutter app. Use to unblock any
OS-level interaction that isn't in the Flutter widget tree: push permission
dialogs, payment sheets, App Tracking Transparency, deep-link handlers, etc.
The value defaults to"true"when omitted.awaitSignal(String name)— new public function exported from
flutter_probe_agent. Returns aFuture<String>that resolves with the
value sent by the CLI. Generalises theawaitBiometricResult()pattern to
any named signal.DeliverSignal(String name, {String value})— new annotation step class
influtter_probe_annotation. Emitsdeliver signal "name"or
deliver signal "name" "value".- Parser:
TOKEN_DELIVER,TOKEN_SIGNAL,VerbDeliverSignal. - Agent:
probe.signalJSON-RPC method,ProbeMethods.signalconstant.
v0.9.8
Fixed
- Biometric no-match on iOS 26+ simulator —
notifyutilno-match notifications
no longer resolveLAContext.evaluatePolicyon iOS 26 / Xcode 26.5. The CLI now
sends aprobe.biometric_signal {result: bool}JSON-RPC command to the agent
after firing platform-level notifications. Test apps callawaitBiometricResult()
fromflutter_probe_agentinstead oflocal_auth.authenticate()in PROBE_AGENT
builds; the agent resolves a DartCompleterwith the CLI-delivered result, making
biometric no-match reliable on all iOS simulator versions.
Added
awaitBiometricResult()— new public function influtter_probe_agent.
Returns aFuture<bool>that resolves when the CLI delivers
probe.biometric_signal. Usage pattern in test apps:final ok = const bool.fromEnvironment('PROBE_AGENT') ? await awaitBiometricResult() : await localAuth.authenticate(...);
probe.biometric_signal— new JSON-RPC method. Sent by the CLI after
the platform-level biometric simulation commands so the result is always
delivered regardless of simulator version behavior.
v0.9.7
Added
- Biometric authentication testing — three new ProbeScript steps that drive Face ID / Touch ID / fingerprint flows on iOS Simulator and Android emulator without real hardware. Skipped on physical devices with a warning (same pattern as
set locationand other simulator-only ops).enroll biometric— marks the simulator/emulator as having an enrolled face or finger. iOS posts thecom.apple.BiometricKit.enrollmentChangedDarwin notification viaxcrun simctl spawn booted notifyutil. Android requires the fingerprint to be pre-enrolled in Settings.biometric match— simulates a successful capture, satisfying any pending biometric prompt. iOS posts*_Sim.faceCapture.matchAND*_Sim.fingerTouch.matchso the same step works on Face ID and Touch ID devices. Android runsadb -s <serial> emu finger touch 1.biometric no match— simulates a failed capture so the app's "authentication failed" path can be tested. iOS posts the.no-matchvariants; Android runsadb emu finger touch 9999(an unregistered id).- Annotation DSL: matching
EnrollBiometric(),BiometricMatch(),BiometricNoMatch()const Step classes influtter_probe_annotation, with a newbiometric_authgolden fixture influtter_probe_gen/test/fixtures/that round-trips through the Go parser via the cross-language integration test. - Parser: 2 new tokens (
TOKEN_BIOMETRIC,TOKEN_ENROLL), 3 newActionVerbconstants, 2 new parser dispatch cases. 3 new unit tests inparser_test.go. - Runner:
EnrollBiometric/BiometricMatch/BiometricNoMatchmethods onDeviceContext, dispatch cases inExecutor.runAction, and human-readable strings instepDescription. - Docs: new section in annotations.md and syntax.md on the website. Per-package CHANGELOGs updated.
v0.9.6
Fixed
flutter_probe_gen:Mockpath silently truncated. The emitter wrote the path unquoted (when the app calls GET /api/products), so the Go lexer split on/and the parser only recorded the first IDENT segment. Now emits the canonical quoted form. Caught by a newmock_and_callgolden + the existing cross-language integration test.flutter_probe_gen:Seesuffixes silently dropped. Whenstate,containing, andmatchingwere all set on a single assertion, only the last branch's text reached the output. Now composes all three suffixes additively:see "x" is enabled contains "y" matching "z". Caught by a newsee_statesgolden covering the matrix.
Added
flutter_probe_annotation:@ProbeCompositeTestannotation. The flagship multi-device composite testing feature finally has a DSL surface. Pair withDevice(alias, target: …),OnDevice(alias, steps: […])per-device groups, andSync(label)cross-device barriers. Emitter generates standardcomposite test/devices/<alias>:/syncblocks that the existing CLI runner picks up unchanged.flutter_probe_annotation:See.id/See.selectorfactories — assertions can now target byValueKeyor any rich selector (Ordinal, Below/Above/LeftOf/RightOf, InContainer, TypeSel) rather than only by literal visible text. Same factories onDontSee. The Go parser always supported this; the DSL just didn't expose it.flutter_probe_annotation:WaitUntil.idAppears/.idDisappears— emits unquoted#keyselector form (Go parser's WaitSelector branch), which is more reliable than text matching for stableValueKey-tagged widgets.flutter_probe_gen: 6 new golden fixtures.mock_and_call,see_states,composite_chat,wait_variants,examples_inline,kitchen_sink. The kitchen sink fixture exercises one of every step, selector kind, and control-flow construct. Every fixture round-trips throughinternal/parser/golden_integration_test.go. Total golden coverage went from 4 → 10 fixtures, builder tests from 5 → 11.
Changed
flutter_probe_annotation:PressandPinchare now@Deprecated. The Go parser has nopressorpinchcase, so emitted text fell through toparseRecipeCalland was misinterpreted. Marked deprecated until runtime support lands. UseGoBack()in place ofPress('back').flutter_probe_gen: emitter no longer coupled to enum declaration order._direction,_httpMethod, and theSeestate lookup now read the enum constant identifier (_namefield) instead of indexing a hard-coded array by.index. ReorderingDirection,HttpMethod, orSeeStateno longer silently corrupts emitted ProbeScript.
Docs
- New website page: Annotation-driven Tests — full reference for the annotation DSL with every step class, selector kind, and the new composite test syntax.
v0.9.5
Fixed
- Dart agent: iOS / Impeller screenshots —
take_screenshotpreviously calledOffsetLayer.toImage()on the root render view. On iOS with the Impeller renderer (Flutter's default on iOS 17+), that returns a GPU-backed texture whosetoByteData(ImageByteFormat.png)isnull, so capture silently produced nothing. The agent now primarily captures via the largest visibleRenderRepaintBoundaryin the widget tree (Impeller-supported); the legacyOffsetLayerpath is only used as a fallback when no boundary is found (Skia). Also awaitsWidgetsBinding.instance.endOfFramebefore capture so the latest frame is always in the image, and uses the actualView.devicePixelRatiorather than a hard-coded2.0.