Geonode logo
Geonode Team

Geonode Team

Mis à jour : 7 octobre 2026

Publié : 2 septembre 2026

Temps morts dans Playwright : comment ça marche et comment y remédier

« Dépassement du délai d'attente de 30 000 ms » est l'erreur la plus courante de Playwright et celle qui fait le plus souvent l'objet d'un diagnostic erroné. Cela signifie rarement que votre test est lent. Cela signifie généralement qu'un élément n'est jamais devenu interactif, et que le délai d'attente correspond simplement au moment où Playwright a cessé d'attendre une condition qui n'allait jamais être remplie. Ce guide passe en revue les six types de délais d’attente distincts, vous aide à identifier celui auquel vous êtes confronté et explique pourquoi augmenter ce délai n’est généralement pas la bonne solution.

Notre point de vue, en toute simplicité : nous sommes Geonode et nous vendons des proxys, et Playwright est souvent exécuté via ces derniers à des fins de scraping et de tests géographiques. Il faut toutefois préciser que la grande majorité des délais d’expiration de Playwright n’ont rien à voir avec les proxys. Un sélecteur qui ne correspond à rien, un élément masqué par une bannière de cookies, une animation qui ne s’affiche jamais… Tout cela génère la même erreur, que votre trafic passe directement ou par six intermédiaires. Il existe toutefois un cas réel lié aux proxys, abordé vers la fin : les connexions résidentielles ajoutent une latence réelle, ce qui fait que les paramètres par défaut optimisés pour les tests locaux génèrent de faux échecs. Mais vérifiez d’abord le sélecteur. Si votre test échoue de la même manière sans proxy configuré, le proxy n’est pas en cause.

Les six délais d'expiration et leurs valeurs par défaut

Il faut tout d'abord comprendre qu'il s'agit de mécanismes distincts dotés de valeurs par défaut différentes ; savoir lequel s'est déclenché vous indique où chercher.

Délai d'expirationValeur par défautDéfinition via
Test30 000 mstestConfig.timeout, test.setTimeout()
Expect5 000 mstestConfig.expect.timeout, option par assertion
ActionPas de délai d'expirationtestOptions.actionTimeout, option par appel
NavigationPas de délai d'expirationtestOptions.navigationTimeout, option par appel
Hooks beforeAll / afterAll30 000 mstest.setTimeout() à l’intérieur du hook
GlobalAucuntestConfig.globalTimeout

Valeurs issues de la documentation sur les délais d’expiration de Playwright.

Deux de ces valeurs surprennent souvent les utilisateurs.

Les actions et la navigation n’ont pas de délai d’expiration par défaut. Elles ne sont limitées que par le délai d’expiration du test. Ainsi, une page.click() non qualifiée attendra jusqu’à épuisement du budget de test restant, et l’erreur que vous obtenez correspond à l’expiration du test plutôt qu’à celle du clic. C’est pourquoi le message indique 30 000 ms même si personne n’a configuré un clic de 30 secondes.

Le délai d’expiration global n’a aucune valeur par défaut. La documentation décrit son objectif comme étant d’empêcher « une utilisation excessive des ressources lorsque tout va de travers » — il est utile de le définir dans l’environnement CI afin qu’une suite bloquée échoue plutôt que d’occuper indéfiniment un exécuteur.

Que signifie réellement le message « Timeout of 30000ms exceeded » ?

Ce message correspond au délai d’expiration du test, et ce délai est un « budget » plutôt qu’un diagnostic. Un élément du processus a pris trop de temps, et le message indique le délai imparti, pas la cause du problème.

Classement par fréquence d’apparition de la cause réelle :

1. Un sélecteur n’a trouvé aucune correspondance. Le sélecteur est incorrect, ou l’élément n’est pas apparu, ou il se trouve à l’intérieur d’une iframe ou d’une « shadow root » que vous n’avez pas prise en compte. Playwright attend patiemment quelque chose qui n’existera jamais.

2. L’élément existe mais n’est pas interactif. Masqué par une superposition, une bannière de cookies ou un en-tête fixe. Désactivé. Toujours en animation. Playwright attend qu’il devienne cliquable, ce qui n’arrivera jamais.

3. Une navigation ne s’est jamais achevée. Une requête réseau bloquée, une boucle de redirection ou une condition d’waitUntil — notamment « networkidle » — qu’une page utilisant des connexions persistantes ne remplira jamais.

4. Une assertion n’est jamais devenue vraie. Une expect qui interroge une condition que l’application n’atteint pas.

5. Le test en fait véritablement trop. Réel, et le moins courant.

L’ordre a son importance car la solution diffère complètement. Seul le cas n° 5 se résout en augmentant le délai d’expiration. Dans les quatre autres, l’augmenter revient à attendre plus longtemps pour rencontrer la même défaillance.

L'« actionnabilité » : la raison pour laquelle votre clic est en attente

Comprendre ce principe dissipe en grande partie la confusion, car cela explique ce que fait Playwright pendant ces trente secondes.

La documentation sur l'actionnabilité indique que Playwright « effectue une série de vérifications d'actionnabilité sur les éléments avant d'effectuer des actions, afin de s'assurer que celles-ci se comportent comme prévu », et qu'il « attend automatiquement que toutes les vérifications pertinentes soient réussies avant d'effectuer l'action demandée ». Lorsque les vérifications ne sont pas satisfaites à temps, « l’action échoue avec l’TimeoutError ».

Les vérifications requises varient selon l’action, et cette différence est déterminante pour le diagnostic :

ActionVérifications requises
click, dblclick, check, uncheck, tap, setCheckedvisible, stable, reçoit des événements, activé
hover, dragTovisible, stable, reçoit des événements
fill, clearvisible, activé
selectOptionvisible, activé
screenshot, selectTextvisible
scrollIntoViewIfNeededstable
blur, focus, press, pressSequentially, dispatchEvent, setInputFilesaucune

Deux éléments ressortent immédiatement de ce tableau.

Le fait qu’une commande « click » expire alors que la commande « fill » fonctionne sur le même élément indique que celui-ci est « stable » ou « reçoit des événements » : l’élément est en mouvement, ou quelque chose se trouve par-dessus. Les animations et les superpositions sont les coupables habituels.

Les actions sans vérification constituent une échappatoire, mais aussi un signal d’alerte. Si locator.click() aboutit à un délai d’expiration mais que dispatchEvent('click') fonctionne, vous n’avez rien corrigé : vous avez simplement contourné la vérification qui vous indiquait qu’un utilisateur réel ne pouvait pas non plus cliquer sur cet élément. Cela peut parfois être acceptable. Mais en général, cela signifie qu’il existe un véritable problème de superposition que votre test a simplement cessé de détecter.

Modifier chaque délai d'expiration au bon endroit

La configuration s'articule en plusieurs niveaux, et la placer au mauvais niveau entraîne des résultats prêtant à confusion.

Configuration globale, dans playwright.config.ts

:

export default defineConfig({
  timeout: 60_000,
  globalTimeout: 60 * 60 * 1000,
  expect: { timeout: 10_000 },
  use: {
    actionTimeout: 15_000,
    navigationTimeout: 30_000,
  },
});

Notez où se trouve chaque élément. timeout

et globalTimeout

relèvent de la configuration de premier niveau ; expect.timeout

se trouve sous expect

; actionTimeout

et navigationTimeout

se trouvent sous use

, car il s'agit d'options de test plutôt que de configuration du programme d'exécution. Si elles sont placées au mauvais niveau, elles sont ignorées sans message d’erreur.

Par test :

test('slow one', async ({ page }) => {
  test.setTimeout(120_000);
  // ...
});

**test.slow()

** triple le délai d’expiration par défaut — une bonne valeur par défaut pour un test dont vous savez qu’il est vraiment long, sans avoir à choisir un nombre arbitraire.

Par assertion :

await expect(page.getByRole('status')).toHaveText('Done', { timeout: 30_000 });

Par action :

await page.getByRole('button', { name: 'Export' }).click({ timeout: 15_000 });

**Dans beforeAll

et afterAll

**, qui disposent de leur propre budget de 30 secondes, appelez test.setTimeout()

à l’intérieur du hook lui-même.

Pour un test lent, attribuez-lui son propre délai d’expiration dans test.extend()

plutôt que d’allonger la durée de tous les tests qui l’utilisent :

export const test = base.extend<{ seeded: void }>({
  seeded: [async ({}, use) => {
    await seedDatabase();
    await use();
  }, { timeout: 60_000 }],
});

Le principe général : définissez la portée la plus restreinte possible qui résout le problème. Augmenter le délai d’expiration global des tests pour s’adapter à un seul test lent ralentit l’échec de tous les autres tests, ce qui coûte du temps réel en CI.

Ce qui est pris en compte dans le délai d’expiration du test

Ce point est souvent mal compris, ce qui explique pourquoi certains tests expirent « avant même d’avoir fait quoi que ce soit ».

La documentation est claire : « Le temps passé par la fonction de test, la configuration des fixtures et les hooks d’beforeEachs est inclus dans le délai d’expiration du test. »

Ainsi, un test « beforeEach » qui se connecte, initialise des données et navigue sur le site consomme les mêmes 30 secondes dont le corps de votre test a besoin. Un test qui semble atteindre le délai d’exécution maximal dès sa première ligne a peut-être passé 28 secondes en configuration.

Par défaut, les fixtures partagent le délai d’expiration du test, ce qui constitue le même piège sous une autre forme : une fixture coûteuse épuise le budget de tous les tests qui en dépendent. Attribuez aux fixtures lentes leur propre délai d’expiration au lieu d’augmenter le délai d’expiration du test partout.

Le « teardown » est séparé : les teardowns des fixtures et les hooks « afterEach » disposent de leur propre budget une fois la fonction de test terminée ; ainsi, un teardown lent ne consomme pas le temps alloué au test.

Conséquence pratique pour le débogage : lorsqu’un test atteint son délai d’expiration, examinez l’ensemble de la chaîne — fixtures, beforeEach et le corps du test — et pas seulement la ligne indiquée par l’erreur.

Diagnostiquez avant d'augmenter la charge

Une séquence qui résout la plupart des délais d'expiration en quelques minutes.

Exécutez-la avec la visionneuse de traces. C'est l'outil le plus précieux qui soit, et pourtant il est sous-utilisé :

npx playwright test --trace on
npx playwright show-trace trace.zip

La trace affiche chaque action, sa durée, les instantanés DOM avant et après, ainsi que l'activité réseau. Un localisateur qui ne correspond à rien est immédiatement visible, tout comme la bannière de cookies qui recouvre votre bouton.

Lancez l’exécution en mode « headed » et au ralenti lorsque vous souhaitez observer le processus :

npx playwright test --headed --debug

Vérifiez si le localisateur fonctionne correctement :

console.log(await page.getByRole('button', { name: 'Save' }).count());

Une valeur nulle indique un problème de sélecteur, et aucune valeur de délai d’expiration ne permettra de le résoudre.

Vérifiez s’il s’agit d’un problème de stabilité en utilisant une action sans vérification à des fins de diagnostic — et non comme solution. Si dispatchEvent('click')

fonctionne alors que click()

expire, cela signifie que quelque chose recouvre ou déplace l’élément.

**Vérifiez s’il y a une attente «networkidle

».** Les pages comportant des balises d’analyse, des WebSockets ou des interrogations régulières peuvent ne jamais atteindre l’état d’inactivité du réseau. Préférez attendre l’événement qui vous intéresse réellement :

await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();

Lisez le message d’erreur dans son intégralité. Les erreurs de délai d’expiration de Playwright incluent le localisateur, le nombre d’éléments résolus et la vérification d’actionnabilité en attente. Ce dernier détail identifie généralement le problème sans ambiguïté.

L'allongement des délais d'attente aggrave généralement les instabilités

C'est un point contre-intuitif, mais important.

Un test instable est un test dont le résultat dépend du moment où il est exécuté. Allonger le délai d’expiration élargit la fenêtre dans laquelle il réussit, ce qui rend l’instabilité plus rare — et, par conséquent, plus difficile à reproduire, plus difficile à diagnostiquer et plus lente à détecter lorsqu’elle survient.

En revanche, chaque échec a un coût. Une suite de 200 tests avec un délai d’expiration de 30 secondes met au maximum 100 minutes à échouer complètement ; à 120 secondes, cela prend 400 minutes. En intégration continue (CI), cela se traduit par un coût financier réel et un temps d’attente réel.

Ce qui résout réellement l’instabilité :

Attendez l’état, pas le temps. « waitForTimeout » est presque toujours erroné. Vérifiez la condition qui vous intéresse et laissez Playwright effectuer les requêtes.

Utilisez des assertions « web-first ». « expect(locator).toBeVisible() » effectue automatiquement des réessais. « expect(await locator.isVisible()).toBe(true) » vérifie une seule fois et échoue dès le premier échec — une source subtile mais très courante d’instabilité.

Gérez les superpositions de manière déterministe. Fermez les bannières de cookies dans un fixture plutôt que d’espérer qu’elles aient disparu.

Désactivez les animations dans votre configuration lorsque c’est possible, plutôt que d’attendre qu’elles s’arrêtent.

Attendez la réponse réseau spécifique dont vous dépendez plutôt que d’attendre que le réseau se calme.

Stabilisez les données. Les tests qui dépendent d’un état mutable partagé sont instables pour des raisons que le délai d’expiration ne résout pas.

Quand déclencher véritablement un délai d’expiration : lorsque l’opération est réellement lente et qu’il n’y a aucun moyen de contourner cela — un téléchargement de fichier volumineux, un rapport dont la génération prend une minute, un profil réseau délibérément bridé. Dans ces cas-là, déclenchez-le de manière ciblée, sur ce test ou cette assertion, et ne modifiez pas les valeurs par défaut.

Délais d'expiration lors de l'exécution via un proxy

Le cas où un « raise » est justifié, et la configuration correspondante.

Playwright prend en charge les paramètres de proxy dans la configuration réseau :

export default defineConfig({
  use: {
    proxy: {
      server: 'http://proxy.example.com:9000',
      username: 'user',
      password: 'pass',
    },
  },
});

Ou par contexte, ce qui est l'option à privilégier lorsque différents tests nécessitent des points de sortie différents :

const context = await browser.newContext({
  proxy: { server: 'http://proxy.example.com:9000' },
});

Trois conséquences pratiques.

Les proxys résidentiels ajoutent une latence réelle. Le trafic sort via une véritable connexion grand public ; il est donc normal d’observer plusieurs centaines de millisecondes supplémentaires par requête, ce n’est pas un dysfonctionnement. Une page effectuant quatre-vingts requêtes accumule ce retard quatre-vingts fois. Les paramètres par défaut optimisés pour localhost produiront des échecs qui ressembleront à des proxys défaillants, alors qu’il s’agit en réalité simplement de la distance.

La bonne approche consiste à mesurer plutôt qu’à deviner : exécutez la suite de tests via le proxy, examinez les temps de trace, puis définissez navigationTimeout

et actionTimeout

en fonction de vos observations, en prévoyant une marge généreuse.

La bande passante représente le véritable coût, et celui-ci est énorme avec un navigateur. Playwright récupère chaque image, police, script et vidéo en préchargement. Sur un forfait résidentiel facturé à 0,79 $/Go, cela représente la majeure partie de vos dépenses. Bloquer les types de ressources inutiles constitue la plus grande source d’économies possible :

await page.route('**/*.{png,jpg,jpeg,webp,gif,woff,woff2,mp4}', r => r.abort());

Cela réduit systématiquement le trafic de la majeure partie du total et accélère vos tests en prime.

Un blocage n’est pas un délai d’expiration. Si une cible renvoie une page de vérification, Playwright expirera en attendant un élément qui ne figure pas sur cette page — ce qui ressemble exactement à un délai d’expiration, mais n’en est pas un. Faites une capture d’écran en cas d’échec et examinez ce qui a réellement été affiché :

use: { screenshot: 'only-on-failure', trace: 'retain-on-failure' }

Il s’agit du schéma d’échec silencieux que nous avons décrit dans l’importance de tester les proxys : la requête a abouti, la page s’est affichée, mais ce n’était pas la bonne page.

Questions fréquentes

Quel est le délai d'expiration par défaut dans Playwright ?

30 000 ms pour un test et pour les hooks « beforeAll» / «afterAll », et 5 000 ms pour les assertions « expect ». Les délais d’expiration des actions et de la navigation n’ont pas de valeur par défaut et sont uniquement limités par le délai d’expiration du test. C’est pourquoi un clic lent indique les 30 secondes du test plutôt que sa propre limite.

Comment augmenter le délai d’expiration d’un test Playwright ?

Appelez test.setTimeout(120_000) à l’intérieur du test, ou test.slow() pour tripler la valeur par défaut. Privilégiez ces méthodes plutôt que d’augmenter le délai d’expiration global, ce qui ralentirait l’échec de tous les autres tests.

Pourquoi mon test Playwright expire-t-il alors que l’élément est présent sur la page ?

Généralement parce qu’il n’est pas cliquable. La vérification « click » exige que l’élément soit visible, stable, récepteur d’événements et activé — ainsi, un élément masqué par une bannière ou encore en animation sera détecté mais ne pourra jamais être cliqué. Le message d’erreur indique quelle vérification était en attente.

Quelle est la différence entre le délai d’expiration du test et le délai d’expiration de la vérification « expect » ?

Le délai d’expiration du test correspond au temps total alloué à la fonction de test, à la configuration des fixtures et aux hooks d’beforeEach, pour un total de 30 secondes par défaut. Le délai d’expiration de l’assertion correspond à la durée pendant laquelle une seule assertion « web-first » effectue des interrogations, avec une valeur par défaut de 5 secondes. Une assertion qui échoue après 5 secondes est due au délai d’expiration de l’assertion, et non au délai d’expiration du test.

Dois-je utiliser waitForTimeout dans Playwright ?

Presque jamais. Un délai d’attente fixe est soit trop court, ce qui rend le test instable, soit trop long, ce qui ralentit la suite de tests — généralement les deux sur des machines différentes. Attendez plutôt que la condition soit remplie à l’aide d’une assertion « web-first », qui effectue automatiquement des tentatives jusqu’à l’expiration du délai.

Pourquoi networkidle ne se résout-il jamais ?

Parce que la page continue d’envoyer des requêtes — balises d’analyse, WebSockets, interrogations, connexions de longue durée. networkidle nécessite un environnement calme, et de nombreuses applications modernes ne sont jamais au repos. Attendez plutôt l’élément ou la réponse spécifique qui vous intéresse.

Le fait d’augmenter le délai d’expiration corrige-t-il les tests instables ?

Cela les masque. Les comportements instables deviennent plus rares, plus difficiles à reproduire et plus lents à se manifester, tandis que chaque véritable échec de la suite de tests prend désormais plus de temps. Traitez la cause : attendez l’état plutôt que le temps, utilisez des assertions avec réessais, fermez les superpositions de manière déterministe et désactivez les animations.

Ai-je besoin de délais d’expiration plus longs lorsque j’utilise un proxy ?

Souvent oui, pour la navigation et les actions, car les proxys résidentiels ajoutent une latence réelle par requête qui s’accumule au fil des nombreuses requêtes effectuées par une page. Mesurez-la via le proxy avec le traçage activé et définissez les valeurs en fonction de ce que vous observez, plutôt que d’augmenter tout de manière préventive.

Conclusion

Le message indique que le test a dépassé les 30 secondes, mais il s’agit là d’une contrainte de temps plutôt que d’une explication. Un élément du test a attendu une condition qui ne s’est jamais produite, et dans quatre cas sur cinq, cette condition correspond à un sélecteur qui ne correspond à rien ou à un élément qui n’est jamais devenu actionnable.

L’ordre à suivre pour gagner du temps est donc le suivant : lisez l’erreur dans son intégralité, qui identifie la vérification d’actionnabilité en attente ; ouvrez la trace, qui vous montre le DOM au moment de l’échec ; vérifiez que le localisateur est résolu ; et ce n’est qu’ensuite que vous devez tenir compte du chiffre. Déclencher un délai d’expiration est la solution appropriée pour une seule cause précise — une opération qui prend réellement plus de temps que le délai imparti — et la mauvaise solution pour les quatre autres cas, où cela ne fait que retarder l’échec.

Lorsque vous en déclenchez un, faites-le de manière ciblée. Par test, par assertion, par fixture. Augmenter les valeurs par défaut globales pour pallier un seul téléchargement lent rend chaque échec de la suite plus coûteux, et le temps de CI est la seule ressource qui ne revient jamais.