Localizando Elementos em Documentos HTML (Selenium e Locators do Playwright)

Depois de concluirmos esta aula, nós:

  • Saberemos como podemos encontrar elementos dentro de documentos HTML.
  • Entenderemos como podemos implementar interfaces de usuário (UI) testáveis.
  • Saberemos como podemos escrever testes End-to-End mais rápidos e mais robustos.

1. Introdução ao Sistema Sob Teste (SUT)

Durante esta aula, escreveremos testes End-to-End que localizam elementos dentro de um documento HTML. O código-fonte do nosso documento HTML de exemplo é o seguinte:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>
<head>
    <title>Hello World!</title>
</head>
<body>
    <h1 id="page-header">Hello World!</h1>

    <p class="text-block">Lorem ipsum</p>

    <blockquote>This is a quote.</blockquote>

    <input name="text-input" value="Random text"/>

    <ul>
        <li><a href="http://www.google.com">Go to Google</a></li>
        <li>
            <a href="https://www.testwithspring.com">
                Go to Test With Spring Course
            </a>
        </li>
    </ul>
</body>
</html>

A seguir, vamos descobrir como podemos encontrar elementos em documentos HTML.


2. Localizando Elementos em Documentos HTML

No Selenium WebDriver, a interface SearchContext declara dois métodos fundamentais que podemos utilizar quando queremos encontrar elementos em documentos HTML:

  • O método findElement(by: By) retorna o primeiro elemento (WebElement) que corresponde ao critério de busca informado.
  • O método findElements(by: By) retorna uma lista de elementos (List<WebElement>) que correspondem ao critério de busca informado.

Ambos os métodos recebem um objeto By como parâmetro. Esse objeto especifica o critério de busca utilizado para localizar o elemento ou elementos desejados. Podemos criar esses objetos invocando os métodos fábrica estáticos da classe By (e, de forma análoga no Playwright, utilizamos os métodos page.locator(...), page.getByRole(...) ou page.getByText(...)).

Antes de utilizarmos esses métodos, precisamos compreender dois conceitos essenciais:

  1. Busca relativa ao contexto atual: Esses métodos retornam elementos encontrados a partir do contexto atual. Isso significa que, se invocarmos esses métodos em um objeto WebDriver (ou Page no Playwright), nosso código buscará os elementos em todo o documento HTML. Por outro lado, se invocarmos esses métodos em um objeto WebElement (ou encadearmos em um Locator pai), nosso código buscará os elementos apenas entre os filhos daquele elemento HTML atual.
  2. Como verificar que um elemento NÃO existe na página: Se quisermos garantir que um elemento não está presente no documento HTML, não devemos usar o método findElement(), pois ele lança uma NoSuchElementException caso nenhum elemento seja encontrado! Em vez disso, devemos invocar o método findElements(By...) e verificar se a lista retornada está vazia (assertThat(browser.findElements(...)).isEmpty(), ou assertThat(locator).hasCount(0) no Playwright).

Vamos ver agora, passo a passo, como localizar cada elemento do nosso documento HTML utilizando os diferentes critérios de busca. Começaremos buscando um elemento pelo nome da sua tag HTML.

2.1. Localizando um Elemento pelo Nome da Tag (By.tagName)

Se quisermos encontrar um elemento que possui um nome de tag específico (como <blockquote>), podemos criar o critério de busca invocando o método estático By.tagName(), passando o nome da tag HTML como parâmetro (no Playwright: page.locator("blockquote")).

Depois de escrevermos o teste End-to-End correspondente, nossa classe de teste fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.testwithspring.intermediate.web

import com.testwithspring.intermediate.SeleniumTest
import com.testwithspring.intermediate.SeleniumWebDriver
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver

@SeleniumTest(driver = ChromeDriver::class)
@Tag("endToEndTest")
class RenderExamplePageTest {

    @SeleniumWebDriver
    lateinit var browser: WebDriver

    @BeforeEach
    fun openExamplePage() {
        browser.get("http://localhost:8080")
    }

    @Test
    fun `deve abrir a pagina com a citacao correta usando tagName`() {
        val quote = browser.findElement(By.tagName("blockquote"))
        assertThat(quote.text).isEqualTo("This is a quote.")
    }
}

Vamos avançar e descobrir como podemos encontrar um elemento que possui um id específico.

2.2. Localizando um Elemento pelo seu ID (By.id)

Se quisermos encontrar um elemento que possui um atributo id específico (como <h1 id="page-header">), criamos o critério de busca invocando o método estático By.id(), passando o id do elemento desejado como parâmetro (no Playwright: page.locator("#page-header")).

Depois de escrevermos esse teste End-to-End, a parte relevante da nossa classe de teste fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@SeleniumTest(driver = ChromeDriver::class)
@Tag("endToEndTest")
class RenderExamplePageTest {

    @SeleniumWebDriver
    lateinit var browser: WebDriver

    @BeforeEach
    fun openExamplePage() {
        browser.get("http://localhost:8080")
    }

    @Test
    fun `deve abrir a pagina com o cabecalho correto usando id`() {
        val pageHeader = browser.findElement(By.id("page-header"))
        assertThat(pageHeader.text).isEqualTo("Hello World!")
    }
}

Vamos ver agora como encontrar um elemento utilizando o nome de uma classe CSS como critério de busca.

2.3. Localizando um Elemento pelo Nome da Classe (By.className)

Se quisermos encontrar um elemento que possui uma classe CSS específica (como <p class="text-block">), criamos o critério de busca invocando o método estático By.className(), passando o nome da classe como parâmetro (no Playwright: page.locator(".text-block")).

Depois de escrevermos esse teste End-to-End, nossa classe de teste fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@SeleniumTest(driver = ChromeDriver::class)
@Tag("endToEndTest")
class RenderExamplePageTest {

    @SeleniumWebDriver
    lateinit var browser: WebDriver

    @BeforeEach
    fun openExamplePage() {
        browser.get("http://localhost:8080")
    }

    @Test
    fun `deve abrir a pagina com o bloco de texto correto usando className`() {
        val textBlock = browser.findElement(By.className("text-block"))
        assertThat(textBlock.text).isEqualTo("Lorem ipsum")
    }
}

A seguir, veremos como utilizar o atributo name do elemento como critério de busca.

2.4. Localizando um Elemento pelo Atributo name (By.name)

Se quisermos encontrar um elemento cujo atributo name possui um valor específico (como <input name="text-input" value="Random text"/>), criamos o critério de busca invocando o método estático By.name(), passando o valor do atributo name como parâmetro (no Playwright: page.locator("[name='text-input']")).

Para lermos o valor preenchido dentro de um <input>, invocamos getAttribute("value") no WebElement retornado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@SeleniumTest(driver = ChromeDriver::class)
@Tag("endToEndTest")
class RenderExamplePageTest {

    @SeleniumWebDriver
    lateinit var browser: WebDriver

    @BeforeEach
    fun openExamplePage() {
        browser.get("http://localhost:8080")
    }

    @Test
    fun `deve abrir a pagina com o input contendo o valor correto usando name`() {
        val textInput = browser.findElement(By.name("text-input"))
        assertThat(textInput.getAttribute("value")).isEqualTo("Random text")
    }
}

Vamos descobrir agora como podemos localizar um elemento de link (<a>) que possui um texto de link específico.

Se quisermos encontrar um elemento <a> que possui um texto visível exato (como <a href="http://www.google.com">Go to Google</a>), criamos o critério de busca invocando o método estático By.linkText(), passando o texto esperado do link como parâmetro (no Playwright: page.getByRole(AriaRole.LINK, Page.GetByRoleOptions().setName("Go to Google"))).

Depois de escrevermos esse teste End-to-End, nossa classe de teste fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@SeleniumTest(driver = ChromeDriver::class)
@Tag("endToEndTest")
class RenderExamplePageTest {

    @SeleniumWebDriver
    lateinit var browser: WebDriver

    @BeforeEach
    fun openExamplePage() {
        browser.get("http://localhost:8080")
    }

    @Test
    fun `deve abrir a pagina com link para o Google usando linkText`() {
        val googleLink = browser.findElement(By.linkText("Go to Google"))
        assertThat(googleLink.getAttribute("href")).isEqualTo("http://www.google.com/")
    }
}

A seguir, veremos como encontrar um link utilizando apenas uma parte do seu texto como critério de busca.

Se quisermos encontrar um elemento <a> cujo texto visível contenha uma substring específica (como "Test With Spring" dentro de "Go to Test With Spring Course"), criamos o critério de busca invocando o método estático By.partialLinkText():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@SeleniumTest(driver = ChromeDriver::class)
@Tag("endToEndTest")
class RenderExamplePageTest {

    @SeleniumWebDriver
    lateinit var browser: WebDriver

    @BeforeEach
    fun openExamplePage() {
        browser.get("http://localhost:8080")
    }

    @Test
    fun `deve abrir a pagina com link para o curso Test With Spring usando partialLinkText`() {
        val testWithSpringLink = browser.findElement(
            By.partialLinkText("Test With Spring")
        )
        assertThat(testWithSpringLink.getAttribute("href"))
            .isEqualTo("https://www.testwithspring.com/")
    }
}

Vamos avançar e ver como podemos localizar um elemento utilizando um seletor CSS.

2.7. Localizando um Elemento com Seletor CSS (By.cssSelector)

Embora os métodos básicos fornecidos pela classe By sejam adequados para a maioria das situações, às vezes precisamos de flexibilidade extra. Quando esse for o caso, podemos especificar nosso critério de busca utilizando um seletor CSS através do método estático By.cssSelector().

Veja como localizamos o elemento com a classe .text-block usando By.cssSelector(".text-block"):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@SeleniumTest(driver = ChromeDriver::class)
@Tag("endToEndTest")
class RenderExamplePageTest {

    @SeleniumWebDriver
    lateinit var browser: WebDriver

    @BeforeEach
    fun openExamplePage() {
        browser.get("http://localhost:8080")
    }

    @Test
    fun `deve abrir a pagina com o bloco de texto correto usando cssSelector`() {
        val textBlock = browser.findElement(By.cssSelector(".text-block"))
        assertThat(textBlock.text).isEqualTo("Lorem ipsum")
    }
}

Por fim, vamos ver como especificar nosso critério de busca utilizando uma expressão XPath.

2.8. Localizando um Elemento com Expressão XPath (By.xpath)

Se os métodos básicos da classe By não forem suficientes e não pudermos (ou não quisermos) utilizar um seletor CSS, também podemos especificar nosso critério de busca utilizando uma expressão XPath através do método estático By.xpath().

Veja como localizamos o elemento <blockquote> utilizando a expressão XPath /html/body/blockquote:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@SeleniumTest(driver = ChromeDriver::class)
@Tag("endToEndTest")
class RenderExamplePageTest {

    @SeleniumWebDriver
    lateinit var browser: WebDriver

    @BeforeEach
    fun openExamplePage() {
        browser.get("http://localhost:8080")
    }

    @Test
    fun `deve abrir a pagina com a citacao correta usando xpath`() {
        val quote = browser.findElement(By.xpath("/html/body/blockquote"))
        assertThat(quote.text).isEqualTo("This is a quote.")
    }
}

Agora que já sabemos encontrar elementos com todos os métodos da classe By, vamos conhecer as boas práticas que nos ajudam a construir interfaces de usuário testáveis e escrever testes End-to-End melhores.


3. Boas Práticas para Interfaces Testáveis e Seletores Resilientes

Existem dois princípios fundamentais que nos ajudam a implementar interfaces de usuário testáveis e escrever testes End-to-End mais rápidos e robustos:

Princípio 1: Atribua IDs, classes, nomes (ou data-testid) claros aos elementos HTML

Devemos atribuir atributos id, class e name (ou atributos dedicados de teste como data-testid) significativos aos elementos HTML da nossa aplicação. Isso nos permite localizar esses elementos utilizando critérios de busca que têm muito menos chance de quebrar quando fizermos alterações visuais ou de layout na nossa interface de usuário.

Quando localizamos elementos em documentos HTML, devemos preferir utilizar o id, name ou class semântica do elemento pelos seguintes quatro motivos:

  1. Performance: Utilizar o id ou name do elemento é a maneira mais rápida (em termos de desempenho de busca no DOM) de encontrar elementos em documentos HTML.
  2. Resiliência a mudanças de layout: Testes End-to-End que utilizam id, class semântica ou name do elemento têm muito menos probabilidade de quebrar quando refatoramos a estrutura visual da página.
  3. Internacionalização (i18n) e Testes A/B: Se a nossa interface de usuário precisar suportar múltiplos idiomas ou se fizermos muitos testes A/B de copywriting, não devemos usar o texto do link (linkText / partialLinkText) como critério de busca, porque manter esses seletores sincronizados dá um trabalho enorme.
  4. Fragilidade de Seletores CSS complexos e XPath estrutural: Seletores que dependem da hierarquia do DOM via CSS ou expressões XPath (como /html/body/div[2]/blockquote) são extremamente frágeis e quebrarão nossos testes End-to-End na primeira alteração de layout. Devemos deixá-los apenas como último recurso quando não houver outra opção.

Resumo

Esta aula nos ensinou quatro pontos fundamentais:

  • Podemos encontrar elementos em documentos HTML utilizando os métodos findElement() e findElements() declarados pela interface SearchContext (ou locator() no Playwright).
  • Podemos especificar o critério de busca utilizando os métodos fábrica estáticos fornecidos pela classe By (id, name, className, tagName, linkText, partialLinkText, cssSelector e xpath).
  • Devemos atribuir ids, classes e names claros aos nossos elementos HTML na camada de view, pois isso torna nossa interface de usuário intrinsecamente testável.
  • Sempre que possível, devemos priorizar a busca por id, name ou class semântica para manter nossos testes End-to-End rápidos e imunes a refatorações visuais.