Listas de Espresso

Espresso ofrece mecanismos para desplazarse o actuar sobre un elemento específico para dos tipos de listas: vistas de adaptador y vistas de reciclador.

Cuando trabajas con listas, en especial aquellas creadas con un objeto RecyclerView o AdapterView, es posible que la vista que te interesa ni siquiera aparezca en la pantalla porque solo se muestra una pequeña cantidad de elementos secundarios y se reciclan a medida que te desplazas. En este caso, no se puede usar el método scrollTo() porque requiere una vista existente.

Cómo interactuar con elementos de lista de la vista de adaptador

En lugar de usar el método onView(), comienza la búsqueda con onData() y proporciona un comparador con los datos que respaldan la vista que quieres. Espresso hará todo el trabajo de encontrar la fila en el objeto Adapter y hacer que el elemento sea visible en el viewport.

Cómo hacer coincidir los datos mediante un buscador de coincidencias de vistas personalizado

La actividad que se muestra a continuación contiene un ListView, respaldado por un SimpleAdapter que contiene datos para cada fila en un objeto Map<String, Object>.

La actividad de lista que se muestra actualmente en la pantalla contiene una lista con 23 elementos. Cada elemento tiene un número, almacenado como una string, asignado a un número diferente, que se almacena como un objeto.

Cada mapa tiene dos entradas: una clave "STR" que contiene una cadena, como "item: x", y una clave "LEN" que contiene un Integer, que representa la longitud del contenido. Por ejemplo:

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

Este es el código para hacer clic en la fila con "item: 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());

Ten en cuenta que Espresso se desplaza por la lista automáticamente según sea necesario.

Desmontemos el Matcher<Object> dentro de onData(). El método is(instanceOf(Map.class)) reduce la búsqueda a cualquier elemento de AdapterView, que está respaldado por un objeto Map.

En nuestro caso, este aspecto de la consulta coincide con cada fila de la vista de lista, pero queremos hacer clic específicamente en un elemento, por lo que acotamos aún más la búsqueda de la siguiente manera:

Kotlin

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

Java

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

Este Matcher<String, Object> coincidirá con cualquier mapa que contenga una entrada con la clave "STR" y el valor "item: 50". Debido a que el código para buscar esto es largo y queremos reutilizarlo en otras ubicaciones, escribiremos un comparador withItemContent personalizado para eso:

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

Usas un BoundedMatcher como base porque solo coincide con objetos de tipo Map. Anula el método matchesSafely(), coloca el comparador que se encontró antes y hazlo coincidir con un Matcher<String> que puedas pasar como argumento. Esto te permite llamar a withItemContent(equalTo("foo")). Para abreviar el código, puedes crear otro comparador que ya llame a equalTo() y acepte un objeto 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));
}

El código para hacer clic en el elemento es simple:

Kotlin

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

Java

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

Para obtener el código completo de esta prueba, consulta el método testClickOnItem50() dentro de la clase AdapterViewTest y este comparador LongListMatchers personalizado en GitHub.

Cómo hacer coincidir una vista secundaria específica

En el ejemplo anterior, se emite un clic en el medio de toda la fila de una ListView. ¿Qué ocurre si deseas realizar operaciones en un elemento secundario específico de la fila? Por ejemplo, nos gustaría hacer clic en la segunda columna de la fila del LongListActivity, que muestra la longitud de la string del contenido en la primera columna:

En este ejemplo, sería beneficioso extraer solo la longitud de un fragmento de contenido en particular. Este proceso implica determinar el valor de la segunda columna de una fila.

Solo agrega una especificación de onChildView() a tu implementación de 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());

Cómo interactuar con los elementos de la lista de la vista de reciclador

Los objetos RecyclerView funcionan de manera diferente que los objetos AdapterView, por lo que no se puede usar onData() para interactuar con ellos.

Para interactuar con RecyclerViews usando Espresso, puedes usar el paquete espresso-contrib, que tiene una colección de RecyclerViewActions que se puede usar para desplazarse a posiciones o realizar acciones en los elementos:

  • scrollTo(): Se desplaza hasta la vista coincidente, si existe.
  • scrollToHolder(): Se desplaza hasta el contenedor de la vista coincidente, si existe.
  • scrollToPosition(): se desplaza hasta una posición específica.
  • actionOnHolderItem(): realiza una acción de vista en un titular de vista coincidente.
  • actionOnItem(): realiza una acción de vista en una vista coincidente.
  • actionOnItemAtPosition(): realiza una acción de vista en la vista de una posición específica.

En los siguientes fragmentos, se muestran algunos ejemplos del ejemplo de 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()));
}

Recursos adicionales

Para obtener más información sobre el uso de listas de Espresso en pruebas de Android, consulta los siguientes recursos.

Ejemplos

  • DataAdapterSample: muestra el punto de entrada de onData() para Espresso, para listas y objetos AdapterView.