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() → le Locator le 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 null

Ce 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émantiquesgetByRole(), getByText(), getByLabel(), etc.
  • TechniquesgetByTestId(), 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

SituationMéthode recommandée
Bouton visible, texte fixegetByRole('button', { name: 'Envoyer' })
Champ de formulairegetByLabel('Email')
Icône sans texte (ex. 🗑️)getByTestId('delete-icon')
Élément dynamique ou listelocator() + .nth() ou .filter()
Vérifier un texte dans le DOMgetByText('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

TechniqueUtilisationAvantage
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écisPratique 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 automatiquePas 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() et nth() pour naviguer dans les structures répétitives.
  • Et surtout : reste toujours sur des Locator (pas ElementHandle) pour bénéficier du auto-waiting.