Code-Apprentice
Code-Apprentice

Reputation: 83557

Passing arguments to a destination in a test

In my Android app, I have the following navgraph:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph"
    app:startDestination="@id/card_list">

    <fragment
        android:id="@+id/card_list"
        android:name="bbct.android.common.fragment.BaseballCardList"
        android:label="@string/app_name"
        tools:layout="@layout/card_list">
        <action
            android:id="@+id/action_details"
            app:destination="@id/card_details" />
    </fragment>
    <fragment
        android:id="@+id/card_details"
        android:name="bbct.android.common.fragment.BaseballCardDetails"
        android:label="@string/card_details_title"
        tools:layout="@layout/card_details">
        <argument
            android:name="id"
            app:argType="long" />
    </fragment>
</navigation>

As you can see card_details takes an id argument. If this is -1, then the detail view is opened with all blank fields and allows the user to create a new card. If id is a positive value, it is used to fetch a row from the database and populates the form for editing.

Now I am writing a test to verify that while editing a card that the app navigates back to card_list when the user clicks the save button:

public class NavigationTest {
    @Test
    public void clickOnSaveNavigatesFromDetailsBackToList() {
        TestNavHostController navController = new TestNavHostController(ApplicationProvider.getApplicationContext());
        FragmentScenario<BaseballCardDetails> detailsScenario = FragmentScenario.launchInContainer(
            BaseballCardDetails.class,
            null,
            R.style.AppTheme
        );
        detailsScenario.onFragment(fragment -> {
            navController.setGraph(R.navigation.nav_graph);
            BaseballCardDetailsArgs args = new BaseballCardDetailsArgs.Builder(0).build();
            navController.setCurrentDestination(R.id.card_details, args.toBundle());
            Navigation.setViewNavController(fragment.requireView(), navController);
        });
        assertThat(Objects.requireNonNull(navController.getCurrentDestination()).getId())
            .isEqualTo(R.id.card_details);
        Espresso.onView(ViewMatchers.withId(R.id.save_button)).perform(ViewActions.click());
        assertThat(Objects.requireNonNull(navController.getCurrentDestination()).getId())
            .isEqualTo(R.id.card_list);
    }
}

However, when this test navigates to card_details, the form fields are not populated by the card's values. To see what is going on, I added Log.d() calls to my app:

public class BaseballCardDetails extends Fragment {
    // ...

    @Override
    public View onCreateView(
            @NonNull LayoutInflater inflater,
            ViewGroup container,
            Bundle savedInstanceState) {
        // ...

        populateTextEdits();
    }

    private void populateTextEdits() {
        Bundle args = getArguments();
        Log.d(TAG, "args: " + args);

        // ...
    }
}

This outputs

05-31 21:35:13.589 29002 29002 D bbct.android.common.fragment.BaseballCardDetails: args: null

As you can see args is null. Why isn't the arguments from my test being passed through to the fragment here? More importantly, how can I fix it?

Upvotes: 0

Views: 558

Answers (1)

ianhanniballake
ianhanniballake

Reputation: 200020

When you write:

FragmentScenario<BaseballCardDetails> detailsScenario = FragmentScenario.launchInContainer(
        BaseballCardDetails.class,
        null,
        R.style.AppTheme
    );

That null are the arguments passed to your fragment. Hence, getArguments() is null because the arguments you've passed to your fragment is null. Since the NavController isn't creating your fragment in your test (instead, it is launchInContainer that is creating it), the arguments you set via setCurrentDestination only apply to the NavBackEntry's arguments (i.e., if you call navController.getCurrentBackStackEntry().getArguments()).

Instead of passing null to launchInContainer, you'll want to move the construction of your args Bundle to before your call to launchInContainer:

@Test
public void clickOnSaveNavigatesFromDetailsBackToList() {
    TestNavHostController navController = new TestNavHostController(ApplicationProvider.getApplicationContext());

    // Move args construction here
    BaseballCardDetailsArgs args = new BaseballCardDetailsArgs.Builder(0).build();
    Bundle bundleArgs = args.toBundle();

    // And pass the bundleArgs into launchInContainer:
    FragmentScenario<BaseballCardDetails> detailsScenario = FragmentScenario.launchInContainer(
        BaseballCardDetails.class,
        bundleArgs,
        R.style.AppTheme
    );
    detailsScenario.onFragment(fragment -> {
        navController.setGraph(R.navigation.nav_graph);

        // Reuse the same bundleArgs here as well
        navController.setCurrentDestination(R.id.card_details, bundleArgs);
        Navigation.setViewNavController(fragment.requireView(), navController);
    });
    assertThat(Objects.requireNonNull(navController.getCurrentDestination()).getId())
        .isEqualTo(R.id.card_details);
    Espresso.onView(ViewMatchers.withId(R.id.save_button)).perform(ViewActions.click());
    assertThat(Objects.requireNonNull(navController.getCurrentDestination()).getId())
        .isEqualTo(R.id.card_list);
}

Upvotes: 1

Related Questions