Cuanto más conozco Selenium WebDriver, más preguntas tengo sobre por qué esta o aquella funcionalidad se implementa de esta manera y no de otra manera. En su discurso,
"Problemas en Selenium WebDriver", Alexey Barantsev arroja luz sobre las sutilezas de implementar esta herramienta de automatización y distingue entre "errores" y "características". Encontrarás muchas cosas interesantes en el video, pero aún quedan algunos puntos (al menos para mí) en la sombra.
En este artículo quiero analizar la herramienta de uso frecuente para esperar un evento en una página, implementada utilizando la clase
WebDriverWait y su método principal
Hasta . Me pregunto si se necesita WebDriverWait y ¿es posible rechazarlo?
Los pensamientos se presentarán en el contexto de C #, aunque no creo que la lógica de implementación de esta clase sea diferente para otros lenguajes de programación.
Al crear una instancia de WebDriverWait, se pasa una instancia de controlador al constructor, que se almacena en el campo de
entrada de entrada . El método Hasta supone un delegado cuyo parámetro de entrada debe ser IWebDriver, de los cuales la entrada es una instancia.
Veamos el código fuente del método
Hasta . El núcleo de su lógica es un ciclo sin fin con dos condiciones para salir de él: el inicio del evento deseado o el tiempo de espera. Los "extras" adicionales ignoran las excepciones predefinidas y devuelven el objeto si el bes no actúa como TResult (más sobre eso más adelante).
La primera limitación que veo es que siempre necesitamos una instancia de IWebDriver, aunque dentro del método Hasta (para ser precisos, como parámetro de entrada para la condición), podríamos administrar completamente ISearchContext. De hecho, en la mayoría de los casos, esperamos algún elemento o cambio en su propiedad y usamos FindElement (s) para buscarlo.
Me arriesgo a declarar que usar ISearchContext sería aún más lógico, porque el código del cliente (clase) no es solo un objeto de página, que, en la búsqueda de elementos secundarios, se repele desde la raíz de la página. A veces, esta es una clase que describe un elemento compuesto cuya raíz es otro elemento de la página, y no la página en sí. Un ejemplo de esto es
SelectElement , que acepta una referencia al padre IWebElement en el constructor.
Volvamos al problema de inicializar WebDriverWait. Esta acción requiere una instancia del controlador. Es decir siempre, de una forma u otra, necesitamos lanzar una instancia de IWebDriver desde fuera del código del cliente, incluso si se trata de una clase de algún elemento compuesto (un ejemplo sobre SelectElement), que ya acepta un "padre". Desde mi punto de vista, esto es innecesario.
Por supuesto, podemos declarar una clase por analogía.
SearchContextWait : DefaultWait<ISearchContext>
Pero no nos apuremos. No lo necesitamos a él.
Veamos cómo se usa la instancia del controlador pasada a condición. Por lo general, se parece a esto:
var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); wait.Until( d => d.FindElements(By.XPath("locator")).Count > 0 );
Surge la pregunta, ¿por qué es necesaria una versión "local" del controlador si la condición siempre está disponible desde el código del cliente? Además, esta es la misma instancia pasada anteriormente a través del constructor. Es decir el código podría verse así:
var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); wait.Until( d => Driver.FindElements(By.XPath("locator")).Count > 0 );
Incluso Simon Stewart usa este enfoque en su
discurso .

No escribe "d -> d.", Sino que escribe "d -> driver", es decir la instancia del controlador que se pasa al método simplemente se ignora. ¡Pero es necesario transmitirlo, ya que esto es requerido por la firma del método!
¿Por qué pasar el controlador dentro de la condición del método? ¿Es posible aislar la búsqueda dentro de este método, como se implementa en
ExpectedConditions ? Eche un vistazo a la implementación del método
TextToBePresentInElement . O
VisibilityOfAllElementsLocatedBy . O
TextToBePresentInElementValue . ¡El controlador transferido ni siquiera se usa en ellos!
Entonces, el primer pensamiento es que no necesitamos el método Hasta con el parámetro delegado que acepta el controlador.
¿Ahora veamos si el método Hasta necesita un valor de retorno? Si bool actúa como TResult, entonces no, no es necesario. De hecho,
en caso de éxito, se volverá verdadero, y
en caso de falla, obtendrá una TimeoutException. ¿Cuál es el contenido de información de tal comportamiento?
Pero, ¿y si el objeto es TResult? Asuma la siguiente construcción:
var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); wait.IgnoreExceptionTypes(typeof(NoSuchElementException)); var element = wait.Until(d => d.FindElement(By.XPath("locator")));
Es decir no solo esperamos la aparición del elemento, sino que también lo usamos (si hemos esperado), eliminando así una llamada innecesaria al DOM. Bueno
Echemos un vistazo más de cerca a estas tres líneas de código. Dentro de la implementación del método Hasta, se reduce a una especie de similitud (código condicional)
try { FindElement } catch (NoSuchElementException) {}
Es decir se lanzará una excepción cada vez hasta que el elemento aparezca en el DOM. Dado que la generación de excepciones es un evento bastante costoso, preferiría evitarlo, especialmente en lugares donde no es difícil. Podemos reescribir el código de la siguiente manera:
var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); var elements = wait.Until(d => d.FindElements(By.XPath("locator")));
Es decir Usamos FindElements, que no arroja una excepción. Espera, ¿este diseño esperará la aparición de los elementos? NO! Porque, si observa el
código fuente , un ciclo infinito se completa inmediatamente, tan pronto como la condición regrese no nula. Y FindElements en caso de falla devuelve una colección vacía, pero no nula de ninguna manera. Es decir Para obtener una lista de elementos, usar hasta no tiene sentido.
Ok, la lista es clara. Pero aún así, ¿cómo devolver el elemento encontrado y no lanzar una excepción? El código podría verse así:
var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); var element = wait.Until(d => d.FindElements(By.XPath("locator")).FirstOrDefault());
En este caso, en cada iteración del bucle, no solo obtendremos la lista IWebElement (que puede estar vacía), sino que también trataremos de extraer el primer elemento de ella. Si los elementos aún no se muestran en la página, obtendremos un valor nulo (valor predeterminado para el objeto) y pasaremos a la siguiente iteración del bucle. Si se encuentra el elemento, saldremos del método y la variable del elemento se inicializará con el valor de retorno.
Y, sin embargo, el segundo pensamiento es que el valor de retorno del método Hasta no se usa en la mayoría de los casos.
El valor pasado es innecesario, el valor de retorno no se utiliza. ¿Cuál es la utilidad de Hasta? ¿Solo en el ciclo y la frecuencia de llamar al método de condición? Este enfoque ya se ha implementado en C # en el método
SpinWait.SpinUntil . Su única diferencia es que no arroja una excepción de tiempo de espera. Esto se puede solucionar de la siguiente manera:
public void Wait(Func<bool> condition, TimeSpan timeout) { var waited = SpinWait.SpinUntil(condition, timeout); if (!waited) { throw new TimeoutException(); } }
Es decir Estas pocas líneas de código en la mayoría de los casos reemplazan la lógica de toda la clase WebDriverWait. ¿Los esfuerzos valen el resultado?
ActualizaciónEn los comentarios al artículo, el usuario de
KSA hizo una observación sensata sobre la diferencia entre SpinUntil y Hasta en términos de la frecuencia de ejecución de la condición. Para WebDriverWait, este
valor es ajustable y su
valor predeterminado es 500 milisegundos. Es decir El método Hasta tiene un retraso entre las iteraciones del bucle. Mientras que para SpinUntil la
lógica es un poco complicada y, a menudo, la espera no supera 1 milisegundo.
En la práctica, esto da como resultado una situación en la que, mientras espera un elemento que aparece en 2 segundos, el método Unitl realiza 4 iteraciones, y el método SpinUntil toma alrededor de 200 o más.
Descartemos SpinUntil y reescribamos el método Wait de la siguiente manera.
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(); }
Agregamos algunas líneas de código, y al mismo tiempo nos acercamos a la lógica del método Hasta.