Uno de mis tests de accesibilidad fallaba en CI cerca de una vez cada seis corridas, y pasaba absolutamente siempre que lo corría localmente. Esa combinación es de lo peor que puede tener un bug, porque “funciona en mi máquina” no es una estrategia de depuración, es una confesión.
La causa real
El test corría un escaneo de axe-core sobre un componente de tarjeta a mitad de un fade —las máquinas de CI son más lentas y menos predeciblemente programadas que mi laptop, así que el escaneo a veces caía mientras la opacidad de una tarjeta todavía estaba animando entre 0 y 1. A axe no le importa la opacidad, pero sí señala discrepancias de aria-hidden, y la transición de fade brevemente alternaba un estado oculto en elementos que todavía no habían terminado de aparecer.
await page.waitForSelector('[data-reveal="done"]');
const results = await new AxeBuilder({ page }).analyze();
El arreglo fue un solo atributo: agregar una marca data-reveal="done" una vez que terminaba el callback del IntersectionObserver de la transición de fade, y esperarla antes de escanear. Escanear el estado ya asentado, no el que está a mitad de animación.
Por qué vale la pena escribirlo
Un test inestable en CI es fácil de tratar como ruido —volver a correrlo, seguir adelante, y dejar de confiar un poco más en la suite cada vez que pasa. La causa real acá no tuvo nada que ver con el framework de testing y todo que ver con que la cosa bajo prueba corría una carrera contra la cosa que la verificaba. Una vez que dejé de asumir que el test estaba mal y empecé a preguntarme qué era distinto del timing de CI, encontrarlo tomó unos diez minutos.