Xcode 7 UI Testing: how to dismiss a series of system alerts in code
Asked Answered
N

10

54

I am writing UI test cases using the new Xcode 7 UI Testing feature. At some point of my app, I ask the user for permission of camera access and push notification. So two iOS popups will show up: "MyApp Would Like to Access the Camera" popup and "MyApp Would Like to Send You Notifications" popup. I'd like my test to dismiss both popups.

UI recording generated the following code for me:

[app.alerts[@"cameraAccessTitle"].collectionViews.buttons[@"OK"] tap];

However, [app.alerts[@"cameraAccessTitle"] exists] resolves to false, and the code above generates an error: Assertion Failure: UI Testing Failure - Failure getting refresh snapshot Error Domain=XCTestManagerErrorDomain Code=13 "Error copying attributes -25202".

So what's the best way of dismissing a stack of system alerts in test? The system popups interrupt my app flow and fail my normal UI test cases immediately. In fact, any recommendations regarding how I can bypass the system alerts so I can resume testing the usual flow are appreciated.

This question might be related to this SO post which also doesn't have an answer: Xcode7 | Xcode UI Tests | How to handle location service alert?

Thanks in advance.

Nalepka answered 21/8, 2015 at 20:42 Comment(3)
Generally, you should not present two alerts at the same time. However, you can determine the order of the alert presentation if you put a slight delay between them.Fevre
If I know the sequence (it's almost always the same sequence), I'm wondering why having [app.alerts[cameraAccessTitle].collectionViews.buttons[@"OK"] tap]; and [app.alerts[notificationAccessTitle].collectionViews.buttons[@"OK"] tap]; never works. It will dismiss the first popup, then it stops. The second tap doesn't happen. I'll add this detail to my post @ILikeTauNalepka
Is it not possible to dismiss the alert before the other one appears?Fevre
P
50

Xcode 7.1

Xcode 7.1 has finally fixed the issue with system alerts. There are, however, two small gotchas.

First, you need to set up a "UI Interuption Handler" before presenting the alert. This is our way of telling the framework how to handle an alert when it appears.

Second, after presenting the alert you must interact with the interface. Simply tapping the app works just fine, but is required.

addUIInterruptionMonitorWithDescription("Location Dialog") { (alert) -> Bool in
    alert.buttons["Allow"].tap()
    return true
}

app.buttons["Request Location"].tap()
app.tap() // need to interact with the app for the handler to fire

The "Location Dialog" is just a string to help the developer identify which handler was accessed, it is not specific to the type of alert.

I believe that returning true from the handler marks it as "complete", which means it won't be called again. For your situation I would try returning false so the second alert will trigger the handler again.

Xcode 7.0

The following will dismiss a single "system alert" in Xcode 7 Beta 6:

let app = XCUIApplication()
app.launch()
// trigger location permission dialog

app.alerts.element.collectionViews.buttons["Allow"].tap()

Beta 6 introduced a slew of fixes for UI Testing and I believe this was one of them.

Also note that I am calling -element directly on -alerts. Calling -element on an XCUIElementQuery forces the framework to choose the "one and only" matching element on the screen. This works great for alerts where you can only have one visible at a time. However, if you try this for a label and have two labels the framework will raise an exception.

Pane answered 26/8, 2015 at 13:29 Comment(15)
Hi Joe, thanks for the answer. What that line does to my app is - the alert is dismissed successfully, then the test fails on the same line with the following error: UI Testing Failure - No matches found for AlertNalepka
Indeed it does. I filed a bug report, rdar://22498241. I suggest anyone experiencing this duplicates it.Pane
If you don't tell it to tap anything it will tap "OK" or whatever the accept value is. I haven't found a way to tap cancel, or no, etc.Ghent
@Nalepka I've updated my answer for Xcode 7.1 which no longer crashes.Pane
If the XCTest runner gets an alert, and the handler monitor block doesn't handle it properly (it doesn't recognize the alert you've specified to be the alert it encouters, or if your block tries to press buttons that the alert it discovers doesn't actually have), it will return NO, then attempt to dismiss the alert by following some undocumented default steps- it will try to press a Cancel button, and if none exists, then it will press any other default button on the alert. I've found that it's good practice to immediately check if the UI alert that crops up is the one that you're expecting:Rosabella
{ if ([locationAlert.label isEqualToString:@"Location Prompt"]) { [locationAlert.label.buttons[@"Allow"] tap]; return YES; } return NO; }]; Basically, you want to make sure that the system recognizes the alert as the UIElement that you specified in the handler monitor. It's essentially the same as working with any other non-alert element.Rosabella
What is the magic word for the address book access? Can it be done language independent so that a spanish simulator also will be happy under test?Departed
Did @JoeMasilotti 's advice to return false in your interruption monitor enable you to dismiss both system alerts? I am dealing with exactly the same thing-- furthermore, my alerts are for notifications and location services, so the buttons are different; I needed to include two interruption monitors. The solution worked wonderfully for a single system alert, but I cannot trigger the second interruption monitor.Screech
My experience is actually the same as ssrobbi, whether I use interruption monitors or not. The framework by default taps the "OK" button of whichever alert appears last. However the test does hit breakpoints inside the interruption monitor handler, and does appear to synthesize taps on the app. It would be really nice for debugging purposes to somehow see where the app is being tapped in the simulator while the app is working...Screech
@JonathanChen to answer your question, I tried it out and it didn't work for me.Paloma
In Xcode 7.3.1, I've found using 'app.swipeUp()' instead of 'app.tap()' to be more reliable. Otherwise though the XCode 7.1 approach continues to work for me.Diaz
@GlenT you are my absolute hero.Eun
Hi, above guys, what if I have localizations in my app? the app name and button title in alert will all be different under different languages, not only English.Isobar
@Isobar yes the problem is that you cannot put accessibility identifiers on default alert buttons, which would make the tap language independent. I believe there is an ugly hack possible to still put an identifier on the alert buttons, but that is not recommended, as it is messing with private Apple API. For language independence on alert buttons I fall back to using indexes, like myAlert.buttons.allElementsBoundByIndex[0].tap for the most left button.Insentient
how can you auto accept system alerts for unit tests ?Oliviaolivie
E
3

Gosh. It always taps on "Don't Allow" even though I deliberately say tap on "Allow"

At least

if app.alerts.element.collectionViews.buttons["Allow"].exists {
    app.tap()
}

allows me to move on and do other tests.

Elevation answered 20/10, 2016 at 7:53 Comment(0)
E
3

Objective - C

-(void) registerHandlerforDescription: (NSString*) description {

    [self addUIInterruptionMonitorWithDescription:description handler:^BOOL(XCUIElement * _Nonnull interruptingElement) {

        XCUIElement *element = interruptingElement;
        XCUIElement *allow = element.buttons[@"Allow"];
        XCUIElement *ok = element.buttons[@"OK"];

        if ([ok exists]) {
            [ok tap];
            return YES;
        }

        if ([allow exists]) {
            [allow tap];
            return YES;
        }

        return NO;
    }];
}

-(void)setUp {

    [super setUp];

    self.continueAfterFailure = NO;
    self.app = [[XCUIApplication alloc] init];
    [self.app launch];

    [self registerHandlerforDescription:@"“MyApp” would like to make data available to nearby Bluetooth devices even when you're not using app."];
    [self registerHandlerforDescription:@"“MyApp” Would Like to Access Your Photos"];
    [self registerHandlerforDescription:@"“MyApp” Would Like to Access the Camera"];
}

Swift

addUIInterruptionMonitorWithDescription("Description") { (alert) -> Bool in
    alert.buttons["Allow"].tap()
    alert.buttons["OK"].tap()
    return true
}
Ette answered 15/3, 2017 at 13:8 Comment(3)
I'm a bit confused reading the objective C sample: why do you register 3 handlers ? Wouldn't be one enough ?Unbar
@Unbar those are the examples. You can add as much or as less as you require.Ette
What is description in the swift example?Hypercritical
E
2

God! I hate how XCTest has the worst time dealing with UIView Alerts. I have an app where I get 2 alerts the first one wants me to select "Allow" to enable locations services for App permissions, then on a splash page the user has to press a UIButton called "Turn on location" and finally there is a notification sms alert in a UIViewAlert and the user has to select "OK". The problem we were having was not being able to interact with the system Alerts, but also a race condition where behavior and its appearance on screen was untimely. It seems that if you use the alert.element.buttons["whateverText"].tap the logic of XCTest is to keep pressing until the time of the test runs out. So basically keep pressing anything on the screen until all the system alerts are clear of view.

This is a hack but this is what worked for me.

func testGetPastTheStupidAlerts() {
    let app = XCUIApplication()
    app.launch()
    
    if app.alerts.element.collectionViews.buttons["Allow"].exists {
        app.tap()
    }

    app.buttons["TURN ON MY LOCATION"].tap()
}

The string "Allow" is completely ignored and the logic to app.tap() is called evreytime an alert is in view and finally the button I wanted to reach ["Turn On Location"] is accessible and the test pass

~Totally confused, thanks Apple.

Erst answered 4/8, 2016 at 6:55 Comment(0)
Q
2

For the ones who are looking for specific descriptions for specific system dialogs (like i did) there is none :) the string is just for testers tracking purposes. Related apple document link : https://developer.apple.com/documentation/xctest/xctestcase/1496273-adduiinterruptionmonitor


Update : xcode 9.2

The method is sometimes triggered sometimes not. Best workaround for me is when i know there will be a system alert, i add :

sleep(2)
app.tap()

and system alert is gone

Queridas answered 27/2, 2018 at 14:42 Comment(0)
T
1

The only thing I found that reliably fixed this was to set up two separate tests to handle the alerts. In the first test, I call app.tap() and do nothing else. In the second test, I call app.tap() again and then do the real work.

Tolu answered 17/10, 2016 at 23:30 Comment(1)
Its stupid, but its the only thing that worked for me as well. None of the other answers on here as of 10.20.17. Thank you.Leonialeonid
M
0

On xcode 9.1, alerts are only being handled if the test device has iOS 11. Doesn't work on older iOS versions e.g 10.3 etc. Reference: https://forums.developer.apple.com/thread/86989

To handle alerts use this:

//Use this before the alerts appear. I am doing it before app.launch()

let allowButtonPredicate = NSPredicate(format: "label == 'Always Allow' || label == 'Allow'")
//1st alert
_ = addUIInterruptionMonitor(withDescription: "Allow to access your location?") { (alert) -> Bool in
    let alwaysAllowButton = alert.buttons.matching(allowButtonPredicate).element.firstMatch
    if alwaysAllowButton.exists {
        alwaysAllowButton.tap()
        return true
    }
    return false
}
//Copy paste if there are more than one alerts to handle in the app
Mannos answered 20/12, 2017 at 13:42 Comment(0)
F
0

@Joe Masilotti's answer is correct and thanks for that, it helped me a lot :)

I would just like to point out the one thing, and that is the UIInterruptionMonitor catches all system alerts presented in series TOGETHER, so that the action you apply in the completion handler gets applied to every alert ("Don't allow" or "OK"). If you want to handle alert actions differently, you have to check, inside the completion handler, which alert is currently presented e.g. by checking its static text, and then the action will be applied only on that alert.

Here's small code snippet for applying the "Don't allow" action on the second alert, in series of three alerts, and "OK" action on the remaining two:

addUIInterruptionMonitor(withDescription: "Access to sound recording") { (alert) -> Bool in
        if alert.staticTexts["MyApp would like to use your microphone for recording your sound."].exists {
            alert.buttons["Don’t Allow"].tap()
        } else {
            alert.buttons["OK"].tap()
        }
        return true
    }
app.tap()
Foliar answered 11/4, 2018 at 14:8 Comment(0)
S
0

This is an old question but there is now another way to handle these alerts.

The system alert isn't accessibly from the app context of the app you are launched in, however you can access the app context anyway. Look at this simple example:

func testLoginHappyPath() {
    let app = XCUIApplication()
    app.textFields["Username"].typeText["Billy"]
    app.secureTextFields["Password"].typeText["hunter2"]
    app.buttons["Log In"].tap()
}

In a vacuum with a simulator already launched and permissions already granted or denied, this will work. But if we put it in a CI pipeline where it gets a brand new simulator, all of the sudden it won't be able to find that Username field because there's a notification alert popping up.

So now there's 3 choices on how to handle that:

Implicitly

There's already a default system alert interrupt handler. So in theory, simply trying to typeText on that first field should check for an interrupting event and handle it in the affirmative.

If everything works as designed, you won't have to write any code but you'll see an interruption logged and handled in the log, and your test will take a couple seconds more.

Explicitly via interruptionmonitor

I won't rewrite the previous work on this, but this is where you explicitly set up an interruptionmonitor to handle the specific alert being popped up - or whatever alerts you expect to happen.

This is useful if the built-in handler doesn't do what you want - or doesn't work at all.

Explicitly via XCUITest framework

In xCode 9.0 and above, you can switch between app contexts fluidly by simply defining multiple XCUIApplication() instances. Then you can locate the field you need via familiar methods. So to do this explicitly would look like the following:

func testLoginHappyPath() {
    let app = XCUIApplication()
    let springboardApp = XCUIApplication(bundleidentifier: "com.apple.springboard")

    if springboardApp.alerts[""FunHappyApp" would like permission to own your soul."].exists {
        springboardApp.alerts.buttons["Allow"].tap()
    }

    app.textFields["Username"].typeText["Billy"]
    app.secureTextFields["Password"].typeText["hunter2"]
    app.buttons["Log In"].tap()
}
Spiteful answered 20/8, 2020 at 23:14 Comment(0)
A
-1

Sounds like the approach to implementing camera access and notifications are threaded as you say, but not physically managed and left to chance when and how they are displayed.

I suspect one is triggered by the other and when it is programatically clicked it wipes out the other one as well (which Apple would probably never allow)

Think of it you're asking for a users permission then making the decision on their behalf? Why? Because you can't get your code to work maybe.

How to fix - trace where these two components are triggering the pop up dialogues - where are they being called?, rewrite to trigger just one, send an NSNotification when one dialogue has been completed to trigger and display the remaining one.

I would seriously discourage the approach of programatically clicking dialogue buttons meant for the user.

Altagraciaaltaic answered 24/8, 2015 at 13:53 Comment(3)
Thanks for the response! The reason I'm dismissing the dialog "for the user" is because this is a UI test case. Just like any other UI test cases that mimics user's interactions, I need my test to dismiss the two popups just like users will doNalepka
OK I understand now - still the two dialogue's are nested together to to complete your test you might have to decouple one from the other. I once had to do the same thing for a location check and notifications permission. I created a common area in the app which caught the notification from 1 dialogue dismissal then fired the second notification. I would take this approach. Good luck.Altagraciaaltaic
Hi latenitecoder, let's take one step back, as I don't think the two popups here are the real issue. Have you tried using Xcode UI testing to dismiss any system popup? Just one popup, not a nested situation. If so, what's the line of code you use to dismiss it? 'cause right now, I can't even get that work. What I need answered is simply - does the new Xcode UI testing feature has the ability to dismiss system alerts at all? If so, how to do it? There's no official documentation on this anywhere.Nalepka

© 2022 - 2024 — McMap. All rights reserved.