2

BottomNavigationItemView implements the ItemView interface which has the setChecked() method.

I tried to assert with Espresso that one itemView is checked but I got the same error, whatever my expected value is, isChecked() or isNotChecked().

My test:

ViewInteraction buttonHome = onView(
    allOf(withId(R.id.bottomHome),
          childAtPosition(
              childAtPosition(
                  withId(R.id.bottom_navigation),
                  0),
              0),
          isDisplayed()));
    buttonHome.check(matches(isNotChecked()));

The error message

android.support.test.espresso.base.DefaultFailureHandler$AssertionFailedWithCauseError: 'with checkbox state: is <true>' doesn't match the selected view.
Expected: with checkbox state: is <true>
Got: "BottomNavigationItemView{id=2131493015, res-name=bottomHome, visibility=VISIBLE, width=360, height=168, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}"

How could I assert a BottomNavigationItemView is the current selected item of the BottomNavigationView?

oldergod
  • 15,033
  • 7
  • 62
  • 88
  • Maybe in that case try to use classic JUnit assertion instead of Espresso assertion. I mean: `assertEquals(((BottomNavigationItemView) findViewById(R.id.bottomHome)).isChecked(), true);` – piotrek1543 Oct 27 '16 at 19:54

2 Answers2

7

Both isChecked() andisNotChecked() expect Checkable interface to be implemented by the view.

Moreover, BottomNavigationItemView hides its checked status inside of the itemData field. This means that Espresso out of the box does not support this kind of a check. Luckily, Espresso is a very extensible framework and you can easily add functionality to it. In this case, we need to write a custom matcher to do the check.

Implementation of the said matcher might look like this:

public static Matcher<View> withBottomNavItemCheckedStatus(final boolean isChecked) {
    return new BoundedMatcher<View, BottomNavigationItemView>(BottomNavigationItemView.class) {
        boolean triedMatching;

        @Override
        public void describeTo(Description description) {
            if (triedMatching) {
                description.appendText("with BottomNavigationItem check status: " + String.valueOf(isChecked));
                description.appendText("But was: " + String.valueOf(!isChecked));
            }
        }

        @Override
        protected boolean matchesSafely(BottomNavigationItemView item) {
            triedMatching = true;
            return item.getItemData().isChecked() == isChecked;
        }
    };
}

And the usage is:

buttonHome.check(matches(withBottomNavItemCheckedStatus(false)));
Be_Negative
  • 4,892
  • 1
  • 30
  • 33
  • 1
    Nice `Matcher`. Minor remark, avoid using `BottomNavigationItemView` because it is visible to only group id. Try to iterate over children of `bottomNavigationView.getChildAt(0)` which implement `MenuItem`. – Nikola Despotoski Nov 05 '16 at 22:32
  • What if I want to tap on specific item of BottomNavigation? – StuartDTO Apr 23 '21 at 11:31
6

Based on the above answer and comments, I created this matcher:

import android.support.design.widget.BottomNavigationView;
import android.support.test.espresso.matcher.BoundedMatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import java.util.HashSet;
import java.util.Set;
import org.hamcrest.Description;
import org.hamcrest.Matcher;

/**
 * Custom matchers for Espresso.
 */
public class EspressoUtils {
  /**
   * Checks that {@link BottomNavigationView} contains an item with provided id and that it is
   * checked.
   *
   * @param id of the item to find in the navigation view
   * @return a matcher that returns true if the item was found and checked
   */
  public static Matcher<View> hasCheckedItem(final int id) {
    return new BoundedMatcher<View, BottomNavigationView>(BottomNavigationView.class) {
      Set<Integer> checkedIds = new HashSet<>();
      boolean itemFound = false;
      boolean triedMatching = false;

      @Override public void describeTo(Description description) {
        if (!triedMatching) {
          description.appendText("BottomNavigationView");
          return;
        }

        description.appendText("BottomNavigationView to have a checked item with id=");
        description.appendValue(id);
        if (itemFound) {
          description.appendText(", but selection was=");
          description.appendValue(checkedIds);
        } else {
          description.appendText(", but it doesn't have an item with such id");
        }
      }

      @Override protected boolean matchesSafely(BottomNavigationView navigationView) {
        triedMatching = true;

        final Menu menu = navigationView.getMenu();
        for (int i = 0; i < menu.size(); i++) {
          final MenuItem item = menu.getItem(i);
          if (item.isChecked()) {
            checkedIds.add(item.getItemId());
          }
          if (item.getItemId() == id) {
            itemFound = true;
          }
        }
        return checkedIds.contains(id);
      }
    };
  }
}

That can be used like:

  @Test public void verifyWalletIsSelected() throws Exception {
    onView(withId(R.id.navigation)).check(matches(hasCheckedItem(R.id.navigation_wallet)));
  }
arekolek
  • 9,128
  • 3
  • 58
  • 79