DeviceLab driver: settle via agent, text matching fixes, runFlow timeout#55
Merged
DeviceLab driver: settle via agent, text matching fixes, runFlow timeout#55
Conversation
The Condition.Script yaml tag was "scriptCondition" (Maestro's internal
field name) instead of "true" (the actual YAML key users write). This
caused when: { true: ... } conditions to be silently ignored, making
them always evaluate to true.
Reported-by: Satyajit Sahoo <satyajit.happy@gmail.com> (@satya164)
Add case-insensitive textMatches/descriptionMatches as fallback after
the existing case-sensitive textContains/descriptionContains. Existing
behavior preserved — exact case match is tried first, case-insensitive
only kicks in when exact case fails.
Fixes Android dialog buttons where textAllCaps displays "CANCEL"
but the view hierarchy text is "Cancel".
TODO: DeviceLab driver agent (devicelab-android-driver) needs fixes:
1. findByUiAutomator selector parsing order (textContains/textMatches
checked before text to avoid prefix collision)
2. Support .clickable(true) filter in UiSelector parsing
Agent fix is in devicelab-android-driver repo but needs verification
that the APK is reinstalled on device (may be cached).
Reported-by: Satyajit Sahoo <satyajit.happy@gmail.com> (@satya164)
Directional swipes without a selector now use screen coordinates matching Maestro's behavior, instead of searching for a scrollable element first. The scrollable detection was causing LEFT/RIGHT swipes to fail when a vertical scrollable container was found. Reported-by: Satyajit Sahoo <satyajit.happy@gmail.com> (@satya164)
Uses AccessibilityEvent listeners (WINDOW_CONTENT_CHANGED, WINDOW_STATE_CHANGED, VIEW_SCROLLED) to detect when UI has settled. Waits until no events fire for a quiet period (default 300ms), with a configurable timeout (default 2000ms). This is more reliable than hierarchy comparison for fast drivers where the hierarchy fetch can complete before the UI starts changing.
Settle before steps that send keystrokes without implicit idle wait. These steps fire immediately — if a screen transition is in progress from a preceding tap, the input may arrive before the field has focus. Uses DeviceLab's native event-based settle (AccessibilityEvent listener) when available, falls back to hierarchy comparison for other drivers. Skipped when waitForIdleTimeout is 0 (browser flows).
settleAfterAction now pierces driver wrappers via core.Unwrap so the WaitForSettle type-assertion succeeds for DeviceLab and delegates to the on-device agent's tree-comparison settle, which is faster and more accurate than the hierarchy-fetch fallback. Also removes the pre-input settle in flow_runner — settle is now expected to run after tap/action steps, not before input steps, so the fallback was firing twice and slowing flows.
Both Android drivers previously tried findFocused/ActiveElement and then fell back to a focused=true selector search when no selector was provided. Now we just send key events directly via SendKeyActions, matching Maestro's "type into whatever the OS has focused" behavior. Removes a round-trip and a fragile fallback path.
The case-insensitive textMatches fallback added in e7cdde4 works on UIAutomator2 but breaks on the DeviceLab agent, whose findByUiAutomator parser ignores chained filters like .clickable(true). With the filter ignored, a textMatches regex that incidentally hit a non-clickable wrapper/label returned that element instead of the intended button, causing taps to miss. UIAutomator2 and Appium paths are untouched — their case-insensitive fixes continue to work. DeviceLab loses the Android dialog "CANCEL" vs "Cancel" fix until the agent parser is taught to honor .clickable(true); that follow-up will re-add these strategies.
Ships the findByUiAutomator fix so chained .clickable(true)/.enabled filters on UiSelector are honored instead of silently ignored. Unblocks re-adding case-insensitive text matching for DeviceLab.
Now that the agent's findByUiAutomator honors .clickable(true), the case-insensitive textMatches/descriptionMatches fallback can be re-enabled on the DeviceLab path without picking up non-clickable wrappers. Restores the Android dialog "CANCEL" vs "Cancel" fix (Login/login/LOGIN/lOgIn all match as contains). This re-applies the DeviceLab half of e7cdde4 that was reverted in b1c228a while the agent parser was broken.
The text-tap fast path used buildSelectors alone, which passes preferClickable=false, so .clickable(true) selectors were never sent to the agent. That meant the agent's ancestor-promotion logic could not activate — text taps fell through to the first non-clickable matching node (e.g. a subtitle TextView) and hit the wrong element. Prepend buildClickableOnlyStrategies so clickable variants are tried first. Combined with the agent's clickable-ancestor promotion, "tapOn: SIGN IN" now hits the clickable login-button ViewGroup by walking up from the "Sign In" TextView child inside it.
Adds hintContains and hintMatches strategies (case-sensitive and case-insensitive) to buildClickableOnlyStrategies and buildSelectorsWithOptions, alongside the existing text and description chains. hintContains/hintMatches are DeviceLab-agent extensions (not native UiSelector predicates) that match against AccessibilityNodeInfo.getHintText(), so "tapOn: 'Email'" can find an empty EditText by its android:hint placeholder. Closes a parity gap with Maestro, whose text filter unions matches across text, accessibility text, and hint text.
Ships the agent changes from devicelab-android-driver@bc76ac9: clickable-ancestor promotion on .clickable(true) selectors, and hintContains/hintMatches predicate support.
Add timeout parameter to runFlow steps that propagates through the
driver interface via SetContext, enabling mid-operation cancellation
of element-finding polling loops when the timeout expires.
Example usage:
- runFlow:
file: common/login.yaml
timeout: 5000 # 5 second timeout
env:
username: devicelab
- runFlow:
timeout: 3000 # inline steps with timeout
commands:
- tapOn: "Submit"
- assertVisible: "Success"
- Add SetContext(ctx) to Driver interface and all implementations
(uiautomator2, wda, appium, devicelab, flutter, mock)
- Propagate timeout context into driver polling loops so element
finding is interrupted immediately on expiry
- Fix parser to include timeout field in runFlow raw struct
- Replace cryptic "context deadline exceeded" errors with informative
messages showing timeout value, flow file, and executing step
- Add enrichTimeoutError for sub-step error enrichment
- Timeout failures are classified as "TIMEOUT" error type in reports
(not "UNKNOWN"), enabling proper failure categorization in HTML,
JUnit, and Allure reports
- Add 4 executor tests and 2 parser tests for timeout behavior
Ref #29
Thanks to @maraujop for the suggestion!
Previously Provider only exposed ExtractMeta (post-Appium-session) and ReportResult (post-run, post-reports). There was no way for a cloud integration to observe the run as it progressed — test cases only appeared in the dashboard after the entire run finished. Adds three new lifecycle methods on Provider: OnRunStart(meta, totalFlows) — once, before first flow OnFlowStart(meta, flowIdx, totalFlows, name, file) — before each flow OnFlowEnd(meta, *FlowResult) — after each flow Errors from the new hooks log at Warn and do not abort the run, matching ReportResult's behavior. Sauce Labs and the example-provider template ship no-op implementations (Sauce's dashboard renders per-test results from the final payload, so there is no per-flow API to ping; future providers that do have live per-case endpoints can override). Wiring in pkg/cli/test.go: - notifyCloudRunStart helper fires OnRunStart once per run, right before runner.Run(), across all three execution paths (executeSingleDevice, ExecuteFlowWithDriver, single-Appium) - onFlowStartWithCloud and onFlowEndWithCloud closures wrap the existing console progress callbacks to also invoke OnFlowStart/End Test coverage: TestProvider_Lifecycle_FiresInOrder asserts the full ExtractMeta → OnRunStart → OnFlowStart → OnFlowEnd → ReportResult sequence with per-flow ordering.
Reported by @ptmkenny in #54 — thank you for the clear repro. The previous installIOSApp() called zipconduit.SendFile() unconditionally on real devices, with no timeout. On iOS 17+ (and particularly iOS 26) the install service sometimes accepts the USB-mux connection but never acks completion, leaving SendFile blocked forever — the reporter saw maestro-runner sit on "Installing app..." with no progress and no way to recover. Fix: prefer Apple's modern CoreDevice installer on real devices: xcrun devicectl device install app --device <UDID> <app-path> Same command the reporter confirmed works on their iPhone 13 / iOS 26. Fallback chain for real devices: 1. xcrun devicectl device install app (if devicectl is on PATH) 2. go-ios zipconduit.SendFile (legacy, older macOS / Xcode) Both paths now run under a 3-minute context timeout so a stuck install surfaces as an error instead of an infinite spinner. Simulator install (xcrun simctl install) is also wrapped in the same timeout. Escape hatch: MAESTRO_RUNNER_IOS_INSTALLER=zipconduit — force the legacy path MAESTRO_RUNNER_IOS_INSTALLER=devicectl — force devicectl, no fallback Verified on iPhone 12 mini running iOS 26.0: devicectl install completes in seconds, replacing the previous hang. Fixes #54
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up PR after #53 (now merged). Consolidates 16 commits covering:
DeviceLab agent + driver — settle and text matching
inputText/eraseTextso key events don't fire mid-transition..clickable(true)text selectors: walks up from a non-clickable text match (e.g. a "Sign In" TextView child) to the nearest clickable ancestor (e.g. login-button ViewGroup).textAllCapsdisplays uppercase but hierarchy text is mixed case).hintContains/hintMatches): match an EditText'sandroid:hintplaceholder — closes a parity gap with Maestro..clickable(true)selectors actually get sent to the agent.SendKeyActions(drops the findFocused/ActiveElement fallback) — matches Maestro's "type into whatever the OS has focused" behavior.runFlow
timeout:parameter on runFlow steps with context propagation into all drivers; mid-operation cancellation of polling loops; informative timeout errors categorized asTIMEOUT.Misc fixes
LEFT/RIGHTon Android: use screen coordinates directly.when: true:expression not parsed (evaluated truthy on wrong platforms).${VAR || "default"},${VAR ?? "fallback"}) now resolved.APK
devicelab-android-driver.apkrefreshes, matching the on-device agent source changes (tracked insupport-tools/devicelab-android-driver).Test plan
text-case-insensitive.yaml) passes 11/11 on Pixel 4a (Android 13, DeviceLab driver).auth/) passes 4/4 on same device — no regressions.go build ./...andgo test ./pkg/executor/... ./pkg/flow/...green.Related