How to hide keyboard in Swift app during UI testing
Asked Answered
D

11

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?

Derisible answered 3/12, 2015 at 10:0 Comment(2)
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 notPend
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.Plantaineater
T
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.

Thrilling answered 8/8, 2016 at 17:32 Comment(3)
It works but in some cases I have error: Assertion Failure: <unknown>:0: failed: Timed out after waiting 1.0s for KeyEventCompleted after sending event for 'Praxiteles
I simply added "\n" in the end of the string i'm sending to the textField and it worked like magic. Thank you.Dancer
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.Etherealize
C
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.

Colour answered 24/10, 2016 at 18:29 Comment(3)
Working fine in all casesCheckerbloom
using this app github.com/Shashikant86/XCUITest-TAU I can't hide the keyboard in any way, not with \n nor with ["hide keyboard"]. very weirdFoetor
Same for me @JosePitaMake
C
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()
        }
    }
}
Carthusian answered 14/6, 2019 at 16:10 Comment(1)
Failed to get matching snapshot: No matches found for Descendants matching type Toolbar from input {(Imf
G
6
XCUIApplication().toolbars.buttons["Done"].tap()
Goon answered 14/9, 2017 at 17:34 Comment(2)
Does not work for all simulators. My tests work for iPhone 6 and 7 but not on iPhoneX.Garnes
Also doesn't work if the keyboard doesn't have a "Done", ie a numpad.Nanceenancey
T
6

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")
}
Trodden answered 20/12, 2018 at 20:24 Comment(0)
F
5

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

XCUIApplication().keyboards.buttons["Hide keyboard"].tap()
Futures answered 7/2, 2017 at 11:20 Comment(3)
Doesn't work for me on iPhone. Could this be iPad only?Winniewinnifred
Value of type 'XCUIApplication' has no member 'keyboard'. @charlieSeligman Does the code works in the device or code works only for simulator?Checkerbloom
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).Merline
B
5

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.

Boroughenglish answered 7/8, 2023 at 11:52 Comment(2)
I tested with Xcode 15 and not work, for the case num pad keyboardMinute
worked great thanks!Phelgen
P
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.

Polynices answered 19/4, 2016 at 20:31 Comment(3)
"make the test dismiss the keyboard" - Do you have any suggestions as to how to accomplish this? XCUIElement has no such notion as resignFirstResponder.Pressure
app.keyboards.buttons["Hide keyboard"].tap()Pressure
I believe you can tap away from keyboard to hide it. Something like app.tap() or app.swipeDown() should do it.Purdah
S
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.

Stilbestrol answered 27/8, 2019 at 23:38 Comment(0)
M
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()
Milter answered 19/6, 2016 at 11:7 Comment(0)
Q
-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.

Quintilla answered 16/2, 2022 at 12:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.