Search code examples
swiftswiftuixcode-ui-testing

How can I write UI tests for a SwiftUI InputField


I have this SwiftUI view (will remove non-relevant parts)

struct LoginView: View {
    
    var body: some View {
        VStack {
            Spacer()
            InputField(title: "Email", text: $email)
            InputField(title: "Password", text: $password, showingSecureField: true)
            InputField(title: "Store", text: $userStore)
            CallToActionButton(
                title: newUser ? "Register User" : "Log In",
                action: userAction
            )
            Button(action: { newUser.toggle() }) {
                HStack {
                    CheckBox(title: "Create Account", isChecked: $newUser)
                    Spacer()
                }
            }
            Spacer()
        }
    }

I can write a UI Test for that button with this code:

    func testClickLoginButton() throws {
        let app = XCUIApplication()
        app.launch()
                  
        let startButton = app.buttons["Log In"]
        XCTAssertTrue(startButton.waitForExistence(timeout: 1))
        XCTAssertTrue(startButton.isEnabled)
        startButton.tap()
    }

And it works, as there's a button with that title on screen.

But then I want to automatically put some text in those InputTexts and test them, but can't find them, as they're neither UITextField nor UITextView. How can I accomplish this? Thanks!

    func testAddEmail() throws {
        let app = XCUIApplication()
        app.launch()

        // fails as can't find any text field containing "Email"
                           
        let emailText = app.textFields["Email"]
        XCTAssertTrue(emailText.waitForExistence(timeout: 1))
        emailText.typeText("[email protected]")
        XCTAssertEqual(emailText.value as! String, "[email protected]")
    }

Solution

  • I am not an expert and have seen different ways to do UITests in SwiftUI. There are perhaps better ways with snapshots etc but I quickly recreated your example for fun and added an accessibility identifier:

    struct InputField: View {
        var title: String
        @Binding var email: String
    
        var body: some View {
            VStack(alignment: .leading) {
                Text("Enter your email ")
                TextField(title, text: $email)
                    .accessibility(identifier: "Email")
    
            }
            .padding()
        }
    }
    
    struct ContentView: View {
        @State private var email: String = ""
    
        var body: some View {
                InputField(title: "Email: ", email: $email)
        }
    }
    

    Also it might be sometimes required to add a tap before entering the email or text. It was giving me an error because of this.
    The UItest would look like this and is passing :)

        func testAddEmail() throws {
            let app = XCUIApplication()
            app.launch()
    
            let emailText = app.textFields["Email"]
            XCTAssertTrue(emailText.waitForExistence(timeout: 1))
            emailText.tap() 
            emailText.typeText("[email protected]")
    
            XCTAssertEqual(emailText.value as! String, "[email protected]")
        }
    

    Also a cool trick I learned is to put a breakpoint like this:

    enter image description here

    and in the debugger at the prompt enter the following:

    (lldb) po XCUIApplication().textFields
    

    enter image description here

    And so you get the list and you see if identifiers are missing. Same thing for buttons and staticTexts etc.