UITest cases to handle with location services alert - ios

I am writing UI test cases for my project.
My project flow is as below:
Login Screen. User enters credentials and press login.
Home Screen. There is location requirement so system as for user's permission. I allow it.
Logout.
So when I do fresh install of application this flow is recorded in test case and works if I perform on new fresh build.
But problem is when I test on old build there is no alert for location permission and the test's gets fail. How can I handle this cases or ask user for permission every time when I run tests?
For resetting credentials of user I am passing launchArguments to XCUIApplication() and handle in AppDelegate.
I have implemented code let me know if its correct way:
addUIInterruptionMonitor(withDescription: "Allow “APP” to access your location?") { (alert) -> Bool in
alert.buttons["Only While Using the App"].tap()
return true
}
The above code works for both if alert comes or not.

Using an interruption monitor is the correct way. However, it's safer to check if the alert being displayed is the alert you're expecting before you interact with the alert:
addUIInterruptionMonitor(withDescription: "Allow “APP” to access your location?") { (alert) -> Bool in
let button = alert.buttons["Only While Using the App"]
if button.exists {
button.tap()
return true // The alert was handled
}
return false // The alert was not handled
}

Try this
let app2 = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let button = app2.alerts.firstMatch.buttons["Allow While Using App"]
button.waitForExistence(timeout: 10)
button.tap()

I use the following code to allow user's location:
// MARK: - Setup
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launch()
addUIInterruptionMonitor(withDescription: "System Dialog") { (alert) -> Bool in
alert.buttons["Allow Once"].tap()
return true
}
}
In this setup, I "register" the interruption monitor for tapping the allow button, so in this case I can dismiss that modal. Now, there's my test:
// MARK: - Test change mall
func testChangeMall() {
let selectorChangeButton = app.buttons["change_mall_button"]
XCTAssert(selectorChangeButton.exists, "Selector change button does not exist")
selectorChangeButton.tap()
app.navigationBars.firstMatch.tap()
let cell = app.staticTexts["Shopping Centre"]
XCTAssert(cell.exists, "There's no cell with this title")
cell.tap()
sleep(1)
let label = app.staticTexts["Shopping Centre"]
XCTAssert(label.exists, "Nothing changes")
}
In this test, simply I go to a view controller with a list sorted by location. First, I need to dismiss the location's system alert. So, first I dismiss that modal and then I tap a cell from my TableView. Then, I need to show it in my main view controller so I dismiss my view controller and I expect the same title.
Happy Coding!

Related

How to know if user shared my app or not?

My app is all about getting points and winning money, and what I need to do is let the user share my app then give him some points.
The problem is that I don't know how to detect if the user truly shared the app or not
I'm using the following code:
func shareTapped(){
let text = "example"
let url = URL(string: "example.com")
let image = UIImage(named: "example_image")
let shareViewController = UIActivityViewController(activityItems: [text, image!, url!] ,applicationActivities: nil)
self.present(shareViewController, animated: true, completion: {() in
print("done")
})
}
The sharing method is working perfectly, but I was wondering if there is any delegate we can call in this situation.
Thanks.
So there is 2 scenarios where user can cancel share.
One is when UIActivityViewController present then there is a cancel button on UIActivityViewController from where user can cancel it and yes you can detect it with method
shareViewController.completionWithItemsHandler = { activity, completed, items, error in
}
In above method completed will be false if user canceled from UIActivityViewController cancel button. and it will return true if user share successfully but here comes second case with it.
So for second case suppose user want to share via watsapp and click on watsapp icon from UIActivityViewController and watsapp user list appear.
But there is a cancel button on that screen from where user can cancel sharing but still you will get completed flag true so there is no way you can detect if user click on cancel button from watsapp user list.

iOS Firebase sign in. Show activity indicator after Google account choosing

I have a ViewController with a Sign in button used to sign in into Firebase with a Google Account:
GIDSignIn.sharedInstance().signIn()
When I click the button, this appears:
Google account choosing
After selecting an account and if the authentication is successful, I want to load a second ViewController. For this, I have a listener in the first ViewController that will sign in again when the authentication state changes, this time successfully, without asking the account again and sending me directly to the second ViewController:
Auth.auth().addStateDidChangeListener({ auth, user in
if let _ = user {
GIDSignIn.sharedInstance().signIn()
}
})
The problem is that I want an activity indicator to be shown when I go back to the first ViewController from the account chooser. Because the app may be there for a few seconds during the authentication process and I don't want the user to tap again the Sign In button, while the first signing in hasn't already finished.
I need a way to recognise that a signing in process is taking place, to show an activity indicator that locks the screen to prevent the user from tapping again Sign in.
WORKAROUND 1
When I click the Sign in with Google button, I set an UserDefaults integer as 1. Then, when the ViewController is reloaded after the Google account chooser, I look for this integer and if it's 1, I don't stop the activity Indicator.
Because I want the activity indicator shown since the user clicks the button until the authentication is completed.
When button is clicked I do:
GIDSignIn.sharedInstance().signIn()
UserDefaults.standard.set(1, forKey: "signingIn")
UserDefaults.standard.synchronize()
In viewWillAppear I do:
if let _ = user {
GIDSignIn.sharedInstance().signIn()
} else {
if UserDefaults.standard.integer(forKey: "signingIn") != 1 {
self.stopActivityIndicator()
} else {
UserDefaults.standard.set(0, forKey: "signingIn")
UserDefaults.standard.synchronize()
}
}
When the authentication is completed, in GIDSignInDelegate there is the function that will be called. In this function, the activity indicator must be stopped:
// The sign-in flow has finished and was successful if |error| is |nil|.
- (void)signIn:(GIDSignIn *)signIn didSignInForUser:(GIDGoogleUser *)user withError:(NSError *)error;
WORKAROUND 2
I do a put the signIn Google function into a completion handler but it doesn't work:
self.completionHandlerSigIn {
self.stopActivityIndicator()
}
And the function is this:
func completionHandlerSigIn(completion: () -> Void) {
GIDSignIn.sharedInstance().signIn()
}
The problem is that the view is reloaded during the sign in process, after the account choosing. I need a way to recognize that I come from the Google Account choosing screen.
Just show the loading indicator right when the user clicks sign in, then hide it either when the authentication process returns with error or after processing the result. I don't use google sign in, but I can give you my example with Twitter.
#IBAction func onTwitterClicked(_ sender: UIButton) {
AuthManager.shared.loginWithTwitter(self)
}
Here is the loginWithTwitter method in AuthManager:
func loginWithTwitter(_ viewController:BaseController) {
self.provider = .twitter
viewController.showLoadingPanel()
TWTRTwitter.sharedInstance().logIn(completion: {session, error in
guard (error == nil) else {
viewController.hideLoadingPanel()
viewController.showInfoAlert("Oops", error!.localizedDescription, nil)
return
}
let credential = TwitterAuthProvider.credential(withToken: session!.authToken, secret: session!.authTokenSecret)
self.auth.signIn(with: credential, completion: {user, error in
viewController.hideLoadingPanel()
guard error == nil else {
viewController.showInfoAlert("Oops", error!.localizedDescription, nil)
return
}
self.tryConfirmUserInFirestore(user, viewController)
})
})
}

dismiss location services request dialog

at the start of my UI test I have
addUIInterruptionMonitor(withDescription: "Location Dialog") { (alert) -> Bool in
let button = alert.buttons["Allow"]
if button.exists {
snapshot("request location service")
button.tap()
return true
}
return false
}
which should dismiss the location services request dialog, but it does nothing and it never reaches the handler. I have also tried to set this code in setUp() but it didn't work either.
I think the problem might be that the first thing that happens in the app is that the dialog is being shown, it may be too soon (it may happen before addUIInterruptionMonitor is called)
How can I solve this issue?
You have to interact with the app right after adding the UIInterruptionMonitor. This can be a simple tap:
addUIInterruptionMonitor(withDescription: "Location Dialog") { (alert) -> Bool in
let button = alert.buttons["Allow"]
if button.exists {
button.tap()
return true
}
return false
}
// interact with the app
app.tap()
If app.tap() interferes with your test you could also use app.swipeUp()
Be aware that the location service permission dialog changed in iOS11. There are now 3 Buttons, so you have to use alert.buttons["Always Allow"] to dismiss the dialog.

Handler of addUIInterruptionMonitor is not called for Alert related to Photos

private func acceptPermissionAlert() {
_ = addUIInterruptionMonitor(withDescription: "") { alert -> Bool in
if alert.buttons["Don’t Allow"].exists { //doesnt get here second time
alert.buttons.element(boundBy: 1).tapWhenExists()
return true
}
return false
}
}
and this doesn't work for:
In the beginning of the app, it works perfect while accepting permission for notifications, but here, it doesn't work. Why is this?
I'vs found that addUIInterruptionMonitor sometimes doesn't handle an alert in time, or until tests have finished. If it isn't working, try using Springboard, which manages the iOS home screen. You can access alerts, buttons, and more from there, and this is particularly useful for tests where you know exactly when an alert will show.
So, something like this:
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let alertAllowButton = springboard.buttons.element(boundBy: 1)
if alertAllowButton.waitForExistence(timeout: 5) {
alertAllowButton.tap()
}
The buttons.element(boundBy:1) will ensure you tap the button on the right, change 1 to 0 to tap the left, (because sometimes the ' in "Don't Allow" causes a problem).
Add:
app.tap()
at the end of the method.
This is because you need to interact with the app for the handler to fire.
After adding the interruption monitor, you should continue to interact with the app as if it has not appeared.
Also note that you have a 'smart quote' in your button identifier, instead of a regular apostrophe.
let photosAlertHandler = addUIInterruptionMonitor(withDescription: "Photo Permissions") { alert -> Bool in
if alert.buttons["Don't Allow"].exists {
alert.buttons.element(boundBy: 1).tapWhenExists()
return true
}
return false
}
// Do whatever you want to do after dismissing the alert
let someButton = app.buttons["someButton"]
someButton.tap() // The interruption monitor's handler will be invoked if the alert is present
When the next interaction happens after the alert appears, the interruption monitor's handler will be invoked and the alert will be handled.
You should also remove the interruption monitor when you think you're done with it, otherwise it will be invoked for any other alerts that appear.
removeUIInterruptionMonitor(photosAlertHandler)

iOS UITesting : Handle all system prompt automatically with addUIInterruptionMonitorWithDescription

I have read through this two.
Xcode7 | Xcode UI Tests | How to handle location service alert?
Xcode 7 UI Testing: Dismiss Push and Location alerts
Could I know the following?
1) For location, putting "Location Dialog" indicate that it gonna handle for location prompt. How does it recognise?
2) How to handle system prompt for accessing photo library or camera? Is there any list for handler description?
here the xcode-documentation of addUIInterruptionMonitorWithDescription.
/*! 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.
#param handlerDescription Explanation of the behavior and purpose of this handler, mainly used for debugging and analysis.
#param 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.
*/
public func addUIInterruptionMonitorWithDescription(handlerDescription: String, handler: (XCUIElement) -> Bool) -> NSObjectProtocol
1) "Location Dialog" is just a handlerDescription for you to identifie what alert you handle. You can write somethings else.
2) You have to use the same method. Just tap the app after.
Here i use this part of code to handle Push notifications:
addUIInterruptionMonitorWithDescription("Push notifications") { (alert) -> Bool in
if alert.buttons["OK"].exists {
alert.buttons["OK"].tap()
return true
}
return false
}
app.tap()
Cheers
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

Resources