Quanto mais perto de conhecer o Selenium WebDriver, mais tenho dúvidas sobre por que essa ou aquela funcionalidade é implementada dessa maneira e não de outra forma. Em seu discurso,
"Problemas no Selenium WebDriver", Alexey Barantsev lança luz sobre as sutilezas da implementação dessa ferramenta de automação e distingue entre "bugs" e "recursos". Você encontrará muitas coisas interessantes no vídeo, mas ainda existem alguns pontos (pelo menos para mim) na sombra.
Neste artigo, quero discutir a ferramenta frequentemente usada para aguardar um evento em uma página, implementada usando a classe
WebDriverWait e seu principal método
Até . Gostaria de saber se o WebDriverWait é necessário, e é possível recusá-lo?
Os pensamentos serão apresentados no contexto do C #, embora eu não ache que a lógica de implementação desta classe seja diferente para outras linguagens de programação.
Ao criar uma instância do WebDriverWait, uma instância do driver é passada para o construtor, que é armazenado no campo de
entrada . O método Até assume um delegado cujo parâmetro de entrada deve ser IWebDriver, cuja entrada é uma instância.
Vamos dar uma olhada no código fonte do método
Até . A espinha dorsal de sua lógica é um ciclo interminável com duas condições para sua saída: o início do evento ou tempo limite desejado. “Brindes” adicionais estão ignorando as exceções predefinidas e retornando o objeto se TROOSULT não for booleano (mais sobre isso posteriormente).
A primeira limitação que vejo é que sempre precisamos exatamente de uma instância do IWebDriver, embora dentro do método Até (para ser mais preciso, como um parâmetro de entrada para a condição) possamos gerenciar completamente o ISearchContext. De fato, na maioria dos casos, esperamos algum elemento ou alteração em sua propriedade e usamos FindElement (s) para procurá-lo.
Atrevo-me a dizer que usar o ISearchContext seria ainda mais lógico, porque o código do cliente (classe) não é apenas um objeto de página que, na busca de filhos, é repelido da raiz da página. Às vezes, essa é uma classe que descreve um elemento composto cuja raiz é outro elemento da página, e não a própria página. Um exemplo disso é
SelectElement , que aceita uma referência ao pai IWebElement no construtor.
Vamos voltar ao problema de inicializar o WebDriverWait. Esta ação requer uma instância do driver. I.e. sempre, de uma maneira ou de outra, precisamos lançar uma instância do IWebDriver de fora do código do cliente, mesmo que seja uma classe de algum elemento composto (um exemplo sobre SelectElement), que já aceita um "pai". Do meu ponto de vista, isso é desnecessário.
Claro, podemos declarar uma classe por analogia
SearchContextWait : DefaultWait<ISearchContext>
Mas não vamos nos apressar. Nós não precisamos dele.
Vamos ver como a instância do driver passada para a condição é usada. Normalmente, é algo parecido com isto:
var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); wait.Until( d => d.FindElements(By.XPath("locator")).Count > 0 );
Surge a pergunta: por que uma versão "local" do driver é necessária se a condição está sempre disponível no código do cliente? Além disso, esta é a mesma instância passada anteriormente pelo construtor. I.e. o código pode ser algo como isto:
var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); wait.Until( d => Driver.FindElements(By.XPath("locator")).Count > 0 );
Até Simon Stewart usa essa abordagem em seu
discurso .

Ele não escreve "d -> d.", Mas escreve "d -> motorista". a instância do driver passada para o método é simplesmente ignorada. Mas é necessário transmiti-lo, pois isso é requerido pela assinatura do método!
Por que passar o driver dentro da condição do método? É possível isolar a pesquisa nesse método, conforme implementado em
ExpectedConditions ? Dê uma olhada na implementação do método
TextToBePresentInElement . Ou
VisibilityOfAllElementsLocatedBy . Ou
TextToBePresentInElementValue . O driver transferido nem é usado neles!
Portanto, o primeiro pensamento é que não precisamos do método Até com o parâmetro delegate que o driver aceita.
Vamos agora descobrir se o método Até precisa de um valor de retorno? Se bool age como TResult, então não, não é necessário. De fato,
em caso de sucesso, você será verdadeiro e,
em caso de falha, receberá uma TimeoutException. Qual é o conteúdo informativo desse comportamento?
Mas e se o objeto for TResult? Suponha a seguinte construção:
var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); wait.IgnoreExceptionTypes(typeof(NoSuchElementException)); var element = wait.Until(d => d.FindElement(By.XPath("locator")));
I.e. não apenas esperamos a aparência do elemento, mas também o usamos (se esperamos), removendo assim uma chamada desnecessária para o DOM. Bom
Vamos dar uma olhada nessas três linhas de código. Dentro da implementação do método Até, ele se resume a um tipo de semelhança (código condicional)
try { FindElement } catch (NoSuchElementException) {}
I.e. uma exceção será lançada toda vez até que o elemento apareça no DOM. Como a geração de exceção é um evento bastante caro, eu preferiria evitá-lo, especialmente em locais onde não é difícil. Podemos reescrever o código da seguinte maneira:
var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); var elements = wait.Until(d => d.FindElements(By.XPath("locator")));
I.e. usamos FindElements, que não lança uma exceção. Aguarde, esse design aguardará a aparência dos elementos? NÃO! Porque, se você olhar para o
código-fonte , um loop infinito será concluído imediatamente, assim que a condição retornar não nula. E FindElements, em caso de falha, retorna uma coleção vazia, mas não nula de forma alguma. I.e. Para uma lista de itens, usar Até não faz sentido.
Ok, a lista é clara. Mas ainda assim, como retornar o elemento encontrado e não lançar uma exceção? O código pode ficar assim:
var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); var element = wait.Until(d => d.FindElements(By.XPath("locator")).FirstOrDefault());
Nesse caso, a cada iteração do loop, não apenas obteremos a lista IWebElement (que pode estar vazia), mas também tentaremos extrair o primeiro elemento dela. Se os elementos ainda não forem exibidos na página, obteremos nulo (valor padrão do objeto) e passaremos para a próxima iteração do loop. Se o elemento for encontrado, sairemos do método e a variável do elemento será inicializada com o valor de retorno.
E, no entanto, o segundo pensamento é que o valor de retorno do método Até não é usado na maioria dos casos.
O valor passado é desnecessário, o valor de retorno não é usado. Qual é a utilidade de Até? Somente no ciclo e na frequência de chamar o método de condição? Essa abordagem já foi implementada em C # no método
SpinWait.SpinUntil . Sua única diferença é que ele não lança uma exceção de tempo limite. Isso pode ser corrigido da seguinte maneira:
public void Wait(Func<bool> condition, TimeSpan timeout) { var waited = SpinWait.SpinUntil(condition, timeout); if (!waited) { throw new TimeoutException(); } }
I.e. essas poucas linhas de código na maioria dos casos substituem a lógica de toda a classe WebDriverWait. Os esforços valem o resultado?
UpdateNos comentários ao artigo, o usuário da
KSA fez uma observação sensata sobre a diferença entre SpinUntil e Até em termos de frequência da condição. Para WebDriverWait, esse
valor é ajustável e o padrão é 500 milissegundos. I.e. o método Até tem um atraso entre as iterações do loop. Enquanto para o SpinUntil, a
lógica é um pouco complicada e, geralmente, a espera não excede 1 milissegundo.
Na prática, isso resulta em uma situação em que, enquanto espera por um elemento que aparece dentro de 2 segundos, o método Unitl executa 4 iterações e o método SpinUntil leva cerca de 200 ou mais.
Vamos descartar o SpinUntil e reescrever o método Wait da seguinte maneira.
public void Wait(Func<bool> condition, TimeSpan timeout, int evaluatedInterval = 500) { Stopwatch sw = Stopwatch.StartNew(); while (sw.Elapsed < timeout) { if (condition()) { return; } Thread.Sleep(evaluatedInterval); } throw new TimeoutException(); }
Adicionamos algumas linhas de código e, ao mesmo tempo, nos aproximamos da lógica do método Até.