XCUITesting for permission popup: alert appears, but UIInterruptionMonitor does not fire

I have noticed this problem as well. It seems like the interruption handlers are run asynchronously and there is no way to assert whether they were called. Also waiting for an expectation seems to prevent the interruption monitor from running at all. It looks like the system is waiting for the expectation to fulfil and the expectation is waiting for the the interruption monitor to fire. A classic case of deadlock.

However, I have found a rather quirky solution that uses NSPredicate-based expecations:

var didShowDialog = false
expectation(for: NSPredicate() {(_,_) in
    XCUIApplication().tap() // this is the magic tap that makes it work
    return didShowDialog
}, evaluatedWith: NSNull(), handler: nil)

addUIInterruptionMonitor(withDescription: "Camera Permission Alert") { (alert) -> Bool in
    alert.buttons.element(boundBy: 0).tap() // not sure if allow = 0 or 1
    didShowDialog = true
    return true
}

goToCameraPage()

waitForExpectations(timeout: 10) { (error: Error?) -> Void in
    print("Error: \(error?.localizedDescription)")
}

Apparently, doing the XCUIApplication().tap() inside the predicate block somehow allows the interruption monitor to be run, even though the test case is waiting for an expectation.

I hope this works as well for you as it did for me!


pancake's answer works, but only if the application is being tested for the first time. If the app has been previously tested in the same simulator, the permission will already be granted to the app, so the alert will never appear, and the test will fail.

My approach is to instead wait for an element that should appear in the app rather than waiting for the alert dialog to have been handled. If the alert dialog is over the app, the app's element will not "exist" because it's not reachable/tappable.

let alertHandler = addUIInterruptionMonitor(withDescription: "Photos or Camera Permission Alert") { (alert) -> Bool in
    if alert.buttons.matching(identifier: "OK").count > 0 {
        alert.buttons["OK"].tap()
        // Required to return focus to app
        app.tap()
        return true
    } else {
        return false
    }
}

app.buttons["Change Avatar"].tap()

if !app.buttons["Use Camera"].waitForExistence(timeout: 5.0) {
    // Cause the alert handler to be invoked if the alert is currently shown.
    XCUIApplication().swipeUp()
}

_ = app.buttons["Use Camera"].waitForExistence(timeout: 2.0)

removeUIInterruptionMonitor(alertHandler)

So pancake's answer worked for me. However, I think it can be simplified. There does appear to be some sort of weird deadlock or race condition when presenting a system alert.

Instead of the NSPredicate expectation I just used sleep(2) after the system alert should be presented and before trying XCUIApplication().tap().

I also decided to use XCUIApplication().swipeUp() since it's less likely to interfere with the test.

Example using Login with Facebook

class LoginWithFacebookTest: XCTestCase {

    let app = XCUIApplication()

    var interruptionMonitor: NSObjectProtocol!
    let alertDescription = "“APP_NAME” Wants to Use “facebook.com” to Sign In"

    override func setUp() {
        super.setUp()
    }

    override func tearDown() {
        super.tearDown()
        self.removeUIInterruptionMonitor(interruptionMonitor)
    }

    func loginWithFacebookTest() {
        app.launch()

        self.interruptionMonitor = addUIInterruptionMonitor(withDescription: self.alertDescription) { (alert) -> Bool in
            // check for a specific button
            if alert.buttons["Continue"].exists {
                alert.buttons["Continue"].tap()
                return true
            }

            return false
        }

        let loginWithFacebook = app.otherElements["login with facebook"]
        loginWithFacebook.tap()

        // Sleep to give the alert time to show up
        sleep(2)

        // Interact with the app to get the above monitor to fire
        app.swipeUp()
    }
}