[Selenium] Utiliser les Waits pour interagir avec les éléments de la page.

Selenium est une suite d’outils permettant de concevoir et d’exécuter des tests automatisés au travers d’un navigateur Web. Une version majeure est sortie il y a quelques mois et la version actuelle est la 4.5.* . Ces tests automatisés correspondent à ce que l’utilisateur d’une application Web pourrait faire par l’intermédiaire de l’interface pour accomplir une tâche. On parle alors de tests de bout en bout ou de end-to-end testing. Cet article présente deux notions essentielles, à savoir, Implicit Wait et Explicit Wait.

Courte bande dessinée montrant un robot qui ne parvient pas à trouver un bouton sur une page Web.
Crédit : @ismonkeyuser

Lorsque l’on écrit des tests automatisés l’une des principales difficultés est de savoir quand un élément de la page Web est prêt à être utilisé. S’il n’est pas encore apparu sur la page ou s’il a changé ou disparu le script retournera une erreur. Regardons comment Selenium nous permet d’interagir de manière adéquate avec un élément.

Le comportement par défaut

Lorsque l’on accède à une page Web, le comportement par défaut de Selenium est d’attendre que le chargement de la page soit terminé avant de poursuivre l’exécution du script. C’est la pageLoadStrategy appliquée par défaut. Concrètement Selenium vérifie que l’état du document, document.readyState, est à completed. Le fait que le document soit complètement chargé ne signifie pas que l’on peut interagir dés maintenant avec des éléments qui seront chargés suite à l’exécution d’un code Javascript par exemple. Nous verrons dans la suite de l’article comment faire pour dialoguer avec ces éléments.

Prenons l’exemple du formulaire ci-dessous. Il dispose d’un champ pour indiquer une valeur en seconde et d’un bouton. Un clic sur le bouton déclenche un traitement en Javascript qui affichera un texte une fois le délai écoulé.

Ci-dessous le HTML du formulaire :

<div class="container">

   <div class="row mt-4 justify-content-center">

      <div class="col-4">
         <label for="secondes" class="form-label">Délai en secondes avant apparition du texte : </label>
         <input type="number" class="form-range" name="secondes" id="secondes" value="1" min="0" step="1">
      </div>

      <div class="col-2 mt-3">
         <button type="button" class="btn btn-primary" id="btAfficherLeTexte">
            Afficher le texte
         </button>
      </div>

   </div>

   <div class="row justify-content-center mt-5" id="ligneAlerteTexte">
   </div>

</div>

Dans le code Java suivant on utilise l’API de Selenium pour aller sur la page puis pour indiquer une valeur en seconde puis pour cliquer sur le bouton. La commande clear est utilisée pour supprimer le contenu du champ. On utilise les id des éléments pour interagir avec eux. Il n’y a pas de difficulté lié à l’exécution de ce script puisque les 2 éléments sont présents dès le chargement de la page.

driver.get("https://justepourtester.net/selenium/selenium_wait.html");
driver.findElement(By.id("secondes")).clear();
driver.findElement(By.id("secondes")).sendKeys("3");
driver.findElement(By.id("btAfficherLeTexte")).click();

Maintenant que le script nous permet d’indiquer une durée et de cliquer sur le bouton nous allons vérifier que le texte est bien affiché.

driver.get("https://justepourtester.net/selenium/selenium_wait.html");
driver.findElement(By.id("secondes")).clear();
driver.findElement(By.id("secondes")).sendKeys("3");
driver.findElement(By.id("btAfficherLeTexte")).click();
String textLu = driver.findElement(By.id("texteAffiche")).getText();
Assert.assertEquals(textLu, "Le texte est affiché ! (délai : 3)");

Le script se termine sur l’erreur no such element: Unable to locate element.

org.openqa.selenium.NoSuchElementException:
no such element: Unable to locate element: {"method":"css selector","selector":"#texteAffiche"}

Au moment où le script a cherché à détecter l’élément avec l’id « textAffiche » ce dernier n’était pas encore présent. En effet il aurait fallu attendre 3 secondes le temps que l’élément qui contient le texte soit affiché. Nous allons voir comment gérer ce problème tout d’abord avec le Implicit Wait Timeout puis avec le Explicit Wait Timeout.

Il est important de ne pas utiliser le Implicit et le Explicit Wait en même temps lors de l’exécution du script. Mélanger les 2 peut produire un fonctionnement erratique comme l’indique la documentation.

Warning: Do not mix implicit and explicit waits. Doing so can cause unpredictable wait times. For example, setting an implicit wait of 10 seconds and an explicit wait of 15 seconds could cause a timeout to occur after 20 seconds.

La documentation

Implicit Wait Timeout

This specifies the time to wait for the implicit element location strategy when locating elements. The default timeout 0 is imposed when a new session is created by WebDriver.

La documentation

La documentation nous dit que par défaut ce timeout est à 0. Essayons en augmentant ça valeur avec driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(3));.

driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(3L));
driver.get("https://justepourtester.net/selenium/selenium_wait.html");
driver.findElement(By.id("secondes")).clear();
driver.findElement(By.id("secondes")).sendKeys("3");
driver.findElement(By.id("btAfficherLeTexte")).click();
String textLu = driver.findElement(By.id("texteAffiche")).getText();
Assert.assertEquals(textLu, "Le texte est affiché ! (délai : 3)");

Ce coup-ci l’élément « texteAffiche » est bien détecté par le script. Si l’on configure un implicitWait à 10 secondes et que l’élément est affiché au bout de 3 secondes, le script détectera l’élément dès qu’il apparaîtra (ici 3 secondes) et poursuivra son exécution. Lorsque le WebDriver essaye de détecter un élément il scrute le DOM (Document Object Model) jusqu’à le trouver ou que le timeout soit atteint. La configuration du implicitWait est valable tout au long de la session pour tous les éléments utilisés par le script. C’est à dire qu’il est valable tout au long de l’existence du driver, il n’est pas nécessaire de le redéclarer pour chaque élément concerné. On peut en modifier la valeur plus loin dans le script. A noter l’existence de la commande driver.manage().timeouts().getImplicitWaitTimeout(); qui permet de connaitre la valeur configurée pour le timeout.

Le Implicit Wait Timeout permet de détecter la présence d’un élément mais ne donne pas la possibilité d’attendre que l’élément en question ait une certaine propriété (cliquable ou non, sélectionné ou non, valeur du texte ou d’un attribut…). Il ne permet donc pas de répondre aux scénarios les plus complexes mais aussi relativement courants dans les application Web modernes.

Explicit Wait Timeout

Le Explicit Wait va tester si un élément rempli une condition (ExpectedConditions) et ce contrôle s’effectue à intervalles réguliers (polling interval) dont la valeur par défaut est de 500 ms. Le test s’arrête soit car la condition est remplie soit car le temps est écoulé. Le Explicit Wait ne s’applique qu’à l’élément que l’on teste contrairement au Implicit Wait qui s’applique à tous les éléments utilisés par le script.

Reprenons l’exemple précédent mais en utilisant le Explicit Wait.

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10L));

driver.get("https://justepourtester.net/selenium/selenium_wait.html");
driver.findElement(By.id("secondes")).clear();
driver.findElement(By.id("secondes")).sendKeys("3");
driver.findElement(By.id("btAfficherLeTexte")).click();

wait.until(ExpectedConditions.textToBePresentInElementLocated(By.id("texteAffiche"), "Le texte est affiché ! (délai : 3)"));

Tout d’abord, il faut déclarer un objet de type WebdriverWait et lui indiquer une durée maximale pour le timeout. On utilise ce wait pour tester la condition textToBePresentInElementLocated. La fonction until retourne une valeur si la fonction retourne autre chose que null ou false avant que le timeout ait expiré. Dès que le texte attendu est affiché le script poursuit son exécution. Dans le cas contraire il y aura une exception.

org.openqa.selenium.TimeoutException:
Expected condition failed: waiting for text ('Le texte est affiché ! (délai : 5)') to be present in element found by By.id: texteAffiche (tried for 10 second(s) with 500 milliseconds interval)

Voici quelques méthodes de la classe ExpectedConditions :

elementToBeClickable, elementToBeSelected, visibilityOf, invisibilityOf, ​attributeToBe, numberOfWindowsToBe

Cliquez ici pour accéder à la liste complète des méthodes.

Nous allons maintenant faire évoluer le script pour qu’un premier texte soit affiché puis qu’un second texte différent soit affiché dans le même élément. Après le premier clic le champ contiendra « Le texte est affiché ! (délai : 3) » et on vérifiera que le champ contient bien ce texte. Ensuite on va à nouveau cliquer mais avec un délai de 5 secondes puis vérifier que le texte est bien « Le texte est affiché ! (délai : 5) ». Si on utilisait un Implicit Wait Timeout on ne parviendrait pas à savoir si le champ contient ce texte car le champ est déjà présent et contient déjà un texte qui indique 3 secondes. Le script n’attendrait pas une modification de la valeur du texte et on aurait une exception (java.lang.AssertionError: expected [Le texte est affiché ! (délai : 5)] but found [Le texte est affiché ! (délai : 3)]). Le Explicit Wait Timeout nous permet de tester la valeur du texte et on peut donc s’assurer qu’au bout de 5 secondes le champ affichera le bon texte. Ce Gif animé montre ce scénario et le code qui suit correspond au script qui le teste.

Un Gif animé montrant le scénario du test.
		WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10L));
		
		driver.get("https://justepourtester.net/selenium/selenium_wait.html");
		
		driver.findElement(By.id("secondes")).clear();
		driver.findElement(By.id("secondes")).sendKeys("3");
		driver.findElement(By.id("btAfficherLeTexte")).click();

		wait.until(ExpectedConditions.textToBePresentInElementLocated(By.id("texteAffiche"), "Le texte est affiché ! (délai : 3)"));
		
		driver.findElement(By.id("secondes")).clear();
		driver.findElement(By.id("secondes")).sendKeys("5");
		driver.findElement(By.id("btAfficherLeTexte")).click();

		wait.until(ExpectedConditions.textToBePresentInElementLocated(By.id("texteAffiche"), "Le texte est affiché ! (délai : 5)"));

On pourrait se demander pourquoi ne pas interrompre le script avec la méthode Thread.sleep(). Cette méthode va mettre le script en pause pour la durée qui lui ait passée en paramètre. Cela répondrait bien au cas que nous avons vu. Il est cependant fortement déconseillé d’utiliser sleep(). La première raison est que le script sera plus lent puisqu’il devra toujours attendre le temps indiqué même si l’élément est déjà présent sur la page. La seconde raison est que le script ne sera pas fiable puisque si l’élément met plus de temps que prévu à apparaître le script échouera. Explicit Wait nous permet de configurer un timeout maximal et peut donc s’adapter au cas où l’élément met plus de temps à apparaître et le script poursuivera son exécution dès lors que l’élément répond à la condition.

Puisque la classe WebDriverWait est une spécialisation de la classe FluentWait il est possible de définir la fréquence à laquelle la condition est vérifiée (polling interval). L’utilisateur peut aussi choisir d’ignorer des exceptions en plus de NoSuchElementExceptions lorsque le wait est en cours. Pour aller plus loin dans la personnalisation de WebDriverWait vous pouvez consulter l’article Personnaliser la méthode apply de WebDriverWait.

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10L));   
wait.pollingEvery(Duration.ofSeconds(1L));
wait.ignoring(ArithmeticException.class);
wait.withMessage("Message personnalisé affiché lorsque le délai est écoulé");

Conclusion

On voit que par l’intermédiaire du Implicit Wait Timeout et du Explicit Wait Timeout, Selenium nous permet d’écrire des tests automatisés qui s’adaptent aux interfaces les plus complexes. Maitriser ces deux concepts est primordiale pour que les scripts soient fiables. Travailler la testabilité de son application permet aussi de ne pas se retrouver dans des situations où il faut écrire un script compliqué. J’espère que cet article vous aura été utile. Bons tests !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *