From dce0939193402073aa55967e929f7b3db1ff76b7 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Sun, 12 May 2024 17:04:01 +0300 Subject: [PATCH 1/2] Removed activity testrule --- .../com/wix/detox/ActivityLaunchHelper.kt | 54 +++++++---------- .../src/full/java/com/wix/detox/Detox.java | 17 +++--- .../com/wix/detox/LaunchIntentsFactory.kt | 5 +- .../java/com/example/DetoxTest.java | 59 ------------------- .../androidTest/java/com/example/DetoxTest.kt | 44 ++++++++++++++ 5 files changed, 77 insertions(+), 102 deletions(-) delete mode 100644 detox/test/android/app/src/androidTest/java/com/example/DetoxTest.java create mode 100644 detox/test/android/app/src/androidTest/java/com/example/DetoxTest.kt diff --git a/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt b/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt index 7ccc113a6d..5c958816fc 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt @@ -1,26 +1,34 @@ package com.wix.detox +import android.app.Activity import android.app.Instrumentation.ActivityMonitor import android.content.Context import android.content.Intent +import androidx.test.core.app.ActivityScenario import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.ActivityTestRule +private const val INTENT_LAUNCH_ARGS_KEY = "launchArgs" +private const val ACTIVITY_LAUNCH_TIMEOUT = 10000L + class ActivityLaunchHelper - @JvmOverloads constructor( - private val activityTestRule: ActivityTestRule<*>, - private val launchArgs: LaunchArgs = LaunchArgs(), - private val intentsFactory: LaunchIntentsFactory = LaunchIntentsFactory(), - private val notificationDataParserGen: (String) -> NotificationDataParser = { path -> NotificationDataParser(path) } +@JvmOverloads +constructor( + private val clazz: Class, + private val launchArgs: LaunchArgs = LaunchArgs(), + private val intentsFactory: LaunchIntentsFactory = LaunchIntentsFactory(), + private val notificationDataParserGen: (String) -> NotificationDataParser = { path -> NotificationDataParser(path) } ) { + + private val activityScenarioRules: MutableList> = mutableListOf() + fun launchActivityUnderTest() { val intent = extractInitialIntent() - activityTestRule.launchActivity(intent) + activityScenarioRules.add(ActivityScenario.launch(intent)) } fun launchMainActivity() { - val activity = activityTestRule.activity - launchActivitySync(intentsFactory.activityLaunchIntent(activity)) + launchActivitySync(intentsFactory.activityLaunchIntent(clazz, context = appContext)) } fun startActivityFromUrl(url: String) { @@ -33,6 +41,10 @@ class ActivityLaunchHelper launchActivitySync(intent) } + fun close() { + activityScenarioRules.forEach { it.close() } + } + private fun extractInitialIntent(): Intent = (if (launchArgs.hasUrlOverride()) { intentsFactory.intentWithUrl(launchArgs.urlOverride, true) @@ -46,33 +58,11 @@ class ActivityLaunchHelper } private fun launchActivitySync(intent: Intent) { - // Ideally, we would just call sActivityTestRule.launchActivity(intent) and get it over with. - // BUT!!! as it turns out, Espresso has an issue where doing this for an activity running in the background - // would have Espresso set up an ActivityMonitor which will spend its time waiting for the activity to load, *without - // ever being released*. It will finally fail after a 45 seconds timeout. - // Without going into full details, it seems that activity test rules were not meant to be used this way. However, - // the all-new ActivityScenario implementation introduced in androidx could probably support this (e.g. by using - // dedicated methods such as moveToState(), which give better control over the lifecycle). - // In any case, this is the core reason for this issue: https://github.com/wix/Detox/issues/1125 - // What it forces us to do, then, is this - - // 1. Launch the activity by "ourselves" from the OS (i.e. using context.startActivity()). - // 2. Set up an activity monitor by ourselves -- such that it would block until the activity is ready. - // ^ Hence the code below. - val activity = activityTestRule.activity - val activityMonitor = ActivityMonitor(activity.javaClass.name, null, true) - activity.startActivity(intent) - - InstrumentationRegistry.getInstrumentation().run { - addMonitor(activityMonitor) - waitForMonitorWithTimeout(activityMonitor, ACTIVITY_LAUNCH_TIMEOUT) - } + activityScenarioRules.add(ActivityScenario.launch(intent)) } private val appContext: Context get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext - companion object { - private const val INTENT_LAUNCH_ARGS_KEY = "launchArgs" - private const val ACTIVITY_LAUNCH_TIMEOUT = 10000L - } + } diff --git a/detox/android/detox/src/full/java/com/wix/detox/Detox.java b/detox/android/detox/src/full/java/com/wix/detox/Detox.java index 93769a11c4..146ea6dded 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/Detox.java +++ b/detox/android/detox/src/full/java/com/wix/detox/Detox.java @@ -1,5 +1,6 @@ package com.wix.detox; +import android.app.Activity; import android.content.Context; import androidx.annotation.NonNull; @@ -79,8 +80,8 @@ private Detox() { * * @param activityTestRule the activityTestRule */ - public static void runTests(ActivityTestRule activityTestRule) { - runTests(activityTestRule, getAppContext()); + public static void runTests(Class clazz) { + runTests(clazz, getAppContext()); } /** @@ -89,8 +90,8 @@ public static void runTests(ActivityTestRule activityTestRule) { * * @param detoxConfig The configurations to apply. */ - public static void runTests(ActivityTestRule activityTestRule, DetoxConfig detoxConfig) { - runTests(activityTestRule, getAppContext(), detoxConfig); + public static void runTests(Class clazz, DetoxConfig detoxConfig) { + runTests(clazz, getAppContext(), detoxConfig); } /** @@ -108,8 +109,8 @@ public static void runTests(ActivityTestRule activityTestRule, DetoxConfig detox * @param activityTestRule the activityTestRule * @param context an object that has a {@code getReactNativeHost()} method */ - public static void runTests(ActivityTestRule activityTestRule, @NonNull final Context context) { - runTests(activityTestRule, context, new DetoxConfig()); + public static void runTests(Class clazz, @NonNull final Context context) { + runTests(clazz, context, new DetoxConfig()); } /** @@ -118,11 +119,11 @@ public static void runTests(ActivityTestRule activityTestRule, @NonNull final Co * * @param detoxConfig The configurations to apply. */ - public static void runTests(ActivityTestRule activityTestRule, @NonNull final Context context, DetoxConfig detoxConfig) { + public static void runTests(Class clazz, @NonNull final Context context, DetoxConfig detoxConfig) { DetoxConfig.CONFIG = detoxConfig; DetoxConfig.CONFIG.apply(); - sActivityLaunchHelper = new ActivityLaunchHelper(activityTestRule); + sActivityLaunchHelper = new ActivityLaunchHelper(clazz); DetoxMain.run(context, sActivityLaunchHelper); } diff --git a/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt b/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt index 80f5801c23..d63e5f8073 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt @@ -15,9 +15,8 @@ class LaunchIntentsFactory { * * @return The resulting intent. */ - fun activityLaunchIntent(activity: Activity) - = Intent(activity.applicationContext, - activity.javaClass).apply { + fun activityLaunchIntent(clazz: Class, context: Context) + = Intent(context, clazz).apply { flags = coreFlags } diff --git a/detox/test/android/app/src/androidTest/java/com/example/DetoxTest.java b/detox/test/android/app/src/androidTest/java/com/example/DetoxTest.java deleted file mode 100644 index 822ad61ece..0000000000 --- a/detox/test/android/app/src/androidTest/java/com/example/DetoxTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.example; - -import android.os.Bundle; - -import com.wix.detox.Detox; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.LargeTest; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.rule.ActivityTestRule; - -@RunWith(AndroidJUnit4.class) -@LargeTest -public class DetoxTest { - - /** - * A selector arg that when set 'true', will launch the {@link SingleInstanceActivity} rather than the default {@link MainActivity}. - * Important so as to allow for some testing of Detox in this particular mode, which has been proven to introduce caveats. - *
Here for internal usage; Not external-API related. - */ - private static final String USE_SINGLE_INSTANCE_ACTIVITY_ARG = "detoxAndroidSingleInstanceActivity"; - - /** Similar concept to that of {@link #USE_SINGLE_INSTANCE_ACTIVITY_ARG}. */ - private static final String USE_CRASHING_ACTIVITY_ARG = "detoxAndroidCrashingActivity"; - - @Rule - public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false); - - @Rule - public ActivityTestRule mSingleInstanceActivityRule = new ActivityTestRule<>(SingleInstanceActivity.class, false, false); - - @Rule - public ActivityTestRule mCrashingActivityTestRule = new ActivityTestRule<>(CrashingActivity.class, false, false); - - @Test - public void runDetoxTests() { - TestButlerProbe.assertReadyIfInstalled(); - - final ActivityTestRule rule = resolveTestRule(); - Detox.runTests(rule); - } - - private ActivityTestRule resolveTestRule() { - final Bundle arguments = InstrumentationRegistry.getArguments(); - final boolean useSingleTaskActivity = Boolean.parseBoolean(arguments.getString(USE_SINGLE_INSTANCE_ACTIVITY_ARG, "false")); - final boolean useCrashingActivity = Boolean.parseBoolean(arguments.getString(USE_CRASHING_ACTIVITY_ARG, "false")); - final ActivityTestRule rule = - useSingleTaskActivity - ? mSingleInstanceActivityRule - : useCrashingActivity - ? mCrashingActivityTestRule - : mActivityRule; - return rule; - } -} diff --git a/detox/test/android/app/src/androidTest/java/com/example/DetoxTest.kt b/detox/test/android/app/src/androidTest/java/com/example/DetoxTest.kt new file mode 100644 index 0000000000..8f36ccef6f --- /dev/null +++ b/detox/test/android/app/src/androidTest/java/com/example/DetoxTest.kt @@ -0,0 +1,44 @@ +package com.example + +import android.app.Activity +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.ActivityTestRule +import com.wix.detox.Detox +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * A selector arg that when set 'true', will launch the [SingleInstanceActivity] rather than the default [MainActivity]. + * Important so as to allow for some testing of Detox in this particular mode, which has been proven to introduce caveats. + * Here for internal usage; Not external-API related. + */ +private const val USE_SINGLE_INSTANCE_ACTIVITY_ARG = "detoxAndroidSingleInstanceActivity" + +/** Similar concept to that of [.USE_SINGLE_INSTANCE_ACTIVITY_ARG]. */ +private const val USE_CRASHING_ACTIVITY_ARG = "detoxAndroidCrashingActivity" + +@RunWith(AndroidJUnit4::class) +@LargeTest +class DetoxTest { + + @Test + fun runDetoxTests() { + TestButlerProbe.assertReadyIfInstalled() + val rule = resolveTestRule() + Detox.runTests(rule) + } + + private fun resolveTestRule(): Class { + val arguments = + InstrumentationRegistry.getArguments() + val useSingleTaskActivity = + arguments.getString(USE_SINGLE_INSTANCE_ACTIVITY_ARG, "false") + .toBoolean() + val useCrashingActivity = + arguments.getString(USE_CRASHING_ACTIVITY_ARG, "false").toBoolean() + return if (useSingleTaskActivity) SingleInstanceActivity::class.java else if (useCrashingActivity) SingleInstanceActivity::class.java else MainActivity::class.java + } +} From 144fd3d52b4b1b578478fb3fbb2c76ecec67fe12 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Sun, 12 May 2024 19:14:21 +0300 Subject: [PATCH 2/2] Fixed android tests --- .../com/wix/detox/ActivityLaunchHelper.kt | 14 +++++------- .../com/wix/detox/ActivityScenarioWrapper.kt | 22 +++++++++++++++++++ .../detox/ActivityScenarioWrapperManager.kt | 15 +++++++++++++ .../src/full/java/com/wix/detox/Detox.java | 1 + .../com/wix/detox/ActivityLaunchHelperTest.kt | 15 +++++++------ 5 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 detox/android/detox/src/full/java/com/wix/detox/ActivityScenarioWrapper.kt create mode 100644 detox/android/detox/src/full/java/com/wix/detox/ActivityScenarioWrapperManager.kt diff --git a/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt b/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt index 5c958816fc..34884fa780 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt @@ -9,22 +9,20 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.ActivityTestRule private const val INTENT_LAUNCH_ARGS_KEY = "launchArgs" -private const val ACTIVITY_LAUNCH_TIMEOUT = 10000L -class ActivityLaunchHelper -@JvmOverloads -constructor( +class ActivityLaunchHelper @JvmOverloads constructor( private val clazz: Class, private val launchArgs: LaunchArgs = LaunchArgs(), private val intentsFactory: LaunchIntentsFactory = LaunchIntentsFactory(), - private val notificationDataParserGen: (String) -> NotificationDataParser = { path -> NotificationDataParser(path) } + private val notificationDataParserGen: (String) -> NotificationDataParser = { path -> NotificationDataParser(path) }, + private val activityScenarioWrapperManager: ActivityScenarioWrapperManager = ActivityScenarioWrapperManager() ) { - private val activityScenarioRules: MutableList> = mutableListOf() + private val activityScenarioRules: MutableList = mutableListOf() fun launchActivityUnderTest() { val intent = extractInitialIntent() - activityScenarioRules.add(ActivityScenario.launch(intent)) + activityScenarioRules.add(activityScenarioWrapperManager.launch(intent)) } fun launchMainActivity() { @@ -58,7 +56,7 @@ constructor( } private fun launchActivitySync(intent: Intent) { - activityScenarioRules.add(ActivityScenario.launch(intent)) + activityScenarioRules.add(activityScenarioWrapperManager.launch(intent)) } private val appContext: Context diff --git a/detox/android/detox/src/full/java/com/wix/detox/ActivityScenarioWrapper.kt b/detox/android/detox/src/full/java/com/wix/detox/ActivityScenarioWrapper.kt new file mode 100644 index 0000000000..22a814e892 --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/ActivityScenarioWrapper.kt @@ -0,0 +1,22 @@ +package com.wix.detox + +import android.app.Activity +import android.content.Intent +import androidx.test.core.app.ActivityScenario + +class ActivityScenarioWrapper private constructor(private val activityScenario: ActivityScenario) { + + fun close() { + activityScenario.close() + } + + companion object { + fun launch(clazz: Class): ActivityScenarioWrapper { + return ActivityScenarioWrapper(ActivityScenario.launch(clazz)) + } + + fun launch(intent: Intent): ActivityScenarioWrapper { + return ActivityScenarioWrapper(ActivityScenario.launch(intent)) + } + } +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/ActivityScenarioWrapperManager.kt b/detox/android/detox/src/full/java/com/wix/detox/ActivityScenarioWrapperManager.kt new file mode 100644 index 0000000000..add2d5dc98 --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/ActivityScenarioWrapperManager.kt @@ -0,0 +1,15 @@ +package com.wix.detox + +import android.app.Activity +import android.content.Intent + +class ActivityScenarioWrapperManager { + + fun launch(clazz: Class): ActivityScenarioWrapper { + return ActivityScenarioWrapper.launch(clazz) + } + + fun launch(intent: Intent): ActivityScenarioWrapper { + return ActivityScenarioWrapper.launch(intent) + } +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/Detox.java b/detox/android/detox/src/full/java/com/wix/detox/Detox.java index 146ea6dded..1acb785c35 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/Detox.java +++ b/detox/android/detox/src/full/java/com/wix/detox/Detox.java @@ -125,6 +125,7 @@ public static void runTests(Class clazz, @NonNull final Cont sActivityLaunchHelper = new ActivityLaunchHelper(clazz); DetoxMain.run(context, sActivityLaunchHelper); + sActivityLaunchHelper.close(); } public static void launchMainActivity() { diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/ActivityLaunchHelperTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/ActivityLaunchHelperTest.kt index 5817635353..ac4a10709c 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/ActivityLaunchHelperTest.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/ActivityLaunchHelperTest.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.content.Intent import android.os.Bundle import org.mockito.kotlin.* -import androidx.test.rule.ActivityTestRule import org.junit.runner.RunWith import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -23,12 +22,13 @@ class ActivityLaunchHelperTest { private lateinit var intent: Intent private lateinit var launchArgsAsBundle: Bundle private lateinit var notificationDataAsBundle: Bundle - private lateinit var testRule: ActivityTestRule + private lateinit var testClazz: Class private lateinit var launchArgs: LaunchArgs private lateinit var intentsFactory: LaunchIntentsFactory private lateinit var notificationDataParser: NotificationDataParser + private lateinit var activityScenarioWrapperManager: ActivityScenarioWrapperManager - private fun uut() = ActivityLaunchHelper(testRule, launchArgs, intentsFactory, { notificationDataParser }) + private fun uut() = ActivityLaunchHelper(testClazz, launchArgs, intentsFactory, { notificationDataParser }, activityScenarioWrapperManager) @Before fun setup() { @@ -36,7 +36,7 @@ class ActivityLaunchHelperTest { launchArgsAsBundle = mock() notificationDataAsBundle = mock() - testRule = mock() + testClazz = Activity::class.java launchArgs = mock() { on { asIntentBundle() }.thenReturn(launchArgsAsBundle) } @@ -44,13 +44,14 @@ class ActivityLaunchHelperTest { notificationDataParser = mock() { on { toBundle() }.thenReturn(notificationDataAsBundle) } + activityScenarioWrapperManager = mock() } @Test fun `default-activity -- should launch using test rule, with a clean intent`() { givenCleanLaunch() uut().launchActivityUnderTest() - verify(testRule).launchActivity(eq(intent)) + verify(activityScenarioWrapperManager).launch(eq(intent)) } @Test @@ -64,7 +65,7 @@ class ActivityLaunchHelperTest { fun `default activity, with a url -- should launch based on the url`() { givenLaunchWithInitialURL() uut().launchActivityUnderTest() - verify(testRule).launchActivity(eq(intent)) + verify(activityScenarioWrapperManager).launch(eq(intent)) verify(intentsFactory).intentWithUrl(initialURL, true) } @@ -79,7 +80,7 @@ class ActivityLaunchHelperTest { fun `default activity, with notification data -- should launch with the data as bundle`() { givenLaunchWithNotificationData() uut().launchActivityUnderTest() - verify(testRule).launchActivity(eq(intent)) + verify(activityScenarioWrapperManager).launch(eq(intent)) verify(intentsFactory).intentWithNotificationData(any(), eq(notificationDataAsBundle), eq(true)) }