Edition 31

Automating Custom Alert Buttons on iOS

iOS alert handling has often been a testy subject (pun! pun!). Luckily, with the current XCUITest driver used by Appium, we can connect directly to Apple-approved method for accepting or dismissing an alert, or even getting its text. Appium provides access to this behavior via the standard suite of WebDriver alert commands, for example:

driver.switchTo().alert().alert()
driver.switchTo().alert().dismiss()
driver.switchTo().alert().getText()

Unfortunately, the WebDriver spec is not quite as flexible as mobile apps when it comes to the design of alerts. The WebDriver spec was based around web browser alerts, which can contain at most two action buttons. Mobile apps can create native alerts which contain 3 or even more buttons! So what do you do when you need to automate the action associated with a button which is not "OK" or "Cancel"?

For example, I've added a feature to The App's list view which enables you to tap a third alert button when you select a cloud type:

An alert with three buttons

When you tap this third button, another alert appears with some extra information about the cloud type:

An additional alert

But back to the first alert. We want to click the "Learn more about Cirrostratus" alert option. In this case, we could probably use the fact that we know the name of the button to find it in the UI hierarchy. But what if we didn't know the text of the button, only that we wanted to tap the third button, whatever it contains? Luckily, Appium has a special command for handling alerts on iOS that you can use for this purpose: mobile: alert.

This command does allow you to accept and dismiss alerts as usual, but that's not very interesting. We're going to look at two new use cases: getting a list of the available button labels, and tapping an alert action via its label. These features together will help solve the problem under considerations. Let's take a look at an example:

HashMap<String, String> args = new HashMap<>();
args.put("action", "getButtons");
List<String> buttons = (List<String>)driver.executeScript("mobile: alert", args);

Following the pattern of the other mobile: methods, we use executeScript with a special command and parameters to get the list of button labels. Once we have this list, we can do whatever we want with it. Today, we'll just try to extract the first label which isn't one of the defaults (taking care to throw an exception if there is no such label):

String buttonLabel = null;
for (String button : buttons) {
    if (button.equals("OK") || button.equals("Cancel")) {
        continue;
    }
    buttonLabel = button;
}

if (buttonLabel == null) {
    throw new Error("Did not get a third alert button as we were expecting");
}

At this point, we have the value of the button we want to press in the buttonLabel variable. We can again use mobile: alert to target this specific button, using a different action parameter than before (with a value of "accept"), and adding the new parameter buttonLabel to let Appium know which button we want it to tap for us:

args.put("action", "accept");
args.put("buttonLabel", buttonLabel);
driver.executeScript("mobile: alert", args);

That's all there is to it! Using mobile: alert we have access to the full set of alert actions that can be displayed in an iOS app. See the full example below to see it working in the context of my test app, or check out the code on GitHub!

import io.appium.java_client.MobileBy;
import io.appium.java_client.MobileElement;
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.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

public class Edition031_iOS_Alerts {

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

    private IOSDriver driver;
    private WebDriverWait wait;

    private By listView = MobileBy.AccessibilityId("List Demo");
    private By cloud = MobileBy.AccessibilityId("Cirrostratus");

    @Before
    public void setUp() throws IOException {
        DesiredCapabilities caps = new DesiredCapabilities();
        caps.setCapability("platformName", "iOS");
        caps.setCapability("platformVersion", "11.4");
        caps.setCapability("deviceName", "iPhone 6");
        caps.setCapability("app", APP);
        driver = new IOSDriver<MobileElement>(new URL("http://localhost:4723/wd/hub"), caps);
        wait  = new WebDriverWait(driver, 10);
    }

    @After
    public void tearDown() {
        try {
            driver.quit();
        } catch (Exception ign) {}
    }

    @Test
    public void testCustomAlertButtons() {
        wait.until(ExpectedConditions.presenceOfElementLocated(listView)).click();

        WebElement cloudItem = wait.until(ExpectedConditions.presenceOfElementLocated(cloud));
        cloudItem.click();

        wait.until(ExpectedConditions.alertIsPresent());
        HashMap<String, String> args = new HashMap<>();
        args.put("action", "getButtons");
        List<String> buttons = (List<String>)driver.executeScript("mobile: alert", args);

        // find the text of the button which isn't 'OK' or 'Cancel'
        String buttonLabel = null;
        for (String button : buttons) {
            if (button.equals("OK") || button.equals("Cancel")) {
                continue;
            }
            buttonLabel = button;
        }

        if (buttonLabel == null) {
            throw new Error("Did not get a third alert button as we were expecting");
        }

        args.put("action", "accept");
        args.put("buttonLabel", buttonLabel);
        driver.executeScript("mobile: alert", args);

        wait.until(ExpectedConditions.alertIsPresent());

        // here we could verify that the new button press worked, but for now just print it out
        String alertText = driver.switchTo().alert().getText();
        System.out.println(alertText);
    }
}