Controlando o Navegador e Espera Explícita (Selenium WebDriver e Playwright)

Depois de concluirmos esta aula, nós:

  • Saberemos como podemos abrir uma página web com o Selenium WebDriver (e Playwright).
  • Entenderemos como podemos clicar em um elemento HTML e aguardar a navegação com WebDriverWait.
  • Saberemos como preencher campos de texto, <select>, checkboxes, radio buttons e submeter um formulário HTML.

1. Abrindo uma Página Web

Podemos abrir uma página web invocando o método get() da interface WebDriver (ou page.navigate() no Playwright). Esse método abre a página solicitada enviando uma requisição HTTP GET e recebe o endereço URL da página web como parâmetro.

Depois de escrevermos um teste End-to-End que abre uma nova página web e verifica o título da aba (browser.title), o código-fonte 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
19
20
21
22
23
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.Tag
import org.junit.jupiter.api.Test
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver

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

    @SeleniumWebDriver
    lateinit var browser: WebDriver

    @Test
    fun `deve abrir a pagina de origem do clique`() {
        browser.get("http://localhost:8080/click-source")
        assertThat(browser.title).isEqualTo("Click Source Page")
    }
}

Devemos entender que o método get() bloqueia a execução até que a página solicitada seja carregada. Ele também seguirá todos os redirecionamentos emitidos pelo servidor HTTP (como 302 Found) ou pelo próprio documento HTML retornado (meta-redirects imediatos).

Entretanto, existem duas situações importantes nas quais não podemos confiar que o método get() carregou o DOM inteiro antes de retornar:

  1. A página carregada utiliza JavaScript assíncrono que faz alterações no DOM (como buscar dados via AJAX/Fetch após o carregamento inicial). Se esse for o caso, nosso teste deve aguardar até que o DOM tenha sido construído.
  2. A página retornada possui um meta-redirect com temporizador que aguarda alguns segundos antes de redirecionar o navegador para outra página. Se esse for o caso, nosso teste deve aguardar até que esse tempo expire e o redirecionamento ocorra; caso contrário, nosso teste poderá inspecionar a página errada!

A próxima seção desta aula descreve exatamente como podemos escrever testes End-to-End que clicam em elementos e aguardam até que uma condição específica seja verdadeira.


2. Clicando em um Elemento HTML e Aguardando com WebDriverWait

Agora escreveremos um teste End-to-End que demonstra como podemos clicar em um elemento encontrado no documento HTML carregado. Nosso teste clicará em um link presente na página HTML de origem (/click-source), cujo código-fonte é o seguinte:

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
<head>
    <title>Click Source Page</title>
</head>
<body>
    <a id="click-target-link" href="/click-target">Click this link</a>
</body>
</html>

Quando clicamos nesse link #click-target-link, nossa aplicação abre um segundo documento HTML (/click-target), cujo código-fonte é o seguinte:

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
<head>
    <title>Click Target Page</title>
</head>
<body>
    <p>This is the click target page</p>
</body>
</html>

Podemos escrever nosso teste End-to-End seguindo estes quatro passos:

Passo 1: Criar a Classe de Teste e Abrir a Página Inicial no @BeforeEach

Primeiro, criamos uma classe de teste End-to-End chamada ClickTargetLinkTest e garantimos que a primeira página (/click-source) seja carregada antes da execução do nosso método de teste:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.testwithspring.intermediate.web

import com.testwithspring.intermediate.SeleniumTest
import com.testwithspring.intermediate.SeleniumWebDriver
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver

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

    @SeleniumWebDriver
    lateinit var browser: WebDriver

    @BeforeEach
    fun openClickSourcePage() {
        browser.get("http://localhost:8080/click-source")
    }
}

Segundo, escrevemos um método de teste que localiza o elemento de link utilizando seu id ("click-target-link") e clica no link encontrado invocando targetLink.click():

 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 ClickTargetLinkTest {

    @SeleniumWebDriver
    lateinit var browser: WebDriver

    @BeforeEach
    fun openClickSourcePage() {
        browser.get("http://localhost:8080/click-source")
    }

    @Test
    fun `deve abrir a pagina de destino ao clicar no link`() {
        val targetLink = browser.findElement(By.id("click-target-link"))
        targetLink.click()
    }
}

Passo 3: Aguardar o Carregamento da Nova Página com WebDriverWait

Terceiro, como o método click() no Selenium WebDriver não aguarda necessariamente até que a próxima página termine de carregar, precisamos garantir que nosso teste espere pelo tempo que for necessário (até um limite máximo). Podemos fazer isso em dois subpassos:

  1. Criar um novo objeto WebDriverWait, passando a instância do browser e o timeout máximo de espera (por exemplo, Duration.ofSeconds(3) no Selenium 4 / Spring Boot 4, equivalente ao construtor WebDriverWait(browser, 3) do Selenium 3).
  2. Invocar o método until() da classe WebDriverWait. Ao invocarmos esse método, especificamos a condição que deve retornar true para que nosso teste End-to-End pare de esperar. A classe ExpectedConditions fornece métodos fábrica estáticos ideais para a maioria dos casos de uso — utilizaremos ExpectedConditions.urlToBe("http://localhost:8080/click-target") para aguardar até que a URL da página atual seja a URL de destino.

Nota sobre o Playwright: No Playwright, o método page.locator("#click-target-link").click() e as asserções assertThat(page).hasURL(...) já possuem auto-waiting nativo embutido, aplicando exatamente essa mesma lógica de polling sob o capô!

Depois de adicionarmos a espera explícita ao nosso teste, o código-fonte 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
import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.support.ui.ExpectedConditions
import org.openqa.selenium.support.ui.WebDriverWait
import java.time.Duration

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

    @SeleniumWebDriver
    lateinit var browser: WebDriver

    @BeforeEach
    fun openClickSourcePage() {
        browser.get("http://localhost:8080/click-source")
    }

    @Test
    fun `deve abrir a pagina de destino ao clicar no link`() {
        val targetLink = browser.findElement(By.id("click-target-link"))
        targetLink.click()

        val wait = WebDriverWait(browser, Duration.ofSeconds(3))
        wait.until(
            ExpectedConditions.urlToBe("http://localhost:8080/click-target")
        )
    }
}

Passo 4: Verificar o Título da Página Carregada

Quarto, verificamos se a página carregada possui o título esperado ("Click Target Page"). O código final da nossa classe ClickTargetLinkTest 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
31
32
33
34
35
36
37
38
39
40
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
import org.openqa.selenium.support.ui.ExpectedConditions
import org.openqa.selenium.support.ui.WebDriverWait
import java.time.Duration

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

    @SeleniumWebDriver
    lateinit var browser: WebDriver

    @BeforeEach
    fun openClickSourcePage() {
        browser.get("http://localhost:8080/click-source")
    }

    @Test
    fun `deve abrir a pagina de destino ao clicar no link`() {
        val targetLink = browser.findElement(By.id("click-target-link"))
        targetLink.click()

        val wait = WebDriverWait(browser, Duration.ofSeconds(3))
        wait.until(
            ExpectedConditions.urlToBe("http://localhost:8080/click-target")
        )

        assertThat(browser.title).isEqualTo("Click Target Page")
    }
}

Vamos descobrir a seguir como podemos preencher e submeter um formulário HTML completo.


3. Preenchendo e Submetendo um Formulário HTML

Agora escreveremos um teste End-to-End que preenche os campos de um formulário, submete-o e verifica se as informações corretas são exibidas na página de resultado.

O template HTML/JSP que renderiza o formulário submetido (/form) possui a seguinte estrutura:

 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
31
32
<!DOCTYPE html>
<html>
<head>
    <title>Example Form Page</title>
</head>
<body>
    <h1 id="page-header">Example Form Page</h1>
    <form id="form" action="/form" method="POST" role="form">
        <div>
            <label for="message">Message</label>
            <input id="message" name="message" type="text"/>
        </div>
        <div>
            <label for="number">Number</label>
            <select id="number" name="number">
                <option value="1">1</option>
                <option value="2">2</option>
                <option value="3">3</option>
            </select>
        </div>
        <div>
            <label for="checkbox">Checkbox</label>
            <input id="checkbox" name="checkbox" type="checkbox" value="true"/>
        </div>
        <div>
            <label for="radio-button">RadioButton</label>
            <input id="radio-button" name="radioButton" type="radio" value="true"/>
        </div>
        <button type="submit">Submit</button>
    </form>
</body>
</html>

Quando submetemos esse formulário, nossa aplicação renderiza uma página de resultado que exibe os valores preenchidos nos campos do formulário:

 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
<!DOCTYPE html>
<html>
<head>
    <title>Form Result Page</title>
</head>
<body>
    <h1 id="page-header">Form Result Page</h1>
    <div id="form-content">
        <div id="form-message">
            <div id="form-message-label">Message: </div>
            <div id="form-message-value">${form.message}</div>
        </div>
        <div id="form-number">
            <div id="form-number-label">Number: </div>
            <div id="form-number-value">${form.number}</div>
        </div>
        <div id="form-checkbox">
            <div id="form-checkbox-label">Checkbox: </div>
            <div id="form-checkbox-value">${form.checkbox}</div>
        </div>
        <div id="form-radio-button">
            <div id="form-radio-button-label">Radio Button: </div>
            <div id="form-radio-button-value">${form.radioButton}</div>
        </div>
    </div>
</body>
</html>

Podemos escrever nosso teste End-to-End para esse formulário seguindo estes quatro passos:

Passo 1: Criar a Classe de Teste e Abrir a Página do Formulário

Primeiro, criamos a classe de teste SubmitExampleFormTest e garantimos que a página do formulário (http://localhost:8080/form) seja carregada no método @BeforeEach:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.testwithspring.intermediate.web

import com.testwithspring.intermediate.SeleniumTest
import com.testwithspring.intermediate.SeleniumWebDriver
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver

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

    @SeleniumWebDriver
    lateinit var browser: WebDriver

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

Passo 2: Declarar as Constantes com os Valores do Formulário

Segundo, adicionamos duas constantes à nossa classe de teste (MESSAGE e NUMBER), que especificam os valores que serão digitados e selecionados nos campos do formulário:

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

    companion object {
        private const val MESSAGE = "Hello world"
        private const val NUMBER = "2"
    }

    @SeleniumWebDriver
    lateinit var browser: WebDriver

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

Passo 3: Implementar o Método Auxiliar fillAndSubmitForm()

Terceiro, implementamos um método privado chamado fillAndSubmitForm() que preenche todos os tipos de campos do formulário e o submete:

  1. Campo de texto (<input type="text">): Localizamos o elemento #message e invocamos sendKeys(MESSAGE) para digitar o texto.
  2. Menu dropdown (<select>): Envolvemos o WebElement #number em um objeto Select (org.openqa.selenium.support.ui.Select) e invocamos selectByValue(NUMBER).
  3. Checkbox (<input type="checkbox">): Localizamos o elemento #checkbox e invocamos click() para marcá-lo.
  4. Radio Button (<input type="radio">): Localizamos o elemento #radio-button e invocamos click() para selecioná-lo.
  5. Submissão do <form>: Localizamos o elemento #form e invocamos submit().
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    private fun fillAndSubmitForm() {
        val messageInput = browser.findElement(By.id("message"))
        messageInput.sendKeys(MESSAGE)

        val numberSelect = Select(browser.findElement(By.id("number")))
        numberSelect.selectByValue(NUMBER)

        val checkbox = browser.findElement(By.id("checkbox"))
        checkbox.click()

        val radioButton = browser.findElement(By.id("radio-button"))
        radioButton.click()

        browser.findElement(By.id("form")).submit()
    }

Importante: Devemos entender que o método submit() de um formulário tradicional (Server-Side Rendered) bloqueia a execução até que a nova página seja carregada. Isso significa que não precisamos adicionar lógica extra de WebDriverWait após chamar submit() em formulários HTML síncronos!

Passo 4: Escrever o Método de Teste e Verificar a Página de Resultado

Quarto, escrevemos o método de teste que invoca fillAndSubmitForm() e verifica se os quatro valores exibidos na página de resultado correspondem aos dados submetidos. O código completo da classe SubmitExampleFormTest 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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
import org.openqa.selenium.support.ui.Select

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

    companion object {
        private const val MESSAGE = "Hello world"
        private const val NUMBER = "2"
    }

    @SeleniumWebDriver
    lateinit var browser: WebDriver

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

    @Test
    fun `deve renderizar a pagina de resultado do formulario com os valores corretos`() {
        fillAndSubmitForm()

        val message = browser.findElement(By.id("form-message-value"))
        assertThat(message.text).isEqualTo(MESSAGE)

        val number = browser.findElement(By.id("form-number-value"))
        assertThat(number.text).isEqualTo(NUMBER)

        val checkbox = browser.findElement(By.id("form-checkbox-value"))
        assertThat(checkbox.text).isEqualTo("true")

        val radioButton = browser.findElement(By.id("form-radio-button-value"))
        assertThat(radioButton.text).isEqualTo("true")
    }

    private fun fillAndSubmitForm() {
        val messageInput = browser.findElement(By.id("message"))
        messageInput.sendKeys(MESSAGE)

        val numberSelect = Select(browser.findElement(By.id("number")))
        numberSelect.selectByValue(NUMBER)

        val checkbox = browser.findElement(By.id("checkbox"))
        checkbox.click()

        val radioButton = browser.findElement(By.id("radio-button"))
        radioButton.click()

        browser.findElement(By.id("form")).submit()
    }
}

Resumo

Esta aula nos ensinou cinco coisas fundamentais:

  • Podemos abrir uma página web utilizando o método get() da interface WebDriver.
  • Podemos clicar em qualquer elemento encontrado em um documento HTML utilizando o método click() da interface WebElement.
  • Podemos submeter um formulário HTML utilizando o método submit() da interface WebElement.
  • Se clicarmos em um link (click()), precisamos adicionar uma lógica de espera explícita (WebDriverWait + ExpectedConditions) ao nosso teste End-to-End, porque o método click() não aguarda automaticamente o carregamento completo da nova página no Selenium.
  • Se abrirmos uma página web com get() ou submetermos um formulário HTML tradicional com submit(), o WebDriver aguarda até que a nova página seja carregada antes de prosseguir.