Search code examples
swiftuixctestxcuitest

Xcode UI test cannot tap menu button in form


Apparently UI tests are unable to tap menu buttons but can tap regular buttons inside forms. Earlier today I was able to see in the Simulator that the UI test tries to tap the button by tapping the center of the containing form row, which works for regular buttons, but not for menu buttons. In fact, when trying in the SwiftUI preview in Xcode it seems that menu buttons have to be tapped exactly on top of them, while regular buttons can be tapped anywhere in the form row. (Now I’m not able to see touches performed by the UI test anymore in the Simulator for an unknown reason, even though I have “Show single touches” enabled in the Simulator settings.)

How can I open a menu button in a UI test?

The UI code:

struct ContentView: View {
    @State private var label1 = "Menu 1"
    @State private var label2 = "Menu 2"

    var body: some View {
        NavigationStack {
            Form {
                LabeledContent("Menu 1") {
                    Button(label1) {
                        label1 = "Menu 1 tapped"
                    }
                    .accessibilityIdentifier("menu1")
                }
                LabeledContent("Menu 2") {
                    Menu(label2) {
                        Button("Button") {
                            
                        }
                        .accessibilityIdentifier("button")
                    }
                    .accessibilityIdentifier("menu2")
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

And the test:

final class problemUITests: XCTestCase {

    func testExample() throws {
        // UI tests must launch the application that they test.
        let app = XCUIApplication()
        app.launch()
        
        app.collectionViews.element(boundBy: 0).buttons["menu1"].tap()
        app.collectionViews.element(boundBy: 0).buttons["menu2"].tap()
        app.collectionViews.element(boundBy: 0).buttons["button"].tap()
    }
}

Solution

  • TL;DR: Append .buttons.firstMatch.descendants(matching: .any).firstMatch.

    Full answer:

    It looks like SwiftUI keeps rendering weird element hierarchies. Similarly to toggles not being tappable in a straightforward day, menus also need some hacky digging into the view hierachy.

    I don't expect this solution to last, it will likely break in iOS 18 or other future versions, but at least it works now with Xcode 15.4 in the iOS 17.5 simulator with the Menu buttons.

    Suppose you have a Menu somewhere on screen:

    Menu {
        Button {
            // some action
        } label: {
            Label("Add document", systemImage: "doc.badge.plus")
        }
        .accessibilityIdentifier("add_document_menu_item")
    } label: {
        Label("Menu", systemImage: "ellipsis.circle")
    }
    .accessibilityIdentifier("my_menu")
    

    Then the UI test for it will look like this:

    // Locate the menu button
    let menuButton = app
        .buttons["my_menu"]
        .buttons
        .firstMatch
        .descendants(matching: .any)
        .firstMatch
    
    // Open menu
    menuButton.tap()
    
    // Locating the menu item is straightforward
    let menuItem = app.buttons["add_document_menu_item"]
    
    // Tap on the menu item
    menuItem.tap()