From 47576b2f5e76637b534553dc75d22f133623199d Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Tue, 17 Dec 2024 16:08:12 +0200 Subject: [PATCH 01/14] Rename .java to .kt --- .../{DetoxBaseIdlingResource.java => DetoxIdlingResource.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/{DetoxBaseIdlingResource.java => DetoxIdlingResource.kt} (100%) diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/DetoxBaseIdlingResource.java b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/DetoxIdlingResource.kt similarity index 100% rename from detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/DetoxBaseIdlingResource.java rename to detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/DetoxIdlingResource.kt From 22ec098eb447d54b6000690cb4963f66c6ec3644 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Tue, 17 Dec 2024 16:08:12 +0200 Subject: [PATCH 02/14] First iteration of idling resources refactoring --- .../com/wix/detox/espresso/EspressoDetox.java | 4 +- .../detox/reactnative/ReactNativeExtension.kt | 1 + .../reactnative/ReactNativeIdlingResources.kt | 229 ------------------ .../AnimatedModuleIdlingResource.java | 215 ---------------- .../idlingresources/BridgeIdlingResource.java | 94 ------- .../idlingresources/DetoxIdlingResource.kt | 36 +-- .../NetworkIdlingResource.java | 134 ---------- .../ReactNativeIdlingResources.kt | 193 +++++++++++++++ .../AnimatedModuleIdlingResource.kt | 75 ++++++ .../bridge/BridgeIdlingResource.kt | 83 +++++++ .../looper/MQThreadsReflector.kt | 47 ++++ .../network/NetworkIdlingResource.kt | 112 +++++++++ .../NetworkingModuleReflected.kt | 2 +- .../AsyncStorageIdlingResource.kt | 65 ++--- .../{ => storage}/SerialExecutorReflected.kt | 2 +- .../DelegatedIdleInterrogationStrategy.kt | 23 -- .../timers/IdleInterrogationStrategy.kt | 16 -- .../timers/TimersIdlingResource.kt | 30 ++- .../uimodule/UIModuleIdlingResource.kt | 4 +- .../AsyncStorageIdlingResourceSpec.kt | 4 +- .../NetworkIdlingResourcesTest.kt | 6 +- .../SerialExecutorReflectedSpec.kt | 1 + 22 files changed, 599 insertions(+), 777 deletions(-) delete mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeIdlingResources.kt delete mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/AnimatedModuleIdlingResource.java delete mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/BridgeIdlingResource.java delete mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResource.java create mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt create mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt create mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/bridge/BridgeIdlingResource.kt create mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/looper/MQThreadsReflector.kt create mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkIdlingResource.kt rename detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/{ => network}/NetworkingModuleReflected.kt (94%) rename detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/{ => storage}/AsyncStorageIdlingResource.kt (69%) rename detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/{ => storage}/SerialExecutorReflected.kt (90%) delete mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/DelegatedIdleInterrogationStrategy.kt delete mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/IdleInterrogationStrategy.kt diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/EspressoDetox.java b/detox/android/detox/src/full/java/com/wix/detox/espresso/EspressoDetox.java index fd21977c08..89ae8c1976 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/EspressoDetox.java +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/EspressoDetox.java @@ -13,7 +13,7 @@ import com.facebook.react.ReactApplication; import com.wix.detox.common.UIThread; import com.wix.detox.reactnative.ReactNativeExtension; -import com.wix.detox.reactnative.idlingresources.NetworkIdlingResource; +import com.wix.detox.reactnative.idlingresources.network.NetworkIdlingResource; import org.hamcrest.Matcher; @@ -120,7 +120,7 @@ public static void setURLBlacklist(final ArrayList urls) { UIThread.postSync(new Runnable() { @Override public void run() { - NetworkIdlingResource.setURLBlacklist(urls); + NetworkIdlingResource.Companion.setURLBlacklist(urls); } }); } diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt index 1711eea37c..7b7281a365 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt @@ -8,6 +8,7 @@ import com.facebook.react.ReactApplication import com.facebook.react.ReactInstanceManager import com.facebook.react.bridge.ReactContext import com.wix.detox.LaunchArgs +import com.wix.detox.reactnative.idlingresources.ReactNativeIdlingResources private const val LOG_TAG = "DetoxRNExt" diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeIdlingResources.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeIdlingResources.kt deleted file mode 100644 index a6bb7d2884..0000000000 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeIdlingResources.kt +++ /dev/null @@ -1,229 +0,0 @@ -package com.wix.detox.reactnative - -import android.os.Looper -import android.util.Log -import androidx.test.espresso.Espresso -import androidx.test.espresso.IdlingRegistry -import androidx.test.espresso.base.IdlingResourceRegistry -import com.facebook.react.bridge.ReactContext -import com.wix.detox.LaunchArgs -import com.wix.detox.reactnative.idlingresources.* -import com.wix.detox.reactnative.idlingresources.timers.TimersIdlingResource -import com.wix.detox.reactnative.idlingresources.timers.getInterrogationStrategy -import com.wix.detox.reactnative.idlingresources.uimodule.UIModuleIdlingResource -import org.joor.Reflect -import org.joor.ReflectException - -private const val LOG_TAG = "DetoxRNIdleRes" - -private class MQThreadsReflector(private val reactContext: ReactContext) { - fun getQueue(queueName: String): MQThreadReflected? { - try { - val queue = Reflect.on(reactContext).field(queueName).get() as Any? - return MQThreadReflected(queue, queueName) - } catch (e: ReflectException) { - Log.e(LOG_TAG, "Could not find queue: $queueName", e) - } - return null - } -} - -private class MQThreadReflected(private val queue: Any?, private val queueName: String) { - fun getLooper(): Looper? { - try { - if (queue != null) { - return Reflect.on(queue).call(METHOD_GET_LOOPER).get() - } - } catch (e: ReflectException) { - Log.e(LOG_TAG, "Could not find looper for queue: $queueName", e) - } - return null - } - - companion object { - const val METHOD_GET_LOOPER = "getLooper" - } -} - -class ReactNativeIdlingResources constructor( - private val reactContext: ReactContext, - private var launchArgs: LaunchArgs, - internal var networkSyncEnabled: Boolean = true -) { - companion object { - private const val FIELD_NATIVE_MODULES_MSG_QUEUE = "mNativeModulesMessageQueueThread" - private const val FIELD_JS_MSG_QUEUE = "mJSMessageQueueThread" - } - - private var timersIdlingResource: TimersIdlingResource? = null - private var asyncStorageIdlingResource: AsyncStorageIdlingResource? = null - private var legacyAsyncStorageIdlingResource: AsyncStorageIdlingResource? = null - private var rnBridgeIdlingResource: BridgeIdlingResource? = null - private var uiModuleIdlingResource: UIModuleIdlingResource? = null - private var animIdlingResource: AnimatedModuleIdlingResource? = null - private var networkIdlingResource: NetworkIdlingResource? = null - - fun registerAll() { - Log.i(LOG_TAG, "Setting up Espresso Idling Resources for React Native") - unregisterAll() - - setupUrlBlacklist() - setupMQThreadsInterrogators() - syncIdlingResources() - setupCustomRNIdlingResources() - syncIdlingResources() - } - - fun unregisterAll() { - unregisterMQThreadsInterrogators() - unregisterCustomRNIdlingResources() - } - - fun setNetworkSynchronization(enable: Boolean) { - if (networkSyncEnabled == enable) { - return - } - - if (enable) { - setupNetworkIdlingResource() - } else { - removeNetworkIdlingResource() - } - networkSyncEnabled = enable - } - - fun pauseNetworkSynchronization() = networkIdlingResource?.pause() - fun resumeNetworkSynchronization() { - if (networkSyncEnabled) { - networkIdlingResource?.resume() - } - } - - fun pauseRNTimersIdlingResource() = timersIdlingResource?.pause() - fun resumeRNTimersIdlingResource() = timersIdlingResource?.resume() - fun pauseUIIdlingResource() = uiModuleIdlingResource?.pause() - fun resumeUIIdlingResource() = uiModuleIdlingResource?.resume() - - fun setBlacklistUrls(urlList: String) { - setIldingResourceBlacklist(urlList) - } - - private fun setIldingResourceBlacklist(urlList: String) { - val urlArray = toFormattedUrlArray(urlList) - NetworkIdlingResource.setURLBlacklist(urlArray) - } - - private fun setupMQThreadsInterrogators() { - if (IdlingRegistry.getInstance().loopers.isEmpty()) { - val mqThreadsReflector = MQThreadsReflector(reactContext) - val mqJS = mqThreadsReflector.getQueue(FIELD_JS_MSG_QUEUE)?.getLooper() - val mqNativeModules = - mqThreadsReflector.getQueue(FIELD_NATIVE_MODULES_MSG_QUEUE)?.getLooper() - - IdlingRegistry.getInstance().apply { - registerLooperAsIdlingResource(mqJS) - registerLooperAsIdlingResource(mqNativeModules) - } - } - } - - private fun setupUrlBlacklist() { - if (launchArgs.hasURLBlacklist()) { - val blacklistUrls = launchArgs.urlBlacklist - setIldingResourceBlacklist(blacklistUrls) - } - } - - private fun setupCustomRNIdlingResources() { - rnBridgeIdlingResource = BridgeIdlingResource(reactContext) - timersIdlingResource = TimersIdlingResource(getInterrogationStrategy(reactContext)!!) - uiModuleIdlingResource = UIModuleIdlingResource(reactContext) - animIdlingResource = AnimatedModuleIdlingResource(reactContext) - - IdlingRegistry.getInstance() - .register( - timersIdlingResource, - rnBridgeIdlingResource, - uiModuleIdlingResource, - animIdlingResource) - - if (networkSyncEnabled) { - setupNetworkIdlingResource() - } - setupAsyncStorageIdlingResource() - } - - private fun syncIdlingResources() { - IdlingRegistry.getInstance().apply { - val irr: IdlingResourceRegistry = - Reflect.on(Espresso::class.java).field("baseRegistry").get() - irr.sync(this.resources, this.loopers) - } - } - - private fun unregisterMQThreadsInterrogators() { - val idlingResourceInstance = IdlingRegistry.getInstance() - val loopersField = Reflect.on(idlingResourceInstance).field("loopers") - loopersField.get>().clear() - } - - private fun unregisterCustomRNIdlingResources() { - IdlingRegistry.getInstance() - .unregister( - timersIdlingResource, - rnBridgeIdlingResource, - uiModuleIdlingResource, - animIdlingResource - ) - rnBridgeIdlingResource?.onDetach() - - removeNetworkIdlingResource() - removeAsyncStorageIdlingResource() - } - - private fun setupAsyncStorageIdlingResource() { - asyncStorageIdlingResource = - AsyncStorageIdlingResource.createIfNeeded(reactContext, false)?.also { - IdlingRegistry.getInstance().register(it) - } - - legacyAsyncStorageIdlingResource = - AsyncStorageIdlingResource.createIfNeeded(reactContext, true)?.also { - IdlingRegistry.getInstance().register(it) - } - } - - private fun removeAsyncStorageIdlingResource() { - asyncStorageIdlingResource?.also { - IdlingRegistry.getInstance().unregister(it) - } - - legacyAsyncStorageIdlingResource?.also { - IdlingRegistry.getInstance().unregister(it) - } - } - - private fun setupNetworkIdlingResource() { - try { - networkIdlingResource = NetworkIdlingResource(reactContext) - IdlingRegistry.getInstance().register(networkIdlingResource) - } catch (e: ReflectException) { - Log.e(LOG_TAG, "Can't set up Networking Module listener", e) - } - } - - private fun removeNetworkIdlingResource() { - networkIdlingResource?.let { - it.pause() - IdlingRegistry.getInstance().unregister(it) - networkIdlingResource = null - } - } - - private fun toFormattedUrlArray(urlList: String): List { - var formattedUrls = urlList - formattedUrls = formattedUrls.replace(Regex("""[()"]"""), "") - formattedUrls = formattedUrls.trim() - return formattedUrls.split(',') - } -} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/AnimatedModuleIdlingResource.java b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/AnimatedModuleIdlingResource.java deleted file mode 100644 index 7381312973..0000000000 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/AnimatedModuleIdlingResource.java +++ /dev/null @@ -1,215 +0,0 @@ -package com.wix.detox.reactnative.idlingresources; - -import android.util.Log; -import android.view.Choreographer; - -import com.wix.detox.espresso.idlingresources.DescriptiveIdlingResource; -import com.wix.detox.reactnative.ReactNativeInfo; - -import org.joor.Reflect; -import org.joor.ReflectException; - -import java.util.HashMap; -import java.util.Map; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * Created by simonracz on 25/08/2017. - */ - -/** - *

- * 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 DescriptiveIdlingResource, Choreographer.FrameCallback { - private static final String LOG_TAG = "Detox"; - - private final static String CLASS_ANIMATED_MODULE = "com.facebook.react.animated.NativeAnimatedModule"; - private final static String METHOD_GET_NATIVE_MODULE = "getNativeModule"; - private final static String METHOD_HAS_NATIVE_MODULE = "hasNativeModule"; - private final static String METHOD_IS_EMPTY = "isEmpty"; - - private final static String LOCK_OPERATIONS = "mOperationsCopyLock"; - private final static String FIELD_OPERATIONS = "mReadyOperations"; - private final static String FIELD_NODES_MANAGER = "mNodesManager"; - - private final static String FIELD_ITERATIONS = "mIterations"; - private final static String FIELD_ACTIVE_ANIMATIONS = "mActiveAnimations"; - private final static String FIELD_UPDATED_NODES = "mUpdatedNodes"; - private final static String FIELD_CATALYST_INSTANCE = "mCatalystInstance"; - - private final static String METHOD_SIZE = "size"; - private final static String METHOD_VALUE_AT = "valueAt"; - - private final static String METHOD_HAS_ACTIVE_ANIMATIONS = "hasActiveAnimations"; - - private final static Map busyHint = new HashMap() {{ - put("reason", "Animations running on screen"); - }}; - - private ResourceCallback callback = null; - private Object reactContext = null; - - public AnimatedModuleIdlingResource(@NonNull Object reactContext) { - this.reactContext = reactContext; - } - - @Override - public String getName() { - return AnimatedModuleIdlingResource.class.getName(); - } - - @NonNull - @Override - public String getDebugName() { - return "ui"; - } - - @Nullable - @Override - public Map getBusyHint() { - return busyHint; - } - - @Override - public boolean isIdleNow() { - Class animModuleClass = null; - try { - animModuleClass = Class.forName(CLASS_ANIMATED_MODULE); - } catch (ClassNotFoundException e) { - Log.e(LOG_TAG, "Animated Module is not on classpath."); - 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, animModuleClass).get()) { - Log.e(LOG_TAG, "Can't find Animated Module."); - if (callback != null) { - callback.onTransitionToIdle(); - } - return true; - } - - if (ReactNativeInfo.rnVersion().getMinor() >= 51) { - if(isIdleRN51(animModuleClass)) { - return true; - } - } else { - if (isIdleRNOld(animModuleClass)) { - 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()); - } - - 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; - - Choreographer.getInstance().postFrameCallback(this); - } - - @Override - public void doFrame(long frameTimeNanos) { - isIdleNow(); - } -} - diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/BridgeIdlingResource.java b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/BridgeIdlingResource.java deleted file mode 100644 index 61bea96ed3..0000000000 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/BridgeIdlingResource.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.wix.detox.reactnative.idlingresources; - -import android.util.Log; - -import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener; -import com.facebook.react.bridge.ReactContext; - -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * Created by simonracz on 01/06/2017. - */ - -/** - *

- * IdlingResource for Espresso, which monitors the traffic of - * React Native's JS bridge. - *

- */ -public class BridgeIdlingResource extends DetoxBaseIdlingResource implements NotThreadSafeBridgeIdleDebugListener { - private static final String LOG_TAG = "Detox"; - private final ReactContext reactContext; - - private AtomicBoolean idleNow = new AtomicBoolean(true); - private ResourceCallback callback = null; - - public BridgeIdlingResource(ReactContext reactContext) { - this.reactContext = reactContext; - this.reactContext.getCatalystInstance().addBridgeIdleDebugListener(this); - } - - public void onDetach() { - this.reactContext.getCatalystInstance().removeBridgeIdleDebugListener(this); - } - - @Override - public String getName() { - return BridgeIdlingResource.class.getName(); - } - - @NonNull - @Override - public String getDebugName() { - return "bridge"; - } - - @Nullable - @Override - public Map getBusyHint() { - return null; - } - - @Override - protected boolean checkIdle() { - boolean ret = idleNow.get(); - if (!ret) { - Log.i(LOG_TAG, "JS Bridge is busy"); - } - return ret; - } - - @Override - public void registerIdleTransitionCallback(ResourceCallback callback) { - this.callback = callback; - } - - @Override - public void onTransitionToBridgeIdle() { - idleNow.set(true); - notifyIdle(); - } - - @Override - public void onTransitionToBridgeBusy() { - idleNow.set(false); - // Log.i(LOG_TAG, "JS Bridge transitions to busy."); - } - - @Override - public void onBridgeDestroyed() { - } - - @Override - protected void notifyIdle() { - // Log.i(LOG_TAG, "JS Bridge transitions to idle."); - if (callback != null) { - callback.onTransitionToIdle(); - } - } -} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/DetoxIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/DetoxIdlingResource.kt index 4e76f7530e..cbd1995631 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/DetoxIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/DetoxIdlingResource.kt @@ -1,29 +1,31 @@ -package com.wix.detox.reactnative.idlingresources; +package com.wix.detox.reactnative.idlingresources -import com.wix.detox.espresso.idlingresources.DescriptiveIdlingResource; +import com.wix.detox.espresso.idlingresources.DescriptiveIdlingResource +import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicBoolean; +abstract class DetoxIdlingResource : DescriptiveIdlingResource { + private var paused: AtomicBoolean = AtomicBoolean(false) -public abstract class DetoxBaseIdlingResource implements DescriptiveIdlingResource { - AtomicBoolean paused = new AtomicBoolean(false); - - public void pause() { - paused.set(true); - notifyIdle(); + fun pause() { + paused.set(true) + notifyIdle() } - public void resume() { - paused.set(false); + fun resume() { + paused.set(false) } - @Override - final public boolean isIdleNow() { + final override fun isIdleNow(): Boolean { if (paused.get()) { - return true; + return true } - return checkIdle(); + return checkIdle() + } + + open fun onUnregistered() { + // no-op } - protected abstract boolean checkIdle(); - protected abstract void notifyIdle(); + protected abstract fun checkIdle(): Boolean + protected abstract fun notifyIdle() } diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResource.java b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResource.java deleted file mode 100644 index 219010fa72..0000000000 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResource.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.wix.detox.reactnative.idlingresources; - -import android.util.Log; -import android.view.Choreographer; - -import com.facebook.react.bridge.ReactContext; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import okhttp3.Call; -import okhttp3.Dispatcher; - - -/** - * Created by simonracz on 09/10/2017. - * - * Idling Resource which monitors React Native's OkHttpClient. - *

- * Must call stop() on it, before removing it from Espresso. - */ -public class NetworkIdlingResource extends DetoxBaseIdlingResource implements Choreographer.FrameCallback { - - private static final String LOG_TAG = "Detox"; - - private ResourceCallback callback; - private Dispatcher dispatcher; - private final Set busyResources = new HashSet<>(); - - private static final ArrayList blacklist = new ArrayList<>(); - - /** - * Must be called on the UI thread. - * - * @param urls list of regexes of blacklisted urls - */ - public static void setURLBlacklist(List urls) { - blacklist.clear(); - if (urls == null) return; - - for (String url : urls) { - try { - blacklist.add(Pattern.compile(url)); - } catch (PatternSyntaxException e) { - Log.e(LOG_TAG, "Couldn't parse regular expression for Black list url: " + url, e); - } - } - } - - public NetworkIdlingResource(@NonNull ReactContext reactContext) { - this(new NetworkingModuleReflected(reactContext).getHttpClient().dispatcher()); - } - - public NetworkIdlingResource(@NonNull Dispatcher dispatcher) { - this.dispatcher = dispatcher; - } - - @Override - public String getName() { - return NetworkIdlingResource.class.getName(); - } - - @NonNull - @Override - public String getDebugName() { - return "network"; - } - - @Nullable - @Override - public synchronized Map getBusyHint() { - return new HashMap() {{ - put("urls", new ArrayList<>(busyResources)); - }}; - } - - @Override - public void registerIdleTransitionCallback(ResourceCallback callback) { - this.callback = callback; - Choreographer.getInstance().postFrameCallback(this); - } - - @Override - public void doFrame(long frameTimeNanos) { - isIdleNow(); - } - - @Override - protected synchronized boolean checkIdle() { - busyResources.clear(); - - List calls = dispatcher.runningCalls(); - for (Call call: calls) { - final String url = call.request().url().toString(); - - if (!isUrlBlacklisted(url)) { - busyResources.add(url); - } - } - - if (!busyResources.isEmpty()) { - Log.i(LOG_TAG, "Network is busy, with " + busyResources.size() + " in-flight calls"); - Choreographer.getInstance().postFrameCallback(this); - return false; - } - - notifyIdle(); - return true; - } - - @Override - protected void notifyIdle() { - if (callback != null) { - callback.onTransitionToIdle(); - } - } - - private boolean isUrlBlacklisted(String url) { - for (Pattern pattern: blacklist) { - if (pattern.matcher(url).matches()) { - return true; - } - } - return false; - } -} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt new file mode 100644 index 0000000000..0618076ef3 --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt @@ -0,0 +1,193 @@ +package com.wix.detox.reactnative.idlingresources + +import android.os.Looper +import android.util.Log +import androidx.test.espresso.Espresso +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.base.IdlingResourceRegistry +import com.facebook.react.bridge.ReactContext +import com.wix.detox.LaunchArgs +import com.wix.detox.reactnative.idlingresources.animations.AnimatedModuleIdlingResource +import com.wix.detox.reactnative.idlingresources.bridge.BridgeIdlingResource +import com.wix.detox.reactnative.idlingresources.looper.MQThreadsReflector +import com.wix.detox.reactnative.idlingresources.network.NetworkIdlingResource +import com.wix.detox.reactnative.idlingresources.storage.AsyncStorageIdlingResource +import com.wix.detox.reactnative.idlingresources.timers.TimersIdlingResource +import com.wix.detox.reactnative.idlingresources.uimodule.UIModuleIdlingResource +import org.joor.Reflect + + +private enum class IdlingResourcesName { + Timers, + AsyncStorage, + RNBridge, + UIModule, + Animations, + Network +} + +private enum class LooperName { + JS, + NativeModules +} + +class ReactNativeIdlingResources( + private val reactContext: ReactContext, + private var launchArgs: LaunchArgs, + internal var networkSyncEnabled: Boolean = true +) { + companion object { + const val LOG_TAG = "DetoxRNIdleRes" + } + + private val idlingResources = mutableMapOf() + private val loopers = mutableMapOf() + + fun registerAll() { + Log.i(LOG_TAG, "Setting up Espresso Idling Resources for React Native") + unregisterAll() + + setupUrlBlacklist() + setupMQThreadsInterrogators() + syncIdlingResources() + setupIdlingResources() + syncIdlingResources() + } + + fun unregisterAll() { + unregisterMQThreadsInterrogators() + unregisterIdlingResources() + } + + fun setNetworkSynchronization(enable: Boolean) { + if (networkSyncEnabled == enable) { + return + } + + if (enable) { + setupIdlingResource(IdlingResourcesName.Network) + } else { + removeIdlingResource(IdlingResourcesName.Network) + } + networkSyncEnabled = enable + } + + fun pauseNetworkSynchronization() = pauseIdlingResource(IdlingResourcesName.Network) + fun resumeNetworkSynchronization() { + if (networkSyncEnabled) { + resumeIdlingResource(IdlingResourcesName.Network) + } + } + + fun pauseRNTimersIdlingResource() = pauseIdlingResource(IdlingResourcesName.Timers) + fun resumeRNTimersIdlingResource() = resumeIdlingResource(IdlingResourcesName.Timers) + fun pauseUIIdlingResource() = pauseIdlingResource(IdlingResourcesName.UIModule) + fun resumeUIIdlingResource() = resumeIdlingResource(IdlingResourcesName.UIModule) + + fun setBlacklistUrls(urlList: String) { + setIdlingResourceBlacklist(urlList) + } + + private fun setIdlingResourceBlacklist(urlList: String) { + val urlArray = toFormattedUrlArray(urlList) + NetworkIdlingResource.setURLBlacklist(urlArray) + } + + private fun setupMQThreadsInterrogators() { + setupMQThreadsInterrogator(LooperName.JS) + setupMQThreadsInterrogator(LooperName.NativeModules) + } + + private fun setupUrlBlacklist() { + if (launchArgs.hasURLBlacklist()) { + val blacklistUrls = launchArgs.urlBlacklist + setIdlingResourceBlacklist(blacklistUrls) + } + } + + private fun setupMQThreadsInterrogator(looperName: LooperName) { + val mqThreadsReflector = MQThreadsReflector(reactContext) + val looper = when (looperName) { + LooperName.JS -> mqThreadsReflector.getJSMQueue()?.getLooper() + LooperName.NativeModules -> mqThreadsReflector.getNativeModulesQueue()?.getLooper() + } + + looper?.let { + IdlingRegistry.getInstance().registerLooperAsIdlingResource(it) + loopers[looperName] = it + } + } + + private fun setupIdlingResources() { + setupIdlingResource(IdlingResourcesName.RNBridge) + setupIdlingResource(IdlingResourcesName.Timers) + setupIdlingResource(IdlingResourcesName.UIModule) + setupIdlingResource(IdlingResourcesName.Animations) + if (networkSyncEnabled) { + setupIdlingResource(IdlingResourcesName.Network) + } + setupIdlingResource(IdlingResourcesName.AsyncStorage) + } + + private fun syncIdlingResources() { + IdlingRegistry.getInstance().apply { + val irr: IdlingResourceRegistry = + Reflect.on(Espresso::class.java).field("baseRegistry").get() + irr.sync(this.resources, this.loopers) + } + } + + private fun unregisterMQThreadsInterrogators() { + loopers.values.forEach { + IdlingRegistry.getInstance().unregisterLooperAsIdlingResource(it) + } + } + + private fun unregisterIdlingResources() { + IdlingResourcesName.entries.forEach { + removeIdlingResource(it) + } + } + + private fun pauseIdlingResource(idlingResourcesName: IdlingResourcesName) { + val idlingResource = idlingResources[idlingResourcesName] + idlingResource?.pause() + } + + private fun resumeIdlingResource(idlingResourcesName: IdlingResourcesName) { + val idlingResource = idlingResources[idlingResourcesName] + idlingResource?.resume() + } + + private fun setupIdlingResource(idlingResourcesName: IdlingResourcesName) { + val idlingResource:DetoxIdlingResource? = when (idlingResourcesName) { + IdlingResourcesName.Timers -> TimersIdlingResource(reactContext) + IdlingResourcesName.AsyncStorage -> AsyncStorageIdlingResource.createIfNeeded(reactContext) + IdlingResourcesName.RNBridge -> BridgeIdlingResource(reactContext) + IdlingResourcesName.UIModule -> UIModuleIdlingResource(reactContext) + IdlingResourcesName.Animations -> AnimatedModuleIdlingResource(reactContext) + IdlingResourcesName.Network -> NetworkIdlingResource(reactContext) + } + + idlingResource?.let { + IdlingRegistry.getInstance().register(it) + idlingResources[idlingResourcesName] = it + } + } + + private fun removeIdlingResource(idlingResourcesName: IdlingResourcesName) { + val idlingResource = idlingResources[idlingResourcesName] + idlingResource?.let { + IdlingRegistry.getInstance().unregister(it) + idlingResource.onUnregistered() + idlingResources.remove(idlingResourcesName) + } + } + + private fun toFormattedUrlArray(urlList: String): List { + var formattedUrls = urlList + formattedUrls = formattedUrls.replace(Regex("""[()"]"""), "") + formattedUrls = formattedUrls.trim() + return formattedUrls.split(',') + } +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt new file mode 100644 index 0000000000..755375563d --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt @@ -0,0 +1,75 @@ +package com.wix.detox.reactnative.idlingresources.animations + +import android.util.Log +import android.view.Choreographer +import androidx.test.espresso.IdlingResource.ResourceCallback +import com.facebook.react.animated.NativeAnimatedModule +import com.facebook.react.bridge.ReactContext +import com.wix.detox.espresso.idlingresources.DescriptiveIdlingResource +import com.wix.detox.reactnative.ReactNativeInfo.rnVersion +import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource +import org.joor.Reflect +import org.joor.ReflectException + +/** + * Created by simonracz on 25/08/2017. + */ +/** + * + * Espresso IdlingResource for React Native's Animated Module. + * + * + * Hooks up to React Native internals to monitor the state of the animations. + * + */ +class AnimatedModuleIdlingResource(private val reactContext: ReactContext) : DetoxIdlingResource(), + Choreographer.FrameCallback { + private var callback: ResourceCallback? = null + + override fun getName(): String { + return AnimatedModuleIdlingResource::class.java.name + } + + override fun getDebugName(): String { + return "AnimatedModule" + } + + override fun getBusyHint(): Map { + return mapOf("reason" to "Animations running on screen") + } + + override fun checkIdle(): Boolean { + val animatedModule = reactContext.getNativeModule(NativeAnimatedModule::class.java) + val hasAnimations = animatedModule?.nodesManager?.hasActiveAnimations() ?: false + + if (hasAnimations) { + Log.i(LOG_TAG, "AnimatedModule is busy."); + Choreographer.getInstance().postFrameCallback(this); + return false + } + + notifyIdle() + return true + } + + override fun notifyIdle() { + callback?.onTransitionToIdle() + } + + + override fun registerIdleTransitionCallback(callback: ResourceCallback) { + this.callback = callback + + Choreographer.getInstance().postFrameCallback(this) + } + + override fun doFrame(frameTimeNanos: Long) { + isIdleNow + } + + companion object { + private const val LOG_TAG = "Detox" + } +} + + diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/bridge/BridgeIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/bridge/BridgeIdlingResource.kt new file mode 100644 index 0000000000..0e6fe7ba08 --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/bridge/BridgeIdlingResource.kt @@ -0,0 +1,83 @@ +package com.wix.detox.reactnative.idlingresources.bridge + +import android.util.Log +import androidx.test.espresso.IdlingResource.ResourceCallback +import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener +import com.facebook.react.bridge.ReactContext +import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Created by simonracz on 01/06/2017. + */ +/** + * + * + * IdlingResource for Espresso, which monitors the traffic of + * React Native's JS bridge. + * + */ +class BridgeIdlingResource(private val reactContext: ReactContext) : DetoxIdlingResource(), + NotThreadSafeBridgeIdleDebugListener { + private val idleNow = AtomicBoolean(true) + private var callback: ResourceCallback? = null + + init { + reactContext.catalystInstance.addBridgeIdleDebugListener(this) + } + + fun onDetach() { + reactContext.catalystInstance.removeBridgeIdleDebugListener(this) + } + + override fun getName(): String { + return BridgeIdlingResource::class.java.name + } + + override fun getDebugName(): String { + return "bridge" + } + + override fun getBusyHint(): Map? { + return null + } + + override fun checkIdle(): Boolean { + val ret = idleNow.get() + if (!ret) { + Log.i(LOG_TAG, "JS Bridge is busy") + } + return ret + } + + override fun registerIdleTransitionCallback(callback: ResourceCallback) { + this.callback = callback + } + + override fun onTransitionToBridgeIdle() { + idleNow.set(true) + notifyIdle() + } + + override fun onTransitionToBridgeBusy() { + idleNow.set(false) + // Log.i(LOG_TAG, "JS Bridge transitions to busy."); + } + + override fun onBridgeDestroyed() { + } + + override fun notifyIdle() { + // Log.i(LOG_TAG, "JS Bridge transitions to idle."); + callback?.onTransitionToIdle() + } + + override fun onUnregistered() { + super.onUnregistered() + onDetach() + } + + companion object { + private const val LOG_TAG = "Detox" + } +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/looper/MQThreadsReflector.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/looper/MQThreadsReflector.kt new file mode 100644 index 0000000000..f3762bf84a --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/looper/MQThreadsReflector.kt @@ -0,0 +1,47 @@ +package com.wix.detox.reactnative.idlingresources.looper + +import android.os.Looper +import android.util.Log +import com.facebook.react.bridge.ReactContext +import com.wix.detox.reactnative.idlingresources.ReactNativeIdlingResources.Companion.LOG_TAG +import org.joor.Reflect +import org.joor.ReflectException + + +private const val METHOD_GET_LOOPER = "getLooper" +private const val FIELD_NATIVE_MODULES_MSG_QUEUE = "mNativeModulesMessageQueueThread" +private const val FIELD_JS_MSG_QUEUE = "mJSMessageQueueThread" + +internal class MQThreadsReflector(private val reactContext: ReactContext) { + + fun getJSMQueue(): MQThreadReflected? { + return getQueue(FIELD_JS_MSG_QUEUE) + } + + fun getNativeModulesQueue(): MQThreadReflected? { + return getQueue(FIELD_NATIVE_MODULES_MSG_QUEUE) + } + + private fun getQueue(queueName: String): MQThreadReflected? { + try { + val queue = Reflect.on(reactContext).field(queueName).get() as Any? + return MQThreadReflected(queue, queueName) + } catch (e: ReflectException) { + Log.e(LOG_TAG, "Could not find queue: $queueName", e) + } + return null + } +} + +internal class MQThreadReflected(private val queue: Any?, private val queueName: String) { + fun getLooper(): Looper? { + try { + if (queue != null) { + return Reflect.on(queue).call(METHOD_GET_LOOPER).get() + } + } catch (e: ReflectException) { + Log.e(LOG_TAG, "Could not find looper for queue: $queueName", e) + } + return null + } +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkIdlingResource.kt new file mode 100644 index 0000000000..28e99c0021 --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkIdlingResource.kt @@ -0,0 +1,112 @@ +package com.wix.detox.reactnative.idlingresources.network + +import android.util.Log +import android.view.Choreographer +import androidx.test.espresso.IdlingResource.ResourceCallback +import com.facebook.react.bridge.ReactContext +import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource +import com.wix.detox.reactnative.idlingresources.network.NetworkIdlingResource +import okhttp3.Dispatcher +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException + +/** + * Created by simonracz on 09/10/2017. + * + * Idling Resource which monitors React Native's OkHttpClient. + * + * + * Must call stop() on it, before removing it from Espresso. + */ +class NetworkIdlingResource(private val dispatcher: Dispatcher) : DetoxIdlingResource(), + Choreographer.FrameCallback { + private var callback: ResourceCallback? = null + private val busyResources: MutableSet = HashSet() + + constructor(reactContext: ReactContext) : this(NetworkingModuleReflected(reactContext).getHttpClient()!!.dispatcher) + + override fun getName(): String { + return NetworkIdlingResource::class.java.name + } + + override fun getDebugName(): String { + return "network" + } + + @Synchronized + override fun getBusyHint(): Map = mapOf("urls" to ArrayList(busyResources)) + + override fun registerIdleTransitionCallback(callback: ResourceCallback) { + this.callback = callback + Choreographer.getInstance().postFrameCallback(this) + } + + override fun doFrame(frameTimeNanos: Long) { + isIdleNow + } + + @Synchronized + override fun checkIdle(): Boolean { + busyResources.clear() + + val calls = dispatcher.runningCalls() + for (call in calls) { + val url = call.request().url.toString() + + if (!isUrlBlacklisted(url)) { + busyResources.add(url) + } + } + + if (busyResources.isNotEmpty()) { + Log.i(LOG_TAG, "Network is busy, with " + busyResources.size + " in-flight calls") + Choreographer.getInstance().postFrameCallback(this) + return false + } + + notifyIdle() + return true + } + + override fun notifyIdle() { + if (callback != null) { + callback!!.onTransitionToIdle() + } + } + + private fun isUrlBlacklisted(url: String): Boolean { + for (pattern in blacklist) { + if (pattern.matcher(url).matches()) { + return true + } + } + return false + } + + companion object { + private const val LOG_TAG = "Detox" + + private val blacklist = ArrayList() + + /** + * Must be called on the UI thread. + * + * @param urls list of regexes of blacklisted urls + */ + fun setURLBlacklist(urls: List?) { + blacklist.clear() + if (urls == null) return + + for (url in urls) { + try { + blacklist.add(Pattern.compile(url)) + } catch (e: PatternSyntaxException) { + Log.e( + LOG_TAG, + "Couldn't parse regular expression for Black list url: $url", e + ) + } + } + } + } +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/NetworkingModuleReflected.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkingModuleReflected.kt similarity index 94% rename from detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/NetworkingModuleReflected.kt rename to detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkingModuleReflected.kt index 1affcf054c..c247f1db62 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/NetworkingModuleReflected.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkingModuleReflected.kt @@ -1,4 +1,4 @@ -package com.wix.detox.reactnative.idlingresources +package com.wix.detox.reactnative.idlingresources.network import android.util.Log import com.facebook.react.bridge.ReactContext diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/AsyncStorageIdlingResource.kt similarity index 69% rename from detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResource.kt rename to detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/AsyncStorageIdlingResource.kt index 5a00637dca..e398325d31 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/AsyncStorageIdlingResource.kt @@ -1,4 +1,4 @@ -package com.wix.detox.reactnative.idlingresources +package com.wix.detox.reactnative.idlingresources.storage import android.util.Log import androidx.test.espresso.IdlingResource @@ -6,11 +6,14 @@ import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactContext import com.wix.detox.espresso.idlingresources.DescriptiveIdlingResource import com.wix.detox.reactnative.helpers.RNHelpers +import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource import org.joor.Reflect import java.util.concurrent.Executor private typealias SExecutorReflectedGenFnType = (executor: Executor) -> SerialExecutorReflected -private val defaultSExecutorReflectedGenFn: SExecutorReflectedGenFnType = { executor: Executor -> SerialExecutorReflected(executor) } + +private val defaultSExecutorReflectedGenFn: SExecutorReflectedGenFnType = + { executor: Executor -> SerialExecutorReflected(executor) } private class ModuleReflected(module: NativeModule, sexecutorReflectedGen: SExecutorReflectedGenFnType) { private val executorReflected: SerialExecutorReflected @@ -25,13 +28,13 @@ private class ModuleReflected(module: NativeModule, sexecutorReflectedGen: SExec get() = executorReflected } -open class AsyncStorageIdlingResource - @JvmOverloads constructor( - module: NativeModule, - sexecutorReflectedGenFn: SExecutorReflectedGenFnType = defaultSExecutorReflectedGenFn) - : DescriptiveIdlingResource { +class AsyncStorageIdlingResource +@JvmOverloads constructor( + module: NativeModule, + sexecutorReflectedGenFn: SExecutorReflectedGenFnType = defaultSExecutorReflectedGenFn +) : DetoxIdlingResource() { - open val logTag: String + val logTag: String get() = LOG_TAG private val moduleReflected = ModuleReflected(module, sexecutorReflectedGenFn) @@ -44,7 +47,7 @@ open class AsyncStorageIdlingResource executeTask(idleCheckTask!!) } else { clearIdleCheckTask() - callback?.onTransitionToIdle() + notifyIdle() } } } @@ -54,25 +57,30 @@ open class AsyncStorageIdlingResource override fun getDebugName() = "io" override fun getBusyHint(): Map? = null - override fun isIdleNow(): Boolean = - checkIdle().also { idle -> - if (!idle) { - Log.d(logTag, "Async-storage is busy!") - enqueueIdleCheckTask() - } - } override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { this.callback = callback enqueueIdleCheckTask() } - private fun checkIdle(): Boolean = + private fun checkIdleInternal(): Boolean = with(moduleReflected.sexecutor) { synchronized(executor()) { !hasActiveTask() && !hasPendingTasks() } } + override fun checkIdle(): Boolean = + checkIdleInternal().also { idle -> + if (!idle) { + Log.d(logTag, "Async-storage is busy!") + enqueueIdleCheckTask() + } + } + + override fun notifyIdle() { + callback?.onTransitionToIdle() + } + private fun enqueueIdleCheckTask() = with(moduleReflected.sexecutor) { synchronized(executor()) { @@ -94,26 +102,21 @@ open class AsyncStorageIdlingResource companion object { private const val LOG_TAG = "AsyncStorageIR" - fun createIfNeeded(reactContext: ReactContext, legacy: Boolean): AsyncStorageIdlingResource? { - Log.d(LOG_TAG, "Checking whether a custom IR for Async-Storage is required... (legacy=$legacy)") + fun createIfNeeded(reactContext: ReactContext): AsyncStorageIdlingResource? { + Log.d(LOG_TAG, "Checking whether a custom IR for Async-Storage is required...") - return RNHelpers.getNativeModule(reactContext, className(legacy))?.let { module -> - Log.d(LOG_TAG, "IR for Async-Storage is required! (legacy=$legacy)") - createInstance(module, legacy) + return RNHelpers.getNativeModule(reactContext, className())?.let { module -> + Log.d(LOG_TAG, "IR for Async-Storage is required!") + createInstance(module) } } - private fun className(legacy: Boolean): String { - val packageName = if (legacy) "com.facebook.react.modules.storage" else "com.reactnativecommunity.asyncstorage" + private fun className(): String { + val packageName = "com.reactnativecommunity.asyncstorage" return "$packageName.AsyncStorageModule" } - private fun createInstance(module: NativeModule, legacy: Boolean) = - if (legacy) AsyncStorageIdlingResourceLegacy(module) else AsyncStorageIdlingResource(module) + private fun createInstance(module: NativeModule) = + AsyncStorageIdlingResource(module) } } - -class AsyncStorageIdlingResourceLegacy(module: NativeModule): AsyncStorageIdlingResource(module) { - override val logTag: String - get() = super.logTag + "Legacy" -} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/SerialExecutorReflected.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/SerialExecutorReflected.kt similarity index 90% rename from detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/SerialExecutorReflected.kt rename to detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/SerialExecutorReflected.kt index e1e0de1dfd..168b81c2e9 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/SerialExecutorReflected.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/SerialExecutorReflected.kt @@ -1,4 +1,4 @@ -package com.wix.detox.reactnative.idlingresources +package com.wix.detox.reactnative.idlingresources.storage import org.joor.Reflect import java.util.concurrent.Executor diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/DelegatedIdleInterrogationStrategy.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/DelegatedIdleInterrogationStrategy.kt deleted file mode 100644 index 0a6cd5e615..0000000000 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/DelegatedIdleInterrogationStrategy.kt +++ /dev/null @@ -1,23 +0,0 @@ - -package com.wix.detox.reactnative.idlingresources.timers - -import com.facebook.react.bridge.ReactContext -import com.facebook.react.modules.core.TimingModule - -private const val BUSY_WINDOW_THRESHOLD = 1500L - -/** - * Delegates the interrogation to the native module itself, added - * [here](https://github.com/facebook/react-native/pull/27539) in the context - * of RN v0.62 (followed by a previous refactor and rename of the class). - */ -class DelegatedIdleInterrogationStrategy(private val timingModule: TimingModule): IdleInterrogationStrategy { - override fun isIdleNow(): Boolean = !timingModule.hasActiveTimersInRange(BUSY_WINDOW_THRESHOLD) - - companion object { - fun create(reactContext: ReactContext): DelegatedIdleInterrogationStrategy { - val timingModule = reactContext.getNativeModule(TimingModule::class.java)!! - return DelegatedIdleInterrogationStrategy(timingModule) - } - } -} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/IdleInterrogationStrategy.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/IdleInterrogationStrategy.kt deleted file mode 100644 index ab7ce15260..0000000000 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/IdleInterrogationStrategy.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.wix.detox.reactnative.idlingresources.timers - -import com.facebook.react.bridge.ReactContext -import com.wix.detox.common.UIThread -import java.util.concurrent.Callable - -interface IdleInterrogationStrategy { - fun isIdleNow(): Boolean -} - -fun getInterrogationStrategy(reactContext: ReactContext): IdleInterrogationStrategy? = - // Getting a native-module (inside) also initializes it if needed. That has to run on a - // looper thread, and the easiest to make sure that happens is to use the main thread. - UIThread.runSync(Callable { - return@Callable DelegatedIdleInterrogationStrategy.create(reactContext) - }) diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResource.kt index 126725a4a9..9d106b5dea 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResource.kt @@ -1,15 +1,22 @@ package com.wix.detox.reactnative.idlingresources.timers +import android.annotation.SuppressLint import android.view.Choreographer import androidx.test.espresso.IdlingResource -import com.wix.detox.reactnative.idlingresources.DetoxBaseIdlingResource +import com.facebook.react.bridge.ReactContext +import com.facebook.react.modules.core.TimingModule +import com.facebook.react.modules.network.NetworkingModule +import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource + +private const val BUSY_WINDOW_THRESHOLD = 1500L class TimersIdlingResource @JvmOverloads constructor( - private val interrogationStrategy: IdleInterrogationStrategy, - private val getChoreographer: () -> Choreographer = { Choreographer.getInstance() } - ) : DetoxBaseIdlingResource(), Choreographer.FrameCallback { + reactContext: ReactContext, + private val getChoreographer: () -> Choreographer = { Choreographer.getInstance() } +) : DetoxIdlingResource(), Choreographer.FrameCallback { private var callback: IdlingResource.ResourceCallback? = null + private val timingModule: TimingModule = reactContext.getNativeModule(TimingModule::class.java)!! override fun getName(): String = this.javaClass.name override fun getDebugName(): String = "timers" @@ -20,14 +27,17 @@ class TimersIdlingResource @JvmOverloads constructor( getChoreographer().postFrameCallback(this) } + @SuppressLint("VisibleForTests") override fun checkIdle(): Boolean { - return interrogationStrategy.isIdleNow().also { result -> - if (result) { - notifyIdle() - } else { - getChoreographer().postFrameCallback(this@TimersIdlingResource) - } + val isIdle = !timingModule.hasActiveTimersInRange(BUSY_WINDOW_THRESHOLD) + + if (isIdle) { + notifyIdle() + } else { + getChoreographer().postFrameCallback(this) } + + return isIdle } override fun doFrame(frameTimeNanos: Long) { diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIModuleIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIModuleIdlingResource.kt index 0722d49a1c..345297275d 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIModuleIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIModuleIdlingResource.kt @@ -5,7 +5,7 @@ import android.view.Choreographer import androidx.test.espresso.IdlingResource.ResourceCallback import com.facebook.react.bridge.ReactContext import com.wix.detox.reactnative.helpers.RNHelpers -import com.wix.detox.reactnative.idlingresources.DetoxBaseIdlingResource +import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource import org.joor.ReflectException /** @@ -13,7 +13,7 @@ import org.joor.ReflectException * Hooks up to React Native internals to grab the pending ui operations from it. */ class UIModuleIdlingResource(private val reactContext: ReactContext) - : DetoxBaseIdlingResource(), Choreographer.FrameCallback { + : DetoxIdlingResource(), Choreographer.FrameCallback { private val rn66workaround = RN66Workaround() private val uiManagerModuleReflected = UIManagerModuleReflected(reactContext) diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResourceSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResourceSpec.kt index 59556ff0c4..02362e7f35 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResourceSpec.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResourceSpec.kt @@ -3,6 +3,8 @@ package com.wix.detox.reactnative.idlingresources import androidx.test.espresso.IdlingResource import com.facebook.react.bridge.NativeModule import com.wix.detox.UTHelpers.yieldToOtherThreads +import com.wix.detox.reactnative.idlingresources.storage.AsyncStorageIdlingResource +import com.wix.detox.reactnative.idlingresources.storage.SerialExecutorReflected import org.assertj.core.api.Assertions.assertThat import org.mockito.kotlin.* import org.spekframework.spek2.Spek @@ -58,7 +60,7 @@ class AsyncStorageIdlingResourceSpec: Spek({ fun verifyTaskEnqueuedTwice() = verify(sexecutorReflected, times(2)).executeTask(any()) it("should have a name") { - assertThat(uut.name).isEqualTo("com.wix.detox.reactnative.idlingresources.AsyncStorageIdlingResource") + assertThat(uut.name).isEqualTo("com.wix.detox.reactnative.idlingresources.storage.AsyncStorageIdlingResource") } it("should have a debug-name") { diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResourcesTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResourcesTest.kt index 6f863e2e47..5bd6843727 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResourcesTest.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResourcesTest.kt @@ -1,6 +1,7 @@ package com.wix.detox.reactnative.idlingresources import com.wix.detox.UTHelpers.yieldToOtherThreads +import com.wix.detox.reactnative.idlingresources.network.NetworkIdlingResource import org.assertj.core.api.Assertions.assertThat import okhttp3.Dispatcher @@ -18,7 +19,10 @@ class NetworkIdlingResourcesTest { @Before fun setup() { dispatcher = Dispatcher() - uut = NetworkIdlingResource(dispatcher) + uut = + NetworkIdlingResource( + dispatcher + ) } // Note: Ideally, we should test that the list of busy resources is protected, diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/SerialExecutorReflectedSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/SerialExecutorReflectedSpec.kt index 31cdb31b51..29e0a2a4f5 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/SerialExecutorReflectedSpec.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/SerialExecutorReflectedSpec.kt @@ -1,5 +1,6 @@ package com.wix.detox.reactnative.idlingresources +import com.wix.detox.reactnative.idlingresources.storage.SerialExecutorReflected import org.assertj.core.api.Assertions.assertThat import org.mockito.kotlin.mock import org.mockito.kotlin.verify From 4f3c43e1bda18b3087f33906b039911263c4a14a Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Wed, 18 Dec 2024 10:48:16 +0200 Subject: [PATCH 03/14] Fixed tests --- .../DelegatedIdleInterrogationStrategySpec.kt | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/timers/DelegatedIdleInterrogationStrategySpec.kt diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/timers/DelegatedIdleInterrogationStrategySpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/timers/DelegatedIdleInterrogationStrategySpec.kt deleted file mode 100644 index cff0df83b3..0000000000 --- a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/timers/DelegatedIdleInterrogationStrategySpec.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.wix.detox.reactnative.idlingresources.timers - -import com.facebook.react.bridge.NativeModule -import org.assertj.core.api.Assertions.assertThat -import org.mockito.kotlin.* -import org.spekframework.spek2.Spek -import org.spekframework.spek2.style.specification.describe -import com.facebook.react.modules.core.TimingModule - -private const val BUSY_INTERVAL_MS = 1500L - -object DelegatedIdleInterrogationStrategySpec : Spek({ - describe("Timers idle-interrogation strategy") { - - lateinit var timingModule: TimingModule - - beforeEachTest { - timingModule = mock() - } - - fun givenActiveTimersInQueue() { - whenever(timingModule.hasActiveTimersInRange(any())).thenReturn(true) - } - - fun givenNoActiveTimersInQueue() { - whenever(timingModule.hasActiveTimersInRange(any())).thenReturn(false) - } - - fun uut() = DelegatedIdleInterrogationStrategy(timingModule) - - it("should be idle if timing module has no active timers") { - givenNoActiveTimersInQueue() - assertThat(uut().isIdleNow()).isTrue() - } - - it("should be busy if timing module has active timers") { - givenActiveTimersInQueue() - assertThat(uut().isIdleNow()).isFalse() - } - - it("should specify the busy-interval as the active-timers lookahead range") { - givenActiveTimersInQueue() - uut().isIdleNow() - verify(timingModule).hasActiveTimersInRange(eq(BUSY_INTERVAL_MS)) - } - } -}) From fd85e8c7edaa80349788c5b22bc07d87e9e3552e Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Wed, 18 Dec 2024 11:11:13 +0200 Subject: [PATCH 04/14] Fixed tests --- .../timers/TimersIdlingResourceSpec.kt | 189 ---------------- .../timers/TimersIdlingResourceTest.kt | 212 ++++++++++++++++++ 2 files changed, 212 insertions(+), 189 deletions(-) delete mode 100644 detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResourceSpec.kt create mode 100644 detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResourceTest.kt diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResourceSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResourceSpec.kt deleted file mode 100644 index dcc14da03f..0000000000 --- a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResourceSpec.kt +++ /dev/null @@ -1,189 +0,0 @@ -package com.wix.detox.reactnative.idlingresources.timers - -import android.view.Choreographer -import androidx.test.espresso.IdlingResource -import org.assertj.core.api.Assertions -import org.mockito.kotlin.* -import org.spekframework.spek2.Spek -import org.spekframework.spek2.style.specification.describe - -private fun anIdlingResourceCallback() = mock() - -object TimersIdlingResourceSpec : Spek({ - describe("React Native timers idling-resource") { - lateinit var choreographer: Choreographer - lateinit var idleInterrogationStrategy: IdleInterrogationStrategy - - beforeEachTest { - idleInterrogationStrategy = mock() - choreographer = mock() - } - - fun uut() = TimersIdlingResource(idleInterrogationStrategy) { choreographer } - - fun givenIdleStrategy() { - whenever(idleInterrogationStrategy.isIdleNow()).thenReturn(true) - } - - fun givenBusyStrategy() { - whenever(idleInterrogationStrategy.isIdleNow()).thenReturn(false) - } - - fun getChoreographerCallback(): Choreographer.FrameCallback { - argumentCaptor().apply { - verify(choreographer).postFrameCallback(capture()) - return firstValue - } - } - - fun invokeChoreographerCallback() { - getChoreographerCallback().doFrame(0L) - } - - it("should return a debug-name") { - Assertions.assertThat(uut().getDebugName()).isEqualTo("timers") - } - - it("should be idle if strategy says so") { - givenIdleStrategy() - Assertions.assertThat(uut().isIdleNow).isTrue() - } - - it("should be busy if strategy says so") { - givenBusyStrategy() - Assertions.assertThat(uut().isIdleNow).isFalse() - } - - it("should transition to idle if found idle by strategy") { - givenIdleStrategy() - - val callback = anIdlingResourceCallback() - - with(uut()) { - registerIdleTransitionCallback(callback) - isIdleNow - } - - verify(callback).onTransitionToIdle() - } - - it("should NOT transition to idle if found busy by strategy") { - givenBusyStrategy() - - val callback = anIdlingResourceCallback() - - with(uut()) { - registerIdleTransitionCallback(callback) - isIdleNow - } - - verify(callback, never()).onTransitionToIdle() - } - - it("should be idle if paused") { - givenBusyStrategy() - - val uut = uut().apply { - pause() - } - - Assertions.assertThat(uut.isIdleNow).isTrue() - } - - it("should be busy if paused and resumed") { - givenBusyStrategy() - - val uut = uut().apply { - pause() - resume() - } - - Assertions.assertThat(uut.isIdleNow).isFalse() - } - - it("should notify of transition to idle upon pausing") { - givenBusyStrategy() - - val callback = anIdlingResourceCallback() - - with(uut()) { - registerIdleTransitionCallback(callback) - pause() - } - - verify(callback).onTransitionToIdle() - } - - it("should enqueue an is-idle check using choreographer when a callback gets registered") { - with(uut()) { - registerIdleTransitionCallback(mock()) - } - - verify(choreographer).postFrameCallback(any()) - } - - it("should transition to idle when preregistered choreographer is dispatched") { - givenIdleStrategy() - - val callback = anIdlingResourceCallback() - - uut().registerIdleTransitionCallback(callback) - invokeChoreographerCallback() - - verify(callback).onTransitionToIdle() - } - - it("should NOT transition to idle if not idle when preregistered choreographer is dispatched") { - givenBusyStrategy() - - val callback = anIdlingResourceCallback() - - uut().registerIdleTransitionCallback(callback) - invokeChoreographerCallback() - - verify(callback, never()).onTransitionToIdle() - } - - it("should re-register choreographer if found idle while preregistered choreographer is dispatched") { - givenBusyStrategy() - - val callback = anIdlingResourceCallback() - - val uut = uut() - uut.registerIdleTransitionCallback(callback) - invokeChoreographerCallback() - - verify(choreographer, times(2)).postFrameCallback(any()) - } - - it("should adhere to pausing also when invoked via choreographer callback") { - givenBusyStrategy() - - val callback = anIdlingResourceCallback() - - uut().apply { - pause() - registerIdleTransitionCallback(callback) - } - val runtimeChoreographerCallback = getChoreographerCallback() - - reset(callback, choreographer) - runtimeChoreographerCallback.doFrame(0L) - - verify(callback, never()).onTransitionToIdle() - verify(choreographer, never()).postFrameCallback(any()) - } - - it("should enqueue an additional idle check (using choreographer) if found busy") { - givenBusyStrategy() - uut().isIdleNow - verify(choreographer).postFrameCallback(any()) - } - - it("should NOT enqueue an additional idle check (using choreographer) if found idle") { - givenIdleStrategy() - uut().isIdleNow - verify(choreographer, never()).postFrameCallback(any()) - } - } -}) diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResourceTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResourceTest.kt new file mode 100644 index 0000000000..eed5297ab4 --- /dev/null +++ b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResourceTest.kt @@ -0,0 +1,212 @@ +package com.wix.detox.reactnative.idlingresources.timers + +import android.view.Choreographer +import androidx.test.espresso.IdlingResource +import com.facebook.react.bridge.ReactContext +import com.facebook.react.modules.core.TimingModule +import org.assertj.core.api.Assertions +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.* +import org.robolectric.RobolectricTestRunner +import org.spekframework.spek2.Spek +import org.spekframework.spek2.style.specification.describe + +private fun anIdlingResourceCallback() = mock() + +@RunWith(RobolectricTestRunner::class) +class TimersIdlingResourceTest { + private val choreographer: Choreographer = mock() + private val context: ReactContext = mock() + private val timersModule: TimingModule = mock() + private lateinit var timersIdlingResource: TimersIdlingResource + + @Before + fun setup() { + whenever(context.getNativeModule(eq(TimingModule::class.java))).thenReturn(timersModule) + timersIdlingResource = TimersIdlingResource(context) { choreographer } + } + + + private fun givenIdleStrategy() { + whenever(timersModule.hasActiveTimersInRange(any())).thenReturn(false) + } + + private fun givenBusyStrategy() { + whenever(timersModule.hasActiveTimersInRange(any())).thenReturn(true) + } + + private fun getChoreographerCallback(): Choreographer.FrameCallback { + argumentCaptor().apply { + verify(choreographer).postFrameCallback(capture()) + return firstValue + } + } + + private fun invokeChoreographerCallback() { + getChoreographerCallback().doFrame(0L) + } + + @Test + fun `should return a debug-name`() { + Assertions.assertThat(timersIdlingResource.getDebugName()).isEqualTo("timers") + } + + @Test + fun `should be idle if strategy says so`() { + givenIdleStrategy() + Assertions.assertThat(timersIdlingResource.isIdleNow).isTrue() + } + + @Test + fun `should be busy if strategy says so`() { + givenBusyStrategy() + Assertions.assertThat(timersIdlingResource.isIdleNow).isFalse() + } + + @Test + fun `should transition to idle if found idle by strategy`() { + givenIdleStrategy() + + val callback = anIdlingResourceCallback() + + with(timersIdlingResource) { + registerIdleTransitionCallback(callback) + isIdleNow + } + + verify(callback).onTransitionToIdle() + } + + @Test + fun `should NOT transition to idle if found busy by strategy`() { + givenBusyStrategy() + + val callback = anIdlingResourceCallback() + + with(timersIdlingResource) { + registerIdleTransitionCallback(callback) + isIdleNow + } + + verify(callback, never()).onTransitionToIdle() + } + + @Test + fun `should be idle if paused`() { + givenBusyStrategy() + + val uut = timersIdlingResource.apply { + pause() + } + + Assertions.assertThat(uut.isIdleNow).isTrue() + } + + @Test + fun `should be busy if paused and resumed`() { + givenBusyStrategy() + + val uut = timersIdlingResource.apply { + pause() + resume() + } + + Assertions.assertThat(uut.isIdleNow).isFalse() + } + + @Test + fun `should notify of transition to idle upon pausing`() { + givenBusyStrategy() + + val callback = anIdlingResourceCallback() + + with(timersIdlingResource) { + registerIdleTransitionCallback(callback) + pause() + } + + verify(callback).onTransitionToIdle() + } + + @Test + fun `should enqueue an is-idle check using choreographer when a callback gets registered`() { + with(timersIdlingResource) { + registerIdleTransitionCallback(mock()) + } + + verify(choreographer).postFrameCallback(any()) + } + + @Test + fun `should transition to idle when preregistered choreographer is dispatched`() { + givenIdleStrategy() + + val callback = anIdlingResourceCallback() + + timersIdlingResource.registerIdleTransitionCallback(callback) + invokeChoreographerCallback() + + verify(callback).onTransitionToIdle() + } + + @Test + fun `should NOT transition to idle if not idle when preregistered choreographer is dispatched`() { + givenBusyStrategy() + + val callback = anIdlingResourceCallback() + + timersIdlingResource.registerIdleTransitionCallback(callback) + invokeChoreographerCallback() + + verify(callback, never()).onTransitionToIdle() + } + + @Test + fun `should re-register choreographer if found idle while preregistered choreographer is dispatched`() { + givenBusyStrategy() + + val callback = anIdlingResourceCallback() + + val uut = timersIdlingResource + uut.registerIdleTransitionCallback(callback) + invokeChoreographerCallback() + + verify(choreographer, times(2)).postFrameCallback(any()) + } + + @Test + fun `should adhere to pausing also when invoked via choreographer callback`() { + givenBusyStrategy() + + val callback = anIdlingResourceCallback() + + timersIdlingResource.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`() { + givenBusyStrategy() + timersIdlingResource.isIdleNow + verify(choreographer).postFrameCallback(any()) + } + + @Test + fun `should NOT enqueue an additional idle check (using choreographer) if found idle`() { + givenIdleStrategy() + timersIdlingResource.isIdleNow + verify(choreographer, never()).postFrameCallback(any()) + } +} + From 738a1583ed3bf1ab2e842994016ea3626b527151 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Wed, 18 Dec 2024 11:49:39 +0200 Subject: [PATCH 05/14] Create factory --- .../ReactNativeIdlingResources.kt | 77 ++++++++----------- .../factory/DetoxIdlingResourceFactory.kt | 25 ++++++ .../factory/IdlingResourcesName.kt | 10 +++ .../idlingresources/factory/LooperName.kt | 6 ++ .../uimodule/RN66Workaround.kt | 71 ----------------- .../uimodule/UIManagerModuleReflected.kt | 46 +++++------ .../uimodule/UIModuleIdlingResource.kt | 7 +- 7 files changed, 91 insertions(+), 151 deletions(-) create mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/DetoxIdlingResourceFactory.kt create mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/IdlingResourcesName.kt create mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/LooperName.kt delete mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/RN66Workaround.kt diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt index 0618076ef3..4bd48520ef 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt @@ -7,34 +7,20 @@ import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.base.IdlingResourceRegistry import com.facebook.react.bridge.ReactContext import com.wix.detox.LaunchArgs -import com.wix.detox.reactnative.idlingresources.animations.AnimatedModuleIdlingResource -import com.wix.detox.reactnative.idlingresources.bridge.BridgeIdlingResource +import com.wix.detox.reactnative.idlingresources.factory.DetoxIdlingResourceFactory +import com.wix.detox.reactnative.idlingresources.factory.IdlingResourcesName +import com.wix.detox.reactnative.idlingresources.factory.LooperName import com.wix.detox.reactnative.idlingresources.looper.MQThreadsReflector import com.wix.detox.reactnative.idlingresources.network.NetworkIdlingResource -import com.wix.detox.reactnative.idlingresources.storage.AsyncStorageIdlingResource -import com.wix.detox.reactnative.idlingresources.timers.TimersIdlingResource -import com.wix.detox.reactnative.idlingresources.uimodule.UIModuleIdlingResource +import kotlinx.coroutines.runBlocking import org.joor.Reflect -private enum class IdlingResourcesName { - Timers, - AsyncStorage, - RNBridge, - UIModule, - Animations, - Network -} - -private enum class LooperName { - JS, - NativeModules -} - class ReactNativeIdlingResources( private val reactContext: ReactContext, private var launchArgs: LaunchArgs, - internal var networkSyncEnabled: Boolean = true + internal var networkSyncEnabled: Boolean = true, + private val idlingResourcesFactory: DetoxIdlingResourceFactory = DetoxIdlingResourceFactory(reactContext) ) { companion object { const val LOG_TAG = "DetoxRNIdleRes" @@ -44,14 +30,16 @@ class ReactNativeIdlingResources( private val loopers = mutableMapOf() fun registerAll() { - Log.i(LOG_TAG, "Setting up Espresso Idling Resources for React Native") - unregisterAll() - - setupUrlBlacklist() - setupMQThreadsInterrogators() - syncIdlingResources() - setupIdlingResources() - syncIdlingResources() + runBlocking { + Log.i(LOG_TAG, "Setting up Espresso Idling Resources for React Native") + unregisterAll() + + setupUrlBlacklist() + setupMQThreadsInterrogators() + syncIdlingResources() + setupIdlingResources() + syncIdlingResources() + } } fun unregisterAll() { @@ -60,16 +48,18 @@ class ReactNativeIdlingResources( } fun setNetworkSynchronization(enable: Boolean) { - if (networkSyncEnabled == enable) { - return + runBlocking { + if (networkSyncEnabled == enable) { + return@runBlocking + } + + if (enable) { + setupIdlingResource(IdlingResourcesName.Network) + } else { + removeIdlingResource(IdlingResourcesName.Network) + } + networkSyncEnabled = enable } - - if (enable) { - setupIdlingResource(IdlingResourcesName.Network) - } else { - removeIdlingResource(IdlingResourcesName.Network) - } - networkSyncEnabled = enable } fun pauseNetworkSynchronization() = pauseIdlingResource(IdlingResourcesName.Network) @@ -118,7 +108,7 @@ class ReactNativeIdlingResources( } } - private fun setupIdlingResources() { + private suspend fun setupIdlingResources() { setupIdlingResource(IdlingResourcesName.RNBridge) setupIdlingResource(IdlingResourcesName.Timers) setupIdlingResource(IdlingResourcesName.UIModule) @@ -159,15 +149,8 @@ class ReactNativeIdlingResources( idlingResource?.resume() } - private fun setupIdlingResource(idlingResourcesName: IdlingResourcesName) { - val idlingResource:DetoxIdlingResource? = when (idlingResourcesName) { - IdlingResourcesName.Timers -> TimersIdlingResource(reactContext) - IdlingResourcesName.AsyncStorage -> AsyncStorageIdlingResource.createIfNeeded(reactContext) - IdlingResourcesName.RNBridge -> BridgeIdlingResource(reactContext) - IdlingResourcesName.UIModule -> UIModuleIdlingResource(reactContext) - IdlingResourcesName.Animations -> AnimatedModuleIdlingResource(reactContext) - IdlingResourcesName.Network -> NetworkIdlingResource(reactContext) - } + private suspend fun setupIdlingResource(idlingResourcesName: IdlingResourcesName) { + val idlingResource = idlingResourcesFactory.create(idlingResourcesName) idlingResource?.let { IdlingRegistry.getInstance().register(it) diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/DetoxIdlingResourceFactory.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/DetoxIdlingResourceFactory.kt new file mode 100644 index 0000000000..438484dc9d --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/DetoxIdlingResourceFactory.kt @@ -0,0 +1,25 @@ +package com.wix.detox.reactnative.idlingresources.factory + +import com.facebook.react.bridge.ReactContext +import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource +import com.wix.detox.reactnative.idlingresources.animations.AnimatedModuleIdlingResource +import com.wix.detox.reactnative.idlingresources.bridge.BridgeIdlingResource +import com.wix.detox.reactnative.idlingresources.network.NetworkIdlingResource +import com.wix.detox.reactnative.idlingresources.storage.AsyncStorageIdlingResource +import com.wix.detox.reactnative.idlingresources.timers.TimersIdlingResource +import com.wix.detox.reactnative.idlingresources.uimodule.UIModuleIdlingResource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class DetoxIdlingResourceFactory(private val reactContext: ReactContext) { + suspend fun create(name: IdlingResourcesName): DetoxIdlingResource? = withContext(Dispatchers.Main) { + return@withContext when (name) { + IdlingResourcesName.Timers -> TimersIdlingResource(reactContext) + IdlingResourcesName.AsyncStorage -> AsyncStorageIdlingResource.createIfNeeded(reactContext) + IdlingResourcesName.RNBridge -> BridgeIdlingResource(reactContext) + IdlingResourcesName.UIModule -> UIModuleIdlingResource(reactContext) + IdlingResourcesName.Animations -> AnimatedModuleIdlingResource(reactContext) + IdlingResourcesName.Network -> NetworkIdlingResource(reactContext) + } + } +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/IdlingResourcesName.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/IdlingResourcesName.kt new file mode 100644 index 0000000000..5a2c438ff0 --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/IdlingResourcesName.kt @@ -0,0 +1,10 @@ +package com.wix.detox.reactnative.idlingresources.factory + +enum class IdlingResourcesName { + Timers, + AsyncStorage, + RNBridge, + UIModule, + Animations, + Network +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/LooperName.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/LooperName.kt new file mode 100644 index 0000000000..fa4728e85c --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/LooperName.kt @@ -0,0 +1,6 @@ +package com.wix.detox.reactnative.idlingresources.factory + +enum class LooperName { + JS, + NativeModules +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/RN66Workaround.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/RN66Workaround.kt deleted file mode 100644 index c347ee7c6f..0000000000 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/RN66Workaround.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.wix.detox.reactnative.idlingresources.uimodule - -import android.util.Log -import android.view.View -import com.facebook.react.uimanager.IllegalViewOperationException -import com.wix.detox.common.DetoxLog.Companion.LOG_TAG -import com.wix.detox.reactnative.ReactNativeInfo -import java.lang.ref.WeakReference - -private const val NUM_TIMES_BEFORE_NOTIFY_IDLE = 10 -private const val SET_NATIVE_VALUE = "setNativeValue" -private const val CLASS_REACT_SWITCH = "com.facebook.react.views.switchview.ReactSwitch" - -class RN66Workaround { - private var timesStuckQueueDetected = 0 - private var stuckOperation: WeakReference? = null - - // This is a workaround for https://github.com/facebook/react-native/issues/32594 - // uses duck typing heuristics to determine that this is probably the stuck Switch operation and if so, ignores it - fun isScarceUISwitchCommandStuckInQueue(uiManagerModuleReflected: UIManagerModuleReflected): Boolean { - var isStuckSwitchOperation = false - - if (isRelevantRNVersion() && uiManagerModuleReflected.getUIOpsCount() >= 1) { - val nextUIOperation = uiManagerModuleReflected.getNextUIOpReflected() - val view = getUIOpView(uiManagerModuleReflected, nextUIOperation) - val isReactSwitch = isReactSwitch(view) - val hasOneRetryIncremented = nextUIOperation?.numRetries == 1 - val isSetNativeValueCommand = (nextUIOperation?.viewCommand ?: "") == SET_NATIVE_VALUE - - if (isReactSwitch && hasOneRetryIncremented && isSetNativeValueCommand) { - if (stuckOperation?.get() == nextUIOperation?.instance) { - timesStuckQueueDetected++ - } else { - stuckOperation = WeakReference(nextUIOperation?.instance) - timesStuckQueueDetected = 0 - } - } - - if (timesStuckQueueDetected >= NUM_TIMES_BEFORE_NOTIFY_IDLE) { - isStuckSwitchOperation = true - } - } else { - timesStuckQueueDetected = 0 - } - return isStuckSwitchOperation - } - - private fun isRelevantRNVersion(): Boolean { - val rnVersion = ReactNativeInfo.rnVersion() - return rnVersion.minor == 66 || (rnVersion.minor == 67 && rnVersion.patch < 4) - } - - private fun getUIOpView(uiManagerModuleReflected: UIManagerModuleReflected, uiOperation: DispatchCommandOperationReflected?): View? { - val nativeViewHierarchyManager = uiManagerModuleReflected.nativeViewHierarchyManager() ?: return null - val tag = uiOperation?.tag ?: return null - return try { - nativeViewHierarchyManager.getViewClass(tag) - } catch(e: IllegalViewOperationException) { - Log.e(LOG_TAG, "failed to get view from tag ", e.cause) - null - } - } - - private fun isReactSwitch(view: View?) = try { - val ReactSwitchClass: Class<*> = Class.forName(CLASS_REACT_SWITCH) - if (view != null) ReactSwitchClass.isAssignableFrom(view.javaClass) else false - } catch (e: ClassNotFoundException) { - Log.e(LOG_TAG, "failed to get $CLASS_REACT_SWITCH class ", e.cause) - false - } -} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIManagerModuleReflected.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIManagerModuleReflected.kt index b93ef51c25..733e11b682 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIManagerModuleReflected.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIManagerModuleReflected.kt @@ -18,20 +18,12 @@ private const val FIELD_NON_BATCHED_OPS_LOCK = "mNonBatchedOperationsLock" class UIManagerModuleReflected(private val reactContext: ReactContext) { - fun getUIOpsCount(): Int = (viewCommandOperations()?.size ?: 0) - fun getNextUIOpReflected() = viewCommandOperations()?.firstCommandReflected() - - fun nativeViewHierarchyManager(): NativeHierarchyManagerReflected? = - getUIOperationQueue()?.let { - NativeHierarchyManagerReflected(it) - } - fun isRunnablesListEmpty(): Boolean = getUIOperationQueue()?.let { - synchronized(Reflect.on(it).field(FIELD_DISPATCH_RUNNABLES_LOCK).get()) { + synchronized(Reflect.on(it).field(FIELD_DISPATCH_RUNNABLES_LOCK).get()) { Reflect.on(it) .field(FIELD_DISPATCH_RUNNABLES) - .call(METHOD_IS_EMPTY).get() + .call(METHOD_IS_EMPTY).get() } } ?: true @@ -39,32 +31,32 @@ class UIManagerModuleReflected(private val reactContext: ReactContext) { getUIOperationQueue()?.let { synchronized(Reflect.on(it).field(FIELD_NON_BATCHED_OPS_LOCK).get()) { Reflect.on(it) - .field(FIELD_NON_BATCHED_OPS) - .call(METHOD_IS_EMPTY).get() + .field(FIELD_NON_BATCHED_OPS) + .call(METHOD_IS_EMPTY).get() } } ?: true fun isOperationQueueEmpty(): Boolean = getUIOperationQueue()?.let { - Reflect.on(it).call(METHOD_IS_EMPTY).get() + Reflect.on(it).call(METHOD_IS_EMPTY).get() } ?: true private fun viewCommandOperations(): ViewCommandOpsQueueReflected? = - getUIOperationQueue()?.let { - ViewCommandOpsQueueReflected(it) - } + getUIOperationQueue()?.let { + ViewCommandOpsQueueReflected(it) + } private fun getUIOperationQueue(): UIViewOperationQueue? = - try { - val uiModuleClass = Class.forName(CLASS_UI_MANAGER_MODULE) - Reflect.on(reactContext) - .call(METHOD_GET_NATIVE_MODULE, uiModuleClass) - .call(METHOD_GET_UI_IMPLEMENTATION) - .field(FIELD_UI_OPERATION_QUEUE) - .get() - } catch (e: Exception) { - Log.e(LOG_TAG, "failed to get $CLASS_UI_MANAGER_MODULE instance ", e) - null - } + try { + val uiModuleClass = Class.forName(CLASS_UI_MANAGER_MODULE) + Reflect.on(reactContext) + .call(METHOD_GET_NATIVE_MODULE, uiModuleClass) + .call(METHOD_GET_UI_IMPLEMENTATION) + .field(FIELD_UI_OPERATION_QUEUE) + .get() + } catch (e: Exception) { + Log.e(LOG_TAG, "failed to get $CLASS_UI_MANAGER_MODULE instance ", e) + null + } } diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIModuleIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIModuleIdlingResource.kt index 345297275d..1eb3894038 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIModuleIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIModuleIdlingResource.kt @@ -15,7 +15,6 @@ import org.joor.ReflectException class UIModuleIdlingResource(private val reactContext: ReactContext) : DetoxIdlingResource(), Choreographer.FrameCallback { - private val rn66workaround = RN66Workaround() private val uiManagerModuleReflected = UIManagerModuleReflected(reactContext) private var callback: ResourceCallback? = null @@ -39,11 +38,7 @@ class UIModuleIdlingResource(private val reactContext: ReactContext) val runnablesAreEmpty = uiManagerModuleReflected.isRunnablesListEmpty() val nonBatchesOpsEmpty = uiManagerModuleReflected.isNonBatchOpsEmpty() - var operationQueueEmpty = uiManagerModuleReflected.isOperationQueueEmpty() - - if (!operationQueueEmpty) { - operationQueueEmpty = rn66workaround.isScarceUISwitchCommandStuckInQueue(uiManagerModuleReflected) - } + val operationQueueEmpty = uiManagerModuleReflected.isOperationQueueEmpty() if (runnablesAreEmpty && nonBatchesOpsEmpty && operationQueueEmpty) { notifyIdle() From f511625f4c38dfbb77d970d6280ce2660c45e944 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Wed, 18 Dec 2024 11:52:37 +0200 Subject: [PATCH 06/14] Minor fixes --- .../idlingresources/ReactNativeIdlingResources.kt | 1 + .../animations/AnimatedModuleIdlingResource.kt | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt index 4bd48520ef..cfd3e63a2b 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt @@ -131,6 +131,7 @@ class ReactNativeIdlingResources( loopers.values.forEach { IdlingRegistry.getInstance().unregisterLooperAsIdlingResource(it) } + loopers.clear() } private fun unregisterIdlingResources() { diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt index 755375563d..0178521406 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt @@ -5,11 +5,7 @@ import android.view.Choreographer import androidx.test.espresso.IdlingResource.ResourceCallback import com.facebook.react.animated.NativeAnimatedModule import com.facebook.react.bridge.ReactContext -import com.wix.detox.espresso.idlingresources.DescriptiveIdlingResource -import com.wix.detox.reactnative.ReactNativeInfo.rnVersion import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource -import org.joor.Reflect -import org.joor.ReflectException /** * Created by simonracz on 25/08/2017. @@ -43,8 +39,8 @@ class AnimatedModuleIdlingResource(private val reactContext: ReactContext) : Det val hasAnimations = animatedModule?.nodesManager?.hasActiveAnimations() ?: false if (hasAnimations) { - Log.i(LOG_TAG, "AnimatedModule is busy."); - Choreographer.getInstance().postFrameCallback(this); + Log.i(LOG_TAG, "AnimatedModule is busy.") + Choreographer.getInstance().postFrameCallback(this) return false } From 28209004432f53f531cd4938eed77e233d1b5443 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Sun, 22 Dec 2024 13:35:06 +0200 Subject: [PATCH 07/14] Fixes after pr --- .../detox/reactnative/ReactNativeExtension.kt | 16 +++----- .../ReactNativeIdlingResources.kt | 39 ++----------------- .../factory/DetoxIdlingResourceFactory.kt | 23 +++++++---- 3 files changed, 24 insertions(+), 54 deletions(-) diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt index 7b7281a365..f3fbf37358 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt @@ -57,7 +57,6 @@ object ReactNativeExtension { Log.i(LOG_TAG, "Reloading React Native") (applicationContext as ReactApplication).let { - val networkSyncEnabled = rnIdlingResources?.networkSyncEnabled ?: true clearIdlingResources() val previousReactContext = getCurrentReactContextSafe(it) @@ -65,7 +64,7 @@ object ReactNativeExtension { reloadReactNativeInBackground(it) val reactContext = awaitNewReactNativeContext(it, previousReactContext) - enableOrDisableSynchronization(reactContext, networkSyncEnabled) + enableOrDisableSynchronization(reactContext) } } @@ -94,11 +93,6 @@ object ReactNativeExtension { return null } - @JvmStatic - fun setNetworkSynchronization(enable: Boolean) { - rnIdlingResources?.setNetworkSynchronization(enable) - } - @JvmStatic fun toggleNetworkSynchronization(enable: Boolean) { rnIdlingResources?.let { @@ -130,11 +124,11 @@ object ReactNativeExtension { return rnLoadingMonitor.getNewContext()!! } - private fun enableOrDisableSynchronization(reactContext: ReactContext, networkSyncEnabled: Boolean = true) { + private fun enableOrDisableSynchronization(reactContext: ReactContext) { if (shouldDisableSynchronization()) { clearAllSynchronization() } else { - setupIdlingResources(reactContext, networkSyncEnabled) + setupIdlingResources(reactContext) } } @@ -143,10 +137,10 @@ object ReactNativeExtension { return launchArgs.hasEnableSynchronization() && launchArgs.enableSynchronization.equals("0") } - private fun setupIdlingResources(reactContext: ReactContext, networkSyncEnabled: Boolean = true) { + private fun setupIdlingResources(reactContext: ReactContext) { val launchArgs = LaunchArgs() - rnIdlingResources = ReactNativeIdlingResources(reactContext, launchArgs, networkSyncEnabled).apply { + rnIdlingResources = ReactNativeIdlingResources(reactContext, launchArgs).apply { registerAll() } } diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt index cfd3e63a2b..83713de1b0 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt @@ -19,7 +19,6 @@ import org.joor.Reflect class ReactNativeIdlingResources( private val reactContext: ReactContext, private var launchArgs: LaunchArgs, - internal var networkSyncEnabled: Boolean = true, private val idlingResourcesFactory: DetoxIdlingResourceFactory = DetoxIdlingResourceFactory(reactContext) ) { companion object { @@ -47,26 +46,9 @@ class ReactNativeIdlingResources( unregisterIdlingResources() } - fun setNetworkSynchronization(enable: Boolean) { - runBlocking { - if (networkSyncEnabled == enable) { - return@runBlocking - } - - if (enable) { - setupIdlingResource(IdlingResourcesName.Network) - } else { - removeIdlingResource(IdlingResourcesName.Network) - } - networkSyncEnabled = enable - } - } - fun pauseNetworkSynchronization() = pauseIdlingResource(IdlingResourcesName.Network) fun resumeNetworkSynchronization() { - if (networkSyncEnabled) { - resumeIdlingResource(IdlingResourcesName.Network) - } + resumeIdlingResource(IdlingResourcesName.Network) } fun pauseRNTimersIdlingResource() = pauseIdlingResource(IdlingResourcesName.Timers) @@ -109,14 +91,10 @@ class ReactNativeIdlingResources( } private suspend fun setupIdlingResources() { - setupIdlingResource(IdlingResourcesName.RNBridge) - setupIdlingResource(IdlingResourcesName.Timers) - setupIdlingResource(IdlingResourcesName.UIModule) - setupIdlingResource(IdlingResourcesName.Animations) - if (networkSyncEnabled) { - setupIdlingResource(IdlingResourcesName.Network) + idlingResources.putAll(idlingResourcesFactory.create()) + idlingResources.forEach { (_, idlingResource) -> + IdlingRegistry.getInstance().register(idlingResource) } - setupIdlingResource(IdlingResourcesName.AsyncStorage) } private fun syncIdlingResources() { @@ -150,15 +128,6 @@ class ReactNativeIdlingResources( idlingResource?.resume() } - private suspend fun setupIdlingResource(idlingResourcesName: IdlingResourcesName) { - val idlingResource = idlingResourcesFactory.create(idlingResourcesName) - - idlingResource?.let { - IdlingRegistry.getInstance().register(it) - idlingResources[idlingResourcesName] = it - } - } - private fun removeIdlingResource(idlingResourcesName: IdlingResourcesName) { val idlingResource = idlingResources[idlingResourcesName] idlingResource?.let { diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/DetoxIdlingResourceFactory.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/DetoxIdlingResourceFactory.kt index 438484dc9d..3fa08e2f13 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/DetoxIdlingResourceFactory.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/DetoxIdlingResourceFactory.kt @@ -12,14 +12,21 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class DetoxIdlingResourceFactory(private val reactContext: ReactContext) { - suspend fun create(name: IdlingResourcesName): DetoxIdlingResource? = withContext(Dispatchers.Main) { - return@withContext when (name) { - IdlingResourcesName.Timers -> TimersIdlingResource(reactContext) - IdlingResourcesName.AsyncStorage -> AsyncStorageIdlingResource.createIfNeeded(reactContext) - IdlingResourcesName.RNBridge -> BridgeIdlingResource(reactContext) - IdlingResourcesName.UIModule -> UIModuleIdlingResource(reactContext) - IdlingResourcesName.Animations -> AnimatedModuleIdlingResource(reactContext) - IdlingResourcesName.Network -> NetworkIdlingResource(reactContext) + suspend fun create(): Map = withContext(Dispatchers.Main) { + val result = mutableMapOf( + IdlingResourcesName.Timers to TimersIdlingResource(reactContext), + IdlingResourcesName.RNBridge to BridgeIdlingResource(reactContext), + IdlingResourcesName.UIModule to UIModuleIdlingResource(reactContext), + IdlingResourcesName.Animations to AnimatedModuleIdlingResource(reactContext), + IdlingResourcesName.Network to NetworkIdlingResource(reactContext) + ) + + val asyncStorageIdlingResource = AsyncStorageIdlingResource.createIfNeeded(reactContext) + if (asyncStorageIdlingResource != null) { + result[IdlingResourcesName.AsyncStorage] = asyncStorageIdlingResource } + + return@withContext result } } + From b2fdb95d444d889f3e7a504f438cb21cd4d79ee8 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Mon, 23 Dec 2024 13:26:06 +0200 Subject: [PATCH 08/14] Fixed tests --- detox/test/src/Screens/StressScreen.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/detox/test/src/Screens/StressScreen.js b/detox/test/src/Screens/StressScreen.js index d27be04217..086c3d136c 100644 --- a/detox/test/src/Screens/StressScreen.js +++ b/detox/test/src/Screens/StressScreen.js @@ -143,11 +143,9 @@ export default class StressScreen extends Component { async storageStressButtonPressed() { try { - await NativeModule.toggleNonStorageSynchronization(false); await AsyncStorage.clear(); await storageHelper.runStressTest(); } finally { - await NativeModule.toggleNonStorageSynchronization(true); await AsyncStorage.clear(); } From 33d2a5fed1aa2ac3677d20add9888e23baca68d7 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Mon, 23 Dec 2024 14:18:28 +0200 Subject: [PATCH 09/14] More OOP in idling resources --- .../idlingresources/DetoxIdlingResource.kt | 13 ++++++++++++- .../animations/AnimatedModuleIdlingResource.kt | 11 ++--------- .../idlingresources/bridge/BridgeIdlingResource.kt | 11 ----------- .../network/NetworkIdlingResource.kt | 12 ++---------- .../storage/AsyncStorageIdlingResource.kt | 11 ----------- .../idlingresources/timers/TimersIdlingResource.kt | 12 ++---------- .../uimodule/UIModuleIdlingResource.kt | 11 ++--------- 7 files changed, 20 insertions(+), 61 deletions(-) diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/DetoxIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/DetoxIdlingResource.kt index cbd1995631..0af43dfd5c 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/DetoxIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/DetoxIdlingResource.kt @@ -1,9 +1,11 @@ package com.wix.detox.reactnative.idlingresources +import androidx.test.espresso.IdlingResource import com.wix.detox.espresso.idlingresources.DescriptiveIdlingResource import java.util.concurrent.atomic.AtomicBoolean abstract class DetoxIdlingResource : DescriptiveIdlingResource { + private var callback: IdlingResource.ResourceCallback? = null private var paused: AtomicBoolean = AtomicBoolean(false) fun pause() { @@ -15,6 +17,11 @@ abstract class DetoxIdlingResource : DescriptiveIdlingResource { paused.set(false) } + + override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { + this.callback = callback + } + final override fun isIdleNow(): Boolean { if (paused.get()) { return true @@ -27,5 +34,9 @@ abstract class DetoxIdlingResource : DescriptiveIdlingResource { } protected abstract fun checkIdle(): Boolean - protected abstract fun notifyIdle() + + + fun notifyIdle() { + callback?.onTransitionToIdle() + } } diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt index 0178521406..18c0ab5965 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt @@ -20,7 +20,6 @@ import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource */ class AnimatedModuleIdlingResource(private val reactContext: ReactContext) : DetoxIdlingResource(), Choreographer.FrameCallback { - private var callback: ResourceCallback? = null override fun getName(): String { return AnimatedModuleIdlingResource::class.java.name @@ -48,14 +47,8 @@ class AnimatedModuleIdlingResource(private val reactContext: ReactContext) : Det return true } - override fun notifyIdle() { - callback?.onTransitionToIdle() - } - - - override fun registerIdleTransitionCallback(callback: ResourceCallback) { - this.callback = callback - + override fun registerIdleTransitionCallback(callback: ResourceCallback?) { + super.registerIdleTransitionCallback(callback) Choreographer.getInstance().postFrameCallback(this) } diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/bridge/BridgeIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/bridge/BridgeIdlingResource.kt index 0e6fe7ba08..2e3ec8b111 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/bridge/BridgeIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/bridge/BridgeIdlingResource.kt @@ -1,7 +1,6 @@ package com.wix.detox.reactnative.idlingresources.bridge import android.util.Log -import androidx.test.espresso.IdlingResource.ResourceCallback import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener import com.facebook.react.bridge.ReactContext import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource @@ -20,7 +19,6 @@ import java.util.concurrent.atomic.AtomicBoolean class BridgeIdlingResource(private val reactContext: ReactContext) : DetoxIdlingResource(), NotThreadSafeBridgeIdleDebugListener { private val idleNow = AtomicBoolean(true) - private var callback: ResourceCallback? = null init { reactContext.catalystInstance.addBridgeIdleDebugListener(this) @@ -50,10 +48,6 @@ class BridgeIdlingResource(private val reactContext: ReactContext) : DetoxIdling return ret } - override fun registerIdleTransitionCallback(callback: ResourceCallback) { - this.callback = callback - } - override fun onTransitionToBridgeIdle() { idleNow.set(true) notifyIdle() @@ -67,11 +61,6 @@ class BridgeIdlingResource(private val reactContext: ReactContext) : DetoxIdling override fun onBridgeDestroyed() { } - override fun notifyIdle() { - // Log.i(LOG_TAG, "JS Bridge transitions to idle."); - callback?.onTransitionToIdle() - } - override fun onUnregistered() { super.onUnregistered() onDetach() diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkIdlingResource.kt index 28e99c0021..140c4c0d7f 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkIdlingResource.kt @@ -5,7 +5,6 @@ import android.view.Choreographer import androidx.test.espresso.IdlingResource.ResourceCallback import com.facebook.react.bridge.ReactContext import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource -import com.wix.detox.reactnative.idlingresources.network.NetworkIdlingResource import okhttp3.Dispatcher import java.util.regex.Pattern import java.util.regex.PatternSyntaxException @@ -20,7 +19,6 @@ import java.util.regex.PatternSyntaxException */ class NetworkIdlingResource(private val dispatcher: Dispatcher) : DetoxIdlingResource(), Choreographer.FrameCallback { - private var callback: ResourceCallback? = null private val busyResources: MutableSet = HashSet() constructor(reactContext: ReactContext) : this(NetworkingModuleReflected(reactContext).getHttpClient()!!.dispatcher) @@ -36,8 +34,8 @@ class NetworkIdlingResource(private val dispatcher: Dispatcher) : DetoxIdlingRes @Synchronized override fun getBusyHint(): Map = mapOf("urls" to ArrayList(busyResources)) - override fun registerIdleTransitionCallback(callback: ResourceCallback) { - this.callback = callback + override fun registerIdleTransitionCallback(callback: ResourceCallback?) { + super.registerIdleTransitionCallback(callback) Choreographer.getInstance().postFrameCallback(this) } @@ -68,12 +66,6 @@ class NetworkIdlingResource(private val dispatcher: Dispatcher) : DetoxIdlingRes return true } - override fun notifyIdle() { - if (callback != null) { - callback!!.onTransitionToIdle() - } - } - private fun isUrlBlacklisted(url: String): Boolean { for (pattern in blacklist) { if (pattern.matcher(url).matches()) { diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/AsyncStorageIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/AsyncStorageIdlingResource.kt index e398325d31..cca77b17f7 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/AsyncStorageIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/AsyncStorageIdlingResource.kt @@ -1,10 +1,8 @@ package com.wix.detox.reactnative.idlingresources.storage import android.util.Log -import androidx.test.espresso.IdlingResource import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactContext -import com.wix.detox.espresso.idlingresources.DescriptiveIdlingResource import com.wix.detox.reactnative.helpers.RNHelpers import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource import org.joor.Reflect @@ -38,7 +36,6 @@ class AsyncStorageIdlingResource get() = LOG_TAG private val moduleReflected = ModuleReflected(module, sexecutorReflectedGenFn) - private var callback: IdlingResource.ResourceCallback? = null private var idleCheckTask: Runnable? = null private val idleCheckTaskImpl = Runnable { with(moduleReflected.sexecutor) { @@ -57,10 +54,6 @@ class AsyncStorageIdlingResource override fun getDebugName() = "io" override fun getBusyHint(): Map? = null - override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { - this.callback = callback - enqueueIdleCheckTask() - } private fun checkIdleInternal(): Boolean = with(moduleReflected.sexecutor) { @@ -77,10 +70,6 @@ class AsyncStorageIdlingResource } } - override fun notifyIdle() { - callback?.onTransitionToIdle() - } - private fun enqueueIdleCheckTask() = with(moduleReflected.sexecutor) { synchronized(executor()) { diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResource.kt index 9d106b5dea..bd8c18e68a 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/TimersIdlingResource.kt @@ -5,7 +5,6 @@ import android.view.Choreographer import androidx.test.espresso.IdlingResource import com.facebook.react.bridge.ReactContext import com.facebook.react.modules.core.TimingModule -import com.facebook.react.modules.network.NetworkingModule import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource private const val BUSY_WINDOW_THRESHOLD = 1500L @@ -15,7 +14,6 @@ class TimersIdlingResource @JvmOverloads constructor( private val getChoreographer: () -> Choreographer = { Choreographer.getInstance() } ) : DetoxIdlingResource(), Choreographer.FrameCallback { - private var callback: IdlingResource.ResourceCallback? = null private val timingModule: TimingModule = reactContext.getNativeModule(TimingModule::class.java)!! override fun getName(): String = this.javaClass.name @@ -23,7 +21,7 @@ class TimersIdlingResource @JvmOverloads constructor( override fun getBusyHint(): Map? = null override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { - this.callback = callback + super.registerIdleTransitionCallback(callback) getChoreographer().postFrameCallback(this) } @@ -41,13 +39,7 @@ class TimersIdlingResource @JvmOverloads constructor( } override fun doFrame(frameTimeNanos: Long) { - callback?.let { - isIdleNow - } - } - - override fun notifyIdle() { - callback?.onTransitionToIdle() + isIdleNow } companion object { diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIModuleIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIModuleIdlingResource.kt index 1eb3894038..2d36e31192 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIModuleIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/UIModuleIdlingResource.kt @@ -16,7 +16,6 @@ class UIModuleIdlingResource(private val reactContext: ReactContext) : DetoxIdlingResource(), Choreographer.FrameCallback { private val uiManagerModuleReflected = UIManagerModuleReflected(reactContext) - private var callback: ResourceCallback? = null override fun getName(): String = UIModuleIdlingResource::class.java.name override fun getDebugName(): String = " ui" @@ -55,8 +54,8 @@ class UIModuleIdlingResource(private val reactContext: ReactContext) return true } - override fun registerIdleTransitionCallback(callback: ResourceCallback) { - this.callback = callback + override fun registerIdleTransitionCallback(callback: ResourceCallback?) { + super.registerIdleTransitionCallback(callback) Choreographer.getInstance().postFrameCallback(this) } @@ -64,12 +63,6 @@ class UIModuleIdlingResource(private val reactContext: ReactContext) isIdleNow } - override fun notifyIdle() { - callback?.run { - onTransitionToIdle() - } - } - companion object { private const val LOG_TAG = "Detox" } From c2cc48c41a81dca311d00dc359105a3916f485a7 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Mon, 23 Dec 2024 14:19:33 +0200 Subject: [PATCH 10/14] Enable storage stress test --- detox/test/e2e/07.stress-tests.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detox/test/e2e/07.stress-tests.test.js b/detox/test/e2e/07.stress-tests.test.js index 53cea2b040..3408963e12 100644 --- a/detox/test/e2e/07.stress-tests.test.js +++ b/detox/test/e2e/07.stress-tests.test.js @@ -40,7 +40,7 @@ describe('StressTests', () => { } }); - it.skip(':android: should handle tap during storage stress', async () => { + it(':android: should handle tap during storage stress', async () => { try { await element(by.text('Storage Stress')).tap(); await expect(element(by.text('StorageStress'))).toBeVisible(); From f43975a1939f3b62d99fd6cc48b6e16d90d1afc3 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Mon, 23 Dec 2024 14:44:21 +0200 Subject: [PATCH 11/14] Fixed Android unit tests --- .../storage/AsyncStorageIdlingResource.kt | 6 + .../AsyncStorageIdlingResourceSpec.kt | 229 ---------------- .../AsyncStorageIdlingResourceTest.kt | 248 ++++++++++++++++++ 3 files changed, 254 insertions(+), 229 deletions(-) delete mode 100644 detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResourceSpec.kt create mode 100644 detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResourceTest.kt diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/AsyncStorageIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/AsyncStorageIdlingResource.kt index cca77b17f7..6203bb4da5 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/AsyncStorageIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/AsyncStorageIdlingResource.kt @@ -1,6 +1,7 @@ package com.wix.detox.reactnative.idlingresources.storage import android.util.Log +import androidx.test.espresso.IdlingResource import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactContext import com.wix.detox.reactnative.helpers.RNHelpers @@ -62,6 +63,11 @@ class AsyncStorageIdlingResource } } + override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { + super.registerIdleTransitionCallback(callback) + enqueueIdleCheckTask() + } + override fun checkIdle(): Boolean = checkIdleInternal().also { idle -> if (!idle) { diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResourceSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResourceSpec.kt deleted file mode 100644 index 02362e7f35..0000000000 --- a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResourceSpec.kt +++ /dev/null @@ -1,229 +0,0 @@ -package com.wix.detox.reactnative.idlingresources - -import androidx.test.espresso.IdlingResource -import com.facebook.react.bridge.NativeModule -import com.wix.detox.UTHelpers.yieldToOtherThreads -import com.wix.detox.reactnative.idlingresources.storage.AsyncStorageIdlingResource -import com.wix.detox.reactnative.idlingresources.storage.SerialExecutorReflected -import org.assertj.core.api.Assertions.assertThat -import org.mockito.kotlin.* -import org.spekframework.spek2.Spek -import org.spekframework.spek2.style.specification.describe -import java.util.concurrent.Executor -import java.util.concurrent.Executors - -private class AsyncStorageModuleStub: NativeModule { - val executor: Executor = mock(name = "native-module's executor") - override fun getName() = "stub" - override fun initialize() {} - override fun canOverrideExistingModule() = false - override fun onCatalystInstanceDestroy() {} - override fun invalidate() {} -} - -class AsyncStorageIdlingResourceSpec: Spek({ - describe("React Native Async-Storage idling-resource") { - lateinit var sexecutor: Executor - lateinit var sexecutorReflected: SerialExecutorReflected - lateinit var sexecutorReflectedGenFn: (executor: Executor) -> SerialExecutorReflected - lateinit var module: AsyncStorageModuleStub - lateinit var uut: AsyncStorageIdlingResource - - beforeEachTest { - sexecutor = mock() - module = AsyncStorageModuleStub() - sexecutorReflected = mock() { - on { executor() }.thenReturn(sexecutor) - } - sexecutorReflectedGenFn = mock() { - on { invoke(eq(module.executor)) }.thenReturn(sexecutorReflected) - } - - - uut = AsyncStorageIdlingResource(module, sexecutorReflectedGenFn) - } - - fun givenNoActiveTasks() = whenever(sexecutorReflected.hasActiveTask()).thenReturn(false) - fun givenAnActiveTask() = whenever(sexecutorReflected.hasActiveTask()).thenReturn(true) - fun givenNoPendingTasks() = whenever(sexecutorReflected.hasPendingTasks()).thenReturn(false) - fun givenPendingTasks() = whenever(sexecutorReflected.hasPendingTasks()).thenReturn(true) - fun givenIdleSExecutor() { - givenNoActiveTasks() - givenNoPendingTasks() - } - fun givenBusySExecutor() { - givenAnActiveTask() - givenNoPendingTasks() - } - fun verifyNoTasksEnqueued() = verify(sexecutorReflected, never()).executeTask(any()) - fun verifyTaskEnqueuedOnce() = verify(sexecutorReflected, times(1)).executeTask(any()) - fun verifyTaskEnqueuedTwice() = verify(sexecutorReflected, times(2)).executeTask(any()) - - it("should have a name") { - assertThat(uut.name).isEqualTo("com.wix.detox.reactnative.idlingresources.storage.AsyncStorageIdlingResource") - } - - it("should have a debug-name") { - assertThat(uut.getDebugName()).isEqualTo("io") - } - - describe("idle-checking") { - it("should be idle") { - givenIdleSExecutor() - assertThat(uut.isIdleNow).isTrue() - } - - it("should be busy if executor is executing") { - givenAnActiveTask() - givenNoPendingTasks() - assertThat(uut.isIdleNow).isFalse() - } - - it("should be busy if executor has pending tasks") { - givenNoActiveTasks() - givenPendingTasks() - assertThat(uut.isIdleNow).isFalse() - } - - it("should be synchronized over actual executor") { - val localExecutor = Executors.newSingleThreadExecutor() - var isIdle: Boolean? = null - synchronized(sexecutor) { - localExecutor.submit { - isIdle = uut.isIdleNow - } - yieldToOtherThreads(localExecutor) - assertThat(isIdle).isNull() - } - yieldToOtherThreads(localExecutor) - } - - it("should enqueue an idle-check task if resource is busy") { - givenBusySExecutor() - uut.isIdleNow - verifyTaskEnqueuedOnce() - } - - it("should not enqueue an idle-check task if resource if idle") { - givenIdleSExecutor() - uut.isIdleNow - verifyNoTasksEnqueued() - } - - it("should not enqueue more than one idle-check task") { - givenBusySExecutor() - - repeat(2) { - uut.isIdleNow - } - verifyTaskEnqueuedOnce() - } - } - - describe("callback registration") { - lateinit var callback: IdlingResource.ResourceCallback - - beforeEachTest { - callback = mock() - } - - fun verifyTransitionToIdleCalled() = verify(callback).onTransitionToIdle() - fun verifyTransitionToIdleNotCalled() = verify(callback, never()).onTransitionToIdle() - - it("should enqueue an idle-check task") { - uut.registerIdleTransitionCallback(callback) - verifyTaskEnqueuedOnce() - } - - it("should be synchronized over actual executor") { - val localExecutor = Executors.newSingleThreadExecutor() - - synchronized(sexecutor) { - localExecutor.submit { - uut.registerIdleTransitionCallback(callback) - } - yieldToOtherThreads(localExecutor) - verifyNoTasksEnqueued() - } - yieldToOtherThreads(localExecutor) - } - - describe("the idle-check task") { - fun executeIdleCheckTask() { - argumentCaptor().also { - verify(sexecutorReflected).executeTask(it.capture()) - }.firstValue.run() - } - - it("should transition to idle") { - givenIdleSExecutor() - - uut.registerIdleTransitionCallback(callback) - executeIdleCheckTask() - - verifyTransitionToIdleCalled() - } - - it("should not transition to idle if busy") { - givenAnActiveTask() - givenPendingTasks() - - uut.registerIdleTransitionCallback(callback) - executeIdleCheckTask() - - verifyTransitionToIdleNotCalled() - } - - it("should not inspect sexecutor for activity, because it runs on the executor itself") { - givenAnActiveTask() - givenPendingTasks() - - uut.registerIdleTransitionCallback(callback) - executeIdleCheckTask() - - verify(sexecutorReflected, never()).hasActiveTask() - } - - it("should reenqueue if still busy") { - givenAnActiveTask() - givenPendingTasks() - - uut.registerIdleTransitionCallback(callback) - executeIdleCheckTask() - - verifyTaskEnqueuedTwice() - } - - it("should be synchronized") { - val localExecutor = Executors.newSingleThreadExecutor() - - givenIdleSExecutor() - uut.registerIdleTransitionCallback(callback) - - synchronized(sexecutor) { - localExecutor.submit { - executeIdleCheckTask() - } - yieldToOtherThreads(localExecutor) - verifyTransitionToIdleNotCalled() - } - yieldToOtherThreads(localExecutor) - } - - describe("vs. immediate idle-check") { - it("should allow for an enqueuing of more tasks after idle-transition") { - uut.registerIdleTransitionCallback(callback) - - givenIdleSExecutor() - executeIdleCheckTask() - - givenBusySExecutor() - uut.isIdleNow - - verifyTaskEnqueuedTwice() - } - } - } - } - } -}) diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResourceTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResourceTest.kt new file mode 100644 index 0000000000..f6ad479e64 --- /dev/null +++ b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/AsyncStorageIdlingResourceTest.kt @@ -0,0 +1,248 @@ +package com.wix.detox.reactnative.idlingresources + +import androidx.test.espresso.IdlingResource +import com.facebook.react.bridge.NativeModule +import com.wix.detox.UTHelpers.yieldToOtherThreads +import com.wix.detox.reactnative.idlingresources.storage.AsyncStorageIdlingResource +import com.wix.detox.reactnative.idlingresources.storage.SerialExecutorReflected +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import java.util.concurrent.Executor +import java.util.concurrent.Executors + +private class AsyncStorageModuleStub : NativeModule { + val executor: Executor = mock(name = "native-module's executor") + override fun getName() = "stub" + override fun initialize() {} + override fun canOverrideExistingModule() = false + override fun onCatalystInstanceDestroy() {} + override fun invalidate() {} +} + +@RunWith(RobolectricTestRunner::class) +class AsyncStorageIdlingResourceTest { + private lateinit var sexecutor: Executor + private lateinit var sexecutorReflected: SerialExecutorReflected + private lateinit var sexecutorReflectedGenFn: (executor: Executor) -> SerialExecutorReflected + private lateinit var module: AsyncStorageModuleStub + private lateinit var uut: AsyncStorageIdlingResource + + @Before + fun setup() { + sexecutor = mock() + module = AsyncStorageModuleStub() + sexecutorReflected = mock() { + on { executor() }.thenReturn(sexecutor) + } + sexecutorReflectedGenFn = mock() { + on { invoke(eq(module.executor)) }.thenReturn(sexecutorReflected) + } + + + uut = AsyncStorageIdlingResource(module, sexecutorReflectedGenFn) + } + + fun givenNoActiveTasks() = whenever(sexecutorReflected.hasActiveTask()).thenReturn(false) + fun givenAnActiveTask() = whenever(sexecutorReflected.hasActiveTask()).thenReturn(true) + fun givenNoPendingTasks() = whenever(sexecutorReflected.hasPendingTasks()).thenReturn(false) + fun givenPendingTasks() = whenever(sexecutorReflected.hasPendingTasks()).thenReturn(true) + fun givenIdleSExecutor() { + givenNoActiveTasks() + givenNoPendingTasks() + } + + fun givenBusySExecutor() { + givenAnActiveTask() + givenNoPendingTasks() + } + + fun verifyNoTasksEnqueued() = verify(sexecutorReflected, never()).executeTask(any()) + fun verifyTaskEnqueuedOnce() = verify(sexecutorReflected, times(1)).executeTask(any()) + fun verifyTaskEnqueuedTwice() = verify(sexecutorReflected, times(2)).executeTask(any()) + + @Test + fun `should have a name`() { + assertThat(uut.name).isEqualTo("com.wix.detox.reactnative.idlingresources.storage.AsyncStorageIdlingResource") + } + + @Test + fun `should have a debug-name`() { + assertThat(uut.getDebugName()).isEqualTo("io") + } + + @Test + fun `should be idle`() { + givenIdleSExecutor() + assertThat(uut.isIdleNow).isTrue() + } + + @Test + fun `should be busy if executor is executing`() { + givenAnActiveTask() + givenNoPendingTasks() + assertThat(uut.isIdleNow).isFalse() + } + + @Test + fun `should be busy if executor has pending tasks`() { + givenNoActiveTasks() + givenPendingTasks() + assertThat(uut.isIdleNow).isFalse() + } + + @Test + fun `should be synchronized over actual executor`() { + val localExecutor = Executors.newSingleThreadExecutor() + var isIdle: Boolean? = null + synchronized(sexecutor) { + localExecutor.submit { + isIdle = uut.isIdleNow + } + yieldToOtherThreads(localExecutor) + assertThat(isIdle).isNull() + } + yieldToOtherThreads(localExecutor) + } + + @Test + fun `should enqueue an idle-check task if resource is busy`() { + givenBusySExecutor() + uut.isIdleNow + verifyTaskEnqueuedOnce() + } + + @Test + fun `should not enqueue an idle-check task if resource if idle`() { + givenIdleSExecutor() + uut.isIdleNow + verifyNoTasksEnqueued() + } + + @Test + fun `should not enqueue more than one idle-check task`() { + givenBusySExecutor() + + repeat(2) { + uut.isIdleNow + } + verifyTaskEnqueuedOnce() + } + + private val callback: IdlingResource.ResourceCallback = mock() + + + fun verifyTransitionToIdleCalled() = verify(callback).onTransitionToIdle() + fun verifyTransitionToIdleNotCalled() = verify(callback, never()).onTransitionToIdle() + + @Test + fun `should enqueue an idle-check task`() { + uut.registerIdleTransitionCallback(callback) + verifyTaskEnqueuedOnce() + } + + @Test + fun `callback registration - should be synchronized over actual executor`() { + val localExecutor = Executors.newSingleThreadExecutor() + + synchronized(sexecutor) { + localExecutor.submit { + uut.registerIdleTransitionCallback(callback) + } + yieldToOtherThreads(localExecutor) + verifyNoTasksEnqueued() + } + yieldToOtherThreads(localExecutor) + } + + + fun executeIdleCheckTask() { + argumentCaptor().also { + verify(sexecutorReflected).executeTask(it.capture()) + }.firstValue.run() + } + + @Test + fun `should transition to idle`() { + givenIdleSExecutor() + + uut.registerIdleTransitionCallback(callback) + executeIdleCheckTask() + + verifyTransitionToIdleCalled() + } + + @Test + fun `should not transition to idle if busy`() { + givenAnActiveTask() + givenPendingTasks() + + uut.registerIdleTransitionCallback(callback) + executeIdleCheckTask() + + verifyTransitionToIdleNotCalled() + } + + @Test + fun `should not inspect sexecutor for activity, because it runs on the executor itself`() { + givenAnActiveTask() + givenPendingTasks() + + uut.registerIdleTransitionCallback(callback) + executeIdleCheckTask() + + verify(sexecutorReflected, never()).hasActiveTask() + } + + @Test + fun `should reenqueue if still busy`() { + givenAnActiveTask() + givenPendingTasks() + + uut.registerIdleTransitionCallback(callback) + executeIdleCheckTask() + + verifyTaskEnqueuedTwice() + } + + @Test + fun `should be synchronized`() { + val localExecutor = Executors.newSingleThreadExecutor() + + givenIdleSExecutor() + uut.registerIdleTransitionCallback(callback) + + synchronized(sexecutor) { + localExecutor.submit { + executeIdleCheckTask() + } + yieldToOtherThreads(localExecutor) + verifyTransitionToIdleNotCalled() + } + yieldToOtherThreads(localExecutor) + } + + + @Test + fun `should allow for an enqueuing of more tasks after idle-transition`() { + uut.registerIdleTransitionCallback(callback) + + givenIdleSExecutor() + executeIdleCheckTask() + + givenBusySExecutor() + uut.isIdleNow + + verifyTaskEnqueuedTwice() + } +} From fcbfc62e52304b8a469ce3ce051e2ccf5823111c Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Tue, 24 Dec 2024 15:05:17 +0200 Subject: [PATCH 12/14] Update detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt Co-authored-by: d4vidi --- .../animations/AnimatedModuleIdlingResource.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt index 18c0ab5965..34059a5216 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt @@ -21,9 +21,7 @@ import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource class AnimatedModuleIdlingResource(private val reactContext: ReactContext) : DetoxIdlingResource(), Choreographer.FrameCallback { - override fun getName(): String { - return AnimatedModuleIdlingResource::class.java.name - } + override fun getName() = AnimatedModuleIdlingResource::class.java.name override fun getDebugName(): String { return "AnimatedModule" From f3bb193f32e87c34572af0f54dfcf82ea6223cd3 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Tue, 24 Dec 2024 15:52:35 +0200 Subject: [PATCH 13/14] Fixes after pr --- .../full/java/com/wix/detox/espresso/EspressoDetox.java | 2 +- .../idlingresources/ReactNativeIdlingResources.kt | 5 ++--- .../animations/AnimatedModuleIdlingResource.kt | 7 +++---- .../idlingresources/network/NetworkIdlingResource.kt | 1 + 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/EspressoDetox.java b/detox/android/detox/src/full/java/com/wix/detox/espresso/EspressoDetox.java index 89ae8c1976..45e6d9b930 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/EspressoDetox.java +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/EspressoDetox.java @@ -120,7 +120,7 @@ public static void setURLBlacklist(final ArrayList urls) { UIThread.postSync(new Runnable() { @Override public void run() { - NetworkIdlingResource.Companion.setURLBlacklist(urls); + NetworkIdlingResource.setURLBlacklist(urls); } }); } diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt index 83713de1b0..48335c0ea7 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt @@ -16,14 +16,13 @@ import kotlinx.coroutines.runBlocking import org.joor.Reflect +private const val LOG_TAG = "DetoxRNIdleRes" + class ReactNativeIdlingResources( private val reactContext: ReactContext, private var launchArgs: LaunchArgs, private val idlingResourcesFactory: DetoxIdlingResourceFactory = DetoxIdlingResourceFactory(reactContext) ) { - companion object { - const val LOG_TAG = "DetoxRNIdleRes" - } private val idlingResources = mutableMapOf() private val loopers = mutableMapOf() diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt index 34059a5216..0ad907ce5a 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt @@ -7,6 +7,9 @@ import com.facebook.react.animated.NativeAnimatedModule import com.facebook.react.bridge.ReactContext import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource +private const val LOG_TAG = "Detox" + + /** * Created by simonracz on 25/08/2017. */ @@ -53,10 +56,6 @@ class AnimatedModuleIdlingResource(private val reactContext: ReactContext) : Det override fun doFrame(frameTimeNanos: Long) { isIdleNow } - - companion object { - private const val LOG_TAG = "Detox" - } } diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkIdlingResource.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkIdlingResource.kt index 140c4c0d7f..69a20c2c4c 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkIdlingResource.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkIdlingResource.kt @@ -85,6 +85,7 @@ class NetworkIdlingResource(private val dispatcher: Dispatcher) : DetoxIdlingRes * * @param urls list of regexes of blacklisted urls */ + @JvmStatic fun setURLBlacklist(urls: List?) { blacklist.clear() if (urls == null) return From c0e1723ae9decda8c2a9abce27540e1d6089df66 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Tue, 24 Dec 2024 16:05:24 +0200 Subject: [PATCH 14/14] Fixed build --- .../reactnative/idlingresources/looper/MQThreadsReflector.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/looper/MQThreadsReflector.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/looper/MQThreadsReflector.kt index f3762bf84a..54d332ac9e 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/looper/MQThreadsReflector.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/looper/MQThreadsReflector.kt @@ -3,11 +3,11 @@ package com.wix.detox.reactnative.idlingresources.looper import android.os.Looper import android.util.Log import com.facebook.react.bridge.ReactContext -import com.wix.detox.reactnative.idlingresources.ReactNativeIdlingResources.Companion.LOG_TAG import org.joor.Reflect import org.joor.ReflectException +private const val LOG_TAG = "DetoxRNIdleRes" private const val METHOD_GET_LOOPER = "getLooper" private const val FIELD_NATIVE_MODULES_MSG_QUEUE = "mNativeModulesMessageQueueThread" private const val FIELD_JS_MSG_QUEUE = "mJSMessageQueueThread"