37

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?

mokagio
  • 16,391
  • 3
  • 51
  • 58
leizeQ
  • 795
  • 1
  • 7
  • 18
  • If u have issue with keyboard hides button, u can just push the whole view up with `setContentOffset` till the button is visible, it doesnt matter if your view is scrollable or not – Tj3n Dec 03 '15 at 10:45
  • 3
    1. If you can't access a button when the keyboard is visible, isn't that a UX problem ? 2. These are just tests. Tests are expected to have hacky code so I don't see what's your problem here :) Just leave it, if it works. – michal.ciurus Dec 03 '15 at 15:35

11 Answers11

34

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.

bbjay
  • 1,728
  • 3
  • 19
  • 35
  • It works but in some cases I have error: Assertion Failure: :0: failed: Timed out after waiting 1.0s for KeyEventCompleted after sending event for ' – Degard Mar 19 '18 at 14:48
  • 2
    I simply added "\n" in the end of the string i'm sending to the textField and it worked like magic. Thank you. – Nisim Naim Aug 07 '19 at 07:42
  • What if I'm using TextEditor, what can I use then? `\n` just adds a new line in the editor, doesn't close the keyboard. – acmpo6ou Aug 03 '23 at 16:07
10

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.

katu
  • 101
  • 1
  • 2
10

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()
        }
    }
}
garritfra
  • 546
  • 4
  • 21
trishcode
  • 3,299
  • 1
  • 18
  • 25
  • 7
    Failed to get matching snapshot: No matches found for Descendants matching type Toolbar from input {( – JBarros35 Oct 08 '20 at 09:22
5

I always use this to programmatically hide the keyboard in Swift UITesting:

XCUIApplication().keyboards.buttons["Hide keyboard"].tap()
Charlie S
  • 4,366
  • 6
  • 59
  • 97
  • Doesn't work for me on iPhone. Could this be iPad only? – Mark Bridges Apr 29 '17 at 15:16
  • 1
    Value of type 'XCUIApplication' has no member 'keyboard'. @charlieSeligman Does the code works in the device or code works only for simulator? – Sujananth Mar 06 '18 at 15:41
  • This is only for the iPad. On the iPhone you can press the "done" button if the keyboard has it (it depends on the UI element), or if it doesn't have it, then the only way is to click somewhere else (e.g. on a label). – Ramy Al Zuhouri Nov 20 '19 at 18:02
5
XCUIApplication().toolbars.buttons["Done"].tap()
reutsey
  • 1,743
  • 1
  • 17
  • 36
5

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")
}
CodeBender
  • 35,668
  • 12
  • 125
  • 132
3

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.

Aaron Sofaer
  • 706
  • 4
  • 19
  • "make the test dismiss the keyboard" - Do you have any suggestions as to how to accomplish this? XCUIElement has no such notion as resignFirstResponder. – Rayfleck Jul 18 '16 at 20:31
  • app.keyboards.buttons["Hide keyboard"].tap() – Rayfleck Jul 18 '16 at 21:02
  • I believe you can tap away from keyboard to hide it. Something like app.tap() or app.swipeDown() should do it. – caffeinum Mar 09 '18 at 08:45
1

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.

gngrwzrd
  • 5,902
  • 4
  • 43
  • 56
1

By accident, I found the following solution on Apple's XCUITest docs:

// Dismiss keyboard
app.children(matching: .window).firstMatch.tap()

This looks very similar to the OP, but I'm not sure it's the same + it's the only "official" "by Apple" solution I've found.

Bartek Pacia
  • 1,085
  • 3
  • 15
  • 37
0

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()
365SplendidSuns
  • 3,175
  • 1
  • 21
  • 28
-1

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.

Kawe
  • 659
  • 8
  • 18