diff --git a/detox/ios/Detox/Utilities/UIView+DetoxUtils.h b/detox/ios/Detox/Utilities/UIView+DetoxUtils.h index 2a3a5b2fe7..fbf1391580 100644 --- a/detox/ios/Detox/Utilities/UIView+DetoxUtils.h +++ b/detox/ios/Detox/Utilities/UIView+DetoxUtils.h @@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, weak) UIViewController* dtx_containingViewController; - (UIImage*)dtx_imageFromView; +- (BOOL)isVisibleAroundPoint:(CGPoint)point; @end diff --git a/detox/ios/Detox/Utilities/UIView+DetoxUtils.m b/detox/ios/Detox/Utilities/UIView+DetoxUtils.m index eb57e947c1..7fde4e5067 100644 --- a/detox/ios/Detox/Utilities/UIView+DetoxUtils.m +++ b/detox/ios/Detox/Utilities/UIView+DetoxUtils.m @@ -349,6 +349,11 @@ - (UIViewController *)dtx_containingViewController #pragma mark - Check Hitability - (BOOL)dtx_isHittable { + // TODO: This workaround should be removed (`sleepForTimeInterval`). + // It was added because DetoxSync appears to ignore UI view controller transitions to be completed + // before the next action is taking place. + [NSThread sleepForTimeInterval:0.5]; + CGPoint point = [self findVisiblePoint]; return [self dtx_isHittableAtPoint:point error:nil]; } @@ -375,8 +380,7 @@ - (BOOL)dtx_isHittableAtPoint:(CGPoint)viewPoint return NO; } - if (![self _isVisibleAroundPoint:viewPoint visibleBounds:self.dtx_visibleBounds - error:error]) { + if (![self _isVisibleAroundPoint:viewPoint error:error]) { if (error) { NSString *description = [NSString stringWithFormat:@"View is not visible around" \ " point.\n- view point: %@\n- visible bounds: %@" \ @@ -393,11 +397,23 @@ - (BOOL)dtx_isHittableAtPoint:(CGPoint)viewPoint return NO; } - UIViewController *topMostViewController = [self _findTopMostViewController]; - UIView *visibleContainer = topMostViewController.view; - CGPoint absPoint = [self calcAbsPointFromLocalPoint:viewPoint]; + UIViewController * _Nullable topMostViewController = [self _topMostViewControllerAtPoint:absPoint]; + if (!topMostViewController) { + if (error) { + NSString *description = [NSString stringWithFormat:@"Failed to interact with the screen " + "at point: %@.", NSStringFromCGPoint(viewPoint)]; + *error = [NSError + errorWithDomain:@"Detox" code:0 + userInfo:@{NSLocalizedDescriptionKey:description}]; + } + + return NO; + } + + UIView *visibleContainer = topMostViewController.view; + if ([self isDescendantOfView:visibleContainer]) { return [self _canHitFromView:self atAbsPoint:absPoint error:error]; } @@ -410,10 +426,13 @@ - (BOOL)dtx_isHittableAtPoint:(CGPoint)viewPoint return [self _canHitFromView:visibleContainer atAbsPoint:absPoint error:error]; } -- (BOOL)_isVisibleAroundPoint:(CGPoint)point visibleBounds:(CGRect)visibleBounds - error:(NSError* __strong __nullable * __nullable)error { +- (BOOL)isVisibleAroundPoint:(CGPoint)point { + return [self _isVisibleAroundPoint:point error:nil]; +} + +- (BOOL)_isVisibleAroundPoint:(CGPoint)point error:(NSError* __strong __nullable * __nullable)error { CGRect intersection = CGRectIntersection( - visibleBounds, CGRectMake(point.x - 0.5, point.y - 0.5, 1, 1)); + self.dtx_visibleBounds, CGRectMake(point.x - 0.5, point.y - 0.5, 1, 1)); return [self _dtx_testVisibilityInRect:intersection percent:100 error:error]; } @@ -446,14 +465,18 @@ - (BOOL)_canHitFromView:(UIView *)originView atAbsPoint:(CGPoint)point return NO; } -- (UIViewController *)_findTopMostViewController { - UIWindow *topMostWindow = UIWindow.dtx_keyWindow; - return [self _findTopMostViewControllerForViewController:topMostWindow.rootViewController]; +- (nullable UIViewController *)_topMostViewControllerAtPoint:(CGPoint)point { + UIWindow * _Nullable topMostWindow = [UIWindow dtx_topMostWindowAtPoint:point]; + if (!topMostWindow) { + return nil; + } + + return [self _topMostViewControllerForViewController:topMostWindow.rootViewController]; } -- (UIViewController *)_findTopMostViewControllerForViewController:(UIViewController *)viewController { +- (UIViewController *)_topMostViewControllerForViewController:(UIViewController *)viewController { if (viewController.presentedViewController) { - return [self _findTopMostViewControllerForViewController:viewController.presentedViewController]; + return [self _topMostViewControllerForViewController:viewController.presentedViewController]; } return viewController; diff --git a/detox/ios/Detox/Utilities/UIWindow+DetoxUtils.h b/detox/ios/Detox/Utilities/UIWindow+DetoxUtils.h index 902b67029d..bbb74546ef 100644 --- a/detox/ios/Detox/Utilities/UIWindow+DetoxUtils.h +++ b/detox/ios/Detox/Utilities/UIWindow+DetoxUtils.h @@ -27,6 +27,7 @@ NS_ASSUME_NONNULL_BEGIN + (void)dtx_enumerateAllWindowsUsingBlock:(void (NS_NOESCAPE ^)(UIWindow* obj, NSUInteger idx, BOOL *stop))block; + (void)dtx_enumerateKeyWindowSceneWindowsUsingBlock:(void (NS_NOESCAPE ^)(UIWindow* obj, NSUInteger idx, BOOL *stop))block; + (void)dtx_enumerateWindowsInScene:(nullable UIWindowScene*)scene usingBlock:(void (NS_NOESCAPE ^)(UIWindow* obj, NSUInteger idx, BOOL *stop))block; ++ (nullable UIWindow *)dtx_topMostWindowAtPoint:(CGPoint)point; @end diff --git a/detox/ios/Detox/Utilities/UIWindow+DetoxUtils.m b/detox/ios/Detox/Utilities/UIWindow+DetoxUtils.m index ac33385a5a..589f5ae5a7 100644 --- a/detox/ios/Detox/Utilities/UIWindow+DetoxUtils.m +++ b/detox/ios/Detox/Utilities/UIWindow+DetoxUtils.m @@ -7,8 +7,10 @@ // #import "UIWindow+DetoxUtils.h" -#import "NSObject+DetoxUtils.h" + #import "DTXAppleInternals.h" +#import "NSObject+DetoxUtils.h" +#import "UIView+DetoxUtils.h" extern NSArray* DTXChildElements(id element); @@ -221,4 +223,38 @@ - (NSString *)dtx_shortDescription return [NSString stringWithFormat:@"<%@: %p; frame = (%@ %@; %@ %@);>", self.class, self, @(frame.origin.x), @(frame.origin.y), @(frame.size.width), @(frame.size.height)]; } ++ (nullable UIWindow *)dtx_topMostWindowAtPoint:(CGPoint)point { + NSArray *windows = UIApplication.sharedApplication.windows; + + NSArray *visibleWindowsAtPoint = [windows + filteredArrayUsingPredicate:[NSPredicate + predicateWithBlock:^BOOL(UIWindow *window, NSDictionary * _Nullable __unused bindings) { + if (!CGRectContainsPoint(window.frame, point)) { + return NO; + } + + if (![window isVisibleAroundPoint:point]) { + return NO; + } + + UIView * _Nullable hitten = [window hitTest:point withEvent:nil]; + if (!hitten) { + // The point lies completely outside the windos's hierarchy. + return NO; + } + + return YES; + }]]; + + if (!visibleWindowsAtPoint) { + return nil; + } + + return [[visibleWindowsAtPoint + sortedArrayUsingComparator:^NSComparisonResult(UIWindow *window1, UIWindow *window2) { + return window1.windowLevel - window2.windowLevel; + }] + lastObject]; +} + @end