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.

UI testing power-up

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 ${SRCROOT}/<MyAppTests>/path/to/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 the magic sauce, the ability to delete our app:

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. :] But really, it's a toss-up since the 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/{me}/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().

SpringBoard automated app delete

Permissions testing

Onto the topic of this post, permissions testing. With the private XCUIApplication(privateWithPath:bundleID:) constructor at our disposal, we put "" as the bundleID: and get 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 simply 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 apps as well - the possibilites are endless. Below are some of the bundle IDs, credits to Joe Blau.

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     |