Списки Espresso

Espresso предлагает механизмы для прокрутки до определенного элемента или выполнения действий с ним для двух типов списков: представлений адаптера и представлений утилизатора.

При работе со списками, особенно созданными с помощью объекта RecyclerView или AdapterView, нужного представления может не быть на экране, поскольку отображается лишь небольшое количество дочерних элементов, которые обновляются при прокрутке. Метод scrollTo() в этом случае использовать нельзя, так как для него требуется существующее представление.

Взаимодействие с элементами списка в представлении адаптера

Вместо метода onView() начните поиск с onData() и укажите сопоставитель для данных, которые поддерживают представление, которое вы хотите сопоставить. Espresso найдет строку в объекте Adapter и сделает элемент видимым в области просмотра.

Как сопоставить данные с помощью специального средства сопоставления представлений

Действие ниже содержит ListView, который поддерживается SimpleAdapter, хранящим данные для каждой строки в объекте Map<String, Object>.

В списке, который сейчас показан на экране, 23 элемента. Каждый элемент имеет номер, хранящийся в виде строки, сопоставленный с другим номером, который вместо этого хранится в виде объекта.

Каждая карта содержит две записи: ключ "STR", содержащий строку, например "item: x", и ключ "LEN", содержащий Integer, который представляет длину контента. Пример:

{"STR" : "item: 0", "LEN": 7}

Код для клика по строке с товаром 50 выглядит так:

Kotlin

onData(allOf(`is`(instanceOf(Map::class.java)), hasEntry(equalTo("STR"),
        `is`("item: 50")))).perform(click())

Java

onData(allOf(is(instanceOf(Map.class)), hasEntry(equalTo("STR"), is("item: 50"))))
    .perform(click());

Обратите внимание, что Espresso автоматически прокручивает список по мере необходимости.

Давайте разберем Matcher<Object> внутри onData(). Метод is(instanceOf(Map.class)) сужает поиск до любого элемента AdapterView, который поддерживается объектом Map.

В нашем случае этот аспект запроса соответствует каждой строке в списке, но мы хотим нажать на определенный элемент, поэтому сужаем область поиска:

Kotlin

hasEntry(equalTo("STR"), `is`("item: 50"))

Java

hasEntry(equalTo("STR"), is("item: 50"))

Этот Matcher<String, Object> будет соответствовать любой карте, содержащей запись с ключом "STR" и значением "item: 50". Поскольку код для поиска этого элемента длинный и мы хотим повторно использовать его в других местах, давайте напишем для этого собственный сопоставитель withItemContent:

Kotlin

return object : BoundedMatcher<Object, Map>(Map::class.java) {
    override fun matchesSafely(map: Map): Boolean {
        return hasEntry(equalTo("STR"), itemTextMatcher).matches(map)
    }

    override fun describeTo(description: Description) {
        description.appendText("with item content: ")
        itemTextMatcher.describeTo(description)
    }
}

Java

return new BoundedMatcher<Object, Map>(Map.class) {
    @Override
    public boolean matchesSafely(Map map) {
        return hasEntry(equalTo("STR"), itemTextMatcher).matches(map);
    }

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

В качестве основы используется BoundedMatcher, чтобы сопоставлять только объекты типа Map. Переопределите метод matchesSafely(), добавив в него найденный ранее сопоставитель, и сопоставьте его с Matcher<String>, который можно передать в качестве аргумента. Это позволит вам звонить на номер withItemContent(equalTo("foo")). Чтобы сократить код, можно создать другой сопоставитель, который уже вызывает equalTo() и принимает объект String:

Kotlin

fun withItemContent(expectedText: String): Matcher<Object> {
    checkNotNull(expectedText)
    return withItemContent(equalTo(expectedText))
}

Java

public static Matcher<Object> withItemContent(String expectedText) {
    checkNotNull(expectedText);
    return withItemContent(equalTo(expectedText));
}

Теперь код для нажатия на элемент выглядит просто:

Kotlin

onData(withItemContent("item: 50")).perform(click())

Java

onData(withItemContent("item: 50")).perform(click());

Полный код этого теста можно найти в методе testClickOnItem50() класса AdapterViewTest и в этом пользовательском сопоставителе LongListMatchers на GitHub.

Соответствие определенному дочернему представлению

В примере выше клик выполняется в середине строки ListView. Но что, если мы хотим работать с конкретным дочерним элементом строки? Например, мы хотим нажать на второй столбец строки LongListActivity, в котором отображается длина строки контента в первом столбце:

В этом примере было бы полезно извлечь только длину определенного фрагмента контента. Этот процесс включает определение значения второго столбца в строке.

Просто добавьте спецификацию onChildView() в реализацию DataInteraction:

Kotlin

onData(withItemContent("item: 60"))
    .onChildView(withId(R.id.item_size))
    .perform(click())

Java

onData(withItemContent("item: 60"))
    .onChildView(withId(R.id.item_size))
    .perform(click());

Как взаимодействовать с элементами списка RecyclerView

Объекты RecyclerView работают иначе, чем объекты AdapterView, поэтому onData() нельзя использовать для взаимодействия с ними.

Для взаимодействия с RecyclerViews с помощью Espresso можно использовать пакет espresso-contrib, который содержит набор RecyclerViewActions, позволяющих прокручивать до нужных позиций или выполнять действия с элементами:

  • scrollTo() – прокручивает экран до соответствующего представления, если оно существует.
  • scrollToHolder() – прокручивает до соответствующего держателя представления, если он существует.
  • scrollToPosition() – прокрутка до определенной позиции.
  • actionOnHolderItem() – выполняет действие View Action для соответствующего View Holder.
  • actionOnItem() – выполняет действие с представлением, соответствующим условию.
  • actionOnItemAtPosition() – выполняет ViewAction для представления в определенной позиции.

Ниже приведены примеры из проекта RecyclerViewSample.

Kotlin

@Test(expected = PerformException::class)
fun itemWithText_doesNotExist() {
    // Attempt to scroll to an item that contains the special text.
    onView(ViewMatchers.withId(R.id.recyclerView))
        .perform(
            // scrollTo will fail the test if no item matches.
            RecyclerViewActions.scrollTo(
                hasDescendant(withText("not in the list"))
            )
        )
}

Java

@Test(expected = PerformException.class)
public void itemWithText_doesNotExist() {
    // Attempt to scroll to an item that contains the special text.
    onView(ViewMatchers.withId(R.id.recyclerView))
            // scrollTo will fail the test if no item matches.
            .perform(RecyclerViewActions.scrollTo(
                    hasDescendant(withText("not in the list"))
            ));
}

Kotlin

@Test fun scrollToItemBelowFold_checkItsText() {
    // First, scroll to the position that needs to be matched and click on it.
    onView(ViewMatchers.withId(R.id.recyclerView))
        .perform(
            RecyclerViewActions.actionOnItemAtPosition(
                ITEM_BELOW_THE_FOLD,
                click()
            )
        )

    // Match the text in an item below the fold and check that it's displayed.
    val itemElementText = "${activityRule.activity.resources
        .getString(R.string.item_element_text)} ${ITEM_BELOW_THE_FOLD.toString()}"
    onView(withText(itemElementText)).check(matches(isDisplayed()))
}

Java

@Test
public void scrollToItemBelowFold_checkItsText() {
    // First, scroll to the position that needs to be matched and click on it.
    onView(ViewMatchers.withId(R.id.recyclerView))
            .perform(RecyclerViewActions.actionOnItemAtPosition(ITEM_BELOW_THE_FOLD,
            click()));

    // Match the text in an item below the fold and check that it's displayed.
    String itemElementText = activityRule.getActivity().getResources()
            .getString(R.string.item_element_text)
            + String.valueOf(ITEM_BELOW_THE_FOLD);
    onView(withText(itemElementText)).check(matches(isDisplayed()));
}

Kotlin

@Test fun itemInMiddleOfList_hasSpecialText() {
    // First, scroll to the view holder using the isInTheMiddle() matcher.
    onView(ViewMatchers.withId(R.id.recyclerView))
        .perform(RecyclerViewActions.scrollToHolder(isInTheMiddle()))

    // Check that the item has the special text.
    val middleElementText = activityRule.activity.resources
            .getString(R.string.middle)
    onView(withText(middleElementText)).check(matches(isDisplayed()))
}

Java

@Test
public void itemInMiddleOfList_hasSpecialText() {
    // First, scroll to the view holder using the isInTheMiddle() matcher.
    onView(ViewMatchers.withId(R.id.recyclerView))
            .perform(RecyclerViewActions.scrollToHolder(isInTheMiddle()));

    // Check that the item has the special text.
    String middleElementText =
            activityRule.getActivity().getResources()
            .getString(R.string.middle);
    onView(withText(middleElementText)).check(matches(isDisplayed()));
}

Дополнительные ресурсы

Дополнительную информацию об использовании списков Espresso в тестах Android можно найти в следующих ресурсах.

Образцы

  • DataAdapterSample: демонстрирует точку входа onData() для Espresso, для списков и объектов AdapterView.