Testes E2E para SPAs JavaScript: Diálogos Modais e Confirmações

Depois de concluirmos esta aula, nós:

  • Entenderemos como selecionar a condição de espera correta (wait condition) quando estamos escrevendo testes End-to-End para funcionalidades que exibem diálogos modais (como modais de confirmação de exclusão).
  • Saberemos como criar um Page Object dedicado para representar o diálogo modal (DeleteTaskConfirmationDialog) e integrá-lo à classe TaskActions.

1. Escrevendo Testes End-to-End para Funcionalidades que Utilizam Diálogos Modais

A página de visualização de tarefa (view task page) contém um link que exclui uma tarefa existente do banco de dados. A parte relevante do template HTML que renderiza a página de visualização da tarefa na nossa SPA é a 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
<h1 id="task-title">{{task.title}}</h1>
<div class="panel panel-default">
    <div class="panel-body">
        <div id="task-description"
             ng-if="!task.description"
             translate="page.view.task.no.description.message"></div>
        <div id="task-description"
             ng-if="task.description">{{task.description}}</div>
    </div>
</div>
<div id="lifecycle-fields" class="container-fluid">
    <!-- Omitido por brevidade -->
</div>
<div id="task-action-links" class="row">
    <div class="col-md-1">
        <span delete-task-link task="task"></span>
    </div>
    <div class="col-md-1">
        <a id="update-task-link"
           ui-sref="task.update({id: task.id})"
           translate="page.view.task.update.task.link.label">
        </a>
    </div>
</div>

Na versão tradicional (Server-Side Rendered) da aplicação, clicar em #delete-task-link disparava imediatamente a exclusão. Já na nossa aplicação JavaScript (SPA), quando um usuário clica no link de exclusão (#delete-task-link), a aplicação abre um diálogo modal de confirmação (#delete-task-dialog) que possui dois botões:

  1. O botão Cancelar fecha o diálogo modal de confirmação sem excluir a tarefa.
  2. O botão Excluir (#delete-task-button) fecha o diálogo modal de confirmação, envia a requisição DELETE /api/task/{id} para o backend e redireciona o usuário para a página de listagem de tarefas.

O template HTML que contém a marcação do diálogo modal é o seguinte:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="delete-task-dialog">
    <div class="modal-header">
        <h4 class="modal-title" translate="dialogs.delete.task.dialog.title">
        </h4>
    </div>
    <div class="modal-body">
        <p translate="dialogs.delete.task.dialog.text"
           translate-values="{title: task.title}"></p>
    </div>
    <div class="modal-footer">
        <button type="button"
                class="btn btn-default"
                data-dismiss="modal"
                translate="dialogs.delete.task.dialog.cancel.button.label"
                ng-click="cancel()"></button>
        <button id="delete-task-button"
                type="button"
                class="btn btn-danger"
                translate="dialogs.delete.task.dialog.delete.button.label"
                ng-click="delete()"></button>
    </div>
</div>

Antes de escrevermos testes End-to-End para a funcionalidade de exclusão de tarefa com modal, precisamos selecionar a condição de espera que deve ser verdadeira após clicarmos no link que abre o modal. Quando escrevemos testes End-to-End para funcionalidades que exibem diálogos modais, escolhemos a condição de espera seguindo estas duas regras simples:

  1. Se quisermos escrever asserções para o texto/conteúdo do diálogo modal, devemos aguardar até que o diálogo modal esteja visível (waitUntilElementIsVisible).
  2. Se quisermos clicar nos botões do diálogo modal, devemos aguardar até que esses botões estejam clicáveis (waitUntilElementIsClickable) — o que é especialmente importante porque frameworks de UI (como Bootstrap, Material UI ou Radix) costumam aplicar animações de fade-in ao abrir um modal!

Vamos ver agora como modelar esse diálogo modal nos nossos Page Objects.


2. Criando um Novo Page Object para o Diálogo Modal (DeleteTaskConfirmationDialog)

Como vimos anteriormente, a classe TaskActions é o Page Object que permite aos nossos testes clicar nos links de excluir e atualizar a tarefa. No entanto, não devemos colocar o código que manipula o diálogo modal de confirmação diretamente dentro da classe TaskActions, pois isso daria responsabilidades demais a uma única classe.

Em vez disso, aplicando a 1ª Boa Prática dos Page Objects, criaremos um novo Page Object chamado DeleteTaskConfirmationDialog que representa exclusivamente o diálogo de confirmação! Podemos criá-lo seguindo estes quatro passos:

Passo 1: Criar a Classe DeleteTaskConfirmationDialog e seus Campos

Primeiro, criamos a classe DeleteTaskConfirmationDialog com duas propriedades privadas:

  • browser: Contém a referência para o objeto WebDriver utilizado.
  • taskActions: Contém a referência para o WebElement que representa o container #task-action-links (onde está o link #delete-task-link).

Passo 2: Implementar o Construtor

Segundo, implementamos o construtor que recebe e armazena browser e taskActions:

1
2
3
4
5
6
7
8
9
package com.testwithspring.intermediate.web.task

import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement

internal class DeleteTaskConfirmationDialog(
    private val browser: WebDriver,
    private val taskActions: WebElement
)

Passo 3: Implementar o Método open()

Terceiro, adicionamos o método open() à classe DeleteTaskConfirmationDialog e o implementamos em três etapas:

  1. Localizar e clicar no link #delete-task-link encontrado dentro de taskActions.
  2. Aguardar até que o nosso teste possa clicar no botão de confirmação #delete-task-button dentro do modal (SeleniumWait.waitUntilElementIsClickable(browser, By.id("delete-task-button"))).
  3. Retornar a referência para este próprio objeto DeleteTaskConfirmationDialog (this).
 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.task

import com.testwithspring.intermediate.SeleniumWait
import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement

internal class DeleteTaskConfirmationDialog(
    private val browser: WebDriver,
    private val taskActions: WebElement
) {
    fun open(): DeleteTaskConfirmationDialog {
        val deleteLink = taskActions.findElement(By.id("delete-task-link"))
        deleteLink.click()

        SeleniumWait.waitUntilElementIsClickable(
            browser,
            By.id("delete-task-button")
        )

        return this
    }
}

Passo 4: Implementar o Método deleteTask()

Quarto, adicionamos o método deleteTask() à classe DeleteTaskConfirmationDialog e o implementamos em duas etapas:

  1. Localizar e clicar no botão de confirmação #delete-task-button dentro do modal.
  2. Aguardar até que a página de listagem de tarefas (TaskListPage) esteja aberta (targetPage.waitUntilPageIsOpen()) e retornar o novo objeto TaskListPage.

Depois de implementarmos esse método, o código completo da classe DeleteTaskConfirmationDialog 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
package com.testwithspring.intermediate.web.task

import com.testwithspring.intermediate.SeleniumWait
import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement

internal class DeleteTaskConfirmationDialog(
    private val browser: WebDriver,
    private val taskActions: WebElement
) {
    fun open(): DeleteTaskConfirmationDialog {
        val deleteLink = taskActions.findElement(By.id("delete-task-link"))
        deleteLink.click()

        SeleniumWait.waitUntilElementIsClickable(
            browser,
            By.id("delete-task-button")
        )

        return this
    }

    fun deleteTask(): TaskListPage {
        val deleteButton = browser.findElement(By.id("delete-task-button"))
        deleteButton.click()

        val targetPage = TaskListPage(browser)
        targetPage.waitUntilPageIsOpen()
        return targetPage
    }
}

Agora que criamos o Page Object que representa o diálogo de confirmação, nosso último passo é fazer com que a classe TaskActions o utilize!


3. Modificando a Classe TaskActions

Podemos atualizar o método deleteTask() da classe TaskActions em apenas duas linhas:

  1. Instanciar DeleteTaskConfirmationDialog(browser, rootElement) e abrir o modal de confirmação chamando .open().
  2. Confirmar a exclusão da tarefa no banco de dados chamando dialog.deleteTask() e retornar a TaskListPage resultante!

Depois de fazermos essa alteração, o código-fonte completo da classe TaskActions 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.task

import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement

internal class TaskActions(
    private val browser: WebDriver,
    private val rootElement: WebElement,
    private val taskId: Long?
) {
    fun deleteTask(): TaskListPage {
        val dialog = DeleteTaskConfirmationDialog(
            browser = browser,
            taskActions = rootElement
        ).open()

        return dialog.deleteTask()
    }

    fun updateTask(): UpdateTaskPage {
        val updateTaskLink = rootElement.findElement(By.id("update-task-link"))
        updateTaskLink.click()

        val targetPage = UpdateTaskPage(browser, taskId)
        targetPage.waitUntilPageIsOpen()

        return targetPage
    }
}

Novamente, repare que não precisamos fazer nenhuma alteração nas nossas classes de teste End-to-End que testam a exclusão de tarefas! Como a assinatura pública de TaskActions.deleteTask(): TaskListPage permaneceu exatamente a mesma, os testes continuam limpos e desacoplados do fato de a exclusão agora passar por um diálogo modal de confirmação.


Resumo

Esta aula nos ensinou três lições essenciais:

  • Devemos colocar o código que manipula um diálogo modal em um novo Page Object dedicado (DeleteTaskConfirmationDialog), evitando sobrecarregar os Page Objects existentes com responsabilidades demais.
  • Se quisermos escrever asserções para o conteúdo textual de um diálogo modal, devemos aguardar até que o diálogo esteja visível (waitUntilElementIsVisible).
  • Se quisermos clicar nos botões de ação de um diálogo modal, devemos aguardar até que esses botões estejam clicáveis (waitUntilElementIsClickable).
  • No próximo e último tópico do Capítulo 02, mergulharemos nas Aulas Bônus: simulação de APIs REST externas com WireMock (incluindo respostas JSON, falhas e timeouts) e recursos avançados do JUnit 6 (Jupiter) (@Nested, @ParameterizedTest, extensões customizadas e integração com Spring)!