Handling System Alerts In UI Tests

The Xcode UI testing framework has been with us since Xcode 7, but I still find it a struggle to use. Apple has the usual API documentation but practical examples are hard to find so I often stumble around when using it on a real project.

Here are my notes on dealing with one common roadblock - the system interrupting to ask for permission for something.

Last updated: Aug 31, 2020

UI Testing

I want to test this initial home screen when my App launches:

Home screen hello

The view has a label, configured in a storyboard with an accessibility identifier:

Label accessibility identifier

My UI test might look like this:

// UITests.swift
import XCTest
class UITests: XCTestCase {
  let app = XCUIApplication()

  override func setUp() {
    continueAfterFailure = false
    app.launch()
  }

  func testHomeScreen() {
    let textQuery = app.staticTexts["WelcomeText"]
    XCTAssertTrue(textQuery.exists, "Missing welcome text")
  }
}

This is fine until I decide my App needs to support local notifications. So I request authorization after my App has launched:

let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert]) { 
  (granted, error) in
  // ...
}

Now my UI test leaves a system dialog open on the Simulator the first time it runs:

System alert would like to send you notifications

This is annoying, especially if I am using a tool like fastlane to capture screenshots. How do you dismiss these system alerts?

Handling UI Interruptions

Assuming you don’t give up and reach for stack overflow you might stumble on the documentation for Monitoring UI Interruptions in XCTestCase. Adding this to our setup method is a good start:

override func setUp() {
  continueAfterFailure = false
  app.launch()
  addUIInterruptionMonitor(withDescription: "System Dialog") {
    (alert) -> Bool in
    alert.buttons["Allow"].tap()
    return true
  }
  app.tap()
}

The XCTestCase instance method addUIInterruptionMonitor registers a handler to call when the UI is interrupted by an alert. The handler is passed an XCUIElement for the alert which we can use to find and tap the “Allow” button. Return true to indicate you handled the alert.

The one caveat is that you need to interact with the App to trigger the handler. Tapping the app works fine in this case:

app.tap()

Sometimes a tap might trigger an unwanted action. Depending on your view hierarchy you might need to tap on a specific view or bar you know is safe:

app.navigationBars.firstMatch.tap()

What if we have multiple system dialogs? For example, if my App also requests access to the microphone:

System alert Would like to access the microphone

Note that the system dialog has an “OK” button this time where the notification dialog had an “Allow” button. Our handler is called for both alerts so one approach can be to check for either button:

addUIInterruptionMonitor(withDescription: "System Dialog") {
  (alert) -> Bool in
  let okButton = alert.buttons["OK"]
  if okButton.exists {
    okButton.tap()
  }

  let allowButton = alert.buttons["Allow"]
  if allowButton.exists {
    allowButton.tap()
  }

  return true
}

If we don’t care what the dialog is we could also click the second button to dismiss it without checking the title (which helps avoid localization issues):

  let button = alert.buttons.element(boundBy: 1)
  if button.exists {
    button.tap()
  }

If we want to be precise about which permissions we are granting the test target we can look for the permission text in the alert:

  let notifPermission = "Would Like to Send You Notifications"
  if alert.labelContains(text: notifPermission) {
    alert.buttons["Allow"].tap()
  }

I have a small helper function defined in an extension of XCUIElement to test for a label containing text with a predicate:

extension XCUIElement {
  func labelContains(text: String) -> Bool {
    let predicate = NSPredicate(format: "label CONTAINS %@", text)
    return staticTexts.matching(predicate).firstMatch.exists
  }
}

Note: You’ll need to localize the expected permission strings if you run in non-English locales.

You can also split the different alerts into their own handlers:

addUIInterruptionMonitor(withDescription: "Local Notifications") {
  (alert) -> Bool in
  let notifPermission = "Would Like to Send You Notifications"
  if alert.labelContains(text: notifPermission) {
    alert.buttons["Allow"].tap()
    return true
  }
  return false
}

addUIInterruptionMonitor(withDescription: "Microphone Access") {
  (alert) -> Bool in
  let micPermission = "Would Like to Access the Microphone"
  if alert.labelContains(text: micPermission) {
    alert.buttons["OK"].tap()
    return true
  }
  return false
}

The handlers are called in the reverse order they were added until one of them returns true. To remove a handler you need to store the token returned when you add the handler:

let handler = addUIInterruptionMonitor(...)
// ...
removeUIInterruptionMonitor(handler)

I Can’t See Why It’s Not Working?

One final tip to save some frustration. What if you want to test the case where the user does not allow the permission? Easy enough you say, change the handler to click on the “Don’t Allow” button:

System alert would like to send you notifications

So something like this:

if alert.labelContains(text: notifPermission) {
  alert.buttons["Don't Allow"].tap()
}

Except it doesn’t work. The Xcode debug console shows the problem, but you have to look carefully:

Find: Descendants matching type Button
Find: Elements matching predicate '"Don't Allow" IN identifiers'
Assertion Failure: UITests.swift:63: No matches found for Find: Elements matching predicate '"Don't Allow" IN identifiers' from input {(
Button, label: 'Don’t Allow',
Button, label: 'Allow'

Can you see it? It may depend on how you are reading this post, but the apostrophe in the alert is a right single quotation mark (U+2019) not the usual straight apostrophe ' (U+0027). The corrected query code (assuming you didn’t give up and query for the first button):

if alert.labelContains(text: notifPermission) {
  alert.buttons["Don’t Allow"].tap()
}

Further Reading

This session from WWDC 2020 is a great summary:

A recap of Xcode UI testing from the WWDC 2015 session: