Skip to content

fix(wda): checked selectors work on iOS - #185

Merged
omnarayan merged 5 commits into
devicelab-dev:mainfrom
vyrahealth:upstream-pr/wda-checked-selectors
Oct 1, 2026
Merged

omnarayan merged 5 commits into
devicelab-dev:mainfrom
vyrahealth:upstream-pr/wda-checked-selectors

Conversation

@bulatgaleev

@bulatgaleev bulatgaleev commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Maestro reads checked on iOS from the element itself, a CheckBox, Switch or Toggle whose value is "1" (IOSDriver.kt), and checked: true or false filters on that. maestro-runner left checked out of the iOS field list (pkg/flow/selector.go), so it printed a warning and dropped the field: checked: true matched a switch in either state, and a tap took whichever came first. The WDA driver now reads and filters on checked.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)

Changes Made

  • The page source parse derives checked from the element's type and value, and the matcher filters on it.
  • A checked selector goes to the page source on every path (tap, assert, relative), since the WDA queries cannot see it. A relative selector with checked as its only own field now filters too.
  • checked is in the iOS field list, so the warning is gone. That list is shared by all iOS drivers, and the devicelab iOS driver still ignores checked.
  • pkg/driver/wda/checked_test.go and pkg/flow/selector_test.go, and a CHANGELOG.md entry.

Related Issues

No existing issue found. #153 fixed checked for Appium on Android; this is the iOS WDA side.

Testing

  • go test for every package except the ones that drive attached devices and the browser suite. The one failure is pkg/driver/mock's TestExecute_SuccessTapStep, which fails now and then on main too.
  • Added tests for new functionality
  • Running on a physical iPhone (iOS 27, WebDriverAgent reached through a forwarded port) in our nightly 46-flow suite, as part of our fork's build
  • go vet ./... and make fmt-check pass (run on the top of the stack, fix(wda): checked selectors work on iOS #185, which contains all five changes)
  • make test as a whole: not run, because its device tests drive whatever device is attached to the machine
  • make lint: the Makefile has no lint target, and the linters make check runs (staticcheck, revive, errcheck, nilaway, gosec) are not installed here

Checklist

  • Code follows project style guidelines
  • Self-reviewed the code
  • Added/updated documentation as needed (no documentation change needed)
  • No breaking changes (or documented if breaking)
  • CHANGELOG.md updated (for notable changes)

Additional Notes

Stack 5 of 5: #181 → #182 → #183 → #184 → #185. Merge in that order. This branch is built on #181, #182, #183 and #184, so it also contains their commits. This PR's own change is the top commit, fa51ea5. Once the PRs below it merge, the rest of the diff disappears, and nothing conflicts. All five sit on main at fe3dd53 (the 1.1.28 release).

The WDA driver reads an element's name, rect, text and displayed in
parallel, and a tap looks an element up four ways at once. The client used
Go's default transport, which keeps two idle connections per host, so every
such burst closed two connections and opened two new ones.

On a real iPhone reached through a forward (SSH, then iproxy over USB) a
new connection costs about 300 ms, and new ones opened together fail at
once with EOF. In one 44-flow run, 297 of 996 element bursts lost exactly
two reads (the two new connections), 2 lost one, and none lost a read on a
kept connection: 976 EOFs, each sent again by the dropped-connection
retry. WebDriverAgent does not close an idle keep-alive connection
(FBHTTPServer exempts them from its reaper), so a stale reuse was not the
cause.

The client now keeps up to eight idle connections per host. In the new
tests, 50 bursts of four reads open 4 connections instead of 102, and
through a server that drops every new connection past the first four, no
read fails (the old client lost 23 reads there, after 196 dropped
connections).
Maestro runs a retry's commands once and then up to maxRetries more times:
`(maxRetries?.toIntOrNull() ?: 1).coerceAtMost(3)`, then
`while (attempt <= maxRetries)` from 0 (Orchestra.kt:934-955, with
MAX_RETRIES_ALLOWED at 1844 and the "1" default at YamlFluentCommand.kt:614).
So `maxRetries: 1` is two attempts, the default is two, and no retry runs
more than four times. The value is evaluated first, so ${...} works
(Commands.kt:986), and anything that is not an integer counts as 1.

The runner ran exactly maxRetries attempts, three when unset, with no cap,
so a flow written with `maxRetries: 1` never retried at all. It also failed
the step when maxRetries was not an integer. The inline and the file form
of retry now count as Maestro does. A negative maxRetries runs no attempt
and passes, since Maestro's loop never starts. A value that is not an
integer is logged and read as 1.
Maestro stops the app before launching it unless the flow says
`stopApp: false`. The WDA driver never read stopApp: with a session open and
no launch arguments it called WDA's launch on the running app, which only
activates it. A relaunch then left the app on the screen it was already on,
so a flow checking what survives a restart (a chat's history, or a changed
subscription state being read back on launch) was not restarting anything.

It now terminates first unless stopApp is false. Launch arguments still
force the stop, since they only apply on a real launch. A launchApp that
creates the session is unchanged: creating it launches the app fresh.
Maestro's notVisible waits for a lookup that ends in ElementNotFound
(Orchestra.kt:1066-1082); any other error from the lookup fails the step.

assertNotVisible and extendedWaitUntil notVisible treated any failed lookup
(err != nil || info == nil) as the element being gone. A page source that
could not be read, from a WDA error or a dropped connection, passed the
assertion without the screen ever being looked at. The when/while and
assertCondition notVisible checks go through assertNotVisible, so they had
the same hole.

The page-source lookups now return a not-found error when they read the
screen and nothing matches (an anchor that is not there included). Only that
counts as gone. Any other error is tried again until the timeout, and the
step then fails with that error ("Could not check that ... is not visible").
TestAssertNotVisibleWithTimeout served an error for every request and
expected a pass; it now serves an empty screen.
Maestro reads checked on iOS from the element itself: a CheckBox, Switch or
Toggle whose value is "1" (IOSDriver.kt:223 and 680-690), and checked: true
or false filters on that (Filters.kt:272-276, Orchestra.kt:1664-1672).

The runner left checked out of the iOS field list (pkg/flow/selector.go), so
it printed a warning and dropped the field. The page source parse had no
checked state and the WDA queries never looked at it, so checked: true
matched a switch in either state, and a tap took whichever came first.

The page source parse now derives checked from type and value, the matcher
filters on it, and a checked selector goes to the page source on every path
(tap, assert, relative), since the WDA queries cannot see it. A relative
selector with checked as its only own field now filters too. checked is in
the iOS field list, so the warning is gone. That list is shared by all iOS
drivers, and the devicelab iOS driver still ignores checked.
@bulatgaleev
bulatgaleev force-pushed the upstream-pr/wda-checked-selectors branch from 33912af to fa51ea5 Compare September 30, 2026 14:49
@omnarayan

Copy link
Copy Markdown
Contributor

Thank you @bulatgaleev, and for the stack as a whole.

One note: since 1.1.28 (out today) the default iOS driver is --driver devicelab, not WDA. Its matcher already reads checked from the element's value, but it never received the field before, because the shared iOS field list dropped it. With this PR, checked: reaches the devicelab iOS driver too. We'll test it on that driver before the release.

Merging into main now with #181–#184; it ships in 1.1.29.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants