swiftuiextractxctestswiftui-picker

How do I extract the choices of a menu picker in XCTest?


My app uses the SwiftUI Picker, and I'd like to test that it only contains the expected choices. When I try to extract the choices in XCTest, I get back an extra entry, Choose an option, Option 1, i.e.,

["Option 1", "Option 2", "Option 3", "Option 4", "Choose an option, Option 1"]

How do I get the actual/provided choices only?

Below is the app code for iOS

struct PickerXCTest: View {
        let options = ["Option 1", "Option 2", "Option 3", "Option 4"]
        @State private var selectedOption = "Option 1"
        
        var body: some View {
            VStack {
                let picker = Picker("Choose an option", selection: $selectedOption) {
                    ForEach(options, id: \.self) {
                        Text($0)
                    }
                }
                
                picker.pickerStyle(.menu)
                    .accessibility(identifier: "myPicker")
            }
        }
    } // PickerXCTest()

Below is XCTest code

 func testMenuPicker() throws {
        let app = XCUIApplication()
        app.launchArguments = ["--appmode", "debug", "--usecase", "stackOverflow", "--subcase", "menuPicker"]
        app.launch()
        
        let picker = app/*@START_MENU_TOKEN@*/.buttons["myPicker"]/*[[".buttons[\"Choose an option, Option 1\"]",".buttons[\"myPicker\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/
        XCTAssertTrue(picker.waitForExistence(timeout: 5), "Picker did not appear in time")
        
        // Tap the picker to bring up the menu.
        picker.tap()
        
        // Access picker items
        let pickerItems = app.buttons.allElementsBoundByIndex
        
        // Extract memu choices.
        var menuChoices: [String] = []
        for item in pickerItems {
            menuChoices.append(item.label)
        }
        
        //XCTAssertTrue(false, "\(menuChoices)")
    } // testMenuPicker()```

I've tried to use different properties associated with app.*, but I've not been able to come up with a way to get back only the actual choices.


Solution

  • macOS solution

    To return the array of choices, I had to change your test like this:

        func testMenuPicker() throws {
            let app = XCUIApplication()
            app.launchArguments = ["--appmode", "debug", "--usecase", "stackOverflow", "--subcase", "menuPicker"]
            app.launch()
            
            let picker = app.popUpButtons["myPicker"]
            XCTAssertTrue(picker.waitForExistence(timeout: 5), "Picker did not appear in time")
            
            // Tap the picker to bring up the menu.
            picker.tap()
            
            // Access picker items
            let pickerItems = picker.menuItems.allElementsBoundByIndex
            
            // Extract menu choices.
            var menuChoices: [String] = []
            for item in pickerItems {
                menuChoices.append(item.title)
            }
            
            XCTAssertEqual(["Option 1", "Option 2", "Option 3", "Option 4"], menuChoices)
        }
    

    iOS solution

    The accessibility hierarchy in iOS in very different. For some reason, the "Option" buttons are not children of the "myPicker" button. So when you query all buttons across the app, you will get the picker button plus the option buttons, which is the problem in your code.
    My solution is to give the options an identifier, then query on that in the test.

    That would be like this:

    struct PickerXCTest: View {
        let options = ["Option 1", "Option 2", "Option 3", "Option 4"]
        @State private var selectedOption = "Option 1"
        
        var body: some View {
            VStack {
                Picker("Choose an option", selection: $selectedOption) {
                    ForEach(options, id: \.self) {
                        Text($0)
                            .accessibilityIdentifier("myPickerOption") // <-- add this!
                    }
                }
    //            .accessibility(identifier: "myPicker") // Deprecated
                .accessibilityIdentifier("myPicker")
                .pickerStyle(.menu)
            }
        }
    }
    

    And the test:

        func testMenuPicker() throws {
            let app = XCUIApplication()
            app.launch()
            
            let picker = app.buttons["myPicker"]
            XCTAssertTrue(picker.waitForExistence(timeout: 5), "Picker did not appear in time")
            
            // Tap the picker to bring up the menu.
            picker.tap()
            
            // Access picker items.
            let pickerItems = app.buttons.containing(.any, identifier: "myPickerOption").allElementsBoundByIndex
            
            // Extract menu choices.
            var menuChoices: [String] = []
            for item in pickerItems {
                menuChoices.append(item.label)
            }
            
            XCTAssertEqual(["Option 1", "Option 2", "Option 3", "Option 4"], menuChoices)
        }