Releases: devicelab-dev/maestro-runner
v1.1.15
Release 1.1.15
A broad reliability + ergonomics release driven mostly by real-user reports across iOS, Android, Flutter and web. Highlights: assertVisible now recognises React Native container testIDs on iOS, Android scroll is rewired to adb input swipe for cross-skin reliability (OneUI in particular), waitForAnimationToEnd actually polls instead of returning 0 ms, web tap is gated by a Playwright-style actionability check, and browser console errors auto-surface in the flow report.
Added
- Web actionability gate before
tapOn/doubleTapOn/longPressOn/inputText— Playwright-style auto-wait MVP. After find-element and before dispatch, the runner now waits up to 2 s polling at 50 ms for the element to be enabled in three orthogonal senses:HTMLElement.disabled !== true,aria-disabled !== "true", andpointer-events !== "none". (Visibility is enforced upstream by the finder cascade — see thefindByAXTreenotes below.) Catches the common "looks tappable, isn't yet" flakes in modals, multi-step forms, and submit buttons that flip enabled onchange. Stable-bounding-box polling is the next slice. When the gate times out, the error message now reports the specific rejection reason (e.g.last rejection: pointer-events-none). - Web: browser console errors + uncaught JS exceptions auto-surface in the flow report — the CDP driver was already capturing
console.log/warn/error/infoandRuntime.exceptionThrownevents, but they were only visible if the flow explicitly calledgetConsoleLogs/assertNoJSErrors. Now every flow gets a collapsed "Browser console" section inreport.html(and aconsoleLogsarray in the per-flow JSON) with counts, colour coding, and full entries. Mobile drivers are unaffected. - Web:
failOnConsoleErrorflow config — opt-in stricter mode that fails the flow when any captured console error (or uncaught exception) fires during the run. Off by default. --user-data-dirflag for persistent Chrome profile (MAESTRO_USER_DATA_DIR) — reuse cookies, localStorage, sessionStorage, and installed extensions across runs. Speeds up auth-heavy CI suites (log in once, reuse across flows) and supports flows that depend on installed extensions. Default unset → existing ephemeral-profile behaviour.--env-fileflag for.env-style environment loading — loadsKEY=VALUEpairs (with single/double quoting,#comments, blank-line skipping) into the flow runtime. Slots between workspaceEnv:block and-e CLIoverrides, so precedence is workspace < env-file <-e. Lets CI keep secrets out of flow YAML.--driver-start-timeout <seconds>flag (MAESTRO_DRIVER_START_TIMEOUT) — overrides the 30 s hard-coded driver-start timeout for UIA2 / DeviceLab Android drivers. AWS Device Farm low-end Samsung devices take ~60–80 s for cold-path APK install + dex2oat + JVM warmup; the runner force-stopped them at +30 s every time. Default 0 keeps the existing 30 s behaviour. Reported by @pk1m (#76).runFlowwithwhen:gets anelse:branch (parity fix) — three interchangeable YAML shapes (else as file, else as inlinecommands:, else inheriting parent file). Cleans up branching auth setups (run sign-in if not logged in, otherwise run the signed-in path) without a second top-level conditional.tapOn/longPressOn/tapOnPointacceptduration:(ms) (parity fix) — routes through each driver's long-press path.tapOn.longPress: truenow works on UIA2 / DeviceLab / Appium too (was WDA-only), defaulting to 1000 ms.longPressOn.durationis also configurable (was hardcoded 1 s).openNotificationsstep (Android) (parity fix) — pulls down the notification shade viacmd statusbar expand-notifications. Dispatched by UIA2 + DeviceLab; no-op on iOS.removeMediastep (Android) (parity fix) — clears the MediaStore index for deterministic test setup. Symmetric withaddMedia. Tries the modular provider first, falls back to legacy.scrollUntilVisible.directionandsetAirplaneMode.enabledsupport${VAR}interpolation (parity fix) — values resolve at execute time, so the same flow YAML works across environments.- Pre-flight warning when
--app-filelooks like a Flutter debug build — scans the.appbundle forFrameworks/App.framework/flutter_assets/kernel_blob.bin(the Dart kernel snapshot, present in debug, absent in release/profile AOT). Prints a yellow startup warning pointing atflutter build ios --release/--profile. Advisory only — unusual setups with a liveflutter rundaemon reachable from the test host can still succeed. - WDA crash-loop circuit breaker — when the same client connects + dies repeatedly with no productive request in between, the runner now bails with a clear error instead of letting the retry storm fill the logs. Drove this through a real iPad Flutter crash that previously surfaced as silent log flooding. Reported by @divan (#38).
Fixed
-
iOS
assertVisiblebyidfor React Native container testIDs (parity fix) —assertVisible: { id: ... }failed against<View testID="…">containers on both iOS simulator and real device. WDA's page-source filter rejected any element XCUITest reports asvisible="false", including RN wrapper views that have no own visual content but host visible children. Maestro CLI never consults that attribute, so the same flow worked on CLI. Added a phased visibility check: prefervisible="true"matches; fall back tovisible="false"candidates only when they host at least one visible descendant — recovers RN container testIDs while still rejecting hidden-but-still-mounted screens. When the rescue path matches, the step result recordsmatchNoteinreport.jsonand the step message becomesElement is visible (matched via visible descendant …). Reported by @AlonG-Papaya (#80). -
Android
scroll/scrollUntilVisibleon Samsung OneUI (parity fix) — three compounding bugs causedscrollUntilVisibleto either short-circuit without scrolling or reportElement not found after 20 scrollswhile the viewport never moved:scrollUntilVisibledeclared success when the target only existed in the off-screen portion of the view hierarchy. Now verifies the matched element actually overlaps the viewport.- Both Android drivers routed scroll through gesture APIs that silently no-op on several Android skins (
/appium/gestures/scrollon OneUI for theuiautomator2driver; the on-device agent's MotionEvent injection with zero-ms duration and inverted direction for thedevicelabdriver). The default scroll backend is nowadb input swipefor both drivers — the same OS-level path you'd get from anadb shell input swipecall by hand. The agent itself was also corrected —scrollnow uses scroll semantics (direction = what gets revealed),swipekeeps touch semantics (direction = finger motion), andspeed <= 0is clamped to 300 ms. Bundled APK rebuilt. - Infrastructure errors during element lookup (dead session, connection refused) were silently counted as "not found yet" and made failures surface as
Element not found after 20 scrolls. Real errors now propagate immediately.
The old gesture path is still available per step for users who need it:
- scrollUntilVisible: element: { id: "give feedback" } direction: DOWN engine: agent # opt out of the default adb swipe
Verified on a Samsung Galaxy M16 (OneUI, Android 14). Reported by @George-Anton-Tarazi (#81), with prior investigation in #28 by @maggialejandro.
-
waitForAnimationToEndactually waits (parity fix) — the UIA2 / DeviceLab / WDA implementations were stubs that returned success in 0 ms (and logged "WARNING: not fully implemented"), making the step a no-op gate. The configuredtimeout:field was parsed but discarded. The step now polls two consecutive screenshots, computes the fraction of differing pixels, returns success once ≤ 0.5 % differ (i.e. screen is static), and respectstimeout:everywhere (default 15 s). On timeout it soft-returns success so a never-settling animation doesn't block the surrounding flow. Web CDP path now honours the user-supplied timeout instead of a hardcoded 10 s. -
Silent wrong-element tap for lazy ListView items on Android Flutter (parity fix) —
tapOn: { id: "X" }against an item in aListView's cache-extent buffer (laid out but not in the visible viewport) silently dispatched a coordinate tap at the cache item's bounds, which often fell inside the status / nav-bar safe area on top of an unrelated widget. Tests "passed" against the wrong target. The Flutter VM service path now rejects taps whose target lies in the top 3 % status bar or bottom 5 % nav / gesture area (or fully off-screen) and returns a clear error pointing atscrollUntilVisibleas the fix. -
Duplicate console events in per-flow report — when
cfg.URLwas set, the CDP driver pre-navigated to that URL during construction, so console events from that load fired before the user's flow started. The flow's firstlaunchAppre-navigated to the same URL and fired the same events again, producing duplicates (8 entries for 4 distinct events in the verified repro). The runner now resets the console buffer at flow start; mobile / native drivers that don't implement the reset interface are unaffected. -
Web
tapOnresolving to non-Element nodes (#text,<title>) — on SPAs that put route labels intodocument.title(e.g. saucedemo, demoblaze...
v1.1.14
Release 1.1.14
This release closes out the Flutter Web testing story. v1.1.13 fixed the finding layer (selectors traverse same-origin iframes, index is a first-class web selector). v1.1.14 completes it: selectors also pierce open shadow roots, tapOn dispatches at correct top-frame viewport coordinates when the target lives inside an iframe (with hit-target verification), the same path extends to doubleTapOn / longPressOn / scrollUntilVisible, visibility checks intersect iframe content viewports, and tapOn handles Flutter Web's <flutter-view> pointer-router glass pane that consumes trusted events before any third-party listener can observe them. A real Flutter Web user — @richjun — drove most of this with two substantial PRs (#73, #74) and two issue reports (#71, #72).
Added
- Selectors pierce open shadow roots on web —
text/ CSS /id/ attribute /
role finders, plus the visibility and wait helpers, now recurse through
every same-origin<iframe>and every openshadowRootreachable from
them. Flutter Web mounts its accessibility tree inside an open shadow root
attached to<flt-glass-pane>, sotapOn: "Close"against a Flutter Web
semantics node now resolves to the actual element. Closed shadow roots
remain unreachable (same constraint every WebDriver-class tool has — no
fix possible without privileged access). Reported by
@richjun (#71). tapOn text + indexenumerates across iframes / shadow roots —
completes the #67 fix from 1.1.13.
Previously the resolver enumerated matches only within the top frame, so
asking for index 1 when matches 0..N-1 lived in the top frame and the
real target lived in an iframe silently re-tapped the in-range top-frame
match — green test, wrong button. Now walks every same-origin root via
_collectRoots(), sorts by document order, and indexes deterministically.
Out-of-range returns a precise error with the actual match count instead
of falling back. Reported by @richjun
(#72).tapOndispatches at top-frame coordinates for iframe-nested targets —
Rod'sElement.Click()used iframe-LOCAL viewport coordinates from
getBoundingClientRect(); CDPInput.dispatchMouseEventoperates in
TOP-FRAME viewport coordinates. The click landed at the wrong place and
tapOnreported success silently. Now ports Playwright's
_checkFrameIsHitTargetwalk: from the target outward, adds each
ancestor<iframe>element's box plus its content-area inset (border +
padding) to convert iframe-local → top-frame viewport coordinates.
Hit-target verification runs as both static pre-flight (rejects
occluded / wrong-element clicks before dispatch) and post-click trusted-
event capture (verifies the click landed on the target's frame tree).
Contributed by @richjun in
#73.doubleTapOn/longPressOn/scrollUntilVisibleinherit the
iframe-coord-translated path — same root cause astapOnhad. Now
routed through a shareddispatchCrossRoothelper.scrollUntilVisible
for iframe-nested targets calls nativeElement.scrollIntoView()inside
the element's own document (the previous page-levelMouse.Scrollonly
scrolled the outer document and never reached iframe content).- Visibility check intersects iframe content viewport —
_isElementVisibleused to do intrinsic-only checks (computed style +
getBoundingClientRect()dimensions) and reported elements scrolled or
clipped outside their iframe's content viewport as "visible." This made
assertVisible/waitForVisible/extendedWaitUntilsilently pass
on iframe-clipped elements, and madescrollUntilVisible's loop exit
on iteration 0 (the newscrollIntoViewbranch was unreachable in
practice). Now walks the iframe ancestor chain at each level,
intersecting with the iframe's content viewport. Empty intersection
returns false; surviving rect is translated to parent coordinates and
rechecked. Top-frame "below the fold" elements stay visible — only
iframe clipping is added. tapOninto Flutter Web semantics — three orthogonal fixes for
Flutter Web targets.findBySearchnow rejects non-tappable text
containers (<script>/<style>/<template>/ etc.) because CDP
DOM.performSearchmatches against serialized HTML and Flutter Web
pages whose JS source contains the button label as a string literal
silently returned the<script>element. The hit-target pre-flight
and post-click verifier both accept the Flutter<flutter-view>glass-
pane occlusion case (target + topmost hit both inside<flutter-view>);
Flutter intercepts trusted pointer events at the document/glass-pane
capture layer and routes them through its own internal pointer router
for semantics dispatch, so the verifier's one-shot listener never fires
and a strict same-element walk-up always reports false occlusion. Non-
Flutter occlusion (overlay div, modal, genuine z-stack) continues to
fail-fast — the Occluded and Transformed regression tests still reject.
Contributed by @richjun in
#74.
Fixed
runScriptper-call scope + persistentoutputmutations — two
related bugs. (a) top-levelconst/let/functiondeclarations
collided acrossrunScriptcalls because the JS engine reused a single
Goja runtime's global scope, surfacing as
SyntaxError: Identifier 'word' has already been declaredon the second
invocation. EachrunScriptnow executes inside an IIFE so top-level
declarations are function-scoped to that invocation. (b) Mutations like
output.list.push(x)did not persist acrossrunScriptcalls because
theoutputproxy returned a snapshot Go map per call — only whole-
value reassignment (output.list = [...]) survived. Theoutputbag
is now a Goja-nativeObjectshared across invocations so mutations
persist. Reported by @Sina-KH
(#70).- iOS
openLinkon simulator —POST /session/<sid>/urlon
WebDriverAgent v12+ returnsUnhandled endpoint: /url. Users who ran
maestro-runner wda updateand got the newer WDA hit a hard failure
on everyopenLinkstep, blocking Expo dev client flows where deep
linking loads the JS bundle from Metro. Bypassed entirely on
simulators by shelling out toxcrun simctl openurl <udid> <url>—
same primitive Maestro CLI uses, faster, no WDA version coupling.
Real iOS devices keep the existing WDA/urlpath (simctlcan't
reach them). Reported by @jongbelegen
(#68). - iOS
clearStateon simulator no longer requires--app-file—
the runner needs to uninstall + reinstall the app to wipe its data
container (Apple doesn't expose a "clear data only" API). Previously
failed with eitherclearState on iOS requires --app-file(no
--app-file) orlstat ... No such file or directory(if
--app-filepointed inside the live sim container, which the
uninstall deleted before install could read it). Now auto-discovers
the installed.appviaxcrun simctl get_app_containerand copies
it to a temp directory before the uninstall — same approach Maestro
CLI uses (LocalSimulatorUtils.kt#reinstallApp). Reported by
@jongbelegen
(#69).
Contributors
- Reported selectors not piercing shadow DOM (#71)
- Reported
tapOn text+indexnot spanning iframes (#72) - Contributed iframe + shadow-root coord-translated
tapOnwith hit-target verification (#73) - Contributed Flutter Web semantics support — finder rejection, pre-flight and post-click glass-pane concession (#74)
- Reported
runScripttop-level declaration collisions and non-persistentoutputmutations (#70)
- Reported iOS
openLinkfailing on simulator after WDA upgrade (#68) - Reported iOS
clearStateon simulator failing without / with--app-file(#69)
Installation
Quick Install
curl -fsSL https://open.devicelab.dev/install/maestro-runner | bashInstall a specific version
curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash -s -- --version 1.1.14Manual install (from the assets below)
# 1. Download the tarball for your platform.
# 2. Extract:
tar -xzf maestro-runner-1.1.14-darwin-arm64.tar.gz
# 3. Run setup (installs into ~/.maestro-runner/bin/):
cd maestro-runner && ./setup.shVerify Installation
maestro-runner --versionPlatform Support
- macOS Intel (amd64) - Signed & Notarized
- macOS Apple Silicon (...
v1.1.13
Added
- Same-origin iframe traversal on web — text/CSS/ID/attribute selectors now
walk into same-origin<iframe>content (e.g. Flutter Web embedded under a
host page). Cross-origin / OOPIF iframes are still skipped, but the
not-found error now surfaces a clear(skipped N cross-origin iframes — full OOPIF support not implemented yet)hint so users debugging a missing
selector can tell the cause is frame isolation, not a typo. Reported by
@richjun (#65). - Mobile-style
indexselector on web —tapOn: { text: "Help", index: 1 }
now picks the second match instead of being silently dropped as
unsupported. The web finder accepts bothindex(string, mobile-style) and
nth(int) via a singleEffectiveNth()helper, so the same flow YAML
works across Android, iOS, and web. Reported by
@richjun (#67). - Sauce Labs job context per flow — the runner now posts
sauce:contextto Sauce on every flow start so jobs surface the YAML
basename in the Sauce UI, and renames empty / "Default Appium Test" jobs
on completion using the first flow's filename. Real-device caps without
appium:jobUuidfall back to VMS + session id so REST status updates
still target the right job. Contributed by
@eyaly (#66).
Fixed
onFlowStarthook with defaultappId—launchApp(and other app
lifecycle steps) insideonFlowStart/onFlowCompletenow resolve the
flow's defaultappIdthe same way as top-level steps. Previously the
hook ran with an emptyAppID, causing a silent no-op on Android. Fixes
#62, reported
by @zcsteele.copyTextFromon Appium 3.x — stop pushing the captured text to the
device clipboard viaPOST /appium/device/set_clipboard, which Appium 3
returns 404 for. The runner already keeps the value in memory (matching
Maestro's design) sopasteTextcontinues to work. Fixes
#61, reported
by @kavithamahesh.- iOS permission dialogs blocking real-device flows — WDA's alerts
monitor only registers whendefaultAlertActionis in the session-creation
capabilities; the runner now defaults toacceptso notification (and
other) permission dialogs auto-dismiss out of the box. Fixes
#64, reported
by @j-ezeh. - assertVisible silently wrong for state filters / nth / role — the JS
fast path bypassed several capabilities the Go finder already implemented,
so selectors withenabled/checked/focused/nth/role/
ID-cascade hit the fast path and produced wrong answers. Centralised
routing now sends those selectors to the Go finder; the JS path'sid
case also runs the samedata-testid/name/aria-labelcascade.
Contributors
- Reported same-origin iframe selector failures with Flutter Web (#65)
- Reported
indexselector being silently dropped on web (#67)
- Reported
onFlowStarthook unable to reference defaultappId(#62)
- Reported
copyTextFromfailing on Appium 3.x with 404 (#61)
- Reported iOS permission dialogs not auto-accepted on real devices (#64)
- Improved Sauce Labs job naming + per-flow context (#66)
Installation
curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash -s -- --version 1.1.13Platform Support
- macOS Intel (amd64) — Signed & Notarized
- macOS Apple Silicon (arm64) — Signed & Notarized
- Linux amd64
- Linux arm64
Built by DeviceLab.dev
v1.1.12
Added
- Tap options —
repeat,delay,retryTapIfNoChange, andwaitToSettleTimeoutMsnow
honored during execution on all drivers (uiautomator2, wda, devicelab, appium, cdp).
Implemented at the executor layer, zero driver-side changes.
(#52, #53)- tapOn: id: "login-button" repeat: 3 delay: 500 retryTapIfNoChange: true waitToSettleTimeoutMs: 2000
- runFlow timeout —
timeout:parameter onrunFlowsteps with context propagation
into driver polling loops. Element-finding cancels immediately on expiry, and failures
are classified asTIMEOUTin reports. Ref
#29, thanks to
@maraujop for the suggestion.- runFlow: file: common/login.yaml timeout: 5000 env: username: devicelab
- Cloud Provider lifecycle hooks —
Providerinterface now exposesOnRunStart,
OnFlowStart, andOnFlowEndalongside the existingExtractMetaandReportResult.
Cloud integrations can update dashboards live per-flow instead of only at run end.
Sauce Labs ships with no-op placeholders for the new hooks. - UI.waitForSettle RPC — on-device tree-comparison settle detection on the DeviceLab
Android driver, used as an auto-settle beforeinputText/eraseTextto avoid key
events firing mid-transition. - Clickable-ancestor promotion — when a DeviceLab tap matches text on a non-clickable
descendant (e.g."Sign In"TextView inside a clickable login-buttonViewGroup), the
agent now walks up to the nearest clickable ancestor. - hintText matching —
hintContains/hintMatchesUiSelector extensions on the
DeviceLab driver match anEditText'sandroid:hintplaceholder. Lets
tapOn: "Email"find an empty email field by its hint. - Case-insensitive text matching on Android —
textContains/descriptionContains
now fall back to case-insensitive match when case-sensitive fails, fixing Android dialog
buttons wheretextAllCapsdisplays"CANCEL"but the view hierarchy text is
"Cancel". Reported by @satya164. - Appium parallel execution — run flows across N Appium sessions concurrently. Each
session connects to the same Appium URL; the server allocates devices.
(#47) --wda-bundle-idflag — custom WebDriverAgent bundle identifier for signing
scenarios where the default bundle id isn't usable.
(#48)- Device info in Appium reports — device info and session ID now surface in console
output and JUnit/Allure reports for Appium runs.
Changed
- Simpler
inputTextwithout selector — DeviceLab and UIAutomator2 drivers now send
key events directly viaSendKeyActionsinstead of attempting
findFocused/ActiveElementfallbacks. Matches Maestro's "type into whatever the OS
has focused" behavior. - Updated DeviceLab Android driver APK to ship
UI.waitForSettle, clickable-ancestor
promotion, and hintText predicate support. - Appium parallel session count is capped at the number of flows (prints a warning
when parallel count exceeds flow count).
Fixed
- iOS install hang on iOS 17+ / iOS 26 — prefer
xcrun devicectl device install app
over the legacygo-ioszipconduit path on real devices. Both paths now run under a
3-minute context timeout so a stuck install surfaces as an error instead of an infinite
spinner. Escape hatch viaMAESTRO_RUNNER_IOS_INSTALLER=zipconduit|devicectl. Fixes
#54, thanks to
@ptmkenny for the clear repro. clearKeychainon iOS — standaloneclearKeychainstep and
launchApp { clearKeychain: true }both now work. Previously the step erred with
Step type '*flow.ClearKeychainStep' is not supported on iOS, and thelaunchApp
flag was a silent no-op (users stayed logged in). On simulators runs
xcrun simctl keychain <udid> reset; on real devices returns a clear unsupported
message pointing toclearStateas the alternative. Fixes
#57, thanks to
@ross-aker for reporting.- Swipe
LEFT/RIGHTon Android — use screen coordinates directly instead of the
previous element-relative computation that misbehaved. when: { true: <expr> }silently always-true — thetrue:field wasn't parsed
(YAML tag bound to the internalscriptConditionname instead), so conditions were
ignored and commands always ran. Fixes
#60, reported by
@satya164 and
@kavithamahesh.- Env var default syntax —
${VAR || "default"}and${VAR ?? "fallback"}now
resolve correctly. Undefined JS variables auto-define asundefinedon
ReferenceError, matching Maestro's GraalJS Proxy behavior. Fixes
#49,
#50.
Contributors
- Reported the iOS install hang on iOS 17+/26 with a clear repro (#54)
- Reported
clearKeychainnot working on iOS Simulator (#57)
- Reported Android dialog
textAllCapscase mismatch (CANCELvsCancel) - Reported
when: { true: <expr> }parsing bug (duplicated by #60)
- Reported
when.truecondition ignored (#60)
- Suggested
runFlowtimeout (#29)
Installation
Quick Install
curl -fsSL https://open.devicelab.dev/install/maestro-runner | bashVersion-specific install
curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash -s -- --version 1.1.12Manual Download
Download the archive for your platform below, extract, and place bin/maestro-runner on your PATH.
Platform Support
- macOS Intel (amd64) — Signed & Notarized
- macOS Apple Silicon (arm64) — Signed & Notarized
- Linux amd64
- Linux arm64
Built by DeviceLab.dev
v1.1.1
Release v1.1.1
Added
- Cloud provider abstraction — automatic detection and result reporting for cloud device providers (Sauce Labs, BrowserStack, LambdaTest, etc.) when using the Appium driver. Test pass/fail status, flow results, and metadata are reported to the provider after the run completes. Based on @eyaly's Sauce Labs integration (#43, #45)
# Sauce Labs — automatically detected from the Appium URL maestro-runner --driver appium --appium-url "https://ondemand.us-west-1.saucelabs.com/wd/hub" \ --caps caps.json test flows/
- Source file path in FlowResult — each flow result now includes the path to the source YAML file, used by cloud providers and report consumers
Changed
- Updated DeviceLab Android driver APK with latest on-device agent
- Airplane mode commands now use
cmd connectivity airplane-mode enable/disable(Android 11+) instead of the legacysettings put global airplane_mode_onapproach
Fixed
- CDP
waitForPageReadycrash — replaced panickingMustWaitLoad()with error-handlingWaitLoad()in the browser CDP driver, preventing test run crashes on pages with deeply nested object references - Removed unused
freePort()function from DeviceLab WebView driver - Removed unused regex variables (
reLabel,reHint,reValue) from Flutter semantics parser - Tightened variable scope in Flutter widget tree parser
Contributors
- Implemented original Sauce Labs pass/fail reporting integration (#43), which formed the basis for the cloud provider abstraction in #45
Installation
Quick Install
curl -fsSL https://raw.githubusercontent.com/devicelab-dev/maestro-runner/main/install-download.sh | bashManual Download
Download the binary for your platform below, make it executable, and move to your PATH.
macOS:
chmod +x maestro-runner-darwin-arm64 # or darwin-amd64
mv maestro-runner-darwin-arm64 /usr/local/bin/maestro-runnerLinux:
chmod +x maestro-runner-linux-amd64 # or linux-arm64
sudo mv maestro-runner-linux-amd64 /usr/local/bin/maestro-runnerVerify Installation
maestro-runner --versionPlatform Support
- macOS Intel (amd64) - Signed & Notarized
- macOS Apple Silicon (arm64) - Signed & Notarized
- Linux amd64
- Linux arm64
Built by DeviceLab.dev
v1.1.0
What's New
WebView CDP Support for Android
The DeviceLab driver now connects to Android WebViews via Chrome DevTools Protocol. When a WebView is detected, maestro-runner automatically uses CDP for element finding and JavaScript execution — no configuration needed.
# Automatic — CDP kicks in when a WebView is visible
maestro-runner --driver devicelab test webview-flow.yaml# Your flow doesn't change — WebView elements are found via CDP transparently
- launchApp:
appId: com.example.app
clearState: true
- tapOn: "Open WebView"
- assertVisible: "Welcome" # Found via CDP inside the WebView
- tapOn:
id: "submit-button" # CDP element findingChrome Browser CDP on Android
The DeviceLab driver can now automate Chrome browser on real Android devices via CDP, enabling web testing directly on Android hardware.
New Commands: evalWebViewScript & runWebViewScript
Execute JavaScript inside a mobile WebView via CDP — the WebView equivalents of evalBrowserScript and runBrowserScript.
evalWebViewScript — inline JavaScript execution:
# Simple — returns document title
- evalWebViewScript: "return document.title"
# With output variable
- evalWebViewScript:
script: "return document.querySelector('#price').textContent"
output: price
# Use the result in assertions
- assertTrue: ${price == '$7.50'}runWebViewScript — execute a JavaScript file:
# Simple file execution
- runWebViewScript: scripts/extract-data.js
# With environment variables and output
- runWebViewScript:
file: scripts/validate-cart.js
env:
EXPECTED_TOTAL: "29.99"
output: validationResultNetwork Idle Detection & DOM Stability Waits
After navigations (in both browser and WebView contexts), maestro-runner now waits for network idle and DOM stability before proceeding. This reduces flakiness on pages with async loading — no more waitForAnimationToEnd hacks after navigation.
CDP Browser Improvements
- RAF-based visibility polling — element visibility checks now use
requestAnimationFrame-based polling, improving reliability for dynamically rendered content <select>option support —tapOnwith<option>elements correctly selects the option via JavaScript instead of attempting a click- JS click fallback — when a native click fails on a browser element, falls back to JavaScript
.click()for better reliability with overlapping elements
Changes
- Default WDA swipe duration changed from 300ms to 100ms for faster, more responsive swipe gestures on iOS
- JavaScript helper code extracted from Go string literals into dedicated embedded
.jsfiles for easier maintenance (#37)
Bug Fixes
- Swipe coordinates now match Maestro behavior across all drivers (UIAutomator2, DeviceLab, WDA, Appium) — previously, swipe start/end positions differed from Maestro's implementation
assertNotVisiblenow correctly polls for disappearance instead of polling for appearance — previously, the command would pass immediately if the element wasn't visible, without waiting for it to disappear after an action- Filter out-of-bounds elements from page source searches — elements with coordinates outside the visible screen bounds are now excluded, preventing false matches on off-screen elements (#39)
- Text node attribute error — fixed
TypeError: this.getAttribute is not a functionwhen browser CDP encounters text nodes (#35, #36) - iOS WDA session lifecycle — improved driver reliability with better session creation, cleanup, and error recovery
--team-idno longer required for auto-detected simulators — when a booted simulator is auto-detected,--team-idis automatically skipped# Before: required --team-id even when simulator is already booted # Now: just works maestro-runner --platform ios test flow.yaml
- Flutter reconnection — skip retries for non-Flutter apps instead of wasting time on connection attempts. Non-Flutter apps now pay zero retry cost
- WebView CDP forwarder — wired
SetWebViewForwarderin the DeviceLab driver, which was never connected — elements were previously found only via native UiAutomator accessibility tree - hideKeyboard reliability — on-device agent now uses
KEYCODE_ESCAPEfirst (keyboard-only, no navigation side-effects), falls back toKEYCODE_BACKif needed. Retries up to 3 times with keyboard visibility polling - In-WebView navigation — when visibility check fails during in-WebView page navigation (JS context destroyed), refreshes page reference and retries instead of skipping CDP entirely
- CDP text match filtering — text-based visibility checks (
text,textContains,textRegex) now filter to the deepest matching element, preventing false positives from ancestor elements whosetextContentincludes hidden children's text
Thanks
Thanks to everyone who reported issues and contributed code!
- @tmahesh — fixed text node attribute error in browser CDP (#36), refactored JS helpers into embedded files (#37)
- @mahesh-e27 — reported text node attribute bug in browser CDP (#35)
- @sircharleswatson — reported
assertVisiblepassing for off-screen text in browser (#39) - @satishs22 — reported
tapOntimeout issue on Android emulator (#25) - @chrisjin-swipe — reported
inputTextcharacter skipping on Android (#32)
Installation
curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash
# Install this specific version
curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash -s -- --version 1.1.0Documentation
Platform Support
- macOS Intel (amd64) — Signed & Notarized
- macOS Apple Silicon (arm64) — Signed & Notarized
- Linux amd64
- Linux arm64
Built by DeviceLab.dev
v1.0.9
What's New
-
Desktop browser testing — new
--platform webwith built-in CDP driver for Chrome/Chromium. Headless by default,--headedfor visible browser. Supports parallel browser executionmaestro-runner --platform web test flow.yaml maestro-runner --platform web --headed --browser chrome test flow.yaml maestro-runner --platform web test --parallel 3 flows/
-
Browser-specific commands —
evalBrowserScript,setCookies,getCookies,saveAuthState,loadAuthState,openTab,switchTab,closeTab,mockNetwork,blockNetwork,setNetworkConditions,waitForRequest,clearNetworkMocks,uploadFile,waitForDownload,grantPermissions,resetPermissions,getConsoleLogs,clearConsoleLogs,assertNoJSErrors,runBrowserScript -
Browser selectors —
cssandxpathselectors for web elements, in addition totextandid- tapOn: css: "button.submit" - inputText: id: "username" text: "hello"
-
--no-app-installflag — skip app installation even if--app-fileis providedmaestro-runner --no-app-install --app-file app.apk test flow.yaml -
--no-driver-installflag — skip driver installation (UIAutomator2, WDA, DeviceLab)maestro-runner --no-driver-install test flow.yaml
Bug Fixes
- iOS simulator no longer requires
--team-id— simulators don't need code signing, so the validation now only enforces--team-idfor real devices - Fixed banner rendering in CI — removed Unicode box-drawing characters and stripped OSC 8 hyperlink escape codes when output is piped
Install
curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash
# Install this specific version
curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash -s -- --version 1.0.9Documentation
v1.0.8
What's New
-
Flutter VM Service fallback — when the native driver can't find a Flutter element, automatically discovers the Dart VM Service and searches the semantics/widget trees in parallel. Works on Android and iOS simulators. Non-Flutter apps pay only one log read on first miss, then fully bypassed. Disable with
--no-flutter-fallback -
Flutter widget tree cross-reference — when semantics tree search fails, falls back to widget tree analysis (hint text, identifiers, suffix icons) and cross-references with semantics nodes for coordinates
-
DeviceLab Android driver — WebSocket-based on-device automation, ~2x faster than UIAutomator2
maestro-runner --driver devicelab --platform android test flow.yaml -
setAirplaneModeandtoggleAirplaneModefor iOS — automates the Settings app to toggle airplane mode on real devices- setAirplaneMode: enabled: true - setAirplaneMode: enabled - toggleAirplaneMode
-
maxTypingFrequencyfor iOS — configurable typing speed via--typing-frequencyflag (default: 30 keys/sec)maestro-runner --typing-frequency 15 test flow.yaml -
scrollUntilVisiblemaxScrolls and timeout — now fully wired for all 4 drivers (previously parsed but ignored)- scrollUntilVisible: element: text: "Sign Out" direction: "down" maxScrolls: 5 timeout: 10000
-
On-failure WebView detection with CDP-aware error enrichment
-
Regex pattern support for ID selectors across all drivers
- tapOn: id: "username-.*" - assertVisible: id: "(username|email)-input"
-
repeatwithwhilecondition now loops correctly instead of executing only once- repeat: while: visible: "Delete" timeout: 2000 commands: - tapOn: "Delete"
Bug Fixes
runFlow: whenconditions with variable expressions (e.g.,${output.element.id}) were never expanded, causing conditions to always evaluate as false- iOS real device:
acceptAlertButtonSelectormatched "Don't Allow" instead of "Allow" — changed toBEGINSWITH[c] 'Allow'withOKfallback AllocatePortwas ignoring existing port allocationsrepeat-whilecondition check timeout reduced from 17s to 7s default- Android
setAirplaneMode/toggleAirplaneModefailed withSecurityExceptionon Android 7+ — now usescmd connectivity airplane-modeon Android 11+, withsettings putfallback for older versions - Implicit wait warning resolved by using Appium settings endpoint
assertVisibleoptional timeout and optimized tap element finding- WDA
launchAppoptimized: parallel permissions and removed sleeps
Test Results (v1.0.8)
| Device | Driver | Result | Duration |
|---|---|---|---|
| Pixel 4a (real) | UIAutomator2 | 9/9 pass | 2m 21s |
| Pixel 4a (real) | DeviceLab | 9/9 pass | 1m 18s |
| iPhone 16 (sim) | WDA | 8/9 pass | 2m 56s |
Contributors
- @gdealmeida1885 — Fixed variable expansion in
runFlowwhenconditions (#10) - @maggialejandro — Fixed
acceptAlertButtonSelectormatching (#24) - @7ammer — Reported
repeatwithwhilecondition bug (#23) - @wrench7 — Reported
setAirplaneModeparsing and permission issues (#27) - @AkashRajvanshi — Reported regex pattern support for ID selectors (#22)
- @jochen-testingbot — Added TestingBot cloud provider docs (#20)
v1.0.7
What's Changed
Added
- Appium driver:
newSessionoption forlaunchApp— creates a fresh Appium session, useful whenclearStatefails on real iOS devices (mobile: clearAppunsupported). On iOS real devices withnewSession: true,clearStateis skipped since a fresh session already provides clean state (#14) - Bundled UIAutomator2 server upgraded from v9.9.0 to v9.11.1 with new LaunchApp endpoint (
getLaunchIntentForPackage+startActivity) - Android: classify error types in report (
element_not_found,timeout,assertion,keyboard_covering, etc.) for better debugging - Android: detect keyboard covering elements after
inputText/inputRandom— when the soft keyboard covers a target element, taps land on the keyboard instead. Now detects this with a clear error message suggesting- hideKeyboard - Auto-create iOS simulators when not enough shutdown simulators exist for
--parallel— created simulators are automatically deleted on shutdown - Parallel device selection: in-use detection via WDA port check (iOS) and socket check (Android) to skip devices already claimed by another maestro-runner instance
Fixed
- iOS real device:
clearStateno longer kills WDA connection — replacedgo-ios(installationproxy/zipconduitover usbmuxd) withxcrun devicectl(over Apple'sremoteddaemon), which doesn't interfere with USB port forwarding - Android:
scrollandscrollUntilVisibledirection was inverted —scroll downwas scrolling up because/appium/gestures/scrollalready uses scroll semantics, no inversion needed (#9) - Android:
launchAppfailed with "No apps can perform this action" on certain devices —resolve-activitywas called without-a android.intent.action.MAIN -c android.intent.category.LAUNCHERflags. New three-tier launch strategy: (1) UIAutomator2 servergetLaunchIntentForPackage()on-device, (2) shell fallback with proper flags +dumpsysparsing + API-level-awaream start, (3) monkey fallback (#15) - Android: server APK install now checks version and handles signing conflicts (uninstall + reinstall when version mismatches)
indexselector was ignored in simple (non-relative) selectors —tapOn: text: X, index: 1always tapped the first match. Now selectors with a non-zeroindexroute through page source parsing, which returns all matches and picks the Nth one-eenv variables were not expanding in flow configappId—appId: ${APP_ID}with-e APP_ID=com.myappsent the literal${APP_ID}to adb. Now expands usingExpandVariables()before setting as a variable (#12)- Parallel device selection: devices are now filtered by platform (excludes tvOS/watchOS/xrOS) and in-use devices are skipped (#11)
- Android: emulator port allocation skipped ports occupied by running emulators
- CLI: flags must come before flow paths in command examples
Thanks
Thanks to everyone who reported issues and helped make this release better! 🙏
- @ditzdragos — reported
launchApp"No apps can perform this action" on Android (#15) - @popatre — reported
clearStatefailing on real iOS devices via Appium (#14) - @hyry2024 — reported
-eenv variables not expanding in flow configappId(#12) - @DouweBos — reported parallel device selection issues — non-iOS simulators selected and in-use devices not skipped (#11)
- @janfreund — reported scroll direction inversion with video evidence (#9)
- @SuperRoach — reported keyboard covering elements after input steps on Android, and
indexselector being ignored in simple selectors
Installation
curl -fsSL https://open.devicelab.dev/maestro-runner/install | bashPlatform Support
- ✅ macOS Intel (amd64) — Signed & Notarized
- ✅ macOS Apple Silicon (arm64) — Signed & Notarized
- ✅ Linux amd64
- ✅ Linux arm64
Built with ❤️ by DeviceLab.dev
v1.0.6
What's New
Fixed
- iOS WDA: off-screen elements no longer returned by
findElement—assertVisible,tapOn,scrollUntilVisible, and all element commands now correctly reject elements not visible in the viewport - iOS WDA:
scrollUntilVisibleno longer skips scrolling when the target element exists in the accessibility tree but is off-screen (#9) - iOS WDA:
scrollUntilVisibledirection matching is now case-insensitive (e.g.,direction: "DOWN"works) - iOS WDA:
waitForIdleTimeoutnow works on iOS via WDA quiescence when: platformcondition was ignored inrunFlowblocks (#8)
Contributors
Thanks to everyone who reported issues and suggested improvements!
- Reported
scrollUntilVisibleand element visibility issues on iOS (#9)
- Reported
when: platformcondition being ignored (#8)
Installation
Quick Install
curl -fsSL https://open.devicelab.dev/maestro-runner/install | bashVerify
maestro-runner --versionPlatform Support
- macOS Apple Silicon (arm64) — Signed & Notarized
- macOS Intel (amd64) — Signed & Notarized
- Linux amd64
- Linux arm64
Built with love by DeviceLab.dev