Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 644
qqq Avatar asked Mar 04 '16 18:03

qqq


3 Answers

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!

like image 143
pancake Avatar answered Nov 11 '22 17:11

pancake


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()
    }
}
like image 6
DoesData Avatar answered Nov 11 '22 15:11

DoesData


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)
like image 4
Robin Daugherty Avatar answered Nov 11 '22 17:11

Robin Daugherty