Listas do Espresso

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

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

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

Em vez de usar o método onView(), comece sua pesquisa com onData() e forneça uma correspondência com os dados que apoiam a visualização que você gostaria de associar. O Espresso fará todo o trabalho para encontrar a linha no objeto Adapter e tornando 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 no momento na tela 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 do conteúdo. Exemplo:

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

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

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

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

Vamos separar Matcher<Object> dentro de onData(). A O método is(instanceOf(Map.class)) restringe a pesquisa a qualquer item do AdapterView, que é apoiado por um objeto Map.

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

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

Este 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 pesquisar isso é e quiser reutilizá-lo em outros locais, vamos criar uma withItemContent para isso:

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

Use um BoundedMatcher como base para corresponder apenas objetos do tipo Map. Substitua o método matchesSafely(), colocando o matcher encontrado anteriormente e compará-lo com um Matcher<String> que você pode transmitir como um . Isso permite que você chame withItemContent(equalTo("foo")). Para código breve, crie outro matcher que já chame os métodos equalTo() e aceita um objeto 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));
}

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

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

Para ver o código completo desse teste, consulte o método testClickOnItem50(). no AdapterViewTest classe e este LongListMatchers personalizado matcher 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, nós quiser clicar na segunda coluna da linha de LongListActivity, que exibe o String.length do conteúdo na primeira coluna:

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

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

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

Os objetos RecyclerView funcionam de maneira diferente dos objetos AdapterView. Portanto, Não é possível usar onData() para interagir com eles.

Para interagir com RecyclerViews usando o Espresso, você pode usar o espresso-contrib, que tem uma coleção de RecyclerViewActions que podem ser usadas 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 RecyclerViewSample (em inglês) amostra:

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

Outros recursos

Para mais informações sobre como usar listas do Espresso em testes do Android, consulte o recursos a seguir.

Amostras

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