Playwright attend automatiquement que les éléments soient visibles, stables, activés et réceptifs aux événements pointeur avant d'agir dessus. Cela élimine la plupart des instabilités liées au timing sans aucun code d'attente manuel.
Comment fonctionne l'auto-attente de Playwright
Quand vous écrivez await page.getByRole('button', { name: 'Submit' }).click(), Playwright ne clique pas immédiatement. Il attend que le bouton soit :
- Attaché au DOM
- Visible (pas caché, pas
display: none) - Stable (pas en animation)
- Activé (pas désactivé)
- Réceptif aux événements pointeur (pas couvert par un autre élément)
Tout ça se fait automatiquement, dans la limite de l'actionTimeout (par défaut : 30 secondes). Pas de code d'attente à écrire. Playwright s'en charge.
C'est pourquoi Playwright est plus rapide à écrire que Selenium. Avec Selenium, on écrirait WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ROLE, 'button'))). Avec Playwright : juste un clic.
Quand l'auto-attente ne suffit pas
L'auto-attente gère les interactions avec les éléments. Elle ne gère pas tout.
Navigation de page : après un submit de formulaire ou un clic sur un lien, l'URL change. L'auto-attente n'attend pas que la nouvelle page soit complètement chargée avant l'action suivante. Chargement de données : un élément existe et est visible, mais il affiche un spinner pendant le chargement des données. Playwright peut cliquer dessus avant l'arrivée des données. Requêtes réseau multiples : un chargement de page déclenche trois appels API. Playwright voit que le DOM est prêt, mais le troisième appel n'est pas encore terminé. Fin d'animation : un élément est techniquement visible mais en cours d'animation. L'auto-attente gère les transitions CSS simples mais pas tous les états d'animation.expect comme outil d'attente
Le pattern d'attente le plus sous-utilisé dans Playwright : les assertions attendent.
// Attend jusqu'au timeout que l'URL corresponde
await expect(page).toHaveURL('/dashboard');
// Attend que le texte apparaisse
await expect(page.getByRole('heading')).toHaveText('Commande confirmée');
// Attend que le nombre d'éléments atteigne 5
await expect(page.getByRole('listitem')).toHaveCount(5);Chaque assertion expect dans Playwright interroge l'état jusqu'à ce qu'elle passe ou que le timeout expire. Les assertions sont donc la façon la plus propre d'attendre un état applicatif.
await page.waitForTimeout(2000); // ne jamais faire ça
await expect(page.getByRole('heading')).toHaveText('Commande confirmée');await expect(page.getByRole('heading')).toHaveText('Commande confirmée');L'expect fait l'attente. Le waitForTimeout est un signal d'alarme : ça veut dire qu'on ne sait pas quoi attendre.
Outils d'attente explicite
Quand une attente explicite est vraiment nécessaire, Playwright fournit des options ciblées.
waitForURL
await page.getByRole('button', { name: 'Se connecter' }).click();
await page.waitForURL('/dashboard');
// Maintenant on peut asserter en toute sécurité sur le contenu du tableau de bordwaitForResponse : attendre un appel API spécifique
const [response] = await Promise.all([
page.waitForResponse(resp => resp.url().includes('/api/orders') && resp.status() === 200),
page.getByRole('button', { name: 'Passer la commande' }).click(),
]);
const orderData = await response.json();
expect(orderData.status).toBe('created');Démarrez l'attente avant l'action qui déclenche la requête. Promise.all garantit qu'on ne rate pas une réponse rapide.
waitForRequest : vérifier qu'une requête a été faite
const [request] = await Promise.all([
page.waitForRequest(req => req.url().includes('/api/track') && req.method() === 'POST'),
page.getByRole('button', { name: 'Acheter' }).click(),
]);
// Vérifier que l'événement analytics a été déclenché
expect(request.postDataJSON()).toMatchObject({ event: 'purchase' });waitForSelector : attendre un état d'élément
// Attendre que le spinner de chargement disparaisse
await page.waitForSelector('.spinner', { state: 'detached' });
// Attendre qu'un élément devienne visible
await page.waitForSelector('[data-testid="results-table"]', { state: 'visible' });Options de state : 'attached', 'detached', 'visible', 'hidden'.
Préférez expect(locator).toBeVisible() à waitForSelector ; l'approche par assertion est plus lisible.
waitForLoadState
await page.goto('/heavy-page');
await page.waitForLoadState('networkidle'); // Attendre jusqu'à l'absence d'activité réseau pendant 500msTrois valeurs de loadState : 'load' (événement window.load, défaut de goto), 'domcontentloaded' (DOM parsé, avant les images et scripts), 'networkidle' (aucune requête réseau pendant 500ms).
'networkidle' est lent et fragile. À éviter sauf si la page n'a vraiment pas d'autre façon de signaler qu'elle est prête. Préférez attendre un élément spécifique.
Configuration des timeouts
Les timeouts sont configurables à trois niveaux :
// playwright.config.ts — s'applique à tous les tests
export default defineConfig({
timeout: 30000, // Timeout du test (test entier)
expect: {
timeout: 5000, // Timeout des assertions
},
use: {
actionTimeout: 15000, // Timeout des actions individuelles (click, fill, etc.)
navigationTimeout: 30000,
},
});Surcharge par test :
test('chargement de données lent', async ({ page }) => {
test.setTimeout(60000); // Ce test a 60 secondes
// ...
});Surcharge par assertion :
await expect(page.getByText('Rapport généré')).toBeVisible({ timeout: 30000 });La règle sur waitForTimeout
page.waitForTimeout(ms) est un sleep. C'est parfois nécessaire en dernier recours (par exemple, attendre un script tiers qu'on ne peut pas observer). Mais traitez chaque occurrence comme un TODO : qu'est-ce qu'on devrait vraiment attendre ici ?
Si vous vous retrouvez à écrire await page.waitForTimeout(1000) dans plus d'un ou deux tests, votre suite a un problème structurel d'attente qui mérite d'être corrigé.