Edition 59

How to Automate Picker Wheel Controls

One of the mobile-specific UI controls that we tend to either love or hate is the "picker wheel"--the mobile version of the "select box". iOS and Android by default do things a little differently, and this article addresses strategies for automating the iOS-specific picker wheel.

To work with a picker wheel, I've added a few to The App. Basically, there's now a new view that asks us to select a month and a day, and then, after tapping a button, tells us a random historical event that happened on that date in some previous year. (I'm using a freely available API I found at history.muffinlabs.com--thanks, Colin!) The new feature looks like this:

The new feature utilizing 2 pickerwheels

We've actually got two picker wheels; one to select the month, and one to select the day. So, how do we automate this?

Method 1: SendKeys

The most straightforward way is just to use driver.sendKeys. What, sendKeys? How does that make sense? Selecting a picker wheel value uses a finger gesture in real life, not any entering of text! All of that is true. However, the main purpose of sendKeys is to allow the user to input data into a form control, and that is exactly what we're doing here. In fact, Selenium uses sendKeys to set the value of an HTML select field, which is more or less what we're doing here.

OK, so now that we've established it's not totally crazy, how does it work? Basically, just use the displayed text value of the picker wheel item you want to select, and that's it. For example, if we have our picker wheel element already found, we could do this:

pickerWheelElement.sendKeys("March");

In the example of our test app, this would set the value of the picker wheel to "March".

If at all possible, this is the preferred method, since it is simple and fast. However, there might be times where you don't know the value of the picker wheel item beforehand; in this case, check out the alternative method below.

Method 2: selectPickerWheelValue

Appium also makes available a mobile: method called selectPickerWheelValue, which is primarily useful for navigating a picker wheel using forward-and-back gestures. The command takes 3 parameters:

  • order: whether you want to go forward ("next") or backward ("previous") in the list of options.
  • offset: how far (in ratios of the picker wheel height) you want the click to happen. The default is 0.2.
  • element: the internal ID of the picker wheel element we're working with.

Once we know what we want to do, we simply have to construct our typical executeScript call with these parameters. For example, if we have the same element as we just saw above:

HashMap<String, Object> params = new HashMap<>();
params.put("order", "next");
params.put("offset", 0.15);
params.put("element", ((RemoteWebElement) pickerWheelElement).getId());
driver.executeScript("mobile: selectPickerWheelValue", params);

This will move the value of the picker wheel to the next one in the list!

Full Example

That's all there is to having full automation control over iOS's picker wheels. You can check out the full code sample I produced to demonstrate these concepts, and it's also reproduced here, with comments, for your perusal. Enjoy!

import io.appium.java_client.MobileBy;
import io.appium.java_client.ios.IOSDriver;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

public class Edition059_Picker_Wheel {

    private String APP_IOS = "https://github.com/cloudgrey-io/the-app/releases/download/v1.9.0/TheApp-v1.9.0.app.zip";

    private IOSDriver driver;
    private WebDriverWait wait;

    private static By pickerScreen = MobileBy.AccessibilityId("Picker Demo");
    private static By pickers = MobileBy.className("XCUIElementTypePickerWheel");
    private static By learnMoreBtn = MobileBy.AccessibilityId("learnMore");

    @Before
    public void setUp() throws IOException {
        DesiredCapabilities capabilities = new DesiredCapabilities();
        capabilities.setCapability("platformName", "iOS");
        capabilities.setCapability("platformVersion", "11.4");
        capabilities.setCapability("deviceName", "iPhone 8");
        capabilities.setCapability("app", APP_IOS);

        driver = new IOSDriver<>(new URL("http://localhost:4723/wd/hub"), capabilities);
        wait = new WebDriverWait(driver, 10);
    }

    @After
    public void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }

    @Test
    public void testPicker() {
        // get to the picker view
        wait.until(ExpectedConditions.presenceOfElementLocated(pickerScreen)).click();

        // find the picker elements
        List<WebElement> pickerEls = wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(pickers));

        // use the sendKeys method to set the picker wheel values directly
        pickerEls.get(0).sendKeys("March");
        pickerEls.get(1).sendKeys("6");

        // trigger the API call to get date info
        driver.findElement(learnMoreBtn).click();

        // verify info was retrieved for the correct date
        wait.until(ExpectedConditions.alertIsPresent());
        Alert alert = driver.switchTo().alert();
        Assert.assertThat(alert.getText(), Matchers.containsString("On this day (3/6) in"));

        // clear the alert
        alert.accept();
        wait.until(ExpectedConditions.not(ExpectedConditions.alertIsPresent()));

        // use the selectPickerWheelValue method to move to the next value in the 'month' wheel
        HashMap<String, Object> params = new HashMap<>();
        params.put("order", "next");
        params.put("offset", 0.15);
        params.put("element", ((RemoteWebElement) pickerEls.get(0)).getId());
        driver.executeScript("mobile: selectPickerWheelValue", params);

        // and move to the previous value in the 'day' wheel
        params.put("order", "previous");
        params.put("element", ((RemoteWebElement) pickerEls.get(1)).getId());
        driver.executeScript("mobile: selectPickerWheelValue", params);

        // trigger the API call to get date info
        driver.findElement(learnMoreBtn).click();

        // and finally verify info was retrieved for the correct date (4/5)
        wait.until(ExpectedConditions.alertIsPresent());
        alert = driver.switchTo().alert();
        Assert.assertThat(alert.getText(), Matchers.containsString("On this day (4/5) in"));
        alert.accept();
    }
}