Edition 43

Setting iOS App Permissions Automatically

There's nothing more frustrating when running an iOS test than to have a system dialog pop up right in the middle of your test. And it only makes matters worse when that dialog is asking whether to grant a certain permission to your app. You're testing the app, so obviously you want it granted! (Unless of course you're testing what happens when it's not granted, in which case, dialog away.)

Luckily, recent versions of Appium (the latest 1.9.2-beta.2 and up) have a new capability that makes it possible to do away with both of these problems simultaneously. The new capability is permissions, and it lets you set any permission that iOS knows about, from location to notifications to photos. One caveat: the new capability only works for simulators, so those of you running on real devices will have to sit tight and keep manually automating those popups.

Setup

Because Appium relies on some newfangled 3rd-party software to make the permissions magic happen, we need to get said software on our machines before we try and run our test. Here's what to do (assuming you're on a Mac and have Homebrew installed; and if you're not using a Mac this guide won't be very useful to you anyway!):

brew tap wix/brew
brew install wix/brew/applesimutils

This gets the applesimutils utility program onto your machine so Appium can take advantage of it. At this point, you can restart Appium and prepare to run your test.

The permissions Capability

There are no new commands tied to this feature, only the permissions capability. It should be a string, in valid JSON format, with the following structure:

{
    "your-bundle-id": {
        "permission-1-name": "permission-1-value",
        "permission-2-name": "permission-2-value",
        ...
    }
}

Of course, replace all these fake values with ones that make sense for your app. How do you know what permission names and values are available? You can always check out the applesimutils documentation for the full list, but I will reproduce them here:

Available Permissions:
    calendar=YES|NO|unset
    camera=YES|NO|unset
    contacts=YES|NO|unset
    health=YES|NO|unset
    homekit=YES|NO|unset
    location=always|inuse|never|unset
    medialibrary=YES|NO|unset
    microphone=YES|NO|unset
    motion=YES|NO|unset
    notifications=YES|NO|unset
    photos=YES|NO|unset
    reminders=YES|NO|unset
    siri=YES|NO|unset
    speech=YES|NO|unset

So let's say we wanted to allow our typical test app to access the GPS data while the app is in use. In that case, our JSON object should look like:

{
    "io.cloudgrey.the-app": {
        "location": "inuse"
    }
}

But of course we need to include this as the capability, so we have to stringify it and include it in our Java client's capability builder, like so:

caps.setCapability("permissions", "{\"io.cloudgrey.the-app\": {\"location\": \"inuse\"}}");

(Notice that I've had to escape the inner quotes).

This is all we need to do to ensure that, when my app launches and before my test even begins, the permissions I need will already be set, so that I don't have to worry about any popups in the course of my test. I can also experiment with setting the values differently for different tests, if I want to see how my app will behave if a user changes the permission to something else, or turns it off entirely.

I've incorporated this example into a complete code sample, which navigates to a geolocation demo view on The App, and attempts to read the latitude and longitude from the view. This of course requires location permissions to be granted, and will only work with the new permissions capability (try removing the capability, and you'll see the popup asking for authorization).

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 org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.html5.Location;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

public class Edition043_iOS_Permissions {

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

    private IOSDriver driver;
    private WebDriverWait wait;

    private By geolocation = MobileBy.AccessibilityId("Geolocation Demo");

    @Before
    public void setUp() throws IOException {
        DesiredCapabilities caps = new DesiredCapabilities();
        caps.setCapability("platformName", "iOS");
        caps.setCapability("platformVersion", "12.1");
        caps.setCapability("deviceName", "iPhone 8");
        caps.setCapability("app", APP);
        caps.setCapability("permissions", "{\"io.cloudgrey.the-app\": {\"location\": \"inuse\"}}");

        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 testLocationPermissions() {
        // first, set the geolocation to something arbitrary
        double newLat = 49.2827, newLong = 123.1207;
        driver.setLocation(new Location(newLat, newLong, 0));

        // now navigate to the location demo
        wait.until(ExpectedConditions.presenceOfElementLocated(geolocation)).click();

        // if permissions were set correctly, we should get no popup and instead be
        // able to read the latitude and longitude that were previously set
        By newLatEl = MobileBy.AccessibilityId("Latitude: " + newLat);
        By newLongEl = MobileBy.AccessibilityId("Longitude: " + newLong);
        wait.until(ExpectedConditions.presenceOfElementLocated(newLatEl));
        wait.until(ExpectedConditions.presenceOfElementLocated(newLongEl));
    }
}

Don't forget to also have a look at the sample on GitHub.