All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- 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 before
inputText/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. LetstapOn: "Email"find an empty email field by its hint. - Case-insensitive text matching on Android —
textContains/descriptionContainsnow 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.
- Simpler
inputTextwithout selector — DeviceLab and UIAutomator2 drivers now send key events directly viaSendKeyActionsinstead of attemptingfindFocused/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).
- iOS install hang on iOS 17+ / iOS 26 — prefer
xcrun devicectl device install appover 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 andlaunchApp { clearKeychain: true }both now work. Previously the step erred withStep type '*flow.ClearKeychainStep' is not supported on iOS, and thelaunchAppflag was a silent no-op (users stayed logged in). On simulators runsxcrun 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 asundefinedonReferenceError, matching Maestro's GraalJS Proxy behavior. Fixes #49, #50.
- 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)
- 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
- 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
- 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
- Implemented original Sauce Labs pass/fail reporting integration (#43), which formed the basis for the cloud provider abstraction in #45
- WebView CDP support for Android — the DeviceLab driver now connects to WebViews via Chrome DevTools Protocol for element finding and JavaScript execution, instead of relying solely on the native UiAutomator accessibility tree
# Automatic — when a WebView is detected, CDP is used transparently maestro-runner --driver devicelab test webview-flow.yaml
- Chrome browser CDP on Android — the DeviceLab driver can now automate Chrome browser on Android devices via CDP, enabling web testing on real Android devices
evalWebViewScriptcommand — execute inline JavaScript in a mobile WebView via CDP. Returns the result as a string, optionally stored in an output variable# Inline script - evalWebViewScript: "return document.title" # With output variable - evalWebViewScript: script: "return document.querySelector('#price').textContent" output: price # Use the result - assertTrue: ${price == '$7.50'}
runWebViewScriptcommand — load and execute a JavaScript file in a mobile WebView via CDP. Supports environment variables injected aswindow.__env# 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: validationResult
- Network idle detection and DOM stability waits — after navigations (in both browser and WebView contexts), maestro-runner now waits for network idle and DOM stability before proceeding, reducing flakiness on pages with async loading
- CDP RAF-based visibility polling — browser commands now use
requestAnimationFrame-based polling for element visibility, improving reliability for dynamically rendered content - CDP
<select>option support —tapOnwith option elements now correctly selects the option via JavaScript instead of attempting a click - CDP JS click fallback — when a native click fails on a browser element, falls back to JavaScript
.click()for better reliability with overlapping elements
- 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)
- 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 from search results, 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 that don't have HTML attributes (#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 since simulators don't need code signing# 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 even when a WebView was present - 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
- Fixed text node attribute error in browser CDP (#36)
- Refactored JS helper code into embedded files (#37)
- Reported text node attribute bug in browser CDP (#35)
- Reported
assertVisiblepassing for off-screen text in browser (#39)
- Reported
tapOntimeout issue on Android emulator (#25)
- Reported
inputTextcharacter skipping on Android (#32)
- 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 provided. Useful when the app is already installedmaestro-runner --no-app-install --app-file app.apk test flow.yaml--no-driver-installflag — skip driver installation (UIAutomator2, WDA, DeviceLab). Useful when drivers are already installed on the devicemaestro-runner --no-driver-install test flow.yaml- Flutter VM Service fallback for element finding — when the native driver (WDA/UIAutomator2) 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 with bounds stabilization for animated elements and special character handling. ~2x faster than UIAutomator2
maestro-runner --driver devicelab --platform android test flow.yaml setAirplaneModeandtoggleAirplaneModecommands for iOS (WDA) — automates the Settings app to toggle airplane mode on real devices. Supports both mapping and scalar syntax# Mapping syntax - setAirplaneMode: enabled: true # Scalar syntax - setAirplaneMode: enabled - setAirplaneMode: disabled # Toggle (flips current state) - toggleAirplaneMode
maxTypingFrequencysupport for WDA (iOS) — configurable typing speed via--typing-frequencyflag. Default: 30 keys/sec (WDA default is 60). Useful for React Native apps where the JS bridge can't keep up at full speedmaestro-runner --typing-frequency 15 test flow.yaml# Or set per-flow in YAML config section: appId: com.example.app typingFrequency: 20 --- - inputText: "hello world"
maxScrollsandtimeoutfields wired up inscrollUntilVisiblefor all 4 drivers — previously parsed but ignored, now each driver uses dual-condition loop (max scrolls AND timeout)- scrollUntilVisible: element: text: "Sign Out" direction: "down" maxScrolls: 5 timeout: 10000
- On-failure WebView detection with CDP-aware error enrichment — background CDP socket monitor with push event architecture
- Regex pattern support for ID selectors across all drivers — use regex patterns like wildcards, alternation, and character classes in
idselectors# Wildcard - tapOn: id: "username-.*" # Alternation - assertVisible: id: "(username|email)-input" # Suffix anchor - tapOn: id: "login.*-button$"
repeatwithwhilecondition now loops correctly instead of executing only once. Supports configurable timeout for the condition check- repeat: while: visible: "Delete" timeout: 2000 # ms to wait before declaring element gone commands: - tapOn: "Delete"
- Cloud Providers section in README with TestingBot setup guide
- iOS simulator no longer requires
--team-id— simulators don't need code signing, so the validation now only enforces--team-idfor real devices# Before: required --team-id even for simulators # Now: just works maestro-runner --platform ios --start-simulator <UDID> test flow.yaml
runFlow: whenconditions with variable expressions (e.g.,${output.element.id}) were never expanded, causing conditions to always evaluate as false and silently skip conditional blocks- iOS real device:
acceptAlertButtonSelectormatched "Don't Allow" instead of "Allow" —CONTAINS[c] 'Allow'matched both buttons, causing WDA to reject permission dialogs. Changed toBEGINSWITH[c] 'Allow'withOKfallback for older iOS versions AllocatePortwas ignoring existing port allocations andassertConditionhad duplicatetimeoutyaml tagrepeatwithwhilecondition executed only once instead of loopingrepeat-whilecondition check timeout reduced from 17s to 7s default- Implicit wait warning resolved by using Appium settings endpoint
assertVisibleoptional timeout and optimized tap element finding- WDA
launchAppoptimized: parallel permissions and removed sleeps - Element finding consolidated: single query with prefetched element name, merged WDA session settings into single HTTP call
- Android
setAirplaneMode/toggleAirplaneModefailed withSecurityException: Permission Denialon Android 7+ —am broadcastrequires system-level permissions. Now usescmd connectivity airplane-modeon Android 11+ (no root needed), withsettings put+ broadcast fallback for older versions (#27)
- Fixed variable expansion in
runFlowwhenconditions (#10)
- Fixed
acceptAlertButtonSelectormatching "Don't Allow" instead of "Allow" (#24)
- Reported
repeatwithwhilecondition executing only once (#23) - Reported implicit wait warning with Appium settings endpoint
- Reported
setAirplaneModescalar syntax parsing issue (#27) - Reported
setAirplaneModebroadcast permission denied on Android 7+ (#27)
- Reported regex pattern support for ID selectors (#22)
- Added TestingBot cloud provider documentation (#20)
- 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)- launchApp: appId: com.example.app newSession: true
- 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 of the element. 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
- 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 because native driver APIs return only a single element. 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
- Reported
launchApp"No apps can perform this action" on Android (#15)
- Reported
clearStatefailing on real iOS devices via Appium (#14)
- Reported
-eenv variables not expanding in flow configappId(#12)
- Reported parallel device selection issues — non-iOS simulators selected and in-use devices not skipped (#11)
- Reported scroll direction inversion with video evidence (#9)
- Reported keyboard covering elements after input steps on Android
- Reported
indexselector being ignored in simple selectors
- 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 - 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)
- Reported
scrollUntilVisibleand element visibility issues on iOS (#9)
- Reported
when: platformcondition being ignored (#8)
tapOn: pointnow supports absolute pixel coordinates (e.g.,point: "286, 819") in addition to percentages- Coordinate validation: negative values, out-of-bounds pixels, and percentage range (0-100%) are all rejected with clear error messages
- Screen size cached at session startup instead of fetching on every tap/swipe/scroll
launchApp: environmentfor passing environment variables via WDAlaunchEnvironment
- Extracted shared helpers (
ParsePointCoords,ParsePercentageCoords,RandomString,SuccessResult, etc.) from drivers intopkg/core - Removed hardcoded 1080x1920 screen size fallback in UIAutomator2 scroll/swipe
launchApp: argumentssilently failed on real iOS devices — early return after session creation, unpopulated env map, activate vs launch, missing variable expansion- Removed unused AI flags (
--analyze,--api-url,--api-key)
- Reported
tapOn: pointnot supporting absolute pixel coordinates (#6) - Spotted unused AI flags (
--analyze,--api-url,--api-key)
- Reported
launchApp: argumentsnot working on real iOS devices (#7)
keyPressoption for character-by-character text input on Android- Stale socket cleanup on force-stop (Ctrl+C / kill -9) with PID-based locking
- iOS Appium driver: element finding and tap reliability (use
labelinstead ofcontent-descfor accessibility) - iOS Appium driver:
pressKeycommand support - iOS Appium driver:
tapOnandinputTextreliability improvements - iOS Appium driver: skip
--app-fileand--team-idpre-checks (not needed for Appium) - iOS Appium driver: skip
clearStateon real devices (mobile: clearApponly works on simulators) - iOS WDA driver: auto-alert handling on simulators (accept/dismiss permission dialogs)
takeScreenshotcommand now correctly saves PNG files- GitHub star link in HTML report
- All
errcheckviolations fixed with proper error logging
- Suggested the
keyPressfeature for character-by-character input - Suggested the
--team-idpre-check for WDA driver - Reported the
takeScreenshotbug
- Reported the stale socket issue on force-stop (Ctrl+C)
- Reported iOS element finding issue —
labelinstead ofcontent-desc(#3) - Reported
pressKeynot working for iOS on Saucelabs (#4)
- Reported clearState and iOS permission dialog handling issues (#2)
- CLI with
validateandruncommands - Configuration loading from
config.yaml - YAML flow parser with support for all Maestro commands
- Flow validator with dependency resolution
- Tag-based test filtering (include/exclude)
- UIAutomator2 driver with native element waiting
- Appium driver with per-flow sessions and capabilities file support
- WDA driver for iOS via WebDriverAgent
- JavaScript scripting engine (
evalScript,assertTrue,runScript) - Regex pattern matching for element selectors (
assertVisible,copyTextFrom) - Coordinate-based swipe and percentage-based tap support
- Nested relative selector support
- Step-level and command-level configurable timeouts
- Context-based timeout management
- Configurable
waitForIdleTimeoutfor UIAutomator2 inputRandomwith DataType support- JSON report output with real-time updates
- HTML report generator with sub-command expansion for
runFlow,repeat,retry - Clickable element prioritization for Appium
- JS
evalScriptandassertTrueparsing for Maestro${...}syntax - Step counting accuracy in reports
- Appium driver regex matching