From fbc0367421b327bb9fb489842c7a8aa57f9e2326 Mon Sep 17 00:00:00 2001 From: Amit Davidi Date: Wed, 12 Dec 2018 14:06:19 +0200 Subject: [PATCH 1/4] Introduce kotlin and reimplement RN-timers idling resource --- detox/android/build.gradle | 4 + detox/android/detox/build.gradle | 17 +- .../ReactNativeTimersIdlingResource.java | 198 -------------- .../ReactNativeTimersIdlingResource.kt | 115 ++++++++ .../ReactNativeTimersIdlingResourceTest.kt | 254 ++++++++++++++++++ .../org.mockito.plugins.MockMaker | 1 + detox/test/android/app/build.gradle | 2 +- detox/test/android/build.gradle | 5 + 8 files changed, 392 insertions(+), 204 deletions(-) delete mode 100644 detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeTimersIdlingResource.java create mode 100644 detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeTimersIdlingResource.kt create mode 100644 detox/android/detox/src/test/java/com/wix/detox/espresso/ReactNativeTimersIdlingResourceTest.kt create mode 100644 detox/android/detox/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/detox/android/build.gradle b/detox/android/build.gradle index 61c374195e..69862de24d 100644 --- a/detox/android/build.gradle +++ b/detox/android/build.gradle @@ -1,10 +1,14 @@ buildscript { + ext.kotlinVersion = '1.3.0' + ext.detoxKotlinVerion = ext.kotlinVersion + repositories { jcenter() google() } dependencies { classpath 'com.android.tools.build:gradle:3.1.4' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" } } diff --git a/detox/android/detox/build.gradle b/detox/android/detox/build.gradle index 1e7358b86a..01bcc2f7b9 100644 --- a/detox/android/detox/build.gradle +++ b/detox/android/detox/build.gradle @@ -1,11 +1,14 @@ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' -def _ext = rootProject.ext; +def _ext = rootProject.ext -def _compileSdkVersion = _ext.has('compileSdkVersion') ? _ext.compileSdkVersion : 25; -def _buildToolsVersion = _ext.has('buildToolsVersion') ? _ext.buildToolsVersion : '27.0.3'; -def _minSdkVersion = _ext.has('minSdkVersion') ? _ext.minSdkVersion : 18; -def _targetSdkVersion = _ext.has('targetSdkVersion') ? _ext.targetSdkVersion : 25; +def _compileSdkVersion = _ext.has('compileSdkVersion') ? _ext.compileSdkVersion : 25 +def _buildToolsVersion = _ext.has('buildToolsVersion') ? _ext.buildToolsVersion : '27.0.3' +def _minSdkVersion = _ext.has('minSdkVersion') ? _ext.minSdkVersion : 18 +def _targetSdkVersion = _ext.has('targetSdkVersion') ? _ext.targetSdkVersion : 25 +def _kotlinVersion = _ext.has('detoxKotlinVersion') ? _ext.detoxKotlinVersion : '1.3.0' android { compileSdkVersion _compileSdkVersion @@ -64,6 +67,8 @@ android { } dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$_kotlinVersion" + minReactNative44Implementation 'com.squareup.okhttp3:okhttp:3.4.1' minReactNative44Implementation 'com.squareup.okhttp3:okhttp-ws:3.4.1' @@ -84,4 +89,6 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation 'org.assertj:assertj-core:3.8.0' testImplementation 'org.apache.commons:commons-io:1.3.2' + testImplementation 'com.nhaarman:mockito-kotlin:1.4.0' + testImplementation "com.facebook.react:react-native:+" } 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 deleted file mode 100644 index 013c99b469..0000000000 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeTimersIdlingResource.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.wix.detox.espresso; - -import android.support.annotation.NonNull; -import android.support.test.espresso.IdlingResource; -import android.util.Log; -import android.view.Choreographer; - -import org.joor.Reflect; -import org.joor.ReflectException; - -import java.util.PriorityQueue; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Created by simonracz on 23/05/2017. - */ - -/** - *

- * Espresso IdlingResource for React Native js timers. - *

- * - *

- * Hooks up to React Native internals to grab the timers queue from it. - *

- *

- * This resource is considered idle if the Timers priority queue is empty or - * the one scheduled the soonest is still too far in the future. - *

- */ -public class ReactNativeTimersIdlingResource implements IdlingResource, Choreographer.FrameCallback { - private static final String LOG_TAG = "Detox"; - - private final static String CLASS_TIMING = "com.facebook.react.modules.core.Timing"; - 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 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 paused = new AtomicBoolean(false); - - private static final long LOOK_AHEAD_MS = 1500; - - private ResourceCallback callback = null; - private Object reactContext = null; - - public ReactNativeTimersIdlingResource(@NonNull Object reactContext) { - this.reactContext = reactContext; - } - - @Override - public String getName() { - return ReactNativeTimersIdlingResource.class.getName(); - } - - @Override - public boolean isIdleNow() { - if (paused.get()) { - return true; - } - - Class timingClass; - try { - timingClass = Class.forName(CLASS_TIMING); - } catch (ClassNotFoundException e) { - Log.e(LOG_TAG, "Can't find Timing or Timing$Timer classes"); - if (callback != null) { - callback.onTransitionToIdle(); - } - return true; - } - - try { - // reactContext.hasActiveCatalystInstance() should be always true here - // if called right after onReactContextInitialized(...) - if (Reflect.on(reactContext).field(FIELD_CATALYST_INSTANCE).get() == null) { - Log.e(LOG_TAG, "No active CatalystInstance. Should never see this."); - return false; - } - - if (!(boolean)Reflect.on(reactContext).call(METHOD_HAS_NATIVE_MODULE, timingClass).get()) { - Log.e(LOG_TAG, "Can't find Timing NativeModule"); - if (callback != null) { - callback.onTransitionToIdle(); - } - return true; - } - - final Object timingModule = Reflect.on(reactContext).call(METHOD_GET_NATIVE_MODULE, timingClass).get(); - final Object timerLock = Reflect.on(timingModule).field(LOCK_TIMER).get(); - synchronized (timerLock) { - final PriorityQueue timers = Reflect.on(timingModule).field(FIELD_TIMERS).get(); - final Object nextTimer = findNextTimer(timers); - if (nextTimer == null) { - if (callback != null) { - callback.onTransitionToIdle(); - } - return true; - } - -// Log.i(LOG_TAG, "Num of Timers : " + timers.size()); - - if (isTimerOutsideBusyWindow(nextTimer)) { - if (callback != null) { - callback.onTransitionToIdle(); - } - return true; - } - } - - Choreographer.getInstance().postFrameCallback(this); - Log.i(LOG_TAG, "JS Timer is busy"); - return false; - } catch (ReflectException e) { - Log.e(LOG_TAG, "Can't set up RN timer listener", e.getCause()); - } - - if (callback != null) { - callback.onTransitionToIdle(); - } - return true; - } - - @Override - public void registerIdleTransitionCallback(ResourceCallback callback) { - this.callback = callback; - - Choreographer.getInstance().postFrameCallback(this); - } - - @Override - public void doFrame(long frameTimeNanos) { - isIdleNow(); - } - - public void pause() { - paused.set(true); - if (callback != null) { - callback.onTransitionToIdle(); - } - } - public void resume() { - paused.set(false); - } - - private Object findNextTimer(PriorityQueue timers) { - Object nextTimer = timers.peek(); - if (nextTimer == null) { - return null; - } - - final boolean isRepetitive = Reflect.on(nextTimer).field(TIMER_FIELD_REPETITIVE).get(); - if (!isRepetitive) { - return nextTimer; - } - - Object timer = null; - long targetTime = Long.MAX_VALUE; - for (Object aTimer : timers) { - final boolean timerIsRepetitive = Reflect.on(aTimer).field(TIMER_FIELD_REPETITIVE).get(); - final long timerTargetTime = Reflect.on(aTimer).field(TIMER_FIELD_TARGET_TIME).get(); - if (!timerIsRepetitive && timerTargetTime < targetTime) { - targetTime = timerTargetTime; - timer = aTimer; - } - } - return timer; - } - - 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(); - -// Log.i(LOG_TAG, "Next timer has duration of: " + intervalMS -// + "; due time is: " + targetTimeMS + ", current is: " + currentTimeMS); - - // 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/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeTimersIdlingResource.kt b/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeTimersIdlingResource.kt new file mode 100644 index 0000000000..a5988fc96c --- /dev/null +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeTimersIdlingResource.kt @@ -0,0 +1,115 @@ +package com.wix.detox.espresso + +import android.support.test.espresso.IdlingResource +import android.view.Choreographer +import com.facebook.react.bridge.ReactContext +import com.facebook.react.modules.core.Timing +import org.joor.Reflect +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean + +const val BUSY_WINDOW_THRESHOLD = 1500 + +class TimerReflected(timer: Any) { + private var reflected = Reflect.on(timer) + + val isRepeating: Boolean + get() = reflected.field("mRepeat").get() + + val interval: Int + get() = reflected.field("mInterval").get() + + val targetTime: Long + get() = reflected.field("mTargetTime").get() +} + +class TimingModuleReflected(reactContext: ReactContext) { + private var nativeModule = reactContext.getNativeModule(Timing::class.java) + + val timersQueue: PriorityQueue + get() = Reflect.on(nativeModule).field("mTimers").get() + + val timersLock: Object + get() = Reflect.on(nativeModule).field("mTimerGuard").get() + + operator fun component1() = timersQueue + operator fun component2() = timersLock +} + +class ReactNativeTimersIdlingResource @JvmOverloads constructor( + private val reactContext: ReactContext, + private val getChoreographer: () -> Choreographer = { Choreographer.getInstance() } + ) : IdlingResource, Choreographer.FrameCallback { + + private var callback: IdlingResource.ResourceCallback? = null + private var paused = AtomicBoolean(false) + + override fun getName(): String = this.javaClass.name + + override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { + this.callback = callback + getChoreographer().postFrameCallback(this) + } + + override fun isIdleNow(): Boolean { + if (paused.get()) { + return true + } + + return checkIdle().apply { + val result = this + if (result) { + callback?.onTransitionToIdle() + } else { + getChoreographer().postFrameCallback(this@ReactNativeTimersIdlingResource) + } + } + } + + override fun doFrame(frameTimeNanos: Long) { + callback?.let { + isIdleNow + } + } + + public fun pause() { + paused.set(true) + callback?.onTransitionToIdle() + } + + public fun resume() { + paused.set(false) + } + + private fun checkIdle(): Boolean { + val now = System.nanoTime() / 1000000L + val (timersQueue, timersLock) = TimingModuleReflected(reactContext) + + synchronized(timersLock) { + val nextTimer = timersQueue.peek() + nextTimer?.let { + return !isTimerInBusyWindow(it, now) && !hasBusyTimers(timersQueue, now) + } + return true + } + } + + private fun isTimerInBusyWindow(timer: Any, now: Long): Boolean { + val timerReflected = TimerReflected(timer) + return when { + timerReflected.isRepeating -> false + timerReflected.targetTime < now -> false + timerReflected.interval > BUSY_WINDOW_THRESHOLD -> false + else -> true + } + } + + private fun hasBusyTimers(timersQueue: PriorityQueue, now: Long): Boolean { + timersQueue.forEach { + if (isTimerInBusyWindow(it, now)) { + return true + } + } + return false + } +} diff --git a/detox/android/detox/src/test/java/com/wix/detox/espresso/ReactNativeTimersIdlingResourceTest.kt b/detox/android/detox/src/test/java/com/wix/detox/espresso/ReactNativeTimersIdlingResourceTest.kt new file mode 100644 index 0000000000..bb9a1f7ba2 --- /dev/null +++ b/detox/android/detox/src/test/java/com/wix/detox/espresso/ReactNativeTimersIdlingResourceTest.kt @@ -0,0 +1,254 @@ +package com.wix.detox.espresso + +import android.support.test.espresso.IdlingResource.ResourceCallback +import android.view.Choreographer +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.modules.core.Timing +import com.nhaarman.mockito_kotlin.* +import org.assertj.core.api.Assertions.assertThat +import org.joor.Reflect +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers +import java.util.* +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +const val BUSY_INTERVAL_MS = 1500 +const val MEANINGFUL_TIMER_INTERVAL = BUSY_INTERVAL_MS + +fun now() = System.nanoTime() / 1000000L + +fun aTimer(interval: Int, isRepeating: Boolean) = aTimer(now() + interval + 10, interval, isRepeating) + +fun aTimer(targetTime: Long, interval: Int, isRepeating: Boolean): Any { + val timerClass = Class.forName("com.facebook.react.modules.core.Timing\$Timer") + return Reflect.on(timerClass).create(-1, targetTime, interval, isRepeating).get() +} + +fun aOneShotTimer(interval: Int) = aTimer(interval, false) +fun aRepeatingTimer(interval: Int) = aTimer(interval, true) +fun anOverdueTimer() = aTimer(now() - 100, 123, false) + +fun anIdlingResourceCallback() = mock() + +class ReactNativeTimersIdlingResourceTest { + + private lateinit var reactAppContext: ReactApplicationContext + private lateinit var timersLock: String + private lateinit var timersNativeModule: Timing + private lateinit var choreographer: Choreographer + private lateinit var pendingTimers: PriorityQueue + + @Before fun setUp() { + pendingTimers = PriorityQueue(2) { _, _ -> 0} + + timersNativeModule = mock() + timersLock = "Lock-Mock" + Reflect.on(timersNativeModule).set("mTimers", pendingTimers) + Reflect.on(timersNativeModule).set("mTimerGuard", timersLock) + + choreographer = mock() + + reactAppContext = mock { + on { hasNativeModule(ArgumentMatchers.any()) }.doReturn(true) + on { getNativeModule(ArgumentMatchers.any()) }.doReturn(timersNativeModule) + } + } + + @Test fun `should be idle if there are no timers in queue`() { + assertThat(uut().isIdleNow).isTrue() + } + + @Test fun `should transition to idle if found idle in query`() { + val callback = anIdlingResourceCallback() + + with(uut()) { + registerIdleTransitionCallback(callback) + isIdleNow + } + + verify(callback).onTransitionToIdle() + } + + @Test fun `should NOT transition to idle if found busy in query`() { + val callback = anIdlingResourceCallback() + + givenTimer(aOneShotTimer(MEANINGFUL_TIMER_INTERVAL)) + + with(uut()) { + registerIdleTransitionCallback(callback) + isIdleNow + } + + verify(callback, never()).onTransitionToIdle() + } + + @Test fun `should be busy if there's a meaningful pending timer`() { + givenTimer(aOneShotTimer(MEANINGFUL_TIMER_INTERVAL)) + assertThat(uut().isIdleNow).isFalse() + } + + @Test fun `should be idle if pending timer is too far away (ie not meaningful)`() { + givenTimer(aOneShotTimer(BUSY_INTERVAL_MS + 1)) + assertThat(uut().isIdleNow).isTrue() + } + + @Test fun `should be idle if the only timer is a repeating one`() { + givenTimer(aRepeatingTimer(MEANINGFUL_TIMER_INTERVAL)) + assertThat(uut().isIdleNow).isTrue() + } + + @Test fun `should be busy if a meaningful pending timer lies beyond a repeating one`() { + givenTimer(aRepeatingTimer(BUSY_INTERVAL_MS / 10)) + givenTimer(aOneShotTimer(BUSY_INTERVAL_MS)) + assertThat(uut().isIdleNow).isFalse() + } + + @Test fun `should be idle if the only timer is overdue (due in the past)`() { + givenTimer(anOverdueTimer()) + assertThat(uut().isIdleNow).isTrue() + } + + @Test fun `should be busy if has a meaningful pending timer set beyond an overdue timer`() { + givenTimer(anOverdueTimer()) + givenTimer(aOneShotTimer(MEANINGFUL_TIMER_INTERVAL)) + assertThat(uut().isIdleNow).isFalse() + } + + @Test fun `should be idle if paused`() { + givenTimer(aOneShotTimer(MEANINGFUL_TIMER_INTERVAL)) + + val uut = uut().apply { + pause() + } + + assertThat(uut.isIdleNow).isTrue() + } + + @Test fun `should be busy if paused and resumed`() { + givenTimer(aOneShotTimer(MEANINGFUL_TIMER_INTERVAL)) + + val uut = uut().apply { + pause() + resume() + } + + assertThat(uut.isIdleNow).isFalse() + } + + @Test fun `should notify of transition to idle upon pausing`() { + val callback = anIdlingResourceCallback() + + givenTimer(aOneShotTimer(MEANINGFUL_TIMER_INTERVAL)) + + with(uut()) { + registerIdleTransitionCallback(callback) + pause() + } + + verify(callback).onTransitionToIdle() + } + + @Test fun `should enqueue an is-idle check using choreographer when a callback gets registered`() { + with(uut()) { + registerIdleTransitionCallback(mock()) + } + + verify(choreographer).postFrameCallback(any()) + } + + @Test fun `should transition to idle when preregistered choreographer is dispatched`() { + val callback = anIdlingResourceCallback() + + uut().registerIdleTransitionCallback(callback) + invokeChoreographerCallback() + + verify(callback).onTransitionToIdle() + } + + @Test fun `should NOT transition to idle if not idle when preregistered choreographer is dispatched`() { + val callback = anIdlingResourceCallback() + + givenTimer(aOneShotTimer(MEANINGFUL_TIMER_INTERVAL)) + + uut().registerIdleTransitionCallback(callback) + invokeChoreographerCallback() + + verify(callback, never()).onTransitionToIdle() + } + + @Test fun `should re-register choreographer if found idle while preregistered choreographer is dispatched`() { + val callback = anIdlingResourceCallback() + + givenTimer(aOneShotTimer(MEANINGFUL_TIMER_INTERVAL)) + + val uut = uut() + uut.registerIdleTransitionCallback(callback) + invokeChoreographerCallback() + + verify(choreographer, times(2)).postFrameCallback(any()) + } + + @Test fun `should adhere to pausing also when invoked via choreographer callback`() { + val callback = anIdlingResourceCallback() + + givenTimer(aOneShotTimer(MEANINGFUL_TIMER_INTERVAL)) + + uut().apply { + pause() + registerIdleTransitionCallback(callback) + } + val runtimeChoreographerCallback = getChoreographerCallback() + + reset(callback, choreographer) + runtimeChoreographerCallback.doFrame(0L) + + verify(callback, never()).onTransitionToIdle() + verify(choreographer, never()).postFrameCallback(any()) + } + + @Test fun `should enqueue an additional idle check (using choreographer) if found busy`() { + givenTimer(aOneShotTimer(MEANINGFUL_TIMER_INTERVAL)) + uut().isIdleNow + verify(choreographer).postFrameCallback(any()) + } + + @Test fun `should NOT enqueue an additional idle check (using choreographer) if found idle`() { + givenTimer(aOneShotTimer(BUSY_INTERVAL_MS + 1)) + uut().isIdleNow + verify(choreographer, never()).postFrameCallback(any()) + } + + @Test fun `should yield to other threads using the timers module`() { + val executor = Executors.newSingleThreadExecutor() + var isIdle: Boolean? = null + + synchronized(timersLock) { + executor.submit { + isIdle = uut().isIdleNow + } + executor.awaitTermination(100L, TimeUnit.MILLISECONDS) + assertThat(isIdle).isNull() + } + executor.awaitTermination(100L, TimeUnit.MILLISECONDS) + assertThat(isIdle).isNotNull() + } + + private fun uut() = ReactNativeTimersIdlingResource(reactAppContext) { choreographer } + + private fun givenTimer(timer: Any) { + pendingTimers.add(timer) + } + + private fun invokeChoreographerCallback() { + getChoreographerCallback().doFrame(0L) + } + + private fun getChoreographerCallback(): Choreographer.FrameCallback { + argumentCaptor().apply { + verify(choreographer).postFrameCallback(capture()) + return firstValue + } + } +} diff --git a/detox/android/detox/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/detox/android/detox/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/detox/android/detox/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/detox/test/android/app/build.gradle b/detox/test/android/app/build.gradle index 6130f7ebb8..eff5dfc844 100644 --- a/detox/test/android/app/build.gradle +++ b/detox/test/android/app/build.gradle @@ -1,4 +1,4 @@ -apply plugin: "com.android.application" +apply plugin: 'com.android.application' apply from: "../../node_modules/react-native/react.gradle" diff --git a/detox/test/android/build.gradle b/detox/test/android/build.gradle index 4fc0a7a28b..77f09b1338 100644 --- a/detox/test/android/build.gradle +++ b/detox/test/android/build.gradle @@ -1,12 +1,17 @@ buildscript { + ext.kotlinVersion = '1.3.0' + ext.detoxKotlinVerion = ext.kotlinVersion + repositories { google() mavenLocal() jcenter() } + dependencies { classpath 'com.android.tools.build:gradle:3.1.4' classpath 'de.undercouch:gradle-download-task:3.4.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" } } From 1f2bc76095e4ab5c7e30ab76a9f97c019f205c1b Mon Sep 17 00:00:00 2001 From: Amit Davidi Date: Sun, 16 Dec 2018 10:01:00 +0200 Subject: [PATCH 2/4] Bump the major version; Update documentation --- detox/package.json | 2 +- docs/Introduction.Android.md | 76 ++++++++++++++++++++++------- docs/Introduction.GettingStarted.md | 4 ++ 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/detox/package.json b/detox/package.json index e410d01252..e6c031c354 100644 --- a/detox/package.json +++ b/detox/package.json @@ -1,7 +1,7 @@ { "name": "detox", "description": "E2E tests and automation for mobile", - "version": "9.1.2", + "version": "10.0.0", "bin": { "detox": "local-cli/detox.js" }, diff --git a/docs/Introduction.Android.md b/docs/Introduction.Android.md index fe761e2600..79bed73685 100644 --- a/docs/Introduction.Android.md +++ b/docs/Introduction.Android.md @@ -5,15 +5,26 @@ title: Detox for Android **Detox for Android currently has several compatibility issues with React Native >= 0.50.** See [#608](https://github.com/wix/detox/issues/608) for details. -## Setup -Detox 7 was updated to support Android gradle plugin 3.0.0. This is a breaking change that makes it impossible to support previous Android gradle plugin versions. +## Breaking Changes :warning: -https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html +* **In version 10, we've made [Kotlin](https://kotlinlang.org/) mandatory for integrating Detox into your Android project.** In the very least, you must include the Kotlin gradle plugin in your project, as we shall see later on. Nevertheless, this is a breaking change so bear that in mind when upgrading. In any case, worry not of the impact on your app, as - unless you effectively use Kotlin in your own native code, **there will be no impact on the final APK**, in terms of size and methods count. -For older Android gradle plugin support use `detox@6.x.x` instead ([previous setup guide here](https://github.com/wix/detox/blob/97654071573053def90e8207be8eba011408f977/docs/Introduction.Android.md)).
-**Detox 6 will not continue to be updated, to continue getting updates and features, update your Android gradle config and migrate to Detox 7.** +* **As of version 7** we require Android gradle plugin 3.0.0 or newer. This is a breaking change that makes it impossible to support previous Android gradle plugin versions. -### 1. Do the initial setup described in the Getting Started Guide + https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html + + For older Android gradle plugin support use `detox@6.x.x` instead ([previous setup guide here](https://github.com/wix/detox/blob/97654071573053def90e8207be8eba011408f977/docs/Introduction.Android.md)).
+ + + + + + **Note: As a rule of thumb, we consider all old major versions discontinued; We only support the latest Detox major version.** + + + +## Setup :gear: +### 1. Run through the initial _Getting Started_ Guide - [Getting Started](Introduction.GettingStarted.md) @@ -21,14 +32,14 @@ For older Android gradle plugin support use `detox@6.x.x` instead ([previous set In `android/settings.gradle` add: -```gradle +```groovy include ':detox' project(':detox').projectDir = new File(rootProject.projectDir, '../node_modules/detox/android/detox') ``` In `android/app/build.gradle` add this to `defaultConfig` section: -```gradle +```groovy defaultConfig { ... testBuildType System.getProperty('testBuildType', 'debug') //this will later be used to control the test apk build type @@ -49,34 +60,63 @@ Please be aware that the `minSdkVersion` needs to be at least 18. In `android/app/build.gradle` add this in `dependencies` section: -```gradle +```groovy dependencies { - ... + // ... androidTestImplementation(project(path: ":detox")) androidTestImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test:rules:1.0.1' - ... } ``` And in `android/build.gradle` you need to add this under `allprojects > repositories`: -```gradle +```groovy buildscript { repositories { - ... + // ... google() - ... } } ``` -### 3. Create Android Test class +### 3. Add Kotlin + +If your project does not already use Kotlin, add the Kotlin Gradle-plugin to your classpath in `android/build.gradle`: + +```groovy +buildscript { + // ... + ext.kotlinVersion = '1.3.0' + + dependencies: { + // ... + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + } +} +``` + +_Note: most guides advise for defining a global `kotlinVersion` constant - as in this example, but that is not mandatory._ + + + +**IMPORTANT:** Detox aims at a playing fair with your app, and so it allows you to explicitly define the kotlin version for it to use - so as to align it with your own; Please do so - in your root `android/build.gradle` configuration file: + +```groovy +buildscript { + ext.kotlinVersion = '1.3.0' // Your app's version + ext.detoxKotlinVersion = ext.kotlinVersion // Detox' version: should be 1.1.0 or higher! +} +``` + +***Note that Detox has been tested for version 1.1.0 of Kotlin, and higher!*** + +### 4. Create Android Test class You need to add the file `android/app/src/androidTest/java/com/[your.package]/DetoxTest.java` and fill it like [this](../detox/test/android/app/src/androidTest/java/com/example/DetoxTest.java), except that you need to change the package to your projects name. -### 4. Add Android configuration +### 5. Add Android configuration Add this part to your `package.json`: @@ -110,7 +150,7 @@ Following device types could be used to control Android devices: `android.attached`. Connect to already-attached android device. The device should be listed in the output of `adb devices` command under provided `name`. Use this type to connect to Genymotion emulator. -### 5. Run the tests +### 6. Run the tests Using the `android.emu.debug` configuration from above, you can invoke it in the standard way. @@ -131,7 +171,7 @@ Execution failed for task ':app:transformResourcesWithMergeJavaResForDebug'. You need to add this to the `android` section of your `android/app/build.gradle`: -```gradle +```groovy packagingOptions { exclude 'META-INF/LICENSE' } diff --git a/docs/Introduction.GettingStarted.md b/docs/Introduction.GettingStarted.md index 0261bb8539..27dc7fd53a 100644 --- a/docs/Introduction.GettingStarted.md +++ b/docs/Introduction.GettingStarted.md @@ -3,6 +3,10 @@ id: Introduction.GettingStarted title: Getting Started --- +**This guide is focused on iOS. For installing Detox for Android, be sure to also go over the [Android guide](Introduction.Android.md)**. + + + This is a step-by-step guide for adding Detox to your React Native project. > TIP: You can also check out this [awesome tutorial](https://medium.com/@bogomolnyelad/how-to-test-your-react-native-app-like-a-real-user-ecfc72e9b6bc) on Medium with video by [@bogomolnyelad](https://medium.com/@bogomolnyelad) From f1fc01d7d0a7671f0a57ad007e495fe743125aa3 Mon Sep 17 00:00:00 2001 From: Amit Davidi Date: Thu, 10 Jan 2019 11:34:14 +0200 Subject: [PATCH 3/4] Removed unneeded spacing in markdowns --- docs/Introduction.Android.md | 9 +-------- docs/Introduction.GettingStarted.md | 2 -- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/docs/Introduction.Android.md b/docs/Introduction.Android.md index 79bed73685..7dbcf90c50 100644 --- a/docs/Introduction.Android.md +++ b/docs/Introduction.Android.md @@ -15,14 +15,8 @@ title: Detox for Android For older Android gradle plugin support use `detox@6.x.x` instead ([previous setup guide here](https://github.com/wix/detox/blob/97654071573053def90e8207be8eba011408f977/docs/Introduction.Android.md)).
- - - - **Note: As a rule of thumb, we consider all old major versions discontinued; We only support the latest Detox major version.** - - ## Setup :gear: ### 1. Run through the initial _Getting Started_ Guide @@ -97,8 +91,7 @@ buildscript { } ``` -_Note: most guides advise for defining a global `kotlinVersion` constant - as in this example, but that is not mandatory._ - +_Note: most guides advise of defining a global `kotlinVersion` constant - as in this example, but that is not mandatory._ **IMPORTANT:** Detox aims at a playing fair with your app, and so it allows you to explicitly define the kotlin version for it to use - so as to align it with your own; Please do so - in your root `android/build.gradle` configuration file: diff --git a/docs/Introduction.GettingStarted.md b/docs/Introduction.GettingStarted.md index 27dc7fd53a..11e83394f6 100644 --- a/docs/Introduction.GettingStarted.md +++ b/docs/Introduction.GettingStarted.md @@ -5,8 +5,6 @@ title: Getting Started **This guide is focused on iOS. For installing Detox for Android, be sure to also go over the [Android guide](Introduction.Android.md)**. - - This is a step-by-step guide for adding Detox to your React Native project. > TIP: You can also check out this [awesome tutorial](https://medium.com/@bogomolnyelad/how-to-test-your-react-native-app-like-a-real-user-ecfc72e9b6bc) on Medium with video by [@bogomolnyelad](https://medium.com/@bogomolnyelad) From dc2052707784f09b01b403526530e1c849db2585 Mon Sep 17 00:00:00 2001 From: Amit Davidi Date: Sun, 13 Jan 2019 10:27:01 +0200 Subject: [PATCH 4/4] v10.0.0 --- CHANGELOG.md | 12 +++++++++++- detox/test/package.json | 4 ++-- examples/demo-native-android/package.json | 4 ++-- examples/demo-native-ios/package.json | 4 ++-- examples/demo-react-native-jest/package.json | 4 ++-- examples/demo-react-native/package.json | 4 ++-- lerna.json | 2 +- 7 files changed, 22 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d79b4384e..14cb625274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## [10.0.0]((https://github.com/wix/detox/tree/9.1.2)) (2019-01-13) +[Full Changelog](https://github.com/wix/detox/compare/9.1.2...10.0.0) + +**Enhancements** +- Android: Introduce Kotlin (v1.3.0 as the default version). +- Android: Rewrite of the JS-timers idling resource in Kotlin (`ReactNativeTimersIdlingResource`). + +**Fixed Bugs** +- Idle timer shadows busy timers at rare cases [\#1115](https://github.com/wix/Detox/issues/1115) + ## [9.1.2](https://github.com/wix/detox/tree/9.1.2) (2018-11-17) [Full Changelog](https://github.com/wix/detox/compare/9.0.7...9.1.2) @@ -1191,4 +1201,4 @@ -\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* \ No newline at end of file +\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* diff --git a/detox/test/package.json b/detox/test/package.json index 656aaf6ef6..d59bce391b 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -1,6 +1,6 @@ { "name": "detox-test", - "version": "9.1.2", + "version": "10.0.0", "private": true, "scripts": { "test": ":", @@ -18,7 +18,7 @@ "react-native": "0.56.0" }, "devDependencies": { - "detox": "^9.1.2", + "detox": "^10.0.0", "express": "^4.15.3", "jest": "^22.3.0", "lodash": "^4.14.1", diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json index 4fcb539b26..dfa54f7b9c 100644 --- a/examples/demo-native-android/package.json +++ b/examples/demo-native-android/package.json @@ -1,6 +1,6 @@ { "name": "detox-demo-native-android", - "version": "9.1.2", + "version": "10.0.0", "private": true, "scripts": { "packager": "react-native start", @@ -8,7 +8,7 @@ "e2e": "mocha e2e --opts ./e2e/mocha.opts" }, "devDependencies": { - "detox": "^9.1.2", + "detox": "^10.0.0", "mocha": "^4.0.0" }, "detox": {} diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json index 390ca15123..a197b7a427 100644 --- a/examples/demo-native-ios/package.json +++ b/examples/demo-native-ios/package.json @@ -1,9 +1,9 @@ { "name": "detox-demo-native-ios", - "version": "9.1.2", + "version": "10.0.0", "private": true, "devDependencies": { - "detox": "^9.1.2", + "detox": "^10.0.0", "mocha": "^4.0.0" }, "detox": { diff --git a/examples/demo-react-native-jest/package.json b/examples/demo-react-native-jest/package.json index 0960fd2d66..1f3825076b 100644 --- a/examples/demo-react-native-jest/package.json +++ b/examples/demo-react-native-jest/package.json @@ -1,6 +1,6 @@ { "name": "demo-react-native-jest", - "version": "9.1.2", + "version": "10.0.0", "private": true, "scripts": { "start": "node node_modules/react-native/local-cli/cli.js start", @@ -14,7 +14,7 @@ "devDependencies": { "babel-jest": "21.2.0", "babel-preset-react-native": "4.0.0", - "detox": "^9.1.2", + "detox": "^10.0.0", "jest": "21.2.1", "react-test-renderer": "16.0.0-beta.5" }, diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json index f3e4d1f061..dc98101a16 100644 --- a/examples/demo-react-native/package.json +++ b/examples/demo-react-native/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "9.1.2", + "version": "10.0.0", "private": true, "scripts": { "start": "react-native start" @@ -10,7 +10,7 @@ "react-native": "0.51.1" }, "devDependencies": { - "detox": "^9.1.2", + "detox": "^10.0.0", "mocha": "^4.0.1" }, "detox": { diff --git a/lerna.json b/lerna.json index 5ef50221e9..81359dd019 100644 --- a/lerna.json +++ b/lerna.json @@ -10,7 +10,7 @@ "examples/demo-react-native-jest", "generation" ], - "version": "9.1.2", + "version": "10.0.0", "npmClient": "npm", "command": { "publish": {