Add support for using Blocks to Provide Dynamic Accessibility Labels & Values #168

wants to merge 1 commit into

2 participants


Let me first preface this pull request by saying that I do not in any way shape or form encourage the use of this nasty, insidious hack. I am sharing this because I truly could not find a better way to adequately test a piece of important UI within my application without it and my Google exploration revealed that it may be useful to others. If you can avoid it please don't do this.

With that said, here we go: I have an instance of the very nice PMCalendar (see in my application. It provides a nice calendar view and has a pretty decent API and is very quick to get started with.

The trouble comes in when you wish to test it using KIF. PMCalendar is implemented as an opaque view in which all of the calendar user interface components are painted directly onto the view in drawRect. There's no UIViews in which to attach accessibility labels, so you are pretty much forced to test it by tapping arbitrary points on the calendar.

To make matters worse, because this is a calendar component the content underneath a given point is moving with time, so you cannot know what date a given point corresponds to until the calendar has been rendered. This provides a secondary problem for KIF -- because Steps expect to know the accessibility label ahead of time, you cannot just use a step to determine the appropriate date and then reference it in a later step.

That is where the code in this pull request comes in: I have patched KIF deep in the internals to allow you to pass in a Block object into the KIF step and it will be executed when KIF tries to lookup the accessibility label and value in the view.

Aside from violating the type safety of the method signatures, this also relies on determining if the accessibility label and value arguments contain a Block object. The only way to really do this is to check the object type against the private NSBlock type. It also results in the KIF steps producing ugly output, as you are going to see the block objects logged out instead of the values they return.

Here is a concrete example from GateGuru 3.0 of how to use the patch:

+ (id)scenarioToEnsureDateSelectionPopulatedProperlyMultipleTimes
    __block GGFlightSearch *flightSearch = nil;
    __block NSString *dateStringForPoint = nil;
    KIFTestScenario *scenario = [KIFTestScenario scenarioWithDescription:@"Flight Search - By Flight Number: Select departure date from calendar, navigate back, then go back to departure date selection and verify currently selected date is displayed"];
    [scenario addStep:[KIFTestStep stepToPresentInitialViewControllerFromStoryboardWithName:@"FlightSearchStoryboard" configurationBlock:^(UIViewController *viewController) {
        GGFlightSearchViewController *flightSearchViewController = (GGFlightSearchViewController *) [(UINavigationController *)viewController topViewController];
        flightSearch = [RKTestFactory insertManagedObjectForEntityForName:@"FlightSearch" inManagedObjectContext:nil withProperties:nil];
        flightSearch.mode = GGFlightSearchByFlightNumberMode;
        flightSearch.departureDate = [NSDate dateWithYear:2012 month:10 day:6];
        flightSearchViewController.flightSearch = flightSearch;
    [scenario addStep:[KIFTestStep stepToTapViewWithAccessibilityLabel:@"October 6, 2012"]];
    [scenario addStep:[KIFTestStep stepToWaitForViewWithAccessibilityLabel:@"Done"]];

    [scenario addStep:[KIFTestStep stepForViewWithAccessibilityLabel:@"Calendar View" description:@"Grab the calendar view" executionBlock:^KIFTestStepResult(KIFTestStep *step, UIView *view, NSError *__autoreleasing *error) {
        UIResponder *responder = view;
        while (![responder isKindOfClass:[UIViewController class]]) {
            responder = [responder nextResponder];
            if (nil == responder) {

        KIFTestCondition(responder, error, @"Could not find the parent view controller for the target view");

        GGDateSelectionViewController *dateSelectionController = (GGDateSelectionViewController *)responder;
        CGPoint targetPoint = [dateSelectionController.view convertPoint:CGPointMake(160, 250) fromView:[UIApplication sharedApplication].keyWindow];
        NSDate *date = [dateSelectionController.calendarController.digitsView dateForPoint:targetPoint];
        NSDate *yesterday = [[NSDate date] dateBySubtractingDays:1];
        NSDate *dateForSelection = [date earlierDate:yesterday];

        dateStringForPoint = [[NSDateFormatter monthAsStringDayCommaYearDateFormatter] stringFromDate:dateForSelection];
        return KIFTestStepResultSuccess;

    // NOTE: PMCalendar paints the digits directly onto the view, which makes it incompatible with KIF testing and Accessibility. We tap an arbitrary point on the view to cause a change in the date selection just to be sure the thing works at all.
    [scenario addStep:[KIFTestStep stepToTapScreenAtPoint:CGPointMake(160, 250)]];
    [scenario addStep:[KIFTestStep stepToTapViewWithAccessibilityLabel:@"Done"]];
    [scenario addStep:[KIFTestStep stepToWaitForAbsenceOfViewWithAccessibilityLabel:@"Done"]];
    [scenario addStep:[KIFTestStep stepToWaitForViewWithAccessibilityLabel:(NSString *) (NSString *)^{ return dateStringForPoint; }]];
    [scenario addStep:[KIFTestStep stepToTapViewWithAccessibilityLabel:(NSString *) (NSString *)^{ return dateStringForPoint; }]];
    [scenario addStep:[KIFTestStep stepToTapViewWithAccessibilityLabel:@"Done"]];
    [scenario addStep:[KIFTestStep stepToWaitForViewWithAccessibilityLabel:(NSString *) (NSString *)^{ return dateStringForPoint; }]];

    return scenario;

Hopefully you won't have to use this ;-)

@blakewatters blakewatters Add support for using Blocks that return an NSString for the Accessib…
…lity Label and Accessibility Value to test with dynamic values

It seems like a better approach would be to properly add accessibility elements to your calendar view: if you're already to the point of patching your dependencies, you might as patch the one that fixes the root problem.

Traditional testing dogma would have you mocking out all of the variables that can change between runs of your app, though I would agree that changing an app's definition of "today" may be more effort than it's worth.


As mentioned in the disclaimer about the nastiness of this hack -- the underlying Calendar dependency draws the entire view using Core Graphics directly using drawRect:. There's literally nothing to attach the accessibility element to, unless I have somehow wildly missed an API that lets you attach them to arbitrary rectangles. I evaluated patching the component directly -- it would have been a complete reimplementation, nothing is built at the UIView layer where accessibility is available. I just didn't want to write a calendar component from scratch and or live with zero functional coverage for anything that passes through this view, so this evil hack was born. :-)

The time is indeed mocked out. The purpose of the test is to ensure that tapping the UI results in an update reflecting the appropriate selection. I only posted this PR for posterity in case anyone else finds it useful -- it may have been better stuffed into a blog somewhere. Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment