Repository navigation
fix(flow): don't split a flow with inline or conditional runFlow steps into a suite - #208
Merged
omnarayan merged 3 commits intoOct 8, 2026
Conversation
…s into a suite
IsSuite treated any flow made only of runFlow steps, two or more of them
with a file, as a suite, and expandSuites ran each file as its own flow.
Inline runFlow blocks (when: + commands:) were dropped without a word, and
a file's when: was ignored, so the file ran whatever the condition said.
A reset flow ("open the screen; when a menu shows, close it; when items
are selected, clear them") ran only its file steps and passed.
A runFlow with inline commands, a when condition or an else branch is part
of the flow, so a flow that has one is no longer a suite. Flows made only
of unconditional file runFlows are still expanded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Thanks @MxScripter. A flow silently losing its inline and conditional steps is a nasty one to debug, especially when the failure shows up tests later as "Queues not found". Treating any inline, On your note about plain file-only suites differing from Maestro ( |
…th CHANGELOG entries)
…th CHANGELOG entries)
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
markharding
pushed a commit
to futo-org/Harbor
that referenced
this pull request
Oct 15, 2026
This PR contains the following updates:
| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [maestro-runner](https://open.devicelab.dev/maestro-runner) ([source](https://github.com/devicelab-dev/maestro-runner)) | [`1.1.25` → `1.1.28`](https://renovatebot.com/diffs/npm/maestro-runner/1.1.25/1.1.28) |  |  |
---
> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/99) for more information.
---
### Release Notes
<details>
<summary>devicelab-dev/maestro-runner (maestro-runner)</summary>
### [`v1.1.28`](https://github.com/devicelab-dev/maestro-runner/blob/HEAD/CHANGELOG.md#1129---2026-10-15)
[Compare Source](https://github.com/devicelab-dev/maestro-runner/compare/v1.1.27...v1.1.28)
This release adds **`--retries`**, which runs flaky flows again in the same run and marks them as flaky in JUnit and HTML. It also runs **Maestro's `test` command line unchanged**, so CI scripts written for Maestro work as they are. Most of the rest closes gaps with Maestro found by running real suites side by side (DuckDuckGo, RNTester, React Navigation, Expo, pager-view, react-native-enriched). Contributors sent a large share of it, as 17 pull requests and a stack of precise bug reports, credited at the end. The DeviceLab Android agent taps faster and recovers from a dropped connection, and the DeviceLab iOS agent reads screens deeper than 62 levels. `test` now sends one anonymous usage event per run; what it holds and how to turn it off are below and on the [telemetry page](https://open.devicelab.dev/maestro-runner/docs/telemetry).
##### ⚠️ Behaviour changes
**`inputText` types at the cursor into a field that already holds text, as Maestro does (DeviceLab Android).** It used to replace the field's whole value, so a rich-text editor typed "1", Enter, "2" kept only "2". An empty field is still set in one call. To replace a field's text, use `eraseText` first, as with Maestro ([#​189](https://github.com/devicelab-dev/maestro-runner/issues/189)).
**`runScript` runs every script as written.** `${...}` in a script is a JavaScript template literal, as in Maestro, and flow variables reach the script as plain names. The runner used to expand `${...}` as flow variables first, so Expo's `${SERVER_URL}/process` became `/process`.
**An undefined `${name}` expands to `undefined`, and `null` to `null`, as in Maestro.** Both used to expand to an empty string.
**`travel` reads `speed` in metres per second (default 4), as Maestro does, and walks each leg in 50 steps.** It read km/h and paused `3600/speed` whole seconds at each point whatever the distance: 72 s a point by default, and none at all above 3600. Leg length is computed as Maestro computes it, so flows keep the timing they were tuned for.
**DeviceLab Android `hideKeyboard` presses Back when ESC does not close the keyboard.** Samsung's keyboard and the AOSP keyboard ignore ESC, so the step passed with the keyboard still open. One Back is sent only while the keyboard is confirmed up, which the keyboard consumes; with no keyboard up nothing is sent, so the step still cannot navigate away ([#​42](https://github.com/devicelab-dev/maestro-runner/issues/42)). This is what the UIAutomator2 driver does. It is still not Maestro's unconditional Back: a flow that relied on `hideKeyboard` to leave a screen should use `back`.
**`test` sends one anonymous usage event at the end of each run.** Counts and versions only: runner version, platform, driver, device kind, flow and step counts, failure kinds and duration. No machine or user id, no flow or file names, selectors, app ids, URLs, environment values or error messages, and nothing is written on your machine; projects are told apart by a one-way hash of the list of flow files run. A local run prints a one-line notice. Turn it off with `MAESTRO_RUNNER_NO_TELEMETRY=1` or `DO_NOT_TRACK=1`; `MAESTRO_RUNNER_TELEMETRY_DEBUG=1` prints the event instead of sending it. The update check is separate and unchanged.
##### Added
- **`--retries N` runs failing flows again in the same run**, round by round, up to N more times, with the driver and the on-device agent kept up between rounds; works with `--parallel`. Each earlier attempt is kept as `flows/<id>.attempt-<n>.json` with its assets; `report.json` fills `attempts` and `attemptHistory`; the console says "passed on attempt 2" or "failed all 3 attempts". JUnit gets an `attempts` property and Surefire's `<flakyFailure>` / `<rerunFailure>`, which CI report viewers show as flaky, and the HTML report badges the flow.
- **Maestro's `test` command line runs unchanged.** Options may follow the flow paths, and global options may follow `test`. `--format junit|html` writes that report to `--output <file>` (default `report.xml` / `report.html`), with the full reports in `--test-output-dir` or `--debug-output`; without `--format`, `--output` is the reports directory as before. `--flatten-debug-output`, `--shard-split` / `--shards` / `-s`, `--screen-size` and `--apple-team-id` map to ours; `--reinstall-driver`, `--no-reinstall-driver`, `--headless`, `--analyze`, `--api-key`, `--api-url`, `--test-suite-name`, `--host`, `--port` and `--driver-host-port` are accepted and ignored; `--shard-all` is refused with a pointer to `--parallel`.
- **`hierarchy` works while a test runs on the device, and prints Maestro's format.** An Android test run answers hierarchy and screenshot requests for its device through the driver it already has, so tools that call `maestro hierarchy` mid-flow (Expo's image comparison) work. `--format maestro` prints Maestro's TreeNode JSON; it is the default when the binary runs as `maestro`, and `--format tree` keeps the normalized output.
- **`console.log` output is kept on the step that printed it.** Each step's report entry carries the lines its `runScript` / `evalScript` printed (a new optional `logs` field), shown in the HTML report's step details ([#​192](https://github.com/devicelab-dev/maestro-runner/issues/192)).
- **Step names show the values defined in the flow, never secrets.** `${NAME}` in a step name is filled in when the value comes from the flow files (`env:`, a `runFlow` / `retry` / `repeat` `env:`, `defineVariables`, a command's output). A value from outside the flow (`-e`, `--env-file`, workspace config, the shell), or one that carries such a value, stays `${NAME}`.
- **`travel` on every driver that can set the location**, including the iOS drivers, which had none.
- **DeviceLab Android honours `--typing-frequency` and `typingFrequency:`**: a pause of 1000/frequency ms after each character typed as key events. They were ignored.
- **DeviceLab Android: the hierarchy marks `password` and `editable` fields and gives a slider's or progress bar's `range-min`, `range-max` and `range-current`**, in the hierarchy each run saves and in the `hierarchy` command.
- **The run log names the DeviceLab Android agent build**, the commit it was built from, and warns when it was built from uncommitted source.
- **DeviceLab iOS finds elements on screens deeper than 62 levels.** A capture that reaches the depth limit re-reads each cut-off branch from where it was cut and joins the pieces, so deep React Native screens no longer lose everything below level 62. At most 12 extra requests and 4 s per capture; a screen that ends above the limit costs nothing extra. `DL_AGENT_DEEP_TREE=0` turns it off. WDA and Appium still stop at 62.
- **DeviceLab iOS saves stall diagnostics before restarting a stuck agent**: a 2 s stack sample of the agent and of each app on the simulator, the process list and a screenshot, in `<report dir>/diagnostics/stall-<n>-<time>/` (at most 3 per run).
##### Changed
- **DeviceLab Android taps are faster.** The agent keeps the element it found and watches only its bounds to settle, instead of finding it again and waiting for four identical whole-tree hashes: on a 2-core emulator, tap median 361 → 287 ms and p90 1268 → 695 ms.
- **DeviceLab Android typing waits once per character, not once per key event.** Each character still reaches the app before the next one starts, so OTP boxes and input masks see the keys one by one; 36 characters take 1.9 s instead of 2.3 s.
- **The Android agent exits after 10 minutes with no client.** A run killed outright (SIGKILL, a cancelled CI job) left it holding UiAutomation, the device's single accessibility automation session, and `uiautomator dump`, Appium and Maestro then failed on that device until someone stopped it by hand.
- **DeviceLab iOS keeps an agent started by `xcodebuild` for the next run**, as it already kept one started by `simctl launch`; a watcher stops it once its simulator shuts down. A harness running one process per flow on GitHub's macOS runners restarted the agent for every flow, about 14 s each. Why `simctl launch` failed is printed to the console.
- **iOS snapshot depth defaults to 62.** Above 62, XCTest returns no elements at all for a screen nested deeper than 62 levels, so such a screen came back empty on DeviceLab, WDA and Appium; `DL_AGENT_SNAPSHOT_MAX_DEPTH` or `MAESTRO_WDA_SNAPSHOT_MAX_DEPTH` override it ([#​171](https://github.com/devicelab-dev/maestro-runner/issues/171)).
##### Fixed
- **`inputText` with empty text types nothing and passes, as in Maestro (DeviceLab Android, UIAutomator2, WDA).** It failed with "No text to input", so a flow that types a multi-line value line by line failed on its first blank line. DeviceLab iOS already passed.
- **DeviceLab Android: a tap the agent never answered is not sent again.** A tap that timed out, or that a dropped connection cut off, may already have happened; the tap loop polled again and could tap twice. The step now fails at once and says the tap is not repeated.
- **DeviceLab Android: a text or id tap on an element partly under the keyboard taps the part that shows.** It failed with "keyboard is covering it" whenever the element's centre was under the keyboard, however much of it showed. The agent taps the clear point nearest the centre, under any window above the app (keyboard, system dialog, overlay), and the step fails only when less than 25 px of the element shows. Double taps, long presses, relative selectors and `point:` keep the centre check.
- **DeviceLab Android: `hideKeyboard` presses Back only for the focused app's keyboard.** Right after an app switch, the previous app's keyboard can still read as shown; Back then went to the newly launched app and closed it ([#​42](https://github.com/devicelab-dev/maestro-runner/issues/42)).
- **WebViews: the one on screen is driven, not the first one.** With several WebViews (a tab or back-stack screen kept alive beside the visible one), a hidden first page turned CDP off, and `css:` selectors failed.
- **DeviceLab iOS: React Native password fields are typed once.** React Native reports a password field's value as bullets, which never show the typed text, so the agent deleted what it typed, typed it again and reported it as not confirmed. Bullets now count as a hidden value, as for a native secure field.
- **DeviceLab Android: `eraseText` clears React inputs in a WebView.** It set the field's value past React's value tracker, so a controlled input kept its old value and showed it again on the next render.
- **A write to a WebView's or mobile browser's DevTools socket gives up after 10 s**, instead of holding the step forever when the device side stops reading.
- **Web: waiting for a page's load event after `launchApp`, `openLink` or `openBrowser` gives up after 30 s.** A page with a resource that never finished held the step with no end.
- **A step inside a `runFlow` that times out says what it waited for.** It printed only Go's "context deadline exceeded", in the console and the report; it now gives the same message as a top-level step, such as "Element 'Pay' not visible within 1m0s (cause: timed out)".
- **The screenshot and hierarchy saved for a failed step wait at most 10 s each.** With a hung agent each one waited out the driver's own timeouts, up to about 90 s per failing step.
- **`checked:` selectors work on DeviceLab Android.** The agent reported no checked state, so `checked: true` never matched a switch that was on and `checked: false` always did ([#​205](https://github.com/devicelab-dev/maestro-runner/issues/205)).
- **`setLocation` moves the device on Android.** Both Android drivers sent a broadcast nothing on the device receives: the step passed and the location never changed. DeviceLab puts test providers in place of gps, network and fused through its agent; UIAutomator2 starts the bundled Appium settings app's location service, installing the app on first use, with `am startservice` on Android 7 and older. The mock ends with the run. Coordinates out of range or not numbers fail the step.
- **Text selectors no longer match the soft keyboard's keys (DeviceLab Android).** With the keyboard up, `assertVisible: "q"` passed on the keyboard's key.
- **DeviceLab Android relative, `index:` and count lookups read every window.** They read only the active window, with a walk that can cut deep subtrees short, so `below:`, `index:` and counts missed elements in sheets, dialogs and popups that a plain text selector found.
- **DeviceLab Android types characters that have no key on the keyboard** (accents, CJK, emoji) by pasting them. They were dropped, and the runner then retyped the whole field by setting it.
- **A key the device refuses fails the step** instead of passing (DeviceLab Android).
- **DeviceLab Android typing keeps touch mode, and input no longer waits for running animations on Android 12+**, where a stuck animation cost about 10 s per tap or key.
- **An `index:` that is not a number fails the step, as in Maestro.** An `index:` of "undefined" (an unset `${VAR}`) or a typo silently meant the first match, so a tap hit the wrong element and passed. `"2.0"` is 2.
- **`scrollUntilVisible` with `speed` outside 0..100 is a parse error**, as in Maestro; it fell back to the default without a word.
- **`scrollUntilVisible` honours `centerElement` on DeviceLab Android, UIAutomator2, Appium and WDA.** It was parsed and ignored, so the scroll stopped with the element at the edge of the list. As in Maestro, scrolling continues, for up to 4 more scrolls, until the element's centre is near the middle.
- **Web: a malformed CSS selector fails at once** with the browser's error, instead of waiting out the timeout as "not found".
- **WDA: a wait that never could read the screen says so.** When every page-source read failed during an `extendedWaitUntil: visible`, the step said only that the element was not visible; it now adds that WDA could not read the screen, with WDA's error, and every WDA error is logged with its message, not only its status.
- **WDA: a screen size missed at startup is read again when needed.** A failed size read at startup left it at 0x0, and every `point:` tap, percentage swipe and scroll of the run failed with "screen dimensions not available" ([#​201](https://github.com/devicelab-dev/maestro-runner/issues/201)).
- **UIAutomator2: `pasteText` after `setClipboard` pastes its text.** It failed with a NullPointerException, and Android 10+ does not let a background app read the clipboard anyway. As in Maestro, `setClipboard` keeps its text for `pasteText`, which types it ([#​202](https://github.com/devicelab-dev/maestro-runner/issues/202)).
- **DeviceLab Android: a tap waits for the screen to change**, up to 300 ms when nothing changes (`MAESTRO_DEVICELAB_TAP_CHANGE_WAIT_MS`), and `waitForAnimationToEnd` no longer drops the settle after a tap. `copyTextFrom` after a tap and `waitForAnimationToEnd` could read a label mid-scroll.
- **DeviceLab Android: `openLink` right after a tap waits for the tap's change to settle**, so the next step no longer reads the page the link was replacing.
- **The taps of a `repeat:` go out back to back, `delay:` apart, as in Maestro.** DeviceLab and WDA settled before every tap after the first, so on a screen that never settles (a playing video) the taps landed seconds apart.
- **DeviceLab Android: an agent that drops its connection is restarted.** The call in flight waited out its 30 s timeout and every later call failed with "broken pipe", so one agent crash failed every remaining flow. Now the next call restarts the agent and recreates the session; read-only calls are sent again, actions never.
- **DeviceLab Android: `inputText` reads whether a field shows its hint from the agent**, instead of taking a snapshot of the whole tree, which could take the agent down in a rich-text editor.
- **DeviceLab iOS: a stuck kept agent is restarted, not re-attached to.** Re-attaching needs a small snapshot answered within 10 s; a call the agent never answers restarts it.
- **DeviceLab iOS: orientation and appearance are re-sent after an agent restart**, instead of failing the step.
- **DeviceLab iOS: rotating or changing appearance on a busy app no longer holds the agent.**
- **DeviceLab iOS: system sheets drawn by another process (HealthKit authorization, share sheet, photo picker) are tapped where they are drawn.** Their rows were reported 62 pt above where an iOS 27 simulator draws them, so taps missed ([#​191](https://github.com/devicelab-dev/maestro-runner/issues/191)).
- **DeviceLab iOS waits for a simulator to finish booting** (`simctl bootstatus`) before installing the app, as WDA already did; a simulator listed as Booted can take minutes more on a CI runner.
- **DeviceLab iOS: `addMedia` gives `simctl addmedia` a minute, then retries once**, and says simctl did not finish instead of "signal: killed".
- **DeviceLab iOS keeps the last 5 `xcodebuild` logs** across agent restarts (`xcodebuild.log.1` … `.5`).
- **A WebView whose DevTools never answers no longer hangs the run.** The whole CDP setup is bounded at 3 s; steps continue on native automation and the connection is retried later. This extends the 1.1.28 fix for [#​190](https://github.com/devicelab-dev/maestro-runner/issues/190), which bounded only the WebSocket handshake.
- **`--device` without `--platform` runs on the device's platform**, as in Maestro: an iOS simulator or connected iPhone id means iOS.
- **A tag filter that leaves no flows says what Maestro says** ("Include / Exclude tags did not match any Flows:"), which scripts look for.
- **JUnit testcases carry Maestro's `id`, `timestamp` and `status`** (SUCCESS, ERROR, …); harnesses that read `status` treated every testcase without it as failed.
- **A `runFlow`, `repeat` or `retry` nested in another one is reported**, steps and all; it was missing from the report and the summary.
- **Live step lines show a step's `label:`**, as the report and summary already did ([#​192](https://github.com/devicelab-dev/maestro-runner/issues/192)).
- **The end-of-run summary shows steps that never ran as skipped**, not with a red ✗.
- **The monkey launch fallback passes `--pct-syskeys 0`**, without which monkey refuses to start on an emulator with no hardware keyboard.
- **A flow run directly is no longer split into a "suite" when it has inline or conditional `runFlow` steps.** A flow made only of `runFlow` steps, two or more of them with a file, was expanded into one flow per file. Inline `runFlow` blocks (`when:` + `commands:`) were dropped without a word, and a file's `when:` was ignored, so the file ran whatever the condition said. A reset flow such as "open the screen, `when` a menu shows close it, `when` items are selected clear them" ran only its file steps and still passed. Such a flow is now run as written; flows made only of unconditional file `runFlow` steps are still suites.
- **DeviceLab iOS: `launchApp` with the default `all: allow` no longer resets the app's permissions on a simulator.** It reset every permission and then granted `all` with `simctl`, but simctl's contacts grant does not take (iOS 18.6 simulator) and it has no notifications service, so a grant made another way (the user's tap on the prompt, applesimutils, an earlier run) was lost on every launch and the app asked again: an app that shows a permission onboarding when a permission is undecided opened on it after every `launchApp`. An explicit `permissions:` map still resets first, so permissions it doesn't name stay undecided ([#​204](https://github.com/devicelab-dev/maestro-runner/issues/204)).
- **`childOf` and `containsChild` follow the element tree, as in Maestro, on DeviceLab, UIAutomator2, Appium and WDA.** They compared rectangles, so any element inside the anchor's bounds counted as its child. A list row behind an overlay that covers it (a search-results list drawn over the full list) "was in" the overlay, and `assertVisible: {id: title, childOf: {id: search_results}}` passed while the results list was empty. Both filters now walk the parent chain; an anchor without tree links (rebuilt from a relative selector) still uses bounds. The DeviceLab iOS driver already did this.
- **UIAutomator2 `hideKeyboard` no longer trusts a single "keyboard hidden" read.** While the keyboard is still coming up, `dumpsys window InputMethod` can report it not shown and Appium's hide_keyboard can answer 404, so the step returned without pressing BACK and the keyboard stayed over the next target. The driver now treats the keyboard as hidden only when two reads 300 ms apart agree, both before and after Appium's call, and still presses BACK only while the keyboard is shown.
- **WDA keeps enough idle connections for its own parallel reads.** The driver reads an element's name, rect, text and displayed at once, and a tap looks an element up four ways at once, but Go's default transport keeps two idle connections per host, so every burst closed two connections and opened two new ones. Through a forwarded port to a physical iPhone, new connections opened together fail with EOF and are sent again, which costs time on every step. The WDA client now keeps up to eight.
- **`retry` counts retries, not attempts, as Maestro does.** `maxRetries: 1` now runs the commands twice (once, then one retry), an unset `maxRetries` means one retry, and the value is capped at 3. The runner ran exactly `maxRetries` attempts, three when unset and with no cap, so `maxRetries: 1` never retried. A value that is not an integer is logged and read as 1 instead of failing the step.
- **WDA `launchApp` restarts a running app unless `stopApp: false`, as Maestro does.** It only activated the running app, so a relaunch left the app on the screen it was already on, and a flow checking what survives a restart restarted nothing.
- **WDA `notVisible` passes only when a lookup finds the element absent.** `assertNotVisible` and `extendedWaitUntil: notVisible` treated any failed lookup, such as an unreadable page source or a dropped connection, as the element being gone, so they could pass without the screen being looked at. Other errors are now retried until the timeout, and then fail the step.
- **`checked` selectors work on iOS.** The iOS drivers dropped `checked` with a warning, so `checked: true` matched a switch in either state. WDA now derives checked from a CheckBox, Switch or Toggle whose value is 1, as Maestro does, and filters on it on every path (tap, assert, relative).
- **`runScript` runs a script file as written, as Maestro does.** The runner expanded `${...}` across the whole file before running it, so a template literal that used the script's own variables, such as `${encodeURIComponent(email)}`, was replaced ahead of the script, against variables that did not exist yet, and came out as `undefined`. A script now runs as plain JavaScript, inline or from a file: `${...}` in it is a template literal, as in Maestro, and flow variables reach it as plain names.
- **A `runFlow`, `retry` or sub-flow `env` no longer leaves its keys behind, as in Maestro.** The runner put each key back to its old value, but a key that had none was set to an empty string instead of being removed, so after `runFlow` with `env: {KEY: ...}` the name stayed defined: `typeof KEY` was `"string"`, `$KEY` expanded to nothing, and `runShell` saw `KEY=""`. A key the block added is now removed when it ends.
- **A script's `http` call waits up to 5 minutes, as in Maestro.** Maestro's script client allows a call 5 minutes. The runner gave up after 30 seconds unless the call set `timeout`, so a script that calls a slow endpoint, such as one that seeds test data, failed with `HTTP request failed` where Maestro waits. A call without a `timeout` option now gets 5 minutes, and the option still wins.
- **WDA sends a read or a lookup again when its connection drops.** Through a forwarded port to a physical iPhone, WDA sometimes closes a connection before it answers, and the request came back as EOF; one dropped text read made `copyTextFrom` copy an empty string. net/http does not send these again (it only repeats a GET on a connection it had used before). A GET, or a POST that only finds elements, is now sent once more, with a warning in the log. An action (a tap, typing, a swipe, launching an app) is never repeated, because it may have reached WDA before the connection went.
- **`when: true:` and `assertTrue` read their value as Maestro does.** Maestro reads the evaluated text as false only when it is blank, `false` in any case, `undefined`, `null` or zero, so `abc` is true. The runner took a string as true only when it was exactly `true`, so `assertTrue: ${name}` failed for any other value. A `when: true: ${name}` condition was worse: the value was run as JavaScript a second time, so `abc` became a ReferenceError, and an unset variable became an empty condition, which ran the branch Maestro skips. A condition that is one `${...}` is now evaluated once, and its value is read by Maestro's rule.
- **A failing `onFlowComplete` step fails the flow, as in Maestro.** The hook ran in a deferred call that ignored every failure, after the flow had already been reported, so a flow whose cleanup failed passed. It now runs before the result is final, also when `onFlowStart` failed. A failing step that is not optional ends the hook and fails a flow that had passed, and a flow that had already failed keeps its own error. The hook's steps are counted in the flow's totals, and a flow that fails in `onFlowStart` is a failed flow for `--video on-failure`, which had thrown its recording away.
##### Contributors
[@​bulatgaleev](https://github.com/bulatgaleev)
1. WDA keeps enough idle connections for its parallel reads ([#​181](https://github.com/devicelab-dev/maestro-runner/pull/181))
2. `retry` counts retries, not attempts ([#​182](https://github.com/devicelab-dev/maestro-runner/pull/182))
3. WDA `launchApp` restarts a running app unless `stopApp: false` ([#​183](https://github.com/devicelab-dev/maestro-runner/pull/183))
4. WDA `notVisible` passes only when a lookup finds the element absent ([#​184](https://github.com/devicelab-dev/maestro-runner/pull/184))
5. `checked` selectors on iOS ([#​185](https://github.com/devicelab-dev/maestro-runner/pull/185))
6. `runScript` runs a script file as written ([#​194](https://github.com/devicelab-dev/maestro-runner/pull/194))
7. A `runFlow` or `retry` `env` no longer leaves its keys behind ([#​195](https://github.com/devicelab-dev/maestro-runner/pull/195))
8. A script's `http` call waits up to 5 minutes ([#​196](https://github.com/devicelab-dev/maestro-runner/pull/196))
9. WDA sends a dropped read or lookup again ([#​197](https://github.com/devicelab-dev/maestro-runner/pull/197))
10. `when: true:` and `assertTrue` read their value as Maestro does ([#​198](https://github.com/devicelab-dev/maestro-runner/pull/198))
11. A failing `onFlowComplete` fails the flow ([#​199](https://github.com/devicelab-dev/maestro-runner/pull/199))
12. Removed unused UIAutomator2 code ([#​200](https://github.com/devicelab-dev/maestro-runner/pull/200))
13. Reported system sheets tapped in the wrong place on iOS ([#​191](https://github.com/devicelab-dev/maestro-runner/issues/191))
[@​MxScripter](https://github.com/MxScripter)
1. `childOf` and `containsChild` follow the element tree ([#​203](https://github.com/devicelab-dev/maestro-runner/pull/203))
2. DeviceLab iOS `launchApp` keeps permission grants with the default `all: allow` ([#​204](https://github.com/devicelab-dev/maestro-runner/issues/204), [#​207](https://github.com/devicelab-dev/maestro-runner/pull/207))
3. A flow with inline or conditional `runFlow` steps is not split into a suite ([#​208](https://github.com/devicelab-dev/maestro-runner/pull/208))
4. Reported `checked:` selectors failing on DeviceLab Android ([#​205](https://github.com/devicelab-dev/maestro-runner/issues/205))
[@​tSquaredd](https://github.com/tSquaredd)
1. Found `setLocation` and `travel` not moving the location on Android, with the Android 7 fallback and the `am` error checks we took from it ([#​209](https://github.com/devicelab-dev/maestro-runner/pull/209))
[@​Lykhoyda](https://github.com/Lykhoyda)
1. UIAutomator2 `hideKeyboard` confirms the keyboard hidden with two reads ([#​193](https://github.com/devicelab-dev/maestro-runner/pull/193))
[@​aralshukaili](https://github.com/aralshukaili)
1. Reported `inputText` replacing a field's text instead of typing at the cursor ([#​189](https://github.com/devicelab-dev/maestro-runner/issues/189))
2. Reported a WebView DevTools connect hanging a flow, which led to bounding the whole CDP setup ([#​190](https://github.com/devicelab-dev/maestro-runner/issues/190))
[@​Cancercookie](https://github.com/Cancercookie)
1. Asked for script output and step labels in reports ([#​192](https://github.com/devicelab-dev/maestro-runner/issues/192))
[@​jalie](https://github.com/jalie)
1. Reported WDA failing every `point:` tap after a missed screen-size read ([#​201](https://github.com/devicelab-dev/maestro-runner/issues/201))
[@​emil-doychev-bijenkorf](https://github.com/emil-doychev-bijenkorf)
1. Reported `pasteText` after `setClipboard` failing on UIAutomator2 ([#​202](https://github.com/devicelab-dev/maestro-runner/issues/202))
[@​vlasov-pepperstone](https://github.com/vlasov-pepperstone)
1. Reported nested elements missing from iOS screens ([#​171](https://github.com/devicelab-dev/maestro-runner/issues/171))
### [`v1.1.27`](https://github.com/devicelab-dev/maestro-runner/blob/HEAD/CHANGELOG.md#1128---2026-09-30)
[Compare Source](https://github.com/devicelab-dev/maestro-runner/compare/v1.1.25...v1.1.27)
This release is about **running a Maestro suite and getting Maestro's answer**. Most of it came from running real Maestro suites (DuckDuckGo Android and iOS, React Native's RNTester, React Navigation) side by side with Maestro and fixing each place the two disagreed. The largest of those is selector matching: text and id selectors now match the whole value, as Maestro does, which is a behaviour change — see below. DeviceLab becomes the default driver on both platforms: on iOS it runs a new agent, on simulators and on real iPhones. Every flow gets `DEVICE_UDID`, `-e` stops splitting values at commas, and the Appium driver closes a long list of gaps with the native drivers.
##### ⚠️ Behaviour changes
**DeviceLab is the default driver on Android and on iOS.** Running without `--driver` now uses the on-device DeviceLab agent on both platforms: on Android instead of UIAutomator2, on iOS instead of WebDriverAgent, on simulators and real iPhones (a real iPhone still needs `--team-id`, as WDA did). Nothing changes for a run that names its driver. To keep the previous defaults, pass `--driver uiautomator2` on Android or `--driver wda` on iOS; both drivers are unchanged. See Changed below for the numbers behind the switch.
**Text and id selectors match the whole value, ignoring case, as in Maestro.** A plain text selector used to match any element whose text, content description or hint *contained* it, so `tapOn: Open` could land on a "Talk · Open" row and `assertVisible: English` passed on "Language: English". Ids matched as substrings too: `id: login` found `com.app:id/login_button`. Maestro compiles every text and id selector as a case-insensitive regex that must match the whole value (an id may match the whole resource-id or the part after the package prefix), falls back to the literal text when the pattern does not compile, and also accepts a value equal to the pattern. That rule now applies on DeviceLab, UIAutomator2 and Appium on Android, on `--driver devicelab` and `--driver devicelab-legacy` on iOS, and on Appium for iOS — both in the queries sent to the device and in the page-source matchers. **A flow that relied on substring matching must say so with a regex.** The WDA driver keeps its existing predicate matching: it prefers an element whose whole text matches and falls back to a substring match when there is none ([#​188](https://github.com/devicelab-dev/maestro-runner/issues/188), [#​161](https://github.com/devicelab-dev/maestro-runner/issues/161)).
```yaml
- tapOn: "Open" # only an element whose whole text is "Open" (any case)
- tapOn: ".*Open.*" # any element whose text contains "Open"
- tapOn: { id: "login" } # "login" or "com.app:id/login", not "login_button"
- tapOn: { id: ".*login.*" } # the old substring behaviour, spelled out
```
**Regex selectors ignore case again.** 1.1.26 made regex text selectors case-sensitive on the premise that Maestro does; it does not — every text and id selector is compiled with `IGNORE_CASE`, and suites depend on it (`(let's get started!|Let's do it!)` against "Let's get started!"). Regexes now ignore case. The problem 1.1.26 fixed is kept solved as a preference: when several elements match, those matching in the pattern's own case come first, so `^SIGN OUT$` still taps the "SIGN OUT" button ahead of a "Sign out" row ([#​151](https://github.com/devicelab-dev/maestro-runner/issues/151)).
**An `index:` past the number of matches finds no element.** It used to fall back to the first match, so `assertNotVisible: {id: row, index: 1}` ("there is no second row") failed with one row on screen, and a tap with an out-of-range index hit the first row. Now it names no element, on every mobile driver ([#​188](https://github.com/devicelab-dev/maestro-runner/issues/188)).
**`-e` keeps commas in values.** `-e REPLY="Thanks, going with Vue"` reached the flow as `REPLY=Thanks`. Each `-e` is now one variable, as in Maestro, so `-e A=1,B=2` sets `A` to `1,B=2`; pass `-e A=1 -e B=2` instead ([#​189](https://github.com/devicelab-dev/maestro-runner/issues/189)).
**Element swipes go from the element's centre to 10% / 90% of the screen**, as Maestro does on Android and iOS, instead of across 90% to -10% of the element's own bounds. On a chat list whose bottom edge sat under a docked composer, the old swipe began on the composer and the list never scrolled. Element swipes now travel further than before; `point:` and `distance:` keep their behaviour ([#​189](https://github.com/devicelab-dev/maestro-runner/issues/189)).
##### Added
- **New iOS driver: `--driver devicelab` runs the DeviceLab iOS agent**, a new XCUITest agent shipped prebuilt in `drivers/ios/devicelab-ios-agent/`, on simulators and real iPhones. It is the iOS default (see Changed); `--driver wda` selects WebDriverAgent, and the previous DeviceLab runner is kept as `--driver devicelab-legacy` so the two can be compared on the same flows.
- **Simulators:** the agent is installed and launched with `simctl` (falling back to `xcodebuild test-without-building`), re-attached when a live agent of the same version is found, and left running between runs (`DEVICELAB_IOS_AGENT_KEEP=0` stops it). The agent's port is derived from the simulator's UDID, so parallel simulators do not collide.
- **Real iPhones:** pass `--team-id`. On the first run the prebuilt agent is re-signed for your team and device (through a small public signing-stub project that Xcode provisions), cached under `~/.maestro-runner/cache/devicelab-ios-agent/device-signed/` until the agent changes or its profile nears expiry, then started over usbmux. A start that fails is retried; an untrusted certificate, Developer Mode off, a locked phone or a rejected signature stops at once with what to do. `DEVICELAB_IOS_AGENT_BUNDLE_ID` signs it under another bundle id for personal teams. `clearKeychain`, `setPermissions`, `setLocation`, `addMedia`, the clipboard and screen recording are not available on a device and say so.
- **Maestro semantics on the host:** the agent returns candidates and Maestro's selector rules pick among them, with relative selectors, index, state and size filters. Only elements at least 10% on screen are looked up, as Maestro's out-of-bounds filter does, and the node budget counts on-screen nodes only, so the footer of a long web page is reachable. Taps, swipes and scrolls are synthesized touches in Maestro's shape and durations; after a step that can move the screen, the next action waits until the screen is still and the app is idle. `scrollUntilVisible` honours `centerElement` and `speed`, and stops when the screen stops changing. Launch arguments follow Maestro's iOS rule (a boolean keeps its key as written). Element bounds are computed from truncated edges, as Maestro does, so `cropOn` screenshots match baselines Maestro took.
- **App lifecycle:** `launchApp` resets permissions then applies the flow's (`all: allow` when none are named) on a simulator; `clearKeychain`, `killApp` and `launchApp: clearKeychain` work; `clearState` reinstalls from a copy of the app staged once per build (`DEVICELAB_IOS_FAST_CLEARSTATE=1` empties the data container instead). On a simulator the driver turns Reduce Motion on and password AutoFill off before the app starts (`DEVICELAB_IOS_SIM_PREFS=0` leaves the simulator untouched).
- **Web content:** a WebKit inspector client watches the app's web pages, so the settle after a tap also waits for the visible page to finish loading and for its network requests to stop (`DL_IOS_WEB_READY=0` turns this off).
- **Recovery:** an agent that dies is restarted once; read-only commands are re-sent, actions never are.
- The agent binaries are proprietary, free to use with maestro-runner; see `drivers/LICENSE-BINARIES.md`.
```bash
# Simulator
maestro-runner --platform ios --driver devicelab test flows/
# Real iPhone
maestro-runner --platform ios --driver devicelab --team-id <TEAM_ID> \
--device <UDID> --app-file MyApp.app test flows/
```
- **`DEVICE_UDID` in every flow** — the serial (Android) or UDID (iOS) of the device the flow runs on, the same for every flow on that device. Under `--parallel` an app that keys its mocks by a per-device launch argument can now tell the devices apart. A `DEVICE_UDID` you set yourself (shell environment or `-e`) is kept. `runShell`'s `MAESTRO_DEVICE_ID` also names the worker's own device under `--parallel` instead of the run's first one. Sharding (`MAESTRO_SHARD_ID`) is not supported ([#​187](https://github.com/devicelab-dev/maestro-runner/issues/187)).
```yaml
- launchApp:
arguments:
deviceId: ${DEVICE_UDID}
```
- **`-e NAME` takes the value from the shell environment**, as `docker -e` does, and warns when `NAME` is not set instead of dropping it silently ([#​189](https://github.com/devicelab-dev/maestro-runner/issues/189)).
```bash
API_TOKEN=abc123 maestro-runner test -e API_TOKEN -e USER=qa flows/
```
- **`disableAnimations` on Android** — Maestro's workspace setting `platform: android: disableAnimations: true` is honoured, `--disable-animations` (`MAESTRO_DISABLE_ANIMATIONS`) turns it on for a run, and a flow's own config can turn it on or off for that flow. The three global animation scales are set to 0 and restored to their saved values when the run or flow ends. DeviceLab and UIAutomator2 drivers; other drivers warn and carry on.
- **Maestro's legacy `action:` spelling** — `- action: back` (and `hideKeyboard`, `scroll`, `clearKeychain`, `pasteText`) parses to the same step as the modern form. It was rejected as an unknown step, so suites written with it failed to parse (30 of DuckDuckGo Android's 166 flows).
- **`--app-file` accepts an http(s) URL** — point it at an artifact store (S3, GCS, a CI artifact) instead of a local path, and the binary is downloaded once and cached under `~/.maestro-runner/app-cache`. A `.zip` is unpacked to the `.app` or `.ipa` inside, so a zipped simulator build works directly. `--app-file-sha256 <hex>` verifies the download and makes the cache content-addressed, so a changed artifact at the same URL is refetched. Presigned URLs are handled safely: the URL is only ever printed with its query string redacted, so the signature never reaches a log, report, or error message. Honours `--insecure` for a self-signed host.
```bash
maestro-runner --app-file https://ci.example.com/builds/app.apk \
--app-file-sha256 3b1f… test flows/
```
- **CommonJS `require` in `runScript`** — a script can now pull in a helper module that lives beside the flow: `const helpers = require('./helpers.js')`, where the module sets `module.exports`. Relative paths resolve against the flow's directory, nested requires resolve relative to the requiring module (Node's algorithm), and an absolute path is read as given. Only CommonJS is supported — goja has no ES-module loader, so `import` is not available. This is a maestro-runner extension; Maestro's `runScript` has no module system.
```yaml
- runScript: |
const { formatDate } = require('./lib/dates.js')
output.today = formatDate(new Date())
```
- **`wait` step** — a plain time delay, for the times a flow needs to pause for a fixed duration rather than for a condition. The duration is in milliseconds, like every other timing field. This is a maestro-runner extension; Maestro has no plain wait (only `extendedWaitUntil` and `waitForAnimationToEnd`), so a flow that uses it will not run on Maestro. It waits on the run's context, so `Ctrl-C` during a long wait stops the run promptly. Prefer a condition wait where one fits; reach for this only for a genuine fixed pause.
```yaml
- wait: 2000 # pause 2 seconds
- wait: { duration: 500, optional: true }
```
- **`--insecure` for `runScript` HTTP calls** — `http.get`/`post`/`put`/`delete` in a `runScript` block can now reach a self-signed or otherwise untrusted TLS endpoint. Pass `--insecure` (or `MAESTRO_INSECURE=1`) to skip certificate verification for every request in the run, or opt in per request with `http.get(url, { insecure: true })`, which overrides the run-wide default either way. Verification stays on unless asked for.
##### Changed
- **DeviceLab is now the default Android driver.** Running Android without `--driver` uses the on-device DeviceLab agent instead of UIAutomator2. On the TestHive suites, measured on a Pixel 4a, DeviceLab matched UIAutomator2's pass rate and ran about 1.5-2x faster per flow (auth, products, cart, and the system media picker). UIAutomator2 is unchanged and still one flag away: `--driver uiautomator2`.
- **DeviceLab is now the default iOS driver.** Running iOS without `--driver` uses the DeviceLab iOS agent instead of WebDriverAgent, on simulators and real iPhones (a real iPhone still needs `--team-id`, as WDA did). On the simulator suites it passed everything WDA passed and more — TestHive 4/4 every run where WDA loses one to its alert race, RNTester 48/48, React Navigation 39/39 — and it runs about 30% faster per step. On a real iPhone SE (iOS 26.6.2) it passed 8 of the 9 TestHive flows; the ninth (search) failed the same way on the simulator, and is fixed below — the 9 now pass on the simulator. WDA is unchanged and one flag away: `--driver wda`. The web default (`cdp`) is unaffected.
- **DeviceLab Android settles like Maestro.** After a tap, the next action waits for a 200ms quiet window on screen (it was 500ms); after steps that start a page load (`openLink`, pressing Enter) the window stays 500ms, and `openLink` first waits up to 3s for the screen to change. A check between a tap and the next action no longer cancels that settle, and with a WebView connected the action also waits for the page to finish loading. A required element is looked for 17s, as Maestro's lookup timeout, instead of 12s. `launchApp`'s default `all: allow` grants only the permissions the app declares, read once per run, instead of all 23 known ones (about 0.7s per launch) ([#​189](https://github.com/devicelab-dev/maestro-runner/issues/189)).
- **`when:` / `while:` checks get Maestro's time budget** — 7s less the time since the last step that could change the app, never below 1s, instead of a fixed 1s. A check right after a quick step now still catches a dialog that is on its way; a present element still resolves at once. `--condition-timeout` sets a fixed budget only when given, and a per-condition `timeout:` still wins.
- `waitToSettleTimeoutMs` on a driver with a native settle (DeviceLab) waits for the screen to stay unchanged for 500ms, up to the step's timeout, instead of comparing two hierarchy reads 200ms apart.
- **The WDA driver swipes, settles and taps like Maestro.** Swipes are W3C touch actions with no hold before the move (the old hold made iOS take a scroll as a tap), scrolls run from the centre to 10% from the edge, the next action after a screen-moving step waits for two identical screenshots (up to 3s), and `tapOn` taps the element's on-screen centre, keeping WDA's element click as the fallback. Of nested matches with the same id, the innermost is tapped (a SwiftUI Toggle's switch, not its row). `scrollUntilVisible` takes the match that is on screen and uses the timeout as its budget unless `maxScrolls` is set. The client talks to `127.0.0.1` rather than `localhost`.
- The previous DeviceLab iOS runner (now `--driver devicelab-legacy`) restarts its on-device runner if it dies mid-session, instead of failing every remaining step with "connection refused". A read-only command is retried on the relaunched runner; an action is not replayed (so a crash-inducing tap cannot loop), but the runner is back for the next step. Proven by killing the runner mid-suite: the run still completed green.
- Bundled WebDriverAgent updated from 11.1.3 to 16.12.8. Brings upstream fixes for taps landing off-target in iPad compatibility mode, stale element snapshots on long-lived elements, a bounded accessibility request path so a frozen app cannot hang the server, and screenshots that no longer queue behind a stuck command. The runner now needs iOS 15 or newer (WDA raised its deployment target). The WDA build cache is now keyed by the bundled WDA version as well as the simulator or device configuration, so the first run after this upgrade (or after `wda update`) rebuilds the runner once instead of silently launching the previous version's build. The session no longer sends the `shouldUseTestManagerForVisibilityDetection` capability, which WDA removed in 12.0.
- `scrollUntilVisible` stops as soon as the screen stops moving — two scrolls that change nothing, or a rubber-band bounce off the edge — instead of spending every scroll `maxScrolls` allows on a target that is not in the list. The failure message says so ("made no progress after N scrolls"). Android (uiautomator2, devicelab), iOS (wda, devicelab) and Appium; the browser driver keeps its fixed 10-scroll cap.
- **`--typing-frequency` now also throttles `keyPress`-mode typing on the uiautomator2 Android driver.** The flag (default 30 keys/sec, override per flow with `typingFrequency:`) is applied by the runner to every driver that types character by character: the WDA and DeviceLab iOS drivers for all typing, and uiautomator2 for `keyPress`-mode `inputText`, which now adds a per-character pause (1000/frequency ms) so apps that drop fast key events — notably React Native fields — receive every keystroke. Lower it if a field still loses characters. Two drivers are unaffected by design: the default DeviceLab Android driver types through its on-device agent (a change there needs the agent, not the runner), and normal Android `inputText` sets the whole string at once so it never needed throttling.
- The prebuilt DeviceLab agents (Android APKs and the iOS agent) are covered by a binaries license, `drivers/LICENSE-BINARIES.md`: free to use with maestro-runner for any purpose, redistributable unmodified as part of maestro-runner, not open source. The rest of the repository stays Apache 2.0.
##### Fixed
- **DeviceLab iOS: a tap on an id two siblings share goes to the control.** TestHive's search icon and its text field both carry the id `search-bar`, the icon first; the tap hit the icon, the field never took focus, and `inputText` failed with `NO_FOCUS` on every device. Of matches that rank alike, a text field, button or other control now comes before an image or a label — the element WDA's "first displayed" rule lands on, and Maestro passes the flow too.
- **DeviceLab iOS: `hideKeyboard` no longer fails when the keyboard stays up.** It tries Maestro's two dismiss swipes and moves on, as Maestro does; some screens do not dismiss the keyboard on a drag, and the flow's next steps decide.
- **DeviceLab iOS: `eraseText` between a tap and `inputText` keeps the tap point**, so `inputText` can still re-tap the field when nothing has keyboard focus.
- **`inputText` after a `tapOn` types into the field the tap focused** on DeviceLab Android. A tap that landed while a bottom sheet was still sliding in did nothing, the previous field kept focus, and the text went there. `inputText` now waits up to 1.5s for a focused field on the tapped element and taps a text field once more if it never takes focus; a tap that worked costs nothing. The DeviceLab iOS agent does the same. `inputText` keeps its set-the-text semantics; `keyPress: true` appends ([#​189](https://github.com/devicelab-dev/maestro-runner/issues/189)).
- **`takeScreenshot` and `assertScreenshot` on DeviceLab Android capture full resolution, lossless.** They used the agent's half-size JPEG, so a `cropOn` crop came back at half the size and every `assertScreenshot` against a baseline taken by Maestro or another driver failed on size. They now capture through `adb exec-out screencap -p`, falling back to the agent's image if adb fails.
- **A text selector on iOS never matches the accessibility identifier.** WDA and Appium matched text against `name`, which XCUITest fills with the accessibility identifier (React Native's `testID`), so `tapOn: Open` tapped a button whose testID was `open-scanner` and whose label was "Scan code". Text now matches the label and value, as in Maestro; `id:` still matches the identifier ([#​178](https://github.com/devicelab-dev/maestro-runner/issues/178)).
- **WDA `tapOn` reported success with nothing tapped.** Text equal to a key name (`Delete`, `Return`, `Enter`, `Tab`, `Space`, `Backspace`) was sent as that key before any lookup; now the element is looked for first and the key is sent only when none is on screen and a keyboard is up. During a push both screens hold an element with the same id, and the lookup took the outgoing one; it now prefers the displayed match, re-looks up after a failed click and never taps off screen. An action right after a tap waits for the tapped element to come to rest, and an action after `scrollUntilVisible` waits for the list to stop, since iOS takes a tap on a moving list as "stop scrolling" ([#​179](https://github.com/devicelab-dev/maestro-runner/issues/179)).
- **Android selector parity:** a regex text selector also matches hint text; an anchored id (`^auth\.login\.emailInput$`) matches an id with a package prefix; ids match regardless of case; text containing a `"` matches on the agent path; an exact match that takes up no space (a collapsed 0px banner) does not count as found; `text: ""` matches elements with no text instead of failing with "no selector specified".
- **`killApp` on Android is a system process death**, as in Maestro (`am kill`): the app keeps its saved state and restores it on the next launch. It ran `am force-stop`, which is `stopApp`. As in Maestro, it only kills an app in the background.
- **Executor fixes:** `${...}` is expanded afresh on every pass of a `repeat`, `retry` or `runFlow`, so a repeat typing `${output.d[output.i]}` no longer types the first item every time; a `runFlow` `when:` condition is re-evaluated on every retry attempt ([#​176](https://github.com/devicelab-dev/maestro-runner/issues/176)); `runShell` and `pasteText` work inside `runFlow` — `runShell` failed as unsupported, and `pasteText` pasted the device clipboard instead of what `copyTextFrom` saved ([#​174](https://github.com/devicelab-dev/maestro-runner/issues/174)); only steps that can change the app restart the `when:` budget clock; a find's short probes count against its timeout, so a `when:` check no longer takes 10-12s against a 7s budget.
- A swipe's non-numeric `speed:` (such as `speed: fast`) is ignored, as in Maestro, instead of failing validation for the whole flow.
- **A retype after dropped characters keeps what the field held.** The retry cleared the field and typed only the step's text, so "abc" followed by a dropped "defgh" came back as "defgh". DeviceLab Android now also reads a field back from the device after typing, so dropped characters are detected and retyped there.
- **Appium driver parity** with the native drivers: clipboard steps work on Appium 3; `LANDSCAPE_LEFT`, `LANDSCAPE_RIGHT` and `UPSIDE_DOWN` on Android; `pressKey` on Android takes the same keys as uiautomator2; a literal id on Android tries the exact id first, then the whole-id rule; `swipe` honours `distance:` for screen swipes; `tapOn` and `dragAndDrop` apply `point:` inside the matched element ([#​175](https://github.com/devicelab-dev/maestro-runner/issues/175)); elements outside the viewport are not visible; `launchApp` applies permissions on every launch, with allow and deny; `takeScreenshot` and `assertScreenshot` honour `cropOn`; `inputText` on iOS no longer types into a field the flow left; every window is read on Android and deep trees on iOS; `eraseText`'s delete fallback works on iOS; `scroll` and `scrollUntilVisible` go left and right; tap selectors with text plus id or state match every field; the focused element is looked up with GET.
- **Appium on iOS no longer dumps the page source after every miss.** On a large tree over a cloud endpoint that call took \~20s and could exceed Appium's 60s limit, ending the session. After a native miss, one more predicate query covering what the page-source matcher accepts now proves absence; any other error falls back as before. On the iOS auth suite, page-source requests went from 804 to 8 ([#​173](https://github.com/devicelab-dev/maestro-runner/issues/173)).
- **DeviceLab Android reliability:** the bundled agent finds other apps' launch intents on Android 11+ and no longer launches with `CLEAR_TASK`, which wiped a task restored after process death; its settle compares fresh tree signatures (it had reported settled at once); an installed agent that matches the bundled APK is kept instead of being reinstalled on every start; a stale "Process crashed." line from a previous run no longer kills a healthy start; a WebView CDP handshake cannot hang past its connect timeout, and a socket that keeps failing is backed off up to a minute; the report's hierarchy covers every window.
- The legacy DeviceLab iOS runner: `eraseText` honours its count and passes with nothing focused, as in Maestro; typing `""` in replace mode clears the field; an app that is off screen has no visible elements, so `assertNotVisible` after `stopApp` passes; a snapshot follows the screen when the target app leaves it.
- Flutter VM discovery is skipped for iOS simulator apps without `Flutter.framework`, saving about 2.4s on the first element step of every non-Flutter run.
- Allure reports now include screenshot files in `allure-results`, with distinct attachment names for screenshots from different source paths. This also applies to nested steps ([#​186](https://github.com/devicelab-dev/maestro-runner/pull/186)).
- **JUnit `<failure>` now carries the error in the element body, not just the `message` attribute.** Maestro writes the failure reason in the body, and CI parsers written against Maestro read it there; ours put the error only in the attribute and left a step description in the body, so a Maestro-tuned parser saw no real error — which defeats being drop-in (caught reviewing the Expo EAS integration). The error now appears in both places; the body falls back to the step description only when no command-level error was captured.
- **WDA now reads a field back after typing and retypes dropped characters.** XCUITest typing can silently lose a character when the app janks — notably digits in Expo / React Native number fields, so `4049876574` lands as `409876574`. When `inputText` targets an element, the WDA driver now reads the value after typing and, if characters are missing, clears the field and types once more, the same recovery the Android drivers already do. Combined with the default 30 keys/sec throttle, this closes the iOS side of the long-standing fast-typing character loss.
- WDA rejects missing or invalid element bounds and retries selector lookup, preventing stale elements from producing successful taps at `(0, 0)` ([#​168](https://github.com/devicelab-dev/maestro-runner/pull/168)).
- **Deep React Native (New Architecture) screens were clipped out of the hierarchy.** WDA snapshots the accessibility tree to a default depth of 50, so a Fabric view tree deeper than that dropped its leaf elements and a `tapOn` near the bottom reported "not found". The session now raises the snapshot depth to 100 on the WDA and legacy DeviceLab iOS drivers; override with `MAESTRO_WDA_SNAPSHOT_MAX_DEPTH` / `DEVICELAB_SNAPSHOT_MAX_DEPTH` ([#​171](https://github.com/devicelab-dev/maestro-runner/issues/171)).
- **WDA on a physical iPhone no longer pins the build destination architecture.** 1.1.27 pinned `arch=arm64` to avoid an ambiguous arm64/arm64e resolution, but on some devices that produced a destination `xcodebuild` could not match and the runner never started. The physical-device destination is now `platform=iOS,id=<udid>` with no arch, letting Xcode pick; `MAESTRO_WDA_DEST_ARCH` still pins one if needed, and `any` is the explicit no-pin ([#​172](https://github.com/devicelab-dev/maestro-runner/issues/172)).
- **A run's `appId` is taken from the first flow that declares one, not from the first flow alone.** A suite could lead with a setup or shared-steps flow that carries no `appId`; the app-version lookup and default `launchApp` target then went empty even though later flows declared one. The runner now scans the flows for the first declared `appId` (or web `url`).
- **Report artifact capture:** the view hierarchy is written with the extension that matches its content (JSON for the native iOS snapshot, XML elsewhere) instead of always `.xml`; an `assertScreenshot` diff image is written into the report's assets keyed by command index rather than beside the reference file outside the report; a failed step's after-screenshot no longer overwrites one an explicit `takeScreenshot` captured; and a failure inside `runFlow`/`repeat`/`retry` now saves its own screenshot and hierarchy.
##### Known issues
- **DuckDuckGo Android `privacy_tests/8` fails its first attempt more often than on 1.1.27** with the DeviceLab driver, and passes on the flow's `retry:`. It fails where a tap on a WebView element (`id: pay-button`) does not take the app to the next page, so the privacy dashboard that follows shows no trackers. Being looked at for 1.1.29.
##### Contributors
[@​aralshukaili](https://github.com/aralshukaili)
1. Reported the selector differences from Maestro on DeviceLab Android — substring text and id, out-of-range index ([#​188](https://github.com/devicelab-dev/maestro-runner/issues/188))
2. Reported the missing settle after actions, `inputText` landing in the wrong field, and `-e` splitting values at commas ([#​189](https://github.com/devicelab-dev/maestro-runner/issues/189))
[@​nt-ben-leblond](https://github.com/nt-ben-leblond)
1. Reported and first patched plain text selectors matching by substring ahead of exact matches ([#​161](https://github.com/devicelab-dev/maestro-runner/issues/161))
[@​Cancercookie](https://github.com/Cancercookie)
1. Asked for a per-device identifier in parallel runs, which became `DEVICE_UDID` ([#​187](https://github.com/devicelab-dev/maestro-runner/issues/187))
[@​ablause](https://github.com/ablause)
1. Reported WDA text selectors matching the accessibility identifier ([#​178](https://github.com/devicelab-dev/maestro-runner/issues/178))
2. Reported WDA `tapOn` succeeding with nothing tapped ([#​179](https://github.com/devicelab-dev/maestro-runner/issues/179))
[@​yesiamdaniel](https://github.com/yesiamdaniel)
1. Reported Appium `tapOn` ignoring an element-relative `point:` ([#​175](https://github.com/devicelab-dev/maestro-runner/issues/175))
2. Reported inline `retry` not re-evaluating a `runFlow` condition, with a minimal repro ([#​176](https://github.com/devicelab-dev/maestro-runner/issues/176))
[@​rhys-thomas](https://github.com/rhys-thomas)
1. Reported `runShell` failing inside `runFlow` ([#​174](https://github.com/devicelab-dev/maestro-runner/issues/174))
[@​team-realtor-mobile](https://github.com/team-realtor-mobile)
1. Reported the Appium iOS page-source fallback exceeding Appium's 60s limit on large trees, with a proposed fix ([#​173](https://github.com/devicelab-dev/maestro-runner/issues/173))
[@​VictorGTdaCruz](https://github.com/VictorGTdaCruz)
1. Reported the WDA physical-device build destination failure ([#​172](https://github.com/devicelab-dev/maestro-runner/issues/172))
[@​vlasov-pepperstone](https://github.com/vlasov-pepperstone)
1. Reported deep React Native trees missing from the WDA hierarchy ([#​171](https://github.com/devicelab-dev/maestro-runner/issues/171))
[@​s4lmon](https://github.com/s4lmon)
1. Fixed WDA accepting missing or invalid element bounds ([#​168](https://github.com/devicelab-dev/maestro-runner/pull/168))
[@​sjchmiela](https://github.com/sjchmiela)
1. Fixed missing Allure screenshot attachments ([#​186](https://github.com/devicelab-dev/maestro-runner/pull/186))
</details>
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- "before 9am on tuesday"
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMzMuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjE2NC41IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiLCJyZW5vdmF0ZSJdfQ==-->
Reviewed-on: https://code.futo.org/harbor/harbor/pulls/1783
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A flow made only of
runFlowsteps, two or more of them with a file, is treated as a suite (IsSuite), andexpandSuitesruns each file as its own flow. That drops the flow's otherrunFlowsteps without a word:when:+commands:) are skipped;runFlow'swhen:is ignored, so the file runs whatever the condition says.Maestro has no suite expansion: it runs the file as written. We hit it with a reset flow of this shape:
maestro-runner reported "Found 3 test flow(s)", ran only the three files, and passed. The location was never cleared and the menu was left open, so every test after it failed for an unrelated-looking reason ("Queues" not found: the open menu hides the tab bar).
Type of Change
Changes Made
IsSuitereturns false when arunFlowstep is inline, has awhencondition, or has an else branch. Such a step is part of the flow, so the flow runs as written.runFlows are still suites, as before.TestFlow_IsSuitecases (two files plus an inline block; two files, one conditional); both fail onmain.Related Issues
No existing issue or PR.
Testing
go test ./pkg/flow/ ./pkg/executor/pass; the new cases fail without the fixgofmt,go vetcleanAdditional Notes
Even a pure file-only "suite" differs from Maestro when run directly (separate flows, so
output.*isn't shared and a failure doesn't stop the later files). That's left as is here; happy to discuss an opt-out if useful.🤖 Generated with Claude Code