XCUITest interact with system dialogs

I have a test I want to be able to run, but need to be able to handle different states, might be logged in, might have saved login credentials in Safari, etc.


When a system dialog, "Safari Saved Password" in this case, comes up, I cannot seem to affect it any way, even though I have recorded a session that selectes "Not Now", the query actually fails to find the button.


The Accessibility inspector shows the button existing about 3 levels down, but there doesn't seem to be a way I can find to access it.


AXApplication

AXWindow

AXButton


Is there a way to get to this thing?


Thanks!

Replies

System Alerts have been very difficult to work with. Things like, "Allow Notifications", "Allow access to Location", "Allow Access to Photos", etc. There seems to be some built in behavior which will "automatically" accept the default option on some of these prompts, but it doesn't appear to actually have the proper behavior to interact with System Alerts or pieces that are outside of the scope of your Application. I'm hoping this gets resolved in for the final release.

Beta 6 added new methods to deal with asynchronous events, but I'm not sure how to use use them:


– addUIInterruptionMonitorWithDescription:handler:


Adds a handler to the current context. Returns a token that can be used to unregister the handler. Handlers are invoked in the reverse order in which they are added until one of the handlers returns true, indicating that it has handled the alert.

- (id<NSObject>)addUIInterruptionMonitorWithDescription:(NSString *)handlerDescription handler:(BOOL ( ^ ) ( XCUIElement *interruptingElement ))handler


Parameters

handlerDescription


Explanation of the behavior and purpose of this handler, mainly used for debugging and analysis.


handler


Handler block for asynchronous UI such as alerts and other dialogs. Handlers should return true if they handled the UI, false if they did not. The handler is passed an XCUIElement representing the top level UI element for the alert.


Declared In

XCTestCase.h


– removeUIInterruptionMonitor:


Removes a handler using the token provided when it was added.

- (void)removeUIInterruptionMonitor:(id<NSObject>)monitor


Declared In

XCTestCase.h

Hi,

here is what you can do

func testExample() {

// set you handler here

let alertHandler = addUIInterruptionMonitorWithDescription("alert handler") { (alert: XCUIElement) -> Bool in

print("alert label => \(alert.label)")

print("alert => \(alert)")

return false

}

// do rest of your test here

let app = XCUIApplication()

...

// remove handler is nescessary

removeUIInterruptionMonitor(alertHandler)

}

I really wish this worked.


It sort of does, the first system interruption dialog gets caught, but a second one does not.


The app first asks for Location, then Notificaitons, then Safari Saved Passwords.


It hangs up on the Notificaitons dialog which never gets caught by the interruption handler.

Has anyone got it working ? The handler is never triggered when there is a prompt for Location. I am using XCode 7.1 Beta with iOS 9.0.1 device.

It isn't working for VPN installation prompts, either. I'm guessing it only works for certain UI dialogs, and that Apple has to manually enable each one individually. If I'm right, then it is important to file bugs about each distinct dialog that doesn't trigger the callback.

We've halfway figured it out. We were assuming that the callback was called for every system dialog and was expected to sort out what to do based on the dialog. In reality, the callback seems to be called only when you've explicitly asked for an action on a particular alert. So you need to do something like:


[app.alerts[@"alert handler"].collectionViews.buttons[@"buttonName"] tap];


and then the handler "alert handler" runs. Unfortunately, immediately afterwards, the test fails because there's no actual alert with that name.

You can check to see if the button exists:


let button = app.alerts["alert handler"].buttons["button name"]
if button.exists {
   button.tap()
}


Re-reading your comment again, I see I was competely misunderstanding what you were saying.


I'm not sure that what you are seeing is correct in terms of interacting with the dialogs by ... name?


Can you explain further?

I've found some more things though the behavior has some weird inconsistencies.

I've found that I can call:

app.tap()


If the alert is present, the handling code gets triggered. The app exists to the tap is valid.


What I've found though is that my handler code to try to dismiss the alert seems to work fine on iPhone, but does not appear to work on iPad.


self.addUIInterruptionMonitorWithDescription("Push notification handler") { (alert) -> Bool in

let everything = alert.descendantsMatchingType(.Any).allElementsBoundByAccessibilityElement

print(everything)

if alert.buttons.matchingIdentifier("OK").count > 0 {

alert.buttons["OK"].tap()

return true

}

return false

}


Using breakpoints, the OK button is detected and the tap occurs.


On iPhone, I can see the alert dismiss when I step over the tap(), but on iPad, nothing happens.


Also somewhat strangely, my test cases appear quite content to tap and handle hittable and exists checks with the alert present on top.

yes.. i've that problem too... has anyone resolve this? ^

i can't dismiss the Push notification system alert...

Hi @dgatwood_af, Did you find a workaround to this issue?

If you return false on the addUIInterruptionMonitor's handler, the framework will tap the cancel button ('userTestingAttributes CONTAINS "cancel-button"').


So, if you enters an addUIInterruptionMonitor you should not return false, fail the test saying you found an unexpected System Alert instead, like so:


XCTFail("Unexpected System Alert")
return false


(Before that fail I like manually taking a screenshot, that you definely help)


If you have more than on System Alert (Notification and Location, for example) you can either


- use one addUIInterruptionMonitor checking for the possible buttons and then, not remove that monitor

- or you can have multiple monitors and make sure to remove each after they are used (if you do not remove, only one monitor will be used).


The second option seems better, but imagine that your first test handles the first alert, but fails before handling the second, in this case, your second test will see the second alert only. If the buttons for the alert are different, the second monitor will not know how to handle it.


First case:

addUIInterruptionMonitor(withDescription: "alert monitor") { (alert) -> Bool in
            let btnAllow = alert.buttons["Allow"]
            let btnAllowAlways = alert.buttons["Always Allow"]
            if btnAllow.exists {
                btnAllow.tap()
                return true
            }
            if btnAllowAlways.exists {
                btnAllowAlways.tap()
                return true
            }
           XCTFail("Unexpected System Alert")
           return false
}

Second case:

locationDialogHandeler = addUIInterruptionMonitor(withDescription: "locationDialogHandeler") { (alert) -> Bool in
            if btnAllowAlways.exists {
                NotificationCenter.default.post(name: self.notificationNotificationDialogHander, object: nil)
                btnAllowAlways.tap()
                return true
            }
            XCTFail("Unexpected System Alert")
            return false
        }
NotificationCenter.default.addObserver(forName: notificationLocationDialogHander, object: nil, queue: nil) { (notification) in
            if let locationDialogHandeler = self.locationDialogHandeler {
                self.removeUIInterruptionMonitor(locationDialogHandeler)
            }
        } 
//...


Remember that you should add UIInterruptionMonitor in reverse order. Therefore, in my case, since Location Alert appears first, that's the last monitor I add.


P.S: I add the monitors even before launching the app (app.launch())

Adding the monitors before launching the app made my alert-related tests work consistently. Before doing that, they were 'hit or miss'. Thanks for adding that P.S, @danielcarlosce.

As of iOS 13 you don't necessarily need an interruption monitor. If you know the alert is present (to be honest this should nearly always be the case since you should know exactly when the alert is triggered during your test run) you can simply locate the alert using the springboard app.

Take a look at this example that returns the buttons for the camera as well as location permission alerts:

import XCTest

enum PermissionElements {
    private static let springboardApp = XCUIApplication(bundleIdentifier: "com.apple.springboard")

    static func allowLocationButton() -> XCUIElement {
        return springboardApp.alerts.buttons["Allow While Using App"]
    }

    static func allowCameraButton() -> XCUIElement {
        return springboardApp.alerts.buttons["OK"]
    }
}

This includes some extra layers of abstraction to conform to SOLID programming principles, but the idea is the same: you don't need to sort through alerts if you know there is only a single alert on screen, and you should know that if you are in control of the AUT's state.