Skip to content

v1.1.31

Latest

Choose a tag to compare

@omnarayan omnarayan released this 12 Nov 05:44

Release 1.1.31

A fix release for 1.1.30, whose GitHub release was withdrawn. Everything listed under 1.1.30 further down ships in 1.1.31. On top of it: WDA answers the permission prompts a flow does not tap (a 1.1.30 regression), taps after a check wait for the screen to settle, WDA page-source lookups on real iPhones are much faster, and a few smaller fixes.

Changed

WDA: page-source lookups are much faster on real iPhones (#266). A lookup that falls back to the page source (a selector not on screen yet, an optional step, when: visible, relative selectors, index:, copyTextFrom) now asks for it without the visible and accessible attributes, which WebDriverAgent computes for every element with an accessibility call each and the matcher never reads: on a heavy screen a page source went from about 25 s to 2.4 s. The report and the hierarchy command still get every attribute.

WDA: --wait-for-idle-timeout above 0 no longer sends settings WebDriverAgent ignores (#267). The value never changed anything on WDA: WebDriverAgent reads shouldWaitForQuiescence only when a session starts, takes waitForIdleTimeout in seconds, and every launchApp set it back to 0. On WDA only 0 has an effect, turning off the settle before each action, and the flag's help now says so.

Fixed

WDA: a flow that taps one permission prompt's button still gets the other prompts answered (#264, #265). Since 1.1.30, a flow that taps a prompt's button itself (tapOn: "Ask App Not to Track") turned off WDA's automatic answer for the whole session, so a later prompt the flow never meant to answer (notifications) stayed up over the app and every later step failed behind it. A lookup that finds nothing now answers such a prompt as WDA would have (Allow…, or Don't Allow… with permissions: {all: deny}) and looks again. The prompts the flow taps are still left to it, and a flow with acceptAlert or dismissAlert is unchanged.

WDA: kAXErrorServerNotFound fails the step at once and says what it is (#268). On real iPhones, after a clearState reinstall, the app sometimes runs without its accessibility server, and every page source fails with this error. A lookup polled to its timeout, each poll a page source of 8 to 10 s, and failed with "not visible within 30s", which read like a slow screen. The condition does not clear within a flow, so the step now fails at the first such read with "the app's accessibility server is not running (kAXErrorServerNotFound): WDA can reach the device but not the app; a relaunch does not help, restarting the device does".

WDA: a tap after a check waits for the screen to settle (#269). The driver settles before an action when the step before it may have moved the screen, but any other step cleared that, so a check or a script between two taps made the second tap skip it. When a tap opened a sheet, the check passed while the sheet was still sliding up (WebDriverAgent reports its final frame), and the next tap on the sheet went out mid-slide and was lost. A step that neither acts nor moves the screen (an assert, a script, a condition) now leaves it as it was.

WDA: eraseText on a simulator no longer hides the keyboard for the rest of the run (#270). WebDriverAgent cleared a field with a hardware keyboard's Clear key. On a simulator iOS then took a hardware keyboard as attached and kept the software keyboard off screen whenever a field had focus, so anything an app shows with the keyboard never appeared. On a simulator the field is now cleared with typed backspaces, as Maestro does; a real iPhone keeps the Clear key.

DeviceLab iOS: launchApp with stopApp: false keeps the running app (#261). The launch applied the default permissions first, and iOS kills an app whose permissions change, so the app restarted and lost its state. With stopApp: false, permissions and clearKeychain are no longer applied (a log line says so when the flow gives them).

DeviceLab iOS: hideKeyboard no longer swipes on the keyboard while it is still sliding in. The iOS agent took the screen size from the keyboard's frame. Right after a field took focus, that frame still lay below the screen, so the dismiss swipes started at the centre of a screen twice as tall: on the keys, where they dismiss nothing. Since 1.1.30 fails the step when the keyboard stays up (#259), flows that passed in 1.1.29 failed there (React Native's RNTester text.yml on iOS 27, 4 of 6 runs). The agent now reads the screen size from the app, waits for the keyboard to stop moving, and never starts the swipes below the keyboard.

Contributors

@luka-vladika-happening-xyz

  1. WDA answers the permission prompts a flow does not tap (#264, #265)
  2. WDA lookups read the page source without visible and accessible (#266)
  3. WDA settles before a tap that follows a check (#269)
  4. WDA eraseText on a simulator keeps the keyboard on screen (#270)
  5. Reported --wait-for-idle-timeout doing nothing on WDA (#267) and kAXErrorServerNotFound (#268)

@benalbrecht

  1. DeviceLab iOS launchApp with stopApp: false keeps the running app (#261)

@emil-doychev-bijenkorf

  1. Reported copyTextFrom ignoring a childOf anchor's index on 1.1.29 (#262), fixed by #241 in 1.1.30

Also in this release: 1.1.30 changes

1.1.30's GitHub release was withdrawn, so this is the first release with these changes since 1.1.29.

This release closes more gaps with Maestro: index: counts matches by their position on screen, a long press holds 3 s on every driver, a ${…} that throws fails its step, and DeviceLab iOS hideKeyboard fails when the keyboard stays up. WDA can see and tap system permission prompts, can drive a WebDriverAgent that is already running (--wda-url) or keep one between runs (--reuse-wda), and reads typed text back to retype dropped characters. New options: --pause-on-failure, --find-timeout and inputText retype: false. The DeviceLab agents on Android and iOS now listen on loopback only and answer only the run that started them. Contributors sent 23 pull requests and many precise bug reports, credited at the end.

⚠️ Behaviour changes

DeviceLab iOS: hideKeyboard fails when the keyboard is still up after its dismiss swipes, as in Maestro (#259). Since 1.1.28 the step passed with the keyboard still up, so the flow failed later, on a tap the keyboard covered or a wait for it to go, with nothing pointing at hideKeyboard. Maestro has failed the step since 2.2. The swipes close the keyboard over a scroll view that dismisses it on drag; on other screens (a React Native ScrollView without keyboardDismissMode) use pressKey: Enter on a field whose return key closes the keyboard, or tap a label. WDA's hideKeyboard presses return and is unchanged. Reported by @georgetarazi-swipejobs.

A ${…} whose JavaScript throws fails its step, even with optional: true, as in Maestro. The expression was left in the step as literal text, so inputText typed ${output.user.email}, a selector searched for it, a when: read it as false, and env: passed it on; the flow then failed somewhere else, or passed with the wrong input. The step now fails with the JavaScript error and does not run. This covers step fields, selectors, runFlow's when: and env:, and repeat's while: and times:. A name that was never declared still reads as undefined. (#250)

DeviceLab Android: inputText types short text into an empty field key by key, as Maestro does. Text of up to 8 ASCII characters goes in as key events, so apps that react to each keystroke see every one: a one-time-code box that moves to the next box, a PIN, an input mask. The field was set in one call, which such apps saw as a single change. Longer or non-ASCII text is still set in one call, and a field that already holds text is still typed into at the cursor. When the app moves focus while the text goes in, the first field is no longer read back and retyped.

A long press with no duration holds 3 s on every driver, as Maestro's does on Android, iOS and web. DeviceLab Android, UIAutomator2, Appium and DeviceLab iOS held 1 s, the old DeviceLab iOS driver 0.8 s and the browser driver 1 s (longPressOn, and tapOn or tapOnPoint with longPress). WDA already held 3 s. A flow that gives duration: is unchanged, except on DeviceLab iOS, which raised any duration under 1 s to 1 s: duration: 500 now holds 500 ms.

A run cancelled before its flows finish reports failed, not passed. Its flows were marked skipped, and a run of skipped flows counted as passed: a run cancelled before any flow started reported passed, with report.json left at running and those flows at pending, and one cancelled inside a flow had report.json say passed. Those flows stay skipped, a flow the run never reached now reads skipped with run cancelled in report.json too, and the run's status is failed unless every flow passed. report.json keeps its fields; only that value changes. This applies to runs driven through the executor with a cancellable context. Ctrl-C on maestro-runner test already exits with code 1, and still does.

index: counts matches by their position on screen, top to bottom then left to right, as Maestro does (DeviceLab Android, UIAutomator2, Appium, WDA, DeviceLab iOS). Clickable matches were put first before the index was applied, so below: "Connections", text: ".+", index: 0 on Android's Settings tapped a navigation-bar button (clickable, matched by its content-desc) instead of the summary line under "Connections", and left the app; copyTextFrom with that selector copied an empty string. Maestro puts clickable matches first only when there is no index, and with one sorts by position. Now so do these drivers, for plain and relative selectors, and a negative index counts from the bottom-right. A flow whose index relied on clickable matches coming first may now pick a different element. A selector without index: is unchanged. DeviceLab iOS counted its matches in the order of the accessibility tree, where a tab bar comes before the content above it; it now counts by position too. (#254)

tapOn with text: or id: and an index: taps that match on DeviceLab Android, and with an index, a match that contains another match is not counted, as in Maestro (DeviceLab Android, UIAutomator2, Appium, WDA). DeviceLab Android sent such a tap to its agent, which tapped the first match whatever the index, so index: 1 tapped the same element as index: 0 and an index past the last match passed. It now picks by position and fails an index with no match ("no element at index 5: 2 match(es)"). A container whose description matches the text of a label inside it, such as YouTube's "Home" tab around the "Home" label, used to count as a match of its own, so index: 1 picked that label instead of the next "Home" on screen. Maestro keeps only the innermost match there, and now so do these drivers when an index is given; without one, nothing changes. On a Pixel with 3-button navigation, tapOn: {text: "Home", index: 1} in YouTube taps the navigation bar's Home button, as in Maestro 2.10. Reported by @losey-kim. (#260)

DeviceLab Android: setOrientation: PORTRAIT and LANDSCAPE turn the screen. They passed without doing anything: the on-device agent answered the request and left the display as it was. Every orientation is now set over adb as Maestro does, with LANDSCAPE turning the same way as LANDSCAPE_LEFT, and the step waits for the display to turn. A flow that ran in portrait after one of these steps now runs in landscape. Reported in #256.

WDA: hideKeyboard drags the keyboard away instead of pressing return, as Maestro does. Return submitted the form or closed the screen the keyboard belonged to, so a hideKeyboard between two fields could send a half-filled form. The driver now makes Maestro's two short drags (up 3% of the screen in 50 ms, then left), which a scroll view turns into a dismissal, and stops as soon as the keyboard is gone. They start at the point nearest the centre of the screen, above the keyboard, that no control covers (8 pt clearance), inside a scroll view when the screen has one: Maestro starts them at the exact centre, where a checkbox or a React Native Pressable takes the drag as a tap. With no such point, or a keyboard that will not go (a screen with no scroll view), the keyboard stays up and the step passes, as in Maestro and DeviceLab iOS. A flow that relied on hideKeyboard to submit needs pressKey: Enter instead.

WDA: a flow that answers permission prompts itself keeps WDA from answering them first (#248). With acceptAlert, dismissAlert, or a tapOn naming "Allow…", "Don't Allow…" or "Ask App Not…" among a flow's own or inline steps, launchApp no longer turns on WDA's automatic answer for that flow, so any other prompt in it waits for a step too. Steps in a runFlow: file: are not looked at.

Added

  • maestro-runner hierarchy shows what Android toggles, sliders and panes announce (DeviceLab Android). The tree and --compact now carry a node's state ("ON", "96%", from Android 11) and pane title ("Status bar", from Android 9), and a field's error. They are for reading only: no selector matches state or pane, as none does in Maestro.
  • --pause-on-failure stops a run at the step that fails its flow, with the device and the app left as they are. Finding out why a step failed meant changing the flow and running it again from the start. Now the run waits at the failure and the terminal takes commands: h [text] prints the screen's elements (or those matching the text), s saves a screenshot under <output>/paused/, x <step> runs a step on the device in the flow's context (its variables, output.* and copiedText), r runs the failed step again, and Enter lets the failure stand and the run go on. A retry that passes replaces the failure and the flow carries on; one that fails pauses again. Steps run with x are probes: they are not added to the report or counted. It pauses only where a failure would fail the flow, not on an optional step; a failure inside a runFlow, repeat or retry pauses on that step, and r runs it again whole. On a local Android driver, maestro-runner --device <id> hierarchy also works from another terminal while paused. The flag (or MAESTRO_PAUSE_ON_FAILURE) is ignored with a warning when stdin is not a terminal, so CI never waits, and with --parallel or several devices. Without it nothing changes, and with it the report has the same shape. (#246)
  • inputText with retype: false leaves a field that reads back short as it is (DeviceLab Android, UIAutomator2, Appium, WDA, DeviceLab iOS). After typing, these drivers read the field back and, when it holds less than was typed (a dropped keystroke), clear it and type again. A field that cuts what it is given (a stake field that drops decimals, a length limit) reads back short too, and typing into it again changes what the app holds. With retype: false the step notes how many characters the field holds and moves on. The default is unchanged. On DeviceLab iOS, where the agent does the read-back, the field is then not read back at all, so there is no note. The browser driver never reads the field back, so the key changes nothing there. Maestro has no retype key, so a flow that uses it won't parse in Maestro. (#247)
  • --find-timeout <ms> (MAESTRO_FIND_TIMEOUT) sets how long a step waits for its element when it gives no timeout: of its own, on every driver. Unset, each driver keeps its default (17 s on most, as Maestro). Lower it, to 3000 say, and a wrong selector fails in 3 s instead of 17 s while you write a flow. A flow's commandTimeout still wins for that flow, and a flow without one gets the flag's value back instead of the previous flow's commandTimeout. when: conditions keep --condition-timeout, and optional: true steps keep their own 7 s budget on the drivers that have one.
  • Telemetry: an Appium run reports where its Appium server runs (appium_vendor): a known cloud by name (saucelabs, browserstack, lambdatest, perfecto, kobiton, headspin, testingbot, digitalai or aws for Device Farm), local for localhost or any IP address, and other for any other server. Only the host is read, and it is never sent, nor are the rest of the URL, the port, credentials or capabilities. Other drivers send nothing new. The telemetry page lists every field, and MAESTRO_RUNNER_NO_TELEMETRY=1 or DO_NOT_TRACK=1 turn it off.
  • --wda-url http://host:port (MAESTRO_WDA_URL) drives a WebDriverAgent that is already running for the --device instead of building and starting one (--driver wda). The run waits up to 30 s for its /status, needs no --team-id since it signs nothing, and at the end ends only its own session, within 5 s, leaving the WebDriverAgent running. It takes exactly one --device, which must still be connected to this machine. A URL on that device's own WDA port on this machine is not read as "device in use". Without Xcode on the host, this is how a run reaches a physical iPhone whose WebDriverAgent another tool started (go-ios's runwda on the machine the phone is plugged into, for one).
  • --reuse-wda (MAESTRO_REUSE_WDA) leaves WebDriverAgent running between runs and reuses it (--driver wda, #213). A run with the flag leaves its WebDriverAgent running when it ends, the first one included, and ends only its own session. The next run with the flag on the same device drives that WebDriverAgent with a new session instead of starting one, when it is still the xcodebuild a run started for that device, on the device's port, from the same build (a hash of the cached test bundle), and /status says ready; otherwise it is stopped and a new one started. Skipping the start-up cut a harness that runs one process per test case by 16–25%. Two runs never drive one WebDriverAgent: a second run with the flag on a device in use is refused. A run without the flag stops a WebDriverAgent left this way and starts its own, as before. On a simulator, a kept WebDriverAgent is stopped when the simulator shuts down. --wda-url takes precedence.
  • WDA: MAESTRO_WDA_HOSTED_FRAMES=1 corrects the frames of a view another process draws over the app, off by default. On iOS 27 XCUITest reports the elements of such a view (Apple Health's authorization sheet, for one) in the view's own coordinates, so they read as much as 48 points too high and every tap on them missed, by page source, by WDA rect and by XCUITest's own click. With the variable set, page-source frames are moved onto the screen where such a space starts, and every lookup reads the page source, so text and id: selectors, scrollUntilVisible and copyTextFrom all get the corrected frame (a page source per lookup, with no fallback to WDA's own queries); nothing inside a web view, whose content already reports screen coordinates, is moved. A scroll view scrolled by exactly its own offset can look the same, which is why it is off by default; 0 and false leave it off. --driver devicelab handles these sheets without the variable. This is the WDA counterpart of the DeviceLab iOS fix for #191 in 1.1.29.
  • DeviceLab iOS answers system permission prompts the way WDA does. With permissions resolving to allow (no permissions: at all, or all: allow), a SpringBoard permission prompt is answered by tapping a button whose label starts with "Allow" ("Allow", "Allow While Using App", "Allow Once", "Allow Full Access", and App Tracking Transparency's "Allow"); with all: deny, "Don't Allow" (either apostrophe) or "Ask App Not to Track". A mixed list answers nothing. The driver looks for a prompt before the steps of the first 3 s after launchApp (and for 3 s after each prompt it answers), when an element has been missing for a second (then at most once a second), and when scrollUntilVisible stops moving; a found element costs no extra call. Only SpringBoard's alerts are read, so the app's own alerts, and a screen the app shows before the system prompt, are left to the flow, as Maestro does. A prompt answered "OK" (Face ID on a device) or "Continue" (contacts, iOS 18 and later) is not accepted for you; use acceptAlert or tapOn there. A step aimed at the prompt itself is left to answer it: acceptAlert, dismissAlert, or a step whose text/id matches one of the prompt's buttons or its title (tapOn: "Allow", tapOn: "Ask App Not to Track", an assertVisible on the question), so flows written for Maestro that tap "Allow" themselves keep passing. acceptAlert and dismissAlert work as before, and DEVICELAB_IOS_AUTO_ALERTS=0 (or false) turns the automatic answers off.
  • DeviceLab Android: scrollUntilVisible with from: scrolls inside that element, as on UIAutomator2. The step failed with "not supported on this driver yet". The swipe now starts and ends inside the from: element's bounds, kept an eighth of its height off its top and bottom edges, so a carousel or nested list scrolls instead of the screen behind it. A from: element that isn't found fails the step with "Scroll container not found".
  • DeviceLab Android warns when a tap's point lies under another window. A tapOn, doubleTapOn or longPressOn on an element found through the page source (a selector with point:, index:, a relative selector, a regex id:, and the like) goes to whatever window is on top at that point, so a keyboard, dialog or overlay there takes it instead of the element. The log now names that window (tap on … the point is under keyboard window …); the tap still goes out and the step is unchanged. Taps by text or id: already refuse a covered point or tap the part left showing. MAESTRO_DISABLE_HIT_TEST=1 turns it off.
  • A runScript file's relative require() resolves against the script's own folder first, then against the flow's folder as before, so a shared helper script can load require("../lib/env.js") the same way from flows at any depth. A path that resolves today from the flow's folder still does, unless a file with the same relative path sits next to the script. Inline scripts resolve from the flow's folder only, a module's own require() stays relative to that module, and an absolute path is read as given. The same relative path required from two folders now loads two modules instead of the first one from the cache (#253).
  • DeviceLab iOS: setAirplaneMode and toggleAirplaneMode work on a real iPhone. They failed on every target with "airplane mode is not available on the iOS simulator", a real iPhone included. As on WDA, the driver opens Settings, reads the Airplane Mode switch and taps it when it is not in the state asked for (toggleAirplaneMode flips it); "already enabled/disabled" taps nothing. Unlike WDA, it reads the switch again after the tap and fails the step if the state did not change, and it brings the app that was in front back to the front without relaunching it, so a flow needs no stopApp/launchApp after the toggle. Settings must open on its main page, as with WDA. On a simulator, which has no airplane mode, both steps are skipped, as on WDA. Not yet tested on a physical iPhone. (#252)
  • DeviceLab iOS on a simulator warns when the simulator cancelled a tap that XCTest reported as delivered. XCTest plays every gesture through a new virtual touchscreen, and when the touch arrives before the simulator has registered that touchscreen, the app gets the touch cancelled instead of a tap while the step passes; the next step then fails on an unchanged screen, most often after the first tap of a session. The driver reads the simulator's backboardd log for the two lines that show it (unknown senderID and digitizer did disappear with an open contact, for the same sender) and logs a warning naming the sender. DL_IOS_LOST_TOUCH=retry also sends such a tap or long press once more, since the log shows the app never got it (a double tap or a swipe is not resent); DL_IOS_LOST_TOUCH=off turns the log reading off. The default only warns and adds no time to a step.
  • MAESTRO_RUNNER_NO_UPDATE_CHECK=1 turns the update check off, and on CI the update notice is no longer printed. An npm install's update notice now says npm i -g maestro-runner@latest instead of the install script's curl command.
  • maestro-runner skills install installs a skill for AI coding agents. It writes SKILL.md to ~/.claude/skills/maestro-runner/, telling Claude Code and other agents that read skills how to write Maestro flows, run them, inspect the screen with hierarchy and read the reports. --dir writes it elsewhere (.claude/skills/maestro-runner in a repository) and --print prints it. The file carries the binary's version: installing again replaces it only when the version differs, or with --force, and a SKILL.md that maestro-runner did not write is left alone unless --force is given.
  • Contributors: make speedgate catches a command type that got slower. It reads the reports of repeated runs of one suite on one device and driver, takes each command type's median over its passed steps, and fails when one is more than 20% and 50 ms slower than the committed baseline (benchmarks/speed-baseline.json); WRITE=1 writes the baseline. report.json is unchanged. See DEVELOPER.md.
  • Contributors: make hierparity DEVICE=<serial> checks that the DeviceLab Android agent reads every labelled element uiautomator dump reads, in the same place. Recall and precision over the screen in front, with what is missing listed; see DEVELOPER.md.
  • Library users: the DeviceLab Android agent reports windows, the app in front and the keyboard, and can wait for a launch, behind new opt-in calls (pkg/maestro). For a host that drives the agent directly, such as an AI agent working step by step. SnapshotWith adds each window's type, layer, focus, title and package, the display size, the app in front and the keyboard's top edge; GetForeground reads the app in front, the lock screen and the screen state without a shell call; LaunchAppWait fails with screen_locked, or with not_foreground naming what is in front, instead of returning while a dialog or the lock screen covers the app; WaitForSettleWith and TreeHashWith cover all of the app's windows (popups, dialogs) and can ignore text changes such as a clock; Observe settles, reads the tree, the app in front and the keyboard, and optionally a screenshot, in one call. Session.create lists them in capabilities. Flows use none of them and run as before: on a Pixel 4a the TestHive suite passed with the same times on DeviceLab, UIAutomator2 and Appium. These packages make no API promise.

Changed

  • DeviceLab iOS: only the run that started the agent can drive it. The agent listened on every network interface with no check, so anyone on the same network could tap, type and take screenshots through it. It now listens on the loopback address only and answers only requests that carry the random token the run started it with; a later run re-attaches using the token saved with the agent's state (readable by the user only). A run checks which agent build is installed on the simulator instead of trusting its own record, and does not re-attach to an agent that answers without the token: an older build another maestro-runner install put on the simulator is replaced by this one. DEVICELAB_IOS_AGENT_BIND=any makes the agent on a real iPhone listen on every interface, should the USB forward fail to reach the loopback address.
  • DeviceLab Android: only the run that started the agent can drive it. The agent listened on port 6791 on every network interface with no check, so another device on the same Wi-Fi, or another app on the phone, could tap, type and read the screen through it. It now listens on the phone's loopback address, where adb forward connects, and refuses a client that does not send the random token the run started it with (am instrument -e token, which only adb can start, so no app can read it). The health check and a restart mid-run use the new token. An older agent ignores the token and works as before; an agent started without one (by an older runner) accepts any local client. DEVICELAB_ANDROID_AGENT_BIND=any makes it listen on every interface again, for a setup that reaches the phone over its network instead of adb.
  • Bundled WebDriverAgent updated from 16.12.8 to 16.17.5, now taken from packages/webdriveragent of appium/appium-ios, where WebDriverAgent moved at 16.16.0. Brings upstream fixes for a coordinate drag that could hang WDA on an infinite point, a deleted session that left the app it launched running, an app brought back from the background before SpringBoard settled, HTTP bytes dropped when the client closed the connection, and a faster accessibility-id lookup and page source. Nothing maestro-runner relies on changed behaviour; the first run after this update rebuilds the WDA runner once, as the build cache is keyed by the bundled version.

Fixed

  • DeviceLab iOS: a run on a simulator ends about 2.5 s sooner and leaves no log stream behind. The watch for lost touches reads the simulator's log through simctl spawn … log stream. Stopping it ended only simctl; the log process inside the simulator kept running and held the pipe, so every run waited 2 s for it and left one more log process on the simulator. It is now ended by name. The Reduce Motion and AutoFill settings are also read, written and put back side by side, about 0.7 s less per run. Suites that start the runner once per flow gain the most: about 3 s a flow.
  • WDA: of two elements with the same id side by side, a tap goes to the control (#259). A search field and its magnifier icon can share an id; the icon came first in tree order and took the tap, so no field got focus and the next inputText failed. A field, button or other control is now taken over an image or label, as the DeviceLab iOS driver already did. A match that holds another is still skipped for the one inside it.
  • WDA: flows can see and tap system permission prompts (#248). A prompt belongs to SpringBoard and WDA's page source holds only the app, so visible:, assertVisible and extendedWaitUntil never saw it and tapOn: "Allow" never found its button. When a text selector finds nothing in the app, these steps now also read an open system alert: its text and buttons for a visibility check, its buttons for a plain tapOn.
  • WDA: acceptAlert, dismissAlert and a tap on a prompt's button pass only once the prompt has closed. A tap sent while a prompt was still animating in was answered 200 and did nothing, so acceptAlert passed with the prompt still up. The tap is now sent again, up to 4 times, and the step fails if the prompt stays open.
  • DeviceLab iOS: inputText with a newline or a tab is no longer typed a second time. The agent reads the focused field back after typing and, when it looks short, deletes what arrived and types the text again. A field never shows a newline or a tab, so text such as "query\n" looked short whenever a field still had focus after return, and the retype pressed return again: a search ran twice or a form was submitted twice, and when return had moved focus on, the delete could reach the next field. That text is no longer read back, as on WDA. Other text is still read back and retyped once when keys went missing.
  • DeviceLab iOS: the content of a web view the app keeps hidden is no longer on screen to flows (#211). WebKit keeps reporting a hidden web view's elements with on-screen frames, so a consent form an ad SDK preloads out of sight (Google's UMP form in a Flutter app) made when: visible true and tapOn: "Continue" hit the app's own button under it. Each web view in a capture is now asked whether it is visible, as WDA's visible does, and a hidden one is sent without its content: at most 6 requests of 0.3 s per capture, and an unanswered one keeps the content as before. On an iOS 27 simulator with a hidden web view holding "Continue", the app's own "Continue" is tapped; TestHive's login flows take as long as before. Reported by @JohnAtTimeless.
  • DeviceLab iOS reads UI that another process draws into the app, such as the photo picker and the device-passcode sheet. Such a sheet can reach a capture as one element with nothing inside, so nothing in it could be found or tapped. Where an element is drawn by another process, the capture now reads it from that process and adds its content: at most 4 such elements and 2 s per capture. DL_AGENT_REMOTE_CONTENT=0 turns it off. On an iOS 27 simulator the photo picker, the share sheet and the Face ID sheet were already readable, so this was not seen to change a result there.
  • DeviceLab Android: an app behind a launch screen Android never removed is read again. A launch can leave its starting window over the app (a React Native debug app on a Pixel 4a), and Android then lists only the status bar as a window, so the read that relative, index: and page-source lookups and hierarchy use had none of the app: 0 of 19 labelled elements on that screen, where uiautomator dump had all 19. The agent now also reads the window in front whenever the window list leaves it out (19 of 19).
  • DeviceLab Android: a text selector finds a field by its validation error, as Maestro does and UIAutomator2 already did. A field's error (Android's setError, Compose error() semantics: "Invalid email") was not read by the agent, so assertVisible: "Invalid email" failed when the message lived only there. The agent now writes it as error in the hierarchy, and text selectors match it after text, description and hint; a field with no error never matches, even .*.
  • DeviceLab Android: percentage points, swipes and scrolls are worked out from the whole display (wm size), as Maestro's are (getRealMetrics). They used the size the agent read from the window in front when the session started, which can leave out the system bars or be a smaller window, so 50% could land higher than in Maestro. On a Pixel 4a both sizes are 1080×2340 today, so taps there land where they did; the size no longer depends on what was on screen when the run began.
  • WDA on a real iPhone: launchApp right after clearState's reinstall waits for iOS to register the app. The launch was sent before iOS knew the reinstalled app and failed with FBSOpenApplicationErrorDomain Code=4 ("unknown to FrontBoard"). The launch, and a new session that launches the app, are now sent again every 500 ms for up to 10 s while they fail that way, with one log line when a retry was needed. A flow whose app is not installed when it starts now waits those 10 s before launchApp installs it. Contributed by @luka-vladika-happening-xyz. (#251, #257)
  • After setOrientation, percentage points, coordinate taps and the on-screen check use the turned screen size (DeviceLab Android, UIAutomator2, Appium, #256). The size read when the session started was kept, so in landscape tapOn: point: "50%, 40%" was worked out from the portrait size and landed elsewhere, DeviceLab refused a point right of the portrait width as off screen, and on Appium assertVisible found an element there "off screen". The size now follows the rotation the step leaves, and setOrientation on UIAutomator2 and Appium waits for the screen to turn before the next step. Reported by @wanglinyu12345-lab.
  • iOS simulators in another language: system permission prompts are answered again (WDA, DeviceLab iOS). The automatic answer matched English labels only ("Allow…", "Don't Allow", "Ask App Not to Track"), so on a simulator set to German the notification prompt's "Erlauben" / "Nicht erlauben" was left open and the flow failed behind it. The driver now reads iOS's own wording of those buttons from the simulator runtime's string tables (notifications, privacy access including App Tracking Transparency, and location), for each of the simulator's preferred languages, once per run and only when needed, and matches a localized button as a whole label with the same meaning as the English one it translates. A translation that would mean both allow and deny counts as neither. The rule is otherwise unchanged: only an allow-type (or, with all: deny, deny-type) button, never an "OK" or "Continue", and on DeviceLab iOS only SpringBoard's alerts. Real iPhones still match English labels only.
  • engine: agent scrolls go the right way again (DeviceLab Android). The agent scrolled the opposite way to the direction asked, so scrollUntilVisible and scroll with engine: agent gave up after two scrolls with no progress (Settings on a Pixel 4a: 0/1, now 1/1 both ways). Fixed in May for #81, the fix was lost from the agent source. The default (adb) engine was not affected.
  • DeviceLab Android: a swipe inside a nested pager pages the inner pager, not the outer one. The swipe moved 1 ms after touching down, and react-native-pager-view decides which pager owns a gesture one frame after the touch, so the outer pager took it. The swipe now rests 50 ms before it moves, about 50 ms more per swipe; how far a swipe scrolls is unchanged.
  • DeviceLab Android: an action on a screen that never stops animating waits 2 s for it to settle once, not before every step. After a tap or swipe the next action waits up to 2 s for the screen to stop changing, and a screen where something keeps moving (an auto-scrolling carousel, a marquee) used all 2 s every time. Once a wait has run out, the next one on the same screen waits at most 500 ms; new text or a new screen gets the full 2 s again, as does a screen that has settled. The screen is told apart by its text and content descriptions, which such movement leaves alone; a ticking timer changes text, so it still gets the full 2 s. Waits after openLink and Enter are unchanged. MAESTRO_DEVICELAB_STUCK_SETTLE_MS sets the shorter wait; 2000 or more turns it off.
  • DeviceLab Android: a view removed while the screen is read no longer fails the read. The agent read the hierarchy node by node, and a node whose view went away mid-read threw, failing the whole read; the step polled again or failed. That node is now left out, with what was under it, and the rest of the screen is returned.
  • UIAutomator2 and Appium on Android no longer tap an element with no area. A 0x0 element (a collapsed view, or a Compose node not yet laid out) was tapped at the centre of its bounds, which is its corner, so the tap landed on whatever was drawn there and the step passed. Such an element is now passed over and the lookup keeps polling: one laid out within the timeout is tapped at its centre, and one that never gets a size fails with "element has no area". Maestro never finds such an element, and the page-source lookups of both drivers and DeviceLab Android already passed it over. Covers tapOn, doubleTapOn and longPressOn, and on UIAutomator2 also a dragAndDrop endpoint given by selector.
  • WDA: hideKeyboard does nothing when no keyboard is on screen. It pressed return regardless, and a focused field with no keyboard showing (a simulator with the hardware keyboard connected) took it as submit, so the form was sent. Maestro's hideKeyboard does nothing then, and DeviceLab iOS already did not.
  • WDA: eraseText with a count on an empty field no longer types part of its placeholder into it. WDA reads an empty text field's placeholder as its text, and erasing fewer characters than that clears the field and types back what should remain: eraseText: 5 on an empty "Email address" field left "Email a" in it. A text that is the field's placeholder now counts as empty, and the field is cleared.
  • DeviceLab iOS: dismissAlert never taps an "Allow" button, and acceptAlert and dismissAlert pick by iOS's layout when no label is known. With no known decline label, dismissAlert tapped the first button and acceptAlert the last, which on a stacked alert of three or more buttons is the wrong end: a location prompt in another language was answered "Allow Once" by dismissAlert. The driver now reads the alert and names the button: a known label first ("Allow…", "OK", "Continue", "Open", "Yes" to accept; "Don't Allow", "Ask App Not to Track", "Cancel", "Not Now", "Close", "No", "Dismiss" to dismiss); else the left button of two for a dismiss and the right for an accept, and the bottom of a stack for a dismiss and the top for an accept; else the one button that does not read as the other answer. An alert where no button can be named fails the step and taps nothing, instead of the step passing as "no alert" after its timeout; a one-button alert is answered with its button, as before.
  • WDA: acceptAlert and dismissAlert tap the alert's accept or decline button whatever permissions resolve to. The button selectors were set only for the matching auto-answer (all: allow or all: deny), and the dismiss one never matched because of the apostrophe in "Don't Allow". Without a match WDA picks by position, the first button for a dismiss, so on a stacked alert (Delete, Archive, Cancel, or a prompt in another language) dismissAlert tapped the top action. Accept now names "Allow…", "OK", "Continue", "Open" and "Yes"; dismiss names "Don't Allow" (either apostrophe), "Ask App Not to Track", "Cancel", "Not Now", "Close", "No" and "Dismiss". An alert with none of them still falls back to WDA's positional pick.
  • DeviceLab iOS: launchApp with stopApp: false brings a running app to the front instead of restarting it, as Maestro does. The simulator launch always passed --terminate-running-process, and on a real iPhone XCTest's launch restarts the app, so the app started fresh and a flow checking what survives a relaunch tested nothing. A running app now comes to the front as it was (its launch arguments do not apply, as in Maestro); an app that is not running is launched as before. The old DeviceLab iOS driver (--driver devicelab-legacy) had the same gap on simulators and is fixed too.
  • iOS 27: hideKeyboard is no longer left with nowhere to start its drag by the floating bar container (WDA, DeviceLab iOS). iOS 27 reports the container as a screen-sized toolbar, and every point under it counted as a control.
  • iOS: a tap no longer presses a key when the software keyboard covers its target (WDA, DeviceLab, DeviceLab legacy). XCUITest reports an element under the keyboard as on screen, so a tapOn on a button below a focused field touched the key above it and the step passed; Maestro does the same. After a step that types, or a tap on a text field, a tap first checks whether the keyboard covers the point it aims at. If it does, the driver hides the keyboard as hideKeyboard does and finds the element again; when the keyboard will not go it taps the part of the element above the keyboard, and when none of it shows the step fails with "… is under the keyboard" instead of pressing a key. A key, or the keyboard's own return button, is tapped where it is. This covers tapOn, doubleTapOn and longPressOn aimed at an element's centre; a point: inside the element is left as given. A tap before anything has been typed asks nothing extra, so a keyboard the app raises by itself (a field focused on screen entry) is not caught until the flow types.
  • maestro-runner wda update finds WebDriverAgent releases again. WebDriverAgent moved to the appium/appium-ios repository at 16.16.0, and the old appium/WebDriverAgent repository stopped at 16.15.3, so wda update and wda update --check reported 16.15.3 as the latest version and wda update 16.16.0 or later failed with HTTP 404. The latest version is now the highest appium-webdriveragent@X.Y.Z tag of appium/appium-ios (the old repository's latest release if that lookup fails), and a version from 16.16.0 is downloaded from appium/appium-ios, installing only its packages/webdriveragent folder; earlier versions still come from appium/WebDriverAgent. The installed WebDriverAgent is now replaced only after the download has been extracted and holds WebDriverAgent.xcodeproj, so a failed update leaves it in place.
  • assertScreenshot against a baseline of another size now says why it failed. A whole-screen baseline recorded on another device, simulator or display scale failed with only screenshot size mismatch: expected WxH, actual WxH; the error now names that cause and the fix (re-record on this device with --update-screenshots, or keep one baseline per device). The comparison is unchanged: a size mismatch still fails, as in Maestro.
  • WDA: when launchApp with stopApp: false brings a backgrounded app back, the next tap reaches the app. Right after the switch back the app can already be idle while SpringBoard is still moving it to the front, and a tap sent then was reported as done but never arrived; an assertVisible in between passed on the app's tree and skipped the settle before the tap. When the app was in the background, launchApp now waits for the screen to stop changing, up to 2 s. An app that was not running, or already in front, waits for nothing, and a launchApp that stops the app first (the default) is unchanged.
  • Old DeviceLab iOS driver (--driver devicelab-legacy): bringing a backgrounded app back waits, up to 2 s, for SpringBoard to go idle before the next step, for the same lost tap. Activating an app that was not in the background waits for nothing.
  • runScript honours when:, as Maestro's does. The condition was parsed and ignored, so a script guarded by when: true: ${MAESTRO_CI == 'true'} ran on every run. An unmet condition now skips the script (Skipped (when condition not met)), at the top level and inside runFlow, repeat and retry, and a skipped script leaves the interaction clock alone, as a skipped runFlow does. (#238)
  • Appium on iOS: selected: true matches a selected tab or segment. XCUITest's page source has no selected attribute for them; it says so in traits="Selected, Button", which is now read, so a check on a tab bar no longer waits out its timeout. (#245)
  • Relative selectors honour focused, enabled, selected and checked when they have no text or id (DeviceLab Android, UIAutomator2, Appium, WDA). childOf: {id: field}, focused: true dropped the state and took the deepest element inside the anchor, so on a Compose text field (the test tag on a wrapper, the focused EditText inside) copyTextFrom copied an empty child view instead of the typed text. The state now narrows the candidates as text and id do, as in Maestro. (#240)
  • copyTextFrom no longer writes the device clipboard on Android (DeviceLab Android, UIAutomator2). Writing it could put the system's clipboard UI over the app, and the next tapOn landed on that instead. The copied text is kept by the runner for ${maestro.copiedText} and pasteText, as Maestro keeps it and as the Appium and iOS drivers already did. setClipboard still writes the device clipboard. (#239)
  • WDA: selected: matches a selected tab or segment when the element is found through the page source. WDA's page source marks selection only in traits="Selected, …", which was not read, so there every element counted as unselected: selected: true never matched and selected: false also matched the selected tab. This hit lookups that read the page source: relative selectors (below:, childOf: and the like), index: above 0, width:/height:, checked:, id: with text:, regex text in a tap, MAESTRO_WDA_HOSTED_FRAMES=1, and the fallback when WDA's own query found nothing. The same fix for Appium on iOS shipped in #245.
  • WDA and the old DeviceLab iOS driver start on simulators again with Xcode before 26 (Xcode 16 and older). Since 1.1.16 the simulator -destination carried arch=, which xcodebuild before Xcode 26 rejects, so every simulator run on such a host failed to start; build images that pin 1.1.15 for Xcode 16, as Expo EAS does, can use current releases. The arch= pin is now added only on Xcode 26 and later, whose resolver lists a simulator under both arm64 and x86_64, and when the Xcode version cannot be read.
  • A launchApp inside retry, repeat or a runFlow else branch now sets up the iOS session's permission alerts. Before the session starts, the runner looks for the flow's first launchApp, and it looked only inside runFlow, so a flow whose only launchApp sat in a retry got no alert handling. A flow file named inside a subflow file is also found next to that file, as it is when the flow runs. On WDA this turns on the session's alert monitor for those flows, as a top-level launchApp does.
  • WDA no longer taps the app's own alerts. With permissions resolving to allow (the default all: allow included), WDA's alert monitor answered the first alert of any app, the app under test too: it tapped the "OK" of an in-app alert such as "Invalid Credentials" about 2 s after it appeared, so a flow asserting that alert and then tapping "OK" failed about half the time. It also tapped an alert's last button when no button matched. The monitor now taps only a button whose label starts with "Allow" (or, for all: deny, "Don't Allow…" and "Ask App Not to Track"), and leaves every other alert to the flow, as Maestro does. A permission prompt answered "OK" (Face ID on a device) or "Continue" (contacts, iOS 18 and later) is no longer accepted for you; use acceptAlert or tapOn there.
  • WDA: a long press with no duration holds 3 s, as Maestro's does on iOS (longPressOn, and tapOn or tapOnPoint with longPress). It held 1 s.
  • WDA: copyTextFrom no longer copies an empty string when reading the text fails. It reads the text again, then takes it from the page source when an element there has the bounds WDA reported, and fails the step otherwise. A page-source match copies the value, else the placeholder, else the label, as Maestro does; it copied only the label. That text is also what report.json and the HTML report show for an element found through the page source, so a text field there now shows its value instead of its label.
  • --wait-for-idle-timeout 0 turns off the wait for the screen to settle before an action, on WDA and DeviceLab iOS (#212). Since 1.1.28 an action after a tap, swipe or typing waits until the screen stops changing, up to 3 s, as Maestro's iOS driver does. On a screen that never stops animating (a looping Rive or Lottie view) every action paid the full 3 s, and 0, documented as "disabled", only turned off XCTest's own idle wait. Any other value keeps the settle, as before.
  • DeviceLab iOS puts the simulator's Reduce Motion and password AutoFill settings back when the run ends (#214). The driver turns Reduce Motion on and AutoFill off for the run, and left them that way. A setting that was already at the run's value is not touched. DEVICELAB_IOS_SIM_PREFS=0 still skips both.
  • The warning for retyped text no longer prints the text. When characters went missing while typing, the warning showed the field's value and the typed text, which could be a password. It now gives character counts.
  • A secret typed with inputText shows as *** in step names, step messages and errors. When the text typed holds the value of a variable named like a secret (a word of the name is password, passwd, pass, secret, token, apikey, key, pin, otp or credential, any case, so API_KEY, userPassword and PIN count and MONKEY does not), that value is replaced with ***. A step name filled in a password set in the flow's env:, and a failed inputText could carry the typed text in its error, into report.json, the HTML, JUnit and Allure reports, the console and maestro-runner.log. The same applies to pasteText and setClipboard, and to steps inside runFlow, repeat and retry. The text typed is unchanged, values shorter than 3 characters are not masked, and other text in reports is unchanged. Variables from -e, --env-file and the shell already stayed ${NAME} in step names.
  • scrollUntilVisible scrolls in a row that runs past its scroll container's edge (DeviceLab Android, UIAutomator2, Appium on Android). The hierarchy doesn't always clip a child to its scroll container: a bottom sheet's last row was reported running past the sheet's list, under the gesture bar. Measured against the screen it was fully visible, so the scroll stopped and the tap hit the gesture bar. A match past its container's leading edge now keeps the scroll going; one the list can't move is accepted after a single scroll, as with #164.
  • launchApp: clearState no longer loses its launch to Android's own cleanup (DeviceLab Android, UIAutomator2). Clearing an app removes its task, and removing a task kills every process the app has. When the app's screens were still showing, that kill came about a second later (Destroy timeout of remove-task in logcat); when another app's activity, such as the permission dialog, sat in its task, it came after the clear had returned. Either way a launch right after the clear could be killed, and the flow waited on a blank screen. The app is now stopped, and its screens waited out, before it is cleared, and its task waited out after, by launchApp: clearState and by clearState. This adds a few dumpsys calls to each clear, usually a few hundred milliseconds; an app that won't let go of its screens or task costs up to 5 s and a warning. Appium isn't covered: it clears through mobile: clearApp, without a shell.
  • WDA: clearState on a real iPhone falls back to go-ios when devicectl is unavailable or fails. It uninstalled and reinstalled the app with xcrun devicectl alone, so it failed wherever devicectl was missing or refused. clearState and the first install now share one installer: devicectl first, then go-ios over usbmuxd. MAESTRO_RUNNER_IOS_INSTALLER applies to both, every install and uninstall has a time limit, and clearState logs which tool it used. A go-ios install that times out now closes its connection instead of leaving it open.
  • A character XML forbids in an app's text no longer breaks every lookup on the screen (UIAutomator2, Appium). One U+0000 or control character in any label made the page source unparseable, so every selector on that screen failed. Such characters now read as U+FFFD, which no selector text matches by accident.
  • The update notice appears only when the latest release is newer than the one running. It announced any latest version that differed from the running one, so a build newer than the latest release, a source build such as v1.1.29-22-gf72df97 among them, was told to "update" to an older one. A build made after a release tag now counts as newer than that release.
  • WDA: a failed session no longer holds xcodebuild for ten minutes collecting diagnostics. On Xcode 26 and later, xcodebuild could run simctl diagnose after a failure; WDA now starts with -collect-test-diagnostics never on Xcode 15 and later, as DeviceLab iOS does.
  • The Appium server URL is shown without its credentials. The console and maestro-runner.log printed --appium-url as given, so a Sauce Labs URL showed the account's access key; they now show it without the user, password and query. The --appium-session-file keeps the full URL, since tools attach to the session with it.
  • A simulator the runner boots opens in DeviceHub on Xcode 27. Xcode 27 has no Simulator.app, so open -a Simulator opened nothing. Where the selected Xcode has no Simulator.app, the runner opens DeviceHub (com.apple.dt.Devices); older Xcode still opens Simulator. The run continues if neither opens.
  • Sending the usage event takes less of the end of each run. It took about 0.45 s, most of it opening the connection; test now opens it in the background when the run starts, and the event, unchanged, is sent over it at the end. A connection the server has closed in between is replaced. Nothing is opened when telemetry is off, and behind a proxy the send works as before. A harness that starts one process per flow paid this per flow.
  • DeviceLab Android: a window that shows nothing no longer refuses a tap as "covered by system window", and a refusal names the window. A tap was refused while any window layered above the app covered its point; on RNTester one refused taps for 17 s while the screen showed no window at all. The agent now ignores a window above the app whose bounds are empty, that is on another display, or that has no visible content (no readable root, a root not visible to the user, or a root with empty bounds), and a window with content covers only as far as that content. The keyboard still counts over its whole window. The refusal in the log now gives the window's type, title, package, bounds and layer, and the agent logs a window it ignored at the tap point with the reason.
  • WDA: inputText into the focused field reads the field back and retypes dropped characters, as inputText with a selector already did. Tapping a field and then typing is how most flows type, and a dropped character went unnoticed. The check is skipped when the text has a newline or a tab, when focus has moved to another field (code boxes, split date or card fields), and when the field gained a character that was not typed, as a mask or a formatter adds one; a field is retyped only when it holds what was typed with characters missing.
  • DeviceLab iOS: the fast clearState (DEVICELAB_IOS_FAST_CLEARSTATE=1) clears the app's UserDefaults on a simulator. It emptied the app's data container, plist included, but the simulator's preferences daemon (cfprefsd) kept the app's preferences in memory and served them to the next launch, so a flow that relied on the clear still saw its saved settings. The clear now restarts that daemon, after stopping the app and before emptying the container, so nothing it still held is written back; it costs one simctl spawn call. The daemon serves the whole simulator, so a preference another app had not yet written to disk is lost. A clear that cannot restart it now fails instead of leaving the values in place. The same applies to --driver devicelab-legacy. The default clearState, which reinstalls the app, and real devices are unchanged.
  • --driver devicelab-legacy on iOS: a screen with a wide slider no longer wedges the runner. A slider at least 160 pt wide (and 1.75× its height) counted as a collapsed tab bar, so every snapshot of that screen read each element of the app one attribute at a time on XCTest's main thread, which cannot be interrupted; on a crowded screen that could hold the runner long enough for it to be restarted as wedged (found and fixed in agent-device, whose runner ours derives from). Sliders no longer count, and a real collapsed tab bar or segmented control is read in one query, started only while the snapshot's 8 s budget still covers what reading the screen took; otherwise the snapshot is returned without those tabs and marked truncated.

Contributors

@luka-vladika-happening-xyz

  1. runScript honours when: (#238)
  2. copyTextFrom keeps the text and leaves the device clipboard alone (#239)
  3. Relative selectors honour focused, enabled, selected and checked without text or id (#240)
  4. childOf, below and the other relative selectors keep their anchor's index (#241)
  5. Appium launchApp passes arguments and environment on iOS; clearState on a real iPhone reinstalls --app-file (#244)
  6. Appium on iOS: selected: true matches a selected tab or segment (#245)
  7. inputText retype: false (#247)
  8. A ${…} whose JavaScript throws fails its step (#250)
  9. WDA launchApp right after a reinstall waits for iOS to register the app (#251, #257)
  10. Asked for pausing a flow at its failing step (#246)

@bulatgaleev

  1. The pre-session scan finds a launchApp in retry and repeat (#216)
  2. WDA clearState on a real device without Xcode (#217)
  3. WDA long press holds 3 s (#218)
  4. WDA reads the focused field back after typing (#219)
  5. WDA copyTextFrom never copies an empty string for a failed read (#220)
  6. Hosted-frame correction (MAESTRO_WDA_HOSTED_FRAMES) (#223, #232)
  7. --wda-url for a WebDriverAgent already running (#224, #229, #233)
  8. CI on Linux and cancellation tests (#215, #222)

@tSquaredd

  1. launchApp: clearState no longer loses its launch to Android's cleanup (#225)
  2. scrollUntilVisible scrolls in a row that runs past its container (#227)

@Nezz

  1. Reported WDA slow since 1.1.28 (#212), asked for WDA reuse (#213), reported simulator settings left changed (#214) and permission prompts WDA could not see (#248)

@georgetarazi-swipejobs

  1. Asked for airplane mode on DeviceLab iOS (#252) and script-relative require() (#253), reported hideKeyboard leaving the keyboard up (#259)

@JohnAtTimeless

  1. Reported hidden web view content on DeviceLab iOS (#211)

@debugholic

  1. Reported index: picking a different element than Maestro (#254)

@wanglinyu12345-lab

  1. Reported the screen size not following a rotation on Android (#256)

@losey-kim

  1. Reported DeviceLab Android tapOn ignoring index: (#260)

@laiskajoonas

  1. Reported a clipped sliver counted as visible by scrollUntilVisible (#164)

Installation

Quick Install

curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash

Install a specific version

curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash -s -- --version 1.1.31

Manual install (from the assets below)

# 1. Download the tarball for your platform.
# 2. Extract:
tar -xzf maestro-runner-1.1.31-darwin-arm64.tar.gz

# 3. Run setup (installs into ~/.maestro-runner/bin/):
cd maestro-runner && ./setup.sh

Verify Installation

maestro-runner --version

Platform Support

  • macOS Intel (amd64) - Signed & Notarized
  • macOS Apple Silicon (arm64) - Signed & Notarized
  • Linux amd64
  • Linux arm64

Built by DeviceLab.dev