How to disable auto-complete when running Xcode UI Tests? - ios

As part of my UI Tests, I'm generating a random string as titles for my objects. The problem is that when this title is input via a keyboard (using XCUIElement.typeText()), iOS sometimes accepts an auto-suggested value instead.
For example, I may want it to type an auto generated string of "calg", but auto correct will choose "calf" instead. When I try to look for this value later on with an assertion, it doesn't exist and fails incorrectly.
Is there a way to tell the UI tests that they shouldn't be using auto correct, or are there an workarounds I can use?

Unless you need auto-suggest for any test scenarios, did you try turning off auto-correction in device/simulator settings.
Settings-> General -> Keyboard -> Auto-Correction

I don't believe you can turn off auto-correction through code from your UI Testing target.
You can, however, turn it off for the individual text view from your production code. To make sure auto-correction is still on when running and shipping the app, one solution would be to subclass UITextField and switch on an environment variable.
First set up your UI Test to set the launchEnvironment property on XCUIApplication.
class UITests: XCTestCase {
let app = XCUIApplication()
override func setUp() {
super.setUp()
continueAfterFailure = false
app.launchEnvironment = ["AutoCorrection": "Disabled"]
app.launch()
}
func testAutoCorrection() {
app.textFields.element.tap()
// type your text
}
}
Then subclass (and use) UITextField to look for this value in the process's environment dictionary. If it's set, turn auto-correction off. If not, just call through to super.
class TestableTextField: UITextField {
override var autocorrectionType: UITextAutocorrectionType {
get {
if NSProcessInfo.processInfo().environment["AutoCorrection"] == "Disabled" {
return UITextAutocorrectionType.No
} else {
return super.autocorrectionType
}
}
set {
super.autocorrectionType = newValue
}
}
}

Here is how I disabled it in my UI Test
app.textFields.element(boundBy: 0).tap()
let keyboards = app.keyboards.count
XCTAssert(keyboards > 0, "You need enable the keyboard in the simulator.")
app.buttons["Next keyboard"].press(forDuration: 2.1)
let predictiveOn = app.switches["Predictive"].value as! String == "1"
if predictiveOn {
app.switches["Predictive"].tap()
} else {
app.buttons["Next keyboard"].tap()
}
app.buttons["Next keyboard"].press(forDuration: 2.1)
let predictiveOff = app.switches["Predictive"].value as! String == "0"
XCTAssert(predictiveOff, "Predictive mode is not disabled")
app.buttons["Next keyboard"].tap()

Related

Changing keyboard language in iOS programmatically in XCUI test

I want to write test cases where i can change the keyboard language for each test case in a text field.
I am using XCUItest framework to write these test cases. But i am not able to change the keyboard language while running the test case.
I have gone through this link
iPhone: Change Keyboard language programmatically
and tried the below solution.
Here i am getting the return value but i am not able to use this to change the language in the text field while running the test.
class CustomTextField: UITextField {
private func getKeyboardLanguage() -> String? {
return "en" // here you can choose keyboard any way you need
}
override var textInputMode: UITextInputMode? {
if let language = getKeyboardLanguage() {
for tim in UITextInputMode.activeInputModes {
if tim.primaryLanguage!.contains(language) {
return tim
}
}
}
return super.textInputMode
}
}

How to write XCUI test for whether I navigate to correct screen or not?

I am writing test UI test case for following UI
I want to test on Login click whether I am navigating correctly on Dashboard screen or not.
Is there any method to do this?
My current testing code is like
func testExample() {
let usernameTextField = app.textFields["Username"]
usernameTextField.tap()
usernameTextField.typeText("abc#gmail.com")
let passwordTextField = app.textFields["Password"]
passwordTextField.tap()
passwordTextField.typeText("abc123")
app.buttons["Login" ].tap()
//let loginButton = app.staticTexts["Login"]
//XCTAssertEqual(loginButton.exists, true)
app.navigationBars["UIView"].buttons["Back"].tap()
}
UI Tests can become really fragile when depending on text values. What I encourage you to do is to set the Accessibility Identifier for your ViewController's view. That way, even if you change the title or change the whole layout, you can still be sure you're in the correct Page/Screen/View.
class DashVC: UIViewController {
override func viewDidLoad() {
view.accessibilityIdentifier = "view_dashboard"
}
}
func test_login_withValidInput_goesDashBoard() {
let app = XCUIApplication()
//...
app.buttons["Login" ].tap()
let dashBoardView = app.otherElements["view_dashboard"]
let dashBoardShown = dashBoardView.waitForExistence(timeout: 5)
XCTAssert(dashBoardShown)
}
Try this
app.buttons["Login - Login"].tap()
XCTAssertEqual(app.navigationBars.element.identifier, "appname.CalculationView") //If your second view controller is SecondViewController, your identifier is appname.SecondView.Like that my second view controller is CalculationViewController so my identifier is CalculationView
Try adding an accessibility indicator to the back button so you can check for availability using backButton.exists or backButton.hittable and assert accordingly. In any case, if you set
continueAfterFailure = false
in setUp(), your test will fail as it is if it doesn’t find a button with “Back”.

UI Tests - isSelected is always returning false

We have updated out Swift 2.3 project to Swift 3 recently using Xcode 8.2.1 (8C1002), and now most of our UI Tests related with tableViews and the isSelected property aren't working. It's always returning false, even when the object is selected (we can see it in the iOS Simulator).
Has anyone experienced similar issues? Our code used to work normally in Swift 2.3 before the conversion. Here is how we retrieve a tableView cell:
let cell = app.tables.cells.element(at: 4)
Note: app is a XCUIApplication.
And here is how we check if it's selected or not:
XCTAssert(cell.isSelected)
Another observation is that we are sure that the object exists because waitForExpectations is returning true:
let existsPredicate = NSPredicate(format: "exists = 1")
expectation(for: existsPredicate, evaluatedWith: cell, handler: nil)
waitForExpectations(timeout: 20, handler: nil)
EDIT: In order to replace isSelected, I've tried to use NSPredicate with selected = 1 and with isSelected = 1. None worked. I also tried to use acessibilityValue based in other question's answer, however it wasn't that simple since sometimes the items in my table view are selected/unselected programatically. Also, that method involved adding test code to the app, which isn't a good practice.
EDIT AFTER BOUNTY END: Since no one could find a solution for that problem and that's obviously a bug in Xcode, I've submitted a bug report to Apple. I will comment here when they release an Xcode version with the fix.
EXTRA EDIT: One day after my last edit, dzoanb came with a functional answer.
I made a few tests and a little research. You can check out the app created for this purpose >>here<<. It would be great if you could check it out (it required a little bit of work). There are also UI tests to prove it works. Also, two options are available, one is vanilla XCTest and one library with a lot of helpers I'm creating with my colleagues AutoMate. But that's not the point.
Here is what I found out:
1) isSelected property of XCUIElement depends on accessibilityTrait. Element to be selected in XCTest has to have UIAccessibilityTraitSelected set.
2) I couldn't reproduce Your problem but I was able to control isSelected property.
3) Yes, it requires a little bit of code, but should work well with VoiceOver if it is important for You.
All necessary code is in Your custom UITableViewCell subclass. And uses overriding UIAccessibilityElement accessibilityTraits property.
private var traits: UIAccessibilityTraits = UIAccessibilityTraitNone
// MARK: UITableViewCell life cycle
override func awakeFromNib() {
super.awakeFromNib()
traits = super.accessibilityTraits
}
// MARK: UIAccessibilityElement
override var accessibilityTraits: UIAccessibilityTraits {
get {
if isSelected {
return traits | UIAccessibilityTraitSelected
}
return traits
}
set {
traits = newValue
}
}
Hope it helps.
Couldn't get that code to compile under Swift 4.
This worked for me.
public override var accessibilityTraits: UIAccessibilityTraits {
get {
if isSelected {
return super.accessibilityTraits.union(.selected)
}
return super.accessibilityTraits
}
set {
super.accessibilityTraits = newValue
}
}
Have you tried making a break point before and after the tap, and check the value of the cell? Like the WWDC video here: https://youtu.be/7zMGf-0OnoU
(See from 10 minutes in)
isSelected only works on views which inherit from UIControl. UIControl.isSelected informs XCUIElement.isSelected.
Since UITableViewCell does not inherit from UIControl, you aren't seeing the value you want in your tests when you observe cell.isSelected.
I suggest that if you want this to be testable via UI tests that you file a feature request with Apple to make UIControl a protocol, which you could then extend your cells to conform to, or add UITableViewCell.isSelected to the properties that inform XCUIElement.isSelected.
#dzoanb solution can work without adding a private var:
override var accessibilityTraits: UIAccessibilityTraits {
get {
if isSelected {
return super.accessibilityTraits | UIAccessibilityTraitSelected
}
return super.accessibilityTraits
}
set {
super.accessibilityTraits = newValue
}
}

Vary User Defined Runtime Attributes for Traits

I have created a view using Storyboard which has rounded corners. However I'd like to remove this for iPhone. I'm using user defined runtime attributes to get this done and I wonder if I can make a variation for iPhone.
Edit: I know I could do this in code, but it sounds silly since it's something visual and the storyboard file sounds like the right place to do this variation.
extension CALayer {
var borderUIColor: UIColor {
set {
if UIDevice.current.model == "iPhone" {
self.borderColor = newValue.cgColor
}
// do whatever you want
}
get {
return UIColor(cgColor: self.borderColor!)
}
}
}
then use layer.borderUIColor to access the attribute.
Write an extension and redefine attributes, judge the device type in the set method would fix this problem! hope this fix your problem !
I don't think it's possible with Storyboards. You could just check for device type in code when loading that particular view and override the properties.
E.g.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if UIDevice.current.model == "iPhone" {
// reset attributes
}
}

How to hide keyboard in Swift app during UI testing

I just started with UI testing in Xcode 7 and hit this problem:
I need to enter text into a textfield and then click a button. Unfortunately this button is hidden behind the keyboard which appeared while entering text into the textfield. Xcode is trying to scroll to make it visible but my view isn't scrollable so it fails.
My current solution is this:
let textField = app.textFields["placeholder"]
textField.tap()
textField.typeText("my text")
app.childrenMatchingType(.Window).elementBoundByIndex(0).tap() // hide keyboard
app.buttons["hidden button"].tap()
I can do this because my ViewController is intercepting touches:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
view.endEditing(false)
super.touchesBegan(touches, withEvent: event)
}
I am not really happy about my solution, is there any other way how to hide the keyboard during UI testing?
If you have set up your text fields to resign FirstResponder (either via textField.resignFirstResponder() or self.view.endEditing(true)) in the textFieldShouldReturn() delegate method, then
textField.typeText("\n")
will do it.
Swift 5 helper function
func dismissKeyboardIfPresent() {
if app.keyboards.element(boundBy: 0).exists {
if UIDevice.current.userInterfaceIdiom == .pad {
app.keyboards.buttons["Hide keyboard"].tap()
} else {
app.toolbars.buttons["Done"].tap()
}
}
}
Based on a question to Joe's blog, I have an issue in which after a few runs on simulator the keyboards fails to hide using this piece of code:
XCUIApplication().keyboard.buttons["Hide keyboard"]
So, I changed it to: (thanks Joe)
XCUIApplication().keyboard.buttons["Hide keyboard"]
let firstKey = XCUIApplication().keys.elementBoundByIndex(0)
if firstKey.exists {
app.typeText("\n")
}
What I try to do here is detecting if the keyboard stills open after tap the hide button, if it is up, I type a "\n", which in my case closes the keyboard too.
This also happens to be tricky, because sometimes the simulator lost the focus of the keyboard typing and this might make the test fail, but in my experience the failure rate is lower than the other approaches I've taken.
I hope this can help.
I always use this to programmatically hide the keyboard in Swift UITesting:
XCUIApplication().keyboards.buttons["Hide keyboard"].tap()
XCUIApplication().toolbars.buttons["Done"].tap()
With Swift 4.2, you can accomplish this now with the following snippet:
let app = XCUIApplication()
if app.keys.element(boundBy: 0).exists {
app.typeText("\n")
}
The answer to your question lies not in your test code but in your app code. If a user cannot enter text using the on-screen software keyboard and then tap on the button, you should either make the test dismiss the keyboard (as a user would have to, in order to tap on the button) or make the view scrollable.
Just make sure that the keyboard is turned off in the simulator before running the tests.
Hardware->Keyboard->Connect Hardware Keyboard.
Then enter your text using the paste board
textField.tap()
UIPasteboard.generalPasteboard().string = "Some text"
textField.doubleTap()
app.menuItems["paste"].tap()
I prefer to search for multiple elements that are possibly visible to tap, or continue, or whatever you want to call it. And choose the right one.
class ElementTapHelper {
///Possible elements to search for.
var elements:[XCUIElement] = []
///Possible keyboard element.
var keyboardElement:XCUIElement?
init(elements:[XCUIElement], keyboardElement:XCUIElement? = nil) {
self.elements = elements
self.keyboardElement = keyboardElement
}
func tap() {
let keyboard = XCUIApplication().keyboards.firstMatch
if let key = keyboardElement, keyboard.exists {
let frame = keyboard.frame
if frame != CGRect.zero {
key.forceTap()
return
}
}
for el in elements {
if el.exists && el.isHittable {
el.forceTap()
return
}
}
}
}
extension XCUIElement {
///If the element isn't hittable, try and use coordinate instead.
func forceTap() {
if self.isHittable {
self.tap()
return
}
//if element isn't reporting hittable, grab it's coordinate and tap it.
coordinate(withNormalizedOffset: CGVector(dx:0, dy:0)).tap()
}
}
It works well for me. This is how I would usually use it:
let next1 = XCUIApplication().buttons["Next"]
let keyboardNext = XCUIApplication().keyboards.firstMatch.buttons["Next"]
ElementTapHelper(elements: [next1], keyboardElement: keyboardNext).tap()
Nice thing about this is you can provide multiple elements that could be tapped, and it searches for keyboard element first.
Another benefit of this is if you are testing on real devices the keyboard opens by default. So why not just press the keyboard button?
I only use this helper when there are multiple buttons that do the same thing, and some may be hidden etc.
If you are using IQKeyboardManager you can easily do this:
app.toolbars.buttons["Done"].tap()
This way you capture the "Done" button in the keyboard toolbar and hide the keyboard. It also works for different localizations.

Resources