Списки эспрессо

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

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

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

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

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

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

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

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

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

Код клика по строке с «item: 50» выглядит следующим образом:

Котлин

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

Ява

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

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

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

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

Котлин

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

Ява

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

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

Котлин

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)
    }
}

Ява

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 :

Котлин

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

Ява

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

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

Котлин

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

Ява

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

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

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

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

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

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

Котлин

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

Ява

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

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

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

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

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

В следующих фрагментах представлены некоторые примеры из образца RecyclerViewSample :

Котлин

@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"))
            )
        )
}

Ява

@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"))
            ));
}

Котлин

@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()))
}

Ява

@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()));
}

Котлин

@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()))
}

Ява

@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 .