Un locator, c'est la façon dont vous indiquez à Playwright quel élément de la page utiliser. Mal choisi, vos tests cassent à chaque fois qu'un développeur change une classe CSS. Bien choisi, ils survivent aux refactorings, aux redesigns et aux migrations de framework. La différence tient au bon type de locator.
Pourquoi les locators comptent
La cause la plus courante des tests flaky n'est pas le timing. Ce sont les locators fragiles. Un test qui trouve un bouton par sa classe CSS (button.btn-primary-v2) casse dès qu'un développeur renomme cette classe. Un test qui trouve un bouton par son rôle et son label (getByRole('button', { name: 'Valider' })) survit à tout changement CSS. Il cherche le bouton comme un utilisateur : par ce qu'il dit et ce qu'il fait.
Playwright propose six types de locators, du plus au moins recommandé.
getByRole : commencez par là
getByRole trouve les éléments par leur rôle ARIA et leur nom accessible. C'est le locator que Playwright recommande par défaut, pour une bonne raison : c'est ainsi que les utilisateurs et les lecteurs d'écran identifient les éléments. Si le nom accessible change, le test doit casser, parce que c'est un vrai changement UX.
// Boutons
await page.getByRole('button', { name: 'Valider' }).click();
await page.getByRole('button', { name: 'Se connecter' }).click();
await page.getByRole('button', { name: 'Supprimer l\'élément' }).click();
// Liens
await page.getByRole('link', { name: 'Accueil' }).click();
await page.getByRole('link', { name: 'Voir les détails' }).click();
// Titres
await expect(page.getByRole('heading', { name: 'My Travel Items' })).toBeVisible();
await expect(page.getByRole('heading', { level: 1 })).toContainText('Dashboard');
// Éléments de formulaire
await page.getByRole('textbox', { name: 'Rechercher' }).fill('Tokyo');
await page.getByRole('checkbox', { name: 'Se souvenir de moi' }).check();
await page.getByRole('combobox', { name: 'Statut' }).selectOption('active');
// Tableaux
const rows = page.getByRole('row');
await expect(rows).toHaveCount(6); // 1 en-tête + 5 lignes de donnéesRôles ARIA courants : button, link, heading, textbox, checkbox, radio, combobox (dropdown), listitem, row, cell, dialog, table, navigation, main.
L'option name correspond au nom accessible de l'élément. Pour les boutons et liens, c'est le texte visible. Pour les inputs, c'est le label associé. Insensible à la casse par défaut.
// exact: false (par défaut) — correspondance partielle
page.getByRole('button', { name: 'val' }) // correspond à "Valider", "Valeur"
// exact: true — correspondance complète uniquement
page.getByRole('button', { name: 'Valider', exact: true }) // seulement "Valider"getByLabel : pour les champs de formulaire
getByLabel trouve un input, select ou textarea par son élément associé. C'est le bon locator pour les formulaires de connexion, les barres de recherche et tout champ qui a un label visible.
await page.getByLabel('Nom d\'utilisateur').fill('admin@becomeqa.com');
await page.getByLabel('Mot de passe').fill('testpass123');
await page.getByLabel('Adresse e-mail').fill('user@example.com');
await page.getByLabel('Date de naissance').fill('1990-01-15');Il fonctionne que le label utilise for/id, aria-label ou enveloppe l'input. Vous n'avez pas besoin de savoir comment le label est implémenté. Playwright s'en charge.
<!-- Ces trois variantes sont toutes trouvées par getByLabel('Email') -->
<label for="email">Email</label><input id="email" />
<label><span>Email</span><input /></label>
<input aria-label="Email" />getByPlaceholder : quand il n'y a pas de label
Certains inputs ont du texte de placeholder plutôt qu'un label visible. getByPlaceholder gère ce cas.
await page.getByPlaceholder('Rechercher des destinations...').fill('Tokyo');
await page.getByPlaceholder('Entrez votre email').fill('test@example.com');Préférez getByLabel quand un label existe. getByPlaceholder est pour les inputs qui n'ont que du texte de placeholder.
getByText : pour les éléments non interactifs
getByText trouve les éléments par leur contenu textuel visible. Utilisez-le pour vérifier que du texte est présent sur la page, pas pour les éléments que vous voulez cliquer (utilisez getByRole pour ça).
// Vérifier que le texte est présent
await expect(page.getByText('My Travel Items')).toBeVisible();
await expect(page.getByText('Identifiants invalides')).toBeVisible();
// Correspondance exacte vs partielle
page.getByText('Travel') // correspond à "My Travel Items", "Travel guide"
page.getByText('Travel', { exact: true }) // seulement "Travel" exact
// Limité à un type d'élément spécifique
page.locator('p').getByText('Erreur survenue') // seulement les éléments <p>getByText trouve tous les éléments contenant ce texte, y compris les conteneurs parents. Si "Valider" apparaît dans un paragraphe et un bouton, getByText('Valider') retourne plusieurs éléments. Pour les éléments interactifs, utilisez getByRole à la place.getByTestId : le contrat explicite
getByTestId trouve les éléments par leur attribut data-testid (configurable). C'est le locator à utiliser quand les développeurs ajoutent explicitement des crochets de test dans le DOM.
<button data-testid="submit-payment">Payer maintenant</button>
<div data-testid="success-message">Paiement effectué</div>await page.getByTestId('submit-payment').click();
await expect(page.getByTestId('success-message')).toBeVisible();L'avantage : les attributs data-testid sont invisibles pour les utilisateurs et n'ont pas de signification fonctionnelle, donc les développeurs ne les renommeront pas accidentellement. En revanche, quelqu'un doit les ajouter au code. Pour les applications que vous contrôlez, c'est parfait.
data-testid, proposez-le. Demandez aux développeurs d'ajouter des attributs data-testid aux éléments interactifs clés. Cela prend quelques minutes par composant et rend les locators trivialement stables.getByAltText et getByTitle
Deux locators moins courants mais occasionnellement utiles :
// Images avec texte alt
await page.getByAltText('Photo de profil utilisateur').click();
await expect(page.getByAltText('Logo de l\'entreprise')).toBeVisible();
// Éléments avec attributs title
await page.getByTitle('Fermer la boîte de dialogue').click();Vous les utiliserez rarement. La plupart des éléments interactifs sont accessibles via getByRole.
Enchaîner les locators
Quand vous devez affiner la recherche à un élément spécifique dans un conteneur, enchaînez les locators :
// Trouver une ligne contenant "Tokyo", puis cliquer sur son bouton Supprimer
const tokyoRow = page.getByRole('row').filter({ hasText: 'Tokyo' });
await tokyoRow.getByRole('button', { name: 'Supprimer' }).click();
// Trouver une section de formulaire, puis interagir avec son input
const addressSection = page.locator('.address-section');
await addressSection.getByLabel('Ville').fill('Paris');filter({ hasText: '...' }) restreint un locator aux éléments contenant du texte spécifique. Combiné avec nth() pour la sélection par index :
// Première ligne du tableau (index 0)
const firstRow = page.getByRole('row').nth(1); // nth(0) est l'en-tête, nth(1) est la première ligne de données
await firstRow.getByRole('button', { name: 'Modifier' }).click();Ce qu'il faut éviter
Sélecteurs CSS. Fragiles, spécifiques à l'implémentation, cassent lors des refactorings :// Mauvais
page.locator('.btn.btn-primary')
page.locator('#submit-button')
page.locator('div > ul > li:nth-child(3)')// Mauvais
page.locator('//button[@class="btn btn-primary" and text()="Valider"]')// Risqué — et si "Modifier" apparaît à plusieurs endroits ?
page.getByText('Modifier')
// Mieux — limité à la ligne qui vous intéresse
page.getByRole('row').filter({ hasText: 'Tokyo' }).getByRole('button', { name: 'Modifier' })Exercice pratique sur lab.becomeqa.com
Ouvrez https://lab.becomeqa.com et essayez d'écrire des locators pour :
1. Le bouton Login dans la navigation
2. Les inputs Nom d'utilisateur et Mot de passe dans la modale de connexion
3. Le bouton Valider dans la modale de connexion
4. La ligne du tableau contenant une destination spécifique après connexion
5. Le bouton Ajouter un élément sur le tableau de bord
import { test, expect } from '@playwright/test';
test('pratique des locators', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
// 1. Bouton de connexion dans la navigation
await page.getByRole('button', { name: 'Login' }).click();
// 2 & 3. Formulaire de connexion
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
// 4. Ligne spécifique dans le tableau
const parisRow = page.getByRole('row').filter({ hasText: 'Paris' });
await expect(parisRow).toBeVisible();
// 5. Bouton d'ajout d'élément
await expect(page.getByRole('button', { name: 'Add Item' })).toBeVisible();
});FAQ
Quand utiliserlocator() directement plutôt que les méthodes getBy* ?
Quand vous avez besoin de sélecteurs CSS ou d'attributs que les méthodes getBy* ne couvrent pas. Par exemple, page.locator('[data-status="active"]') trouve tous les éléments avec une valeur d'attribut de données spécifique. Utilisez-le en dernier recours, pas en premier choix.
Oui. locator.and(otherLocator) trouve les éléments correspondant aux deux :
// Un bouton qui est à la fois visible et a le texte "Valider"
page.getByRole('button').and(page.getByText('Valider'))Playwright lève une strict mode violation si un locator correspond à plus d'un élément lors d'une interaction. Corrigez en rendant le locator plus spécifique : ajoutez un filter, utilisez nth(), ou limitez-le à un conteneur parent.
Utilisez le mode highlight dans Playwright Inspector : PWDEBUG=1 npx playwright test. Ou appelez await locator.highlight() dans votre test pour marquer visuellement l'élément trouvé lors d'une exécution en mode visible.