Playwright – locator()
L’utilisation de locator() — l’API moderne et recommandée, car elle gère les attentes automatiques (auto-wait) et évite les null-checks manuels
Exemple:
import { test, expect } from '@playwright/test';
test('vérifie le login et la navigation', async ({ page }) => {
// Aller sur la page de login
await page.goto('https://example.com/login');
// Les locators gèrent automatiquement l’attente de l’élément
const usernameInput = page.locator('#username');
const passwordInput = page.locator('#password');
const loginButton = page.locator('button[type="submit"]');
// Remplir le formulaire
await usernameInput.fill('admin');
await passwordInput.fill('secret');
await loginButton.click();
// Attendre que la redirection ait lieu et vérifier un élément du tableau de bord
const dashboardTitle = page.locator('h1');
await expect(dashboardTitle).toHaveText('Tableau de bord');
// Exemple de test d’un élément interactif
const menuButton = page.locator('[data-test=menu]');
await menuButton.click();
const logoutLink = page.locator('text=Se déconnecter');
await expect(logoutLink).toBeVisible();
});
🧩 Points clés
page.locator()→ crée un Locator stable, qui gère automatiquement :- L’attente que l’élément soit dans le DOM.
- L’attente qu’il soit visible ou activable (selon l’action :
click,fill, etc.). - L’absence d’erreurs
null/undefined.
- Pas besoin de
waitForSelector()→ leLocatorle fait pour vous. - Compatible avec
expect()→ permet des assertions propres et synchronisées.
Remarques:
A – Pour une sélection plus fine avec locator(), voici quelques exemples de fonctions :
locator().nth()- page.
getByRole() locator.filter()
B – page.locator('selector') ne renvoie pas un ElementHandle | null
Au contraire, il renvoie toujours un objet Locator, jamais null.
Le Locator est une abstraction paresseuse (lazy) :
il ne recherche pas immédiatement l’élément dans le DOM.
👉 En clair :
const elt = page.locator('button#login');
console.log(elt); // Locator {...}, jamais nullCe n’est qu’au moment où on interagist avec le Locator (click(), fill(), textContent(), etc.)
que Playwright :
- Cherche l’élément dans le DOM,
- Attend automatiquement qu’il existe, soit visible ou stable,
- Et échoue si ce n’est pas le cas.
💥 Que se passe-t-il si l’élément n’existe jamais ?
Le test échouera automatiquement lors d’une action ou d’une assertion :
await page.locator('#doesNotExist').click();
// ❌ TimeoutError: locator.click: Timeout 30s exceeded.
// De plus, on n’a pas besoin de vérifier if (elt) ou de faire waitForSelector() manuellement.
// Même si le bouton met 2 secondes à apparaître, Playwright gère ça tout seul.
const submitButton = page.locator('button[type=submit]');
await submitButton.click(); // Attend automatiquement que le bouton soit visible & cliquable🔍 Autres méthodes renvoyant un locator
Les méthodes comme getByTestId(), getByRole(), getByText(), etc.
→ ne font qu’enrichir locator() avec une syntaxe plus sémantique.
Par exemple :
page.getByTestId('submit')
// équivaut à :
page.locator('[data-testid="submit"]')
// Et les deux renvoient un Locator → donc même comportement d’attente automatique.🎯 Objectif : des sélecteurs stables, expressifs et maintenables
Playwright fournit plusieurs stratégies pour cibler les éléments :
* Bonnes pratiques entre getByRole, getByTestId(), getByText(), …
- Sémantiques →
getByRole(),getByText(),getByLabel(), etc. - Techniques →
getByTestId(),locator('css=...'), etc.
Chaque approche a ses cas d’usage 👇
🥇 1. getByRole() → la méthode recommandée par défaut
Utilise la sémantique du DOM / ARIA.
Playwright comprend automatiquement le rôle d’un élément (ex. button, link, textbox, checkbox, etc.).
✅ Avantages :
- Très robuste aux changements de structure HTML.
- Reflète ce que voit un lecteur d’écran (tests plus proches de l’expérience utilisateur).
- Pas besoin d’ajouter d’attributs spéciaux.
🚫 Limites :
Il faut que ton application ait une sémantique HTML correcte (role, aria-label, alt, texte visible, etc.).
Exemple :
await page.getByRole('button', { name: 'Se connecter' }).click();🧩 2. getByTestId() → pour les cas où la sémantique ne suffit pas
Quand on ne peuxtpas cibler un élément de manière fiable par rôle, texte ou label :
- icônes sans texte,
- menus dynamiques,
- éléments techniques sans rôle défini, etc.
✅ Avantages :
- Stable même si le texte visible change (utile pour les applis traduites).
- Lisible et explicite côté code.
⚠️ À n’utiliser que si getByRole() ou getByText() ne conviennent pas.
Trop de data-testid = code plus verbeux et moins sémantique.
Exemple:
await page.getByTestId('delete-button').click();⚖️ 3. Recommandation de combinaison
| Situation | Méthode recommandée |
|---|---|
| Bouton visible, texte fixe | getByRole('button', { name: 'Envoyer' }) |
| Champ de formulaire | getByLabel('Email') |
| Icône sans texte (ex. 🗑️) | getByTestId('delete-icon') |
| Élément dynamique ou liste | locator() + .nth() ou .filter() |
| Vérifier un texte dans le DOM | getByText('Bienvenue') |
💡 Exemple complet “mixte” :
test('création d’un compte utilisateur', async ({ page }) => {
await page.goto('https://app.example.com/signup');
await page.getByLabel('Nom d’utilisateur').fill('toto');
await page.getByLabel('Mot de passe').fill('supersecret');
await page.getByRole('button', { name: 'Créer un compte' }).click();
await expect(page.getByText('Bienvenue, toto')).toBeVisible();
// Utilisation ciblée de getByTestId pour un élément technique
await page.getByTestId('user-avatar').click();
await page.getByRole('menuitem', { name: 'Se déconnecter' }).click();
});👌 Un exemple complet et réaliste, comme sur une appli avec un tableau de données + actions par ligne (éditer / supprimer, etc.) et en combinant getByRole, getByTestId, locator.filter(), nth()
import { test, expect } from '@playwright/test';
test('gestion des utilisateurs : édition et suppression', async ({ page }) => {
await page.goto('https://app.example.com/admin/users');
// Vérifie que le tableau d’utilisateurs est visible
const table = page.getByRole('table', { name: 'Liste des utilisateurs' });
await expect(table).toBeVisible();
// 🔍 Sélectionner une ligne spécifique du tableau (ex: utilisateur "Jean Dupont")
const row = table.getByRole('row').filter({ hasText: 'Jean Dupont' });
// Vérifie que la ligne existe bien
await expect(row).toBeVisible();
// 🧭 Exemple 1 : Cliquer sur le bouton "Éditer" dans la ligne
const editButton = row.getByRole('button', { name: 'Éditer' });
await editButton.click();
// Vérifie qu’un modal d’édition apparaît
const modal = page.getByRole('dialog', { name: 'Modifier utilisateur' });
await expect(modal).toBeVisible();
// Remplir un champ dans le modal (ex: email)
await modal.getByLabel('Email').fill('jean.dupont@example.com');
await modal.getByRole('button', { name: 'Enregistrer' }).click();
// Vérifie que le modal disparaît
await expect(modal).toBeHidden();
// 🧭 Exemple 2 : Supprimer le 2e utilisateur du tableau (via nth)
const secondRow = table.getByRole('row').nth(2);
const deleteButton = secondRow.getByTestId('delete-user');
await deleteButton.click();
// Vérifie la modale de confirmation
const confirmDialog = page.getByRole('dialog', { name: 'Confirmer la suppression' });
await expect(confirmDialog).toBeVisible();
await confirmDialog.getByRole('button', { name: 'Confirmer' }).click();
// ✅ Vérifie que la ligne correspondante a bien disparu
await expect(row).toHaveCount(0);
});
🧩 Points clés du code
| Technique | Utilisation | Avantage |
|---|---|---|
getByRole() | Pour cibler les éléments sémantiques (table, row, button, dialog) | Stable, lisible, conforme ARIA |
getByTestId() | Pour cibler des actions “techniques” (ex. bouton supprimer sans texte) | Robuste même si le texte change |
locator.filter() | Pour cibler une ligne contenant un texte précis | Pratique dans les tableaux dynamiques |
nth() | Pour sélectionner un élément par position (ex. 2e ligne) | Simple et rapide pour les cas non textuels |
expect(locator) | Assertions explicites avec attente automatique | Pas besoin de waitForSelector |
🧠 Variante avancée : combinaison de filtres
Combiner plusieurs filtres pour être ultra précis:
const userRow = table.getByRole('row').filter({
has: page.getByRole('cell', { name: 'Jean Dupont' }),
hasNot: page.getByText('Admin désactivé'),
});
await userRow.getByTestId('edit-user').click();
// 💡 Ici, has et hasNot permettent de filtrer des éléments imbriqués — très utile dans les tableaux complexes.✅ En résumé
- Utilise
getByRole()partout où le HTML est sémantique (table, bouton, dialog…). - Utilise
getByTestId()seulement quand il n’y a aucun texte / rôle fiable. - Combine avec
filter()etnth()pour naviguer dans les structures répétitives. - Et surtout : reste toujours sur des
Locator(pasElementHandle) pour bénéficier du auto-waiting.