Este é o segundo artigo da série sobre Protractor. Se você ainda não leu o primeiro, veja aqui como dar os primeiros passos com Protractor. Hoje vamos conhecer os locators mais utilizados, saber em qual situação utilizar e quais as vantagens de cada um deles, além de entender como o Protractor trabalha com as promisses. Bora lá?
O Protractor exporta a função global element, que é responsável por encontrar um elemento no DOM. Caso você precise manipular múltiplos elementos, pode usar a função element.all, e com essas funções é possível utilizar os métodos das ações como click(), getText() e sendKeys().
O ElementFinder do Protractor executa as ações de forma assíncrona, ou seja, todas elas são enviadas para o browser, que vai gerenciá-las utilizando o Protocolo do WebDriver. O browser, então, executa essas ações como um usuário comum.
Além de todos os locators específicos do WebDriver, nós temos a opção de utilizar locators específicos para aplicações AngularJS. Na minha opinião, essa é uma das principais vantagens de usar Protractor para automatizar nossos testes. Veja abaixo os principais locators do Protractor e quando utilizar cada um deles:
BY.ADDLOCATOR
Acho que esse é um dos mais legais. Com ele nós podemos criar nosso locator customizado. Isso mesmo, podemos escolher como encontrar nossos elementos na tela. Demais, né? E como utilizar?
Primeiro, imagine que no seu html você pode ter um elemento que utiliza data attributes. Por exemplo:
<input type="text" ng-model="PokemonsController.filtro" data-test="filtro">
Repare que temos um atributo data-test=”filtro”. Quando queremos manipular esse elemento via JS, podemos encontrá-lo dessa forma:
document.querySelector('[data-test="filtro"]')
Beleza Cássio, você já falou sobre data-attributes, já falou como encontrar esse elemento usando JS, mas queremos utilizar o Protractor para encontrar esse elemento nos nossos testes.
Pois bem, agora acontece a “mágica”.
Primeiro, temos que ir ao nosso arquivo de configuração do Protractor e adicionar esse trecho de código dentro da nossa função onPrepare:
by.addLocator('dataTest',function(text) { return document.querySelector(`[data-test="${text}"]`) });
Pronto, feito isso você já pode utilizar o seu locator customizado dentro dos seus testes, da seguinte forma:
element(by.dataTest('filtro'));
Legal, né? Isso pode ser uma ótima forma de deixar seus testes mais resilientes contra falhas, pois muitas vezes id’s e name’s acabam sendo alterados e quebram nossos testes. Pode até adotar como sendo um padrão dentro do seu projeto a utilização de data attributes para facilitar a manipulação dos elementos.
BY.BINDING E BY.EXACTBINDING
Para elementos que contenham a diretiva ng-bind (do próprio AngularJS), podemos utilizar os locators by.binding ou by.exactBinding:
<span>{{pokemon.name}}</span> <span ng-bind="pokemon.name"></span>
E para encontrar elementos com essa diretiva, basta fazer da seguinte forma:
element(by.binding('pokemon.name'));
Ao utilizar o locator by.binding, podemos passar como parâmetro apenas uma parte do valor da diretiva, como:
element(by.binding('name'));
Já para o by.exactBinding, temos que passar exatamente o valor que está declarado na diretiva.
BY.MODEL
Encontra um elemento no DOM que tenha a diretiva ng-model (do próprio AngularJS):
<input type="text" ng-model="pokemon.name">
Encontramos esse elemento da seguinte forma no teste com Protractor:
element(by.model('pokemon.name'))
BY.BUTTONTEXT E BY.PARTIALBUTTONTEXT
Ambos encontram um elemento no DOM baseado pelo texto do botão:
<button>Carregar mais Posts</button>
Para localizar esse elemento, usamos:
element(by.buttonText('Carregar mais Posts'));
Ou, por apenas uma parte do texto do botão, utilizamos o by.partialButtonText:
element(by.partialButtonText('Carregar'));
BY.REPEATER E BY.EXACTREPEATER
Encontra elemento com a diretiva do AngularJS ng-repeat:
<tr ng-repeat="item in PokemonsController.pokemons|filter: PokemonsController.filtro"> <td> <a href="#!/details/{{item.id}}"> <strong> #{{item.id}} </strong> </a> </td> <td> <a href="#!/details/{{item.id}}"> <img ng-src="{{item.image}}" width="60px" alt=""> </a> </td> <td> <a href="#!/details/{{item.id}}"> <span ng-bind="item.name"></span> </a> </td> <td> <a href="#!/details/{{item.id}}"> <span class="label bg-{{item.type1}}" ng-bind="item.type1"></span> <span class="label bg-{{item.type2}}" ng-bind="item.type2"></span> </a> </td> </tr>
Esse locator é muito útil e ajuda muito quando precisamos lidar com uma lista de elementos. Com ele podemos extrair várias informações úteis, como:
Quantidade de registros listados
const resultados = element.all(by.repeater('item in PokemonsController.pokemons')); resultados.count();
Retornar um dado específico da listagem:
element(by.repeater('item in PokemonsController.pokemons').row(0).column('item.name'));
Retornar todos os valores de uma determinada coluna
element.all(by.repeater('item in PokemonsController.pokemons').column('item.name'));
Para usar o by.exactRepeater, como o próprio nome sugere, devemos passar por parâmetro o valor exatamente igual ao que está no valor da diretiva. Nesse caso, fica assim:
element.all(by.repeater('item in PokemonsController.pokemons|filter: PokemonsController.filtro'));
Observação: Podemos continuar extraindo as informações da mesma forma que o by.repeater.
Em breve teremos um artigo com algumas dicas de como trabalhar listas usando Protractor e alguns métodos auxiliares.
BY.CSSCONTAININGTEXT
Esse locator encontra elemento por CSS que contenha um determinado texto. Recebe dois parâmetros, o primeiro é o seletor css e o segundo é o texto que vai filtrar. Veja abaixo um exemplo:
Dado que tenha mais de um elemento html com a mesma class:
<ul> <li class="pokemon">Bulbasaur</li> <li class="pokemon">Charizard</li> </ul>
Para filtrar um específico, usamos da seguinte maneira:
var bulbasaur = element(by.cssContainingText('.pokemon', 'Bulbasaur'));
BY.OPTIONS
Encontra um elemento com a diretiva do AngularJS ng-options
<select ng-options="pokemon.type for type in types"></select>
Para encontrar esse elemento utilizando Protractor:
element(by.options('pokemon.type for type in types'));
Como o Protractor trabalha com as Promisses?
Como já falei anteriormente, todos as ações do Protractor são assíncronas, isso significa que todos os métodos retornam uma promisse. Então, se quisermos saber o valor de um determinado elemento, precisamos fazer assim:
var pokemonName = element(by.model('pokemon.name')); pokemonName.getText().then(function(text) { console.log(text); });
Se quisermos saber se um elemento está habilitado:
var saveButton = element(by.buttonText('Salvar')); saveButton.isEnabled().then(function(enable) { console.log(enable); });
Se quisermos saber se um elemento está visível:
var saveButton = element(by.buttonText('Salvar')); saveButton.isDisplayed().then(function(visible) { console.log(visible); });
Observação: Alguns locators do Protractor ainda não estão funcionando adequadamente com as novas versões do AngularJS. Tem uma issue aberta no Github do Protractor sobre esse assunto, e se você quiser acompanhar é só clicar aqui.
O Protractor possui uma grande variedade de locators e saber qual podemos utilizar em cada situação é uma grande vantagem para melhorar nossos testes. Então, antes de sair utilizando XPATH e CSS Selector (só usem em último caso e ainda sim, saibam como utilizar com inteligência), tentem utilizar ao máximo os locators do Protractor. Fechado? Se tiver alguma dúvida ou tem algo a acrescentar, é só usar os campos abaixo. Até a próxima!
Quer saber mais? Acesse:
- Protractor API Docs
- Making your UI tests resilient to change (Kent Dodds)
- Promise Manager (Protractor – WebDriverJs)
***
Este artigo foi publicado originalmente em: https://www.concrete.com.br/2017/11/20/primeiros-passos-com-o-protractor-parte-2/