Cypress Flake Fixer
Audit une suite Cypress existante pour identifier les patterns de code qui causent des échecs intermittents, puis les corriger sans masquer le problème par des retries, des timeouts plus longs ou force: true.
Quand utiliser cette compétence
- Une spec Cypress passe localement et échoue en CI, ou échoue une fois tous les quelques essais
- La suite contient
cy.wait(<number>),{ force: true }, ou des tests qui ne passent que dans l'ordre - Les erreurs mentionnent « detached from the DOM », « timed out retrying » ou un subject qui « is no longer attached »
- Le projet met à niveau Cypress sur une version majeure (12 à 16)
- Un audit du code des tests Cypress est demandé
Pour écrire des tests Cypress à partir de zéro, les standards de codage suffisent. Cette compétence est destinée au diagnostic et à la réparation d'une suite existante.
Prérequis
- Node.js 18 ou ultérieur pour exécuter le scanner (il n'a pas de dépendances et n'a pas besoin que Cypress soit installé)
- La version de Cypress, lue depuis
package.json. Les correctifs diffèrent selon la version majeure
Workflow
1. Scanner
node ./scripts/scan-cypress.mjs <project-root>
node ./scripts/scan-cypress.mjs <project-root> --json # format lisible par machine
node ./scripts/scan-cypress.mjs <project-root> --strict # ajouter les règles de faible confiance
node ./scripts/scan-cypress.mjs cypress/e2e/checkout.cy.ts # un seul fichier
Le scanner lit la version de Cypress depuis le package.json le plus proche et classe les APIs supprimées en error quand la version majeure installée les rejette déjà, et en warn quand elles échouent lors de la mise à niveau. Lancez --list-rules pour le catalogue.
Les résultats sont heuristiques. Ouvrez chaque localisation et confirmez avant d'éditer.
2. Mesurer avant de changer quoi que ce soit
Quand l'objectif est de corriger une spec flaky spécifique, enregistrez d'abord la fréquence des échecs. Sans ligne de base, il n'y a aucun moyen de distinguer un correctif de la chance.
for i in $(seq 1 20); do npx cypress run --spec "cypress/e2e/checkout.cy.ts" --quiet || echo "FAIL $i"; done
Une spec qui échoue 1 fois sur 10 a besoin d'environ 30 exécutions réussies après le correctif avant que le correctif soit crédible.
3. Corriger dans cet ordre
| Ordre | Règles | Pourquoi en premier |
|---|---|---|
| 1 | CY002, CY003, CY007 classées error |
Le test est incorrect ou déjà cassé ; les autres correctifs s'appuient sur un flux de commande correct |
| 2 | CY001, intercept ordering | Les attentes basées sur le temps sont la plus grande source unique de flake |
| 3 | CY005, CY006 | L'absence de retry-ability cause les erreurs detached-DOM et les assertions one-shot |
| 4 | CY004 | force: true masque un vrai chevauchement, une animation ou un état désactivé |
| 5 | CY008, CY010, CY012 | Fuites d'état et dépendance d'ordre |
| 6 | CY013, CY101, CY102 | Hygiène des sélecteurs et des assertions |
Le code avant/après pour chaque règle est dans references/anti-patterns.md. Les remplaçants pour les APIs supprimées sont dans references/removed-apis.md. Lisez la section pour la règle en cours de correction ; ne chargez pas les deux fichiers d'avance.
4. Remplacer chaque attente corrigée par la condition qu'elle couvrait
Ne supprimez pas un cy.wait(n) ou ne le remplacez pas par une hypothèse. Établissez ce que le délai couvrait :
| L'attente couvrait | Remplacer par |
|---|---|
| Une requête réseau | cy.intercept(...).as('x') enregistrée avant le déclenchement, puis cy.wait('@x') |
| Un rendu qui suit une requête ou un changement d'état | Un .should() sur le DOM qui en résulte |
| Un debounce, throttle, polling interval ou toast timeout | cy.clock() avant cy.visit(), puis cy.tick(ms) |
| Une animation ou transition CSS | Une assertion sur l'état final ; Cypress attend déjà que l'élément cesse de bouger avant d'agir |
| Hydratation client du HTML rendu côté serveur | Un signal que l'app définit quand l'hydratation se termine (voir Gotchas) |
| Inconnue | Lancez le test dans npx cypress open, sélectionnez l'attente dans le Command Log et lisez les requêtes et changements DOM qui se sont produits durant celle-ci |
5. Vérifier
- Lancez la spec modifiée seule, puis répétez-la au moins autant de fois que l'exécution de base.
- Relancez le scanner et comparez les comptages par règle.
- Lancez la suite complète une fois pour détecter la dépendance d'ordre exposée par le changement.
Règles pour le correctif lui-même
- Modifiez uniquement le code de test. Si le test expose un bug applicatif (double submit, état de chargement manquant, race dans l'UI), signalez-le plutôt que de le contourner.
- Ne relevez pas
defaultCommandTimeout, n'ajoutez pas deretriesou{ force: true }pour faire passer un test. Chacun convertit un échec visible en un plus lent ou masqué. - Ne remplacez pas
cy.wait(2000)parcy.wait(5000), ou par.should('exist')sur un élément déjà présent. - Gardez une catégorie de règle par commit pour qu'une régression puisse être bisectée.
- Corrigez d'abord les custom commands partagées dans
cypress/support. Une attente corrigée à l'intérieur d'une command s'exécute dans chaque test qui l'appelle.
Gotchas
-
Enregistrez
cy.intercept()avant que la requête puisse partir. Pour les requêtes faites au chargement de la page, cela signifie avantcy.visit(). Un intercept enregistré après le départ de la requête ne correspond jamais, etcy.wait('@alias')expire intermittent selon la vitesse du réseau. -
Chaque
cy.wait('@alias')consomme une requête. Si l'app appelle l'endpoint deux fois, la première attente résout au premier appel et l'assertion s'exécute sur les données obsolètes. Attendez une fois par appel attendu, ou assertez sur le DOM. -
GraphQL utilise une URL pour chaque opération. Aliasez dans le handler :
cy.intercept('POST', '/graphql', (req) => { if (req.body.operationName === 'GetOrders') req.alias = 'getOrders' }). -
.should('not.exist')passe avant que l'élément n'ait rendu. L'utiliser pour attendre qu'un spinner disparaisse réussit instantanément sur une page lente. Assertez l'état final positif. -
Les apps rendues côté serveur acceptent les clics avant que les handlers ne soient attachés. Dans Next.js, Nuxt, SvelteKit et Remix, le bouton est visible et activé dans le HTML serveur, donc Cypress le clique lors de l'hydratation et rien ne se passe. Faites en sorte que l'app définisse un marqueur quand l'hydratation se termine (par exemple un attribut
data-hydratedsur<body>depuis un mount effect) et assertez-le aprèscy.visit(). -
Un
.should()au milieu d'une chaîne épingle le subject. Les queries après ne peuvent pas re-requérir passé l'assertion, donc un re-render produit une erreur detached-DOM. Divisez la chaîne à l'assertion. -
.then()casse la chaîne de retry même sans assertion à l'intérieur. Tout avant s'exécute une seule fois. -
Cypress 16 tape sans délai entre les touches (le défaut de
keystrokeDelayest passé de 10 à 0). Les inputs qui re-rendent par frappe ou debounce sur input peuvent maintenant perdre des caractères ou se déclencher une fois. Assertez la valeur du champ après la frappe au lieu de restaurer le délai. -
Cypress 16 a transformé les getters de cookie et de stockage en queries.
cy.getCookie(),cy.getCookies(),cy.getAllCookies(),cy.getAllLocalStorage()etcy.getAllSessionStorage()réessaient maintenant les assertions.should()chaînées, ce qui élimine le besoin d'attentes autour des cookies d'auth. -
La page est vierge après
cy.session(). Unecy.visit()manquante après ressemble à une échec de sélecteur flaky. -
testIsolation: falsepartage l'état entre les tests. Si la config le définit, chaque échec dépendant de l'ordre est un comportement attendu, pas une flake. Vérifiez la config avant de diagnostiquer. -
Les retries masquent la flake sauf s'ils sont configurés pour la signaler. Pour trouver les tests flaky au lieu de les masquer, utilisez la stratégie expérimentale qui réessaie et échoue toujours :
retries: { experimentalStrategy: 'detect-flake-but-always-fail', experimentalOptions: { maxRetries: 2, stopIfAnyPassed: true }, openMode: true, runMode: true, } -
CY005 est un appel au jugement.
cy.get(input).clear().type('x')sur un input qui ne re-render jamais est correct. Divisez la chaîne quand l'élément appartient à une liste, une form library qui remonte les champs, ou n'importe quoi piloté par l'état async.
Format du rapport
## Audit flake Cypress : <scope>
- Version Cypress : <range>, majeure <n>
- Fichiers scannés : <n>
- Ligne de base : <spec> a échoué <f> sur <n> exécutions
| Règle | Avant | Après | Notes |
|---|---|---|---|
| CY001 fixed-wait | 14 | 0 | 9 remplacées par intercept aliases, 5 par assertions |
### Non corrigées
- <location> : <raison, par exemple nécessite un changement applicatif>
### Problèmes applicatifs trouvés
- <comportement que les tests ont exposé>
### Vérification
- <spec> : <n> sur <n> exécutions réussies après le changement
Dépannage
| Problème | Solution |
|---|---|
| Le scanner signale « Aucun fichier Cypress trouvé » | Passez le dossier qui contient cypress/ ou les fichiers *.cy.*. Les specs en dehors de ces conventions peuvent être passées comme chemins de fichier explicites |
| Un résultat est un faux positif | Ajoutez // cy-scan-ignore CY0xx sur la même ligne ou la ligne précédente, avec la raison |
| La version affiche « not detected » | Passez --cypress-major <n> ; dans une monorepo, lancez le scanner depuis le package qui dépend de Cypress |
| Le test échoue uniquement en CI après les corrections | Comparez viewport, browser et CPU. Lancez localement avec le browser CI et --browser chrome --headless, et limitez le CPU pour exposer les brèches de timing |
cy.wait('@alias') expire après le changement |
L'intercept est enregistré après la requête, le pattern URL manque une query string (ajoutez *), ou la requête est servie du cache et ne atteint jamais le réseau |
Références
- references/anti-patterns.md : code avant/après pour chaque règle du scanner
- references/removed-apis.md : APIs supprimées par version majeure Cypress, avec code de migration pour
Cypress.env()etcy.exec() - Cypress retry-ability
- Cypress migration guide