public final class

RecyclerViewActions

extends java.lang.Object

 java.lang.Object

↳androidx.test.espresso.contrib.RecyclerViewActions

Gradle dependencies

compile group: 'androidx.test.espresso', name: 'espresso-contrib', version: '3.6.1'

  • groupId: androidx.test.espresso
  • artifactId: espresso-contrib
  • version: 3.6.1

Artifact androidx.test.espresso:espresso-contrib:3.6.1 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.test.espresso:espresso-contrib com.android.support.test.espresso:espresso-contrib

Androidx class mapping:

androidx.test.espresso.contrib.RecyclerViewActions android.support.test.espresso.contrib.RecyclerViewActions

Overview

ViewActions to interact RecyclerView. RecyclerView works differently than AdapterView. In fact, RecyclerView is not an AdapterView anymore, hence it can't be used in combination with Espresso.

To use ViewActions in this class use Espresso with a Matcher that matches your RecyclerView, then perform a ViewAction from this class.

Summary

Methods
public static RecyclerViewActions.PositionableRecyclerViewActionactionOnHolderItem(<any> viewHolderMatcher, ViewAction viewAction)

Performs a ViewAction on a view matched by viewHolderMatcher.

public static RecyclerViewActions.PositionableRecyclerViewActionactionOnItem(<any> itemViewMatcher, ViewAction viewAction)

Performs a ViewAction on a view matched by viewHolderMatcher.

public static ViewActionactionOnItemAtPosition(int position, ViewAction viewAction)

Performs a ViewAction on a view at position.

public static RecyclerViewActions.PositionableRecyclerViewActionscrollTo(<any> itemViewMatcher)

Returns a ViewAction which scrolls RecyclerView to the view matched by itemViewMatcher.

public static RecyclerViewActions.PositionableRecyclerViewActionscrollToHolder(<any> viewHolderMatcher)

Returns a ViewAction which scrolls RecyclerView to the view matched by viewHolderMatcher.

public static ViewActionscrollToLastPosition()

Returns a ViewAction which scrolls RecyclerView to the last position.

public static ViewActionscrollToPosition(int position)

Returns a ViewAction which scrolls RecyclerView to a position.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Methods

public static RecyclerViewActions.PositionableRecyclerViewAction scrollToHolder(<any> viewHolderMatcher)

Returns a ViewAction which scrolls RecyclerView to the view matched by viewHolderMatcher.

This approach uses RecyclerView.ViewHolders to find the target view. It will create one ViewHolder per item type and bind adapter data to the ViewHolder. If the itemViewMatcher matches a ViewHolder the current position of the View is used to perform a RecyclerView.scrollToPosition(int). Note: scrollTo method is not overloaded, method overloading with generic parameters is not possible.

Parameters:

viewHolderMatcher: a Matcher that matches an item view holder in RecyclerView

public static RecyclerViewActions.PositionableRecyclerViewAction scrollTo(<any> itemViewMatcher)

Returns a ViewAction which scrolls RecyclerView to the view matched by itemViewMatcher.

This approach uses RecyclerView.ViewHolders to find the target view. It will create one ViewHolder per item type and bind adapter data to the ViewHolder. If the itemViewMatcher matches a ViewHolder the current position of the View is used to perform a RecyclerView.scrollToPosition(int).

Parameters:

itemViewMatcher: a Matcher that matches an item view in RecyclerView

public static ViewAction scrollToPosition(int position)

Returns a ViewAction which scrolls RecyclerView to a position.

Parameters:

position: the position of the view to scroll to

public static ViewAction scrollToLastPosition()

Returns a ViewAction which scrolls RecyclerView to the last position.

public static RecyclerViewActions.PositionableRecyclerViewAction actionOnItem(<any> itemViewMatcher, ViewAction viewAction)

Performs a ViewAction on a view matched by viewHolderMatcher.

  1. Scroll Recycler View to the view matched by itemViewMatcher
  2. Perform an action on the matched view

Parameters:

itemViewMatcher: a Matcher that matches an item view in RecyclerView
viewAction: the action that is performed on the view matched by itemViewMatcher

public static RecyclerViewActions.PositionableRecyclerViewAction actionOnHolderItem(<any> viewHolderMatcher, ViewAction viewAction)

Performs a ViewAction on a view matched by viewHolderMatcher.

  1. Scroll Recycler View to the view matched by itemViewMatcher
  2. Perform an action on the matched view
Note: actionOnItem method is not overloaded, method overloading with generic parameters is not possible.

Parameters:

viewHolderMatcher: a Matcher that matches an item view holder in RecyclerView
viewAction: the action that is performed on the view matched by viewHolderMatcher

public static ViewAction actionOnItemAtPosition(int position, ViewAction viewAction)

Performs a ViewAction on a view at position.

  1. Scroll Recycler View to position
  2. Perform an action on the view at position

Parameters:

position: position of a view in RecyclerView
viewAction: the action that is performed on the view matched by itemViewMatcher

Source

/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.test.espresso.contrib;

import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.internal.util.Checks.checkArgument;
import static androidx.test.internal.util.Checks.checkNotNull;
import static org.hamcrest.Matchers.allOf;

import android.util.SparseArray;
import android.view.View;
import android.widget.AdapterView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.util.HumanReadables;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;

/**
 * {@link ViewAction}s to interact {@link RecyclerView}. RecyclerView works differently than {@link
 * AdapterView}. In fact, RecyclerView is not an AdapterView anymore, hence it can't be used in
 * combination with {@link Espresso#onData(Matcher)}.
 *
 * <p>To use {@link ViewAction}s in this class use {@link Espresso#onView(Matcher)} with a <a
 * href="http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html"><code>Matcher
 * </code></a> that matches your {@link RecyclerView}, then perform a {@link ViewAction} from this
 * class.
 */
public final class RecyclerViewActions {
  private static final int NO_POSITION = -1;

  private RecyclerViewActions() {
    // no instance
  }

  /**
   * Most RecyclerViewActions are given a matcher to select a particular view / viewholder within
   * the RecyclerView. In this case the default behaviour is to expect that the matcher matches 1
   * and only one item within the RecyclerView.
   *
   * <p>This interface gives users the ability to override that type of behaviour and explicitly
   * select an item in the RecyclerView at a given position. This is similar to on the
   * onData(...).atPosition() api for AdapterViews.
   */
  public interface PositionableRecyclerViewAction extends ViewAction {

    /**
     * Returns a new ViewAction which will cause the ViewAction to operate upon the position-th
     * element which the matcher has selected.
     *
     * @param position a 0-based index into the list of matching elements within the RecyclerView.
     * @return PositionableRecyclerViewAction a new ViewAction focused on a particular position.
     * @throws IllegalArgumentException if position < 0.
     */
    public PositionableRecyclerViewAction atPosition(int position);
  }

  /**
   * Returns a {@link ViewAction} which scrolls {@link RecyclerView} to the view matched by
   * viewHolderMatcher.
   *
   * <p>This approach uses {@link ViewHolder}s to find the target view. It will create one
   * ViewHolder per item type and bind adapter data to the ViewHolder. If the itemViewMatcher
   * matches a ViewHolder the current position of the View is used to perform a {@link
   * RecyclerView#scrollToPosition(int)}. Note: scrollTo method is not overloaded, method
   * overloading with generic parameters is not possible.
   *
   * @param viewHolderMatcher a <a
   *     href="http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html"><code>Matcher
   *     </code></a> that matches an item view holder in {@link RecyclerView}
   * @throws PerformException if there are more than one items matching given viewHolderMatcher.
   */
  public static <VH extends ViewHolder> PositionableRecyclerViewAction scrollToHolder(
      final Matcher<VH> viewHolderMatcher) {
    return new ScrollToViewAction<VH>(viewHolderMatcher);
  }

  /**
   * Returns a {@link ViewAction} which scrolls {@link RecyclerView} to the view matched by
   * itemViewMatcher.
   *
   * <p>This approach uses {@link ViewHolder}s to find the target view. It will create one
   * ViewHolder per item type and bind adapter data to the ViewHolder. If the itemViewMatcher
   * matches a ViewHolder the current position of the View is used to perform a {@link
   * RecyclerView#scrollToPosition(int)}.
   *
   * @param itemViewMatcher a <a
   *     href="http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html"><code>Matcher
   *     </code></a> that matches an item view in {@link RecyclerView}
   * @throws PerformException if there are more than one items matching given viewHolderMatcher.
   */
  public static <VH extends ViewHolder> PositionableRecyclerViewAction scrollTo(
      final Matcher<View> itemViewMatcher) {
    Matcher<VH> viewHolderMatcher = viewHolderMatcher(itemViewMatcher);
    return new ScrollToViewAction<VH>(viewHolderMatcher);
  }

  /**
   * Returns a {@link ViewAction} which scrolls {@link RecyclerView} to a position.
   *
   * @param position the position of the view to scroll to
   */
  public static <VH extends ViewHolder> ViewAction scrollToPosition(final int position) {
    return new ScrollToPositionViewAction(position);
  }

  /** Returns a {@link ViewAction} which scrolls {@link RecyclerView} to the last position. */
  @NonNull
  public static <VH extends ViewHolder> ViewAction scrollToLastPosition() {
    return new ScrollToLastPositionViewAction();
  }

  /**
   * Performs a {@link ViewAction} on a view matched by viewHolderMatcher.
   *
   * <ol>
   *   <li>Scroll Recycler View to the view matched by itemViewMatcher
   *   <li>Perform an action on the matched view
   * </ol>
   *
   * @param itemViewMatcher a <a
   *     href="http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html"><code>Matcher
   *     </code></a> that matches an item view in {@link RecyclerView}
   * @param viewAction the action that is performed on the view matched by itemViewMatcher
   * @throws PerformException if there are more than one items matching given viewHolderMatcher.
   */
  public static <VH extends ViewHolder> PositionableRecyclerViewAction actionOnItem(
      final Matcher<View> itemViewMatcher, final ViewAction viewAction) {
    Matcher<VH> viewHolderMatcher = viewHolderMatcher(itemViewMatcher);
    return new ActionOnItemViewAction<VH>(viewHolderMatcher, viewAction);
  }

  /**
   * Performs a {@link ViewAction} on a view matched by viewHolderMatcher.
   *
   * <ol>
   *   <li>Scroll Recycler View to the view matched by itemViewMatcher
   *   <li>Perform an action on the matched view
   * </ol>
   *
   * Note: actionOnItem method is not overloaded, method overloading with generic parameters is not
   * possible.
   *
   * @param viewHolderMatcher a <a
   *     href="http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html"><code>Matcher
   *     </code></a> that matches an item view holder in {@link RecyclerView}
   * @param viewAction the action that is performed on the view matched by viewHolderMatcher
   * @throws PerformException if there are more than one items matching given viewHolderMatcher.
   */
  public static <VH extends ViewHolder> PositionableRecyclerViewAction actionOnHolderItem(
      final Matcher<VH> viewHolderMatcher, final ViewAction viewAction) {
    return new ActionOnItemViewAction<VH>(viewHolderMatcher, viewAction);
  }

  private static final class ActionOnItemViewAction<VH extends ViewHolder>
      implements PositionableRecyclerViewAction {
    private final Matcher<VH> viewHolderMatcher;
    private final ViewAction viewAction;
    private final int atPosition;
    private final ScrollToViewAction<VH> scroller;

    private ActionOnItemViewAction(Matcher<VH> viewHolderMatcher, ViewAction viewAction) {
      this(viewHolderMatcher, viewAction, NO_POSITION);
    }

    private ActionOnItemViewAction(
        Matcher<VH> viewHolderMatcher, ViewAction viewAction, int atPosition) {
      this.viewHolderMatcher = checkNotNull(viewHolderMatcher);
      this.viewAction = checkNotNull(viewAction);
      this.atPosition = atPosition;
      this.scroller = new ScrollToViewAction<VH>(viewHolderMatcher, atPosition);
    }

    @SuppressWarnings("unchecked")
    @Override
    public Matcher<View> getConstraints() {
      return allOf(isAssignableFrom(RecyclerView.class), isDisplayed());
    }

    @Override
    public PositionableRecyclerViewAction atPosition(int position) {
      checkArgument(position >= 0, "%d is used as an index - must be >= 0", position);
      return new ActionOnItemViewAction<VH>(viewHolderMatcher, viewAction, position);
    }

    @Override
    public String getDescription() {
      if (atPosition == NO_POSITION) {
        return String.format(
            Locale.ROOT,
            "performing ViewAction: %s on item matching: %s",
            viewAction.getDescription(),
            viewHolderMatcher);

      } else {
        return String.format(
            Locale.ROOT,
            "performing ViewAction: %s on %d-th item matching: %s",
            viewAction.getDescription(),
            atPosition,
            viewHolderMatcher);
      }
    }

    @Override
    public void perform(UiController uiController, View root) {
      RecyclerView recyclerView = (RecyclerView) root;
      try {
        scroller.perform(uiController, root);
        uiController.loopMainThreadUntilIdle();
        // the above scroller has checked bounds, dupes (maybe) and brought the element into screen.
        int max = atPosition == NO_POSITION ? 2 : atPosition + 1;
        int selectIndex = atPosition == NO_POSITION ? 0 : atPosition;
        List<MatchedItem> matchedItems = itemsMatching(recyclerView, viewHolderMatcher, max);
        actionOnItemAtPosition(matchedItems.get(selectIndex).position, viewAction)
            .perform(uiController, root);
        uiController.loopMainThreadUntilIdle();
      } catch (RuntimeException e) {
        throw new PerformException.Builder()
            .withActionDescription(this.getDescription())
            .withViewDescription(HumanReadables.describe(root))
            .withCause(e)
            .build();
      }
    }
  }

  /**
   * Performs a {@link ViewAction} on a view at position.
   *
   * <ol>
   *   <li>Scroll Recycler View to position
   *   <li>Perform an action on the view at position
   * </ol>
   *
   * @param position position of a view in {@link RecyclerView}
   * @param viewAction the action that is performed on the view matched by itemViewMatcher
   */
  public static <VH extends ViewHolder> ViewAction actionOnItemAtPosition(
      final int position, final ViewAction viewAction) {
    return new ActionOnItemAtPositionViewAction<VH>(position, viewAction);
  }

  private static final class ActionOnItemAtPositionViewAction<VH extends ViewHolder>
      implements ViewAction {
    private final int position;
    private final ViewAction viewAction;

    private ActionOnItemAtPositionViewAction(int position, ViewAction viewAction) {
      this.position = position;
      this.viewAction = viewAction;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Matcher<View> getConstraints() {
      return allOf(isAssignableFrom(RecyclerView.class), isDisplayed());
    }

    @Override
    public String getDescription() {
      return "actionOnItemAtPosition performing ViewAction: "
          + viewAction.getDescription()
          + " on item at position: "
          + position;
    }

    @Override
    public void perform(UiController uiController, View view) {
      RecyclerView recyclerView = (RecyclerView) view;

      new ScrollToPositionViewAction(position).perform(uiController, view);
      uiController.loopMainThreadUntilIdle();

      @SuppressWarnings("unchecked")
      VH viewHolderForPosition = (VH) recyclerView.findViewHolderForAdapterPosition(position);
      if (null == viewHolderForPosition) {
        throw new PerformException.Builder()
            .withActionDescription(this.toString())
            .withViewDescription(HumanReadables.describe(view))
            .withCause(new IllegalStateException("No view holder at position: " + position))
            .build();
      }

      View viewAtPosition = viewHolderForPosition.itemView;
      if (null == viewAtPosition) {
        throw new PerformException.Builder()
            .withActionDescription(this.toString())
            .withViewDescription(HumanReadables.describe(viewAtPosition))
            .withCause(new IllegalStateException("No view at position: " + position))
            .build();
      }

      viewAction.perform(uiController, viewAtPosition);
    }
  }

  /**
   * {@link ViewAction} which scrolls {@link RecyclerView} to the view matched by itemViewMatcher.
   * See {@link RecyclerViewActions#scrollTo(Matcher)} for more details.
   */
  private static final class ScrollToViewAction<VH extends ViewHolder>
      implements PositionableRecyclerViewAction {
    private final Matcher<VH> viewHolderMatcher;
    private final int atPosition;

    private ScrollToViewAction(Matcher<VH> viewHolderMatcher) {
      this(viewHolderMatcher, NO_POSITION);
    }

    private ScrollToViewAction(Matcher<VH> viewHolderMatcher, int atPosition) {
      this.viewHolderMatcher = viewHolderMatcher;
      this.atPosition = atPosition;
    }

    @Override
    public PositionableRecyclerViewAction atPosition(int position) {
      checkArgument(position >= 0, "%d is used as an index - must be >= 0", position);
      return new ScrollToViewAction<VH>(viewHolderMatcher, position);
    }

    @SuppressWarnings("unchecked")
    @Override
    public Matcher<View> getConstraints() {
      return allOf(isAssignableFrom(RecyclerView.class), isDisplayed());
    }

    @Override
    public String getDescription() {
      if (atPosition == NO_POSITION) {
        return "scroll RecyclerView to: " + viewHolderMatcher;
      } else {
        return String.format(
            Locale.ROOT,
            "scroll RecyclerView to the: %dth matching %s.",
            atPosition,
            viewHolderMatcher);
      }
    }

    @SuppressWarnings("unchecked")
    @Override
    public void perform(UiController uiController, View view) {
      RecyclerView recyclerView = (RecyclerView) view;
      try {
        int maxMatches = atPosition == NO_POSITION ? 2 : atPosition + 1;
        int selectIndex = atPosition == NO_POSITION ? 0 : atPosition;
        List<MatchedItem> matchedItems = itemsMatching(recyclerView, viewHolderMatcher, maxMatches);

        if (selectIndex >= matchedItems.size()) {
          throw new RuntimeException(
              String.format(
                  "Found %d items matching %s, but position %d was requested.",
                  matchedItems.size(), viewHolderMatcher.toString(), atPosition));
        }
        if (atPosition == NO_POSITION && matchedItems.size() == 2) {
          StringBuilder ambiguousViewError = new StringBuilder();
          ambiguousViewError.append(
              String.format("Found more than one sub-view matching %s", viewHolderMatcher));
          for (MatchedItem item : matchedItems) {
            ambiguousViewError.append(item + "\n");
          }
          throw new RuntimeException(ambiguousViewError.toString());
        }
        recyclerView.scrollToPosition(matchedItems.get(selectIndex).position);
        uiController.loopMainThreadUntilIdle();
      } catch (RuntimeException e) {
        throw new PerformException.Builder()
            .withActionDescription(this.getDescription())
            .withViewDescription(HumanReadables.describe(view))
            .withCause(e)
            .build();
      }
    }
  }

  /**
   * {@link ViewAction} which scrolls {@link RecyclerView} to a given position. See {@link
   * RecyclerViewActions#scrollToPosition(int)} for more details.
   */
  private static final class ScrollToPositionViewAction implements ViewAction {
    private final int position;

    private ScrollToPositionViewAction(int position) {
      this.position = position;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Matcher<View> getConstraints() {
      return allOf(isAssignableFrom(RecyclerView.class), isDisplayed());
    }

    @Override
    public String getDescription() {
      return "scroll RecyclerView to position: " + position;
    }

    @Override
    public void perform(UiController uiController, View view) {
      RecyclerView recyclerView = (RecyclerView) view;
      recyclerView.scrollToPosition(position);
      uiController.loopMainThreadUntilIdle();
    }
  }

  /**
   * {@link ViewAction} which scrolls {@link RecyclerView} to the last position. See {@link
   * RecyclerViewActions#scrollToLastPosition} for more details.
   */
  private static final class ScrollToLastPositionViewAction implements ViewAction {

    @SuppressWarnings("unchecked")
    @Override
    public Matcher<View> getConstraints() {
      return allOf(isAssignableFrom(RecyclerView.class), isDisplayed());
    }

    @Override
    public String getDescription() {
      return "scroll RecyclerView to last position";
    }

    @Override
    public void perform(UiController uiController, View view) {
      RecyclerView recyclerView = (RecyclerView) view;
      recyclerView.scrollToPosition(recyclerView.getAdapter().getItemCount() - 1);
    }
  }

  /**
   * Finds positions of items in {@link RecyclerView} which is matching given viewHolderMatcher.
   * This is similar to positionMatching(RecyclerView, Matcher<VH>), except that it returns list of
   * multiple positions if there are, rather than throwing Ambiguous view error exception.
   *
   * @param recyclerView recycler view which is hosting items.
   * @param viewHolderMatcher a <a
   *     href="http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html"><code>Matcher
   *     </code></a> that matches an item view in {@link RecyclerView}
   * @return list of MatchedItem which contains position and description of items in recyclerView.
   * @throws RuntimeException if more than one item or item could not be found.
   */
  @SuppressWarnings("unchecked")
  private static <T extends VH, VH extends ViewHolder> List<MatchedItem> itemsMatching(
      final RecyclerView recyclerView, final Matcher<VH> viewHolderMatcher, int max) {
    final Adapter<T> adapter = recyclerView.getAdapter();
    SparseArray<VH> viewHolderCache = new SparseArray<VH>();
    List<MatchedItem> matchedItems = new ArrayList<MatchedItem>();
    for (int position = 0; position < adapter.getItemCount(); position++) {
      int itemType = adapter.getItemViewType(position);
      VH cachedViewHolder = viewHolderCache.get(itemType);
      // Create a view holder per type if not exists
      if (null == cachedViewHolder) {
        cachedViewHolder = adapter.createViewHolder(recyclerView, itemType);
        viewHolderCache.put(itemType, cachedViewHolder);
      }
      // Bind data to ViewHolder and apply matcher to view descendants.
      adapter.bindViewHolder((T) cachedViewHolder, position);
      if (viewHolderMatcher.matches(cachedViewHolder)) {
        matchedItems.add(
            new MatchedItem(
                position,
                HumanReadables.getViewHierarchyErrorMessage(
                    cachedViewHolder.itemView,
                    /* problemViews= */ null,
                    "\n\n*** Matched ViewHolder item at position: " + position + " ***",
                    /* problemViewSuffix= */ null)));
        adapter.onViewRecycled((T) cachedViewHolder);
        if (matchedItems.size() == max) {
          break;
        }
      } else {
        adapter.onViewRecycled((T) cachedViewHolder);
      }
    }
    return matchedItems;
  }

  /**
   * Wrapper for matched items in recycler view which contains position and description of matched
   * view.
   */
  private static class MatchedItem {
    public final int position;
    public final String description;

    private MatchedItem(int position, String description) {
      this.position = position;
      this.description = description;
    }

    @Override
    public String toString() {
      return description;
    }
  }

  /**
   * Creates matcher for view holder with given item view matcher.
   *
   * @param itemViewMatcher a item view matcher which is used to match item.
   * @return a matcher which matches a view holder containing item matching itemViewMatcher.
   */
  private static <VH extends ViewHolder> Matcher<VH> viewHolderMatcher(
      final Matcher<View> itemViewMatcher) {
    return new TypeSafeMatcher<VH>() {
      @Override
      public boolean matchesSafely(RecyclerView.ViewHolder viewHolder) {
        return itemViewMatcher.matches(viewHolder.itemView);
      }

      @Override
      public void describeTo(Description description) {
        description.appendText("holder with view: ");
        itemViewMatcher.describeTo(description);
      }
    };
  }
}