Issue
Below I have a test class designed to launch a fragment in isolation and test the navController's ability to navigate.
The first test, landingToGameFragmentTest()
works perfectly!
The second test launches a fragment that depends on safe args to be passed to it. Aside from that, there is no difference I can perceive in how they are executed.
// Declare navController at top level so it can be accessed from any test in the class
private lateinit var navController: TestNavHostController
// Use Generic type with fragment as upper bound to pass any type of FragmentScenario
private fun <T : Fragment> init(scenario: FragmentScenario<T>) {
// Create a test navController
navController = TestNavHostController(
ApplicationProvider.getApplicationContext()
)
scenario.onFragment { fragment ->
// Link navController to its graph
navController.setGraph(R.navigation.nav_graph)
// Link fragment to its navController
Navigation.setViewNavController(fragment.requireView(), navController)
}
}
@Test
fun landingToGameFragmentTest() {
init(launchFragmentInContainer<LandingFragment>(themeResId = THEME))
// Click button to navigate to GameFragment
onView(withId(R.id.button_start_game))
.perform(click())
assertEquals("Navigation to GameFragment failed",
R.id.gameFragment,
navController.currentDestination?.id)
}
@Test
fun gameToLandingFragmentTest() {
init(launchFragmentInContainer<GameFragment>(themeResId = THEME, fragmentArgs = Bundle()))
onView(withId(R.id.button_end_game))
.perform(click())
assertEquals("Navigation to LandingFragment failed",
R.id.landingFragment,
navController.currentDestination?.id)
}
I set a default value for its arguments, but I still got a null arguments exception until I passed it an empty bundle. Now the fragment will launch but it appears not to be able to navigate to any other fragment!
I could find no similar questions on SO, and the stack output is beyond me.
After the init(launchFragmentInContainer())
line I stepped through the code and found this throwing an illegalArgumentException
:
public static int parseInt(@RecentlyNonNull String s, int radix) throws NumberFormatException {
throw new RuntimeException("Stub!");
}
Which then leads to getNavigator()
, which passes the name "fragment".
However the only navigators are "navigation" and "test", of which I believe it should be test.
The illegalStateException
is then thrown:
/**
* Retrieves a registered [Navigator] by name.
*
* @param name name of the navigator to return
* @return the registered navigator with the given name
*
* @throws IllegalStateException if the Navigator has not been added
*
* @see NavigatorProvider.addNavigator
*/
@Suppress("UNCHECKED_CAST")
@CallSuper
public open fun <T : Navigator<*>> getNavigator(name: String): T {
require(validateName(name)) { "navigator name cannot be an empty string" }
val navigator = _navigators[name]
?: throw IllegalStateException(
"Could not find Navigator with name \"$name\". You must call " +
"NavController.addNavigator() for each navigation type."
)
return navigator as T
}
Finally navigate()
is called on onView(withId(R.id.button_end_game))
generating:
I likely don't have to keep this test in this specific instance. However, I certainly will need to know how to launch a Fragment in isolation (that depends on safe args) in the future.
Thank you for your consideration!!
Solution
You have two entirely separate problems here:
Your Fragment needs to have arguments passed to it.
Arguments are passed to your fragment via the fragmentArgs
parameter of launchFragmentInContainer
as explained in the Fragment testing guide.
Each Args class, such as your LandingFragmentArgs
has a constructor that lets you construct that Args
class directly. You can then use the toBundle()
method to make the Bundle
that you pass to launchFragmentInContainer
:
val args = GameFragmentArgs(/* pass in your required args here */)
val bundle = args.toBundle()
init(launchFragmentInContainer<GameFragment>(fragmentArgs = bundle, themeResId = THEME))
Your NavController needs to have its state set to the GameFragment destination
Your TestNavHostController
doesn't know that your test needs to start on the destination associated with GameFragment
- by default, it will just be on the startDestination
of your graph (where whatever action you are trying to trigger doesn't exist).
As per the Test Navigation documentation:
TestNavHostController
provides asetCurrentDestination
method that allows you to set the current destination so that the NavController is in the correct state before your test begins.
So you need to make sure you call setCurrentDestination
after your init
call:
val args = GameFragmentArgs(/* pass in your required args here */)
val bundle = args.toBundle()
val scenario = launchFragmentInContainer<GameFragment>(fragmentArgs = bundle, themeResId = THEME)
init(scenario)
// Ensure that the NavController is set to the expected destination
// using the ID from your navigation graph associated with GameFragment
scenario.onFragment {
// Just like setGraph(), this needs to be called on the main thread
navController.setCurrentDestination(R.id.game_fragment, bundle)
}
Answered By - ianhanniballake
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.