Search code examples
androidandroid-espressoandroid-architecture-navigation

Espresso test: TestNavHostController.setCurrentDestination not being set correctly


In my Espresso test I have set up to create an Activity with a Fragment and Navigation Graph:

protected inline fun <reified A : AppCompatActivity, reified F : Fragment> launchFragment(
    @NavigationRes navigationResource: Int,
    @IdRes fragmentId: Int
) {
    launchFragmentInHiltContainer<A, F> {
        navController.setGraph(navigationResource)
        navController.setCurrentDestination(fragmentId) //<-- I believe the problem is here
        this.viewLifecycleOwnerLiveData.observeForever {
            //navController.setCurrentDestination(fragmentId)
            Navigation.setViewNavController(this.requireView(), navController)
        }
    }
}

inline fun <reified A : AppCompatActivity, reified F : Fragment> launchFragmentInHiltContainer(
    fragmentArgs: Bundle? = null,
    fragmentFactory: FragmentFactory? = null,
    crossinline action: Fragment.() -> Unit = {}
) {
    val startActivityIntent = Intent.makeMainActivity(
        ComponentName(
            ApplicationProvider.getApplicationContext(),
            A::class.java
        )
    )

    ActivityScenario.launch<A>(startActivityIntent).onActivity { activity ->
        fragmentFactory?.let {
            activity.supportFragmentManager.fragmentFactory = it
        }
        val fragment: Fragment = activity.supportFragmentManager.fragmentFactory.instantiate(
            Preconditions.checkNotNull(F::class.java.classLoader),
            F::class.java.name
        )

        fragment.arguments = fragmentArgs
        activity.supportFragmentManager
            .beginTransaction()
            .add(android.R.id.content, fragment, "") //<-- I have also tried the Id of the fragment container in the activity
            .commitNow()

        fragment.action()
    }
}

And my Activity and Fragment load up fine, but if I log the result of findNavController().currentDestination it shows me the first screen in my flow (I am trying to test from the 4th screen)

This is not such a big problem except that when in my test setup the Fragment tries to open a new fragment I get the following error:

Caused by: java.lang.IllegalArgumentException: Navigation action/destination packageName:id/action_screen4_to_ErrorScreen cannot be found from the current destination Destination(packageName:id/screen1Fragment) label=screen1 class=screen1Fragment

So the Navigation thinks I'm on the startDestination fragment and not the fragment that I'm trying to load, so I can't navigate away FROM my Fragment in test. I am not trying to pop, I know there would be no backstack.

The following code is what I use to navigate inside my fragment, which works fine "in flow".

findNavController()
    .navigate(
        R.id.action_screen4_to_ErrorScreen,
        null
    )

I have tried

  • accessing the requireView() instead
  • using binding.root.findNavController()
  • requireActivity().findNavController(requireView().id)
  • Navigation.findNavController(requireActivity(), R.id.fragmentContainerId)
  • Navigation.findNavController(requireView())

I have created a repo that demonstrates the problem: https://github.com/qbalsdon/espressoTestFramework


Solution

  • So I found out that this is quite possible, and you can load a fragment into any activty if you use deep linking rather than trying to set the destination after the activity is loaded:

    protected inline fun <reified A : AppCompatActivity> launchFragment(
        @NavigationRes navigationResource: Int,
        @IdRes fragmentId: Int,
        fragmentArgs: Bundle? = null
    ) {
        launchFragmentWithDeepLink<A>(fragmentArgs, null, navigationResource, fragmentId)
    }
    
    inline fun <reified A : AppCompatActivity> launchFragmentWithDeepLink(
        fragmentArgs: Bundle? = null,
        fragmentFactory: FragmentFactory? = null,
        @NavigationRes navigationResource: Int,
        @IdRes fragmentId: Int
    ) {
    
        val launchIntent = NavDeepLinkBuilder(InstrumentationRegistry.getInstrumentation().targetContext)
            .setGraph(navigationResource)
            .setComponentName(A::class.java)
            .setDestination(fragmentId)
            .setArguments(fragmentArgs)
            .createTaskStackBuilder().intents[0]
    
        ActivityScenario.launch<A>(launchIntent).onActivity { activity ->
            fragmentFactory?.let {
                activity.supportFragmentManager.fragmentFactory = it
            }
        }
    }
    

    And then the usage:

    @get:Rule(order = 1)
    override val hiltRule = HiltAndroidRule(this)
    @get:Rule(order = 2)
    val activityTestRule = ActivityScenarioRule(MainActivity::class.java)
    
    private fun showFragment() {
        launchFragment<MainActivity>(
            R.navigation.navigation_graph,
            R.id.my_fragment_id
        )
    }