Listy espresso

Espresso oferuje mechanizmy przewijania do konkretnego elementu lub wykonywania na nim czynności w przypadku 2 typów list: widoków adaptera i widoków Recykling.

W przypadku list, zwłaszcza utworzonych za pomocą obiektu RecyclerView lub AdapterView, widok, który Cię interesuje, może w ogóle nie znajdować się na ekranie, ponieważ wyświetla się tylko niewielka liczba dzieci, które są ponownie wykorzystywane podczas przewijania. W tym przypadku nie można użyć metody scrollTo(), ponieważ wymaga ona istniejącego widoku.

Interakcja z elementami listy widoku adaptera

Zamiast korzystać z metody onView(), rozpocznij wyszukiwanie od parametru onData() i podaj dopasowanie do danych opartych na widoku, który chcesz dopasować. Espresso znajdzie wiersz w obiekcie Adapter i umieści go w widocznym obszarze.

Dopasowanie danych za pomocą dopasowania widoku niestandardowego

Poniższa aktywność zawiera element ListView, na którym opiera się SimpleAdapter, który przechowuje dane z poszczególnych wierszy w obiekcie Map<String, Object>.

Aktywność listy widoczna obecnie na ekranie zawiera listę z 23 elementami. Każdy element ma liczbę zapisaną jako ciąg znaków zmapowaną na inną liczbę, która jest przechowywana jako obiekt.

Każda mapa ma 2 wpisy: klucz "STR" zawierający ciąg znaków, np. "item: x", i klucz "LEN" zawierający wartość Integer, która określa długość treści. Na przykład:

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

Kod kliknięcia w wierszu z wartością „item: 50” wygląda tak:

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

Pamiętaj, że Espresso przewija listę automatycznie w razie potrzeby.

Rozróżnimy Matcher<Object> w środku onData(). Metoda is(instanceOf(Map.class)) zawęża wyszukiwanie do dowolnego elementu właściwości AdapterView, który jest obsługiwany przez obiekt Map.

W naszym przypadku ten aspekt zapytania pasuje do każdego wiersza widoku listy, a my chcemy kliknąć określony element, więc możemy zawęzić wyszukiwanie za pomocą:

Kotlin

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

Java

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

Ten obiekt Matcher<String, Object> będzie pasował do każdej mapy zawierającej wpis z kluczem "STR" i wartością "item: 50". Kod, który należy wyszukać, jest długi, a chcemy wykorzystać go ponownie w innych lokalizacjach, więc napiszmy niestandardowe dopasowanie parametru 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);
    }
};

Używasz BoundedMatcher jako podstawy, ponieważ dopasowujesz tylko obiekty typu Map. Zastąp metodę matchesSafely(), umieszczając wcześniej znaleziony odpowiednik i dopasowując go do obiektu Matcher<String>, który można przekazać jako argument. Dzięki temu możesz zadzwonić pod numer withItemContent(equalTo("foo")). Aby zapewnić przejrzystość kodu, możesz utworzyć kolejną funkcję dopasowywania, która wywołuje już equalTo() i akceptuje obiekt 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));
}

Teraz kod do klikania elementu jest prosty:

Kotlin

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

Java

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

Pełny kod tego testu znajdziesz w metodzie testClickOnItem50() w klasie AdapterViewTest oraz tym niestandardowym module dopasowywania LongListMatchers na GitHubie.

Dopasuj do określonego widoku podrzędnego

W przykładzie powyżej kliknięcie znajduje się w środku całego wiersza wartości ListView. A co, jeśli chcemy wykonać działanie na określonym elemencie podrzędnym wiersza? Na przykład chcemy kliknąć drugą kolumnę w wierszu LongListActivity, która wyświetli wartość String.length treści z pierwszej kolumny:

W tym przykładzie warto wyodrębnić tylko długość konkretnego materiału. Proces ten obejmuje określenie wartości drugiej kolumny z rzędu.

Wystarczy dodać specyfikację onChildView() do implementacji 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());

Interakcja z elementami listy w widoku Recyklingu

Obiekty RecyclerView działają inaczej niż obiekty AdapterView, więc nie można używać onData() do interakcji.

Aby korzystać z obiektów RecyclerView za pomocą Espresso, możesz użyć pakietu espresso-contrib zawierającego kolekcję RecyclerViewActions, których można używać do przewijania elementów lub wykonywania na nich działań:

  • scrollTo() – przewija do pasującego widoku (jeśli istnieje).
  • scrollToHolder() – przewija widok do pasującego właściciela widoku (jeśli istnieje).
  • scrollToPosition() – przewija do określonej pozycji.
  • actionOnHolderItem() – wykonuje działanie związane z wyświetleniem na pasującego posiadaczu wyświetleń.
  • actionOnItem() – wykonuje działanie związane z wyświetleniem dopasowanego widoku.
  • actionOnItemAtPosition() – wykonuje działanie typu ViewAction w odniesieniu do widoku w określonej pozycji.

Poniższe fragmenty kodu zawierają kilka przykładów z przykładu 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()));
}

Dodatkowe materiały

Więcej informacji o używaniu list espresso w testach na Androidzie znajdziesz w tych materiałach.

Próbki

  • DataAdapterSample: pokazuje punkt wejścia onData() dla Espresso dla list i obiektów AdapterView.