From a0965d67d386d0bf844101062b57ea6c5f76c8da Mon Sep 17 00:00:00 2001 From: talkol Date: Wed, 12 Oct 2016 13:46:57 +0300 Subject: [PATCH] swipe action, closes #29 --- detox/ios/Detox/GREYMatchers+Detox.h | 2 ++ detox/ios/Detox/GREYMatchers+Detox.m | 17 +++++++++++++ detox/src/ios/expect.js | 32 +++++++++++++++++++++++++ detox/test/e2e/c-actions.js | 5 ++++ detox/test/src/Screens/ActionsScreen.js | 24 +++++++++++++++++-- 5 files changed, 78 insertions(+), 2 deletions(-) diff --git a/detox/ios/Detox/GREYMatchers+Detox.h b/detox/ios/Detox/GREYMatchers+Detox.h index b666987669..3652289162 100644 --- a/detox/ios/Detox/GREYMatchers+Detox.h +++ b/detox/ios/Detox/GREYMatchers+Detox.h @@ -14,6 +14,8 @@ + (id)detoxMatcherForScrollChildOfMatcher:(id)matcher; ++ (id)detoxMatcherAvoidingProblematicReactNativeElements:(id)matcher; + + (id)detoxMatcherForBoth:(id)firstMatcher and:(id)secondMatcher; + (id)detoxMatcherForBoth:(id)firstMatcher andAncestorMatcher:(id)ancestorMatcher; diff --git a/detox/ios/Detox/GREYMatchers+Detox.m b/detox/ios/Detox/GREYMatchers+Detox.m index 40530ec97c..879a88c6aa 100644 --- a/detox/ios/Detox/GREYMatchers+Detox.m +++ b/detox/ios/Detox/GREYMatchers+Detox.m @@ -37,6 +37,23 @@ @implementation GREYMatchers (Detox) grey_ancestor(matcher), nil), nil); } ++ (id)detoxMatcherAvoidingProblematicReactNativeElements:(id)matcher +{ + Class RN_RCTScrollView = NSClassFromString(@"RCTScrollView"); + if (!RN_RCTScrollView) + { + return matcher; + } + + // RCTScrollView is problematic because EarlGrey's visibility matcher adds a subview and this causes a RN assertion + // solution: if we match RCTScrollView, switch over to matching its contained UIScrollView + + return grey_anyOf(grey_allOf(grey_kindOfClass([UIScrollView class]), + grey_ancestor(grey_allOf(matcher, grey_kindOfClass(RN_RCTScrollView), nil)), nil), + grey_allOf(matcher, + grey_not(grey_kindOfClass(RN_RCTScrollView)), nil), nil); +} + + (id)detoxMatcherForBoth:(id)firstMatcher and:(id)secondMatcher { return grey_allOf(firstMatcher, secondMatcher, nil); diff --git a/detox/src/ios/expect.js b/detox/src/ios/expect.js index db6fffc2d6..36c56eeede 100644 --- a/detox/src/ios/expect.js +++ b/detox/src/ios/expect.js @@ -37,6 +37,11 @@ class Matcher { this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherForBoth:andDescendantMatcher:', _originalMatcherCall, matcher._call); return this; } + _avoidProblematicReactNativeElements() { + const _originalMatcherCall = this._call; + this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherAvoidingProblematicReactNativeElements:', _originalMatcherCall); + return this; + } } class LabelMatcher extends Matcher { @@ -235,6 +240,28 @@ class ScrollEdgeAction extends Action { } } +class SwipeAction extends Action { + constructor(direction, speed) { + super(); + if (typeof direction !== 'string') throw new Error(`SwipeAction ctor 1st argument must be a string, got ${typeof direction}`); + if (typeof speed !== 'string') throw new Error(`SwipeAction ctor 2nd argument must be a string, got ${typeof speed}`); + switch (direction) { + case 'left': direction = 1; break; + case 'right': direction = 2; break; + case 'up': direction = 3; break; + case 'down': direction = 4; break; + default: throw new Error(`SwipeAction direction must be a 'left'/'right'/'up'/'down', got ${direction}`); + } + if (speed == 'fast') { + this._call = invoke.call(invoke.IOS.Class('GREYActions'), 'actionForSwipeFastInDirection:', invoke.IOS.NSInteger(direction)); + } else if (speed == 'slow') { + this._call = invoke.call(invoke.IOS.Class('GREYActions'), 'actionForSwipeSlowInDirection:', invoke.IOS.NSInteger(direction)); + } else { + throw new Error(`SwipeAction speed must be a 'fast'/'slow', got ${speed}`); + } + } +} + class Interaction { execute() { if (!this._call) throw new Error(`Interaction.execute cannot find a valid _call, got ${typeof this._call}`); @@ -353,6 +380,11 @@ class Element { this._selectElementWithMatcher(new ExtendedScrollMatcher(this._originalMatcher)); return new ActionInteraction(this, new ScrollEdgeAction(edge)).execute(); } + swipe(direction, speed = 'fast') { + // override the user's element selection with an extended matcher that avoids RN issues with RCTScrollView + this._selectElementWithMatcher(this._originalMatcher._avoidProblematicReactNativeElements()); + return new ActionInteraction(this, new SwipeAction(direction, speed)).execute(); + } } class Expect {} diff --git a/detox/test/e2e/c-actions.js b/detox/test/e2e/c-actions.js index 2ca5b0ae13..3c9b47218b 100644 --- a/detox/test/e2e/c-actions.js +++ b/detox/test/e2e/c-actions.js @@ -63,4 +63,9 @@ describe('Actions', function () { expect(element(by.label('Text1'))).toBeVisible(); }); + it('should swipe down until pull to reload is triggered', function () { + element(by.id('ScrollView799')).swipe('down', 'fast'); + expect(element(by.label('PullToReload Working!!!'))).toBeVisible(); + }); + }); diff --git a/detox/test/src/Screens/ActionsScreen.js b/detox/test/src/Screens/ActionsScreen.js index 185266146a..16f818fa5d 100644 --- a/detox/test/src/Screens/ActionsScreen.js +++ b/detox/test/src/Screens/ActionsScreen.js @@ -4,7 +4,8 @@ import { View, TouchableOpacity, TextInput, - ScrollView + ScrollView, + RefreshControl } from 'react-native'; export default class ActionsScreen extends Component { @@ -15,7 +16,8 @@ export default class ActionsScreen extends Component { greeting: undefined, typeText: '', clearText: 'some stuff here..', - numTaps: 0 + numTaps: 0, + isRefreshing: false }; } @@ -60,6 +62,13 @@ export default class ActionsScreen extends Component { + + + }> + + + ); } @@ -108,4 +117,15 @@ export default class ActionsScreen extends Component { } } + onRefresh() { + this.setState({ + isRefreshing: true + }); + setTimeout(() => { + this.setState({ + greeting: 'PullToReload Working' + }); + }, 500); + } + }