From 43a2d72dc970485ad5e37b2f875644d551bf54fe Mon Sep 17 00:00:00 2001 From: Eric Butler Date: Mon, 28 Feb 2022 09:41:28 -0500 Subject: [PATCH 01/15] Update mockito Fixes running tests on MacOS due to this bug: https://github.com/raphw/byte-buddy/issues/732 --- detox/android/detox/build.gradle | 2 +- .../adapters/server/DetoxActionHandlersSpec.kt | 2 +- .../server/QueryStatusActionHandlerSpec.kt | 6 ++++-- .../com/wix/detox/common/JsonConverterTest.kt | 1 - .../java/com/wix/detox/common/TapEventsSpec.kt | 2 +- .../wix/detox/common/collect/PairsIteratorSpec.kt | 1 - .../action/AdjustSliderToPositionActionTest.kt | 2 +- .../detox/espresso/action/DetoxMultiTapSpec.kt | 2 +- .../espresso/action/GetAttributesActionTest.kt | 8 ++++---- .../espresso/matcher/ViewAtIndexMatcherSpec.kt | 9 +++++---- .../espresso/registry/IRStatusInquirerTest.kt | 10 +++++----- .../wix/detox/espresso/scroll/DetoxSwipeSpec.kt | 4 +--- .../detox/espresso/scroll/FlinglessSwiperSpec.kt | 2 +- .../wix/detox/espresso/scroll/SwipeHelperSpec.kt | 15 ++++++++++----- .../com/wix/detox/espresso/utils/Vector2DSpec.kt | 6 ++++-- .../instruments/DetoxInstrumentsManagerSpec.kt | 2 +- .../AsyncStorageIdlingResourceSpec.kt | 2 +- .../SerialExecutorReflectedSpec.kt | 3 ++- .../DelegatedIdleInterrogationStrategySpec.kt | 2 +- .../timers/TimersIdlingResourceSpec.kt | 2 +- .../java/com/wix/invoke/JsonParserTest.java | 4 ++-- .../java/com/wix/invoke/MethodInvocationTest.java | 6 +++--- .../java/com/wix/invoke/types/InvocationTest.java | 3 ++- 23 files changed, 52 insertions(+), 44 deletions(-) diff --git a/detox/android/detox/build.gradle b/detox/android/detox/build.gradle index 9afd1abaac..0ff5f59411 100644 --- a/detox/android/detox/build.gradle +++ b/detox/android/detox/build.gradle @@ -131,7 +131,7 @@ dependencies { testImplementation 'org.assertj:assertj-core:3.16.1' testImplementation "org.jetbrains.kotlin:kotlin-test:$_kotlinVersion" testImplementation 'org.apache.commons:commons-io:1.3.2' - testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' + testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0' testImplementation 'org.robolectric:robolectric:4.3.1' } diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/adapters/server/DetoxActionHandlersSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/adapters/server/DetoxActionHandlersSpec.kt index 7dcb893d3d..0bd18e8eda 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/adapters/server/DetoxActionHandlersSpec.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/adapters/server/DetoxActionHandlersSpec.kt @@ -1,7 +1,6 @@ package com.wix.detox.adapters.server import android.content.Context -import com.nhaarman.mockitokotlin2.* import com.wix.detox.TestEngineFacade import com.wix.detox.UTHelpers.yieldToOtherThreads import com.wix.detox.instruments.DetoxInstrumentsException @@ -9,6 +8,7 @@ import com.wix.detox.instruments.DetoxInstrumentsManager import com.wix.invoke.MethodInvocation import org.assertj.core.api.Assertions import org.json.JSONObject +import org.mockito.kotlin.* import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe import java.lang.reflect.InvocationTargetException diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/adapters/server/QueryStatusActionHandlerSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/adapters/server/QueryStatusActionHandlerSpec.kt index 58f5c373c6..1299464e43 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/adapters/server/QueryStatusActionHandlerSpec.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/adapters/server/QueryStatusActionHandlerSpec.kt @@ -1,11 +1,13 @@ package com.wix.detox.adapters.server -import android.content.Context import androidx.test.espresso.IdlingResource -import com.nhaarman.mockitokotlin2.* import com.wix.detox.TestEngineFacade import com.wix.detox.reactnative.idlingresources.DescriptiveIdlingResource import com.wix.detox.reactnative.idlingresources.IdlingResourceDescription +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/common/JsonConverterTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/common/JsonConverterTest.kt index a4cafd9c21..352fd6d3c9 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/common/JsonConverterTest.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/common/JsonConverterTest.kt @@ -6,7 +6,6 @@ import org.json.JSONObject import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -import java.lang.IllegalArgumentException import kotlin.test.assertFailsWith @RunWith(RobolectricTestRunner::class) diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/common/TapEventsSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/common/TapEventsSpec.kt index def9a295ea..e0955fa2ec 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/common/TapEventsSpec.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/common/TapEventsSpec.kt @@ -1,10 +1,10 @@ package com.wix.detox.common import android.view.MotionEvent -import com.nhaarman.mockitokotlin2.* import com.wix.detox.espresso.action.common.MotionEvents import com.wix.detox.espresso.action.common.TapEvents import org.assertj.core.api.Assertions.assertThat +import org.mockito.kotlin.* import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/common/collect/PairsIteratorSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/common/collect/PairsIteratorSpec.kt index 31adbda4b0..47f1bc3fbe 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/common/collect/PairsIteratorSpec.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/common/collect/PairsIteratorSpec.kt @@ -3,7 +3,6 @@ package com.wix.detox.common.collect import org.assertj.core.api.Assertions.assertThat import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe -import java.lang.Exception import kotlin.test.assertFailsWith object PairsIteratorSpec: Spek({ diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/action/AdjustSliderToPositionActionTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/action/AdjustSliderToPositionActionTest.kt index 4a351d4fcb..0399c61f86 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/action/AdjustSliderToPositionActionTest.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/action/AdjustSliderToPositionActionTest.kt @@ -3,11 +3,11 @@ package com.wix.detox.espresso.action import android.view.View import com.facebook.react.views.slider.ReactSlider import com.facebook.react.views.slider.ReactSliderManager -import com.nhaarman.mockitokotlin2.* import org.assertj.core.api.Assertions.assertThat import org.hamcrest.Matcher import org.junit.Before import org.junit.Test +import org.mockito.kotlin.* @Suppress("IllegalIdentifier") class AdjustSliderToPositionActionTest { diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/action/DetoxMultiTapSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/action/DetoxMultiTapSpec.kt index 3bd5c4512f..3086e71dfa 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/action/DetoxMultiTapSpec.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/action/DetoxMultiTapSpec.kt @@ -3,12 +3,12 @@ package com.wix.detox.espresso.action import android.view.MotionEvent import androidx.test.espresso.UiController import androidx.test.espresso.action.Tapper -import com.nhaarman.mockitokotlin2.* import com.wix.detox.common.DetoxLog import com.wix.detox.common.proxy.CallInfo import com.wix.detox.espresso.UiControllerSpy import com.wix.detox.espresso.action.common.TapEvents import org.assertj.core.api.Assertions.assertThat +import org.mockito.kotlin.* import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe import kotlin.test.assertFailsWith diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/action/GetAttributesActionTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/action/GetAttributesActionTest.kt index 1ac3e31b9f..1b15801c6a 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/action/GetAttributesActionTest.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/action/GetAttributesActionTest.kt @@ -5,15 +5,15 @@ import android.widget.CheckBox import android.widget.ProgressBar import android.widget.TextView import com.google.android.material.slider.Slider -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.whenever import org.assertj.core.api.Assertions.assertThat import org.json.JSONObject import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/matcher/ViewAtIndexMatcherSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/matcher/ViewAtIndexMatcherSpec.kt index da04bbf501..b726d591e0 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/matcher/ViewAtIndexMatcherSpec.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/matcher/ViewAtIndexMatcherSpec.kt @@ -1,10 +1,12 @@ package com.wix.detox.espresso.matcher import android.view.View -import com.nhaarman.mockitokotlin2.* import org.hamcrest.Description import org.hamcrest.Matcher -import org.mockito.Matchers +import org.hamcrest.Matchers +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe @@ -30,8 +32,7 @@ object ViewAtIndexMatcherSpec: Spek({ it("should append a valid description for index≥0") { val uut = ViewAtIndexMatcher(7, innerMatcher) uut.describeTo(description) - com.nhaarman.mockitokotlin2. - verify(description).appendText(Matchers.contains("View at index #7")) + verify(description).appendText("View at index #7, of those matching MATCHER(innerMatcher description)") } } } diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/registry/IRStatusInquirerTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/registry/IRStatusInquirerTest.kt index 49ec542f08..4e183c2fe3 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/registry/IRStatusInquirerTest.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/registry/IRStatusInquirerTest.kt @@ -3,18 +3,18 @@ package com.wix.detox.espresso.registry import android.os.Looper import androidx.test.espresso.IdlingResource import androidx.test.espresso.base.IdlingResourceRegistry -import com.nhaarman.mockitokotlin2.doAnswer -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.whenever import com.wix.detox.UTHelpers import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner -import java.util.concurrent.* +import java.util.concurrent.Executors @RunWith(RobolectricTestRunner::class) class IRStatusInquirerTest { diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/DetoxSwipeSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/DetoxSwipeSpec.kt index 2bdfc3ad1a..f9025a1f9c 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/DetoxSwipeSpec.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/DetoxSwipeSpec.kt @@ -1,11 +1,9 @@ package com.wix.detox.espresso.scroll -import com.nhaarman.mockitokotlin2.* import org.mockito.AdditionalMatchers +import org.mockito.kotlin.* import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe -import java.lang.Exception -import java.lang.RuntimeException private fun floatEq3(value: Float) = AdditionalMatchers.eq(value, 0.001f) diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/FlinglessSwiperSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/FlinglessSwiperSpec.kt index a790df23b0..71856eab0e 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/FlinglessSwiperSpec.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/FlinglessSwiperSpec.kt @@ -3,9 +3,9 @@ package com.wix.detox.espresso.scroll import android.view.MotionEvent import android.view.ViewConfiguration import androidx.test.espresso.UiController -import com.nhaarman.mockitokotlin2.* import com.wix.detox.espresso.action.common.MotionEvents import org.assertj.core.api.Assertions.assertThat +import org.mockito.kotlin.* import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/SwipeHelperSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/SwipeHelperSpec.kt index f469ca6bca..f4ae682545 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/SwipeHelperSpec.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/SwipeHelperSpec.kt @@ -5,15 +5,20 @@ import android.content.res.Resources import android.util.DisplayMetrics import android.view.View import androidx.test.espresso.ViewAction -import androidx.test.espresso.action.* -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.whenever -import com.wix.detox.action.common.* -import com.wix.detox.espresso.common.* +import androidx.test.espresso.action.CoordinatesProvider +import androidx.test.espresso.action.PrecisionDescriber +import androidx.test.espresso.action.Press +import androidx.test.espresso.action.Swiper +import com.wix.detox.action.common.MOTION_DIR_DOWN +import com.wix.detox.action.common.MOTION_DIR_LEFT +import com.wix.detox.action.common.MOTION_DIR_RIGHT +import com.wix.detox.action.common.MOTION_DIR_UP import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import kotlin.test.assertEquals data class SwipeArguments( diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/utils/Vector2DSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/utils/Vector2DSpec.kt index 1eae5e1a7b..d824b8bd55 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/utils/Vector2DSpec.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/utils/Vector2DSpec.kt @@ -1,7 +1,9 @@ package com.wix.detox.espresso.utils -import com.wix.detox.action.common.* -import com.wix.detox.espresso.common.* +import com.wix.detox.action.common.MOTION_DIR_DOWN +import com.wix.detox.action.common.MOTION_DIR_LEFT +import com.wix.detox.action.common.MOTION_DIR_RIGHT +import com.wix.detox.action.common.MOTION_DIR_UP import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe import kotlin.test.assertEquals diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/instruments/DetoxInstrumentsManagerSpec.kt b/detox/android/detox/src/testFull/java/com/wix/detox/instruments/DetoxInstrumentsManagerSpec.kt index 9257e01753..142d00522b 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/instruments/DetoxInstrumentsManagerSpec.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/instruments/DetoxInstrumentsManagerSpec.kt @@ -1,7 +1,7 @@ package com.wix.detox.instruments import android.content.Context -import com.nhaarman.mockitokotlin2.* +import org.mockito.kotlin.* import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe import java.io.File 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 e48011640f..fae40aa733 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 @@ -2,9 +2,9 @@ package com.wix.detox.reactnative.idlingresources import androidx.test.espresso.IdlingResource import com.facebook.react.bridge.NativeModule -import com.nhaarman.mockitokotlin2.* import com.wix.detox.UTHelpers.yieldToOtherThreads 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 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 abc0e3464e..31cdb31b51 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,9 +1,10 @@ package com.wix.detox.reactnative.idlingresources import org.assertj.core.api.Assertions.assertThat +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe -import com.nhaarman.mockitokotlin2.* import java.util.concurrent.Executor 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 index 5106021158..66c04fe024 100644 --- 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 @@ -1,8 +1,8 @@ package com.wix.detox.reactnative.idlingresources.timers import com.facebook.react.bridge.NativeModule -import com.nhaarman.mockitokotlin2.* import org.assertj.core.api.Assertions.assertThat +import org.mockito.kotlin.* import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe 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 index 2632f84bb7..2b63203a81 100644 --- 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 @@ -2,9 +2,9 @@ package com.wix.detox.reactnative.idlingresources.timers import android.view.Choreographer import androidx.test.espresso.IdlingResource -import com.nhaarman.mockitokotlin2.* import com.wix.detox.reactnative.idlingresources.IdlingResourceDescription import org.assertj.core.api.Assertions +import org.mockito.kotlin.* import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe diff --git a/detox/android/detox/src/testFull/java/com/wix/invoke/JsonParserTest.java b/detox/android/detox/src/testFull/java/com/wix/invoke/JsonParserTest.java index 19c8ebd683..136cbe396d 100644 --- a/detox/android/detox/src/testFull/java/com/wix/invoke/JsonParserTest.java +++ b/detox/android/detox/src/testFull/java/com/wix/invoke/JsonParserTest.java @@ -1,5 +1,7 @@ package com.wix.invoke; +import static org.assertj.core.api.Java6Assertions.assertThat; + import com.wix.invoke.parser.JsonParser; import com.wix.invoke.types.ClassTarget; import com.wix.invoke.types.Invocation; @@ -11,8 +13,6 @@ import java.util.ArrayList; -import static org.assertj.core.api.Java6Assertions.assertThat; - /** * Created by rotemm on 13/10/2016. */ diff --git a/detox/android/detox/src/testFull/java/com/wix/invoke/MethodInvocationTest.java b/detox/android/detox/src/testFull/java/com/wix/invoke/MethodInvocationTest.java index 0ec617cc6e..2bf4ec31d8 100644 --- a/detox/android/detox/src/testFull/java/com/wix/invoke/MethodInvocationTest.java +++ b/detox/android/detox/src/testFull/java/com/wix/invoke/MethodInvocationTest.java @@ -1,4 +1,7 @@ package com.wix.invoke; + +import static org.assertj.core.api.Java6Assertions.assertThat; + import com.wix.invoke.types.ClassTarget; import com.wix.invoke.types.Invocation; import com.wix.invoke.types.InvocationTarget; @@ -8,9 +11,6 @@ import org.junit.Test; -import static org.assertj.core.api.Java6Assertions.assertThat; - - /** * Created by rotemm on 10/10/2016. */ diff --git a/detox/android/detox/src/testFull/java/com/wix/invoke/types/InvocationTest.java b/detox/android/detox/src/testFull/java/com/wix/invoke/types/InvocationTest.java index 82416e790b..2ac6170aaf 100644 --- a/detox/android/detox/src/testFull/java/com/wix/invoke/types/InvocationTest.java +++ b/detox/android/detox/src/testFull/java/com/wix/invoke/types/InvocationTest.java @@ -1,10 +1,11 @@ package com.wix.invoke.types; +import static org.assertj.core.api.Java6Assertions.assertThat; + import org.junit.Test; import java.util.HashMap; import java.util.LinkedHashMap; -import static org.assertj.core.api.Java6Assertions.assertThat; /** * Created by rotemm on 26/10/2016. */ From 5295900b50d5b98f4f7107fde9494dd90449b4ed Mon Sep 17 00:00:00 2001 From: Eric Butler Date: Mon, 28 Feb 2022 09:40:44 -0500 Subject: [PATCH 02/15] Implement backdoor feature This makes it possible for tests to send arbitrary messages to the app being tested for the purpose of configuring state or running any other special actions. Usage inside a test looks like: await device.backdoor({ action: "do-something" }); Then receive the event in a React Native app: const emitter = Platform.OS === "ios" ? NativeAppEventEmitter : DeviceEventEmitter; emitter.addListener("detoxBackdoor", ({ action }) => { // do something based on action }); Inspired by a similar feature in the Xamarin test framework: https://docs.microsoft.com/en-us/appcenter/test-cloud/frameworks/uitest/features/backdoors --- .../src/full/java/com/wix/detox/DetoxMain.kt | 1 + .../adapters/server/DetoxActionHandlers.kt | 12 ++++++++ .../detox/reactnative/ReactNativeExtension.kt | 18 ++++++++++++ detox/index.d.ts | 6 ++++ detox/ios/Detox/DetoxManager.swift | 3 ++ .../ReactNativeSupport/ReactNativeHeaders.h | 5 ++++ .../ReactNativeSupport/ReactNativeSupport.h | 1 + .../ReactNativeSupport/ReactNativeSupport.m | 11 +++++++ detox/src/client/Client.js | 4 +++ detox/src/client/Client.test.js | 1 + detox/src/client/actions/actions.js | 20 +++++++++++++ detox/src/devices/runtime/RuntimeDevice.js | 5 ++++ .../src/devices/runtime/RuntimeDevice.test.js | 7 +++++ .../runtime/drivers/DeviceDriverBase.js | 4 +++ docs/APIRef.DeviceObjectAPI.md | 4 +++ docs/Guide.Backdoors.md | 29 +++++++++++++++++++ 16 files changed, 131 insertions(+) create mode 100644 docs/Guide.Backdoors.md diff --git a/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt b/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt index 5e4f0bd88b..990863f424 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt @@ -70,6 +70,7 @@ object DetoxMain { this@DetoxMain.doTeardown(serverAdapter, actionsDispatcher, testEngineFacade) } }) + associateActionHandler("backdoor", BackdoorActionHandler(rnHostHolder, serverAdapter, testEngineFacade)) if (DetoxInstrumentsManager.supports()) { val instrumentsManager = DetoxInstrumentsManager(rnHostHolder) diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt index ef0416465e..22e7496730 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt @@ -6,6 +6,7 @@ import com.wix.detox.TestEngineFacade import com.wix.detox.common.extractRootCause import com.wix.detox.instruments.DetoxInstrumentsException import com.wix.detox.instruments.DetoxInstrumentsManager +import com.wix.detox.reactnative.ReactNativeExtension import com.wix.invoke.MethodInvocation import org.json.JSONObject import java.lang.reflect.InvocationTargetException @@ -40,6 +41,17 @@ open class ReactNativeReloadActionHandler( } } +class BackdoorActionHandler( + private val appContext: Context, + private val outboundServerAdapter: OutboundServerAdapter, + private val testEngineFacade: TestEngineFacade) + : DetoxActionHandler { + + override fun handle(params: String, messageId: Long) { + ReactNativeExtension.emitBackdoorEvent(appContext, params) + outboundServerAdapter.sendMessage("backdoorDone", emptyMap(), messageId) + } +} class InvokeActionHandler @JvmOverloads constructor( private val methodInvocation: MethodInvocation, 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 1865623dd5..2d9660a141 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 @@ -6,8 +6,13 @@ import android.util.Log import androidx.test.platform.app.InstrumentationRegistry import com.facebook.react.ReactApplication import com.facebook.react.ReactInstanceManager +import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactContext +import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter import com.wix.detox.LaunchArgs +import com.wix.detox.common.JsonConverter +import org.json.JSONObject + private const val LOG_TAG = "DetoxRNExt" @@ -114,6 +119,19 @@ object ReactNativeExtension { } } + @JvmStatic + fun emitBackdoorEvent(applicationContext: Context, params: String) { + if (!ReactNativeInfo.isReactNativeApp()) { + return + } + + val bundle = JsonConverter(JSONObject(params)).toBundle() + val payload = Arguments.fromBundle(bundle) + + val reactContext = getCurrentReactContextSafe(applicationContext as ReactApplication) ?: return + reactContext.getJSModule(RCTDeviceEventEmitter::class.java).emit("detoxBackdoor", payload) + } + private fun reloadReactNativeInBackground(reactApplication: ReactApplication) { val rnReloader = ReactNativeReLoader(InstrumentationRegistry.getInstrumentation(), reactApplication) rnReloader.reloadInBackground() diff --git a/detox/index.d.ts b/detox/index.d.ts index a090b57cb8..07c9d2cd6e 100644 --- a/detox/index.d.ts +++ b/detox/index.d.ts @@ -762,6 +762,12 @@ declare global { * This is a no-op when running on iOS. */ unreverseTcpPort(port: number): Promise; + + /** + * Sends a backdoor message to the app being tested. + * For more information, see {@link https://wix.github.io/Detox/docs/guide/backdoors}. + */ + backdoor(message: object): Promise; } /** diff --git a/detox/ios/Detox/DetoxManager.swift b/detox/ios/Detox/DetoxManager.swift index 6d2659dae1..071b7d7fa1 100644 --- a/detox/ios/Detox/DetoxManager.swift +++ b/detox/ios/Detox/DetoxManager.swift @@ -402,6 +402,9 @@ public class DetoxManager : NSObject, WebSocketDelegate { rvParams["captureViewHierarchyError"] = "User ran process with -detoxDisableHierarchyDump YES" } self.webSocket.sendAction(done, params: rvParams, messageId: messageId) + case "backdoor": + ReactNativeSupport.emitBackdoorEvent(params) + self.safeSend(action: done, messageId: messageId) default: fatalError("Unknown action type received: \(type)") } diff --git a/detox/ios/Detox/Utilities/ReactNativeSupport/ReactNativeHeaders.h b/detox/ios/Detox/Utilities/ReactNativeSupport/ReactNativeHeaders.h index e47c0f014d..9d4f16600a 100644 --- a/detox/ios/Detox/Utilities/ReactNativeSupport/ReactNativeHeaders.h +++ b/detox/ios/Detox/Utilities/ReactNativeSupport/ReactNativeHeaders.h @@ -25,6 +25,11 @@ typedef void (^RN_RCTJavaScriptCallback)(id json, NSError *error); - (id) uiManager; - (id)moduleForName:(NSString *)moduleName; - (id)moduleForClass:(Class)moduleClass; +- (void)enqueueJSCall:(NSString *)module + method:(NSString *)method + args:(NSArray *)args + completion:(dispatch_block_t)completion; + @property (nonatomic, readonly, getter=isLoading) BOOL loading; @property (nonatomic, readonly, getter=isValid) BOOL valid; diff --git a/detox/ios/Detox/Utilities/ReactNativeSupport/ReactNativeSupport.h b/detox/ios/Detox/Utilities/ReactNativeSupport/ReactNativeSupport.h index a4d6a7781b..6f83595095 100644 --- a/detox/ios/Detox/Utilities/ReactNativeSupport/ReactNativeSupport.h +++ b/detox/ios/Detox/Utilities/ReactNativeSupport/ReactNativeSupport.h @@ -14,6 +14,7 @@ + (void)reloadApp; + (void)waitForReactNativeLoadWithCompletionHandler:(void(^)(void))handler; ++ (void)emitBackdoorEvent:(id)data; @end diff --git a/detox/ios/Detox/Utilities/ReactNativeSupport/ReactNativeSupport.m b/detox/ios/Detox/Utilities/ReactNativeSupport/ReactNativeSupport.m index b04284cce2..8a25cb4c02 100644 --- a/detox/ios/Detox/Utilities/ReactNativeSupport/ReactNativeSupport.m +++ b/detox/ios/Detox/Utilities/ReactNativeSupport/ReactNativeSupport.m @@ -55,4 +55,15 @@ + (void)waitForReactNativeLoadWithCompletionHandler:(void (^)(void))handler [DTXReactNativeSupport waitForReactNativeLoadWithCompletionHandler:handler]; } ++ (void)emitBackdoorEvent:(id)data +{ + if([DTXReactNativeSupport hasReactNative] == NO) + { + return; + } + + id bridge = [NSClassFromString(@"RCTBridge") valueForKey:@"currentBridge"]; + [bridge enqueueJSCall:@"RCTNativeAppEventEmitter" method:@"emit" args:@[@"detoxBackdoor", data] completion:nil]; +} + @end diff --git a/detox/src/client/Client.js b/detox/src/client/Client.js index 9b513d72b9..1b21b6b061 100644 --- a/detox/src/client/Client.js +++ b/detox/src/client/Client.js @@ -227,6 +227,10 @@ class Client { await this.sendAction(new actions.Shake()); } + async backdoor(data) { + return await this.sendAction(new actions.Backdoor(data)); + } + async setOrientation(orientation) { await this.sendAction(new actions.SetOrientation(orientation)); } diff --git a/detox/src/client/Client.test.js b/detox/src/client/Client.test.js index 1e54e90b80..344905cdf8 100644 --- a/detox/src/client/Client.test.js +++ b/detox/src/client/Client.test.js @@ -363,6 +363,7 @@ describe('Client', () => { ['waitForActive', 'waitForActiveDone', actions.WaitForActive], ['waitUntilReady', 'ready', actions.Ready], ['currentStatus', 'currentStatusResult', actions.CurrentStatus, {}, { status: { app_status: 'idle' } }], + ['backdoor', 'backdoorDone', actions.Backdoor], ])('.%s', (methodName, expectedResponseType, Action, params, expectedResponseParams) => { beforeEach(async () => { await client.connect(); diff --git a/detox/src/client/actions/actions.js b/detox/src/client/actions/actions.js index a4eae2e5cd..0c6908d98e 100644 --- a/detox/src/client/actions/actions.js +++ b/detox/src/client/actions/actions.js @@ -320,8 +320,28 @@ class CaptureViewHierarchy extends Action { } } +class Backdoor extends Action { + constructor(params) { + super ('backdoor', params); + } + + get isAtomic() { + return false; + } + + get timeout() { + return 0; + } + + async handle(response) { + this.expectResponseOfType(response, 'backdoorDone'); + return response; + } +} + module.exports = { Action, + Backdoor, Login, WaitForBackground, WaitForActive, diff --git a/detox/src/devices/runtime/RuntimeDevice.js b/detox/src/devices/runtime/RuntimeDevice.js index 336b15915c..6bdf08d9d5 100644 --- a/detox/src/devices/runtime/RuntimeDevice.js +++ b/detox/src/devices/runtime/RuntimeDevice.js @@ -15,6 +15,7 @@ class RuntimeDevice { runtimeErrorComposer, }, deviceDriver) { wrapWithStackTraceCutter(this, [ + 'backdoor', 'captureViewHierarchy', 'clearKeychain', 'disableSynchronization', @@ -134,6 +135,10 @@ class RuntimeDevice { return this.deviceDriver.takeScreenshot(name); } + async backdoor(data) { + await this.deviceDriver.backdoor(data); + } + async captureViewHierarchy(name = 'capture') { return this.deviceDriver.captureViewHierarchy(name); } diff --git a/detox/src/devices/runtime/RuntimeDevice.test.js b/detox/src/devices/runtime/RuntimeDevice.test.js index 02e03398de..0ec2238f5e 100644 --- a/detox/src/devices/runtime/RuntimeDevice.test.js +++ b/detox/src/devices/runtime/RuntimeDevice.test.js @@ -730,6 +730,13 @@ describe('Device', () => { }); }); + it(`backdoor() should pass to device driver`, async () => { + const device = await aValidDevice(); + await device.backdoor({ action: 'test' }); + expect(driverMock.driver.backdoor).toHaveBeenCalledWith({ action: 'test' }); + expect(driverMock.driver.backdoor).toHaveBeenCalledTimes(1); + }); + it(`sendToHome() should pass to device driver`, async () => { const device = await aValidDevice(); await device.sendToHome(); diff --git a/detox/src/devices/runtime/drivers/DeviceDriverBase.js b/detox/src/devices/runtime/drivers/DeviceDriverBase.js index b52c18eb3b..9c720d73a0 100644 --- a/detox/src/devices/runtime/drivers/DeviceDriverBase.js +++ b/detox/src/devices/runtime/drivers/DeviceDriverBase.js @@ -127,6 +127,10 @@ class DeviceDriverBase { return await this.client.reloadReactNative(); } + async backdoor(data) { + return await this.client.backdoor(data); + } + createPayloadFile(notification) { const notificationFilePath = path.join(this.createRandomDirectory(), `payload.json`); fs.writeFileSync(notificationFilePath, JSON.stringify(notification, null, 2)); diff --git a/docs/APIRef.DeviceObjectAPI.md b/docs/APIRef.DeviceObjectAPI.md index 4cdfba9147..df33f34be3 100644 --- a/docs/APIRef.DeviceObjectAPI.md +++ b/docs/APIRef.DeviceObjectAPI.md @@ -528,3 +528,7 @@ Exposes [`UiAutomator`’s `UiDevice` API](https://developer.android.com/referen **This is not a part of the official Detox API**, it may break and change whenever an update to `UiDevice` or `UiAutomator` Gradle dependencies (`androidx.test.uiautomator:uiautomator`) is introduced. [`UiDevice`’s autogenerated code](../detox/src/android/espressoapi/UIDevice.js) + +#### `device.backdoor(data)` + +Send a backdoor message to the app being tested. See [Backdoors](/docs/guide/backdoors) for details. diff --git a/docs/Guide.Backdoors.md b/docs/Guide.Backdoors.md new file mode 100644 index 0000000000..43d750521c --- /dev/null +++ b/docs/Guide.Backdoors.md @@ -0,0 +1,29 @@ +--- +id: backdoors +slug: guide/backdoors +title: Backdoors +sidebar_label: Backdoors +--- + +## Backdoors + +Detox provides a backdoor feature that makes it possible for tests to send +arbitrary messages to the app being tested for the purpose of configuring +state or running any other special actions. + +### Usage + +#### In your test + +```tsx +await device.backdoor({ action: "do-something" }); +``` + +#### In your app + +```tsx +const emitter = Platform.OS === "ios" ? NativeAppEventEmitter : DeviceEventEmitter; +emitter.addListener("detoxBackdoor", ({ action }) => { + // do something based on action +}); +``` From 80c574edcc83b1b365849d94d354243ca3c4547b Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 5 Oct 2023 10:54:49 +0300 Subject: [PATCH 03/15] test: add e2e for device.backdoor() --- detox/test/e2e/03.actions.test.js | 5 +++++ detox/test/src/Screens/ActionsScreen.js | 22 ++++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/detox/test/e2e/03.actions.test.js b/detox/test/e2e/03.actions.test.js index 5ef6698e08..bb67df2883 100644 --- a/detox/test/e2e/03.actions.test.js +++ b/detox/test/e2e/03.actions.test.js @@ -26,6 +26,11 @@ describe('Actions', () => { await driver.tapsElement.assertTappedOnce(); }); + it('should be able to send backdoor commands', async () => { + await device.backdoor({ action: 'greet', text: 'Arbitrary Text' }); + await expect(element(by.text('Arbitrary Text!!!'))).toBeVisible(); + }); + it.each([ 'activate', 'magicTap', diff --git a/detox/test/src/Screens/ActionsScreen.js b/detox/test/src/Screens/ActionsScreen.js index 8ebf46c415..cfdce04d04 100644 --- a/detox/test/src/Screens/ActionsScreen.js +++ b/detox/test/src/Screens/ActionsScreen.js @@ -1,14 +1,16 @@ import React, { Component } from 'react'; import { - Text, BackHandler, - View, - TouchableOpacity, - ScrollView, - RefreshControl, - Platform, + DeviceEventEmitter, Dimensions, + NativeAppEventEmitter, + Platform, + RefreshControl, + ScrollView, StyleSheet, + Text, + TouchableOpacity, + View, Slider as LegacySlider, SafeAreaView, requireNativeComponent, @@ -31,6 +33,9 @@ const styles = StyleSheet.create({ const isIos = Platform.OS === 'ios'; const isAndroid = Platform.OS === 'android'; +const RNEmitter = isIos + ? NativeAppEventEmitter + : DeviceEventEmitter; export default class ActionsScreen extends Component { @@ -49,6 +54,11 @@ export default class ActionsScreen extends Component { componentDidMount() { BackHandler.addEventListener('hardwareBackPress', this.backHandler.bind(this)); + RNEmitter.addListener('detoxBackdoor', ({ action, text }) => { + if (action === 'greet') { + this.setState({ greeting: text }); + } + }); } render() { From 32674dfbf2f4c52641e11c0f314d46da71c05ae0 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 5 Oct 2023 12:27:51 +0300 Subject: [PATCH 04/15] docs: update the docs --- docs/api/device.md | 41 ++++++++++- docs/guide/mocking.md | 156 ++++++++++++++++++++++++++++++++---------- 2 files changed, 157 insertions(+), 40 deletions(-) diff --git a/docs/api/device.md b/docs/api/device.md index 76cad0a7c5..60211c7791 100644 --- a/docs/api/device.md +++ b/docs/api/device.md @@ -377,9 +377,46 @@ if (device.getPlatform() === 'ios') { Takes a screenshot of the device. For full details on taking screenshots with Detox, refer to the [screen-shots guide](../guide/taking-screenshots.md). -### `device.captureViewHierarchy([name])` +### `device.backdoor(message)` -**iOS Only.** Saves a view hierarchy snapshot (`*.viewhierarchy`) of the +:::tip + +Learn how to use Backdoor API in our [Mocking Guide](../guide/mocking.md#dynamic-mocking-with-backdoor-api). + +::: + +Send a backdoor message (any serializable object) to the app being tested, e.g.: + +```js +await device.backdoor({ + // the property names are up to you + action: 'my-testing-action', + arg1: 'value1', + arg2: 2 +}); +``` + +On the application side, you have to implement a handler for the backdoor message, e.g.: + +```js +import { + DeviceEventEmitter, + NativeAppEventEmitter, + Platform +} from 'react-native'; + +const RNEmitter = Platform.OS === "ios" + ? NativeAppEventEmitter + : DeviceEventEmitter; + +RNEmitter.addListener("detoxBackdoor", (msg) => { + /* ... */ +}); +``` + +### `device.captureViewHierarchy([name])` **iOS Only** + +Saves a view hierarchy snapshot (`*.viewhierarchy`) of the currently opened application to a temporary folder and schedules putting it to the artifacts' folder upon the completion of the current test. The file can be opened later in Xcode 12.0 and above. diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index 74e7af99f1..124484ffd1 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -1,4 +1,8 @@ -# Mocking +--- +id: mocking +--- + +# Mocking Guide :::info @@ -35,18 +39,14 @@ Let's start with the quicker way. 1. Pick a module that you are going to mock, e.g.: - ```js file=src/config.js - // src/config.js - + ```js title=src/config.js export const SERVER_URL = 'https://production.mycompany.name/api'; export const FETCH_TIMEOUT = 60000; ``` -1. Create a mock module alongside, with an arbitrary extension (e.g. `.mock.js`): - - ```js file=src/config.js - // src/config.mock.js +1. Create a mock module alongside, with an arbitrary extension (e.g. `.e2e.js`): + ```js title=src/config.e2e.js export * from './config.js'; // override the url from the original file: @@ -56,42 +56,48 @@ Let's start with the quicker way. 1. Stop your _Metro bundler_ if it has been already running, and run it again with the corresponding file extension override, e.g.: ```bash - npx react-native start --sourceExts mock.js,js,json,ts,tsx + npx react-native start --sourceExts e2e.js,js,json,ts,tsx ``` - This command is already enough to start your application in an altered mode, and you can start running your tests. Now, if some module imports `./src/config`, you tell _Metro bundler_ to prefer `./src/config.mock.js` over the plain `./src/config.js`, which means the consumer gets the mocked implementation. + This command is already enough to start your application in an altered mode, and you can start running your tests. Now, if some module imports `./src/config`, you tell _Metro bundler_ to prefer `./src/config.e2e.js` over the plain `./src/config.js`, which means the consumer gets the mocked implementation. -> CAVEAT: whichever file extension you might take for the mock files – make sure you don’t accidentally "pick up" unforeseen file overrides from `node_modules/**/*.your-extension.js`! -> _Metro bundler_ does not limit itself to your project files only – applying those `--sourceExts` also affects the resolution of the `node_modules` content! +:::caution Caveat + +Whichever file extension you might take for the mock files – make sure you don’t accidentally "pick up" unforeseen file overrides from `node_modules/**/*.your-extension.js`! +_Metro bundler_ does not limit itself to your project files only – applying those `--sourceExts` also affects the resolution of the `node_modules` content! + +::: ## Configuring Metro bundler While the mentioned way is good enough for the **debug mode**, it falls short for the **release builds**. The problem is that the `--sourceExts` argument is supported only by `react-native start` command. Hence, you’d need a CLI-independent way to configure your Metro bundler, and that is patching your project's `metro.config.js`: -```diff title="metro.config.js" - /** - * Metro configuration for React Native - * https://github.com/facebook/react-native - * - * @format - */ -+const defaultSourceExts = require('metro-config/src/defaults/defaults').sourceExts; - - module.exports = { -+ resolver: { -+ sourceExts: process.env.MY_APP_MODE === 'mocked' -+ ? ['mock.js', ...defaultSourceExts] -+ : defaultSourceExts, -+ }, - transformer: { - getTransformOptions: async () => ({ - transform: { - experimentalImportSupport: false, - inlineRequires: true, - }, - }), - }, - }; +```js title="metro.config.js" +/** + * Metro configuration for React Native + * https://github.com/facebook/react-native + * + * @format + */ +const defaultSourceExts = require('metro-config/src/defaults/defaults').sourceExts; + +module.exports = { +// highlight-start + resolver: { + sourceExts: process.env.MY_APP_MODE === 'mocked' + ? ['e2e.js', ...defaultSourceExts] + : defaultSourceExts, + }, +// highlight-end + transformer: { + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: true, + }, + }), + }, +}; ``` This way, we are enforcing a custom convention that if the Metro bundler finds the `MY_APP_MODE=mocked` environment variable, it should apply our `sourceExts` override instead of the default values. @@ -119,10 +125,84 @@ xcodebuild -workspace ... -configuration release -scheme ... Please note that preparing React Native apps for the release mode requires groundwork for both [iOS](https://reactnative.dev/docs/publishing-to-app-store) and [Android](https://reactnative.dev/docs/signed-apk-android), which is out of scope of this current article. -As you might have noticed, this tutorial has no direct connection to Detox itself, which is a correct observation. -The suggested mocking techniques are a part of the React Native world itself, so please consult the further resources: +As you might have noticed, until now, this tutorial had no direct connection to Detox itself, and that's a correct observation. +The suggested static mocking techniques are a part of the React Native world itself, so please consult the further resources if you need more information: - - +## Dynamic Mocking with Backdoor API + +In scenarios where static mocking is not sufficiently flexible, Detox's [Backdoor API](../api/device.md#backdoor) presents a strategy for **dynamic mocking** during test runtime. +This allows tests to instruct the app to modify its internal state without interacting with the UI, thereby providing additional control over the app's behavior during test execution. + +:::info Before you continue + +The dynamic mocking is an extension of the static mocking approach, so make sure your **read the previous section**, as it provides the necessary background. + +::: + +### Example + +Imagine you have a time service in your app, responsible for providing the current time: + +```js title=src/services/TimeService.js +export class TimeService { + now() { + return Date.now(); + } +} +``` + +Now, for testing purposes, we can create a mocked counterpart that allows its internal time to be set dynamically: + +```js title=src/services/TimeService.e2e.js +import {DeviceEventEmitter, NativeAppEventEmitter, Platform} from 'react-native'; + +const RNEmitter = Platform.OS === "ios" ? NativeAppEventEmitter : DeviceEventEmitter; + +export class FakeTimeService { + #now = Date.now(); + + constructor() { + RNEmitter.addListener("detoxBackdoor", ({ action, time }) => { + if (action === "set-mock-time") { + this.#now = time; + } + }); + } + + now() { + return this.#now; + } +} +``` + +In the mock implementation, `TimeService.e2e.js`, we're listening for `detoxBackdoor` events and, when received, we adjust the internal `#now` value if the `action` is `"set-mock-time"`. + +:::danger Security Notice + +Avoid using `detoxBackdoor` listener in your production code, as it might expose a security vulnerability. + +Leave these listeners to **mock files only**, and make sure they are excluded from the public release builds. +Backdoor API is a **testing tool**, and it should be isolated to test environments only. + +::: + +During your Detox test, you can now utilize the Backdoor API to send a signal to modify this internal state dynamically: + +```js +await device.backdoor({ + action: "set-mock-time", + time: 1672531199000, +}); +``` + +This way, you can test your app's behavior in the past or future, without having to wait for the actual time to pass. + +Summarizing the above, the **Backdoor API** enables your tests to directly "speak" to your app, altering its state without UI interaction. +Provided that your app is designed with testability in mind, this can be a powerful tool for testing edge cases that are otherwise hard to reproduce, +like changing the internal clock, simulating network conditions, GPS locations, handling simplified authentication, and more. +Use it wisely, and... + Happy Detoxing! From b159bc52aa9b602ca367cb674ce347d3f9fb1d9a Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 5 Oct 2023 13:00:31 +0300 Subject: [PATCH 05/15] fix: docs and test --- detox/test/src/Screens/ActionsScreen.js | 15 ++++++++++----- docs/guide/mocking.md | 18 ++++++++++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/detox/test/src/Screens/ActionsScreen.js b/detox/test/src/Screens/ActionsScreen.js index cfdce04d04..4e19f1c170 100644 --- a/detox/test/src/Screens/ActionsScreen.js +++ b/detox/test/src/Screens/ActionsScreen.js @@ -54,13 +54,18 @@ export default class ActionsScreen extends Component { componentDidMount() { BackHandler.addEventListener('hardwareBackPress', this.backHandler.bind(this)); - RNEmitter.addListener('detoxBackdoor', ({ action, text }) => { - if (action === 'greet') { - this.setState({ greeting: text }); - } - }); } + componentWillUnmount() { + this.onBackdoor.remove(); + } + + onBackdoor = RNEmitter.addListener('detoxBackdoor', ({ action, text }) => { + if (action === 'greet') { + this.setState({ greeting: text }); + } + }); + render() { if (this.state.greeting) return this.renderAfterButton(); if (this.state.backPressed) return this.renderPopupBackPressedDetected(); diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index 124484ffd1..b0a4c2e6e7 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -142,8 +142,6 @@ The dynamic mocking is an extension of the static mocking approach, so make sure ::: -### Example - Imagine you have a time service in your app, responsible for providing the current time: ```js title=src/services/TimeService.js @@ -201,8 +199,16 @@ await device.backdoor({ This way, you can test your app's behavior in the past or future, without having to wait for the actual time to pass. Summarizing the above, the **Backdoor API** enables your tests to directly "speak" to your app, altering its state without UI interaction. -Provided that your app is designed with testability in mind, this can be a powerful tool for testing edge cases that are otherwise hard to reproduce, -like changing the internal clock, simulating network conditions, GPS locations, handling simplified authentication, and more. -Use it wisely, and... -Happy Detoxing! +Provided that your app is designed with testability in mind, this can be a powerful tool for testing cases where native tools fall short: +changing the internal clock, simulating network conditions, geolocation, quick authentication, and more. + +:::tip + +Make sure your `detoxBackdoor` event listeners don't throw unhandled exceptions, as this might turn into another +source of confusion. In devRelease mode your app will crash, and Detox will be the bearer of the bad news, although +it has nothing to do with the actual problem. + +::: + +Use it wisely, and... happy Detoxing! From e543a1a224ad3459e95544698263067884d9e59d Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 5 Oct 2023 13:04:35 +0300 Subject: [PATCH 06/15] docs: update links --- detox/index.d.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/detox/index.d.ts b/detox/index.d.ts index 717e471caa..6408f14c07 100644 --- a/detox/index.d.ts +++ b/detox/index.d.ts @@ -897,6 +897,13 @@ declare global { */ takeScreenshot(name: string): Promise; + /** + * Sends a backdoor message to the app being tested. + * For more information, see {@link https://wix.github.io/Detox/docs/guide/mocking} + * and {@link https://wix.github.io/Detox/docs/api/device#devicebackdoormessage} + */ + backdoor(message: object): Promise; + /** * (iOS only) Saves a view hierarchy snapshot (*.viewhierarchy) of the currently opened application * to a temporary folder and schedules putting it to the artifacts folder upon the completion of @@ -984,12 +991,6 @@ declare global { * This is a no-op when running on iOS. */ unreverseTcpPort(port: number): Promise; - - /** - * Sends a backdoor message to the app being tested. - * For more information, see {@link https://wix.github.io/Detox/docs/guide/backdoors}. - */ - backdoor(message: object): Promise; } /** From 206fb3a7b489e84e1985fc53b1c477dc964c5f24 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 5 Oct 2023 13:04:48 +0300 Subject: [PATCH 07/15] docs: update links --- docs/guide/mocking.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index b0a4c2e6e7..0c37b59fbd 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -133,7 +133,7 @@ The suggested static mocking techniques are a part of the React Native world its ## Dynamic Mocking with Backdoor API -In scenarios where static mocking is not sufficiently flexible, Detox's [Backdoor API](../api/device.md#backdoor) presents a strategy for **dynamic mocking** during test runtime. +In scenarios where static mocking is not sufficiently flexible, Detox's [Backdoor API](../api/device.md#devicebackdoormessage) presents a strategy for **dynamic mocking** during test runtime. This allows tests to instruct the app to modify its internal state without interacting with the UI, thereby providing additional control over the app's behavior during test execution. :::info Before you continue From ea8d699d51d9c6a822b764b6dcfca7b59454a813 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 5 Oct 2023 13:08:59 +0300 Subject: [PATCH 08/15] docs: delete old backdoors guide --- docs/Guide.Backdoors.md | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 docs/Guide.Backdoors.md diff --git a/docs/Guide.Backdoors.md b/docs/Guide.Backdoors.md deleted file mode 100644 index 43d750521c..0000000000 --- a/docs/Guide.Backdoors.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -id: backdoors -slug: guide/backdoors -title: Backdoors -sidebar_label: Backdoors ---- - -## Backdoors - -Detox provides a backdoor feature that makes it possible for tests to send -arbitrary messages to the app being tested for the purpose of configuring -state or running any other special actions. - -### Usage - -#### In your test - -```tsx -await device.backdoor({ action: "do-something" }); -``` - -#### In your app - -```tsx -const emitter = Platform.OS === "ios" ? NativeAppEventEmitter : DeviceEventEmitter; -emitter.addListener("detoxBackdoor", ({ action }) => { - // do something based on action -}); -``` From 2a62435c920468df7784b74df79df8ee94e59754 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 5 Oct 2023 13:10:54 +0300 Subject: [PATCH 09/15] fix: mark backdoor action as atomic --- detox/src/client/actions/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detox/src/client/actions/actions.js b/detox/src/client/actions/actions.js index decdaf61af..5d6d5e19b9 100644 --- a/detox/src/client/actions/actions.js +++ b/detox/src/client/actions/actions.js @@ -326,7 +326,7 @@ class Backdoor extends Action { } get isAtomic() { - return false; + return true; } get timeout() { From 1656d9908b6bd9547e467e7174aa58b853fb496a Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Fri, 6 Oct 2023 12:40:34 +0300 Subject: [PATCH 10/15] add detox/react-native facade --- detox/index.d.ts | 3 +- detox/react-native/BackdoorEmitter.js | 77 +++++++++++++++++++++++++ detox/react-native/index.js | 10 ++++ detox/test/e2e/03.actions.test.js | 3 +- detox/test/src/Screens/ActionsScreen.js | 18 ++---- detox/test/types/detox-module-tests.ts | 1 + docs/api/device.md | 33 ++++++----- docs/guide/mocking.md | 68 ++++++++++++---------- 8 files changed, 156 insertions(+), 57 deletions(-) create mode 100644 detox/react-native/BackdoorEmitter.js create mode 100644 detox/react-native/index.js diff --git a/detox/index.d.ts b/detox/index.d.ts index 6408f14c07..f196420259 100644 --- a/detox/index.d.ts +++ b/detox/index.d.ts @@ -899,10 +899,11 @@ declare global { /** * Sends a backdoor message to the app being tested. + * The message should be an object with `action` property and any other custom data. * For more information, see {@link https://wix.github.io/Detox/docs/guide/mocking} * and {@link https://wix.github.io/Detox/docs/api/device#devicebackdoormessage} */ - backdoor(message: object): Promise; + backdoor(message: { action: string; [key: string]: any }): Promise; /** * (iOS only) Saves a view hierarchy snapshot (*.viewhierarchy) of the currently opened application diff --git a/detox/react-native/BackdoorEmitter.js b/detox/react-native/BackdoorEmitter.js new file mode 100644 index 0000000000..131271d722 --- /dev/null +++ b/detox/react-native/BackdoorEmitter.js @@ -0,0 +1,77 @@ +/* eslint-disable node/no-unsupported-features/es-syntax */ + +export class BackdoorEmitter { + constructor(emitter) { + this._emitter = emitter; + this._handlers = {}; + this._listeners = {}; + this._subscription = this._emitter.addListener('detoxBackdoor', (event) => { + const listener = this._handlers[event.action] || this.onUnhandledAction; + listener(event); + }); + } + + /** + * Strict mode prevents interference errors when action handlers are not properly removed + * or overwritten one by another. It is enabled by default. + */ + strict = true; + + /** + * Sets a handler for a backdoor action. + * Note that there can only be one handler per action to avoid confusion, + * because in future versions the handlers will be able to return a promise or a value. + * + * @param {string} actionName + * @param {function} handler + * @throws {Error} if a handler for this action has already been set + * @throws {Error} if handler is not a function + * @example + * detoxBackdoor.setActionHandler('displayText', ({ text }) => { + * setText(text); + * }); + */ + setActionHandler(actionName, handler) { + if (typeof actionName !== 'string') { + throw new Error('Detox backdoor action name must be a string'); + } + + if (typeof handler !== 'function') { + throw new Error(`Detox backdoor handler for action "${actionName}" must be a function`); + } + + if (this.strict && this._handlers[actionName]) { + throw new Error(`Detox backdoor handler for action "${actionName}" has already been set`); + } + + this._handlers[actionName] = handler; + } + + /** + * Removes a handler for a backdoor action. + * By default, unremoved handlers will prevent new handlers from being set. + */ + removeActionHandler(actionName) { + delete this._handlers[actionName]; + } + + /** + * This fallback handler is called when no handler is set for a backdoor action. + * By default, it throws an error in strict mode and logs a warning otherwise. + * You can override it to provide a custom behavior. + * @param {object} event + * @param {string} event.action + * + * @example + * detoxBackdoor.onUnhandledAction = ({ action, ...args }) => { /* noop *\/ }; + */ + onUnhandledAction = ({ action }) => { + const message = `Failed to find Detox backdoor handler for action "${action}"`; + + if (this.strict) { + throw new Error(message); + } else { + console.warn(message); + } + }; +} diff --git a/detox/react-native/index.js b/detox/react-native/index.js new file mode 100644 index 0000000000..956a3eba96 --- /dev/null +++ b/detox/react-native/index.js @@ -0,0 +1,10 @@ +/* eslint-disable node/no-unsupported-features/es-syntax, import/namespace, node/no-unpublished-import */ +import { DeviceEventEmitter, NativeAppEventEmitter, Platform } from 'react-native'; + +import { BackdoorEmitter } from './BackdoorEmitter'; + +export const detoxBackdoor = new BackdoorEmitter( + Platform.OS === 'ios' + ? NativeAppEventEmitter + : DeviceEventEmitter +); diff --git a/detox/test/e2e/03.actions.test.js b/detox/test/e2e/03.actions.test.js index bb67df2883..76a3029135 100644 --- a/detox/test/e2e/03.actions.test.js +++ b/detox/test/e2e/03.actions.test.js @@ -26,7 +26,8 @@ describe('Actions', () => { await driver.tapsElement.assertTappedOnce(); }); - it('should be able to send backdoor commands', async () => { + // TODO: we have an unresolved issue with Metro bundler and symlinks + it.skip('should be able to send backdoor commands', async () => { await device.backdoor({ action: 'greet', text: 'Arbitrary Text' }); await expect(element(by.text('Arbitrary Text!!!'))).toBeVisible(); }); diff --git a/detox/test/src/Screens/ActionsScreen.js b/detox/test/src/Screens/ActionsScreen.js index 4e19f1c170..23c43fda85 100644 --- a/detox/test/src/Screens/ActionsScreen.js +++ b/detox/test/src/Screens/ActionsScreen.js @@ -1,9 +1,7 @@ import React, { Component } from 'react'; import { BackHandler, - DeviceEventEmitter, Dimensions, - NativeAppEventEmitter, Platform, RefreshControl, ScrollView, @@ -15,6 +13,7 @@ import { SafeAreaView, requireNativeComponent, } from 'react-native'; +// import { detoxBackdoor } from 'detox/react-native'; import TextInput from '../Views/TextInput'; import Slider from '@react-native-community/slider'; @@ -33,9 +32,6 @@ const styles = StyleSheet.create({ const isIos = Platform.OS === 'ios'; const isAndroid = Platform.OS === 'android'; -const RNEmitter = isIos - ? NativeAppEventEmitter - : DeviceEventEmitter; export default class ActionsScreen extends Component { @@ -54,18 +50,16 @@ export default class ActionsScreen extends Component { componentDidMount() { BackHandler.addEventListener('hardwareBackPress', this.backHandler.bind(this)); + + // detoxBackdoor.setActionHandler('greet', ({ text }) => { + // this.setState({ greeting: text }); + // }); } componentWillUnmount() { - this.onBackdoor.remove(); + // detoxBackdoor.removeActionHandler('greet'); } - onBackdoor = RNEmitter.addListener('detoxBackdoor', ({ action, text }) => { - if (action === 'greet') { - this.setState({ greeting: text }); - } - }); - render() { if (this.state.greeting) return this.renderAfterButton(); if (this.state.backPressed) return this.renderPopupBackPressedDetected(); diff --git a/detox/test/types/detox-module-tests.ts b/detox/test/types/detox-module-tests.ts index d3ae389e91..c6c3ee7882 100644 --- a/detox/test/types/detox-module-tests.ts +++ b/detox/test/types/detox-module-tests.ts @@ -70,6 +70,7 @@ describe('Test', () => { .toBeVisible() .withTimeout(2000); await device.pressBack(); + await device.backdoor({ action: 'someAction', anyArgument: 42 }); await waitFor(element(by.text('Text5'))) .toBeVisible() .whileElement(by.id('ScrollView630')) diff --git a/docs/api/device.md b/docs/api/device.md index 60211c7791..1968ece5b1 100644 --- a/docs/api/device.md +++ b/docs/api/device.md @@ -385,12 +385,14 @@ Learn how to use Backdoor API in our [Mocking Guide](../guide/mocking.md#dynamic ::: -Send a backdoor message (any serializable object) to the app being tested, e.g.: + +Sends a backdoor message to the app being tested. +The message should be an object with `action` property and any other custom data. ```js await device.backdoor({ - // the property names are up to you action: 'my-testing-action', + // the rest is optional and arbitrary arg1: 'value1', arg2: 2 }); @@ -399,21 +401,26 @@ await device.backdoor({ On the application side, you have to implement a handler for the backdoor message, e.g.: ```js -import { - DeviceEventEmitter, - NativeAppEventEmitter, - Platform -} from 'react-native'; - -const RNEmitter = Platform.OS === "ios" - ? NativeAppEventEmitter - : DeviceEventEmitter; +import { detoxBackdoor } from 'detox/react-native'; -RNEmitter.addListener("detoxBackdoor", (msg) => { - /* ... */ +detoxBackdoor.setActionHandler('my-testing-action', ({ arg1, arg2 }) => { + // ... }); + +// There can be only one handler per action, so you're advised to remove it when it's no longer needed +detoxBackdoor.removeActionHandler('my-testing-action'); + +// You can supress errors about overwriting existing handlers +detoxBackdoor.strict = false; + +// You can set a global handler for all unhandled actions +detoxBackdoor.onUnhandledAction = ({ action, ...params }) => { + // ... +}; ``` +Make sure your code using `detoxBackdoor` is not included in production builds. + ### `device.captureViewHierarchy([name])` **iOS Only** Saves a view hierarchy snapshot (`*.viewhierarchy`) of the diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index 0c37b59fbd..7026da86d4 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -155,39 +155,48 @@ export class TimeService { Now, for testing purposes, we can create a mocked counterpart that allows its internal time to be set dynamically: ```js title=src/services/TimeService.e2e.js -import {DeviceEventEmitter, NativeAppEventEmitter, Platform} from 'react-native'; - -const RNEmitter = Platform.OS === "ios" ? NativeAppEventEmitter : DeviceEventEmitter; +import { detoxBackdoor } from 'detox/react-native'; export class FakeTimeService { - #now = Date.now(); - - constructor() { - RNEmitter.addListener("detoxBackdoor", ({ action, time }) => { - if (action === "set-mock-time") { - this.#now = time; - } - }); - } + #now = Date.now(); - now() { - return this.#now; - } + constructor() { + detoxBackdoor.setActionHandler('set-mock-time', ({ time }) => this.setNow(time)); + } + + // If you have a single instance through the app, you might not need this. + dispose() { + detoxBackdoor.removeActionHandler('set-mock-time'); + } + + setNow(time) { + this.#now = time; + } + + now() { + return this.#now; + } } ``` -In the mock implementation, `TimeService.e2e.js`, we're listening for `detoxBackdoor` events and, when received, we adjust the internal `#now` value if the `action` is `"set-mock-time"`. +In the mock implementation, `TimeService.e2e.js`, we're listening for `detoxBackdoor` actions with `set-mock-time` name and, when received, we adjust the internal `#now` value if the `action` is `"set-mock-time"`. -:::danger Security Notice +:::tip -Avoid using `detoxBackdoor` listener in your production code, as it might expose a security vulnerability. +- **One-at-a-Time:** Only a single action handler can be set per action name. Design your logic accordingly if you have multiple instances needing action handling. -Leave these listeners to **mock files only**, and make sure they are excluded from the public release builds. -Backdoor API is a **testing tool**, and it should be isolated to test environments only. +- **Bidirectional flow:** Not supported yet, but planned for the future. Every action handler will be able to return a value back to the caller, which is why the limitation is in place. + +- **Strict Mode (Default=On):** This throws errors upon accidental overwrites of handlers, or firing unknown backdoor actions. Although not recommended, you can disable this behavior by setting: + ```js + detoxBackdoor.strict = false; + ``` + +- **Handle with Care:** Ensure your handlers do not throw unhandled exceptions. At the moment, Detox won’t report them back seamlessly to the test runner. If your app crashes, especially in _devRelease_ mode – read the crash message with attention before pointing fingers at Detox! :slightly_smiling_face: ::: -During your Detox test, you can now utilize the Backdoor API to send a signal to modify this internal state dynamically: +Now, when your app mocks are in place, you can open your Detox test file and instruct the app to use the mocked time service: ```js await device.backdoor({ @@ -196,19 +205,18 @@ await device.backdoor({ }); ``` -This way, you can test your app's behavior in the past or future, without having to wait for the actual time to pass. +Summarizing the above, Backdoor API enables your tests to directly "speak" to your app, altering its state without UI interaction. -Summarizing the above, the **Backdoor API** enables your tests to directly "speak" to your app, altering its state without UI interaction. +:::danger Security Notice -Provided that your app is designed with testability in mind, this can be a powerful tool for testing cases where native tools fall short: -changing the internal clock, simulating network conditions, geolocation, quick authentication, and more. +Avoid using `detoxBackdoor` in your production code, as it might expose a security vulnerability. -:::tip - -Make sure your `detoxBackdoor` event listeners don't throw unhandled exceptions, as this might turn into another -source of confusion. In devRelease mode your app will crash, and Detox will be the bearer of the bad news, although -it has nothing to do with the actual problem. +Leave these action handlers to **mock files only**, and make sure they are excluded from the public release builds. +Backdoor API is a **testing tool**, and it should be isolated to test environments only. ::: +Provided that your app is designed with testability in mind, Backdoor API can be a powerful tool for testing cases where native tools fall short: +changing the internal clock, simulating network conditions, geolocation, quick authentication, and more. + Use it wisely, and... happy Detoxing! From 7c96b5399b3b568a4246a82e3803e60b38f43b43 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Fri, 6 Oct 2023 13:08:05 +0300 Subject: [PATCH 11/15] docs: fix lint --- docs/api/device.md | 1 - docs/guide/mocking.md | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/device.md b/docs/api/device.md index 1968ece5b1..3c8125e728 100644 --- a/docs/api/device.md +++ b/docs/api/device.md @@ -385,7 +385,6 @@ Learn how to use Backdoor API in our [Mocking Guide](../guide/mocking.md#dynamic ::: - Sends a backdoor message to the app being tested. The message should be an object with `action` property and any other custom data. diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index 7026da86d4..6d282b8aab 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -188,6 +188,7 @@ In the mock implementation, `TimeService.e2e.js`, we're listening for `detoxBack - **Bidirectional flow:** Not supported yet, but planned for the future. Every action handler will be able to return a value back to the caller, which is why the limitation is in place. - **Strict Mode (Default=On):** This throws errors upon accidental overwrites of handlers, or firing unknown backdoor actions. Although not recommended, you can disable this behavior by setting: + ```js detoxBackdoor.strict = false; ``` From 37a627245909725a2d2d5eee2b26d2e98a5f29ef Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Sat, 7 Oct 2023 11:41:06 +0300 Subject: [PATCH 12/15] extended BackdoorEmitter --- detox/react-native/BackdoorEmitter.js | 83 ++++++++++++++++++++++--- detox/test/src/Screens/ActionsScreen.js | 4 +- docs/api/device.md | 16 +++-- docs/guide/mocking.md | 12 +++- 4 files changed, 99 insertions(+), 16 deletions(-) diff --git a/detox/react-native/BackdoorEmitter.js b/detox/react-native/BackdoorEmitter.js index 131271d722..a97d285a06 100644 --- a/detox/react-native/BackdoorEmitter.js +++ b/detox/react-native/BackdoorEmitter.js @@ -5,10 +5,7 @@ export class BackdoorEmitter { this._emitter = emitter; this._handlers = {}; this._listeners = {}; - this._subscription = this._emitter.addListener('detoxBackdoor', (event) => { - const listener = this._handlers[event.action] || this.onUnhandledAction; - listener(event); - }); + this._subscription = this._emitter.addListener('detoxBackdoor', this._onBackdoorEvent); } /** @@ -18,7 +15,7 @@ export class BackdoorEmitter { strict = true; /** - * Sets a handler for a backdoor action. + * Registers a handler for a backdoor action. * Note that there can only be one handler per action to avoid confusion, * because in future versions the handlers will be able to return a promise or a value. * @@ -27,11 +24,11 @@ export class BackdoorEmitter { * @throws {Error} if a handler for this action has already been set * @throws {Error} if handler is not a function * @example - * detoxBackdoor.setActionHandler('displayText', ({ text }) => { + * detoxBackdoor.registerActionHandler('displayText', ({ text }) => { * setText(text); * }); */ - setActionHandler(actionName, handler) { + registerActionHandler(actionName, handler) { if (typeof actionName !== 'string') { throw new Error('Detox backdoor action name must be a string'); } @@ -51,10 +48,63 @@ export class BackdoorEmitter { * Removes a handler for a backdoor action. * By default, unremoved handlers will prevent new handlers from being set. */ - removeActionHandler(actionName) { + clearActionHandler(actionName) { delete this._handlers[actionName]; } + /** + * Adds a listener for a backdoor action. + * There can be multiple listeners per action, but you cannot return a value from a listener. + * @param {string} actionName + * @param {function} listener + * @throws {Error} if actionName is not a string + * @throws {Error} if listener is not a function + * @example + * detoxBackdoor.addActionListener('logMessage', ({ message }) => { + * console.log(message); + * }); + */ + addActionListener(actionName, listener) { + if (typeof actionName !== 'string') { + throw new Error('Detox backdoor action name must be a string'); + } + + if (typeof listener !== 'function') { + throw new Error(`Detox backdoor listener for action "${actionName}" must be a function`); + } + + if (!this._listeners[actionName]) { + this._listeners[actionName] = []; + } + + this._listeners[actionName].push(listener); + } + + /** + * Removes a listener for a backdoor action. + * @param {string} actionName + * @param {function} listener + * @throws {Error} if actionName is not a string + * @throws {Error} if listener is not a function + */ + removeActionListener(actionName, listener) { + if (typeof actionName !== 'string') { + throw new Error('Detox backdoor action name must be a string'); + } + + if (typeof listener !== 'function') { + throw new Error(`Detox backdoor listener for action "${actionName}" must be a function`); + } + + const listeners = this._listeners[actionName]; + if (listeners) { + const index = listeners.indexOf(listener); + if (index !== -1) { + listeners.splice(index, 1); + } + } + } + /** * This fallback handler is called when no handler is set for a backdoor action. * By default, it throws an error in strict mode and logs a warning otherwise. @@ -74,4 +124,21 @@ export class BackdoorEmitter { console.warn(message); } }; + + /** @private */ + _onBackdoorEvent = (event) => { + const listeners = this._listeners[event.action]; + if (listeners) { + for (const listener of listeners) { + listener(event); + } + } + + const handler = this._handlers[event.action]; + if (handler) { + handler(event); + } else if (!listeners) { + this.onUnhandledAction(event); + } + }; } diff --git a/detox/test/src/Screens/ActionsScreen.js b/detox/test/src/Screens/ActionsScreen.js index 23c43fda85..30563c0398 100644 --- a/detox/test/src/Screens/ActionsScreen.js +++ b/detox/test/src/Screens/ActionsScreen.js @@ -51,13 +51,13 @@ export default class ActionsScreen extends Component { componentDidMount() { BackHandler.addEventListener('hardwareBackPress', this.backHandler.bind(this)); - // detoxBackdoor.setActionHandler('greet', ({ text }) => { + // detoxBackdoor.registerActionHandler('greet', ({ text }) => { // this.setState({ greeting: text }); // }); } componentWillUnmount() { - // detoxBackdoor.removeActionHandler('greet'); + // detoxBackdoor.clearActionHandler('greet'); } render() { diff --git a/docs/api/device.md b/docs/api/device.md index 3c8125e728..9d16de6605 100644 --- a/docs/api/device.md +++ b/docs/api/device.md @@ -400,21 +400,27 @@ await device.backdoor({ On the application side, you have to implement a handler for the backdoor message, e.g.: ```js -import { detoxBackdoor } from 'detox/react-native'; +import { detoxBackdoor } from 'detox/react-native'; // to be used only from React Native -detoxBackdoor.setActionHandler('my-testing-action', ({ arg1, arg2 }) => { +detoxBackdoor.registerActionHandler('my-testing-action', ({ arg1, arg2 }) => { // ... }); // There can be only one handler per action, so you're advised to remove it when it's no longer needed -detoxBackdoor.removeActionHandler('my-testing-action'); +detoxBackdoor.clearActionHandler('my-testing-action'); // You can supress errors about overwriting existing handlers detoxBackdoor.strict = false; -// You can set a global handler for all unhandled actions +// If you want to have multiple listeners for the same action, you can use `registerActionListener` instead +const listener = ({ arg1, arg2 }) => { /* Note that you can't return a value from a listener */ }; +detoxBackdoor.registerActionListener('my-testing-action', listener); +// You can remove a listener the same way as a handler +detoxBackdoor.removeActionListener('my-testing-action'); + +// You can set a global handler for all actions without a handler and listeners detoxBackdoor.onUnhandledAction = ({ action, ...params }) => { - // ... + // By default, it throws an error or logs a warning (in non-strict mode) }; ``` diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index 6d282b8aab..38ac9efb2a 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -161,7 +161,7 @@ export class FakeTimeService { #now = Date.now(); constructor() { - detoxBackdoor.setActionHandler('set-mock-time', ({ time }) => this.setNow(time)); + detoxBackdoor.registerActionHandler('set-mock-time', ({ time }) => this.setNow(time)); } // If you have a single instance through the app, you might not need this. @@ -197,6 +197,16 @@ In the mock implementation, `TimeService.e2e.js`, we're listening for `detoxBack ::: +If you still would like to have multiple listeners for the same action, you can use the `detoxBackdoor.addActionListener` method instead: + +```js +const listener = ({ time }) => console.log(`Received time: ${time}`); +detoxBackdoor.addActionListener('set-mock-time', listener); +detoxBackdoor.removeActionListener('set-mock-time', listener); +``` + +The weaker side of action listeners is that they will never be able to return a value back to the caller by design. + Now, when your app mocks are in place, you can open your Detox test file and instruct the app to use the mocked time service: ```js From 9df8ba8fb0fd2f8f5fcbcc199733a1b3bb04ab56 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Sat, 7 Oct 2023 11:45:13 +0300 Subject: [PATCH 13/15] docs: fix method name --- docs/api/device.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/device.md b/docs/api/device.md index 9d16de6605..5446dd9361 100644 --- a/docs/api/device.md +++ b/docs/api/device.md @@ -412,9 +412,9 @@ detoxBackdoor.clearActionHandler('my-testing-action'); // You can supress errors about overwriting existing handlers detoxBackdoor.strict = false; -// If you want to have multiple listeners for the same action, you can use `registerActionListener` instead +// If you want to have multiple listeners for the same action, you can use `addActionListener` instead const listener = ({ arg1, arg2 }) => { /* Note that you can't return a value from a listener */ }; -detoxBackdoor.registerActionListener('my-testing-action', listener); +detoxBackdoor.addActionListener('my-testing-action', listener); // You can remove a listener the same way as a handler detoxBackdoor.removeActionListener('my-testing-action'); From 8ded0acd9e8b6684fcd7a97ec618b28037a399e7 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Sat, 7 Oct 2023 11:47:01 +0300 Subject: [PATCH 14/15] docs: fix method signature --- docs/api/device.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/device.md b/docs/api/device.md index 5446dd9361..6b496250b3 100644 --- a/docs/api/device.md +++ b/docs/api/device.md @@ -415,8 +415,8 @@ detoxBackdoor.strict = false; // If you want to have multiple listeners for the same action, you can use `addActionListener` instead const listener = ({ arg1, arg2 }) => { /* Note that you can't return a value from a listener */ }; detoxBackdoor.addActionListener('my-testing-action', listener); -// You can remove a listener the same way as a handler -detoxBackdoor.removeActionListener('my-testing-action'); +// You can remove a listener in a similar way +detoxBackdoor.removeActionListener('my-testing-action', listener); // You can set a global handler for all actions without a handler and listeners detoxBackdoor.onUnhandledAction = ({ action, ...params }) => { From 13551173270f2ff0119b609b49c47c01e5c59310 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Mon, 11 Mar 2024 18:49:14 +0200 Subject: [PATCH 15/15] fixes --- detox/ios/Detox/DetoxManager.swift | 10 ++++----- detox/test/e2e/03.actions.test.js | 3 +-- detox/test/metro.config.js | 29 ++++++++++--------------- detox/test/src/Screens/ActionsScreen.js | 11 ++++++---- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/detox/ios/Detox/DetoxManager.swift b/detox/ios/Detox/DetoxManager.swift index c05ee7adfa..653551194d 100644 --- a/detox/ios/Detox/DetoxManager.swift +++ b/detox/ios/Detox/DetoxManager.swift @@ -402,11 +402,11 @@ public class DetoxManager : NSObject, WebSocketDelegate { rvParams["captureViewHierarchyError"] = "User ran process with -detoxDisableHierarchyDump YES" } self.webSocket.sendAction(done, params: rvParams, messageId: messageId) - case "backdoor": - ReactNativeSupport.emitBackdoorEvent(params) - self.safeSend(action: done, messageId: messageId) - default: - fatalError("Unknown action type received: \(type)") + case "backdoor": + ReactNativeSupport.emitBackdoorEvent(params) + self.safeSend(action: done, messageId: messageId) + default: + fatalError("Unknown action type received: \(type)") } } diff --git a/detox/test/e2e/03.actions.test.js b/detox/test/e2e/03.actions.test.js index f1e95c5bb1..ab72951a8f 100644 --- a/detox/test/e2e/03.actions.test.js +++ b/detox/test/e2e/03.actions.test.js @@ -27,8 +27,7 @@ describe('Actions', () => { await driver.tapsElement.assertTappedOnce(); }); - // TODO: we have an unresolved issue with Metro bundler and symlinks - it.skip('should be able to send backdoor commands', async () => { + it('should be able to send backdoor commands', async () => { await device.backdoor({ action: 'greet', text: 'Arbitrary Text' }); await expect(element(by.text('Arbitrary Text!!!'))).toBeVisible(); }); diff --git a/detox/test/metro.config.js b/detox/test/metro.config.js index f90cae0692..a9a5c1ff92 100644 --- a/detox/test/metro.config.js +++ b/detox/test/metro.config.js @@ -1,17 +1,5 @@ -let createBlacklist; -try { - // RN .64 - createBlacklist = require('metro-config/src/defaults/exclusionList'); -} catch (ex) { - try { - createBlacklist = require('metro-config/src/defaults/blacklist'); - } catch (e) { - createBlacklist = require('metro-bundler').createBlacklist; - } -} - - -const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); +const path = require('node:path'); +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); /** * Metro configuration @@ -22,11 +10,18 @@ const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); const config = {}; const baseConfig = mergeConfig(getDefaultConfig(__dirname), config); - - module.exports = { ...baseConfig, resolver: { - blacklistRE: createBlacklist([/detox\/node_modules\/react-native\/.*/]), + ...baseConfig.resolver, + + nodeModulesPaths: [ + path.resolve('node_modules'), + path.resolve('../node_modules'), + path.resolve('../../node_modules'), + ], }, + watchFolders: [ + path.resolve('..'), + ] }; diff --git a/detox/test/src/Screens/ActionsScreen.js b/detox/test/src/Screens/ActionsScreen.js index 312465c648..6be8db28ca 100644 --- a/detox/test/src/Screens/ActionsScreen.js +++ b/detox/test/src/Screens/ActionsScreen.js @@ -1,17 +1,20 @@ import React, { Component } from 'react'; import { + Text, BackHandler, - Dimensions, - Platform, - RefreshControl, + View, + TouchableOpacity, ScrollView, + RefreshControl, + Platform, + Dimensions, StyleSheet, SafeAreaView, requireNativeComponent, } from 'react-native'; -import { detoxBackdoor } from 'detox/react-native'; import TextInput from '../Views/TextInput'; import Slider from '@react-native-community/slider'; +import { detoxBackdoor } from 'detox/react-native'; let LegacySlider; try {