diff --git a/.travis.yml b/.travis.yml index c6f3f0e963..3544c67c5a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ matrix: os: osx osx_image: xcode9.2 env: - - REACT_NATIVE_VERSION=0.49.3 + - REACT_NATIVE_VERSION=0.51.1 install: - ./scripts/install.ios.sh script: @@ -25,7 +25,7 @@ matrix: os: linux jdk: oraclejdk8 env: - - REACT_NATIVE_VERSION=0.49.3 + - REACT_NATIVE_VERSION=0.51.1 android: components: - build-tools-27.0.2 @@ -44,7 +44,7 @@ matrix: os: osx osx_image: xcode9 env: - - REACT_NATIVE_VERSION=0.49.3 + - REACT_NATIVE_VERSION=0.51.1 install: - ./scripts/install.ios.sh script: diff --git a/detox/android/detox/src/main/java/com/wix/detox/DetoxManager.java b/detox/android/detox/src/main/java/com/wix/detox/DetoxManager.java index abfa2638b0..8a460f0883 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/DetoxManager.java +++ b/detox/android/detox/src/main/java/com/wix/detox/DetoxManager.java @@ -67,7 +67,7 @@ class DetoxManager implements WebSocketClient.ActionHandler { void start() { if (detoxServerUrl != null && detoxSessionId != null) { if (ReactNativeSupport.isReactNativeApp()) { - ReactNativeSupport.waitForReactNativeLoad(reactNativeHostHolder); + ReactNativeCompat.waitForReactNativeLoad(reactNativeHostHolder); } wsClient = new WebSocketClient(this); @@ -124,7 +124,6 @@ public void run() { } break; case "isReady": - // It's always ready, because reload, waitForRn are both synchronous. wsClient.sendAction("ready", Collections.emptyMap(), messageId); break; case "cleanup": diff --git a/detox/android/detox/src/main/java/com/wix/detox/ReactNativeCompat.java b/detox/android/detox/src/main/java/com/wix/detox/ReactNativeCompat.java new file mode 100644 index 0000000000..790681c491 --- /dev/null +++ b/detox/android/detox/src/main/java/com/wix/detox/ReactNativeCompat.java @@ -0,0 +1,42 @@ +package com.wix.detox; + +import org.joor.Reflect; + +import java.util.HashMap; +import java.util.Map; + +public class ReactNativeCompat { + static Map VERSION; + + static { + try { + Class reactNativeVersion = Class.forName("com.facebook.react.modules.systeminfo.ReactNativeVersion"); + VERSION = Reflect.on(reactNativeVersion).field("VERSION").get(); + } catch (ClassNotFoundException e) { + //ReactNativeVersion was introduced in RN50, default to latest previous version. + VERSION = new HashMap<>(); + VERSION.put("major", 0); + VERSION.put("minor", 49); + VERSION.put("patch", 0); + } + + } + + public static int getMinor() { + return (Integer) VERSION.get("minor"); + } + + public static void waitForReactNativeLoad(Object reactNativeHostHolder) { + if (getMinor() >= 50) { + ReactNativeSupport.waitForReactNativeLoad(reactNativeHostHolder); + try { + //TODO- Temp hack to make Detox usable for RN>=50 till we find a better sync solution. + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } else { + ReactNativeSupport.waitForReactNativeLoad(reactNativeHostHolder); + } + } +} diff --git a/detox/android/detox/src/main/java/com/wix/detox/ReactNativeSupport.java b/detox/android/detox/src/main/java/com/wix/detox/ReactNativeSupport.java index b0f98a50aa..a2aa18cd70 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/ReactNativeSupport.java +++ b/detox/android/detox/src/main/java/com/wix/detox/ReactNativeSupport.java @@ -138,7 +138,7 @@ public void run() { } }); - waitForReactNativeLoad(reactNativeHostHolder); + ReactNativeCompat.waitForReactNativeLoad(reactNativeHostHolder); } // Ideally we would not store this at all. diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/AnimatedModuleIdlingResource.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/AnimatedModuleIdlingResource.java index b35076e340..4a868db134 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/AnimatedModuleIdlingResource.java +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/AnimatedModuleIdlingResource.java @@ -5,11 +5,11 @@ import android.util.Log; import android.view.Choreographer; +import com.wix.detox.ReactNativeCompat; + import org.joor.Reflect; import org.joor.ReflectException; -import java.sql.Ref; - /** * Created by simonracz on 25/08/2017. */ @@ -18,13 +18,14 @@ *

* Espresso IdlingResource for React Native's Animated Module. *

- * + *

*

* Hooks up to React Native internals to monitor the state of the animations. *

- * + *

* This Idling Resource is inherently tied to the UI Module IR. It must be registered after * the UI Module IR. This order is not enforced now. + * * @see AnimatedModule */ public class AnimatedModuleIdlingResource implements IdlingResource, Choreographer.FrameCallback { @@ -82,7 +83,7 @@ public boolean isIdleNow() { return false; } - if (!(boolean)Reflect.on(reactContext).call(METHOD_HAS_NATIVE_MODULE, animModuleClass).get()) { + if (!(boolean) Reflect.on(reactContext).call(METHOD_HAS_NATIVE_MODULE, animModuleClass).get()) { Log.e(LOG_TAG, "Can't find Animated Module."); if (callback != null) { callback.onTransitionToIdle(); @@ -90,83 +91,94 @@ public boolean isIdleNow() { return true; } - Object animModule = Reflect.on(reactContext) - .call(METHOD_GET_NATIVE_MODULE, animModuleClass) - .get(); - Object operationsLock = Reflect.on(animModule) - .field(LOCK_OPERATIONS) - .get(); - boolean operationsAreEmpty; - boolean animationsConsideredIdle; - synchronized (operationsLock) { - Object operations = Reflect.on(animModule) - .field(FIELD_OPERATIONS).get(); - if (operations == null) { - operationsAreEmpty = true; - } else { - operationsAreEmpty = Reflect.on(operations).call(METHOD_IS_EMPTY).get(); + if (ReactNativeCompat.getMinor() >= 51) { + if(isIdleRN51(animModuleClass)) { + return true; } - } - Object nodesManager = Reflect.on(animModule) - .field(FIELD_NODES_MANAGER) - .get(); - - // We do this in this complicated way - // to not consider looped animations - // as a busy state. - int updatedNodesSize = Reflect.on(nodesManager) - .field(FIELD_UPDATED_NODES) - .call(METHOD_SIZE) - .get(); - if (updatedNodesSize > 0) { - animationsConsideredIdle = false; } else { - Object activeAnims = Reflect.on(nodesManager) - .field(FIELD_ACTIVE_ANIMATIONS) - .get(); - int activeAnimsSize = Reflect.on(activeAnims) - .call(METHOD_SIZE) - .get(); - if (activeAnimsSize == 0) { - animationsConsideredIdle = true; - } else { - animationsConsideredIdle = true; - for (int i = 0; i < activeAnimsSize; ++i) { - int iterations = Reflect.on(activeAnims) - .call(METHOD_VALUE_AT, i) - .field(FIELD_ITERATIONS) - .get(); - // -1 means it is looped - if (iterations != -1) { - animationsConsideredIdle = false; - break; - } - } + if (isIdleRNOld(animModuleClass)) { + return true; } } - if (operationsAreEmpty && animationsConsideredIdle) { - if (callback != null) { - callback.onTransitionToIdle(); - } - // Log.i(LOG_TAG, "AnimatedModule is idle."); - return true; - } - Log.i(LOG_TAG, "AnimatedModule is busy."); Choreographer.getInstance().postFrameCallback(this); return false; } catch (ReflectException e) { - // Log.e(LOG_TAG, "Couldn't set up RN AnimatedModule listener, old RN version?"); - // Log.e(LOG_TAG, "Can't set up RN AnimatedModule listener", e.getCause()); + Log.e(LOG_TAG, "Couldn't set up RN AnimatedModule listener, old RN version?"); + Log.e(LOG_TAG, "Can't set up RN AnimatedModule listener", e.getCause()); } if (callback != null) { callback.onTransitionToIdle(); } +// Log.i(LOG_TAG, "AnimatedModule is idle."); return true; } + private boolean isIdleRN51(Object animModuleClass) { + Object animModule = Reflect.on(reactContext).call(METHOD_GET_NATIVE_MODULE, animModuleClass).get(); + Object nodesManager = Reflect.on(animModule).call("getNodesManager").get(); + boolean hasActiveAnimations = Reflect.on(nodesManager).call("hasActiveAnimations").get(); + if (!hasActiveAnimations) { + if (callback != null) { + callback.onTransitionToIdle(); + } +// Log.i(LOG_TAG, "AnimatedModule is idle, no operations"); + return true; + } + return false; + } + + private boolean isIdleRNOld(Object animModuleClass) { + Object animModule = Reflect.on(reactContext).call(METHOD_GET_NATIVE_MODULE, animModuleClass).get(); + Object operationsLock = Reflect.on(animModule).field(LOCK_OPERATIONS).get(); + boolean operationsAreEmpty; + boolean animationsConsideredIdle; + synchronized (operationsLock) { + Object operations = Reflect.on(animModule).field(FIELD_OPERATIONS).get(); + if (operations == null) { + operationsAreEmpty = true; + } else { + operationsAreEmpty = Reflect.on(operations).call(METHOD_IS_EMPTY).get(); + } + } + Object nodesManager = Reflect.on(animModule).field(FIELD_NODES_MANAGER).get(); + + // We do this in this complicated way + // to not consider looped animations + // as a busy state. + int updatedNodesSize = Reflect.on(nodesManager).field(FIELD_UPDATED_NODES).call(METHOD_SIZE).get(); + if (updatedNodesSize > 0) { + animationsConsideredIdle = false; + } else { + Object activeAnims = Reflect.on(nodesManager).field(FIELD_ACTIVE_ANIMATIONS).get(); + int activeAnimsSize = Reflect.on(activeAnims).call(METHOD_SIZE).get(); + if (activeAnimsSize == 0) { + animationsConsideredIdle = true; + } else { + animationsConsideredIdle = true; + for (int i = 0; i < activeAnimsSize; ++i) { + int iterations = Reflect.on(activeAnims).call(METHOD_VALUE_AT, i).field(FIELD_ITERATIONS).get(); + // -1 means it is looped + if (iterations != -1) { + animationsConsideredIdle = false; + break; + } + } + } + } + + if (operationsAreEmpty && animationsConsideredIdle) { + if (callback != null) { + callback.onTransitionToIdle(); + } +// Log.i(LOG_TAG, "AnimatedModule is idle."); + return true; + } + return false; + } + @Override public void registerIdleTransitionCallback(ResourceCallback callback) { this.callback = callback; diff --git a/detox/android/detox/src/minReactNative46/java/com/wix/detox/WebSocketClient.java b/detox/android/detox/src/minReactNative46/java/com/wix/detox/WebSocketClient.java index d7d3fecff6..565a8d19a8 100644 --- a/detox/android/detox/src/minReactNative46/java/com/wix/detox/WebSocketClient.java +++ b/detox/android/detox/src/minReactNative46/java/com/wix/detox/WebSocketClient.java @@ -36,7 +36,7 @@ public void onOpen(WebSocket webSocket, Response response) { @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { - Log.e(LOG_TAG, "Detox Error: ", t); +// Log.e(LOG_TAG, "Detox Error: ", t); //OKHttp won't recover from failure if it got ConnectException, // this is a workaround to make the websocket client try reconnecting when failed. diff --git a/detox/src/android/expect.js b/detox/src/android/expect.js index 0ace6dfaf4..7e5beec154 100644 --- a/detox/src/android/expect.js +++ b/detox/src/android/expect.js @@ -20,7 +20,6 @@ function setInvocationManager(im) { invocationManager = im; } -const ViewAssertions = 'android.support.test.espresso.assertion.ViewAssertions'; const DetoxMatcher = 'com.wix.detox.espresso.DetoxMatcher'; const DetoxAssertion = 'com.wix.detox.espresso.DetoxAssertion'; const EspressoDetox = 'com.wix.detox.espresso.EspressoDetox'; @@ -107,7 +106,6 @@ class SwipeAction extends Action { class Interaction { async execute() { - //if (!this._call) throw new Error(`Interaction.execute cannot find a valid _call, got ${typeof this._call}`); await invocationManager.execute(this._call); } } diff --git a/detox/src/client/Client.js b/detox/src/client/Client.js index 62d02e7020..426becbd12 100644 --- a/detox/src/client/Client.js +++ b/detox/src/client/Client.js @@ -1,6 +1,7 @@ const AsyncWebSocket = require('./AsyncWebSocket'); const actions = require('./actions/actions'); const argparse = require('../utils/argparse'); +const retry = require('../utils/retry'); class Client { constructor(config) { diff --git a/detox/test/package.json b/detox/test/package.json index 1bc0a504b0..a2cc9917b2 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -12,8 +12,8 @@ "build:android": "detox build --configuration android.emu.release" }, "dependencies": { - "react": "16.0.0-beta.5", - "react-native": "0.49.3" + "react": "16.0.0", + "react-native": "0.51.x" }, "devDependencies": { "detox": "^7.0.0", @@ -69,4 +69,4 @@ } } } -} +} \ No newline at end of file