v1.1.25
Release 1.1.25
This release is about the first five minutes and the oldest complaints: a doctor that says exactly what your machine is missing, a devices listing, a screenshot command, runShell for the one adb call every suite eventually needs, and an inputText that checks what actually landed in the field. It is also about gestures that behave like a user's finger: a real dragAndDrop on every driver — press, hold until the item lifts, move slowly, settle, release — and a scrollUntilVisible that stops when the element is actually visible instead of one pixel in. Flutter apps go from barely drivable to ahead of WDA on the DeviceLab iOS driver, --step-delay slows any flow down for demos and animation-heavy apps, and JUnit reports carry test-tracking properties and failure artifacts into CI.
Added
doctor— checks the toolchain and says what to do about each gap, with--jsonfor CI. A missing platform is a warning rather than an error, so it gates a Linux runner without failing it for having no Xcode; only a broken install or a team ID Xcode does not have exits non-zero. Three checks earn their place: Command-Line-Tools-instead-of-full-Xcode, which otherwise surfaces far from its cause; an AVD naming a system image that is not installed, which nothing reports at run time and which shows up only as a driver polling for a device that will never boot; and--team-idchecked against the signing certificates in the login keychain, which is the entire content oferror: No Account for Team "..."at WDA build time.✗ Signing identity for team abcd123456 — no certificate belongs to team abcd123456; this Mac has: A3RCAA2YAX Pass one of the team IDs listed above as --team-id, or add that team's account in Xcode.devices— Android devices and emulators, iOS simulators and connected phones in one listing, with--jsonand--allfor the shut-down simulators. A phone that answers usbmux but refuses the lockdown handshake is listed as not-ready with the reason, instead of looking identical to a working one.screenshot— capture the current screen with the same device and driver flags a run takes. Writes a file, or-for stdout.runShell— run a host command from a flow, for the adb, simctl or xcrun call every suite eventually needs.- runShell: command: adb -s $MAESTRO_DEVICE_ID shell getprop ro.build.version.sdk output: SDK
output:binds the command's trimmed output to a flow variable for later steps. The environment carries the flow's variables, the step's ownenv:, andMAESTRO_DEVICE_ID/MAESTRO_PLATFORM/MAESTRO_APP_ID— the device id being the one that matters, since a bareadb shellfails outright when--parallelhas two devices attached. Bounded at 30s by default (timeout:to change it), output capped at 64KB keeping the tail, and a non-zero exit fails the step unless it isoptional: true.scrollUntilVisiblecan scroll inside a container —from:names the scrollable element, for a screen whose scrolling part is an inner list or a horizontal carousel rather than the screen itself. The gesture is centred in the container and inset at each end, so a swipe starting on its edge is not claimed by the parent list instead. UIAutomator2 for now; the other drivers refusefrom:by name rather than silently scrolling the screen.- scrollUntilVisible: element: id: "product-item-Playwright" from: id: "products-list"
--video never|always|on-failure— keep screen recordings only for the runs worth watching.--recordstill meansalways. A recording cannot be started retroactively, soon-failurerecords every flow and discards the passing ones at the end. The vocabulary matches--artifactsrather than inventing a second spelling for the same idea.lint --json—{checked, failed, results[]}on stdout, for editors, CI and anything generating flows that wants to check its own output before it costs a device.dragAndDrop— long-press an element (or point) and drag it to another, the way reorder UIs expect.from:/to:each take a selector or apoint:;holdDuration(ms, default 1000) is the press before movement,duration(ms, default 1000) the movement itself. Works on every driver: UIAutomator2 (W3C actions with precise hold and paced moves), DeviceLab Android (input draganddrop, Android 12+ — hold length comes from the system long-press timeout), WDA (press(forDuration:thenDragTo:)— XCUITest paces the move itself), DeviceLab iOS (runner drag with a new backward-compatiblemoveDurationMsfield), web (paced CDP mouse sequence), and Appium (W3C actions).Heads-up for web: pages using native HTML5- dragAndDrop: from: id: "item-3" to: point: "50%, 20%" holdDuration: 800
draggableignore synthetic mouse events by design — mouse/touch/pointer-event drag implementations work.--step-delay <ms>— pace a pause between top-level steps, for demos and for apps whose animations outrun the assertions. AlsoMAESTRO_STEP_DELAYin the environment, and per-flow override withstepDelay:in the flow config.- Flow
properties:in JUnit reports — a flow'sproperties:map (Maestro-compatible syntax) now lands as<property>entries on its JUnit testcase, next to the standard file and device properties — so flows can carry test-tracking ids into CI (#84).properties: testID: Test-1234
- Faster parallel runs — the work queue is filled longest-flow-first using durations from the previous run, instead of file order. A run cannot finish before its longest remaining flow does, so one slow flow late in the alphabet used to set the wall clock for every worker. Flows with no recorded time are weighted as the median. Best-effort: no previous run, or an unreadable report, falls back to file order.
- A mid-flow app death is explained rather than blamed on the selector (Android) — when the app under test dies, the runner now asks the platform why instead of reporting a bare "element not found". That covers the two cases logcat cannot see: a low-memory kill writes nothing at all, and a system resource kill writes nothing useful. The DeviceLab driver had no crash detection whatsoever and is now level with UIAutomator2.
- Failure artifacts in JUnit reports — failed testcases attach the failing step's screenshot, and any
--recordvideo, via the[[ATTACHMENT|path]]convention Jenkins-style tooling reads; paths are relative to the report directory. A green run attaches only the video. - Negation globs in
config.yaml— aflows:pattern prefixed with!subtracts the files it would otherwise have selected, so a workspace can take everything and carve out the fixtures. Exclusions reuse the inclusion matcher, so!a/**excludes exactly whata/**would have included.flows: - "**" - "!fixtures/**"
- DeviceLab iOS targets the app that's actually on screen — a command with no
appIdused to activate the runner's own placeholder host app, hijacking whatever you were looking at with a blank screen, because XCUITest's public API can't address "the app in front". It now resolves the frontmost application through the active-application PIDs and targets it with no activation at all, falling back to the host app (and logging which guard missed) when any step is unavailable. - The DeviceLab iOS runner source ships inside the Go module — library consumers previously built the runner from whatever was in
~/.maestro-runner/drivers/ios, a hidden runtime dependency and a protocol-skew hazard where a pinned Go client could drive runner sources from a different version. The runner a consumer runs is now version-locked to the module it compiled against, with no maestro-runner installation needed. The CLI keeps using the installed directory — identical content, shared build cache.
Fixed
-
inputTextreported success without checking that the text arrived — nothing read the field back, so a character lost to a janky frame was indistinguishable from a clean run, and the flow failed several steps later on an assertion that had nothing to do with the cause. Every Android path now reads the field after typing and, when characters are missing, clears and types once more; a dropped keystroke is a timing accident, and the second attempt almost always succeeds. Three rules keep it from causing more harm than it prevents: the check is a suffix rather than an equality, because mobile text entry appends to whatever the field held; a reading that did not change is treated as telling us nothing, because some drivers report an empty field's hint, so "Username" comes back whether or not a name was typed; and a value at least as long as what was typed has been reformatted by the app — a phone mask, an autocomplete, a secure field reporting bullets — so those are recognised and left alone. A field that still disagrees after the retry is reported in the step result rather than failed. Measured on a Pixel 4a at roughly 75ms per field. Not yet wired on WDA or DeviceLab iOS. -
Two device errors described something other than what had happened — on iOS, with nothing booted and no device named, an empty device list was reported as a code-signing error demanding
--team-id, because the "is this a simulator?" check correctly answers no when there is no simulator. An empty device list now says so, before signing is considered at all. On Android, the parallel path reported "no available Android devices found" whether nothing was attached or every attached device was already being driven by another maestro-runner; those have completely different fixes, and offline or unauthorized devices are now counted separately again. Both point atmaestro-runner devicesfor what the machine can actually see. -
scrollUntilVisiblestopped on elements it shouldn't have — every driver accepted any 1px viewport overlap (or mere presence) as "visible", and the documentedvisibilityPercentageknob was parsed but wired to nothing. The stop criterion is now a real visibility check on every driver: fully visible by default, honoringvisibilityPercentagewhen set. Heads-up: flows that relied on a sliver of the element counting as visible may scroll one step further now. On DeviceLab iOS, frames that arrive pre-clipped to the viewport (Flutter semantics especially) additionally need to hold still across one extra scroll before being accepted, so a 12pt sliver of an 80pt row can't masquerade as fully visible. -
Flutter apps were undrivable on the DeviceLab iOS driver — four stacked causes, each fixed: the Flutter VM fallback was wired only into the WDA path; the driver implemented neither the coordinate-tap step the fallback delivers taps through nor
tapOn: point:; the fallback's short find windows leaked into the runner's HTTP timeout and cut XCUITest calls off mid-flight; and scroll gestures released mid-motion, so iOS spent the next tap cancelling residual deceleration instead of activating anything — a silent no-op. Scrolls now hold still 250ms before lifting, the same dead-stop lesson the Android agent swipes learned. On the Flutter issue-repro suite the driver goes from 9/19 to 17/19 — ahead of WDA-with-fallback at 14/19; the two remaining failures reproduce open Flutter framework bugs and fail on every driver. -
assertVisibleon the Appium driver asserted presence, not visibility — it returned success as soon as the element was findable, andassertNotVisiblerequired it to be unfindable, so a present-but-hidden element passed the first and failed the second. Both now check the displayed state, which was already being fetched and discarded. Heads-up: a flow that passed on a hidden element will now fail — that is the intent, but it is a behaviour change. Android only: XCUITest reports elements as not displayed that are plainly on screen, so the check is not applied there. -
The Appium driver granted 32 permissions the app never asked for —
launchAppwalked a hardcoded list and issued onepm grantshell call per entry, ignoring the failures that most of them produced, since granting an undeclared permission raises a SecurityException. It now reads what the manifest declares and grants that list in a single call. On a Pixel 4a that is 33 requests and 2.0s down to 3 requests and 0.3s per launch, and it works on hosts that disable theadb_shellinsecure feature, which the old approach silently did not. -
The Appium driver ran every selector strategy against elements that were not there — finding by text tried six UiAutomator strategies in order, and while polling for an element that had not appeared yet all six missed, every cycle. Measured on a device, 22 of 30 finds in one flow were misses costing more than the successful finds. The case-insensitive forms are supersets of the rest, so two probes now decide whether anything matches before the specific strategies run.
-
The Appium driver fetched element properties nothing read — describing an element cost five round trips, of which
enabledwas never consumed and the accessibility description was wanted by one command out of nineteen. Now three, with the description fetched on demand. -
point:was silently ignored ondoubleTapOnandlongPressOnby the Appium and web drivers, which tapped the element centre instead. Every driver now honours it, so a knob that worked on four drivers and was quietly dropped by two behaves the same everywhere. Fixing the web path surfaced a second dead option: web long press hardcoded a one-second hold and droppedduration:entirely. -
assertScreenshotcompared whatever frame arrived — capturing mid-animation seeds a baseline nothing will match again, and the failure that follows looks exactly like a real visual regression. It now re-captures until two consecutive frames agree, the same settle the drivers already used forwaitForAnimationToEnd. A screen that never settles (a spinner, a video, a caret) falls through to the last frame rather than hanging. -
DeviceLab iOS rejected clipped frames by scrolling again — iOS clamps a descendant's frame to its scroll container's visible bounds, so a sliver of a row far past the fold reads as fully visible and the tap that follows does nothing. Rather than spending an extra scroll to see whether a suspicious rect moves, the driver now checks the parent chain: a descendant claiming to be on screen beneath an ancestor that is wholly off it is a contradiction, found in one pass.
-
Web flow-header variables reached the browser unexpanded — a flow declaring
url: ${BASE_URL}sent the literal${BASE_URL}to Chromium, which rejected it as an invalid URL, so--platform webcould not be driven from the environment at all. The header is now expanded through the same script engine, in the same precedence order, that expands steps — so-e,--env-fileand workspace config all reach the initial navigation (#145). The header is now expanded once centrally, so Android and iOS stop silently dropping the app version from reports when a flow usesappId: ${VAR}, and an unset variable is named at startup instead of surfacing later as a bare "no URL specified". -
A dead DeviceLab iOS runner left no trail — mid-session runner deaths surfaced only as
connection refused:runner.logwas truncated on every start, the runner exited on the first listener failure, and nothing distinguished a requested shutdown from a spontaneous one. The listener now rebinds on failure (5 retries, 1s backoff) rather than exiting, since simulator network daemons crash-loop on some Xcode/runtime combinations; every deliberate exit logs its reason;runner.logrotates through three generations so the log explaining a death survives the burst of failed restarts that follows it; and anxcodebuildthat exits mid-session without a Stop being requested now says so on stderr.
Contributors
- Asked why
assertVisibleneeded five element requests, which led to the Appium driver making roughly 38% fewer calls per flow
- Requested custom properties in JUnit report files (#84)
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.25Manual install (from the assets below)
# 1. Download the tarball for your platform.
# 2. Extract:
tar -xzf maestro-runner-1.1.25-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 (arm64) - Signed & Notarized
- Linux amd64
- Linux arm64
Built by DeviceLab.dev