Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ios, UIView+DetoxUtils): locate top-most window at point. #3145

Merged
merged 5 commits into from
Dec 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions detox/ios/Detox/Utilities/UIView+DetoxUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_BEGIN

@property (nonatomic, readonly, weak) UIViewController* dtx_containingViewController;
- (UIImage*)dtx_imageFromView;
- (BOOL)isVisibleAroundPoint:(CGPoint)point;

@end

Expand Down
49 changes: 36 additions & 13 deletions detox/ios/Detox/Utilities/UIView+DetoxUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand All @@ -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: %@" \
Expand All @@ -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];
}
Expand All @@ -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];
}

Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions detox/ios/Detox/Utilities/UIWindow+DetoxUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
38 changes: 37 additions & 1 deletion detox/ios/Detox/Utilities/UIWindow+DetoxUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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<UIWindow *> *windows = UIApplication.sharedApplication.windows;

NSArray<UIWindow *> *visibleWindowsAtPoint = [windows
filteredArrayUsingPredicate:[NSPredicate
predicateWithBlock:^BOOL(UIWindow *window, NSDictionary<NSString *, id> * _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