Saltar al contenido
Development

El botón de cancelar que no cancelaba

Por Victor Da Luz
sveltetestingdev-loggreenhouse

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

Development

Un dashboard que dejó de decir la hora

Un bloque $derived que leyó Date.now() exactamente una vez, una medianoche que significaba cosas distintas para el frontend y para el motor en Rust, y la oración gramaticalmente rota que lo demostró.

Leer