iOS UI Test: how to get message of UIAlertController - ios

My app has a login screen. If the user presses the login button without entering any text in either the username or password fields, the app will display a UIAlertController with an error message.
I am trying to model this logic in UI Tests, and want to assert that the UIAlertController is displaying the correct message. However, I can't find a way for the UI Test to access the message property of the alert. Here is the code generated by the test recorder:
func testLoginWithoutPasswort() {
let app = XCUIApplication()
let emailTextField = app.textFields["email"]
emailTextField.tap()
emailTextField.typeText("xxx#gmail.com")
app.buttons["Login"].tap()
app.alerts["Error"].collectionViews.buttons["OK"].tap()
}
Is there any way I can extract the String value of the alert's message, so I can put an assertion on it?

You can't directly test the alert's message. You can, however, test if the alert contains your error message's copy (at all).
For example, say your alert looks like this:
To assert that the alert contains the "Final Score" message, use:
XCTAssert(app.alerts.element.staticTexts["Final Score: 27 - 25"].exists)
You can also test the title of the alert directly:
XCTAssertEqual(app.alerts.element.label, "You won!")
More examples available in my UI Testing Cheat Sheet and Examples post and sample app.

I think it is: alert.elements()[2].name()
Inside onAlert callback function add alert.logElementTree() to see AlertView elements. It might be nil, maybe just title is shown.

Further to the answers above, which I struggled to get to work, there is another way.
Creata a Bool within your UItest method that is false:
var alertPressed = false
Then add a UIInterruptionMonitor and set the bool to true within it's closure:
addUIInterruptionMonitor(withDescription: "System Dialog") {
(alert) -> Bool in
alert.buttons["Allow"].tap()
alertPressed = true
return true
}
Then interact with the app again, and assert that the Bool is true
app.tap()
XCTAssert(alertPressed)
I hope this is helpful to someone.

Related

Switching between test and real url using conditional statement in swift

I want to use local urls for testing. How do I switch between test and real url at compile time?
I'm aware of Active Compilation Conditions but I don't want to use that as it's debug/release thing. I want to switch between test and real urls whenever I wish during my development phase and testing. To do so I want to have a flag which I can change before compilation.
Here is what I want to achieve, it's a pseudo code.
#define TEST=TRUE (or FALSE)
#if TEST
static let URL = "http://127.0.0.1/api/"
//... other code
#else
static let URL = "https:// domain.com/api/"
//... other code
#endif
Alright, then if you want to be able to change URL during development without making a new build, you have different options, but a quick one is always a hidden configuration popup or menu that you access from your first screen before doing login or whatever.
Where the hidden menu comes out: is something you want to decide but maybe, somewhere in a starting ViewController, or onboarding or login, you want to add a Gesture Recognizer for example to show your hidden menu, let's say a double tap anywhere in your ViewController.view which will present a hidden configuration alert only in a Debug build, and not a release, just before the login or any relevant api call:
#if DEBUG
let tap = UITapGestureRecognizer(target: self, action: #selector(presentHiddenConfigurationAlert))
tap.numberOfTapsRequired = 2
view.addGestureRecognizer(tap)
#endif
How the hidden menu looks like and act like: now as well in the same ViewController (so as in my example, a LoginVC or whatever you wish) you'd have the selector method on double tap as this one for example to show an alert that can let you change your current url (as always this can happen only in a Debuggable build):
#if DEBUG
#objc func presentHiddenConfigurationAlert() {
let currentURL = UserDefaults.string(forKey: "current_url")
let alertView = UIAlertController(title: "Hidden Configuration", message: "You are using \(currentURL)", preferredStyle: .alert)
// add a textview to let developer input the url he wants to use as an action
// or add some actions as buttons to choose between a Test URL button action and Dev URL button action
// store the URL as of "current_url" so you can retrieve it in an instance used across the app and here in the message
self.present(alert, animated: true, completion: nil)
}
#endif
And overall the use case is, in my example:
you install the app and are in a debuggable build
a login viewcontroller is the first thing that is shown (for example)
you or another developer using this build double tap anywhere in this viewcontroller
a hidden menu alert config is shown where you can change the current URL that is used across the app
you store this URL in user defaults or where you like most
now wherever you access the URL variable, you want that in debug you use the one from user defaults and in release you use a release one
#if DEBUG
var API_URL = getURLFromUserDefaults()
#else
var API_URL = "https://www.release-product-url.com"
#endif

Best practice for presenting App internal error to user?

I use guard statement and fatalError() a lot in my app to make sure data are in consistent state. They help to catch bugs in development phase. Now I'm in the late phase of the project and start to consider how to deal with those fatalError() calls in release build.
I don't want to remove them because they help to expose unknown bugs. I don't want to just leave them as is in the product release either because they would just abort the App, which doesn't provide user any helpful information about what went wrong. What I'd like to achieve is to show an error message on the screen and then abort when user press "OK". I think there may be two approaches to do it:
1) Don't call fatalError(). Throw error instead. Let the top level code handles the error (e.g., showing an alert). The issue with the approach is that it requires to change many functions to become throwable, which I think is inconvenient.
2) The second approach is that from what I read on the net, it's possible for code to create alert without access to the current view controller on screen. The trick is to create a new window. I haven't investigated the details yet.
My concern is that I think both approaches have same inherent limitation and are not applicable in all situations. For example, suppose there is something goes wrong in a UITableViewControler's data source delegate method, does it work to present an alert from within the delegate method? I doubt it.
So I wonder what's the common practice to present the fatal error message to user? Thanks for any suggestions.
similar to create a window, there is a way to get 'currentViewController', you can use it to show an alert anywhere.
{
let view = UIViewController.current.view
Alert.show(on: view, message: errorMsg)
//or just: Alert.show(error), handle it in Alert class
}
extension UIViewController {
class func current(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return current(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
return current(base: tab.selectedViewController)
}
if let presented = base?.presentedViewController {
return current(base: presented)
}
return base
}
}
for UITableView/UIScrollView/UICollectionView, you can use runtime swizzle method to add a placeholder image when there is no data or an error occored for all views. such as EmptyDataSet
recored the errors and save the log into a local file, upload it to your server if necessary, analyse them and help users to solve there problem.

How to acknowledge system alerts on a device with KIF testing framework?

I found out how to acknowledge system alerts while on a simulator from this post using this line of code:
self.viewTester.acknowledgeSystemAlert()
Unfortunately, the KIF code has #if TARGET_IPHONE_SIMULATOR wrapped around it, so it won't work on a device. How can I get around permission alerts on a device during automated testing?
I had same issue and here the solution I found:
its right than this KIF function doesn't work on device, its only for simulators! so, You can have a UITest in the UITarget and just a single Test case in it that will add a UIMonitors like this:
// ask for all the permission from users then :
_ = addUIInterruptionMonitor(withDescription: "") { alert -> Bool in
let confirmLabels = ["Allow", "OK"]
for (_, label) in confirmLabels.enumerated() {
let allow = alert.buttons[label]
if allow.exists {
allow.tap()
break
}
}
return true
}
// do some UI interaction here like tapping on some view in app
So you can call this UITest each time before running your UnitTests and that will prepare your app to have all the permissions.
btw, if anyone has better solution please provide cause I wanna know, too ;)

Is there any way to check element exists in XCTest other than .exists function?

Currently, I am working in XCTest iOS framework and using .exists function to check the element presence. Want to know if there is any other way to check the presence of element on UI as .exists is getting problem. Tests get successful on the first run but when it runs second time, it gets failed because script clicks the element which is not exist on the UI might be because elements loaded first time in app remains hidden but exists.
Please let me know any function which checks the current screen elements presence.
I too am looking for a way to wait until an element appears instead of using the sleep method. sometimes UI elements take longer to load, but that doesn't mean it won't appear, eventually. At which point you can check use the .exists
There is a XCUIApplication Class Reference, not hosted by apple, but its in OBJ-C:
http://masilotti.com/xctest-documentation/index.html
UPDATE: I think I found a solution from this link Delay/Wait in a test case of Xcode UI testing
Using the NSPredicate class worked for me.
let welcomePage = app.staticTexts["Landing Page"]
let existsPredicate = NSPredicate(format: "exists == 1")
expectationForPredicate(existsPredicate, evaluatedWithObject: LandingPageLoad, handler: nil)
waitForExpectationsWithTimeout(5, handler: nil)
First create a constant that of a value or object you're waiting for
Create another constant using the NSPredicate class to compare a value of an
object
Then use the expectationForPredicate method along with vars
And lastly give the handler a timeout upper limit
I refer to this answer and this XCUIElementQuery function can handle the case the element is not created:
open func matching(identifier: String) -> XCUIElementQuery
ref:
Testing if an element is visible with XCode 7 UITest

UIAutomation: Any way to dismiss "Would Like To Use Your Current Location" alert?

my app is using location services, and for automated testing, I want to be able to dismiss the "APP Would Like To Use Your Current Location" popup. However, when I try to do this in Instruments with a UIAutomation script, I get this error:
Fail: Could not start script, target application is not frontmost.
This kind of makes sense, because the alert is produced by a different process. But still, what is Apple's plan for helping people automate their tests in this situation?
**Try**
UIATarget.onAlert = function onAlert(alert)
{
return true;
}
alertTitle = target.frontMostApp().alert().name();
if(alertTitle==="APP Would Like To Use Your Current Location")
{
target.frontMostApp().alert().buttons()["OK"].tap();
}
The solution appears to be to use AppleScript, run after a sufficient delay to allow for the alert to appear. You tell it to click on the alert button in the window of the Simulator.
Use this code before you trigger the appearance of the Location request dialog. Note the special quotation marks around the app name.
UIATarget.onAlert = function onAlert(alert)
{
if( alert.name()=="“local” Would Like to Use Your Current Location" )
{
alert.buttons()["OK"].tap();
}
else
{
UIALogger.logFail( "Location request dialog expected.");
}
return true;
}

Resources