feat(ios): add setPickerValue command for UIPickerView wheels#3321
feat(ios): add setPickerValue command for UIPickerView wheels#3321lindyl wants to merge 1 commit into
Conversation
ea96685 to
b950620
Compare
|
Hey — I noticed #3313 recently added a
Everything else (all other CI checks, plus manual end-to-end testing across 3 device sizes) looks good. Just need your guidance on the driver artifacts. Thanks! |
|
Your LLM has written a massive amount of text in the description, but accidentally missed out what the new implementation does. This won't get properly considered without an e2e test. What's this doing? How does that affect the representation versus what a user would do when presented with a control of this type? I'm not sure I understand why this doesn't include an Android implementation - can you elaborate? |
Adds a new YAML command that drives XCUIElement.adjust(toPickerWheelValue:)
directly via the XCTest runner, eliminating the need for repeated swipe
sequences to navigate UIPickerView wheels.
YAML usage:
- setPickerValue: "United States"
- setPickerValue:
value: "March"
wheelIndex: 1
waitToSettleTimeoutMs: 5000
Closes mobile-dev-inc#1407.
iOS-only command. Non-iOS Driver implementations (AndroidDriver,
WebDriver, CdpWebDriver) throw UnsupportedOperationException.
Chain: YamlSetPickerValue -> SetPickerValueCommand -> MaestroCommand
-> Orchestra -> Maestro.setPickerValue -> IOSDriver -> IOSDevice
-> LocalIOSDevice -> XCTestIOSDevice -> XCTestDriverClient
-> POST /setPickerValue -> SetPickerValueRouteHandler ->
XCUIElement.adjust(toPickerWheelValue:).
The Swift handler:
- Queries pickerWheels.element(boundBy: wheelIndex ?? 0) on the
foreground app.
- Waits for the wheel to exist (configurable via waitToSettleTimeoutMs,
default 2000ms).
- Calls .adjust(toPickerWheelValue:).
- Verifies the wheel landed on the target value (XCTest doesn't
consistently throw on miss across SDK versions).
- Surfaces actionable error messages with the wheel count and a hint
about case-sensitive label matching.
Includes serialization round-trip tests and a YAML parser test
covering the shorthand string form, full-map form with wheelIndex,
full-map with waitToSettleTimeoutMs, and label/optional fields.
b950620 to
37c81c9
Compare
Add
setPickerValuecommand for iOS UIPickerView wheelsCloses #1407.
Motivation
iOS picker wheels (
UIPickerView) currently can't be driven directly by Maestro — the only way to interact with one is a sequence ofswipecommands. This has been a known gap since #1407 was opened in September 2023.The practical impact: writing a flow that selects a country, date, or any other picker value requires hardcoding how many swipes (and at what coordinates) it takes to land on the target row. That's brittle to:
I hit this in a registration flow that needed to land on "United States" in a 250-item country picker. The working solution was 23 hardcoded swipes — 15 up to reach Zimbabwe (a reliable alphabetical stop), then 8 down to back into United States. It worked, but it was the kind of test code that everyone agrees is bad. This PR replaces it with one line.
Appium has had this primitive for years via
sendKeyson picker elements, which under the hood calls Apple'sXCUIElement.adjust(toPickerWheelValue:). This PR brings the equivalent to Maestro.What this PR adds
A new YAML command,
setPickerValue:What it does mechanically
When the command runs, the Swift XCTest runner:
RunningApp.getForegroundApp()(same helper used byTextInputHelperand other existing handlers).app.pickerWheels.element(boundBy: wheelIndex ?? 0)— the n-thUIPickerViewwheel currently visible on screen.waitToSettleTimeoutMs(default 2000ms) for that wheel to exist, usingXCUIElement.waitForExistence(...).wheel.adjust(toPickerWheelValue: value). This is Apple's public XCTest API — it computes the pan distance from the wheel's current selected row to the target value, then synthesizes the corresponding touch events. It does not reach into the app's internal state. It drives the wheel the same way a user's finger would, through the same gesture pipeline thatswipealready uses — just with the distance pre-computed so the wheel lands exactly on the target row.wheel.valueand compares it to the target. If they don't match (e.g. the value isn't in the wheel, or has a trailing whitespace), returns an explicit error with the actual wheel value. This guard exists becauseadjust(toPickerWheelValue:)doesn't consistently throw on a missed value across iOS SDK versions.So from the user-action perspective: this is functionally equivalent to a user opening the picker, swiping until "United States" is in the selected position, and stopping. Same gestures, same iOS event pipeline — Apple just pre-computes the swipe distance for us instead of us guessing it with hardcoded
swipe:commands.Files touched (chain order)
A few design choices worth flagging
iOS-only. This is the main design constraint worth understanding:
UIPickerViewis a distinctive native control with a well-defined accessibility API (pickerWheelsquery,.valueattribute,adjust(toPickerWheelValue:)action). The mapping from "set this picker to X" to a single XCTest call is clean.NumberPicker(scrolled viaUiScrollable.scrollTextIntoView), but apps frequently useSpinner(atap → tap menu iteminteraction),WheelPickerfrom various third-party libraries, or composables that look picker-like but aren't backed by any of those. Each has different driver semantics — there's no "the Android picker."Calling the Android version
setPickerValuewould either (a) cover only one of those Android control types and confuse users when it doesn't work on the others, or (b) try to be polymorphic and quietly do different things on different controls. Neither feels right for a first implementation.This PR keeps the scope tight:
AndroidDriver,WebDriver, andCdpWebDriverall throwUnsupportedOperationExceptionforsetPickerValueso the iOS contract isn't muddied. If a future Android implementation lands, it can either reuse this name (if a clear "the Android picker" emerges) or use a different name (e.g.selectFromSpinner) without breaking the iOS contract.No element selector. The handler operates on
app.pickerWheels.element(boundBy: wheelIndex ?? 0). This matches the common case (one picker visible on screen at a time) and keeps the API simple. If we want a richer selector later (on: { below: "Where do you live?" }), it can be added without breaking existing usage. I'd rather ship the simple version and learn whether anyone needs more.waitToSettleTimeoutMsparameter. The Swift handler waits up to 2000ms (default) for the picker wheel to appear before failing. This is needed because pickers often animate in. The timeout is overridable to handle slow cold-sim cases.Post-adjust verification. Apple's
adjust(toPickerWheelValue:)doesn't consistently throw on miss across SDK versions. The handler readswheel.valueafter the call and returns a clear error if it didn't land on the target value — including the actual value the wheel ended up on. This catches common mistakes like a trailing whitespace in the YAML value, which would otherwise silently submit the wrong country/month/etc.Testing
Unit tests (in this PR):
MaestroCommandSerializationTest— three new round-trip tests for the command shape (nowheelIndex, withwheelIndex, withwaitToSettleTimeoutMs)YamlCommandReaderTest— a new test (034_setPickerValue.yaml) covering the shorthand string form, full-map withwheelIndex, full-map withwaitToSettleTimeoutMs, andlabel/optionalE2E test (in this PR — new):
PickerTestViewController(e2e/demo_app/ios/Runner/PickerTestViewController.swift) with a realUIPickerViewcontaining 32 countries. Surfaced through a Flutter method channel (com.example.demo_app/picker_test/openPickerTest), matching the existing pattern used byPasswordTestViewController. Worth noting: Flutter'sCupertinoPickerdoes not render as a nativeUIPickerView— XCTest'spickerWheelsquery returns 0 elements against it. A real native control is required to exerciseadjust(toPickerWheelValue:).e2e/demo_app/.maestro/issues/setPickerValue.yaml) that launches the demo app, opens the native picker via the method channel, asserts default selection is "Afghanistan", callssetPickerValue: "United States", and asserts the selection updated. Taggediosso it's skipped on Android runs. Verified passing locally against the iPhone 17 Simulator.Manual end-to-end on a real app (in addition to the e2e above):
setPickerValueline in the production flow; full registration completed successfullyQuick standalone test (against the XCTest runner, no CLI needed):
What's NOT in this PR
maestro-docspage — happy to add one if this lands; the inline KDoc onDriver.setPickerValuedocuments the iOS-only intent for now.Thanks for reviewing.