Chapitre en accès libre

Atelier : 12 XPath validés dans la console

20 min de travail — lecture et pratique

Vous allez écrire douze expressions XPath et vérifier chacune d’elles dans le navigateur, avant qu’une seule ligne de Java n’existe. C’est le geste qui sépare une session d’automatisation fluide d’un après-midi passé à relancer des tests pour découvrir une NoSuchElementException.

Pourquoi valider dans la console d’abord

Quand un XPath est faux dans un test Selenium, voici la boucle que vous subissez : compiler, lancer le test, attendre l’ouverture de Chrome, attendre le chargement de la page, attendre l’expiration du wait, lire la trace d’erreur, corriger, recommencer. Chaque tentative vous coûte une compilation, un démarrage de navigateur et, si l’élément est introuvable, l’attente complète du timeout. Un XPath un peu tordu demande rarement une seule tentative.

Dans la console du navigateur, la même vérification prend le temps d’appuyer sur Entrée. La page est déjà chargée, le DOM est déjà là, et vous voyez immédiatement ce que votre expression sélectionne — ou qu’elle ne sélectionne rien.

L’outil s’appelle $x(). C’est une fonction utilitaire fournie par les outils de développement de Chrome, Edge et Firefox. Elle n’existe que dans la console : elle n’est pas disponible dans le code JavaScript d’une page, et elle n’a rien à voir avec jQuery malgré le $.

La règle de travail qu’on installe ici, et qu’on ne quittera plus :

Aucun XPath n’entre dans un fichier .java avant d’avoir été validé dans une console.

Préparer le terrain

1. Créer la page cible

Nous n’utiliserons pas un site public. Un site public change sans prévenir : une classe CSS renommée, un bandeau de cookies ajouté, et les résultats attendus de ce chapitre ne correspondent plus à ce que vous voyez. Vous passeriez votre temps à vous demander si vous vous êtes trompé ou si le site a bougé.

Vous allez donc travailler sur une page locale, figée, dont le contenu est connu au caractère près. Les douze résultats annoncés plus bas ont été vérifiés sur cette page. Si vous obtenez autre chose, c’est votre expression qui est en cause, jamais la cible.

Créez un fichier nommé atelier-xpath.html dans un dossier de travail, avec exactement ce contenu :

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Atelier XPath - Suivi des tickets</title>
<style>
  body { font-family: system-ui, sans-serif; margin: 2rem; }
  table { border-collapse: collapse; margin-top: 8px; }
  th, td { border: 1px solid #999; padding: 4px 10px; }
  .champ { margin-bottom: 8px; }
</style>
</head>
<body>

<header id="entete">
  <h1>Suivi des tickets</h1>
  <p class="sous-titre">Atelier XPath</p>
</header>

<nav id="menu">
  <ul class="liens">
    <li><a href="/accueil">Accueil</a></li>
    <li><a href="/tickets/ouverts">Tickets ouverts</a></li>
    <li><a href="/tickets/clos">Tickets clos</a></li>
    <li><a href="/aide">Aide</a></li>
  </ul>
</nav>

<section id="recherche">
  <div class="champ">
    <label for="titre">Titre</label>
    <input id="titre" name="titre" type="text" placeholder="Titre du ticket">
  </div>
  <div class="champ">
    <label for="auteur">Auteur</label>
    <input id="auteur" name="auteur" type="text" placeholder="Nom de l'auteur">
  </div>
  <div class="champ">
    <span class="enveloppe">
      <input id="archives" name="archives" type="checkbox">
    </span>
    <label for="archives">Inclure les archives</label>
  </div>
  <button id="btn-rechercher" type="button">Rechercher</button>
  <button id="btn-reinitialiser" type="button">Reinitialiser</button>
</section>

<section id="resultats">
  <table id="table-tickets">
    <thead>
      <tr>
        <th>Reference</th><th>Titre</th><th>Statut</th><th>Auteur</th>
      </tr>
    </thead>
    <tbody>
      <tr class="ligne" data-ref="TCK-1041">
        <td>TCK-1041</td><td>Connexion impossible</td><td class="statut">Ouvert</td><td>Nadia</td>
      </tr>
      <tr class="ligne" data-ref="TCK-1042">
        <td>TCK-1042</td><td>Export CSV tronque</td><td class="statut">Ouvert</td><td>Marc</td>
      </tr>
      <tr class="ligne" data-ref="TCK-1043">
        <td>TCK-1043</td><td>Lenteur du tableau de bord</td><td class="statut">Clos</td><td>Nadia</td>
      </tr>
    </tbody>
  </table>
</section>

<section id="notes">
  <ul class="notes-ouvertes">
    <li>Relancer le client</li>
    <li>Verifier les logs</li>
  </ul>
  <ul class="notes-archive">
    <li>Ancien incident reseau</li>
    <li>Migration terminee</li>
  </ul>
</section>

<footer id="pied">
  <p>Atelier XPath - version 1</p>
</footer>

</body>
</html>

Le texte affiché par la page est volontairement écrit sans accents. Les chaînes que vous taperez dans vos expressions (Ouvert, Clos, TCK-1042) ne contiennent donc aucun caractère qui puisse mal passer selon l’encodage de votre éditeur ou la disposition de votre clavier. C’est une simplification d’atelier, pas une règle de la vraie vie : sur une application réelle, vous chercherez des libellés accentués, et il faudra que votre fichier source soit en UTF-8.

2. Ouvrir la page

Double-cliquer sur le fichier fonctionne. En ligne de commande, depuis le dossier qui contient le fichier :

Système Commande
Windows, invite de commandes (cmd) start atelier-xpath.html
Windows, PowerShell Invoke-Item .\atelier-xpath.html
macOS open atelier-xpath.html
Linux xdg-open atelier-xpath.html

En PowerShell, start est un alias de Start-Process, qui résout les chemins relatifs à partir du répertoire courant du processus et non de l’emplacement affiché par la console : après un cd, il peut répondre que le fichier est introuvable. Invoke-Item n’a pas ce défaut.

Critère de réussite. Vous voyez un titre « Suivi des tickets », deux champs de saisie, une case à cocher, deux boutons, un tableau de trois lignes de tickets et deux listes de notes. La barre d’adresse commence par file:///.

3. Ouvrir la console

Navigateur Windows / Linux macOS
Chrome, Edge Ctrl + Maj + J Cmd + Option + J
Firefox Ctrl + Maj + K Cmd + Option + K

Un panneau s’ouvre avec une invite. Cliquez dedans.

4. Le premier $x

Tapez ceci, puis Entrée :

$x("//h1")

Critère de réussite. La console affiche un tableau contenant un élément, quelque chose comme (1) [h1]. Cliquez sur le triangle à gauche pour le déplier : vous voyez le <h1>Suivi des tickets</h1>. Survolez cette ligne : le titre se met en surbrillance dans la page.

Si vous obtenez autre chose, allez directement à la section « Quand ça rate » plus bas — en particulier le point sur le collage dans Chrome, qui surprend au premier essai.

Comment lire un résultat, sans se mentir

$x() renvoie un tableau. Ce qui vous intéresse, presque toujours, c’est sa taille. Prenez tout de suite le réflexe d’ajouter .length :

$x("//td").length

Vous devez obtenir exactement 12.

C’est le critère le plus fiable, parce qu’il ne dépend ni du navigateur, ni de la façon dont la console formate son affichage. Un XPath destiné à findElement doit renvoyer 1. Un XPath destiné à findElements doit renvoyer le nombre de lignes que vous attendez. Tout le reste est un bug qui vous attend à l’exécution.

Deux formes valent la peine d’être connues :

$x("//td")[0]

Le premier élément du tableau, dans le format d’objet DOM de la console. Utile pour inspecter.

$x("count(//td)")

Dans Chrome et Edge, $x() sait renvoyer un nombre quand l’expression XPath en produit un : vous obtenez 12. Firefox ne garantit pas ce comportement — si vous êtes sur Firefox, restez sur .length.

Un dernier réflexe, gratuit :

$x("//li").length

Résultat : 8. Dépliez le tableau : les huit li arrivent dans l’ordre du document, c’est-à-dire l’ordre dans lequel ils apparaissent dans le HTML — les quatre liens du menu, puis les deux notes ouvertes, puis les deux notes archivées. Ce n’est pas un ordre arbitraire, et c’est ce qui donne un sens à la notion de « premier ».

Les douze expressions

Chaque expression vient avec son intention, son expression, et son résultat attendu. Tapez-les dans l’ordre. Ne passez pas à la suivante tant que le compte n’est pas celui annoncé.

Expression 1 — Le chemin absolu

Intention. Sélectionner la cellule « référence » de la deuxième ligne du tableau, en partant de la racine du document. C’est exactement ce que produit le « Copy full XPath » des outils de développement.

$x("/html/body/section[2]/table/tbody/tr[2]/td[1]").length

Résultat attendu : 1. L’élément est <td>TCK-1042</td>.

Lisez le chemin : depuis html, vers body, vers la deuxième section enfant directe de body, vers sa table, son tbody, sa deuxième tr, sa première td. Chaque / est un cran de descente, et chaque cran est une hypothèse sur la structure de la page.

Pourquoi l’absolu casse — faites-le casser maintenant

Un chemin absolu ne survit pas au premier changement de mise en page. Prouvez-le. Collez ceci dans la console :

document.body.insertAdjacentHTML("afterbegin", "<section id='bandeau'><p>Maintenance prevue samedi</p></section>");

Un bandeau apparaît en haut de la page. Vous venez de simuler ce qu’un développeur fait un mardi après-midi : ajouter un encart d’information. Maintenant relancez, sans rien changer, l’expression 1 :

$x("/html/body/section[2]/table/tbody/tr[2]/td[1]").length

Résultat attendu : 0.

Rien n’a changé dans le tableau. Aucune référence, aucun texte, aucun attribut n’a bougé. Mais section[2] ne désigne plus la même section : la nouvelle s’est insérée en première position, et l’ancienne deuxième est devenue la troisième. Votre localisateur pointe désormais sur le bloc de recherche, qui ne contient aucune table. Vérifiez-le si vous voulez en avoir le cœur net :

$x("/html/body/section[2]")[0].id

Résultat : "recherche".

C’est ainsi que meurent les suites de tests écrites au « Copy full XPath ». Le test échoue, le message dit « élément introuvable », et la fonctionnalité testée marche parfaitement.

Rechargez la page (F5, ou Cmd + R sur macOS) pour retirer le bandeau avant de continuer.

Expression 2 — Le chemin relatif équivalent

Intention. Viser la même cellule, mais en partant d’un point d’ancrage stable au lieu de la racine.

$x("//h1")
0

Résultat attendu : 1. Le même <td>TCK-1042</td>.

Le // initial signifie « n’importe où dans le document ». On cherche la table par son identifiant — une information que le développeur a mise là exprès, et qui a peu de raisons de changer — puis on descend de trois crans seulement.

Vérifiez la robustesse : réinsérez le bandeau avec la commande du paragraphe précédent, puis relancez l’expression 2. Elle renvoie toujours 1. Rechargez ensuite la page.

Ce qu’il faut retenir. Un XPath n’est pas jugé sur sa longueur, mais sur le nombre d’hypothèses qu’il fait sur la page. L’expression 1 enchaîne sept crans, donc sept hypothèses de structure, et il suffit qu’une seule tombe pour que tout tombe. L’expression 2 en fait quatre, dont la première — l’identifiant table-tickets — est une donnée posée volontairement par le développeur.

Expression 3 — Le / qui ne trouve rien

Intention. Récupérer les champs de saisie du bloc de recherche. Naïvement : « les input de la section recherche ».

$x("//section[@id='recherche']/input").length

Résultat attendu : 0.

Aucun résultat. Pourtant la section contient bien trois input, vous les voyez à l’écran. Le problème est le / : il exige un enfant direct. Or les input sont enfermés dans des <div class="champ">. Entre la section et l’input, il y a un étage.

Ce zéro est facile à produire et difficile à voir : dans Selenium, il ne se manifeste qu’à l’exécution, sous forme de NoSuchElementException après l’expiration complète du wait.

Expression 4 — Le // en milieu d’expression

Intention. La même chose, mais en acceptant n’importe quelle profondeur.

$x("//section[@id='recherche']//input").length

Résultat attendu : 3.

Le // au milieu de l’expression ne veut pas dire « descends de deux crans ». Il veut dire « descends de n’importe quel nombre de crans, y compris zéro ». En écriture complète, a//b est l’abréviation de a/descendant-or-self::node()/child::b. Retenez cette forme développée : elle explique à elle seule les deux pièges des expressions 9 et 10.

C’est très commode, et c’est exactement pour ça que c’est un piège. Comparez :

$x("//div[@class='champ']/input").length

Résultat : 2.

$x("//div[@class='champ']//input").length

Résultat : 3.

Les trois div.champ contiennent chacun un input, mais celui de la case à cocher est enveloppé dans un <span class="enveloppe">. Le / en attrape deux, le // en attrape trois.

La règle de travail. Un // en milieu d’expression rend le localisateur tolérant à la structure, ce qui est bien, mais il peut aussi ramasser des éléments que vous n’aviez pas prévus — un champ caché, un bouton dans un sous-composant. Chaque fois que vous en mettez un, vérifiez le .length. C’est tout l’intérêt de la console.

Expression 5 — Le prédicat sur attribut

Intention. Viser un champ précis par son attribut name.

$x("//input[@name='auteur']").length

Résultat attendu : 1.

Les crochets contiennent un prédicat : une condition que le nœud doit satisfaire. @ désigne un attribut. Cette forme, //balise[@attribut='valeur'], est celle que vous écrirez le plus souvent.

Quelques variantes utiles, à essayer :

$x("//tr[@data-ref]").length

Résultat : 3. Sans =, le prédicat teste seulement la présence de l’attribut.

$x("//*[@id='titre']").length

Résultat : 1. L’astérisque veut dire « n’importe quelle balise ».

Expression 6 — Le prédicat sur texte

Intention. Trouver toutes les cellules de statut dont le texte est exactement « Ouvert ».

$x("//td[text()='Ouvert']").length

Résultat attendu : 2.

text() désigne le contenu textuel direct du nœud. Deux tickets sur trois sont ouverts : le compte est cohérent avec ce que vous lisez dans le tableau.

Contrôle croisé :

$x("//td[text()='Clos']").length

Résultat : 1.

Le piège à connaître dès maintenant. text() ne voit que le texte direct du nœud, pas celui de ses descendants. Essayez :

$x("//td").length
0

Résultat : 0. Le texte « Accueil » appartient au <a>, pas au <li>. Maintenant :

$x("//li[. = 'Accueil']").length

Résultat : 1. Le point désigne la valeur textuelle de l’élément et de tout ce qu’il contient.

Retenez la distinction : text() pour le texte propre, . pour le texte de l’ensemble du sous-arbre. Sur une page réelle, où les libellés sont souvent enveloppés dans un <span>, c’est . qui vous dépannera le plus souvent.

Expression 7 — contains()

Intention. Trouver les liens de navigation qui mènent aux tickets, sans dépendre de l’URL exacte.

$x("//a[contains(@href, 'tickets')]").length

Résultat attendu : 2. Les liens « Tickets ouverts » et « Tickets clos ». Le lien « Accueil » et le lien « Aide » sont écartés.

contains(chaîne, sous-chaîne) renvoie vrai si la première contient la seconde. C’est la fonction qui sauve la mise quand une valeur d’attribut est partiellement générée par le serveur ou le framework — un identifiant du type form-4821-submit, par exemple.

Même mise en garde qu’à l’expression 6 : contains(text(), '...') ne regarde que le texte direct. Sur du texte imbriqué, utilisez contains(., '...').

$x("//li[contains(., 'Accueil')]").length

Résultat : 1.

Expression 8 — starts-with()

Intention. Sélectionner toutes les cellules de référence, reconnaissables à leur préfixe.

$x("//td[starts-with(text(), 'TCK-')]").length

Résultat attendu : 3.

starts-with() est plus strict que contains() : la sous-chaîne doit être au début. Quand un préfixe est stable et significatif — un code produit, un préfixe d’identifiant technique — starts-with() dit mieux votre intention et attrape moins de faux positifs.

Notez qu’il n’existe pas de ends-with() utilisable ici. On y revient à la fin du chapitre.

Expression 9 — La position [1]

Intention. Prendre le premier élément de liste de chaque liste de la page.

$x("//ul/li[1]").length

Résultat attendu : 3.

Trois, et pas un. C’est le point qui surprend à la première lecture, et il vaut la peine de s’y arrêter.

La page contient trois <ul> : le menu de navigation, les notes ouvertes, les notes archivées. //ul/li[1] se lit : « pour chaque ul trouvé, le li qui occupe la première position parmi ses propres enfants ». Le prédicat de position s’applique dans le contexte de chaque parent, pas au niveau du document. C’est la forme développée vue à l’expression 4 qui le dit : le [1] porte sur l’étape child::li, pas sur le résultat final.

Dépliez le résultat pour le voir : vous obtenez « Accueil », « Relancer le client » et « Ancien incident reseau ».

Si vous écriviez ce localisateur dans un driver.findElement(...), Selenium vous rendrait le premier des trois sans le moindre avertissement. Votre test passerait, en testant autre chose que ce que vous croyez.

Expression 10 — La différence avec (…)[1]

Intention. Prendre le tout premier li du document, un seul.

$x("(//ul/li)[1]").length

Résultat attendu : 1. C’est le <li> du lien « Accueil ».

Les parenthèses changent tout. Elles disent : « calcule d’abord l’ensemble complet des nœuds correspondant à //ul/li, remets-les dans l’ordre du document, puis prends le premier ».

Expression Sens Résultat
//ul/li[1] le premier li de chaque ul 3 éléments
(//ul/li)[1] le premier li de la page 1 élément

Quand vous voulez « le premier bouton de la page » et que vous écrivez //button[1], vous demandez en réalité « tout bouton qui est le premier enfant button de son parent ». Sur cette page :

$x("//button[1]").length

Résultat : 1, parce qu’une seule section contient des boutons, et que le second bouton n’est pas le premier button de son parent. Sur une page avec plusieurs barres d’actions, vous en auriez autant que de barres. La forme sans ambiguïté, celle qui ne vous trahira pas, est (//button)[1].

Expression 11 — L’axe following-sibling

Intention. Partir d’une cellule dont vous connaissez le contenu — la référence du ticket — et atteindre une autre cellule de la même ligne, dont vous ne connaissez pas le contenu. C’est un besoin constant dès qu’on teste un tableau : « quel est le statut du ticket TCK-1042 ? »

$x("//td[text()='TCK-1042']/following-sibling::td[2]").length

Résultat attendu : 1. L’élément est <td class="statut">Ouvert</td>.

Décomposons. //td[text()='TCK-1042'] trouve la cellule d’ancrage. following-sibling::td sélectionne toutes les cellules qui suivent, au même niveau, sous le même parent. Vérifiez :

$x("//td[text()='TCK-1042']/following-sibling::td").length

Résultat : 3 — le titre, le statut, l’auteur. Le [2] de l’expression 11 prend le deuxième de cet ensemble, donc le statut.

Attention : la numérotation part du nœud d’ancrage, pas du début de la ligne. following-sibling::td[2] est la deuxième cellule après la référence, c’est-à-dire la troisième colonne du tableau. C’est une source d’erreur classique quand on compte de tête.

L’axe symétrique existe : preceding-sibling::, pour remonter vers la gauche.

Expression 12 — L’axe parent

Intention. Partir d’une cellule et récupérer la ligne entière, par exemple pour cliquer dessus ou pour compter ses colonnes.

$x("//td")[0]
0

Résultat attendu : 1. L’élément est <tr class="ligne" data-ref="TCK-1043">.

parent::tr remonte d’un cran et vérifie au passage que le parent est bien un tr. Vous pouvez écrire parent::* pour remonter sans condition de balise, ou .., qui est le raccourci équivalent.

L’usage courant consiste à remonter puis redescendre :

$x("//td[text()='TCK-1041']/parent::tr/td[4]")[0].textContent

Résultat attendu : "Nadia".

Vous venez d’écrire, en une expression, « donne-moi l’auteur du ticket TCK-1041 ». C’est ce type de localisateur qui rend un test lisible : il exprime la règle métier, pas la géométrie de la page.

Récapitulatif : les douze, validés

# Expression Attendu Notion
1 /html/body/section[2]/table/tbody/tr[2]/td[1] 1 chemin absolu (fragile)
2 //table[@id='table-tickets']/tbody/tr[2]/td[1] 1 chemin relatif ancré
3 //section[@id='recherche']/input 0 / = enfant direct
4 //section[@id='recherche']//input 3 // = descendant
5 //input[@name='auteur'] 1 prédicat sur attribut
6 //td[text()='Ouvert'] 2 prédicat sur texte
7 //a[contains(@href, 'tickets')] 2 contains()
8 //td[starts-with(text(), 'TCK-')] 3 starts-with()
9 //ul/li[1] 3 position par parent
10 (//ul/li)[1] 1 position dans le document
11 //td[text()='TCK-1042']/following-sibling::td[2] 1 axe following-sibling
12 //td[text()='TCK-1043']/parent::tr 1 axe parent

Si les douze comptes correspondent, l’atelier est réussi. Gardez ce tableau : c’est votre grille de relecture quand un localisateur vous résistera.

Quand ça rate

Chrome refuse le collage et affiche un avertissement

Symptôme : vous collez une expression, et au lieu de l’exécuter la console affiche un avertissement sur les risques du collage de code, en vous demandant de taper une phrase de confirmation.

C’est une protection normale de Chrome, pas une panne. Tapez dans la console exactement la phrase demandée (en anglais, allow pasting), validez, puis recollez. La console ne vous le redemandera plus pour cet onglet.

Uncaught ReferenceError: $x is not defined

$x() n’existe que dans l’invite de la console des outils de développement. Vérifiez que vous tapez bien dans l’onglet Console, et non dans l’onglet Sources, dans un Snippet, ou dans un fichier de la page.

Second cas possible : le sélecteur de contexte en haut de la console (il affiche top par défaut) pointe sur une iframe ou sur une extension. Remettez-le sur top.

SyntaxError sur une expression pourtant correcte

Presque toujours un problème de guillemets. Si vous avez copié l’expression depuis un traitement de texte, une messagerie ou un support de cours, les guillemets droits " et ' ont pu être transformés en guillemets typographiques “ ” ‘ ’. JavaScript ne les reconnaît pas comme des délimiteurs de chaîne.

Réécrivez l’expression à la main dans la console. Si elle passe, le problème venait de là.

$x("...") renvoie [] alors que l’élément est visible à l’écran

Dans l’ordre, vérifiez :

  1. La casse des noms de balise. Sur un document HTML, //DIV ne trouve rien alors que //div fonctionne. Écrivez toujours les balises en minuscules.
  2. Le / au lieu du //. C’est l’expression 3 de cet atelier. Remplacez temporairement le / par // : si le compte devient non nul, il y avait un niveau intermédiaire.
  3. text() au lieu de .. C’est l’expression 6. Si le texte est dans un enfant, text() ne le voit pas.
  4. Les espaces dans le texte. Une cellule écrite <td> Ouvert </td> dans le HTML source ne satisfait pas text()='Ouvert'. Utilisez normalize-space(text())='Ouvert' : cette fonction supprime les espaces de début et de fin, et réduit à un seul espace toute suite d’espaces à l’intérieur.
  5. L’élément est dans une iframe. XPath ne traverse pas les frontières d’iframe. Dans la console, changez le contexte avec le sélecteur en haut du panneau ; dans Selenium, il faudra un driver.switchTo().frame(...).
  6. L’élément a été ajouté après le chargement. Sur une page dynamique, relancez l’expression après avoir déclenché l’action qui fait apparaître l’élément.

Le compte est bon mais l’élément surligné n’est pas le bon

Dépliez le tableau et survolez chaque entrée : le navigateur met en surbrillance l’élément correspondant dans la page. C’est la vérification qui coûte deux secondes et évite un test qui passe pour de mauvaises raisons.

La page affiche des caractères étranges

Votre éditeur a enregistré le fichier dans un encodage autre qu’UTF-8, alors que la page déclare <meta charset="utf-8">. Dans VS Code, l’encodage du fichier ouvert est affiché en bas à droite de la fenêtre. Cliquez dessus : « Reopen with Encoding » vous permet de relire le fichier dans son encodage réel pour retrouver le texte correct, et « Save with Encoding », puis « UTF-8 », le réenregistre proprement.

Ce que XPath sait faire et que Selenium refuse

Vous allez rencontrer, dans des articles ou des réponses de forum, des expressions XPath qui ne fonctionneront pas chez vous. Voici pourquoi, et où est la limite exacte.

Les navigateurs n’implémentent que XPath 1.0

Selenium ne possède pas son propre moteur XPath : il transmet votre expression au navigateur, qui l’évalue avec document.evaluate. Or les navigateurs implémentent XPath 1.0. Les fonctions apparues en XPath 2.0 et au-delà sont hors d’atteinte.

Essayez dans la console :

$x("//td[ends-with(text(), '1043')]")

Le navigateur rejette l’expression et signale qu’elle n’est pas une expression XPath valide — le libellé exact varie selon le navigateur. Ce n’est pas une faute de frappe : ends-with() n’existe pas dans le moteur.

Sont dans le même cas : matches(), replace(), tokenize(), lower-case() et upper-case(), ainsi que les expressions if/then/else et les boucles for.

Deux contournements que vous utiliserez régulièrement :

Simuler ends-with() avec substring() et string-length() :

$x("//td[substring(text(), string-length(text()) - 3) = '1043']").length

Résultat : 1. On extrait les quatre derniers caractères et on les compare. Le - 3 et non - 4 vient du fait que les positions XPath commencent à 1.

Simuler une comparaison insensible à la casse avec translate() :

$x("//td[translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') = 'clos']").length

Résultat : 1. translate() remplace caractère par caractère ; c’est laborieux, mais c’est ce qu’on a.

Selenium n’accepte que des éléments

La console est plus permissive que Selenium. Essayez :

$x("//input/@name")

Vous obtenez trois nœuds d’attribut : titre, auteur, archives. C’est du XPath parfaitement valide.

Passez la même expression à By.xpath(...) dans un test Java et Selenium lève une InvalidSelectorException, avec un message expliquant que le résultat de l’expression n’est pas un élément. Même chose pour count(//tr), qui renvoie un nombre, ou pour //td/text(), qui renvoie un nœud texte.

La règle. Un localisateur Selenium doit désigner des éléments, et rien d’autre. Pour obtenir un attribut, sélectionnez l’élément puis appelez getAttribute("name"). Pour obtenir du texte, sélectionnez l’élément puis appelez getText(). Pour compter, utilisez findElements(...).size().

Deux frontières que XPath ne franchit pas

Les iframes. Le document d’une iframe est un document séparé. Aucune expression XPath ne peut partir de la page parente et atteindre un élément dedans.

Le Shadow DOM. Les composants web encapsulent leur contenu dans un arbre séparé que document.evaluate ne traverse pas. Si $x() ne trouve rien alors que l’élément est bien visible dans l’inspecteur, cherchez la mention #shadow-root au-dessus de lui dans l’arbre : votre XPath ne l’atteindra jamais.

Ces deux cas ont leurs solutions côté Selenium, mais elles ne relèvent pas de XPath.

Le passage au Java, pour plus tard

Vous n’avez pas écrit de Java dans ce chapitre, et c’était volontaire. Voici tout de même à quoi ressemble le passage de relais, pour que vous voyiez que le travail fait ici se transporte tel quel. Ne l’exécutez pas maintenant : gardez-le pour le chapitre où l’on installe le projet Maven.

import java.nio.file.Paths;
import java.time.Duration;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;

public class LectureXPath {

    public static void main(String[] args) {
        ChromeOptions options = new ChromeOptions();
        WebDriver driver = new ChromeDriver(options);
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
        try {
            String url = Paths.get("atelier-xpath.html").toAbsolutePath().toUri().toString();
            driver.get(url);

            WebElement statut = driver.findElement(
                    By.xpath("//td[text()='TCK-1042']/following-sibling::td[2]"));
            System.out.println(statut.getText());
        } finally {
            driver.quit();
        }
    }
}

Trois remarques sur ce code. Il n’y a aucun System.setProperty et aucun chromedriver à télécharger : Selenium Manager, intégré à Selenium 4, résout le pilote tout seul. Paths.get("atelier-xpath.html") cherche le fichier dans le répertoire de travail du programme, donc à la racine du projet Maven — copiez-y atelier-xpath.html le moment venu, sinon Chrome affichera une page d’erreur et l’élément restera introuvable. Enfin, la chaîne passée à By.xpath(...) est, caractère pour caractère, l’expression 11 que vous avez validée : rien n’a été retouché, rien n’a été deviné. Le programme affiche Ouvert.

Ce que ce chapitre ne couvre pas

Pour que vous sachiez où vous en êtes, et ce qu’il reste :

  • Les sélecteurs CSS. Plus courts que XPath sur les cas simples, mais sans axes ni fonctions de texte. Ils se valident dans la console avec $$("..."), exactement selon la même méthode. Ils font l’objet d’un autre chapitre.
  • Les autres localisateurs Selenium : By.id, By.name, By.className, By.linkText, et les Relative Locators de Selenium 4.
  • Les attentes. Un XPath juste ne suffit pas si l’élément n’est pas encore là. WebDriverWait, Duration.ofSeconds(...) et les ExpectedConditions sont traités séparément.
  • Les axes restants. Nous en avons vu deux sur treize. ancestor, preceding, following, descendant, self et les autres obéissent à la même grammaire ; la fiche XPath du parcours les liste.
  • Les fonctions XPath 1.0 numériques et booléennes (sum(), floor(), round(), not(), boolean()), plus rarement utiles en test d’interface.
  • Le passage des iframes et du Shadow DOM côté Selenium.
  • Les extensions de navigateur d’aide au XPath. Elles sont pratiques, mais elles vous dispensent de comprendre ce que vous écrivez, et vous ne les aurez ni sur la machine du client, ni dans le navigateur vierge que Selenium démarre. La console, si.
  • Les performances comparées de XPath et des sélecteurs CSS. Le sujet existe, il fait couler beaucoup d’encre, et il ne devrait pas guider vos choix tant que vos localisateurs ne sont pas corrects.

Ce que vous emportez

Trois choses, et elles tiennent en trois phrases.

Un XPath se valide dans la console avant d’exister en Java, et le critère est .length, pas l’impression que « ça a l’air bon ».

Un chemin absolu est une accumulation d’hypothèses sur une structure qui va changer ; un chemin relatif ancré sur un attribut stable en fait beaucoup moins.

// n’est pas un / plus tolérant, [1] n’est pas (…)[1], et text() n’est pas . — ce sont les trois confusions à vérifier en premier quand un localisateur « ne marche pas ».

Gardez le fichier atelier-xpath.html. Il vous servira de bac à sable chaque fois que vous voudrez vérifier une idée sans lancer de navigateur piloté.

STRATLEARN est l’activité de formation de STRATEST SARL. Mentions légales