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

3

14

I would like to write a test like so:

When my app goes to a certain pane, it should request permission to use the camera.

I want to test whether or not the pane appears. I am using XC's builtin UITest framework to do this. Per what I found on google and here, it seems like I should do the following:

let dialogAppearedExpectation = expectationWithDescription("Camera Permission Dialog Appears")

addUIInterruptionMonitorWithDescription("Camera Permission Alert") { (alert) -> Bool in
    dialogAppearedExpectation.fulfill()
    return true
}

goToCameraPage()

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

The test began with failing, great. I implemented goToCameraPage, which correctly causes the "give permission" popup to appear. However, I would expect this to trigger the interruption monitor. No such interruption is caught, however, and fulfillment does not occur.

I read somewhere that you should do app.tap() after the dialog appears. However, when I do that, it clicks the "allow" button. The dialog disappears and still no interruption is handled.

Is there some way in which permission dialogs are not considered "alerts" or can't be handled? I even went in and replaced the interruption bit with a thing which just looks at app.alerts, but that turns out to be empty, even as I'm looking right at the popup in Simulator.

Thanks! I am using Xcode7.2, iOS 9.2 simulator for iPhone 6s.

Dupaix answered 4/3, 2016 at 18:51 Comment(0)
C
9

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!

Casimiracasimire answered 2/12, 2016 at 14:47 Comment(4)
i don't like this approach, but having looped back around to a similar issue again I did get this to work.Dupaix
I don't like it either :DCasimiracasimire
I noticed that there is no need to keep track of a dialog, interruption monitor closure is called as soon as the app is tapped in expectation's predicate closure, so it can just return true right awayBlah
This solution works 50/50 for my tests on Xcode 11.3Peruzzi
E
8

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()
    }
}
Esotropia answered 9/8, 2019 at 15:9 Comment(1)
I hate apple for giving us this crappy API's which work exactly the way it shouldn't. Nevertheless, your answer works.Gangster
M
4

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)
Martian answered 21/4, 2018 at 19:46 Comment(2)
the app.tap step doesn't seem necessary on iOS 12 / Xcode 10.1Avila
It does seem necessary on IOS 13 beta.Capsulize

© 2022 - 2024 — McMap. All rights reserved.