Search code examples
objective-cxcode8accessibilityuipickerviewxcode-ui-testing

How to specify accessibility identifiers for multiple UIPickerViews during XCUITest


When developing Xcode UI testcases for a view controller with multiple UIPickerViews I ran into several bugs preventing success all relating to being able to uniquely identify the pickers within XCUITest.

What "should" work is to simply set the accessibility identifier, or accessibility label, from within storyboard like so:

enter image description here

But this does not work at all for a UIPickerView, though I verified the accessibilityLabel and accessibilityIdentifier properties are set for the UIPickerView. And yes, I tried it with one or the other or both set. I even tried programmatically setting one or the other or both. The below lines within an XCUITest case fail to locate the picker regardless:

  XCUIElement *shippingMethodPicker = app.pickerWheels[@"Shipping method"];
 [shippingMethodPicker adjustToPickerWheelValue:@"USPS Media Mail"];

It would seem that this is a known issue, and that the solution would be to make the view controller also a UIPickerViewAccessibilityDelegate, and implement the - (NSString *)pickerView:(UIPickerView *)pickerView accessibilityLabelForComponent:(NSInteger)component delegate method.

The Apple API Documentation would seem to describe exactly what we need to uniquely apply an accessibility label to each pickerWheels component.

But this is also bugged, the pickerView parameter is not actually a UIPickerView *, as referenced in this stackoverflow link Unable to get pickerView.tag in -pickerView:accessibilityLabelForComponent: method

Due to the implementation defect with the delegate method, you cannot determine which UIPickerView the delegate is being called for rendering it useless for a view with more than one picker.

With the storyboard approach bugged, and the accessibility delegate also bugged, I could not locate a way to uniquely identify two or more UIPickerViews in a view controller from within a XCUITest testcase.

Anyone have a solution?


Solution

  • Focusing on the previous stackoverflow link comments, I determined a solution that worked for my requirements.

    From the debugger we can confirm the previous links comment observation:

    enter image description here

    The accessibility delegate method,

    - (NSString *)pickerView:(UIPickerView *)pickerView accessibilityLabelForComponent:(NSInteger)component;
    

    is quite bugged and does not have a UIPickerView *, but rather a private class UIAccessibilityPickerComponent *. As we can note in the debugger, there is an actual UIPickerView * at the private _picker property of this private class.

    Radar opened.

    Well this is an internal test problem, it's not something we would ship in the app for the App Store. So we CAN use private interfaces to get around this problem. We will only compile this when we are performing UI testing.

    First, create a new build configuration in Xcode that you would only use for Testing, duplicated from Debug. Within that create a new preprocessor define -DXCUITEST and be sure to set this new build config in your scheme for Test.

    Then implement the accessibility delegate as follows:

    #pragma mark - UIPickerViewAccessibilityDelegate
    
    #ifdef XCUITEST
    - (NSString *)pickerView:(UIPickerView *)pickerView accessibilityLabelForComponent:(NSInteger)component {
    
        NSString *label;
        UIPickerView *realPickerView;
        Ivar picker;
    
        // we are going to work around a bug where the pickerView on this delegate is the wrong class by
        // pulling the UIPickerView * that we need from the private property of the UIAccessibilityPickerComponent class
        picker = class_getInstanceVariable([NSClassFromString(@"UIAccessibilityPickerComponent") class], "_picker");
    
        // check if the bug still exists and apply workaround only if necessary
        if (![pickerView isKindOfClass:[UIPickerView class]])
            realPickerView = object_getIvar(pickerView, picker);
        else
            realPickerView = pickerView;  
    
        if (realPickerView == self.shippingMethod)
            label = @"Shipping method";
        else if (realPickerView == self.someOtherPicker)
            label = @"SomeOtherPicker";
    
        return label;
    }
    #endif
    

    With this workaround the XCUITest testcases finally executed as expected, successfully testing situations of two and even three UIPickerViews on a single view all uniquely identified. Note in my case these were single wheel pickers, if you wanted to solve the problem for multi-wheel pickers, then implement the component logic in the delegate, which is not bugged and works as expected.

    Also, don't forget to add this header to the top of your view controller class file:

    #ifdef XCUITEST
    #import <objc/runtime.h>
    #endif