diff --git a/EarlGrey/Core/GREYElementInteraction.m b/EarlGrey/Core/GREYElementInteraction.m index 4c0cab040..51d5e33d6 100644 --- a/EarlGrey/Core/GREYElementInteraction.m +++ b/EarlGrey/Core/GREYElementInteraction.m @@ -53,6 +53,8 @@ NSString *const kGREYAssertionElementUserInfoKey = @"kGREYAssertionElementUserInfoKey"; NSString *const kGREYAssertionErrorUserInfoKey = @"kGREYAssertionErrorUserInfoKey"; +typedef void (^GREYInteractionBlock)(id element, NSError *matchError, __strong NSError **error); + @interface GREYElementInteraction() @end @@ -179,83 +181,43 @@ - (instancetype)performAction:(id)action { } - (instancetype)performAction:(id)action error:(__strong NSError **)errorOrNil { - NSParameterAssert(action); - I_CHECK_MAIN_THREAD(); - - @autoreleasepool { - NSError *executorError; - __block NSError *actionError = nil; - __weak __typeof__(self) weakSelf = self; - NSNotificationCenter *defaultNotificationCenter = [NSNotificationCenter defaultCenter]; - CFTimeInterval timeout = GREY_CONFIG_DOUBLE(kGREYConfigKeyInteractionTimeoutDuration); - - [[GREYUIThreadExecutor sharedInstance] executeSyncWithTimeout:timeout block:^{ - __typeof__(self) strongSelf = weakSelf; - NSAssert(strongSelf, @"strongSelf must not be nil"); - - NSArray *elements = [strongSelf matchedElementsWithTimeout:timeout error:&actionError]; - if (elements.count > 1) { - actionError = [strongSelf grey_errorForMultipleMatchingElements:elements]; - } else { - id element = [elements firstObject]; - // Notification that the action is to be performed on the found element. - NSMutableDictionary *actionUserInfo = [[NSMutableDictionary alloc] init]; - [actionUserInfo setObject:action forKey:kGREYActionUserInfoKey]; - if (element) { - [actionUserInfo setObject:element forKey:kGREYActionElementUserInfoKey]; - } - [defaultNotificationCenter postNotificationName:kGREYWillPerformActionNotification - object:nil - userInfo:actionUserInfo]; - // We only call perform:error: if element is not nil, as it is a default constraint for all - // actions. WillPerformAction and DidPerformAction notifications will always be sent, even - // if element is nil. - if (element && ![action perform:element error:&actionError] && !actionError) { - // Action didn't succeed yet no error was set. - NSString *description = @"Reason for action failure was not provided."; - actionError = [NSError errorWithDomain:kGREYInteractionErrorDomain - code:kGREYInteractionActionFailedErrorCode - userInfo:@{ NSLocalizedDescriptionKey : description }]; - } - if (actionError) { - [actionUserInfo setObject:actionError forKey:kGREYActionErrorUserInfoKey]; - } - // Notification for the action being successfully completed on the found element. - [defaultNotificationCenter postNotificationName:kGREYDidPerformActionNotification - object:nil - userInfo:actionUserInfo]; - } - // If we encountered a failure and are going to raise an exception, raise it right away before - // the main runloop drains any further. - if (actionError && !errorOrNil) { - [strongSelf grey_failInteraction:[NSString stringWithFormat:@"Action '%@'", action.name] - exception:kGREYActionFailedException - error:actionError]; - } - } error:&executorError]; - - // Failure to execute due to timeout should be represented as interaction timeout. - if ([executorError.domain isEqualToString:kGREYUIThreadExecutorErrorDomain] && - executorError.code == kGREYUIThreadExecutorTimeoutErrorCode) { - NSString *description = - [NSString stringWithFormat:@"Failed to perform action within %g seconds.", timeout]; - actionError = [NSError errorWithDomain:kGREYInteractionErrorDomain - code:kGREYInteractionTimeoutErrorCode - userInfo:@{ NSUnderlyingErrorKey : executorError, - NSLocalizedDescriptionKey : description }]; + NSString *name = [NSString stringWithFormat:@"Action '%@'", action.name]; + [self grey_performInteraction:name + failedException:kGREYActionFailedException + errorOrNil:errorOrNil + interactionBlock:^(id element, NSError *matchError, __strong NSError **error) { + NSParameterAssert(error); + + if (matchError) { + *error = matchError; } - if (actionError) { - if (errorOrNil) { - *errorOrNil = actionError; - } else { - [self grey_failInteraction:[NSString stringWithFormat:@"Action '%@'", action.name] - exception:kGREYActionFailedException - error:actionError]; - } + NSMutableDictionary *userInfo = + [NSMutableDictionary dictionaryWithDictionary:@{ kGREYActionUserInfoKey : action }]; + if (element) { + [userInfo setObject:element forKey:kGREYActionElementUserInfoKey]; } - // Drain once to update idling resources and redraw the screen. - [[GREYUIThreadExecutor sharedInstance] drainOnce]; - } + [[NSNotificationCenter defaultCenter] postNotificationName:kGREYWillPerformActionNotification + object:nil + userInfo:userInfo]; + // We only call perform:error: if element is not nil, as it is a default constraint for all + // actions. WillPerformAction and DidPerformAction notifications will always be sent, even if + // element is nil. + if (element && ![action perform:element error:error] && !*error) { + // Action didn't succeed yet no error was set. + NSString *description = [NSString stringWithFormat:@"%@ failed: no details given.", name]; + *error = [NSError errorWithDomain:kGREYInteractionErrorDomain + code:kGREYInteractionActionFailedErrorCode + userInfo:@{ NSLocalizedDescriptionKey : description }]; + } + if (*error) { + [userInfo setObject:*error forKey:kGREYActionErrorUserInfoKey]; + } + [[NSNotificationCenter defaultCenter] postNotificationName:kGREYDidPerformActionNotification + object:nil + userInfo:userInfo]; + }]; + // Drain once to update idling resources and redraw the screen. + [[GREYUIThreadExecutor sharedInstance] drainOnce]; return self; } @@ -264,89 +226,41 @@ - (instancetype)assert:(id)assertion { } - (instancetype)assert:(id)assertion error:(__strong NSError **)errorOrNil { - NSParameterAssert(assertion); - I_CHECK_MAIN_THREAD(); - - @autoreleasepool { - NSError *executorError; - __block NSError *assertionError = nil; - __weak __typeof__(self) weakSelf = self; - NSNotificationCenter *defaultNotificationCenter = [NSNotificationCenter defaultCenter]; - CFTimeInterval timeout = GREY_CONFIG_DOUBLE(kGREYConfigKeyInteractionTimeoutDuration); - - [[GREYUIThreadExecutor sharedInstance] executeSyncWithTimeout:timeout block:^{ - __typeof__(self) strongSelf = weakSelf; - NSAssert(strongSelf, @"strongSelf must not be nil"); - - // An error object that holds error due to element not found (if any). It is used only when - // an assertion fails because element was nil. That's when we surface this error. - NSError *matchError = nil; - NSArray *elements = [strongSelf matchedElementsWithTimeout:timeout error:&matchError]; - if (elements.count > 1) { - assertionError = [strongSelf grey_errorForMultipleMatchingElements:elements]; - } else { - id element = [elements firstObject]; - // Notification for the assertion to be checked on the found element. - // We send the notification for an assert even if no element was found. - NSMutableDictionary *assertionUserInfo = [[NSMutableDictionary alloc] init]; - [assertionUserInfo setObject:assertion forKey:kGREYAssertionUserInfoKey]; - if (element) { - [assertionUserInfo setObject:element forKey:kGREYAssertionElementUserInfoKey]; - } - [defaultNotificationCenter postNotificationName:kGREYWillPerformAssertionNotification - object:nil - userInfo:assertionUserInfo]; - if (![assertion assert:element error:&assertionError] && !assertionError) { - // Assertion didn't succeed yet no error was set. - NSString *description = @"Reason for assertion failure was not provided."; - assertionError = [NSError errorWithDomain:kGREYInteractionErrorDomain - code:kGREYInteractionAssertionFailedErrorCode - userInfo:@{ NSLocalizedDescriptionKey : description }]; - } - if (assertionError) { - [assertionUserInfo setObject:assertionError forKey:kGREYAssertionErrorUserInfoKey]; - } - if ([assertionError.domain isEqualToString:kGREYInteractionErrorDomain] && - assertionError.code == kGREYInteractionElementNotFoundErrorCode) { - assertionError = [NSError errorWithDomain:kGREYInteractionErrorDomain - code:kGREYInteractionElementNotFoundErrorCode - userInfo:@{ NSUnderlyingErrorKey : matchError }]; - } - // Notification for the assertion being successfully completed on the found element. - [defaultNotificationCenter postNotificationName:kGREYDidPerformAssertionNotification - object:nil - userInfo:assertionUserInfo]; - } - // If we encountered a failure and are going to raise an exception, raise it right away before - // the main runloop drains any further. - if (assertionError && !errorOrNil) { - NSString *interactionName = [NSString stringWithFormat:@"Assertion '%@'", assertion.name]; - [strongSelf grey_failInteraction:interactionName - exception:kGREYAssertionFailedException - error:assertionError]; - } - } error:&executorError]; - - // Failure to execute due to timeout should be represented as interaction timeout. - if ([executorError.domain isEqualToString:kGREYUIThreadExecutorErrorDomain] && - executorError.code == kGREYUIThreadExecutorTimeoutErrorCode) { - NSString *description = - [NSString stringWithFormat:@"Failed to execute assertion within %g seconds.", timeout]; - assertionError = [NSError errorWithDomain:kGREYInteractionErrorDomain - code:kGREYInteractionTimeoutErrorCode - userInfo:@{ NSUnderlyingErrorKey : executorError, - NSLocalizedDescriptionKey : description }]; + NSString *name = [NSString stringWithFormat:@"Assertion '%@'", assertion.name]; + [self grey_performInteraction:name + failedException:kGREYAssertionFailedException + errorOrNil:errorOrNil + interactionBlock:^(id element, NSError *matchError, __strong NSError **error) { + NSParameterAssert(error); + + NSMutableDictionary *userInfo = + [NSMutableDictionary dictionaryWithDictionary:@{ kGREYAssertionUserInfoKey : assertion }]; + if (element) { + [userInfo setObject:element forKey:kGREYAssertionElementUserInfoKey]; } - if (assertionError) { - if (errorOrNil) { - *errorOrNil = assertionError; - } else { - [self grey_failInteraction:[NSString stringWithFormat:@"Assertion '%@'", assertion.name] - exception:kGREYAssertionFailedException - error:assertionError]; - } + [[NSNotificationCenter defaultCenter] postNotificationName:kGREYWillPerformAssertionNotification + object:nil + userInfo:userInfo]; + if (![assertion assert:element error:error] && !*error) { + // Assertion didn't succeed yet no error was set. + NSString *description = [NSString stringWithFormat:@"%@ failed: no details given.", name]; + *error = [NSError errorWithDomain:kGREYInteractionErrorDomain + code:kGREYInteractionAssertionFailedErrorCode + userInfo:@{ NSLocalizedDescriptionKey : description }]; } - } + if ([(*error).domain isEqualToString:kGREYInteractionErrorDomain] && + (*error).code == kGREYInteractionElementNotFoundErrorCode) { + *error = [NSError errorWithDomain:kGREYInteractionErrorDomain + code:kGREYInteractionElementNotFoundErrorCode + userInfo:@{ NSUnderlyingErrorKey : matchError }]; + } + if (*error) { + [userInfo setObject:*error forKey:kGREYAssertionErrorUserInfoKey]; + } + [[NSNotificationCenter defaultCenter] postNotificationName:kGREYDidPerformAssertionNotification + object:nil + userInfo:userInfo]; + }]; return self; } @@ -370,6 +284,69 @@ - (instancetype)usingSearchAction:(id)action #pragma mark - Private +- (BOOL)grey_performInteraction:(NSString *)interactionName + failedException:(NSString *)exceptionName + errorOrNil:(__strong NSError **)errorOrNil + interactionBlock:(GREYInteractionBlock)interactionBlock { + I_CHECK_MAIN_THREAD(); + NSParameterAssert(interactionName); + NSParameterAssert(exceptionName); + NSParameterAssert(interactionBlock); + + NSError *executorError; + __block NSError *internalError = nil; + __weak __typeof__(self) weakSelf = self; + CFTimeInterval timeout = GREY_CONFIG_DOUBLE(kGREYConfigKeyInteractionTimeoutDuration); + + [[GREYUIThreadExecutor sharedInstance] executeSyncWithTimeout:timeout block:^{ + __typeof__(self) strongSelf = weakSelf; + NSAssert(strongSelf, @"strongSelf must not be nil"); + + NSError *matchError = nil; + NSArray *elements = [strongSelf matchedElementsWithTimeout:timeout error:&matchError]; + if (elements.count > 1) { + NSMutableArray *elementDescriptions = [NSMutableArray arrayWithCapacity:elements.count]; + [elements enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + [elementDescriptions addObject:[obj grey_description]]; + }]; + NSString *description = + [NSString stringWithFormat:@"%@ failed because multiple elements were matched: %@. Use " + @"matchers to narrow the selection down to a single element.", + interactionName, elementDescriptions]; + internalError = [NSError errorWithDomain:kGREYInteractionErrorDomain + code:kGREYInteractionMultipleElementsMatchedErrorCode + userInfo:@{ NSLocalizedDescriptionKey : description }]; + } else { + @autoreleasepool { + interactionBlock([elements firstObject], matchError, &internalError); + } + } + // If we encountered a failure and are going to raise an exception, raise it right away before + // the main runloop drains any further. + if (internalError && !errorOrNil) { + [strongSelf grey_failInteraction:interactionName exception:exceptionName error:internalError]; + } + } error:&executorError]; + + // Failure to execute due to timeout should be represented as interaction timeout. + if ([executorError.domain isEqualToString:kGREYUIThreadExecutorErrorDomain] && + executorError.code == kGREYUIThreadExecutorTimeoutErrorCode) { + NSString *description = [NSString stringWithFormat:@"%@ failed: timed out after %g seconds.", + interactionName, timeout]; + internalError = [NSError errorWithDomain:kGREYInteractionErrorDomain + code:kGREYInteractionTimeoutErrorCode + userInfo:@{ NSUnderlyingErrorKey : executorError, + NSLocalizedDescriptionKey : description }]; + } + if (internalError && errorOrNil) { + *errorOrNil = internalError; + } else if (internalError) { + [self grey_failInteraction:interactionName exception:exceptionName error:internalError]; + } + // Method accepting NSError ** should have a non-void return value. + return internalError ? NO : YES; +} + /** * Handles failure of an @c interaction. * @@ -419,26 +396,4 @@ - (void)grey_failInteraction:(NSString *)interactionName _elementMatcher, error); } -/** - * Provides an error with @c kGREYInteractionMultipleElementsMatchedErrorCode for multiple elements - * matching the specified matcher. - * - * @param matchingElements A set of matching elements - * - * @return Error for matching multiple elements. - */ -- (NSError *)grey_errorForMultipleMatchingElements:(NSArray *)matchingElements { - NSMutableArray *elementDescriptions = [NSMutableArray arrayWithCapacity:matchingElements.count]; - [matchingElements enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - [elementDescriptions addObject:[obj grey_description]]; - }]; - NSString *description = - [NSString stringWithFormat:@"Interaction failed because multiple elements were matched: %@. " - @"Use matchers to narrow the selection down to a single element.", - elementDescriptions]; - return [NSError errorWithDomain:kGREYInteractionErrorDomain - code:kGREYInteractionMultipleElementsMatchedErrorCode - userInfo:@{ NSLocalizedDescriptionKey : description }]; -} - @end diff --git a/Tests/UnitTests/Sources/GREYElementInteractionTest.m b/Tests/UnitTests/Sources/GREYElementInteractionTest.m index 6d9e50ecc..d761fa4de 100644 --- a/Tests/UnitTests/Sources/GREYElementInteractionTest.m +++ b/Tests/UnitTests/Sources/GREYElementInteractionTest.m @@ -235,7 +235,7 @@ - (void)testPerformWithErrorFailsWithoutReason { }]; NSDictionary *userInfo = - @{ NSLocalizedDescriptionKey : @"Reason for action failure was not provided." }; + @{ NSLocalizedDescriptionKey : @"Action 'test' failed: no details given." }; NSError *expectedError = [NSError errorWithDomain:kGREYInteractionErrorDomain code:kGREYInteractionActionFailedErrorCode userInfo:userInfo]; @@ -360,7 +360,7 @@ - (void)testCheckWithErrorFailsWithoutReason { }]; NSDictionary *userInfo = - @{ NSLocalizedDescriptionKey : @"Reason for assertion failure was not provided." }; + @{ NSLocalizedDescriptionKey : @"Assertion 'name' failed: no details given." }; NSError *expectedError = [NSError errorWithDomain:kGREYInteractionErrorDomain code:kGREYInteractionAssertionFailedErrorCode userInfo:userInfo];