Matching a view next to another view

A layout could contain certain views that are not unique by themselves (e.g. a repeating call button in a table of contacts could have the same R.id, contain the same text and have the same properties as other call buttons within the view hierarchy).

For example, in this activity, the view with text “7” repeats across multiple rows:

has sibling

Often, the non-unique view will be paired with some unique label that’s located next to it (e.g. a name of the contact next to the call button). In this case, you can use the hasSibling matcher to narrow down your selection:

onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
  .perform(click());

Matching a view that is inside an ActionBar

The ActionBarTestActivity has two different action bars: a normal ActionBar and a contextual action bar that is created from a options menu. Both action bars have one item that is always visible and two items that are only visible in overflow menu. When an item is clicked, it changes a TextView to the content of the clicked item.

Matching visible icons on both of the action bars is easy:

public void testClickActionBarItem() {
  // We make sure the contextual action bar is hidden.
  onView(withId(R.id.hide_contextual_action_bar))
    .perform(click());

  // Click on the icon - we can find it by the r.Id.
  onView(withId(R.id.action_save))
    .perform(click());

  // Verify that we have really clicked on the icon by checking the TextView content.
  onView(withId(R.id.text_action_bar_result))
    .check(matches(withText("Save")));
}

The code looks identical for the contextual action bar:

public void testClickActionModeItem() {
  // Make sure we show the contextual action bar.
  onView(withId(R.id.show_contextual_action_bar))
    .perform(click());

  // Click on the icon.
  onView((withId(R.id.action_lock)))
    .perform(click());

  // Verify that we have really clicked on the icon by checking the TextView content.
  onView(withId(R.id.text_action_bar_result))
    .check(matches(withText("Lock")));
}

Clicking on items in the overflow menu is a bit trickier for the normal action bar as some devices have a hardware overflow menu button (they will open the overflowing items in an options menu) and some devices have a software overflow menu button (they will open a normal overflow menu). Luckily, Espresso handles that for us.

For the normal action bar:

public void testActionBarOverflow() {
  // Make sure we hide the contextual action bar.
  onView(withId(R.id.hide_contextual_action_bar))
    .perform(click());

  // Open the overflow menu OR open the options menu,
  // depending on if the device has a hardware or software overflow menu button.
  openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext());

  // Click the item.
  onView(withText("World"))
    .perform(click());

  // Verify that we have really clicked on the icon by checking the TextView content.
  onView(withId(R.id.text_action_bar_result))
    .check(matches(withText("World")));
}

This is how this looks on devices with a hardware overflow menu button:

For the contextual action bar it is really easy again:

public void testActionModeOverflow() {
  // Show the contextual action bar.
  onView(withId(R.id.show_contextual_action_bar))
    .perform(click());

  // Open the overflow menu from contextual action mode.
  openContextualActionModeOverflowMenu();

  // Click on the item.
  onView(withText("Key"))
    .perform(click());

  // Verify that we have really clicked on the icon by checking the TextView content.
  onView(withId(R.id.text_action_bar_result))
    .check(matches(withText("Key")));
  }

See the full code for these samples: ActionBarTest.java.

Asserting that a view is not displayed

After performing a series of actions, you will certainly want to assert the state of the UI under test. Sometimes, this may be a negative case (for example, something is not happening). Keep in mind that you can turn any hamcrest view matcher into a ViewAssertion by using ViewAssertions.matches.

In the example below, we take the isDisplayed matcher and reverse it using the standard “not” matcher:

import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.Matchers.not;

onView(withId(R.id.bottom_left))
  .check(matches(not(isDisplayed())));

The above approach works if the view is still part of the hierarchy. If it is not, you will get a NoMatchingViewException and you need to use ViewAssertions.doesNotExist (see below).

Asserting that a view is not present

If the view is gone from the view hierarchy (e.g. this may happen if an action caused a transition to another activity), you should use ViewAssertions.doesNotExist:

import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.doesNotExist;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;

onView(withId(R.id.bottom_left))
  .check(doesNotExist());

Asserting that a data item is not in an adapter

To prove a particular data item is not within an AdapterView you have to do things a little differently. We have to find the AdapterView we’re interested in and interrogate the data its holding. We don’t need to use onData(). Instead, we use onView to find the AdapterView and then use another matcher to work on the data inside the view.

First the matcher:

private static Matcher<View> withAdaptedData(final Matcher<Object> dataMatcher) {
  return new TypeSafeMatcher<View>() {

    @Override
    public void describeTo(Description description) {
      description.appendText("with class name: ");
      dataMatcher.describeTo(description);
    }

    @Override
    public boolean matchesSafely(View view) {
      if (!(view instanceof AdapterView)) {
        return false;
      }
      @SuppressWarnings("rawtypes")
      Adapter adapter = ((AdapterView) view).getAdapter();
      for (int i = 0; i < adapter.getCount(); i++) {
        if (dataMatcher.matches(adapter.getItem(i))) {
          return true;
        }
      }
      return false;
    }
  };
}

Then the all we need is an onView that finds the AdapterView:

@SuppressWarnings("unchecked")
public void testDataItemNotInAdapter(){
  onView(withId(R.id.list))
      .check(matches(not(withAdaptedData(withItemContent("item: 168")))));
  }

And we have an assertion that will fail if an item that is equal to “item: 168” exists in an adapter view with the id list.

For the full sample look at AdapterViewTest#testDataItemNotInAdapter.

Using a custom failure handler

Replacing the default FailureHandler of Espresso with a custom one allows for additional (or different) error handling - e.g. taking a screenshot or dumping extra debug information.

The CustomFailureHandlerTest example demonstrates how to implement a custom failure handler:

private static class CustomFailureHandler implements FailureHandler {
  private final FailureHandler delegate;

  public CustomFailureHandler(Context targetContext) {
    delegate = new DefaultFailureHandler(targetContext);
  }

  @Override
  public void handle(Throwable error, Matcher<View> viewMatcher) {
    try {
      delegate.handle(error, viewMatcher);
    } catch (NoMatchingViewException e) {
      throw new MySpecialException(e);
    }
  }
}

This failure handler throws a MySpecialException instead of a NoMatchingViewException and delegates all other failures to the DefaultFailureHandler. The CustomFailureHandler can be registered with Espresso in the setUp() of the test:

@Override
public void setUp() throws Exception {
  super.setUp();
  getActivity();
  setFailureHandler(new CustomFailureHandler(getInstrumentation().getTargetContext()));
}

For more information see the FailureHandler interface and Espresso.setFailureHandler.

Using inRoot to target non-default windows

Surprising, but true - Android supports multiple windows. Normally, this is transparent (pun intended) to the users and the app developer, yet in certain cases multiple windows are visible (e.g. an auto-complete window gets drawn over the main application window in the search widget). To simplify your life, by default Espresso uses a heuristic to guess which Window you intend to interact with. This heuristic is almost always “good enough”; however, in rare cases, you’ll need to specify which window an interaction should target. You can do this by providing your own root window (aka Root) matcher:

onView(withText("South China Sea"))
  .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
  .perform(click());

As is the case with ViewMatchers, we provide a set of pre-canned RootMatchers. Of course, you can always implement your own Matcher.

Take a look at the sample or the sample on GitHub.

Matching a view that is a footer/header in a ListView

Headers and footers are added to ListViews via the addHeaderView()/addFooterView() APIs. To ensure Espresso.onData() knows what data object to match, make sure to pass a preset data object value as the second parameter to addHeaderView()/addFooterView(). For example:

public static final String FOOTER = "FOOTER";
...
View footerView = layoutInflater.inflate(R.layout.list_item, listView, false);
((TextView) footerView.findViewById(R.id.item_content)).setText("count:");
((TextView) footerView.findViewById(R.id.item_size)).setText(String.valueOf(data.size()));
listView.addFooterView(footerView, FOOTER, true);

Then, you can write a matcher for the footer:

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;

@SuppressWarnings("unchecked")
public static Matcher<Object> isFooter() {
  return allOf(is(instanceOf(String.class)), is(LongListActivity.FOOTER));
}

And loading the view in a test is trivial:

import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
import static com.google.android.apps.common.testing.ui.espresso.sample.LongListMatchers.isFooter;

public void testClickFooter() {
  onData(isFooter())
    .perform(click());
  ...
}

Take a look at the full code sample at: AdapterViewtest#testClickFooter