diff --git a/detox/ios/Detox.xcodeproj/project.pbxproj b/detox/ios/Detox.xcodeproj/project.pbxproj index 9595c66ba5..d2bcdbf1aa 100644 --- a/detox/ios/Detox.xcodeproj/project.pbxproj +++ b/detox/ios/Detox.xcodeproj/project.pbxproj @@ -72,6 +72,8 @@ 39CEFCDB1E34E91B00A09124 /* DetoxUserNotificationDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CEFCDA1E34E91B00A09124 /* DetoxUserNotificationDispatcher.swift */; }; 468731A51E6C6D0500F151BE /* EarlGrey+Detox.h in Headers */ = {isa = PBXBuildFile; fileRef = 468731A31E6C6D0500F151BE /* EarlGrey+Detox.h */; }; 468731A61E6C6D0500F151BE /* EarlGrey+Detox.m in Sources */ = {isa = PBXBuildFile; fileRef = 468731A41E6C6D0500F151BE /* EarlGrey+Detox.m */; }; + A7F76A151ED33DE500FFE77E /* WXAnimatedDisplayLinkIdlingResource.h in Headers */ = {isa = PBXBuildFile; fileRef = A7F76A131ED33DE500FFE77E /* WXAnimatedDisplayLinkIdlingResource.h */; }; + A7F76A161ED33DE500FFE77E /* WXAnimatedDisplayLinkIdlingResource.m in Sources */ = {isa = PBXBuildFile; fileRef = A7F76A141ED33DE500FFE77E /* WXAnimatedDisplayLinkIdlingResource.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -217,6 +219,8 @@ 39CEFCDA1E34E91B00A09124 /* DetoxUserNotificationDispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetoxUserNotificationDispatcher.swift; sourceTree = ""; }; 468731A31E6C6D0500F151BE /* EarlGrey+Detox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "EarlGrey+Detox.h"; sourceTree = ""; }; 468731A41E6C6D0500F151BE /* EarlGrey+Detox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "EarlGrey+Detox.m"; sourceTree = ""; }; + A7F76A131ED33DE500FFE77E /* WXAnimatedDisplayLinkIdlingResource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WXAnimatedDisplayLinkIdlingResource.h; sourceTree = ""; }; + A7F76A141ED33DE500FFE77E /* WXAnimatedDisplayLinkIdlingResource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WXAnimatedDisplayLinkIdlingResource.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -353,6 +357,8 @@ 394767CA1DBF98D900D72256 /* WXJSTimerObservationIdlingResource.m */, 394767CB1DBF98D900D72256 /* WXRunLoopIdlingResource.h */, 394767CC1DBF98D900D72256 /* WXRunLoopIdlingResource.m */, + A7F76A131ED33DE500FFE77E /* WXAnimatedDisplayLinkIdlingResource.h */, + A7F76A141ED33DE500FFE77E /* WXAnimatedDisplayLinkIdlingResource.m */, ); name = ReactNativeSupport; sourceTree = ""; @@ -400,6 +406,7 @@ 391FA5E91E7FD96D0056F82F /* GREYIdlingResourcePrettyPrint.h in Headers */, 39A34C711E30F10D00BEBB59 /* DetoxAppDelegateProxy.h in Headers */, 397EC9B51E7EDE0B00D5F2BB /* EarlGreyStatistics.h in Headers */, + A7F76A151ED33DE500FFE77E /* WXAnimatedDisplayLinkIdlingResource.h in Headers */, 394767AE1DBF987E00D72256 /* DetoxManager.h in Headers */, 468731A51E6C6D0500F151BE /* EarlGrey+Detox.h in Headers */, 394767CD1DBF98D900D72256 /* ReactNativeHeaders.h in Headers */, @@ -631,6 +638,7 @@ 394767B51DBF987E00D72256 /* TestRunner.m in Sources */, 394767D31DBF98D900D72256 /* WXJSTimerObservationIdlingResource.m in Sources */, 394767C01DBF98A700D72256 /* GREYCondition+Detox.m in Sources */, + A7F76A161ED33DE500FFE77E /* WXAnimatedDisplayLinkIdlingResource.m in Sources */, 394767D11DBF98D900D72256 /* WXJSDisplayLinkIdlingResource.m in Sources */, 39A34C721E30F10D00BEBB59 /* DetoxAppDelegateProxy.m in Sources */, 394767B11DBF987E00D72256 /* MethodInvocation.m in Sources */, diff --git a/detox/ios/Detox/ReactNativeHeaders.h b/detox/ios/Detox/ReactNativeHeaders.h index 37abfb8968..e47c0f014d 100644 --- a/detox/ios/Detox/ReactNativeHeaders.h +++ b/detox/ios/Detox/ReactNativeHeaders.h @@ -23,6 +23,8 @@ typedef void (^RN_RCTJavaScriptCallback)(id json, NSError *error); + (id)currentBridge; - (void)requestReload; - (id) uiManager; +- (id)moduleForName:(NSString *)moduleName; +- (id)moduleForClass:(Class)moduleClass; @property (nonatomic, readonly, getter=isLoading) BOOL loading; @property (nonatomic, readonly, getter=isValid) BOOL valid; diff --git a/detox/ios/Detox/ReactNativeSupport.m b/detox/ios/Detox/ReactNativeSupport.m index 57f54b37a7..f799c203be 100644 --- a/detox/ios/Detox/ReactNativeSupport.m +++ b/detox/ios/Detox/ReactNativeSupport.m @@ -14,6 +14,7 @@ #import "WXRunLoopIdlingResource.h" #import "WXJSDisplayLinkIdlingResource.h" #import "WXJSTimerObservationIdlingResource.h" +#import "WXAnimatedDisplayLinkIdlingResource.h" @import ObjectiveC; @import Darwin; @@ -84,8 +85,10 @@ void setupForTests() // m = class_getInstanceMethod(cls, NSSelectorFromString(@"addToRunLoop:")); // orig_addToRunLoop = (void(*)(id, SEL, NSRunLoop*))method_getImplementation(m); // method_setImplementation(m, (IMP)swz_addToRunLoop); - + [[GREYUIThreadExecutor sharedInstance] registerIdlingResource:[WXJSTimerObservationIdlingResource new]]; + + [[GREYUIThreadExecutor sharedInstance] registerIdlingResource:[WXAnimatedDisplayLinkIdlingResource new]]; } @implementation ReactNativeSupport diff --git a/detox/ios/Detox/WXAnimatedDisplayLinkIdlingResource.h b/detox/ios/Detox/WXAnimatedDisplayLinkIdlingResource.h new file mode 100644 index 0000000000..28c91e1b2a --- /dev/null +++ b/detox/ios/Detox/WXAnimatedDisplayLinkIdlingResource.h @@ -0,0 +1,14 @@ +// +// WXAnimatedDisplayLinkIdlingResource.h +// Detox +// +// Created by Sergey Ilyevsky on 22/05/2017. +// Copyright © 2017 Wix. All rights reserved. +// + +#import +#import + +@interface WXAnimatedDisplayLinkIdlingResource : NSObject + +@end diff --git a/detox/ios/Detox/WXAnimatedDisplayLinkIdlingResource.m b/detox/ios/Detox/WXAnimatedDisplayLinkIdlingResource.m new file mode 100644 index 0000000000..5fed87c1b1 --- /dev/null +++ b/detox/ios/Detox/WXAnimatedDisplayLinkIdlingResource.m @@ -0,0 +1,34 @@ +// +// WXAnimatedDisplayLinkIdlingResource.m +// Detox +// +// Created by Sergey Ilyevsky on 22/05/2017. +// Copyright © 2017 Wix. All rights reserved. +// + +#import "WXAnimatedDisplayLinkIdlingResource.h" +#import "ReactNativeHeaders.h" + +@implementation WXAnimatedDisplayLinkIdlingResource { + id _bridge; +} + +- (NSString *)idlingResourceName +{ + return NSStringFromClass([self class]); +} + +- (NSString *)idlingResourceDescription +{ + return @"Monitors CADisplayLink objects created by React Native Animated"; +} + +- (BOOL)isIdleNow +{ + id bridge = [NSClassFromString(@"RCTBridge") valueForKey:@"currentBridge"]; + id animatedModule = [bridge moduleForClass:NSClassFromString(@"RCTNativeAnimatedModule")]; + id displayLink = [animatedModule valueForKeyPath:@"_nodesManager._displayLink"]; + return displayLink == nil; +} + +@end diff --git a/detox/test/e2e/l-animations.js b/detox/test/e2e/l-animations.js new file mode 100644 index 0000000000..26308f40e7 --- /dev/null +++ b/detox/test/e2e/l-animations.js @@ -0,0 +1,58 @@ +let _ = require('lodash'); + +describe('Animations', () => { + beforeEach(async () => { + await device.reloadReactNative(); + await element(by.label('Animations')).tap(); + }); + + async function _startTest(driver, options = {}) { + let driverControlSegment = element(by.text(driver).withAncestor(by.id('UniqueId_AnimationsScreen_useNativeDriver'))); + await driverControlSegment.tap(); + + if(options.loops !== undefined) { + let loopSwitch = element(by.id('UniqueId_AnimationsScreen_enableLoop')); + await loopSwitch.tap(); + await expect(loopSwitch).toHaveValue('1'); + await element(by.id('UniqueId_AnimationsScreen_numberOfIterations')).replaceText(String(options.loops)); + } + + if(options.duration !== undefined) { + await element(by.id('UniqueId_AnimationsScreen_duration')).replaceText(String(options.duration)); + } + + if(options.delay !== undefined) { + await element(by.id('UniqueId_AnimationsScreen_delay')).replaceText(String(options.delay)); + } + + await element(by.id('UniqueId_AnimationsScreen_startButton')).tap(); + } + + _.forEach(['JS', 'Native'], (driver) => { + it(`should find element (driver: ${driver})`, async () => { + await _startTest(driver); + await expect(element(by.id('UniqueId_AnimationsScreen_afterAnimationText'))).toBeVisible(); + }); + + it(`should detect loops with final number of iterations (driver: ${driver})`, async () => { + await _startTest(driver, {loops: 4}); + await expect(element(by.id('UniqueId_AnimationsScreen_afterAnimationText'))).toBeVisible(); + }); + + it.skip(`should not wait for infinite animations (driver: ${driver})`, async() => { + await _startTest(driver, {loops: -1}); + await expect(element(by.id('UniqueId_AnimationsScreen_afterAnimationText'))).toBeVisible(); + }); + + it(`should not wait during delays longer than 1.5s (driver: ${driver})`, async () => { + await _startTest(driver, {delay: 1600}); + await expect(element(by.id('UniqueId_AnimationsScreen_afterAnimationText'))).toNotExist(); + }); + + it(`should wait during delays shorter than 1.5s (driver: ${driver})`, async () => { + await _startTest(driver, {delay: 500}); + await expect(element(by.id('UniqueId_AnimationsScreen_afterAnimationText'))).toExist(); + }); + + }); +}); \ No newline at end of file diff --git a/detox/test/index.ios.js b/detox/test/index.ios.js index 812eeb6b38..1c5e732f81 100644 --- a/detox/test/index.ios.js +++ b/detox/test/index.ios.js @@ -70,6 +70,7 @@ class example extends Component { {this.renderScreenButton('Switch Root', Screens.SwitchRootScreen)} {this.renderScreenButton('Timeouts', Screens.TimeoutsScreen)} {this.renderScreenButton('Orientation', Screens.Orientation)} + {this.renderScreenButton('Animations', Screens.AnimationsScreen)} ); } diff --git a/detox/test/package.json b/detox/test/package.json index 803a86f15b..b63254eaf2 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -9,13 +9,13 @@ "build": "detox build --configuration ios.sim.release" }, "dependencies": { - "lodash": "^4.14.1", "react": "16.0.0-alpha.6", "react-native": "0.44.0" }, "devDependencies": { "detox": "^5.0.0", - "mocha": "^3.2.0" + "mocha": "^3.2.0", + "lodash": "^4.14.1" }, "detox": { "specs": "e2e", diff --git a/detox/test/src/Screens/AnimationsScreen.js b/detox/test/src/Screens/AnimationsScreen.js new file mode 100644 index 0000000000..b627c6aa3b --- /dev/null +++ b/detox/test/src/Screens/AnimationsScreen.js @@ -0,0 +1,156 @@ +import React, { Component } from 'react'; +import { + Text, + View, + Animated, + Button, + SegmentedControlIOS, + Switch, + TextInput +} from 'react-native'; +import _ from 'lodash'; + + +class AnimationsComponent extends Component { + constructor(props) { + super(props); + + this._faidInValue = new Animated.Value(0); + + this.state = { + showAfterAnimationText: false + }; + } + + componentDidMount() { + let fadeInAnimation = Animated.timing(this._faidInValue, { + toValue: 1, + duration: this.props.duration, + delay: this.props.delay, + useNativeDriver: this.props.useNativeDriver + }); + var animation; + if(this.props.loops === undefined) { + animation = fadeInAnimation; + } else { + animation = Animated.loop( + Animated.sequence([ + fadeInAnimation, + Animated.timing(this._faidInValue, { + toValue: 0.5, + duration: 0, + useNativeDriver: this.props.useNativeDriver + }) + ]), + { + iterations: this.props.loops + } + ); + } + + animation.start(() => this.setState({ showAfterAnimationText: true })); + } + + render() { + return ( + + + Fading in text + + {(() => { + if (this.state.showAfterAnimationText) return ( + + After-animation-text + + ) + })()} + + ); + } +} + + +export default class AnimationsScreen extends Component { + constructor(props) { + super(props); + + this.state = { + useNativeDriver: undefined, + enableLoop: false, + numberOfIterations: -1, + duration: 400, + delay: 0, + testStarted: false + }; + } + + render() { + if (this.state.testStarted) { + return ( + + ); + } + + let numOfIterationsColor = this.state.enableLoop ? 'black' : 'grey'; + return ( + + + Driver: + this.setState({ useNativeDriver: value === 'Native' })} + /> + + + Loop: + this.setState({enableLoop: value})} + /> + Number of iterations: + this.setState({numberOfIterations: Number(value)})} + placeholder={String(this.state.numberOfIterations)} + /> + + + Duration: + this.setState({duration: Number(value)})} + placeholder={String(this.state.duration)} + /> + + + Delay: + this.setState({delay: Number(value)})} + placeholder={String(this.state.delay)} + /> + +