16 ans déjà que Selenium existe ! Première version en 2004, Selenium 2 en 2011, Selenium 3 en 2016, et depuis le printemps 2019 il est déjà possible de s’amuser avec Selenium 4. Toujours en version alpha en février 2020, cette nouvelle mouture propose entre autres une nouvelle manière d’identifier les éléments d’une page web : les Relative Locators. Vous qui avez à coeur de faire de la veille sur les outils de test automatisé, c’est le moment de découvrir cette nouvelle fonctionnalité !
Que sont les Relative Locators ?
Vous connaissiez déjà les multiples manières proposées par Selenium pour identifier les objets : par attribut « id » ou « name », par « tag » (balise HTML), par xPath et par sélecteur CSS.
En plus de ces différentes manières, il est désormais possible de sélectionner des élément en fonction de leur position par rapport à d’autres éléments. Par exemple : l’image qui se trouve en-dessous du menu, le lien qui se trouve à gauche du logo, le bouton qui se trouve près (moins de 50 pixels) du footer. Seul l’élément par rapport auquel on se positionne nécessite un sélecteur ; pour l’élément cible, on n’indique que le nom de sa balise HTML.
Exemple de Relative Locator
// Avant WebElement imageEnDessousDuMenu = driver.findElement(By.xpath("//div[@id=’menu’]//..//img")); // Ca ne vérifie pas que l’image est en-dessous du menu, juste qu’ils ont le même parent, par exemple une div // Après WebElement imageEnDessousDuMenu = driver.findElement(withTagName("img").below(By.id("menu")));
Les Relative Locators devaient d’abord s’appeler « Friendly Locators », et on comprend pourquoi : en intégrant dans le framework des notions visuelles, le code peut devenir plus simple à lire.
Sans vouloir jouer les trouble-fêtes…
Pour autant, nous n’avons pas réussi à nous souvenir de cas où ces localisateurs nous auraient vraiment sauvé la vie. Jusqu’à présent, nous avons toujours pu trouver des solutions satisfaisantes et robustes se basant sur les localisateurs historiques. Alors, les Relative Locators seraient-ils un ajout « sympa mais pas si utile » ?
Peut-on maintenant vérifier la position des éléments ?
Alors… si, il y a bien quelque chose qui pourrait être très utile : ce serait de vérifier la position relative d’un élément par rapport à un autre.
Comment ça, ce n’est pas ce qu’on vient de faire ?
Hé non, pas tout à fait. Certes, à chaque fois que notre code Selenium interagit avec un élément, cela nous permet de savoir qu’il existe. Pour autant, si le localisateur de notre « imageEnDessousDuMenu » renvoie une erreur « NoSuchElementException », on ne saura pas si c’est parce qu’il n’y a pas d’image sur la page, si le menu a changé d’ID, ou s’il y a bien la bonne image mais qu’elle n’est plus en-dessous mais au-dessus. Et parfois, on a besoin de savoir précisément si tel élément est bien situé par exemple à un autre. Par exemple, quand on teste le caractère responsive d’une page web.
Nous pensions que Selenium 4 arriverait avec des méthodes pratiques facilitant les assertions, du genre isElementBelow(WebElement unAutreElement), mais ce n’est pas (encore ?) le cas.
Cependant, il est bien sûr possible de s’en sortir avec des méthodes sur mesure.
Ce qui nous amène à notre exemple de mise en application !
Test de position des éléments avec Selenium 4
Cas d’usage : plein écran et demi-écran
Je visite le site https://service-public.nc/ depuis mon ordinateur portable, dont le système d’exploitation est Windows 10. La fenêtre de mon navigateur est au maximum. Ce que je lis est intéressant, je voudrais prendre des notes : je fais basculer mon navigateur en mode demi-écran, pour avoir le bloc-notes sur l’autre partie de l’écran.
Sur la page d’accueil du menu « Particuliers », en plein écran, les blocs se présentent ainsi :
En responsive, les blocs se réorganisent. Je souhaite évidemment que les éléments du site soient visibles et cohérents dans les deux affichages.
Vues différenciées
Vue desktop
Vue « demi-desktop »
Assertions à faire
Dans notre test automatisé Selenium 4, nous allons vérifier les assertions suivantes.
En mode desktop :
- Le titre « Particuliers » est au-dessus de l’article 1
- Le titre « Particuliers » est à la même abscisse (X) que l’article 1
- Le titre « Particuliers » est à la même ordonnée (Y) que le bloc « Vos services en un clic »
- Le titre « Particuliers » est à gauche du bloc « Vos services en un clic »
- L’article 1 est à la même ordonnée (Y) que l’article 2
- L’article 1 est à gauche de l’article 2.
En mode « demi-desktop » :
- Le titre « Particuliers » est au-dessus de l’article 1 (ça ne change pas)
- Le titre « Particuliers » est à la même abscisse (X) que l’article 1 (ça ne change pas)
- Le titre « Particuliers » est au-dessus du bloc « Vos services en un clic »
- Le titre « Particuliers » est à la même abscisse (X) « Vos services en un clic »
- L’article 1 est au-dessus de l’article 2
- L’article 1 est à la même abscisse (X) que l’article 2.
Allez, c’est parti !
Des méthodes de bas niveau pour vérifier la position des éléments
A gauche, à droite en haut ou en bas ?
A l’aide d’un enum, on liste seulement deux possibilités, histoire d’homogénéiser les choses dès le début. On n’a pas envie de se demander si c’est le menu X qui est à gauche du formulaire Y ou si c’est le formulaire Y qui est à gauche du menu X…
public enum POSITION { A_GAUCHE_DE, AU_DESSUS_DE }
On utilise ensuite cet enum dans une méthode de bas niveau. Attention, cette méthode utilise l’objet « Selecteur » ; vous ne le connaissez peut-être pas encore, mais nous vous recommandons d’en créer un pour vos projets (pour plus d’infos, rendez-vous sur ce précédent article, qui explique les avantages du Selecteur).
On vérifie 3 choses dans cette méthode :
- Premièrement, que les deux éléments existent
- Deuxièmement, qu’il y a au moins un élément du même type que l’élément 1 dans la position souhaitée par rapport à l’élément 2
- Troisièmement, que l’élément 1 fait partie de ces éléments
A chaque vérification son message d’erreur spécifique.
Attention, la méthode ne vérifie pas si l’élément 1 est le seul à répondre au critère, ni s’il est le plus proche. A vous de voir si ce niveau de contrôle vous suffit, ou si vous souhaitez le renforcer.
public void verifierPosition(String tagElement1, Selecteur element1, POSITION position, Selecteur element2){ // Vérification 1 Assert.assertTrue(element1.getNom() + " aurait dû être présent(e)", driver.findElements(element1.getChemin()).size() != 0); Assert.assertTrue(element2.getNom() + " aurait dû être présent(e)", driver.findElements(element2.getChemin()).size() != 0); // Vérification 2 List<WebElement> elementsMemeTypeMemePositionRelative = null; if(position == POSITION.A_GAUCHE_DE) { elementsMemeTypeMemePositionRelative = driver.findElements(withTagName(tagElement1).toLeftOf(element2.getChemin())); } else if (position == POSITION.AU_DESSUS_DE) { elementsMemeTypeMemePositionRelative = driver.findElements(withTagName(tagElement1).above(element2.getChemin())); } Assert.assertTrue("au moins un élément " + tagElement1 + " aurait dû être présent à la position relative souhaitée (" + position.name() + ") par rapport à l'élément suivant : " + element2.getNom() + ")", elementsMemeTypeMemePositionRelative.size() >= 1); // Vérification 3 WebElement webElement1 = driver.findElement(element1.getChemin()); // Assert.assertTrue(element1.getNom() + " aurait dû être affiché(e) dans la bonne position (" + position.name() + ") par rapport à l'élément suivant : " + element2.getNom(), elementsMemeTypeMemePositionRelative.contains(webElement1)); }
Au passage, avez-vous repéré les méthodes Selenium 4, toLeftOf() et above() ? 🙂
Libre à vous ensuite de surcharger cette méthode de bas niveau afin d’améliorer la lisibilité de votre code :
public void verifierElementAGaucheDe(String tagElement1, AbstractPage.Selecteur element1, AbstractPage.Selecteur element2){ verifierPosition(tagElement1, element1, AbstractPage.POSITION.A_GAUCHE_DE, element2); } public void verifierElementAuDessusDe(String tagElement1, AbstractPage.Selecteur element1, AbstractPage.Selecteur element2){ verifierPosition(tagElement1, element1, AbstractPage.POSITION.AU_DESSUS_DE, element2); }
Même abscisse ou même ordonnée
On commence encore d’abord par un enum pour déclarer les deux axes possibles :
public enum AXE { ABSCISSE_X, ORDONNEE_Y }
On a encore 3 vérifications :
- Les deux éléments existent
- Il y a au moins un élément du même type que l’élément 1 qui ne soit ni à gauche ni à droite (ou ni en haut ni en bas) de l’élément 2
- L’élément 1 fait partie de ces éléments
public void verifierMemeAxe(AXE axe, String tagElement1, AbstractPage.Selecteur element1, Selecteur element2){ Assert.assertTrue(element1.getNom() + " aurait dû être présent(e)", driver.findElements(element1.getChemin()).size() != 0); Assert.assertTrue(element2.getNom() + " aurait dû être présent(e)", driver.findElements(element2.getChemin()).size() != 0); List<WebElement> elementsMemeType = driver.findElements(withTagName(tagElement1)); if(axe == AXE.ABSCISSE_X){ // Là, on liste les éléments à gauche et à droite de notre élément (ceux qui ont un X différent) List<WebElement> elementsAGauche = driver.findElements(withTagName(tagElement1).toLeftOf(element2.getChemin())); List<WebElement> elementsADroite = driver.findElements(withTagName(tagElement1).toRightOf(element2.getChemin())); // On supprime de notre liste initiale tous les élements qui ont un X différent de notre élément elementsMemeType.removeAll(elementsAGauche); elementsMemeType.removeAll(elementsADroite); } else if (axe == AXE.ORDONNEE_Y) { List<WebElement> elementsAuDessus = driver.findElements(withTagName(tagElement1).above(element2.getChemin())); List<WebElement> elementsEnDessous = driver.findElements(withTagName(tagElement1).below(element2.getChemin())); elementsMemeType.removeAll(elementsAuDessus); elementsMemeType.removeAll(elementsEnDessous); } Assert.assertTrue("au moins un élément " + tagElement1 + " aurait dû être présent à la même " + axe.name() + " que l'élément suivant : " + element2.getNom() + ")", elementsMemeType.size() >= 1); WebElement webElement1 = driver.findElement(element1.getChemin()); Assert.assertTrue(element1.getNom() + " aurait dû être affiché(e) avec la même " + axe.name() + " que l'élément suivant : " + element2.getNom(), elementsMemeType.contains(webElement1)); }
Et les méthodes « surchargeantes » :
public void verifierElementMemeOrdonnee(String tagElement1, Selecteur element1, Selecteur element2){ verifierMemeAxe(AXE.ORDONNEE_Y, tagElement1, element1, element2); } public void verifierElementsMemeAbscisse(String tagElement1, Selecteur element1, Selecteur element2){ verifierMemeAxe(AXE.ABSCISSE_X, tagElement1, element1, element2); }
Résultat final !
On a désormais tout ce qu’il nous faut pour effectuer automatiquement les vérifications évoquées plus haut. Et les voilà, au sein d’un objet page (encore une fois si ce concept ne vous parle pas, rendez-vous sur cet article qui explique l’intérêt d’avoir des objets page) :
public class ServicePublicPage extends AbstractPage { private final Selecteur ongletParticuliersMenu = new Selecteur("l'onglet 'Particuliers' du menu", By.xpath("//a[contains(., 'Particuliers')]")); private final Selecteur titreParticuliers = new Selecteur("le titre 'Particuliers'", By.id("page-title")); private final Selecteur blocVosServices = new Selecteur("le bloc intitulé 'Vos services en un clic'", By.id("block-gnc-services-services-menu")); private final Selecteur blocSanteSocial = new Selecteur("le bloc intitulé 'Santé - Social'", By.xpath("//article[contains(., 'Santé - Social')]")); private final Selecteur blocTravail = new Selecteur("le bloc intitulé 'Travail'", By.xpath("//article[contains(., 'Travail')]")); public void accesPartieParticuliers(){ clickElement(ongletParticuliersMenu); } public void verifierMenuDesktop(){ driver.manage().window().maximize(); verifierElementAuDessusDe("h1", titreParticuliers, blocSanteSocial); verifierElementsMemeAbscisse( "h1", titreParticuliers, blocSanteSocial); verifierElementMemeOrdonnee( "h1", titreParticuliers, blocVosServices); verifierElementAGaucheDe("h1", titreParticuliers, blocVosServices); verifierElementMemeOrdonnee( "article", blocSanteSocial, blocTravail); verifierElementAGaucheDe( "article", blocSanteSocial, blocTravail); } public void verifierMenuDesktop_demiEcran(){ driver.manage().window().maximize(); int largeurEcran = driver.manage().window().getSize().width; int hauteurEcran = driver.manage().window().getSize().height; driver.manage().window().setSize(new Dimension(largeurEcran/2, hauteurEcran)); verifierElementAuDessusDe("h1", titreParticuliers, blocSanteSocial); verifierElementsMemeAbscisse( "h1", titreParticuliers, blocSanteSocial); verifierElementAuDessusDe("h1", titreParticuliers, blocVosServices); verifierElementsMemeAbscisse( "h1", titreParticuliers, blocVosServices); verifierElementAuDessusDe( "article", blocSanteSocial, blocTravail); verifierElementsMemeAbscisse( "article", blocSanteSocial, blocTravail); } }
Maintenant, vous n’avez plus qu’à utiliser ces méthodes dans vos tests !
public class testServicePublicNC extends AbstractTest { @DisplayName("Vérifier l'ordre du menu en mode desktop") @Test public void verifierOrdreMenu_desktop(){ ServicePublicPage servicePublicPage = new ServicePublicPage(); servicePublicPage.accesPartieParticuliers(); servicePublicPage.verifierMenuDesktop(); } @DisplayName("Vérifier l'ordre du menu en mode desktop - demi-écran") @Test public void verifierOrdreMenu_desktop_demiEcran(){ ServicePublicPage servicePublicPage = new ServicePublicPage(); servicePublicPage.accesPartieParticuliers(); servicePublicPage.verifierMenuDesktop_demiEcran(); } }
Bonus : si vous devez attendre avant de monter de version
N’oublions pas que Selenium 4 est encore en version Alpha. En attendant qu’une version stable soit livrée, et si vous en avez besoin dans vos projets actuels, il est déjà possible de réaliser les vérifications évoquées dans l’article. Cet article fournit un tutoriel très clair, en Java également.