Skip to content

Commit

Permalink
Refactor: Move common code in GREYElementInteraction performAction an…
Browse files Browse the repository at this point in the history
…d assert to a helper method
  • Loading branch information
axi0mX committed Aug 22, 2016
1 parent fb4da02 commit 278f3c7
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 180 deletions.
311 changes: 133 additions & 178 deletions EarlGrey/Core/GREYElementInteraction.m
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
NSString *const kGREYAssertionElementUserInfoKey = @"kGREYAssertionElementUserInfoKey";
NSString *const kGREYAssertionErrorUserInfoKey = @"kGREYAssertionErrorUserInfoKey";

typedef void (^GREYInteractionBlock)(id element, NSError *matchError, __strong NSError **error);

@interface GREYElementInteraction() <GREYInteractionDataSource>
@end

Expand Down Expand Up @@ -179,83 +181,43 @@ - (instancetype)performAction:(id<GREYAction>)action {
}

- (instancetype)performAction:(id<GREYAction>)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;
}

Expand All @@ -264,89 +226,41 @@ - (instancetype)assert:(id<GREYAssertion>)assertion {
}

- (instancetype)assert:(id<GREYAssertion>)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;
}

Expand All @@ -370,6 +284,69 @@ - (instancetype)usingSearchAction:(id<GREYAction>)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.
*
Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions Tests/UnitTests/Sources/GREYElementInteractionTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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];
Expand Down

0 comments on commit 278f3c7

Please sign in to comment.