Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Android): Remove ActivityTestRule and support ActivityScenario instead #4488

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
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

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) }
private const val INTENT_LAUNCH_ARGS_KEY = "launchArgs"

class ActivityLaunchHelper @JvmOverloads constructor(
private val clazz: Class<out Activity>,
private val launchArgs: LaunchArgs = LaunchArgs(),
private val intentsFactory: LaunchIntentsFactory = LaunchIntentsFactory(),
private val notificationDataParserGen: (String) -> NotificationDataParser = { path -> NotificationDataParser(path) },
private val activityScenarioWrapperManager: ActivityScenarioWrapperManager = ActivityScenarioWrapperManager()
) {

private val activityScenarioRules: MutableList<ActivityScenarioWrapper> = mutableListOf()

fun launchActivityUnderTest() {
val intent = extractInitialIntent()
activityTestRule.launchActivity(intent)
activityScenarioRules.add(activityScenarioWrapperManager.launch(intent))
}

fun launchMainActivity() {
val activity = activityTestRule.activity
launchActivitySync(intentsFactory.activityLaunchIntent(activity))
launchActivitySync(intentsFactory.activityLaunchIntent(clazz, context = appContext))
}

fun startActivityFromUrl(url: String) {
Expand All @@ -33,6 +39,10 @@ class ActivityLaunchHelper
launchActivitySync(intent)
}

fun close() {
activityScenarioRules.forEach { it.close() }
}

private fun extractInitialIntent(): Intent =
(if (launchArgs.hasUrlOverride()) {
intentsFactory.intentWithUrl(launchArgs.urlOverride, true)
Expand All @@ -46,33 +56,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(activityScenarioWrapperManager.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
}

}
Original file line number Diff line number Diff line change
@@ -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<Activity>) {

fun close() {
activityScenario.close()
}

companion object {
fun launch(clazz: Class<Activity>): ActivityScenarioWrapper {
return ActivityScenarioWrapper(ActivityScenario.launch(clazz))
}

fun launch(intent: Intent): ActivityScenarioWrapper {
return ActivityScenarioWrapper(ActivityScenario.launch(intent))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.wix.detox

import android.app.Activity
import android.content.Intent

class ActivityScenarioWrapperManager {

fun launch(clazz: Class<Activity>): ActivityScenarioWrapper {
return ActivityScenarioWrapper.launch(clazz)
}

fun launch(intent: Intent): ActivityScenarioWrapper {
return ActivityScenarioWrapper.launch(intent)
}
}
18 changes: 10 additions & 8 deletions detox/android/detox/src/full/java/com/wix/detox/Detox.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.wix.detox;

import android.app.Activity;
import android.content.Context;

import androidx.annotation.NonNull;
Expand Down Expand Up @@ -79,8 +80,8 @@ private Detox() {
*
* @param activityTestRule the activityTestRule
*/
public static void runTests(ActivityTestRule activityTestRule) {
runTests(activityTestRule, getAppContext());
public static void runTests(Class<? extends Activity> clazz) {
runTests(clazz, getAppContext());
}

/**
Expand All @@ -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<? extends Activity> clazz, DetoxConfig detoxConfig) {
runTests(clazz, getAppContext(), detoxConfig);
}

/**
Expand All @@ -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<? extends Activity> clazz, @NonNull final Context context) {
runTests(clazz, context, new DetoxConfig());
}

/**
Expand All @@ -118,12 +119,13 @@ 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<? extends Activity> 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);
sActivityLaunchHelper.close();
}

public static void launchMainActivity() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ class LaunchIntentsFactory {
*
* @return The resulting intent.
*/
fun activityLaunchIntent(activity: Activity)
= Intent(activity.applicationContext,
activity.javaClass).apply {
fun activityLaunchIntent(clazz: Class<out Activity>, context: Context)
= Intent(context, clazz).apply {
flags = coreFlags
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,34 +22,36 @@ class ActivityLaunchHelperTest {
private lateinit var intent: Intent
private lateinit var launchArgsAsBundle: Bundle
private lateinit var notificationDataAsBundle: Bundle
private lateinit var testRule: ActivityTestRule<Activity>
private lateinit var testClazz: Class<Activity>
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() {
intent = Intent()
launchArgsAsBundle = mock()
notificationDataAsBundle = mock()

testRule = mock()
testClazz = Activity::class.java
launchArgs = mock() {
on { asIntentBundle() }.thenReturn(launchArgsAsBundle)
}
intentsFactory = mock()
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
Expand All @@ -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)
}

Expand All @@ -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))
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<out Activity> {
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
}
}
Loading