square/KIF — pull request #168 by blakewatters (2012-11-14 19:18:46+00:00)
Title: Add support for using Blocks to Provide Dynamic Accessibility Labels & Values

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 https://github.com/kovpas/PMCalendar) 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:

```objc
+ (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) {
break;
}
}

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 ;-)