Automated permissions testing

Permissions testing via an extended XCUIApplication, ready to interact with iOS SpringBoard.

Previously we’ve covered deeplinks with automated tests. Let’s now do the same for permissions. We’ll only showcase doing so for notifications, but the same would apply to others: camera, photos, location, calendar, reminders, microphone, etc.


First we need to empower UI testing with ability to interact with SpringBoard - and thus be able to delete the app-under-test between test cases.

Download private headers XCUIApplication.h and XCUIElement.h from WebDriverAgent. Then add a bridge header file, we’ll name it Bridge.h, that imports the downloaded headers:

// Bridge.h
#import "XCUIApplication.h"
#import "XCUIElement.h"

For the test-target, set Objective-C Bridging Header to ${ROOT}/<MyAppTests>/.../Bridge.h:

Bridging Header setting

Now we start using the newly gotten private APIs:

import XCTest

final class Springboard {
  static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "")!

Add a helper that deletes our app-under-test:

class func deleteMyApp() {

  // Resolve the query for the SpringBoard rather than launching it

  let icon = springboard.icons["MyApp"]

  // Bring up the little "X" button to delete the app 1.3)

  // The little "X" button is, it seems, not exposed directly
  let xButtonCoordinate = CGVector(dx: (icon.frame.minX + 3) / springboard.frame.maxX,
                                   dy: (icon.frame.minY + 3) / springboard.frame.maxY)
  // Delete the app
  springboard.coordinate(withNormalizedOffset: xButtonCoordinate).tap()

  // Stop icons from jiggling

(Sidenote: why class rather than static? First one is 1 character shorter. Joking aside, it’s a toss-up since Springboard class is final.)

We also don’t have to hardcode the app name and can replace above "MyApp" with below myAppName:

private class var myAppName: String {
  // XCUIApplication().path example:
  // "/Users/{name}/Library/Developer/Xcode/DerivedData/MyApp-{someID}/Build/Products/Debug-iphonesimulator/"
  let appName = XCUIApplication().path
    .components(separatedBy: "/").last!
    .components(separatedBy: ".").dropLast()
  return Array(appName).first!

Finally, verify everything works thus far by calling Springboard.deleteMyApp() in, say, test class’s setUp(). Here’s how it should look like:

SpringBoard automated app delete

Permissions testing

We now have all prerequisites for permissions testing. With the private XCUIApplication(privateWithPath:bundleID:) constructor at our disposal, we call it with "" as the bundleID:. It returns us a handle on, and with it we get to reproducibly set our app’s notification settings and then verify that the app responds accordingly.

final class Springboard {
  static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "")!
  static let settings = XCUIApplication(privateWithPath: nil, bundleID: "")!  static let settingsIcon = springboard.icons["Settings"]

First add a helper function:

private class func openSettings() {
  // Make sure home screen is frontmost

  // Go to SpringBoard's first screen
  Thread.sleep(forTimeInterval: 0.5)

  // Launch from a known state


Finally, notifications. These are slightly more tricky, as, besides notifications being ON or OFF, we also have to consider the 3rd state, when the app hasn’t yet requested notifications permissions and thus might not be listed in Settings. Taking this aside for now, here’s how to turn notifications ON or OFF:

enum NotificationsState {
  case on, off

class func turnNotifications(_ desiredState: NotificationsState) {

  var currentState: NotificationsState {
    let soundsCell = settingsTable.cells.staticTexts["Sounds"]
    return soundsCell.exists && soundsCell.isHittable
      ? .on
      : .off

  if (desiredState == .on && currentState == .off)
    || (desiredState == .off && currentState == .on) {
    let allowNotificationsSwitch = settings.switches.element(boundBy: 0)

See the sample project which puts the above together. Also note that not all permissions require deletion of the app; e.g. location permissions can be reset in Settings > General:

class func resetLocationAndPrivacy() {

  settings.tables.staticTexts["Reset Location & Privacy"].tap()
  settings.buttons["Reset Warnings"].tap()

But Wait, There’s More!

Not only Settings, we can interact with other Apple’s apps as well - below are some of the bundle IDs.

App Name	| Bundle ID
App Store	|
Calculator	|
Calendar	|
Camera		|
Clock		|
Compass		|
Contacts	|
FaceTime	|
Files       | # iOS 11+
iCloud Drive|  # iOS 10
Find Friends|
Find iPhone	|
Health		|
Home		|
iBooks		|
iTunes Store|
Mail		|
Maps		|
Messages	|
Music		|
News		|
Notes		|
Phone		|
Photos		|
Podcasts	|
Reminders	|
Safari		|
Settings	|
Siri        |
Stocks		|
Tips		|
TV			|
Videos		|
Voice Memos	|
Wallet		|
Watch		|
Weather		|