Neste artigo mostrarei, de maneira resumida, como automatizar a navegação em uma página web para fins diversos.

Mais especificamente: como eu fiz para conseguir um horário melhor num sistema de agendamento online através de um pequeno código que verifica novos horários vagos automaticamente. 😀

Contexto

Recentemente eu escrevi sobre como a tecnologia deveria tornar a nossa vida melhor. Posso dizer que consegui um pequeno progresso nesta área.

Basicamente eu havia feito um agendamento num sistema online qualquer, tendo selecionado o primeiro horário disponível. Porém, a data estava longe e eu gostaria de adiantar esse agendamento caso alguém desistisse.

Qual seria a solução convencional? Entrar no sistema repetidamente, sempre que eu lembrar, e ver se aparece algum slot disponível.

Certo, isto funcionaria. Mas quais as changes de eu esquecer e não acessar a página em intervalos regulares? E se alguém pegar o lugar antes de mim? Quanto tempo iria gastar acessando o sistema várias vezes por dia, durante vários dias?

Solução

Fiz um desafio pessoal. Tendo já trabalhado bastante com testes automatizados usando WebDriver, será que conseguiria montar um “script” para automatizar a tarefa de verificar um novo horário disponível no sistema que me interessasse e então me notificar? Tudo isso em até uma hora (lembre-se a tecnologia tem que ajudar você a ser mais eficiente)?

Felizmente, deu certo e agora vou explicar como.

Antes de mais nada, é bom mencionar que o WebDriver é o padrão de facto para automação de testes automatizados em sistemas web. Ele permite controlar o navegador e simular o uso real de um usuário navegando numa página ou sistema web.

Estrutura do projeto

O primeiro passo foi criar um projeto na minha IDE. Qualquer uma serve, mas atualmente uso o IntelliJ.

Para facilitar, criei um projeto Maven. Assim não tenho que caçar jars para lá e para cá na Internet.

Pesquisando por webdriver maven no Google, encontrei rapidamente a documentação que explica as dependências necessárias. Mostrarei o pom.xml completo mais abaixo.

Em seguida, utilizei o primeiro exemplo da documentação como base para meu código.

Talvez aqui você esteja se perguntando se copiar e colar código assim é uma boa prática. Há duas respostas para isso:

  1. Sim, para prototipação rápida como é o caso.
  2. Não, para código em produção, pois além de introduzir comandos que talvez você não tenha certeza do que faz, pode incorrer em problemas de direitos autorais.

Enfim, colei o código de exemplo dentro do método main de uma nova classe e aí começou a “ação” de verdade.

Automatizando a navegação

Automatizar a navegação com o WebDriver é bem simples. Basta ter conhecimentos intermediários sobre navegação numa página, além de conhecimentos básicos sobre HTML e CSS e como identificar elementos numa página.

Antes de mais nada, precisamos do "algoritmo", isto é, o procedimento de verificação de um horário vago.

Nunca comece a implementação sem saber o que precisa ser feito!

São os passos:

  1. Abrir a página no navegador.
  2. Preencher dois campos texto (não eram usuário e senha, caso esteja curioso).
  3. Submeter os dados clicando num botão.
  4. Na próxima página, selecionar a opção de mudar a data do agendamento, que consiste em clicar num botão específico.
  5. Na próxima página, verificar numa tabela quais os horários estão vagos (esta é a parte mais complicada para automatizar).

Além disso, havia dois requisitos importantes:

  1. Repetir o procedimento continuamente, por exemplo a cada 5 minutos.
  2. Notificar quando horários de interesse forem encontrados.

Quanto ao processo para efetivar o reagendamento, como iria fazer apenas uma vez, simplesmente decidi não implementar.

Abrir uma página

O método driver.get(url) abre uma página qualquer. Basta passar a URL como parâmetro. Fácil!

Preenchendo os campos

Antes de preencher algo, é preciso identificar qual campo você quer. É simples.

Primeiro, use a ferramenta do seu navegador para inspecionar o elemento:

Screen Shot 2016-07-25 at 7.13.21 PM

Clique com o botão direito no campo, botão ou elemento de interesse e selecione a opção Inspecionar ou Inspect.

Você vai ser levado para a janela do desenvolvedor com os elementos e vai ver algo assim:

<input type="submit" name="nomeBotao" id="idBotao" class="button button-primary button-large" value="Label do Botao">

Nesse exemplo, você pode identificar o elementos deste botão de forma única através do seu atributo id. Campos de formulário, quando não possuem id, devem possuir um atributo name que provavelmente é único.

No meu caso, eu queria encontrar dois campos e um botão, na primeira tela. O WebDriver torna isto fácil. Usei driver.findElement(By.name("nomeDoCampo")) para localizar os campos de texto pelo name e driver.findElement(By.id("idBotao")) para localizar o botão pelo id.

O método findElement retorna um objeto do tipo WebElement, que é uma espécie de referência para o elemento no DOM do navegador.

Para “digitar” nos campos, existe o método element.sendKeys("texto").

Como o sistema tinha lógica para validação dos campos quando eram preenchidos, forcei uma tabulação no final para simular um usuário navegando pelo sistema: element.sendKeys(Keys.TAB).

Submetendo o formulário

Para enviar o formulário, o método element.submit() deu conta do recado, sendo o elemento aqui o botão capturado anteriormente.

Selecionando a opção desejada na próxima tela

Algo importante em algumas páginas é aguardar para que os scripts tenham sido carregados e executados, caso contrário o WebDriver pode clicar muito rapidamente em algum elemento que depende de javaScript para funcionar, antes que o manipulador de eventos seja vinculado ao elemento, então nenhuma ação será executada.

Uma tática comum para evitar esse tipo de “condição de corrida” é verificar estado do documento HTML através do atributo document.readyState. Fazendo uma pesquisa rápida e adaptando um código do Stack Overflow, cheguei ao seguinte método:

public static void waitDocumentReady(WebDriver driver) {
  new WebDriverWait(driver, 30).until((ExpectedCondition<Boolean>) wd ->
        ((JavascriptExecutor) wd).executeScript("return document.readyState").equals("complete"));
}

A classe WebDriverWait define no WebDriver uma condição de espera que vai ser verificada repetidamente até que tenha sido satisfeita. Somente depois a interação com a página continua.

Neste caso, ela executa um JavaScript na página que testa o valor de document.readyState e só retorna quando este for igual a complete. Esta verificação garante que a página está completamente carregada antes de interagirmos com ela.

Depois da página carregar, eu quero precisava clicar num outro botão para que o sistema abrisse a tabela de agendamento. Entretanto, o submit() não funcionou aqui porque o botão não estava num formulário. Mas isso também é fácil, basta usar o método click() e tudo se resolveu.

Várias formas de aguardar por elementos

O estado complete é o último estágio do carregamento da página. Na maioria das vezes é desnecessário esperar tanto, quando você pode fazer sua automação muito mais rápida.

Por outro lado, 100% dos sistemas que eu conheço que possuem testes implementados com WebDriver têm problemas com falhas aleatórias, sendo a principal causa algum elemento demorar um pouco mais para estar presente na tela ou pronto para ser clicado.

Portanto, num sistema real, você sempre deve ter alguma condição atendida antes de executar o próximo passo do seu teste ou do seu script.

Para elementos HTML convencionais, a forma mais eficiente é criar uma condição que espere pela presença do elemento na tela.

Outro caso comum é um elemento que depende de algum script ou algum evento antes de estar pronto. Pense numa popup que fica oculta na página, estando presente no HTML, mas você não quer interagir com ela até que ela esteja visível. Então você pode verificar a visibilidade do elemento principal, usar um atributo no HTML que muda de acordo com a visibilidade ou ainda uma classe CSS para demarcar quando o tal elemento está pronto para interagir.

O terceiro caso mais comum é o uso de AJAX, isto é, JavaScript que carrega dados assincronamente. Se o AJAX serve para adicionar elementos na tela, basta aguardar pela presença dos mesmos. Se, no entanto, o resultado do AJAX não é fácil de determinar, talvez você tenha que alterar o código do sistema e adicionar manualmente, por exemplo, uma variável JavaScript que indique quando a requisição assíncrona está em andamento e quando ela terminou de executar.

Verificando horários vagos

Analisar uma tabela HTML programaticamente nem sempre é uma tarefa agradável. Mas, para minha sorte, ao analisar o código HTML escobri que o desenvolvedor deste sistema utilizou-se de um mínimo de boas práticas e definiu classes CSS e ids diferentes para as células disponíveis. \o/

Bastou então listar as células dentro da tabela com as características que eu queria, no caso, com a classe available:

List<WebElement> availableSpots = 
    driver.findElements(By.cssSelector("#idTabela .available"));

Note que o seletor #idTabela .available é o mesmo que você usaria em uma folha de estilo ou JavaScript. Então foi até possível testar o seletor usando o console do navegador:

document.querySelectorAll("#idTabela .available");

Colocando esse código no console da ferramenta do desenvolvedor com a página alvo aberta, o navegador retorna a lista de elementos selecionados.

Depois disso, faltava filtrar as datas de interesse. Há dezenas de maneiras diferentes para fazer isto, mas como apenas me interessavam alguns dias específicos e os ids tinham o formato <dia>_<hora>, apenas recuperei o id como uma String e fiz uma comparação simples com o método startsWith() para filtrar os dias desejados, independente do horário.

Repetindo a verificação em intervalores regulares

Há várias formas de se fazer isto. Fiz a que achei mais simples no momento: um shell script implementando um laço infinito e uma pausa de alguns segundos entre cada iteração.

Para executar o projeto Java, usei o exec-maven-plugin, pois digitando no terminal mvn exec:java o Maven se encarrega de executar o projeto com o classpath e a classe principal corretos.

Notificando via e-mail

Copiei o primeiro exemplo que apareceu na busca. Utilizei a versão com SSL por ser mais segura, alterando apenas minhas credenciais do gmail.

Com isso, tinha em mãos um método simples para enviar um e-mail ainda mais simples e me notificar quando horários disponíveis fossem encontrados.

Detalhes adicionais

Abaixo, alguns detalhes não tão importantes para este protótipo, mas que cedo ou tarde podem ser úteis para você.

Browser

O WebDriver suporta diversos navegadores, mas o Firefox é de longe o que tenho visto mais vezes sendo usado na automação de testes. Os principais motivos são:

  • É fácil manter uma instalação standalone, que não precisa estar realmente instalada como IE ou Chrome. Basta copiar o executável e referenciar o local.
  • Ele tem se mostrado o mais consistente, com menos bugs (no que se refere ao WebDriver) e integrado com os testes automatizados. A fundação Mozilla tem investido bastante em automação de testes.

É recomendável você usar um executável do Firefox que não seja o mesmo que está instalado em seu computador e que não seja automaticamente atualizado. Atualizações constantes dos navegadores tendem a quebrar os testes de forma inesperado, na grande maioria das vezes sem relevância, pois não é o sistema que deixa de funcionar e sim alguma integração com o WebDriver, por exemplo.

Eu usei uma cópia do Firefox que já tinha salva em meu disco e apenas referenciei o diretório ao instanciar o WebDriver.

Evitar o Adobe Flash

O Flash estava deixando o teste mais lento, pois havia uma animação pesada na página.

Para resolver, bastou definir uma configuração no perfil usado pelo WebDriver para abrir o Firefox:

FirefoxProfile profile = new FirefoxProfile();
profile.setPreference("plugin.state.flash", 0);

Resultados

Estrutura final do projeto

Screen Shot 2016-07-27 at 7.18.45 PM

Código do teste (Java)

import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxBinary;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxProfile;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.File;
import java.util.List;
import java.util.Properties;

public class CheckSpot {
    
    public static void main(String[] args) throws InterruptedException {
        //iniciar firefox
        FirefoxProfile profile = new FirefoxProfile();
        profile.setPreference("plugin.state.flash", 0);
        WebDriver driver = new FirefoxDriver(
                new FirefoxBinary(new File("firefox/firefox-bin")),
                profile);
        driver.manage().window().maximize();

        try {
            //abrir pagina
            driver.get("https://sistema-de-agendamento.com.br/");
            waitDocumentReady(driver);

            //preencher primeiro campo
            WebElement firstField = driver.findElement(By.name("campo1"));
            firstField.sendKeys("dados");
            firstField.sendKeys(Keys.TAB);

            //preencher segundo campo
            WebElement secondField = driver.findElement(By.name("campo2"));
            secondField.sendKeys("mais dados importantes");
            secondField.sendKeys(Keys.TAB);

            //submeter formulario
            WebElement loginButton = driver.findElement(By.id("idDoBotao"));
            loginButton.submit();
            waitDocumentReady(driver);

            //selecionar menu de reagendamento
            WebElement changeDateTimeButton = driver.findElement(By.id("idDaOpcaoDoMenuDeAgendamento"));
            changeDateTimeButton.click();
            waitDocumentReady(driver);

            //listar horarios disponiveis
            List<WebElement> availableSpots = driver.findElements(By.cssSelector("#idTabela .available"));
            for (WebElement spot : availableSpots) {

                //seleciona elemento "pai" para pegar o ID
                WebElement parent = spot.findElement(By.xpath(".."));
                String id = parent.getAttribute("id");
          
                //verifica se o ID tem a data que eu quero
                if (id != null && id.startsWith("terca_")) {
                    mail("terca", spot.getText());
                }
            }

            //Espera dois segundos pra eu poder espiar os resultados de vez em quando :P
            Thread.sleep(2000);

        } finally {
            //fecha o navegador (isto é importante!)
            driver.close();
        }
    }

    public static void waitDocumentReady(WebDriver driver) {
        //espera a página carregar por completo
        new WebDriverWait(driver, 30).until((ExpectedCondition<Boolean>) wd ->
                ((JavascriptExecutor) wd).executeScript("return document.readyState").equals("complete"));
    }

    public static void mail(String dia, String hora) {
        Properties props = new Properties();
        props.put("mail.smtp.host", "smtp.gmail.com");
        props.put("mail.smtp.socketFactory.port", "465");
        props.put("mail.smtp.socketFactory.class",
                "javax.net.ssl.SSLSocketFactory");
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.port", "465");

        Session session = Session.getDefaultInstance(props,
                new javax.mail.Authenticator() {
                    protected PasswordAuthentication getPasswordAuthentication() {
                        return new PasswordAuthentication("usuario","senha");
                    }
                });

        try {
            Message message = new MimeMessage(session);
            message.setFrom(new InternetAddress("conta@gmail.com"));
            message.setRecipients(Message.RecipientType.TO,
                    InternetAddress.parse("destinatario@gmail.com"));
            message.setSubject("Vaga disponivel");
            message.setText("Dia: " + dia + "." +
                    "\n\nHora:" + hora + ".");
            Transport.send(message);
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }
    }

}

Configuração do Maven

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.webdriver</groupId>
    <artifactId>automate</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <encoding>UTF-8</encoding>
    </properties>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.4.0</version>
                <configuration>
                    <mainClass>CheckSpot</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>2.53.1</version>
        </dependency>
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>htmlunit-driver</artifactId>
            <version>2.21</version>
        </dependency>
        <dependency>
            <groupId>javax.mail</groupId>
            <artifactId>mail</artifactId>
            <version>1.4.7</version>
        </dependency>
    </dependencies>
</project>

Script de repetição:

#!/usr/bin/env bash

while true; do
    echo "Trying again... because practice makes purrfect!"
    mvn exec:java
    sleep 360;
done

Considerações

O site em que apliquei esta técnica não vem ao caso, mas é impossível não imaginar as diversas aplicações onde a automação pode beneficiar a nossa vida, seja para testes automatizados, para uma simples brincadeira (como foi descrito neste post) ou ainda para muitas outras aplicações onde você apenas precisa usar a sua…

imaginacao

Por outro lado, já pensou que um sistema de agendamento online mais eficiente seria muito mais desejável? Imagine um sistema genérico de agendamentos onde você pode se cadastrar para receber notificações automaticamente caso novos horários sejam liberados! Alguém se habilita!?

Se você não conhece muito sobre WebDriver e gostou do que viu até aqui, há muito material para você aprender por aí. Encerro o artigo com um vídeo que vai lhe dar um gostinho visual do que você acabou de ler em forma de texto:


Já ia me esquecendo: eu consegui um horário melhor!

uhu