Как позволить Espresso щелкнуть определенный элемент RecyclerView?

Я пытаюсь написать инструментальный тест с Espresso для своего приложения Android, в котором используется RecyclerView. Вот основной макет:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/grid"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

</android.support.design.widget.CoordinatorLayout>

... и макет представления элемента:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/label"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</RelativeLayout>

Это означает, что на экране есть пара TextViews, каждый из которых имеет идентификатор label. Я пытаюсь щелкнуть конкретную метку. Я попытался позволить тестовому рекордеру Espresso сгенерировать код:

onView(allOf(withId(R.id.grid), isDisplayed()))
    .perform(actionOnItemAtPosition(5, click()));

Я скорее хочу что-то вроде:

// Not working
onView(allOf(withId(R.id.grid), isDisplayed()))
    .perform(actionOnItem(withText("Foobar"), click()));

Чтения

Я довольно много читал на эту тему, но до сих пор не могу понять, как тестировать элемент RecyclerView на основе его содержимого, а не на основе его индекса позиции.




Ответы (2)


Вы можете реализовать свой собственный RecyclerView сопоставитель. Предположим, у вас есть RecyclerView, где у каждого элемента есть тема, которую вы хотите сопоставить:

public static Matcher<RecyclerView.ViewHolder> withItemSubject(final String subject) {
    Checks.checkNotNull(subject);
    return new BoundedMatcher<RecyclerView.ViewHolder, MyCustomViewHolder>(
            MyCustomViewHolder.class) {

        @Override
        protected boolean matchesSafely(MyCustomViewHolder viewHolder) {
            TextView subjectTextView = (TextView)viewHolder.itemView.findViewById(R.id.subject_text_view_id);

            return ((subject.equals(subjectTextView.getText().toString())
                    && (subjectTextView.getVisibility() == View.VISIBLE)));
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("item with subject: " + subject);
        }
    };
}

И использование:

onView(withId(R.id.my_recycler_view_id)
    .perform(RecyclerViewActions.actionOnHolderItem(withItemSubject("My subject"), click()));

В принципе, вы можете соответствовать чему угодно. В этом примере мы использовали тему TextView, но это может быть любой элемент внутри элемента RecyclerView.

Еще один момент, который нужно уточнить, это проверка видимости (subjectTextView.getVisibility() == View.VISIBLE). Нам нужно иметь его, потому что иногда другие представления внутри RecyclerView могут иметь ту же тему, но это будет с View.GONE. Таким образом, мы избегаем множественных совпадений нашего пользовательского сопоставления и нацеливаемся только на элемент, который действительно отображает нашу тему.

person denys    schedule 03.11.2016
comment
Кто-нибудь знает, какова функция этого appendText? - person Richardweber32; 29.11.2017
comment
Таким образом, вы настраиваете свое описание ошибки в случае сбоя. - person denys; 30.11.2017

actionOnItem() совпадает с itemView ViewHolder. В этом случае TextView завернут в RelativeLayout, поэтому вам нужно обновить Matcher, чтобы учесть его.

onView(allOf(withId(R.id.grid), isDisplayed()))
    .perform(actionOnItem(withChild(withText("Foobar")), click()));

Вы также можете использовать hasDescendant() для переноса вашего сопоставления является случаем более сложной вложенности.

person Be_Negative    schedule 02.11.2016
comment
Обходит ли hasDescendant() автоматически уровни иерархии независимо от того, насколько глубоко вложены представления? Или мне нужно использовать hasDescendant(hasDescendant(...))? - person JJD; 02.11.2016
comment
Да, hasDescendant() будет обрабатывать всю иерархию представлений от корня, который находится в части onView(). В случае действий recyclerview корнем является itemView. - person Be_Negative; 02.11.2016