El botón de cancelar que no cancelaba
El trabajo de esta semana en Greenhouse fue un lote pequeño de UX: renombrar un proyecto en línea, una etiqueta de “neglected for Nd” en las tarjetas de trabajo estancadas, previsualización de imágenes junto a la previsualización de audio existente, y un enlace de “why these rules?” de vuelta a la pantalla de reglas de onboarding. Nada de eso sonaba riesgoso.
El error que casi se publica
La interfaz de renombrado es sencillísima: se hace clic en un ícono de lápiz, el título se convierte en un campo de texto, Enter o hacer clic afuera guarda, Escape cancela. Primero escribí la versión obvia:
function cancel() {
editing = false;
}
Se veía bien. Después me senté a escribir la prueba de regresión específicamente para Escape, y para que la prueba realmente significara algo simulé la secuencia real: presionar Escape, y luego disparar el blur que ocurre justo después (quitar del DOM un input enfocado dispara un evento blur sobre él, antes de que el navegador termine de desmontarlo).
Ahí fue cuando se rompió. Mi función save() corre en el blur, encontró el texto sin confirmar que todavía estaba escrito en el valor vinculado (porque cancel() nunca lo tocaba, solo cerraba el editor), y lo guardó de todas formas. Escape parecía funcionar en una prueba manual normal. En realidad no cancelaba nada, solo tenía la suerte de que normalmente nada dispara un blur sobre un input que está a mitad de ser eliminado.
El arreglo
Una línea: reiniciar el valor antes de cerrar.
function cancel() {
value = original;
editing = false;
}
save() ya tenía una guarda de “no hacer nada si no cambió nada” (necesaria para el caso normal de abrir el campo y perder el foco sin escribir nada). Una vez que cancel() reinicia el valor primero, esa guarda vuelve inofensiva toda la condición de carrera sin importar en qué orden lleguen los eventos.
Por qué hacer clic manualmente no habría detectado esto
Esta es la parte que se me quedó grabada. Si solo se hubiera hecho clic en la función a mano, Escape se ve completamente correcto, nada se guarda visiblemente. El error solo existe en el hueco entre “el input se cierra” y “¿se dispara un evento perdido del navegador durante ese cierre?”, y los navegadores no disparan ese blur de forma confiable en cada camino de eliminación, así que incluso pruebas manuales repetidas podrían no haberlo mostrado. Solo lo encontré porque escribir una prueba de regresión real obligó a simular el orden exacto de eventos, no porque estuviera buscando una condición de carrera.
También en esta sesión
Se verificaron las cuatro piezas del lote contra la app real en ejecución, sobre IPC real, no pruebas simuladas, usando el arnés de WebDriver construido en sesiones anteriores. Una nota pequeña más, al pie: no se puede pasar un elemento del DOM encontrado como argumento de script al endpoint de ejecución de scripts de este arnés y esperar que siga siendo un elemento real del otro lado, se deserializa como un objeto plano. El arreglo es simplemente volver a consultarlo con document.querySelector dentro del propio script.
Lecturas relacionadas
El error que mis pruebas unitarias nunca podrían haber encontrado
Un tablero kanban renderizaba una prop que quedaba obsoleta con datos nuevos, y las pruebas simuladas pasaban porque un mock no puede expresar eso.
Una compuerta de accesibilidad de dos segundos que corre en cada push
axe-core sobre el arnés de pruebas de componentes ya existente, limitado a reglas WCAG reales, y lo que una compuerta en jsdom no puede detectar.
Agregando español y portugués a una app de escritorio, y quedando atrapado en mi propia suposición
Un cambio de idioma basado en recarga probado contra la app real, un archivo de config que sí importaba, y una palabra en español del hemisferio equivocado.
También te podría ser útil
Proton VPN
VPN comercial con filtrado NetShield e interruptor de apagado automático.
Como socio de Proton, obtengo ingresos por las compras que califican de los servicios de privacidad y seguridad de Proton (Pass, Mail, VPN, Drive).
Más informaciónAdGuard para iOS
Bloqueo de anuncios y rastreadores en todo el sistema en iOS, sin necesidad de un servidor DNS aparte.
Como afiliado de AdGuard, obtengo ingresos por las compras que califican.
Más informacióneSIM Airalo
eSIM de datos local para viajes - sin necesidad de cambiar una SIM física.
Este es mi enlace de referido de Airalo. Obtienes un descuento en tu primer eSIM y yo obtengo crédito de Airalo para el mío.
Más información