From 0ef1ef652a500942377b8800b094ec27d04a5743 Mon Sep 17 00:00:00 2001 From: Amit Davidi Date: Sun, 11 Nov 2018 16:14:46 +0200 Subject: [PATCH 1/2] Increase RN-timers idling resources look-ahead to 1.5sec --- .../com/wix/detox/ReactNativeSupport.java | 12 ++- .../wix/detox/espresso/DetoxViewActions.java | 46 ++++++++++++ .../ReactNativeTimersIdlingResource.java | 74 +++++++++++++------ detox/src/android/espressoapi/ViewActions.js | 5 +- 4 files changed, 111 insertions(+), 26 deletions(-) create mode 100644 detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxViewActions.java 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 24f3c63a48..5a482ad114 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 @@ -110,7 +110,6 @@ public void run() { * @param reactNativeHostHolder the object that has a getReactNativeHost() method */ static void waitForReactNativeLoad(@NonNull Context reactNativeHostHolder) { - if (!isReactNativeApp()) { return; } @@ -184,7 +183,6 @@ private static void setupEspressoIdlingResources(@NonNull ReactContext reactCont setupReactNativeQueueInterrogators(reactContext); - rnBridgeIdlingResource = new ReactBridgeIdlingResource(reactContext); rnTimerIdlingResource = new ReactNativeTimersIdlingResource(reactContext); rnUIModuleIdlingResource = new ReactNativeUIModuleIdlingResource(reactContext); @@ -303,4 +301,14 @@ private static void removeNetworkIdlingResource() { networkIR = null; } } + + public static void pauseRNTimersIdlingResource() { + if (rnTimerIdlingResource != null) { + rnTimerIdlingResource.pause(); + } + } + + public static void resumeRNTimersIdlingResource() { + rnTimerIdlingResource.resume(); + } } diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxViewActions.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxViewActions.java new file mode 100644 index 0000000000..d19f402443 --- /dev/null +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxViewActions.java @@ -0,0 +1,46 @@ +package com.wix.detox.espresso; + +import android.support.test.espresso.UiController; +import android.support.test.espresso.ViewAction; +import android.support.test.espresso.action.ViewActions; +import android.view.View; + +import com.wix.detox.ReactNativeSupport; + +import org.hamcrest.Matcher; + +/** + * An alternative to {@link ViewActions} - providing alternative implementations, where needed. + */ +public class DetoxViewActions { + private static class BusyViewActionWrapper implements ViewAction { + private final ViewAction espressoViewAction; + + BusyViewActionWrapper(ViewAction clickAction) { + this.espressoViewAction = clickAction; + } + + @Override + public Matcher getConstraints() { + return espressoViewAction.getConstraints(); + } + + @Override + public String getDescription() { + return espressoViewAction.getDescription(); + } + + @Override + public void perform(UiController uiController, View view) { + ReactNativeSupport.pauseRNTimersIdlingResource(); + espressoViewAction.perform(uiController, view); + ReactNativeSupport.resumeRNTimersIdlingResource(); + uiController.loopMainThreadUntilIdle(); + } + } + + public static ViewAction click() { + final ViewAction clickAction = ViewActions.click(); + return new BusyViewActionWrapper(clickAction); + } +} diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeTimersIdlingResource.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeTimersIdlingResource.java index 9c7035a354..ead257410a 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeTimersIdlingResource.java +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeTimersIdlingResource.java @@ -35,13 +35,15 @@ public class ReactNativeTimersIdlingResource implements IdlingResource, Choreogr private final static String METHOD_GET_NATIVE_MODULE = "getNativeModule"; private final static String METHOD_HAS_NATIVE_MODULE = "hasNativeModule"; private final static String FIELD_TIMERS = "mTimers"; - private final static String FIELD_TARGET_TIME = "mTargetTime"; + private final static String TIMER_FIELD_TARGET_TIME = "mTargetTime"; + private final static String TIMER_FIELD_INTERVAL = "mInterval"; + private final static String TIMER_FIELD_REPETITIVE = "mRepeat"; private final static String FIELD_CATALYST_INSTANCE = "mCatalystInstance"; private final static String LOCK_TIMER = "mTimerGuard"; - private AtomicBoolean stopped = new AtomicBoolean(false); + private AtomicBoolean paused = new AtomicBoolean(false); - private static final long LOOK_AHEAD_MS = 15; + private static final long LOOK_AHEAD_MS = 1500; private ResourceCallback callback = null; private Object reactContext = null; @@ -57,12 +59,10 @@ public String getName() { @Override public boolean isIdleNow() { - if (stopped.get()) { - if (callback != null) { - callback.onTransitionToIdle(); - } + if (paused.get()) { return true; } + Class timingClass; try { timingClass = Class.forName(CLASS_TIMING); @@ -93,28 +93,21 @@ public boolean isIdleNow() { Object timingModule = Reflect.on(reactContext).call(METHOD_GET_NATIVE_MODULE, timingClass).get(); Object timerLock = Reflect.on(timingModule).field(LOCK_TIMER).get(); synchronized (timerLock) { - PriorityQueue timers = Reflect.on(timingModule).field(FIELD_TIMERS).get(); - if (timers.isEmpty()) { + final PriorityQueue timers = Reflect.on(timingModule).field(FIELD_TIMERS).get(); + final Object nextTimer = timers.peek(); + if (nextTimer == null) { if (callback != null) { callback.onTransitionToIdle(); } return true; } - // Log.i(LOG_TAG, "Num of Timers : " + timers.size()); - - long targetTime = Reflect.on(timers.peek()).field(FIELD_TARGET_TIME).get(); - long currentTimeMS = System.nanoTime() / 1000000; - - // Log.i(LOG_TAG, "targetTime " + targetTime + " currentTime " + currentTimeMS); +// Log.i(LOG_TAG, "Num of Timers : " + timers.size()); - if (targetTime - currentTimeMS > LOOK_AHEAD_MS || targetTime < currentTimeMS) { - // Timer is too far in the future. Mark it as OK for now. - // This is similar to what Espresso does internally. + if (isTimerOutsideBusyWindow(nextTimer)) { if (callback != null) { callback.onTransitionToIdle(); } - // Log.i(LOG_TAG, "JS Timer is idle: true"); return true; } } @@ -144,7 +137,46 @@ public void doFrame(long frameTimeNanos) { isIdleNow(); } - public void stop() { - stopped.set(true); + public void pause() { + paused.set(true); + if (callback != null) { + callback.onTransitionToIdle(); + } + } + public void resume() { + paused.set(false); + } + + private boolean isTimerOutsideBusyWindow(Object nextTimer) { + final long currentTimeMS = System.nanoTime() / 1000000L; + final Reflect nextTimerReflected = Reflect.on(nextTimer); + final long targetTimeMS = nextTimerReflected.field(TIMER_FIELD_TARGET_TIME).get(); + final int intervalMS = nextTimerReflected.field(TIMER_FIELD_INTERVAL).get(); + final boolean isRepetitive = nextTimerReflected.field(TIMER_FIELD_REPETITIVE).get(); + +// Log.i(LOG_TAG, "Next timer has duration of: " + intervalMS +// + "; due time is: " + targetTimeMS + ", current is: " + currentTimeMS +// + "; is " + (isRepetitive ? "repeating" : "a one-shot")); + + // Before making any concrete checks, be sure to ignore repeating timers or we'd loop forever. + // TODO: Should we iterate to the first, non-repeating timer? + if (isRepetitive) { + return true; + } + + // Core condition is for the timer interval (duration) to be set beyond our window. + // Note: we check the interval in an 'absolute' way rather than comparing to the 'current time' + // since it always takes a while till we get dispatched (compared to when the timer was created), + // and that could make a significant difference in timers set close to our window (up to ~ LOOK_AHEAD_MS+200ms). + if (intervalMS > LOOK_AHEAD_MS) { + return true; + } + + // Edge case: timer has expired during this probing process and is yet to have left the queue. + if (targetTimeMS <= currentTimeMS) { + return true; + } + + return false; } } diff --git a/detox/src/android/espressoapi/ViewActions.js b/detox/src/android/espressoapi/ViewActions.js index f01f12c6d9..b9dc954d59 100644 --- a/detox/src/android/espressoapi/ViewActions.js +++ b/detox/src/android/espressoapi/ViewActions.js @@ -5,7 +5,6 @@ */ - class ViewActions { static clearGlobalAssertions() { return { @@ -33,7 +32,7 @@ class ViewActions { return { target: { type: "Class", - value: "android.support.test.espresso.action.ViewActions" + value: "com.wix.detox.espresso.DetoxViewActions" }, method: "click", args: [] @@ -238,4 +237,4 @@ class ViewActions { } -module.exports = ViewActions; \ No newline at end of file +module.exports = ViewActions; From 063022a9bcc9de154a393cc4e47d4fcddd56e97f Mon Sep 17 00:00:00 2001 From: Amit Davidi Date: Mon, 12 Nov 2018 13:57:19 +0200 Subject: [PATCH 2/2] 'Unlock' tests, previously marked ios-only --- detox/test/e2e/03.actions.test.js | 2 +- detox/test/e2e/09.stress-timeouts.test.js | 2 +- detox/test/e2e/12.animations.test.js | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/detox/test/e2e/03.actions.test.js b/detox/test/e2e/03.actions.test.js index e008bd4cf7..1148dda1ad 100644 --- a/detox/test/e2e/03.actions.test.js +++ b/detox/test/e2e/03.actions.test.js @@ -14,7 +14,7 @@ describe('Actions', () => { await expect(element(by.text('Long Press Working!!!'))).toBeVisible(); }); - it(':ios: should long press with duration on an element', async () => { + it('should long press with duration on an element', async () => { await element(by.text('Long Press Me 1.5s')).longPress(1500); await expect(element(by.text('Long Press With Duration Working!!!'))).toBeVisible(); }); diff --git a/detox/test/e2e/09.stress-timeouts.test.js b/detox/test/e2e/09.stress-timeouts.test.js index 1bdff2ef8b..af2c72a91d 100644 --- a/detox/test/e2e/09.stress-timeouts.test.js +++ b/detox/test/e2e/09.stress-timeouts.test.js @@ -4,7 +4,7 @@ describe('StressTimeouts', () => { await element(by.text('Timeouts')).tap(); }); - it(':ios: should handle a short timeout', async () => { + it('should handle a short timeout', async () => { await element(by.id('TimeoutShort')).tap(); await expect(element(by.text('Short Timeout Working!!!'))).toBeVisible(); }); diff --git a/detox/test/e2e/12.animations.test.js b/detox/test/e2e/12.animations.test.js index 9a5c46fcaf..327f628b4d 100644 --- a/detox/test/e2e/12.animations.test.js +++ b/detox/test/e2e/12.animations.test.js @@ -50,11 +50,11 @@ describe('Animations', () => { await _startTest(driver, {delay: 1600}); await expect(element(by.id('UniqueId_AnimationsScreen_afterAnimationText'))).toNotExist(); }); - - it(`:ios: should wait during delays shorter than 1.5s (driver: ${driver})`, async () => { + + 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(); }); - - }); + + }); });