blob: 7d112819f2e45487feb8e2677a36debcc267d4b3 [file] [log] [blame]
//
// Copyright 2016 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#import "Core/GREYElementInteraction.h"
#import "Action/GREYAction.h"
#import "Additions/NSError+GREYAdditions.h"
#import "Additions/NSObject+GREYAdditions.h"
#import "Assertion/GREYAssertion.h"
#import "Assertion/GREYAssertionDefines.h"
#import "Assertion/GREYAssertions.h"
#import "Assertion/GREYAssertions+Internal.h"
#import "Common/GREYConfiguration.h"
#import "Common/GREYError.h"
#import "Common/GREYError+Internal.h"
#import "Common/GREYErrorConstants.h"
#import "Common/GREYDefines.h"
#import "Common/GREYLogger.h"
#import "Common/GREYObjectFormatter.h"
#import "Common/GREYStopwatch.h"
#import "Core/GREYElementFinder.h"
#import "Core/GREYElementInteraction+Internal.h"
#import "Core/GREYInteractionDataSource.h"
#import "Exception/GREYFrameworkException.h"
#import "Matcher/GREYAllOf.h"
#import "Matcher/GREYMatcher.h"
#import "Matcher/GREYMatchers.h"
#import "Provider/GREYElementProvider.h"
#import "Provider/GREYUIWindowProvider.h"
#import "Synchronization/GREYUIThreadExecutor.h"
@interface GREYElementInteraction() <GREYInteractionDataSource>
@end
@implementation GREYElementInteraction {
id<GREYMatcher> _rootMatcher;
id<GREYMatcher> _searchActionElementMatcher;
id<GREYMatcher> _elementMatcher;
id<GREYAction> _searchAction;
// If _index is set to NSUIntegerMax, then it is unassigned.
NSUInteger _index;
}
@synthesize dataSource;
- (instancetype)initWithElementMatcher:(id<GREYMatcher>)elementMatcher {
NSParameterAssert(elementMatcher);
self = [super init];
if (self) {
_elementMatcher = elementMatcher;
_index = NSUIntegerMax;
[self setDataSource:self];
}
return self;
}
- (instancetype)inRoot:(id<GREYMatcher>)rootMatcher {
_rootMatcher = rootMatcher;
return self;
}
- (instancetype)atIndex:(NSUInteger)index {
_index = index;
return self;
}
#pragma mark - Package Internal
- (NSArray *)matchedElementsWithTimeout:(CFTimeInterval)timeout error:(__strong NSError **)error {
NSParameterAssert(error);
GREYLogVerbose(@"Scanning for element matching: %@", _elementMatcher);
id<GREYInteractionDataSource> strongDataSource = [self dataSource];
NSAssert(strongDataSource, @"strongDataSource must be set before fetching UI elements");
GREYElementProvider *entireRootHierarchyProvider =
[GREYElementProvider providerWithRootProvider:[strongDataSource rootElementProvider]];
id<GREYMatcher> elementMatcher = _elementMatcher;
if (_rootMatcher) {
elementMatcher = grey_allOf(elementMatcher, grey_ancestor(_rootMatcher), nil);
}
GREYElementFinder *elementFinder = [[GREYElementFinder alloc] initWithMatcher:elementMatcher];
NSError *searchActionError = nil;
CFTimeInterval timeoutTime = CACurrentMediaTime() + timeout;
BOOL timedOut = NO;
while (YES) {
@autoreleasepool {
// Find the element in the current UI hierarchy.
GREYStopwatch *elementFinderStopwatch = [[GREYStopwatch alloc] init];
[elementFinderStopwatch start];
NSArray *elements = [elementFinder elementsMatchedInProvider:entireRootHierarchyProvider];
[elementFinderStopwatch stop];
GREYLogVerbose(@"Element found for matcher: %@\n with time: %f seconds",
_elementMatcher,
[elementFinderStopwatch elapsedTime]);
if (elements.count > 0) {
return elements;
} else if (!_searchAction) {
if (error) {
NSString *desc =
@"Interaction cannot continue because the desired element was not found.";
GREYPopulateErrorOrLog(error,
kGREYInteractionErrorDomain,
kGREYInteractionElementNotFoundErrorCode,
desc);
}
return nil;
} else if (searchActionError) {
break;
}
CFTimeInterval currentTime = CACurrentMediaTime();
if (currentTime >= timeoutTime) {
timedOut = YES;
break;
}
// Keep applying search action.
id<GREYInteraction> interaction =
[[GREYElementInteraction alloc] initWithElementMatcher:_searchActionElementMatcher];
// Don't fail if this interaction error's out. It might still have revealed the element
// we're looking for.
[interaction performAction:_searchAction error:&searchActionError];
// Drain here so that search at the beginning of the loop looks at stable UI.
[[GREYUIThreadExecutor sharedInstance] drainUntilIdle];
}
}
if (searchActionError) {
GREYPopulateNestedErrorOrLog(error,
kGREYInteractionErrorDomain,
kGREYInteractionElementNotFoundErrorCode,
@"Search action failed",
searchActionError);
} else if (timedOut) {
CFTimeInterval interactionTimeout =
GREY_CONFIG_DOUBLE(kGREYConfigKeyInteractionTimeoutDuration);
NSString *desc = [NSString stringWithFormat:@"Interaction timed out after %g seconds while "
@"searching for element.", interactionTimeout];
NSError *timeoutError = GREYErrorMake(kGREYInteractionErrorDomain,
kGREYInteractionTimeoutErrorCode,
desc);
GREYPopulateNestedErrorOrLog(error,
kGREYInteractionErrorDomain,
kGREYInteractionElementNotFoundErrorCode,
@"",
timeoutError);
}
return nil;
}
#pragma mark - GREYInteractionDataSource
/**
* Default data source for this interaction if no datasource is set explicitly.
*/
- (id<GREYProvider>)rootElementProvider {
return [GREYUIWindowProvider providerWithAllWindows];
}
#pragma mark - GREYInteraction
- (instancetype)performAction:(id<GREYAction>)action {
return [self performAction:action error:nil];
}
- (instancetype)performAction:(id<GREYAction>)action error:(__strong NSError **)errorOrNil {
NSParameterAssert(action);
I_CHECK_MAIN_THREAD();
GREYLogVerbose(@"--Action started--");
GREYLogVerbose(@"Action to perform: %@", [action name]);
GREYStopwatch *stopwatch = [[GREYStopwatch alloc] init];
[stopwatch start];
@autoreleasepool {
NSError *executorError;
__block NSError *actionError = nil;
__weak __typeof__(self) weakSelf = self;
// Create the user info dictionary for any notificatons and set it up with the action.
NSMutableDictionary *actionUserInfo = [[NSMutableDictionary alloc] init];
[actionUserInfo setObject:action forKey:kGREYActionUserInfoKey];
NSNotificationCenter *defaultNotificationCenter = [NSNotificationCenter defaultCenter];
CFTimeInterval interactionTimeout =
GREY_CONFIG_DOUBLE(kGREYConfigKeyInteractionTimeoutDuration);
// Assign a flag that provides info if the interaction being performed failed.
__block BOOL interactionFailed = NO;
BOOL executionSucceeded =
[[GREYUIThreadExecutor sharedInstance] executeSyncWithTimeout:interactionTimeout
block:^{
__typeof__(self) strongSelf = weakSelf;
NSAssert(strongSelf, @"Must not be nil");
// Obtain all elements from the hierarchy and populate the passed error in case of
// an element not being found.
NSError *elementNotFoundError = nil;
NSArray *elements = [strongSelf matchedElementsWithTimeout:interactionTimeout
error:&elementNotFoundError];
id element = nil;
if (elements) {
// Get the uniquely matched element. If this is nil, then it means that there has been
// an error in finding a unique element, such as multiple matcher error.
element = [strongSelf grey_uniqueElementInMatchedElements:elements
andError:&actionError];
if (element) {
[actionUserInfo setObject:element forKey:kGREYActionElementUserInfoKey];
} else {
interactionFailed = YES;
[actionUserInfo setObject:actionError forKey:kGREYActionErrorUserInfoKey];
}
} else {
interactionFailed = YES;
actionError = elementNotFoundError;
[actionUserInfo setObject:elementNotFoundError forKey:kGREYActionErrorUserInfoKey];
}
// Post notification that the action is to be performed on the found element.
[defaultNotificationCenter postNotificationName:kGREYWillPerformActionNotification
object:nil
userInfo:actionUserInfo];
GREYLogVerbose(@"Performing action: %@\n with matcher: %@\n with root matcher: %@",
[action name], _elementMatcher, _rootMatcher);
if (element && ![action perform:element error:&actionError]) {
interactionFailed = YES;
// Action didn't succeed yet no error was set.
if (!actionError) {
actionError = GREYErrorMake(kGREYInteractionErrorDomain,
kGREYInteractionActionFailedErrorCode,
@"Reason for action failure was not provided.");
}
// Add the error obtained from the action to the user info notification dictionary.
[actionUserInfo setObject:actionError forKey:kGREYActionErrorUserInfoKey];
}
// Post notification for the process of an action's execution being completed. This
// notification does not mean that the action was performed successfully.
[defaultNotificationCenter postNotificationName:kGREYDidPerformActionNotification
object:nil
userInfo:actionUserInfo];
// If we encounter a failure and going to raise an exception, raise it right away before
// the main runloop drains any further.
if (interactionFailed && !errorOrNil) {
[strongSelf grey_handleFailureOfAction:action
actionError:actionError
userProvidedOutError:nil];
}
} error:&executorError];
// Failure to execute due to timeout should be represented as interaction timeout.
if (!executionSucceeded) {
if ([executorError.domain isEqualToString:kGREYUIThreadExecutorErrorDomain] &&
executorError.code == kGREYUIThreadExecutorTimeoutErrorCode) {
NSString *actionTimeoutDesc =
[NSString stringWithFormat:@"Failed to perform action within %g seconds.",
interactionTimeout];
actionError = GREYNestedErrorMake(kGREYInteractionErrorDomain,
kGREYInteractionTimeoutErrorCode,
actionTimeoutDesc,
executorError);
}
}
// Since we assign all errors found to the @c actionError, if either of these failed then
// we provide it for error handling.
BOOL actionFailed = !executionSucceeded || interactionFailed;
if (actionFailed) {
[self grey_handleFailureOfAction:action
actionError:actionError
userProvidedOutError:errorOrNil];
}
// Drain once to update idling resources and redraw the screen.
[[GREYUIThreadExecutor sharedInstance] drainOnce];
[stopwatch stop];
if (actionFailed) {
GREYLogVerbose(@"Action failed: %@ with time: %f seconds",
[action name],
[stopwatch elapsedTime]);
} else {
GREYLogVerbose(@"Action succeeded: %@ with time: %f seconds",
[action name],
[stopwatch elapsedTime]);
}
}
GREYLogVerbose(@"--Action finished--");
return self;
}
- (instancetype)assert:(id<GREYAssertion>)assertion {
return [self assert:assertion error:nil];
}
- (instancetype)assert:(id<GREYAssertion>)assertion error:(__strong NSError **)errorOrNil {
NSParameterAssert(assertion);
I_CHECK_MAIN_THREAD();
GREYLogVerbose(@"--Assertion started--");
GREYLogVerbose(@"Assertion to perform: %@", [assertion name]);
GREYStopwatch *stopwatch = [[GREYStopwatch alloc] init];
[stopwatch start];
@autoreleasepool {
NSError *executorError;
__block NSError *assertionError = nil;
__weak __typeof__(self) weakSelf = self;
NSNotificationCenter *defaultNotificationCenter = [NSNotificationCenter defaultCenter];
CGFloat interactionTimeout =
(CGFloat)GREY_CONFIG_DOUBLE(kGREYConfigKeyInteractionTimeoutDuration);
// Assign a flag that provides info if the interaction being performed failed.
__block BOOL interactionFailed = NO;
BOOL executionSucceeded =
[[GREYUIThreadExecutor sharedInstance] executeSyncWithTimeout:interactionTimeout 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 *elementNotFoundError = nil;
// Obtain all elements from the hierarchy and populate the passed error in case of
// an element not being found.
NSArray *elements = [strongSelf matchedElementsWithTimeout:interactionTimeout
error:&elementNotFoundError];
id element = (elements.count != 0) ?
[strongSelf grey_uniqueElementInMatchedElements:elements andError:&assertionError] : nil;
// Create the user info dictionary for any notificatons and set it up with the assertion.
NSMutableDictionary *assertionUserInfo = [[NSMutableDictionary alloc] init];
[assertionUserInfo setObject:assertion forKey:kGREYAssertionUserInfoKey];
// Post notification for the assertion to be checked on the found element.
// We send the notification for an assert even if no element was found.
BOOL multipleMatchesPresent = NO;
if (element) {
[assertionUserInfo setObject:element forKey:kGREYAssertionElementUserInfoKey];
} else if (assertionError) {
// Check for multiple matchers since we don't want the assertion to be checked when this
// error surfaces.
multipleMatchesPresent =
(assertionError.code == kGREYInteractionMultipleElementsMatchedErrorCode ||
assertionError.code == kGREYInteractionMatchedElementIndexOutOfBoundsErrorCode);
[assertionUserInfo setObject:assertionError forKey:kGREYAssertionErrorUserInfoKey];
}
[defaultNotificationCenter postNotificationName:kGREYWillPerformAssertionNotification
object:nil
userInfo:assertionUserInfo];
GREYLogVerbose(@"Performing assertion: %@\n with matcher: %@\n with root matcher: %@",
[assertion name], _elementMatcher, _rootMatcher);
// In the case of an assertion, we can have a nil element present as well. For this purpose,
// we check the assertion directly and see if there was any issue. The only case where we
// are completely sure we do not need to perform the action is in the case of a multiple
// matcher.
if (multipleMatchesPresent) {
interactionFailed = YES;
} else if (![assertion assert:element error:&assertionError]) {
interactionFailed = YES;
// Set the elementNotFoundError to the assertionError since the error has been utilized
// already.
if ([assertionError.domain isEqualToString:kGREYInteractionErrorDomain] &&
(assertionError.code == kGREYInteractionElementNotFoundErrorCode)) {
assertionError = elementNotFoundError;
}
// Assertion didn't succeed yet no error was set.
if (!assertionError) {
assertionError = GREYErrorMake(kGREYInteractionErrorDomain,
kGREYInteractionAssertionFailedErrorCode,
@"Reason for assertion failure was not provided.");
}
// Add the error obtained from the action to the user info notification dictionary.
[assertionUserInfo setObject:assertionError forKey:kGREYAssertionErrorUserInfoKey];
}
// Post notification for the process of an assertion's execution on the specified element
// being completed. This notification does not mean that the assertion was performed
// successfully.
[defaultNotificationCenter postNotificationName:kGREYDidPerformAssertionNotification
object:nil
userInfo:assertionUserInfo];
// If we encounter a failure and going to raise an exception, raise it right away before
// the main runloop drains any further.
if (interactionFailed && !errorOrNil) {
[strongSelf grey_handleFailureOfAssertion:assertion
assertionError:assertionError
userProvidedOutError:nil];
}
} error:&executorError];
// Failure to execute due to timeout should be represented as interaction timeout.
if (!executionSucceeded) {
if ([executorError.domain isEqualToString:kGREYUIThreadExecutorErrorDomain] &&
executorError.code == kGREYUIThreadExecutorTimeoutErrorCode) {
NSString *assertionTimeoutDesc =
[NSString stringWithFormat:@"Failed to execute assertion within %g seconds.",
interactionTimeout];
assertionError = GREYNestedErrorMake(kGREYInteractionErrorDomain,
kGREYInteractionTimeoutErrorCode,
assertionTimeoutDesc,
executorError);
}
}
BOOL assertionFailed = !executionSucceeded || interactionFailed;
if (assertionFailed) {
[self grey_handleFailureOfAssertion:assertion
assertionError:assertionError
userProvidedOutError:errorOrNil];
}
[stopwatch stop];
if (assertionFailed) {
GREYLogVerbose(@"Assertion failed: %@ with time: %f seconds",
[assertion name],
[stopwatch elapsedTime]);
} else {
GREYLogVerbose(@"Assertion succeeded: %@ with time: %f seconds",
[assertion name],
[stopwatch elapsedTime]); }
}
GREYLogVerbose(@"--Assertion finished--");
return self;
}
- (instancetype)assertWithMatcher:(id<GREYMatcher>)matcher {
return [self assertWithMatcher:matcher error:nil];
}
- (instancetype)assertWithMatcher:(id<GREYMatcher>)matcher error:(__strong NSError **)errorOrNil {
id<GREYAssertion> assertion = [GREYAssertions grey_createAssertionWithMatcher:matcher];
return [self assert:assertion error:errorOrNil];
}
- (instancetype)usingSearchAction:(id<GREYAction>)action
onElementWithMatcher:(id<GREYMatcher>)matcher {
NSParameterAssert(action);
NSParameterAssert(matcher);
_searchActionElementMatcher = matcher;
_searchAction = action;
return self;
}
# pragma mark - Private
/**
* From the set of matched elements, obtain one unique element for the provided matcher. In case
* there are multiple elements matched, then the one selected by the _@c index provided is chosen
* else the provided @c interactionError is populated.
*
* @param[out] interactionError A passed error for populating if multiple elements are found.
* If this is nil then cases like multiple matchers cannot be checked
* for.
*
* @return A uniquely matched element, if any.
*/
- (id)grey_uniqueElementInMatchedElements:(NSArray *)elements
andError:(__strong NSError **)interactionError {
// If we find that multiple matched elements are present, we narrow them down based on
// any index passed or populate the passed error if the multiple matches are present and
// an incorrect index was passed.
if (elements.count > 1) {
// If the number of matched elements are greater than 1 then we have to use the index for
// matching. We perform a bounds check on the index provided here and throw an exception if
// it fails.
if (_index == NSUIntegerMax) {
*interactionError = [self grey_errorForMultipleMatchingElements:elements
withMatchedElementsIndexOutOfBounds:NO];
return nil;
} else if (_index >= elements.count) {
*interactionError = [self grey_errorForMultipleMatchingElements:elements
withMatchedElementsIndexOutOfBounds:YES];
return nil;
} else {
return [elements objectAtIndex:_index];
}
}
// If you haven't got a multiple / element not found error then you have one single matched
// element and can select it directly.
return [elements firstObject];
}
/**
* Handles failure of an @c action.
*
* @param action The action that failed.
* @param actionError Contains the reason for failure.
* @param[out] userProvidedError The out error (or nil) provided by the user.
* @throws NSException to denote the failure of an action, thrown if the @c userProvidedError
* is nil on test failure.
*
* @return Junk boolean value to suppress xcode warning to have "a non-void return
* value to indicate an error occurred"
*/
- (BOOL)grey_handleFailureOfAction:(id<GREYAction>)action
actionError:(NSError *)actionError
userProvidedOutError:(__strong NSError **)userProvidedError {
NSParameterAssert(actionError);
// Throw an exception if userProvidedError isn't provided and the action failed.
if (!userProvidedError) {
// First check errors that can happen at the inner most level such as timeouts.
NSDictionary * errorDescriptions =
[[GREYError grey_nestedErrorDictionariesForError:actionError] objectAtIndex:0];
if (errorDescriptions != nil) {
NSString *errorDomain = errorDescriptions[kErrorDomainKey];
NSInteger errorCode = [errorDescriptions[kErrorCodeKey] integerValue];
if (([errorDomain isEqualToString:kGREYInteractionErrorDomain]) &&
(errorCode == kGREYInteractionTimeoutErrorCode)) {
NSMutableDictionary *errorDetails = [[NSMutableDictionary alloc] init];
errorDetails[kErrorDetailActionNameKey] = action.name;
errorDetails[kErrorDetailRecoverySuggestionKey] = @"Increase timeout for matching element";
NSArray *keyOrder = @[ kErrorDetailActionNameKey,
kErrorDetailRecoverySuggestionKey ];
NSString *reasonDetail = [GREYObjectFormatter formatDictionary:errorDetails
indent:kGREYObjectFormatIndent
hideEmpty:YES
keyOrder:keyOrder];
NSString *reason = [NSString stringWithFormat:@"Matching element timed out.\n"
@"Exception with Action: %@\n",
reasonDetail];
I_GREYTimeout(reason,
@"Error Trace: %@",
[GREYError grey_nestedDescriptionForError:actionError]);
return NO;
} else if (([errorDomain isEqualToString:kGREYUIThreadExecutorErrorDomain]) &&
(errorCode == kGREYUIThreadExecutorTimeoutErrorCode)) {
NSMutableDictionary *errorDetails = [[NSMutableDictionary alloc] init];
errorDetails[kErrorDetailActionNameKey] = action.name;
errorDetails[kErrorDetailElementMatcherKey] = _elementMatcher.description;
NSArray *keyOrder = @[ kErrorDetailActionNameKey, kErrorDetailElementMatcherKey ];
NSString *reasonDetail = [GREYObjectFormatter formatDictionary:errorDetails
indent:kGREYObjectFormatIndent
hideEmpty:YES
keyOrder:keyOrder];
NSString *reason =
[NSString stringWithFormat:@"Timed out while waiting to perform action.\n"
@"Exception with Action: %@\n", reasonDetail];
if ([actionError isKindOfClass:[GREYError class]]) {
[(GREYError *)actionError setErrorInfo:errorDetails];
}
I_GREYTimeout(reason, @"Error Trace: %@",
[GREYError grey_nestedDescriptionForError:actionError]);
return NO;
}
}
// Second, check for errors with less specific reason (such as interaction error).
if ([actionError.domain isEqualToString:kGREYInteractionErrorDomain]) {
NSString *searchAPIInfo = [self grey_searchActionDescription];
switch (actionError.code) {
case kGREYInteractionElementNotFoundErrorCode: {
NSMutableDictionary *errorDetails = [[NSMutableDictionary alloc] init];
errorDetails[kErrorDetailActionNameKey] = action.name;
errorDetails[kErrorDetailElementMatcherKey] = _elementMatcher.description;
errorDetails[kErrorDetailRecoverySuggestionKey] =
@"Check if element exists in the UI, modify assert criteria, or adjust the matcher";
NSArray *keyOrder = @[ kErrorDetailActionNameKey,
kErrorDetailElementMatcherKey,
kErrorDetailRecoverySuggestionKey ];
NSString *reasonDetail = [GREYObjectFormatter formatDictionary:errorDetails
indent:kGREYObjectFormatIndent
hideEmpty:YES
keyOrder:keyOrder];
NSString *reason = [NSString stringWithFormat:@"Cannot find UI element.\n"
@"Exception with Action: %@\n",
reasonDetail];
if ([actionError isKindOfClass:[GREYError class]]) {
[(GREYError *)actionError setErrorInfo:errorDetails];
}
I_GREYElementNotFound(reason,
@"%@Error Trace: %@",
searchAPIInfo,
[GREYError grey_nestedDescriptionForError:actionError]);
return NO;
}
case kGREYInteractionMultipleElementsMatchedErrorCode: {
NSMutableDictionary *errorDetails = [[NSMutableDictionary alloc] init];
errorDetails[kErrorDetailActionNameKey] = action.name;
errorDetails[kErrorDetailElementMatcherKey] = _elementMatcher.description;
errorDetails[kErrorDetailRecoverySuggestionKey] =
@"Create a more specific matcher to narrow matched element";
NSArray *keyOrder = @[ kErrorDetailActionNameKey,
kErrorDetailElementMatcherKey,
kErrorDetailRecoverySuggestionKey ];
NSString *reasonDetail = [GREYObjectFormatter formatDictionary:errorDetails
indent:kGREYObjectFormatIndent
hideEmpty:YES
keyOrder:keyOrder];
NSString *reason = [NSString stringWithFormat:@"Multiple UI elements matched "
@"for given criteria.\n"
@"Exception with Assertion: %@\n",
reasonDetail];
if ([actionError isKindOfClass:[GREYError class]]) {
[(GREYError *)actionError setErrorInfo:errorDetails];
}
I_GREYMultipleElementsFound(reason,
@"%@Error Trace: %@",
searchAPIInfo,
[GREYError grey_nestedDescriptionForError:actionError]);
return NO;
}
case kGREYInteractionConstraintsFailedErrorCode: {
NSArray *keyOrder = @[ kErrorDetailActionNameKey,
kErrorDetailElementDescriptionKey,
kErrorDetailConstraintRequirementKey,
kErrorDetailConstraintDetailsKey,
kErrorDetailRecoverySuggestionKey ];
NSDictionary *errorInfo = [(GREYError *)actionError errorInfo];
NSString *reasonDetail = [GREYObjectFormatter formatDictionary:errorInfo
indent:kGREYObjectFormatIndent
hideEmpty:YES
keyOrder:keyOrder];
NSString *reason = [NSString stringWithFormat:@"Cannot perform action due to "
@"constraint(s) failure.\n"
@"Exception with Action: %@\n",
reasonDetail];
NSString *nestedError = [GREYError grey_nestedDescriptionForError:actionError];
I_GREYConstraintsFailedWithDetails(reason, nestedError);
return NO;
}
}
}
// Add unique failure messages for failure with unknown reason.
NSMutableDictionary *errorDetails = [[NSMutableDictionary alloc] init];
errorDetails[kErrorDetailActionNameKey] = action.name;
errorDetails[kErrorDetailElementMatcherKey] = _elementMatcher.description;
NSArray *keyOrder = @[ kErrorDetailActionNameKey,
kErrorDetailElementMatcherKey ];
NSString *reasonDetail = [GREYObjectFormatter formatDictionary:errorDetails
indent:kGREYObjectFormatIndent
hideEmpty:YES
keyOrder:keyOrder];
NSString *reason = [NSString stringWithFormat:@"An action failed. "
@"Please refer to the error trace below.\n"
@"Exception with Action: %@\n",
reasonDetail];
I_GREYActionFail(reason,
@"Error Trace: %@",
[GREYError grey_nestedDescriptionForError:actionError]);
} else {
*userProvidedError = actionError;
}
return NO;
}
/**
* Handles failure of an @c assertion.
*
* @param assertion The asserion that failed.
* @param assertionError Contains the reason for the failure.
* @param[out] userProvidedError Error (or @c nil) provided by the user. When @c nil, an exception
* is thrown to halt further execution of the test case.
* @throws NSException to denote an assertion failure, thrown if the @c userProvidedError
* is @c nil on test failure.
*
* @return Junk boolean value to suppress xcode warning to have "a non-void return
* value to indicate an error occurred"
*/
- (BOOL)grey_handleFailureOfAssertion:(id<GREYAssertion>)assertion
assertionError:(NSError *)assertionError
userProvidedOutError:(__strong NSError **)userProvidedError {
NSParameterAssert(assertionError);
// Throw an exception if userProvidedError isn't provided and the assertion failed.
if (!userProvidedError) {
// first check errors that can happens at the inner most level
// for example: executor error
NSDictionary * errorDescriptions =
[[GREYError grey_nestedErrorDictionariesForError:assertionError] objectAtIndex:0];
if (errorDescriptions != nil) {
NSString *errorDomain = errorDescriptions[kErrorDomainKey];
NSInteger errorCode = [errorDescriptions[kErrorCodeKey] integerValue];
if (([errorDomain isEqualToString:kGREYInteractionErrorDomain]) &&
(errorCode == kGREYInteractionTimeoutErrorCode)) {
NSMutableDictionary *errorDetails = [[NSMutableDictionary alloc] init];
errorDetails[kErrorDetailAssertCriteriaKey] = assertion.name;
errorDetails[kErrorDetailRecoverySuggestionKey] = @"Increase timeout for matching element";
NSArray *keyOrder = @[ kErrorDetailAssertCriteriaKey,
kErrorDetailRecoverySuggestionKey ];
NSString *reasonDetail = [GREYObjectFormatter formatDictionary:errorDetails
indent:kGREYObjectFormatIndent
hideEmpty:YES
keyOrder:keyOrder];
NSString *reason = [NSString stringWithFormat:@"Matching element timed out.\n"
@"Exception with Assertion: %@\n",
reasonDetail];
if ([assertionError isKindOfClass:[GREYError class]]) {
[(GREYError *)assertionError setErrorInfo:errorDetails];
}
I_GREYTimeout(reason,
@"Error Trace: %@",
[GREYError grey_nestedDescriptionForError:assertionError]);
return NO;
} else if (([errorDomain isEqualToString:kGREYUIThreadExecutorErrorDomain]) &&
(errorCode == kGREYUIThreadExecutorTimeoutErrorCode)) {
NSMutableDictionary *errorDetails = [[NSMutableDictionary alloc] init];
errorDetails[kErrorDetailAssertCriteriaKey] = assertion.name;
errorDetails[kErrorDetailElementMatcherKey] = _elementMatcher.description;
NSArray *keyOrder = @[ kErrorDetailAssertCriteriaKey,
kErrorDetailElementMatcherKey ];
NSString *reasonDetail = [GREYObjectFormatter formatDictionary:errorDetails
indent:kGREYObjectFormatIndent
hideEmpty:YES
keyOrder:keyOrder];
NSString *reason =
[NSString stringWithFormat:@"Timed out while waiting to perform assertion.\n"
@"Exception with Assertion: %@\n", reasonDetail];
if ([assertionError isKindOfClass:[GREYError class]]) {
[(GREYError *)assertionError setErrorInfo:errorDetails];
}
I_GREYTimeout(reason,
@"Error Trace: %@",
[GREYError grey_nestedDescriptionForError:assertionError]);
return NO;
}
}
// second, check for errors with less specific reason (such as interaction error)
if ([assertionError.domain isEqualToString:kGREYInteractionErrorDomain]) {
NSString *searchAPIInfo = [self grey_searchActionDescription];
switch (assertionError.code) {
case kGREYInteractionElementNotFoundErrorCode: {
NSMutableDictionary *errorDetails = [[NSMutableDictionary alloc] init];
errorDetails[kErrorDetailAssertCriteriaKey] = assertion.name;
errorDetails[kErrorDetailElementMatcherKey] = _elementMatcher.description;
errorDetails[kErrorDetailRecoverySuggestionKey] =
@"Check if element exists in the UI, modify assert criteria, or adjust the matcher";
NSArray *keyOrder = @[ kErrorDetailAssertCriteriaKey,
kErrorDetailElementMatcherKey,
kErrorDetailRecoverySuggestionKey ];
NSString *reasonDetail = [GREYObjectFormatter formatDictionary:errorDetails
indent:kGREYObjectFormatIndent
hideEmpty:YES
keyOrder:keyOrder];
NSString *reason = [NSString stringWithFormat:@"Cannot find UI Element.\n"
@"Exception with Assertion: %@\n",
reasonDetail];
if ([assertionError isKindOfClass:[GREYError class]]) {
[(GREYError *)assertionError setErrorInfo:errorDetails];
}
I_GREYElementNotFound(reason,
@"%@Error Trace: %@",
searchAPIInfo,
[GREYError grey_nestedDescriptionForError:assertionError]);
return NO;
}
case kGREYInteractionMultipleElementsMatchedErrorCode: {
NSMutableDictionary *errorDetails = [[NSMutableDictionary alloc] init];
errorDetails[kErrorDetailAssertCriteriaKey] = assertion.name;
errorDetails[kErrorDetailElementMatcherKey] = _elementMatcher.description;
errorDetails[kErrorDetailRecoverySuggestionKey] =
@"Create a more specific matcher to narrow matched element";
NSArray *keyOrder = @[ kErrorDetailAssertCriteriaKey,
kErrorDetailElementMatcherKey,
kErrorDetailRecoverySuggestionKey ];
NSString *reasonDetail = [GREYObjectFormatter formatDictionary:errorDetails
indent:kGREYObjectFormatIndent
hideEmpty:YES
keyOrder:keyOrder];
NSString *reason = [NSString stringWithFormat:@"Multiple UI elements matched "
@"for given criteria.\n"
@"Exception with Assertion: %@\n",
reasonDetail];
if ([assertionError isKindOfClass:[GREYError class]]) {
[(GREYError *)assertionError setErrorInfo:errorDetails];
}
I_GREYMultipleElementsFound(reason,
@"%@Error Trace: %@",
searchAPIInfo,
[GREYError grey_nestedDescriptionForError:assertionError]);
return NO;
}
}
}
// Add unique failure messages for failure with unknown reason
NSMutableDictionary *errorDetails = [[NSMutableDictionary alloc] init];
errorDetails[kErrorDetailAssertCriteriaKey] = assertion.name;
errorDetails[kErrorDetailElementMatcherKey] = _elementMatcher.description;
NSArray *keyOrder = @[ kErrorDetailAssertCriteriaKey,
kErrorDetailElementMatcherKey ];
NSString *reasonDetail = [GREYObjectFormatter formatDictionary:errorDetails
indent:kGREYObjectFormatIndent
hideEmpty:YES
keyOrder:keyOrder];
NSString *reason = [NSString stringWithFormat:@"An assertion failed.\n"
@"Exception with Assertion: %@\n",
reasonDetail];
I_GREYAssertionFail(reason,
@"Error Trace: %@",
[GREYError grey_nestedDescriptionForError:assertionError]);
} else {
*userProvidedError = assertionError;
}
return NO;
}
/**
* Provides an error with @c kGREYInteractionMultipleElementsMatchedErrorCode for multiple
* elements matching the specified matcher. In case we have multiple matchers and the Index
* provided for not matching with it is out of bounds, then we set the error code to
* @c kGREYInteractionMatchedElementIndexOutOfBoundsErrorCode.
*
* @param matchingElements A set of matching elements.
* @param outOfBounds A boolean that flags if the index for finding a matching element
* is out of bounds.
*
* @return Error for matching multiple elements.
*/
- (NSError *)grey_errorForMultipleMatchingElements:(NSArray *)matchingElements
withMatchedElementsIndexOutOfBounds:(BOOL)outOfBounds {
// Populate an array with the matching elements that are causing the exception.
NSMutableArray *elementDescriptions =
[[NSMutableArray alloc] initWithCapacity:matchingElements.count];
[matchingElements enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[elementDescriptions addObject:[obj grey_description]];
}];
// Populate the multiple matching elements error.
NSString *errorDescription;
NSInteger errorCode;
if (outOfBounds) {
// Populate with an error specifying that the index provided for matching the multiple elements
// was out of bounds.
errorDescription = [NSString stringWithFormat:@"Multiple elements were matched: %@ with an "
@"index that is out of bounds of the number of "
@"matched elements. Please use an element "
@"index from 0 to %tu",
elementDescriptions,
([elementDescriptions count] - 1)];
errorCode = kGREYInteractionMatchedElementIndexOutOfBoundsErrorCode;
} else {
// Populate with an error specifying that multiple elements were matched without providing
// an index.
errorDescription = [NSString stringWithFormat:@"Multiple elements were matched: %@. Please "
@"use selection matchers to narrow the "
@"selection down to single element.",
elementDescriptions];
errorCode = kGREYInteractionMultipleElementsMatchedErrorCode;
}
// Populate the user info for the multiple matching elements error.
return GREYErrorMake(kGREYInteractionErrorDomain, errorCode, errorDescription);
}
/**
* @return A String description of the current search action.
*/
- (NSString *)grey_searchActionDescription {
if (_searchAction) {
return [NSString stringWithFormat:@"Search action: %@. \nSearch action element matcher: %@.\n",
_searchAction, _searchActionElementMatcher];
} else {
return @"";
}
}
@end