Skip to content

Commit

Permalink
added waitFor to API which allows to wait for things to happen, the f…
Browse files Browse the repository at this point in the history
…irst one is waiting for an element to be visible while scrolling
  • Loading branch information
talkol committed Oct 9, 2016
1 parent 38a9e25 commit cfc9587
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 15 deletions.
2 changes: 2 additions & 0 deletions detox/ios/Detox/GREYMatchers+Detox.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@

+ (id<GREYMatcher>)detoxMatcherForScrollChildOfMatcher:(id<GREYMatcher>)matcher;

+ (id<GREYMatcher>)detoxMatcherForBoth:(id<GREYMatcher>)firstMatcher and:(id<GREYMatcher>)secondMatcher;

@end
5 changes: 5 additions & 0 deletions detox/ios/Detox/GREYMatchers+Detox.m
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,9 @@ @implementation GREYMatchers (Detox)
grey_ancestor(matcher), nil), nil);
}

+ (id<GREYMatcher>)detoxMatcherForBoth:(id<GREYMatcher>)firstMatcher and:(id<GREYMatcher>)secondMatcher
{
return grey_allOf(firstMatcher, secondMatcher, nil);
}

@end
105 changes: 90 additions & 15 deletions detox/src/ios/expect.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ class ExtendedScrollMatcher extends Matcher {
}
}

class CombineBothMatcher extends Matcher {
constructor(firstMatcher, secondMatcher) {
super();
if (!firstMatcher instanceof Matcher) throw new Error(`CombineBothMatcher ctor 1st argument must be a valid Matcher, got ${typeof firstMatcher}`);
if (!secondMatcher instanceof Matcher) throw new Error(`CombineBothMatcher ctor 2nd argument must be a valid Matcher, got ${typeof secondMatcher}`);
this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherForBoth:and:', firstMatcher._call, secondMatcher._call);
}
}

class Action {}

class TapAction extends Action {
Expand Down Expand Up @@ -166,18 +175,61 @@ class Interaction {
class ActionInteraction extends Interaction {
constructor(element, action) {
super();
if (!element instanceof Element) throw new Error(`ActionInteraction ctor argument must be a valid Element, got ${typeof element}`);
if (!action instanceof Action) throw new Error(`ActionInteraction ctor argument must be a valid Action, got ${typeof action}`);
if (!element instanceof Element) throw new Error(`ActionInteraction ctor 1st argument must be a valid Element, got ${typeof element}`);
if (!action instanceof Action) throw new Error(`ActionInteraction ctor 2nd argument must be a valid Action, got ${typeof action}`);
this._call = invoke.call(element._call, 'performAction:', action._call);
// TODO: move this.execute() here from the caller
}
}

class MatcherAssertionInteraction extends Interaction {
constructor(element, matcher) {
super();
if (!element instanceof Element) throw new Error(`MatcherAssertionInteraction ctor argument must be a valid Element, got ${typeof element}`);
if (!matcher instanceof Matcher) throw new Error(`MatcherAssertionInteraction ctor argument must be a valid Matcher, got ${typeof matcher}`);
if (!element instanceof Element) throw new Error(`MatcherAssertionInteraction ctor 1st argument must be a valid Element, got ${typeof element}`);
if (!matcher instanceof Matcher) throw new Error(`MatcherAssertionInteraction ctor 2nd argument must be a valid Matcher, got ${typeof matcher}`);
this._call = invoke.call(element._call, 'assertWithMatcher:', matcher._call);
// TODO: move this.execute() here from the caller
}
}

class WaitForInteraction extends Interaction {
constructor(element, matcher) {
super();
if (!element instanceof Element) throw new Error(`WaitForInteraction ctor 1st argument must be a valid Element, got ${typeof element}`);
if (!matcher instanceof Matcher) throw new Error(`WaitForInteraction ctor 2nd argument must be a valid Matcher, got ${typeof matcher}`);
this._element = element;
this._originalMatcher = matcher;
}
withTimeout(timeout) {
throw new Error('not implemented');
}
whileElement(searchMatcher) {
return new WaitForActionInteraction(this._element, this._originalMatcher, searchMatcher);
}
}

class WaitForActionInteraction extends Interaction {
constructor(element, matcher, searchMatcher) {
super();
if (!element instanceof Element) throw new Error(`WaitForActionInteraction ctor 1st argument must be a valid Element, got ${typeof element}`);
if (!matcher instanceof Matcher) throw new Error(`WaitForActionInteraction ctor 2nd argument must be a valid Matcher, got ${typeof matcher}`);
if (!searchMatcher instanceof Matcher) throw new Error(`WaitForActionInteraction ctor 3rd argument must be a valid Matcher, got ${typeof searchMatcher}`);
this._element = element;
this._originalMatcher = matcher;
this._searchMatcher = searchMatcher;
// we need to override the original matcher for the element and add matcher to it as well
this._element._selectElementWithMatcher(new CombineBothMatcher(this._element._originalMatcher, matcher));
}
_execute(searchAction) {
if (!searchAction instanceof Action) throw new Error(`WaitForActionInteraction _execute argument must be a valid Action, got ${typeof searchAction}`);
const _interactionCall = invoke.call(this._element._call, 'usingSearchAction:onElementWithMatcher:', searchAction._call, this._searchMatcher._call);
this._call = invoke.call(_interactionCall, 'assertWithMatcher:', this._originalMatcher._call);
this.execute();
}
scroll(amount, direction = 'down') {
// override the user's element selection with an extended matcher that looks for UIScrollView children
this._searchMatcher = new ExtendedScrollMatcher(this._searchMatcher);
this._execute(new ScrollAmountAction(direction, amount));
}
}

Expand Down Expand Up @@ -223,36 +275,57 @@ class ExpectElement extends Expect {
constructor(element) {
super();
if (!element instanceof Element) throw new Error(`ExpectElement ctor argument must be a valid Element, got ${typeof element}`);
this._object = element;
this._element = element;
}
toBeVisible() {
return new MatcherAssertionInteraction(this._object, new VisibleMatcher()).execute();
return new MatcherAssertionInteraction(this._element, new VisibleMatcher()).execute();
}
toBeNotVisible() {
return new MatcherAssertionInteraction(this._object, new NotVisibleMatcher()).execute();
return new MatcherAssertionInteraction(this._element, new NotVisibleMatcher()).execute();
}
toExist() {
return new MatcherAssertionInteraction(this._object, new ExistsMatcher()).execute();
return new MatcherAssertionInteraction(this._element, new ExistsMatcher()).execute();
}
toNotExist() {
return new MatcherAssertionInteraction(this._object, new NotExistsMatcher()).execute();
return new MatcherAssertionInteraction(this._element, new NotExistsMatcher()).execute();
}
toHaveText(value) {
return new MatcherAssertionInteraction(this._object, new TextMatcher(value)).execute();
return new MatcherAssertionInteraction(this._element, new TextMatcher(value)).execute();
}
toHaveLabel(value) {
return new MatcherAssertionInteraction(this._object, new LabelMatcher(value)).execute();
return new MatcherAssertionInteraction(this._element, new LabelMatcher(value)).execute();
}
toHaveId(value) {
return new MatcherAssertionInteraction(this._object, new IdMatcher(value)).execute();
return new MatcherAssertionInteraction(this._element, new IdMatcher(value)).execute();
}
}

class WaitFor {}

class WaitForElement extends WaitFor {
constructor(element) {
super();
if (!element instanceof Element) throw new Error(`WaitForElement ctor argument must be a valid Element, got ${typeof element}`);
this._element = element;
}
toBeVisible() {
return new WaitForInteraction(this._element, new VisibleMatcher());
}
toExist() {
return new WaitForInteraction(this._element, new ExistsMatcher());
}
}

//// syntax

function expect(object) {
if (object instanceof Element) return new ExpectElement(object);
throw new Error(`expect() argument is invalid, got ${typeof object}`);
function expect(element) {
if (element instanceof Element) return new ExpectElement(element);
throw new Error(`expect() argument is invalid, got ${typeof element}`);
}

function waitFor(element) {
if (element instanceof Element) return new WaitForElement(element);
throw new Error(`waitFor() argument is invalid, got ${typeof element}`);
}

function element(matcher) {
Expand All @@ -267,12 +340,14 @@ const by = {
const exportGlobals = function () {
global.element = element;
global.expect = expect;
global.waitFor = waitFor;
global.by = by;
};

export {
exportGlobals,
expect,
waitFor,
element,
by
};
17 changes: 17 additions & 0 deletions detox/test/e2e/e-waitfor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
describe('WaitFor', function () {

beforeEach(function (done) {
simulator.reloadReactNativeApp(done);
});

beforeEach(function () {
element(by.label('WaitFor')).tap();
});

it('should find element by scrolling until it is visible', function () {
expect(element(by.label('Text5'))).toBeNotVisible();
waitFor(element(by.label('Text5'))).toBeVisible().whileElement(by.id('ScrollView630')).scroll(50, 'down');
expect(element(by.label('Text5'))).toBeVisible();
});

});
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions detox/test/index.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class example extends Component {
{this.renderScreenButton('Matchers', Screens.MatchersScreen)}
{this.renderScreenButton('Actions', Screens.ActionsScreen)}
{this.renderScreenButton('Assertions', Screens.AssertionsScreen)}
{this.renderScreenButton('WaitFor', Screens.WaitForScreen)}
{this.renderScreenButton('Stress', Screens.StressScreen)}
</View>
);
Expand Down
51 changes: 51 additions & 0 deletions detox/test/src/Screens/WaitForScreen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { Component } from 'react';
import {
Text,
View,
TouchableOpacity,
TextInput,
ScrollView
} from 'react-native';

export default class WaitForScreen extends Component {

constructor(props) {
super(props);
this.state = {
greeting: undefined
};
}

render() {
if (this.state.greeting) return this.renderAfterButton();
return (
<View style={{flex: 1, paddingTop: 40, justifyContent: 'flex-start'}}>

<View style={{height: 100, borderColor: '#c0c0c0', borderWidth: 1, backgroundColor: '#f8f8ff', marginBottom: 20}}>
<ScrollView testID='ScrollView630'>
<Text style={{height: 30, backgroundColor: '#e8e8f8', padding: 5, margin: 10}}>Text1</Text>
<Text style={{height: 30, backgroundColor: '#e8e8f8', padding: 5, margin: 10}}>Text2</Text>
<Text style={{height: 30, backgroundColor: '#e8e8f8', padding: 5, margin: 10}}>Text3</Text>
<Text style={{height: 30, backgroundColor: '#e8e8f8', padding: 5, margin: 10}}>Text4</Text>
<Text style={{height: 30, backgroundColor: '#e8e8f8', padding: 5, margin: 10}}>Text5</Text>
<Text style={{height: 30, backgroundColor: '#e8e8f8', padding: 5, margin: 10}}>Text6</Text>
<Text style={{height: 30, backgroundColor: '#e8e8f8', padding: 5, margin: 10}}>Text7</Text>
<Text style={{height: 30, backgroundColor: '#e8e8f8', padding: 5, margin: 10}}>Text8</Text>
</ScrollView>
</View>

</View>
);
}

renderAfterButton() {
return (
<View style={{flex: 1, paddingTop: 20, justifyContent: 'center', alignItems: 'center'}}>
<Text style={{fontSize: 25}}>
{this.state.greeting}!!!
</Text>
</View>
);
}

}
2 changes: 2 additions & 0 deletions detox/test/src/Screens/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import SanityScreen from './SanityScreen';
import MatchersScreen from './MatchersScreen';
import ActionsScreen from './ActionsScreen';
import AssertionsScreen from './AssertionsScreen';
import WaitForScreen from './WaitForScreen';
import StressScreen from './StressScreen';

export {
SanityScreen,
MatchersScreen,
ActionsScreen,
AssertionsScreen,
WaitForScreen,
StressScreen
};

0 comments on commit cfc9587

Please sign in to comment.