UI Test deleting text in text field
Asked Answered
F

21

97

In my test I have a text field with a pre-existing text. I want to delete the content and type a new string.

let textField = app.textFields
textField.tap()
// delete "Old value"
textField.typeText("New value")

When deleting string with hardware keyboard Recording generated for me nothing. After doing the same with software keyboard I got:

let key = app.keys["Usuń"] // Polish name for the key
key.tap()
key.tap() 
... // x times

or

app.keys["Usuń"].pressForDuration(1.5)

I was worried that my test is language-dependent so I have created something like this for my supported languages:

extension XCUIElementQuery {
    var deleteKey: XCUIElement {
        get {
            // Polish name for the key
            if self["Usuń"].exists {
                return self["Usuń"]
            } else {
                return self["Delete"]
            }
        }
    }
}

It looks nicer in code:

app.keys.deleteKey.pressForDuration(1.5)

but it is very fragile. After quitting from Simulator Toggle software keyboard was reset and I've got a failing test. My solution doesn't work well with CI testing. How can this be solved to be more universal?

Freiburg answered 28/9, 2015 at 11:28 Comment(5)
I can't reproduce the failure you are experiencing. I added the same extension, switched my simulator's language to Polish, and verified that the "Usún" key is getting tapped. Restarting/resetting the simulator doesn't seem to have any affect on the Toggle software keyboard setting. Is there something else in the text view that could be hiding/dismissing the keyboard?Dimercaprol
Maybe after restarting system I've got Toggle software keyboard reset. It is not default for the simulator (and I don't know if this can be changed). This way or the other my method isn't reliable until you can fix language and software keyboard settings from the level of the test (or testing scheme).Petersburg
My own comment gave me an idea and I have found language setting in Scheme > Options > Application Language. Keyboard issue still remains unresolved.Petersburg
Nice! Does that fix the failure?Dimercaprol
Fixed only localized delete key name problem.Petersburg
R
175

I wrote an extension method to do this for me and it's pretty fast:

extension XCUIElement {
    /**
     Removes any current text in the field before typing in the new value
     - Parameter text: the text to enter into the field
     */
    func clearAndEnterText(text: String) {
        guard let stringValue = self.value as? String else {
            XCTFail("Tried to clear and enter text into a non string value")
            return
        }

        self.tap()

        let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count)

        self.typeText(deleteString)
        self.typeText(text)
    }
}

This is then used pretty easily: app.textFields["Email"].clearAndEnterText("[email protected]")

Robbins answered 1/10, 2015 at 18:6 Comment(21)
You can create "delete string" in more functional fashion with: let deleteString = stringValue.characters.map { _ in "\u{8}" }.joinWithSeparator("")Petersburg
Oh awesome! I didn't know they had added joinWithSeparator in Swift 2. Thanks!Robbins
Consider incorporating functional approach into your answer for better readability.Petersburg
For longer text I have encouraged problem with Connect Hardware Keyboard set on. Please consider adding small bash script before running tests, then this method works like a charm! https://mcmap.net/q/143143/-ui-testing-failure-neither-element-nor-any-descendant-has-keyboard-focus-on-securetextfieldLimen
Where does \u{8} come from? I replaced this with deleteString += XCUIKeyboardKeyDelete (which evaluates to \u{7f}).Propel
@Propel it's the UTF8 decimal value for Backspace: w3schools.com/charsets/ref_utf_basic_latin.asp . But I didn't know about XCUIKeyboardKeyDelete which is neat!Robbins
Works great for text fields that have prediction / spelling check disabled. Otherwise you might have to deal with spelling suggestion alerts as characters get deleted.Deas
joinWithSeparator was changed in Swift 3 let deleteString = stringValue.characters.map { _ in XCUIKeyboardKeyDelete }.joined(separator: "")Nakamura
I am unsure why but no matter what I try, this doesn't seem to delete the entire text in the text field even when I inspect it and see the same number of delete characters as there are characters to delete. It's seemingly random and deletes different amounts of characters each iteration.Conformance
For swift4, use XCUIKeyboardKey.delete.rawValueQuaternity
For Swift 4, you can just use let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.characters.count)Biblio
@Stasel, Daniel XCUIKeyboardKey does not have a delete member. In addition this will not work because the usage of characters is deprecated and xcode will show an error preventing you from running your app.Aigneis
joinedWithSeparator was renamed to joined(separator:"")Charteris
https://mcmap.net/q/217111/-ui-test-deleting-text-in-text-field in Xcode 9 for empty fields, there is a bug where value == placeholderValue.Fluoro
Late to the party, but if you want to go functional, the least introsive would be reduce... stringValue.reduce("") { current, _ in current + XCUIKeyboardKey.delete.rawValue }Ungrudging
For Swift 4.2: let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count)Maggot
This doesn't seem to work if the software keyboard is not showing (which is the case after resetting a simulator)Ballance
On iPhone X this script is broken, it's not selecting all the text.Destine
Thanks, you made my day (Swift 5)Hartman
If you select your TextField based on its content then this will fail because the second typeText() will re-evaluate the query and fail to find it (since it was just cleared out). To solve that you can just combine these into a single action: typeText("\(deleteString)\(text)")Hume
Only solution that works for me for long text on iOS 16 & Xcode 14: https://mcmap.net/q/217111/-ui-test-deleting-text-in-text-fieldEqually
F
27

this will work for textfield and textview

for SWIFT 3

extension XCUIElement {
    func clearText() {
        guard let stringValue = self.value as? String else {
            return
        }

        var deleteString = String()
        for _ in stringValue {
            deleteString += XCUIKeyboardKeyDelete
        }
        self.typeText(deleteString)
    }
}

for SWIFT 4, SWIFT 5

extension XCUIElement {
    func clearText() {
        guard let stringValue = self.value as? String else {
            return
        }

        var deleteString = String()
        for _ in stringValue {
            deleteString += XCUIKeyboardKey.delete.rawValue
        }
        typeText(deleteString)
    }
}

UPDATE XCODE 9

There is an apple bug where if the textfield is empty, value and placeholderValue are equal

extension XCUIElement {
    func clearText() {
        guard let stringValue = self.value as? String else {
            return
        }
        // workaround for apple bug
        if let placeholderString = self.placeholderValue, placeholderString == stringValue {
            return
        }

        var deleteString = String()
        for _ in stringValue {
            deleteString += XCUIKeyboardKey.delete.rawValue
        }
        typeText(deleteString)
    }
}
Fluoro answered 22/11, 2017 at 5:54 Comment(6)
Wow, you can predict until Swift 99?Quaternity
@Quaternity haha was just annoyed that we had to change it for swift 2, 3, 4.Fluoro
@SujitBaranwal ok will check whats changedFluoro
This code works fine on my simulator and device but when I test the same on browserstack which is a cloud based platform for testing on real devices remotely, the first guard statement returns from there only. Meaning the text inside my textfield isn't getting recognized as string. I am sure the text is there because the test cases are recorded as well. Any help ?Wirra
Doesn't work for longer strings in Xcode 15 beta.Formicary
XCode 9 update works in XCode 15.4Plasticizer
B
22

Since you fixed your localized delete key name problem in the comments of your questions, I'll assume you can access the delete key by just calling it "Delete".

The code below will allow you to reliably delete the contents of your field:

while (textField.value as! String).characters.count > 0 {
    app.keys["Delete"].tap()
}

or Swift 4+:

while !(textView.value as! String).isEmpty {
    app.keys["Delete"].tap()
}

But at the same time, your issue might indicate the need to solve this more elegantly to improve the usability of your app. On the text field you can also add a Clear button with which a user can immediately empty the text field;

Open the storyboard and select the text field, under the attributes inspector find "Clear button" and set it to the desired option (e.g. is always visible).

Clear button selection

Now users can clear the field with a simple tap on the cross at the right of the text field:

Clear button

Or in your UI test:

textField.buttons["Clear text"].tap()
Brindabrindell answered 29/9, 2015 at 10:53 Comment(8)
I like the idea with clear button, but I'm still looking a way to force the software keyboard in tests. As if it comes to while loop - every tap has delay after it, so it makes tests slow for long strings.Petersburg
+1 Yes! Use the process of writing the tests to improve the software. Maybe it didn't quite meet the OP's requirements but it's exactly the pointer I needed when I hit a similar issue.Testicle
This gives me "UI Testing Failure - No matches found for "Delete" Key" in Xcode 7.2Octavie
Changing app behaviour in flavour to it's testing is a no-no IMHO. @bay.philips answer seems way more legit for this purpose.Styptic
I wrote this under the assumption you're writing tests for actual use-cases instead of just to do random things in your tests. Using every bit of feedback you can get to improve usability leads to better apps for users.Brindabrindell
I agree that you can find things in testing that help you improve the app, and if that's the case, do it. However, don't change the app just to get a test to work (because that change could break something else in your app).Ferrin
I like this approach thanks. Worth noting that currently (Xcode 10.1) the key you need is delete (no leading uppercase) and the keyboard must be visible on the device.Highbinder
There's a potential issue with the first example: if the cursor lands not at the end of the text field then it will delete to the start of the field but there will still be characters after it – the while loop break condition will never be met, and you'll be on the train to Infinite Loop City.Riesling
M
15

I found following solution:

let myTextView = app.textViews["some_selector"]
myTextView.pressForDuration(1.2)
app.menuItems["Select All"].tap()
app.typeText("New text you want to enter") 
// or use app.keys["delete"].tap() if you have keyboard enabled

When you tap and hold on the text field it opens menu where you can tap on "Select all" button. After that all you need is to remove that text with "delete" button on the keyboard or just enter new text. It will overwrite the old one.

Minstrel answered 9/2, 2016 at 12:56 Comment(2)
you can also use myTextView.doubleTap() to bring up the menu, which might be a little fasterRox
This doesn't always work as sometimes long pressing can highlight a word inside the text field and this results in "Select all" button not being visibleVulcan
H
8

Xcode 9, Swift 4

Tried the solutions above, but none worked due to some weird behavior on tap - it moved the cursor to either beginning of the text field, or at some random point in text. The approach I used is what @oliverfrost described here, but I've added some touches to work around the issues and combine it in a neat extension. I hope it can be useful for someone.

extension XCUIElement {
    func clearText(andReplaceWith newText:String? = nil) {
        tap()
        tap() //When there is some text, its parts can be selected on the first tap, the second tap clears the selection
        press(forDuration: 1.0)
        let selectAll = XCUIApplication().menuItems["Select All"]
        //For empty fields there will be no "Select All", so we need to check
        if selectAll.waitForExistence(timeout: 0.5), selectAll.exists {
            selectAll.tap()
            typeText(String(XCUIKeyboardKey.delete.rawValue))
        }
        if let newVal = newText { typeText(newVal) }
    }
}

Usage:

let app = XCUIApplication()
//Just clear text
app.textFields["field1"].clearText() 
//Replace text    
app.secureTextFields["field2"].clearText(andReplaceWith: "Some Other Text")
Haaf answered 12/5, 2018 at 20:31 Comment(0)
W
8

You can use doubleTap to select all text and type new text to replace:

extension XCUIElement {
  func typeNewText(_ text: String) {
    if let existingText = value as? String, !existingText.isEmpty {
      if existingText != text {
        doubleTap()
      } else {
        return
      }
    }

    typeText(text)
  }
}

Usage:

textField.typeNewText("New Text")
Wholewheat answered 1/12, 2020 at 2:30 Comment(2)
Best solution so farKodak
This won't work if the text has punctuation that prevents a double-tap from selecting the entire string, such as a UUID like 0ac8c806-3f43-4d00-bc73-56073142950a--depending on where the insertion point is (and as far as I can tell, this is nondeterministic), only a segment between -s will be selected.Libertylibia
C
4

So, I didn't found any good solution yet :/

And I don't like locale dependent solutions, like above with explicit "Clear text" lookup.

So, I do type check, then trying to find clear button in the text field It works well unless you have custom text field with more that one button

My best now is (I have no custom text fields with more buttons):

    class func clearTextField(textField : XCUIElement!) -> Bool {

        guard textField.elementType != .TextField else {
            return false
        }

        let TextFieldClearButton = textField.buttons.elementBoundByIndex(0)

        guard TextFieldClearButton.exists else {
            return false
        }

        TextFieldClearButton.tap()

        return true
    }
Carrizales answered 6/11, 2015 at 14:53 Comment(0)
V
4

I found a solution that uses an iOS feature that selects the the entirety of the text fields by tapping it several times. We then delete the text field by typing any character or by pressing delete (if keyboard is enabled).

let myTextView = app.textViews["some_selector"]
myTextView.tap(withNumberOfTaps: 2, numberOfTouches: 1)
app.typeText("New text you want to enter") 
// or use app.keys["delete"].tap() if you have keyboard enabled
Vulcan answered 11/12, 2019 at 15:0 Comment(5)
Looks like a quadruple-tap is the magic number per AppleForsberg
But, I sometimes find I need to do a .tap() beforehand to set focus. Here's my code snippet.Forsberg
let toSelectWord = 2 // Select a word: Double-tap the word with one finger. let toSelectSentence = 3 // Select a sentence: Triple-tap the sentence with one finger. let toSelectParagraph = 4 // Select a paragraph: Quadruple-tap with one finger. app.textFields["sd.Title"].tap() app.textFields["sd.Title"].tap(withNumberOfTaps: toSelectParagraph, numberOfTouches: 1) app.textFields["sd.Title"].typeText("Old Task")Forsberg
Just withNumberOfTaps: 2 is sufficient.Sarette
I found a triple tap works best. Double tap only selects one word. I've found it to be a bit flakey but so far so good with triple tap.Towny
E
4

This is the only solution that works for me reliably with text that may be partially hidden in the text view due to its length. Tested on iOS 16 and Xcode 14.

extension XCUIElement {
    /// Removes any current text in the field before typing in the new value and submitting
    /// Based on: https://stackoverflow.com/a/32894080
    func clear() {
        if self.value as? String == nil {
            XCTFail("Tried to clear and enter text into a non string value")
            return
        }

        // Repeatedly delete text as long as there is something in the text field.
        // This is required to clear text that does not fit in to the textfield and is partially hidden initally.
        // Important to check for placeholder value, otherwise it gets into an infinite loop.
        while let stringValue = self.value as? String, !stringValue.isEmpty, stringValue != self.placeholderValue {
            // Move the cursor to the end of the text field
            let lowerRightCorner = self.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.9))
            lowerRightCorner.tap()
            let delete = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count)
            self.typeText(delete)
        }
    }

    func clearAndEnterText(text: String) {
        self.clear()
        // new line at end submits
        self.typeText("\(text)\n")
    }
}

Equally answered 25/9, 2022 at 20:3 Comment(0)
K
2

Do this to delete the current string value in a text box without relying on virtual keyboard.

//read the value of your text box in this variable let textInTextField:String =

  let characterCount: Int = textInTextField.count
  for _ in 0..<characterCount {
    textFields[0].typeText(XCUIKeyboardKey.delete.rawValue)
  }

good thing about this solution is that it works regardless of simulator has virtual keyboard or not.

Karmen answered 20/6, 2018 at 6:24 Comment(0)
B
2

Now in swift 4.2 maybe you should try the following code:

extension XCUIElement {
    /**
     Removes any current text in the field before typing in the new value
     - Parameter text: the text to enter into the field
     */
    func clearAndEnterText(text: String) {
        guard let stringValue = self.value as? String else {
            XCTFail("Tried to clear and enter text into a non string value")
            return
        }

        self.tap()
        for _ in 0..<stringValue.count {
            self.typeText(XCUIKeyboardKey.delete.rawValue)
        }

        self.typeText(text)
    }
}
Bulwark answered 5/12, 2018 at 20:11 Comment(1)
@eonist - disable the hardware keyboard and I think it'll work. See: https://mcmap.net/q/143143/-ui-testing-failure-neither-element-nor-any-descendant-has-keyboard-focus-on-securetextfieldAmbassadress
S
2

For those who are still using Objective-C

@implementation XCUIElement (Extensions)

-(void)clearText{
    if (!self){
        return;
    }
    if (![self.value isKindOfClass:[NSString class]]){
        return;
    }
    NSString* stringValue = (NSString*)self.value;
    for (int i=0; i<stringValue.length ; i++) {
        [self typeText:XCUIKeyboardKeyDelete];
    }
}

@end
Ship answered 8/1, 2019 at 10:19 Comment(0)
B
0

I had some difficulty getting the above solutions to work for a similar problem I was having: The curser would place itself before text and then work backwards from there. Additionally, I wanted to check that the textfield had text in it before deleting. Here's my solution inspired by the extension https://stackoverflow.com/users/482361/bay-phillips wrote. I should note that the tapping the delete key can take a long time, and it can be substituted with .pressForDuration

func clearAndEnterText(element: XCUIElement, text: String) -> Void
    {
        guard let stringValue = element.value as? String else {
            XCTFail("Tried to clear and enter text into a non string value")
            return
        }

        element.tap()

        guard stringValue.characters.count > 0 else
        {
            app.typeText(text)
            return
        }

       for _ in stringValue.characters
        {
            app.keys["delete"].tap()
        }
        app.typeText(text)
    }
Bittersweet answered 27/6, 2016 at 15:13 Comment(0)
E
0

I am new to UI testing with iOS but I was able to clear text fields with this simple workaround. Working with Xcode8 and plan on refactoring this soon:

func testLoginWithCorrectUsernamePassword() {
      //Usually this will be completed by Xcode
    let app = XCUIApplication()
      //Set the text field as a constant
    let usernameTextField = app.textFields["User name"]
      //Set the delete key to a constant
    let deleteKey = app.keys["delete"]
      //Tap the username text field to toggle the keyboard
    usernameTextField.tap()
      //Set the time to clear the field.  generally 4 seconds works
    deleteKey.press(forDuration: 4.0);
      //Enter your code below...
}
Eddi answered 22/6, 2017 at 19:35 Comment(0)
O
0

I used what @oliverfrost described but it wasn't working on IPhone XR, I changed it a little for my own use, to this

extension XCUIElement {
func clearText(andReplaceWith newText:String? = nil) {
    tap()
    tap() //When there is some text, its parts can be selected on the first tap, the second tap clears the selection
    press(forDuration: 1.0)
    let select = XCUIApplication().menuItems["Select"]
    //For empty fields there will be no "Select All", so we need to check
    if select.waitForExistence(timeout: 0.5), select.exists {
        select.tap()
        typeText(String(XCUIKeyboardKey.delete.rawValue))
    }
    if let newVal = newText { typeText(newVal) }
}
}

and as @zysoft said, you can use it like:

let app = XCUIApplication()
//Just clear text
app.textFields["field1"].clearText() 
//Replace text    
app.secureTextFields["field2"].clearText(andReplaceWith: "Some Other Text")
Origin answered 6/1, 2019 at 15:47 Comment(0)
F
0

Swift 5

based on @Bay Phillips answer,

extension XCUIElement {

    func clearAndEnterText(text: String) {
        guard let stringValue = self.value as? String else {
            XCTFail("Tried to clear and enter text into a non string value")
            return
        }

        self.tap()

        let deleteString = stringValue.map { _ in "\u{8}" }.joined(separator: "")

        self.typeText(deleteString)
        self.typeText(text)
    }

}
Flavio answered 20/10, 2019 at 11:22 Comment(0)
I
0

I know in the original question this was marked as "fragile", but nowadays this seems to be the easiest solution:

myTextView.press(forDuration: 1.2)
app.menuItems["Select All"].tap()
app.menuItems["Cut"].tap()
Initial answered 20/7, 2022 at 14:55 Comment(0)
T
0

This is my solution based on Aghost Biro's answer. It adds a maxAttempts parameter so that the test will fail instead of infinitely loop if it hasn't cleared in a number of times attempting to. (Implying something went wrong or the clearing logic needs to be updated such as for a new iOS version). If there is a specific textfield that needs a lot of clearing for some reason. Then the maxAttempts parameter can be increased.

extension XCUIElement {
    func clearValue(maxAttempts: Int = 3) {
        for attempt in 0...maxAttempts {
            if attempt == maxAttempts {
                XCTFail("Couldn't clear value of element: \(self)")
                break // In case XCTestCase.continueAfterFailure == true
            }
            // If the text field is empty, then value returns the placeholderValue. So if they are equal then it can be considered cleared.
            if let value = value as? String, !value.isEmpty && value != placeholderValue {
                // Move the cursor to the end of the text field
                let lowerRightCorner = coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.8))
                lowerRightCorner.tap()
                typeText(NSString().padding(toLength: value.count, withPad: XCUIKeyboardKey.delete.rawValue, startingAt: 0))
            } else {
                break
            }
        }
    }
}
Tonsorial answered 13/6, 2023 at 21:30 Comment(0)
A
0

Swift 5

Tap on textField to start clear text, finally press return key.

extension XCUIElement {
    func clearText() {
        tap()
        XCUIApplication().staticTexts["Select All"].tap()
        XCUIApplication().keys["delete"].tap()

    }
}

//usage
textField.tap()
textField.clearText()
textField.typeText("-52.3%")
app.buttons["Return"].tap()
Allonym answered 29/1 at 8:21 Comment(3)
Not helpful. "Select All" not visible, Cut, Copy, Paste exist.Inductor
You might need to check the tested textfield is empty or not :pAllonym
Sure, I will check again!Inductor
S
0

A bit late to the party, but I deleted the text with this:

textFields["MyTextField"].tap(withNumberOfTaps: 4, numberOfTouches: 1)
textFields["MyTextField"].typeText("\u{8}")

This is actually what you probably would do manually, i.e. click enough times until all text is selected and then hit delete button.

Sergias answered 31/1 at 9:58 Comment(0)
K
0

This is what worked for me:

    let name = app.textFields["Name"]
    name.tap(withNumberOfTaps: 3, numberOfTouches: 1)
    app.typeText("UI Test")
Kirkendall answered 12/2 at 1:42 Comment(2)
Same as https://mcmap.net/q/217111/-ui-test-deleting-text-in-text-field. The modification to use a triple tap is suggested in a comment. This should be a comment too, perhaps.Whisker
@Whisker I guess I always think comments that are better answers should be answers. And that comment doesn't have code, just prose, which I find less useful/searchable/clear. And this answer is slightly different also in another way, which is textFields instead of textViews.Kirkendall

© 2022 - 2024 — McMap. All rights reserved.