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.
To return the array of choices, I had to change your test like this:
the picker role is popUpButtons
, not buttons
.
access the picker
you have been testing for existence, instead of something new.
the choices are in XCUIElement.title
, not .label
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)
}
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)
}