android-test icon indicating copy to clipboard operation
android-test copied to clipboard

ActivityScenario: expectations for Activity.isTaskRoot() behavior

Open ssaqua opened this issue 6 years ago • 9 comments

Description

When using ActivityScenario to test activities, the activity under test is not considered as the task root. I guess this is by design since FLAG_ACTIVITY_NEW_TASK is masked when launched via BootstrapActivity in InstrumentationActivityInvoker. For activities that make use of Activity.isTaskRoot() this could lead to unexpected behaviors during testing since the activity under test isn't actually the root of the task. This is a behavior change compared to running the test with ActivityTestRule or in some cases compared to running the real application. Is it reasonable to expect the activity under test to be considered as the task root even if it is somehow faked or configurable to return true?

Steps to Reproduce

minimal sample:

// in main source set as the launcher activity
class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (!isTaskRoot) {
            finish()
        }
    }
}

// in test sources
class MainActivityTest {
    @Test
    fun scenario() {
        ActivityScenario.launch(MainActivity::class.java).onActivity { }
    }

    @Test
    fun testRule() {
        val rule = ActivityTestRule(MainActivity::class.java, true, false)
        rule.launchActivity(null)
        assertTrue(rule.activity.isTaskRoot)
    }
}

Expected Results

Be able to test activities that are making use of isTaskRoot() reliably.

Actual Results

scenario test case cannot run onActivity since the activity is immediately finished. testRule test case works as expected.

AndroidX Test and Android OS Versions

1.2.1-alpha02, API 28

ssaqua avatar Aug 15 '19 04:08 ssaqua

You can start the activity with intent ( isTaskRoot() will be true ) :

 val intent = Intent(
           ApplicationProvider.getApplicationContext<%mainApplication%>(),
           %yourActivity%::class.java
       )

       val activityScenario = ActivityScenario.launch<%yourActivity%>(intent)

works for me

EliyahuShwartz avatar Jan 08 '20 08:01 EliyahuShwartz

You can start the activity with intent ( isTaskRoot() will be true ) :

 val intent = Intent(
           ApplicationProvider.getApplicationContext<%mainApplication%>(),
           %yourActivity%::class.java
       )

       val activityScenario = ActivityScenario.launch<%yourActivity%>(intent)

works for me

The issue occurs when you call onActivity after launch. The test will pass if you just call launch by itself regardless of whether you pass an intent or actvity class.

java.lang.NullPointerException: Cannot run onActivity since Activity has been destroyed already

launch(Class<A> activityClass) internally just calls the intent version anyway, so I don't believe we should expect a different result.

ssaqua avatar Jan 08 '20 21:01 ssaqua

I'm not sure this is directly related, but once I migrated to ActivityScenario, my tests involving opening deep links started failing, since NavigationComponent will now recreate the stack, but not pop it when pressing back button for example. The only change was to migrate to ActivityScenario, and we did pass Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK in the intent previously to make sure NavComponent recreates the backstack properly

lwasyl avatar Feb 05 '20 02:02 lwasyl

Is there any update on this? Seeing as ActivityTestRule is deprecated, it'd be nice to have a clear migration for tests using deep links with navigation component

lwasyl avatar Apr 28 '20 14:04 lwasyl

Similar situation here, we have very large tests flowing through several activities. We have a launcher activity that relies on isTaskRoot and migrating from ActivityTestRule to androidx testing is a problem

guillermomuntaner avatar Feb 10 '21 19:02 guillermomuntaner

@brettchabot can you please provide an update here? It stops all of us from migrating from ActivityTestRule for tests using deep links.

gosr avatar Mar 11 '21 12:03 gosr

Same here ! I want to test if the Activity A is shown after I open the Activity B with a Deeplink and close the Activity B

RegFacu avatar Mar 12 '21 14:03 RegFacu

I've made a custom workaround, which registers ActivityLifecycleCallbacks and saves / removes Activities to a stack. Then I check whether a given Activity is the task root as follows:

    **
     * Returns whether the given Activity is the root of the task while ignoring BootstrapActivity
     * started by UI tests.
     */
    public static boolean isTaskRoot(Activity activity) {
        Activity root = mActivityStack.peekLast();
        if (root != null && Objects.equals(root.getLocalClassName(), "androidx.test.core.app.InstrumentationActivityInvoker$BootstrapActivity")) {
            // BootstrapActivity is the real root -> return true if our Activity is second
            return mActivityStack.size() >= 2 && mActivityStack.get(mActivityStack.size() - 2) == activity;
        } else {
            // there is no BootstrapActivity -> just use isTaskRoot()
            return activity.isTaskRoot();
        }
    }

milhauscz avatar Apr 16 '21 10:04 milhauscz

What i've done to solve this issue is to use a bundle argument to disable the check:

if (!isTaskRoot && !intent.getBooleanExtra("ignoreTaskRootCheck", false)) {

and when i launch:

        val scenario = ActivityScenario.launch<SplashActivity>(
            Intent(ApplicationProvider.getApplicationContext(), SplashActivity::class.java).apply {
                putExtra("ignoreTaskRootCheck", true)
            }
        )

It's a bit hacky, but it works.

mattinger avatar Dec 15 '21 00:12 mattinger