Pourquoi vos tests Playwright cassent à chaque sprint (et ce n'est pas la faute de Playwright)

Si votre suite e2e est fragile, ce n'est pas parce que le framework est mauvais. C'est parce que vos sélecteurs empruntent leur stabilité à des choses qui n'ont jamais été conçues pour être stables. Voici le vrai diagnostic, avec des noms.

Pourquoi vos tests Playwright cassent à chaque sprint (et ce n'est pas la faute de Playwright)

Si vous êtes Lead QA et que vous lisez ça un lundi matin, il y a de bonnes chances que vous ayez déjà passé 30 minutes à réparer un test qui marchait vendredi après-midi. Le sélecteur pointait vers .btn-primary >> nth=2. Un dev a déplacé un bouton. La CI est rouge. Vous soupirez, ouvrez les DevTools, fabriquez un nouveau chemin, push, merge. Vous le referez au prochain sprint.

La plupart des équipes dans cette situation finissent par conclure que "Playwright est flaky". Elles envisagent de migrer vers Cypress. Ou Selenium. Ou, dramatiquement, "on arrête les e2e, on fera juste de l'unitaire."

C'est le mauvais diagnostic. Playwright n'a pas changé. Cypress non plus. Le DOM, lui, a changé. Et vos sélecteurs étaient liés à des choses qui n'ont jamais été matière à contrat.

Cet article passe en revue les vrais coupables, avec des noms, des exemples, et un verdict pour chacun.

Le modèle mental : sélecteurs contractuels vs accidentels

Avant de nommer les coupables, voici le cadre qui rend tout le reste évident.

Un sélecteur est contractuel quand les deux parties (le dev qui écrit le markup, le QA qui écrit le test) sont explicitement d'accord sur le fait que cette chaîne représente un point d'accroche stable. Le renommer demande une coordination. Les deux parties possèdent le contrat.

Un sélecteur est accidentel quand il fonctionne aujourd'hui parce que le markup se trouve avoir cette structure, mais personne ne s'est engagé à la maintenir stable. La classe CSS .btn-primary est accidentelle. Le troisième bouton enfant est accidentel. Le texte "Valider" est accidentel (l'équipe marketing peut le renommer en "Enregistrer les modifications" demain sans vous prévenir).

L'écrasante majorité de vos tests cassés n'est pas Playwright qui se comporte mal. C'est des sélecteurs accidentels qui deviennent faux au prochain refactor.

Coupable 1 : les sélecteurs par classe CSS

À quoi ça ressemble :

await page.locator('.btn-primary').click()
await page.locator('.user-row > .actions > button').click()

Pourquoi ça casse :

Les classes CSS ont une seule mission, et ce n'est pas le test. Elles existent pour appliquer des styles. Le jour où un designer décide que "les boutons primaires doivent maintenant utiliser la variante .btn-action-emphasis", chaque test qui utilise .btn-primary casse. Le jour où un dev front migre de BEM vers Tailwind, tous vos sélecteurs basés sur des classes meurent en même temps.

Pire : les migrations de framework (Vue 2 vers Vue 3, CSS-in-JS vers CSS classique, Tailwind v3 vers v4) reshufflent souvent les noms de classes générées. Vos tests ne survivent pas à la mise à jour.

Verdict : faible. Acceptable comme patch temporaire quand rien d'autre n'est disponible, mais ça emprunte du temps qu'il faudra rendre.

Coupable 2 : nth-child / nth-of-type

À quoi ça ressemble :

await page.locator('table tr:nth-child(3) td:nth-child(4) button').click()
await page.locator('div.cards > div:nth-of-type(2)').click()

Pourquoi ça casse :

Les sélecteurs basés sur la position couplent votre test à l'ordre visuel des éléments. Le jour où un dev ajoute une nouvelle colonne, intervertit deux lignes pour des raisons d'accessibilité, ou enrobe une section dans un div supplémentaire, l'index se décale et le test pointe sur le mauvais élément.

Le mode d'échec vraiment insidieux est le silencieux : le test passe toujours, mais il clique maintenant sur un bouton différent de ce qui était prévu. Votre suite reste verte tout en ne testant rien d'utile.

Verdict : le plus faible de tous. À éviter même comme patch temporaire.

Coupable 3 : le contenu textuel

À quoi ça ressemble :

await page.getByText('Valider').click()
await page.getByRole('button', { name: 'Enregistrer' }).click()

Pourquoi ça casse :

Les sélecteurs basés sur du texte sont étroitement couplés à la copy. Trois exemples concrets :

À noter : getByRole({ name: 'Valider' }) est meilleur que getByText brut parce que le rôle lui-même est structurel et ne bouge pas avec la copy. Mais la partie name matche toujours sur le label accessible, qui est souvent le texte visible. Même risque, légèrement atténué.

Verdict : utilisable pour les CTAs cruciaux dont vous contrôlez la copy, fragile ailleurs. À traiter comme un tier intermédiaire.

Coupable 4 : les IDs auto-générés

À quoi ça ressemble :

await page.locator('#mui-component-select-3847').click()
await page.locator('#radix-:r19:').click()

Pourquoi ça casse :

Les bibliothèques UI modernes (Material UI, Radix, headless UI, etc.) génèrent des IDs au runtime. Ils ont l'air stables dans les DevTools mais sont régénérés à chaque render. Le numéro après mui-component-select- s'incrémente à chaque fois qu'un autre composant se monte avant celui-ci. Votre test passe en local et échoue en CI parce que l'ordre des montages diffère.

Verdict : piège. Ils ressemblent à des vrais IDs mais se comportent comme du nth-child.

Coupable 5 : les longs XPath

À quoi ça ressemble :

await page.locator('xpath=//*[@id="root"]/div/div[2]/main/section[3]/form/button[2]').click()

Pourquoi ça casse :

Le long XPath cumule toutes les fragilités précédentes. Il dépend de la structure de l'arbre, des positions, des types d'éléments, des valeurs d'attributs. N'importe lequel de ces changements invalide le chemin. Et le pire : le long XPath est ce que les enregistreurs de test génèrent quand ils ne trouvent pas d'identifiant stable, ce qui veut dire que c'est un signal qu'aucun identifiant stable n'existe sur cet élément.

Votre enregistreur de test n'est pas paresseux. Il vous dit que le dev n'a pas rendu cet élément testable.

Verdict : code smell au niveau de l'élément, pas du test. Le fix n'est pas un meilleur XPath, c'est un meilleur élément.

À quoi ressemble un sélecteur contractuel

Trois patterns survivent aux refactors, aux migrations de framework, et aux changements de copy :

Sélecteur Pourquoi il survit
data-testid (ou data-cy, data-test) Conçu spécifiquement pour les tests. Personne ne le style, ne le traduit, ni ne le renomme pour des raisons marketing.
id stable écrit à la main Un vrai id que le dev traite comme un contrat (pas auto-généré par une lib).
aria-label détenu par l'équipe accessibilité L'équipe a11y a un intérêt direct à le maintenir stable. Bonus : améliore l'accessibilité en même temps.

Si votre stratégie de sélecteurs ne s'appuie pas principalement sur ces trois-là, votre suite de tests est en sursis.

Le fix n'est pas plus d'ingénierie de locators. C'est une culture de testabilité.

Le piège dans lequel tombent la plupart des équipes quand elles réalisent que leurs sélecteurs sont fragiles : elles passent une semaine à refactorer les tests pour utiliser des chemins CSS plus "intelligents" ou des XPath plus flexibles. Elles se sentent productives. La fragilité revient au sprint suivant.

Le vrai fix est moins glorieux. C'est de faire de la testabilité un sujet partagé entre dev et QA, avec trois règles :

  1. Le nouveau code ajoute un data-testid (ou un aria-label) sur chaque élément interactif que QA touche réellement. Les reviewers refusent les PRs qui n'en ont pas.
  2. Quand un test casse, le fix n'est pas un meilleur sélecteur. C'est demander au dev d'ajouter un point d'accroche stable, puis mettre à jour le test. Un ticket, deux minutes de dev, fix permanent.
  3. Utilisez un outil pour auditer ce qui manque au lieu d'inspecter chaque élément à la main. TestID Hunter enregistre une session QA, classe chaque élément interagi par stabilité de sélecteur (Solid / Usable / Weak), et génère un ticket prêt à coller pour le dev : "ajoute ces data-testid au composant X pour stabiliser le parcours d'inscription."

Ce troisième point compte parce que l'audit manuel ne scale pas au-delà de 5-6 parcours. Vous le sauterez, vous taperez le chaos du lundi matin, et vous conclurez "Playwright est flaky" une nouvelle fois.

La conclusion

Arrêtez de chercher un meilleur framework. Commencez à chercher un meilleur contrat.

Un sélecteur qui survit au prochain refactor, c'est celui que votre dev s'est engagé à maintenir stable. Tout le reste, peu importe à quel point c'est malin, emprunte du temps à votre futur vous.