android-test
android-test copied to clipboard
ActivityScenario: expectations for Activity.isTaskRoot() behavior
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
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
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.
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
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
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
@brettchabot can you please provide an update here? It stops all of us from migrating from ActivityTestRule for tests using deep links.
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
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();
}
}
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.