Listas do Espresso

O Espresso oferece mecanismos para rolar ou realizar ações em um determinado item em dois tipos de listas: visualizações do adaptador e visualizações de reciclagem.

Ao lidar com listas, especialmente aquelas criadas com um objeto RecyclerView ou AdapterView, a visualização do seu interesse pode nem estar na tela, porque apenas um pequeno número de filhos é exibido e é recicado conforme você rola a tela. O método scrollTo() não pode ser usado nesse caso porque exige uma visualização já existente.

Interagir com itens da lista de visualizações do adaptador

Em vez de usar o método onView(), inicie sua pesquisa com onData() e forneça um correspondente aos dados que estão apoiando a visualização que você quer associar. O Espresso fará todo o trabalho para encontrar a linha no objeto Adapter e tornar o item visível na janela de visualização.

Associar dados usando um matcher de visualização personalizado

A atividade abaixo contém um ListView, que é apoiado por um SimpleAdapter que contém dados para cada linha em um objeto Map<String, Object>.

A atividade de lista mostrada na tela no momento contém uma lista com
          23 itens. Cada item tem um número, armazenado como uma string, mapeado para um
          número diferente, que é armazenado como um objeto.

Cada mapa tem duas entradas: uma chave "STR", que contém uma string, como "item: x", e uma chave "LEN", que contém um Integer, que representa o comprimento do conteúdo. Por exemplo:

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

O código para um clique na linha com "item: 50" fica assim:

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

Observe que o Espresso rola a lista automaticamente, conforme necessário.

Vamos separar Matcher<Object> dentro de onData(). O método is(instanceOf(Map.class)) restringe a pesquisa a qualquer item da AdapterView, que tem como base um objeto Map.

No nosso caso, esse aspecto da consulta corresponde a cada linha da visualização em lista, mas como queremos clicar especificamente em um item, restringimos ainda mais a pesquisa com:

Kotlin

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

Java

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

Esse Matcher<String, Object> corresponderá a qualquer mapa que contenha uma entrada com a chave "STR" e o valor "item: 50". Como o código para essa pesquisa é longo e queremos reutilizá-lo em outros locais, vamos criar um matcher withItemContent personalizado para isso:

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

Use um BoundedMatcher como base para corresponder apenas a objetos do tipo Map. Substitua o método matchesSafely(), inserindo o correspondente encontrado anteriormente e fazendo a correspondência com um Matcher<String> que você pode transmitir como argumento. Isso permite que você chame withItemContent(equalTo("foo")). Para simplificar o código, você pode criar outro matcher que já chame o equalTo() e aceite um 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));
}

Agora, o código para clicar no item é simples:

Kotlin

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

Java

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

Para ver o código completo desse teste, consulte o método testClickOnItem50() na classe AdapterViewTest e este matcher LongListMatchers personalizado no GitHub.

Associar uma visualização filha específica

O exemplo acima emite um clique no meio da linha inteira de uma ListView. E se quisermos realizar uma operação em um elemento filho específico da linha? Por exemplo, gostaríamos de clicar na segunda coluna da linha de LongListActivity, que exibe o String.length do conteúdo na primeira coluna:

Nesse exemplo, seria vantajoso extrair apenas o comprimento de
          uma parte específica do conteúdo. Esse processo envolve a determinação do valor da segunda coluna em uma linha.

Basta adicionar uma especificação onChildView() à implementação 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());

Interagir com itens da lista de visualizações de reciclagem

Como os objetos RecyclerView funcionam de forma diferente dos objetos AdapterView, onData() não pode ser usado para interagir com eles.

Para interagir com RecyclerViews usando o Espresso, use o pacote espresso-contrib, que tem uma coleção de RecyclerViewActions que pode ser usada para rolar para posições ou realizar ações em itens:

  • scrollTo(): rola para a visualização correspondente, se houver.
  • scrollToHolder(): rola para o fixador da visualização correspondente, se houver.
  • scrollToPosition(): rola para uma posição específica.
  • actionOnHolderItem(): realiza uma ação de visualização em um fixador de visualização correspondente.
  • actionOnItem(): realiza uma ação de visualização em uma visualização correspondente.
  • actionOnItemAtPosition(): realiza uma ViewAction em uma visualização em uma posição específica.

Os snippets a seguir apresentam alguns exemplos do exemplo 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()));
}

Outros recursos

Para saber mais sobre o uso de listas do Espresso em testes do Android, consulte os recursos abaixo.

Exemplos

  • DataAdapterSample (link em inglês): mostra o ponto de entrada onData() do Espresso, para listas e objetos AdapterView.