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

Add support for swiping from the specified edge of the screen #1286

Merged
merged 5 commits into from
Jul 12, 2023
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
28 changes: 28 additions & 0 deletions KIF Tests/GestureTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,34 @@ - (void)testSwipingDownWithIdentifier
[tester waitForViewWithAccessibilityLabel:@"Down"];
}

- (void)testSwipingFromScreenEdgeLeft
{
UIView *view = [tester waitForViewWithAccessibilityIdentifier:@"gestures.swipeMe"];
CGSize windowSize = view.window.bounds.size;
CGPoint point = CGPointMake(0.5, 200);
point = [view convertPoint:point fromView:view.window];
KIFDisplacement displacement = CGPointMake(windowSize.width * 0.5, 5);
[view dragFromPoint:point displacement:displacement steps:20];
[tester waitForAbsenceOfViewWithAccessibilityLabel:@"LeftEdge"];

[tester swipeFromEdge:UIRectEdgeLeft];
[tester waitForViewWithAccessibilityLabel:@"LeftEdge"];
}

- (void)testSwipingFromScreenEdgeRight
{
UIView *view = [tester waitForViewWithAccessibilityIdentifier:@"gestures.swipeMe"];
CGSize windowSize = view.window.bounds.size;
CGPoint point = CGPointMake(windowSize.width - 0.5, 200);
point = [view convertPoint:point fromView:view.window];
KIFDisplacement displacement = CGPointMake(-windowSize.width * 0.5, 5);
[view dragFromPoint:point displacement:displacement steps:20];
[tester waitForAbsenceOfViewWithAccessibilityLabel:@"RightEdge"];

[tester swipeFromEdge:UIRectEdgeRight];
[tester waitForViewWithAccessibilityLabel:@"RightEdge"];
}

- (void)testScrolling
{
// Needs to be offset from the edge to prevent the navigation controller's interactivePopGestureRecognizer from triggering
Expand Down
12 changes: 12 additions & 0 deletions KIF Tests/GestureTests_ViewTestActor.m
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,16 @@ - (void)testMissingScrollableElement
KIFExpectFailure([[[viewTester usingTimeout:0.25] usingIdentifier:@"Unknown"] scrollByFractionOfSizeHorizontal:0.5 vertical:0.5]);
}

- (void)testSwipingFromScreenEdgeLeft
{
[viewTester swipeFromEdge:UIRectEdgeLeft];
[[viewTester usingLabel:@"LeftEdge"] waitForView];
}

- (void)testSwipingFromScreenEdgeRight
{
[viewTester swipeFromEdge:UIRectEdgeRight];
[[viewTester usingLabel:@"RightEdge"] waitForView];
}

@end
1 change: 1 addition & 0 deletions Sources/KIF/Additions/UITouch-KIFAdditions.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@

- (void)setLocationInWindow:(CGPoint)location;
- (void)setPhaseAndUpdateTimestamp:(UITouchPhase)phase;
- (void)setIsFromEdge:(BOOL)isFromEdge;

@end
7 changes: 7 additions & 0 deletions Sources/KIF/Additions/UITouch-KIFAdditions.m
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ - (void)_setIsFirstTouchForView:(BOOL)firstTouchForView;
- (void)_setIsTapToClick:(BOOL)tapToClick;

- (void)_setHidEvent:(IOHIDEventRef)event;
- (void)_setEdgeType:(NSInteger)edgeType;

@end

Expand Down Expand Up @@ -117,6 +118,12 @@ - (void)setPhaseAndUpdateTimestamp:(UITouchPhase)phase
[self setPhase:phase];
}

- (void)setIsFromEdge:(BOOL)isFromEdge
{
NSInteger edgeType = isFromEdge ? 4 : 0;
[self _setEdgeType:edgeType];
}
Comment on lines +121 to +125
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would happen if we directly called _UITouchSetBasicTouchPropertiesFromEvent? Does Apple already have the appropriate heuristics to set this properly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apple derives the value of the property from the HIDEvent. We would need to synthesize a different HIDEvent for edge pan before calling _UITouchSetBasicTouchPropertiesFromEvent. This introduces additional complexity and I lack the necessary knowledge to implement it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could replace these magic numbers with Apple enums.


- (void)kif_setHidEvent {
IOHIDEventRef event = kif_IOHIDEventWithTouches(@[self]);
[self _setHidEvent:event];
Expand Down
1 change: 1 addition & 0 deletions Sources/KIF/Additions/UIView-KIFAdditions.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ typedef CGPoint KIFDisplacement;
- (void)dragFromPoint:(CGPoint)startPoint toPoint:(CGPoint)endPoint;
- (void)dragFromPoint:(CGPoint)startPoint toPoint:(CGPoint)endPoint steps:(NSUInteger)stepCount;
- (void)dragFromPoint:(CGPoint)startPoint displacement:(KIFDisplacement)displacement steps:(NSUInteger)stepCount;
- (void)dragFromEdge:(UIRectEdge)startEdge toEdge:(UIRectEdge)endEdge;
- (void)dragAlongPathWithPoints:(CGPoint *)points count:(NSInteger)count;
- (void)twoFingerPanFromPoint:(CGPoint)startPoint toPoint:(CGPoint)toPoint steps:(NSUInteger)stepCount;
- (void)pinchAtPoint:(CGPoint)centerPoint distance:(CGFloat)distance steps:(NSUInteger)stepCount;
Expand Down
26 changes: 26 additions & 0 deletions Sources/KIF/Additions/UIView-KIFAdditions.m
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,27 @@ - (void)dragFromPoint:(CGPoint)startPoint displacement:(KIFDisplacement)displace
[self dragPointsAlongPaths:@[path]];
}

- (void)dragFromEdge:(UIRectEdge)startEdge toEdge:(UIRectEdge)endEdge
{
CGFloat width = self.bounds.size.width;
CGFloat height = self.bounds.size.height;
CGFloat edgeInset = 0.5;
NSDictionary *edgeToPoint = @{
@(UIRectEdgeTop): @(CGPointMake(width / 2, edgeInset)),
@(UIRectEdgeLeft): @(CGPointMake(edgeInset, height / 2)),
@(UIRectEdgeBottom): @(CGPointMake(width / 2, height - edgeInset)),
@(UIRectEdgeRight): @(CGPointMake(width - edgeInset, height / 2)),
};
CGPoint startPoint = [edgeToPoint[@(startEdge)] CGPointValue];
CGPoint endPoint = [edgeToPoint[@(endEdge)] CGPointValue];

CGPoint screenPoint = [self convertPoint:startPoint toView:self.window];
BOOL isFromScreenEdge = (screenPoint.x < 1 || screenPoint.x > self.window.bounds.size.width - 1);

NSArray<NSValue *> *path = [self pointsFromStartPoint:startPoint toPoint:endPoint steps:20];
[self dragPointsAlongPaths:@[path] isFromEdge:isFromScreenEdge];
}

- (void)dragAlongPathWithPoints:(CGPoint *)points count:(NSInteger)count;
{
// convert point array into NSArray with NSValue
Expand All @@ -648,6 +669,10 @@ - (void)dragAlongPathWithPoints:(CGPoint *)points count:(NSInteger)count;
}

- (void)dragPointsAlongPaths:(NSArray<NSArray<NSValue *> *> *)arrayOfPaths {
[self dragPointsAlongPaths:arrayOfPaths isFromEdge:NO];
}

- (void)dragPointsAlongPaths:(NSArray<NSArray<NSValue *> *> *)arrayOfPaths isFromEdge:(BOOL)isFromEdge {
// There must be at least one path with at least one point
if (arrayOfPaths.count == 0 || arrayOfPaths.firstObject.count == 0)
{
Expand Down Expand Up @@ -692,6 +717,7 @@ - (void)dragPointsAlongPaths:(NSArray<NSArray<NSValue *> *> *)arrayOfPaths {
point = [self convertPoint:point fromView:self.window];
UITouch *touch = [[UITouch alloc] initAtPoint:point inView:self];
[touch setPhaseAndUpdateTimestamp:UITouchPhaseBegan];
[touch setIsFromEdge:isFromEdge];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should alternatively come up with a heuristic here for what constitutes an edge swipe and do it implicitly. Being explicit seems reasonable as well though.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think with this we can make an API specifically for swiping at an edge, similarly to like tapping on a button or status bar instead of using x,y coordinatotes. As well as implicitly doing it from the edge if we can detect it being around where it should be. I think that'd be in line with other APIs of KIf

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about doing it implicitly like

if (startPoint.x < 1 || startPoint.x > screen.size.width - 1) {
    [touch setIsFromEdge:isFromEdge];
}

But this could be a breaking change for users who utilize the dragFromPoint method without intending to trigger a screen edge pan gesture.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried to implement an alternative way based on the suggestions, which one do you prefer?

[touches addObject:touch];
}
UIEvent *eventDown = [self eventWithTouches:[NSArray arrayWithArray:touches]];
Expand Down
6 changes: 6 additions & 0 deletions Sources/KIF/Classes/KIFUITestActor.h
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,12 @@ typedef NS_ENUM(NSUInteger, KIFPullToRefreshTiming) {
*/
- (void)tapScreenAtPoint:(CGPoint)screenPoint;

/*!
@abstract Performs a swipe gesture starting from the specified edge of the screen.
@param edge The edge from which the swipe gesture should start.
*/
- (void)swipeFromEdge:(UIRectEdge)edge;

/*!
@abstract Performs a long press on a particular view in the view hierarchy.
@discussion The view or accessibility element with the given label is searched for in the view hierarchy. If the element isn't found or isn't currently tappable, then the step will attempt to wait until it is. Once the view is present and tappable, touch events are simulated in the center of the view or element.
Expand Down
52 changes: 40 additions & 12 deletions Sources/KIF/Classes/KIFUITestActor.m
Original file line number Diff line number Diff line change
Expand Up @@ -366,18 +366,7 @@ - (void)tapAccessibilityElement:(UIAccessibilityElement *)element inView:(UIView
- (void)tapScreenAtPoint:(CGPoint)screenPoint
{
[self runBlock:^KIFTestStepResult(NSError **error) {

// Try all the windows until we get one back that actually has something in it at the given point
UIView *view = nil;
for (UIWindow *window in [[[UIApplication sharedApplication] windowsWithKeyWindow] reverseObjectEnumerator]) {
CGPoint windowPoint = [window convertPoint:screenPoint fromView:nil];
view = [window hitTest:windowPoint withEvent:nil];

// If we hit the window itself, then skip it.
if (view != window && view != nil) {
break;
}
}
UIView *view = [self viewAtPoint:screenPoint];

KIFTestWaitCondition(view, error, @"No view was found at the point %@", NSStringFromCGPoint(screenPoint));

Expand All @@ -389,6 +378,45 @@ - (void)tapScreenAtPoint:(CGPoint)screenPoint
}];
}

- (void)swipeFromEdge:(UIRectEdge)edge
{
CGSize screenSize = UIScreen.mainScreen.bounds.size;
CGPoint screenPoint;
if (edge == UIRectEdgeLeft) {
screenPoint = CGPointMake(0.3, screenSize.height / 2);
} else if (edge == UIRectEdgeRight) {
screenPoint = CGPointMake(screenSize.width - 0.3, screenSize.height / 2);
} else {
return;
}
[self runBlock:^KIFTestStepResult(NSError **error) {
UIView *view = [self viewAtPoint:screenPoint];

KIFTestWaitCondition(view, error, @"No view was found at the point %@", NSStringFromCGPoint(screenPoint));

UIRectEdge endEdge = (UIRectEdgeLeft | UIRectEdgeRight) - edge;
[view dragFromEdge:edge toEdge:endEdge];

return KIFTestStepResultSuccess;
}];
}

- (UIView *)viewAtPoint:(CGPoint)screenPoint
{
// Try all the windows until we get one back that actually has something in it at the given point
UIView *view = nil;
for (UIWindow *window in [[[UIApplication sharedApplication] windowsWithKeyWindow] reverseObjectEnumerator]) {
CGPoint windowPoint = [window convertPoint:screenPoint fromView:nil];
view = [window hitTest:windowPoint withEvent:nil];

// If we hit the window itself, then skip it.
if (view != window && view != nil) {
break;
}
}
return view;
}

- (void)longPressViewWithAccessibilityLabel:(NSString *)label duration:(NSTimeInterval)duration;
{
[self longPressViewWithAccessibilityLabel:label value:nil traits:UIAccessibilityTraitNone duration:duration];
Expand Down
6 changes: 6 additions & 0 deletions Sources/KIF/Classes/KIFUIViewTestActor.h
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ extern NSString *const inputFieldTestString;
*/
- (void)swipeInDirection:(KIFSwipeDirection)direction;

/*!
@abstract Performs a swipe gesture starting from the specified edge of the screen.
@param edge The edge from which the swipe gesture should start.
*/
- (void)swipeFromEdge:(UIRectEdge)edge;

#pragma mark Waiting & Finding

/*!
Expand Down
5 changes: 5 additions & 0 deletions Sources/KIF/Classes/KIFUIViewTestActor.m
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,11 @@ - (void)swipeInDirection:(KIFSwipeDirection)direction;
}
}

- (void)swipeFromEdge:(UIRectEdge)edge
{
[self.actor swipeFromEdge:edge];
}

#pragma mark - Scroll/Table/CollectionView Actions

- (void)scrollByFractionOfSizeHorizontal:(CGFloat)horizontalFraction vertical:(CGFloat)verticalFraction;
Expand Down
Loading