Especificación visual. Caso Point it out!
He tenido la necesidad de crear formas posicionables sobre sitios web, señalando o conectando elementos. Muchas veces. Es un must en aplicaciones interactivas que requieren de algún tipo de señalización, como podría ocurrir en tutoriales.
Harto de reinventar la rueda decido buscar una biblioteca sencillita que me resuelva este problema. Esperaba encontrar muchas para ello, o quizás como alguna feature de alguna muy gorda y conocida. Estaba casi convencido de que existiría algo, pero que aún así quizás tendría que hacer yo mismo una biblioteca propia con filosofía minimalista.
Pero lo cierto es que, al menos en unas pocas búsquedas sencillas, no he encontrado nada. Que no sea fácil de encontrar no significa que no exista, no te imaginas cuántas superherramientas sin visibilidad he encontrado entre repositorios perdidos.
A falta de soluciones (o como resultado de una mala investigación), me pongo manos a la obra en el desarrollo de una biblioteca para ello (Point it out) y me doy cuenta de que necesito algo para especificar qué resultado visual busco. Necesito alguna técnica de especificación visual, si es que existe algo así y si significa lo que pretendo que signifique.
Describiendo el problema: cómo señalizar elementos
Por ejemplo, ¿qué podría hacer si quisiera señalizar estas palabras? Si todo funciona bien deberías estar viendo un rectángulo verde sobre las palabras indicadas.
“Pero Alexander, ¿me estás diciendo que ya existe entonces solución?” No, lo que estás viendo es fruto del resultado final (o al menos de lo que ya tengo desarrollado). Dado que ya tengo una medio solución, ¿por qué no mostrarlo?
AVISO: ¿Estás buscando un tutorial paso a paso de cómo desarrollar una biblioteca de manera profesional? Pues este NO es el lugar. Solo encontrarás fragmentos de código relevantes para plantear el temita de la especificación visual.
En una primera versión, la biblioteca debía aportar al menos un primer mecanismo simple de señalización. Para ello especifiqué la siguiente función:
create(shape: ShapeName, {target: string | HTMLElement})
ShapeName
se trata de una string, un tipo de unión ‘rect’ | ‘image’, aunque
en un primer momento solo existe ‘rect’. La opción target es un selector CSS
o una referencia directa al elemento objetivo.
- Si es un selector, trabajará con el primer elemento que encuentre con ese selector, o lanzará excepción si no encuentra nada.
- Creará un elemento SVG que añadirá como hijo al body.
- Dicho SVG debe estar posicionado absolutamente sobre el elemento a señalar.
- Dicho SVG debe tener un tamaño similar al elemento a señalar.
- Digo similar porque podría ser más grande, si quiere rodearse sin taparlo.
En las primeras iteraciones de herramientas gráficas me gusta centrarme en la exploración tecnológica. Hacer un caso mínimo funcional que dé un resultado visual inmediato.
Especificaciones formales con algún framework de testing
Aquí empieza lo realmente interesante. Hay que testar visualmente, eso está claro, estamos ante una librería cuyo objetivo es renderizar cosas. Pero eso no significa que inmediatamente todo vaya a ser regresión visual y E2E. Considera la siguiente spec:
describe('create(...)', ()=>{
...
it('creates an SVG and appends it to the body', ()=>{
...
})
})
Esto no requiere renderizado. Vale con un DOM virtual, comprobar que se crea un nuevo nodo y si queda colgando del body. Hacer una prueba E2E para esto es matar moscas a cañonazos.
Especificación visual
Me gusta la regresión visual, pero tiene un pequeño problema. Por si no sabes de qué te estoy hablando, consiste en la generación y comparación mediante snapshots (ficheros que describan el estado de una aplicación ante ciertas condiciones. Típicamente suelen ser screenshots). Su fortaleza reside en la cobertura futura. Por lo general se generan snapshots una vez se tiene el resultado buscado para que en futuras comparaciones tras refactoring recibamos un feedback de si algo ha cambiado más de lo esperado.
Y aunque es una herramienta útil y bienvenida en mantenimiento de aplicaciones que controlan muy bien la interfaz de usuario y donde es crítico que estas no cambien inesperadamente, la regresión visual no forma exactamente parte del flujo de las metodologías estilo TDD, BDD o ATDD. Recordemos que son metodologías de diseño y la regresión visual no aporta nada en la fase de diseño o para definir especificaciones preimplementación.
Buscando la alternativa a la regresión visual
La siguiente forma de proceder y opiniones son, hasta donde sé, de Cosexa Pgropia™. No he hecho una investigación exhaustiva ni sé si hay autores que han escrito sobre esto a favor o en contra. Por eso no asumas nada de lo que expongo aquí, evita todo prejuicio positivo y afila la sospecha y la duda.
El proceso que he realizado es el siguiente: adaptar una biblioteca o framework con capacidades para regresión visual (en este caso Playwright) para que en su lugar genere screenshots de dos páginas distintas y las compare. Una de las páginas presenta el escenario donde ejecutas la acción a probar; la otra es la versión con el resultado final esperado, hecha a mano y centrándonos solo en el resultado visual.
Supongamos la siguiente especificacion:
describe("create('rect', {...})", ()=> {
...
describe('Default behaviors (no options)', () => {
...
it('creates an orange rect of 4px width around target', ()=>{
...
})
})
})
Nuestro escenario de pruebas podría contener, entre otras cosas, lo siguiente:
<div class="test-box"></div>
.test-box {
background: #333;
width: 250px;
height: 150px;
}
Y por otra parte, una página copia modificada con el resultado VISUAL esperado
<div class="test-box test-box--expected"></div>
...
.test-box--expected {
box-sizing: content-box;
border: 4px solid orange;
}
SÉ LO QUE ESTÁS PENSANDO (o deberías): “pero Alexander, en el esperado no hay ningún SVG, ¡solo le has puesto un borde naranja de 4 píxeles al elemento!”
A esto es a lo que me refiero con especificación visual. Podría haber un SVG hecho a mano (de hecho los hay en specs más complejas) y seguiría siendo válido, pero en realidad no importa la estructura, no es lo que estamos especificando ni testando aquí.
Al igual que con tests convencionales, buscamos la forma menos compleja y más directa de definir lo que queremos (en este caso lo que queremos visualmente).
Créeme, esto no lo hago por mejorar la adaptabilidad y reducir la fragilidad del test, aunque sean sus efectos positivos principales, sino porque simplemente es la forma más sencilla que se me ha ocurrido de conseguir lo que se busca visualmente.
En una clásica “especificación por ejemplo” con GIVEN-WHEN-THEN, esta parte correspondería al THEN, y al igual que en las especificaciones convencionales, debe expresarse de la forma más directa y clara atendiendo en este caso exclusivamente al resultado visible (ya que es la única responsabilidad en estos tests).
Veamos un test similar al del Mundo Real™. El original y funcional lo tienes en el repositorio (enlace al final). Es un test parametrizado, se genera con variaciones de expectedWidth (xW) y expectedHeight (xH).
it(`creates an orange rect of 4px width around target (${xW}x${xH})`, async ({ page }) => {
await comparePages({
testingURL: `/${xW}x${xH}`,
expectedURL: `/expected/rect/${xW}x${xH}-default`,
action: () => {
return page.evaluate(() => {
pio.create('rect', {target: `.test-box--${xW}x${xH}`})
})
}
})
})
La función es autoexplicativa. Recibe la página donde se ejecutará la acción a testar, la página con la que comparar, y la acción a realizar. Tras lanzar el test debería fallar al no tener implementación inicialmente. El test report de Playwright en este caso me reporta una comparación de múltiples formas, entre ellas una muy chula con un slider. Captura:
Sólo lo visible
Tengo que insistir en que sólo hago esto con fines de testar el resultado visual. Puede que te preguntes algo como “¿No estás perdiendo la oportunidad de especificar directamente un resultado final que sirva también de especificación del DOM deseado para una prueba E2E completa?”
No conozco (todavía) el DOM deseado y no puedo perder tiempo planificando. Esto me permite especificar el resultado visual, que es lo único que tengo claro ahora mismo. Además los SVG para un mismo objetivo se pueden generar de múltiples maneras. Exagerar el detalle aumenta la rigidez del diseño, no permite posponer decisiones y aumenta la fragilidad de los tests.
Por otra parte también nos limitaría más con escenarios menos deterministas. Imagina que quieres que el recuadro siga al elemento si este se reposiciona (típico al reescalar pantalla). Esto es algo que debe hacer la biblioteca, ¿cómo especificaríamos algo así? Implicaría lógica en el propio test. En los expects se evitan lógicas complejas a favor de resultados directos deterministas. Un borde por CSS no solo está fijado a un elemento, también funciona aunque reescales o lo muevas.
Otras preguntas relacionadas que me he planteado
¿Por qué hacer un esfuerzo en especificar partes del aspecto visual esperado en un desarrollo si muy probablemente haya partes que no podremos especificar de todas formas?, ¿no es como intentar construir una casa en la que sabemos de antemano que nunca tendremos techo o alguna otra parte fundamental?
Ya tenemos la respuesta en la jerarquía de tests. Sabemos que las pruebas unitarias no nos sirven para todo, ni tampoco las de integración. Pero lo hacemos igualmente porque nos guían y cubren el desarrollo, cada una a su manera, con pros y contras. Técnicamente podríamos usar solo tests de aceptación si estos fueran infinitamente rápidos, y aún así, muy probablemente se seguirían realizando pruebas unitarias y de integración junto a ellos.
Puedes tener un muy buen maquetador, diseñador, artista o artista técnico en tu equipo durante el desarrollo de una aplicación, con capacidad para diseñar/especificar rápidamente el resultado que quieres pero sin competencias o tiempo para realizar la herramienta que produzca ese resultado.
He conocido empresas que trabajan con un flujo basado en prototipos, y eso está perfecto, pero podría ser muy útil entender esos prototipos como especificaciones y realizar pruebas directas contra ellos.
¿Cómo de útil es esto realmente?
La respuesta es algo evidente: si fuera muy útil habría literatura al respecto y alguna que otra herramienta conocida. Créeme, no soy ningún genio extendiendo el estado del arte. Esta “técnica” viene de perlas para el desarrollo de esta biblioteca en concreto, pero, ¿se te ocurre alguna otra? No es especialmente extrapolable. Ni siquiera es suficiente para todos los casos de esta biblioteca.
Voy a describirte un caso difícil de explicar: imagina que quieres señalizar con una flecha que apunta siempre a un objetivo. Supón que incluso aunque este se mueva en pantalla la flecha mantenga su posición y gire apuntando siempre al objetivo (un “look at” en gráficos).
La descripción anterior es incluso difícil de imaginar para algunos, supone problemas en cómo especificarla y cómo testarla. Y ni siquiera es algo realmente complejo. En las artes visuales muchas veces no queda más remedio que romper el orden rojo-verde porque el esfuerzo de especificar puede ser indistinguible del de implementar.
Es ahí donde destaca la regresión visual clásica. Implementas y cuando estés satisfecho, snapshot que servirá como “expected”. No te habrá servido para guiar tu diseño e implementación, pero seguirás protegiendo tu código y permitiendo el refactoring.
Pero no olvides que al final eres tú (o tu equipo) quien decide si cualquier técnica es o no rentable de aplicar en el flujo de trabajo. Incluso aunque existiera garantía de que aplicarlo mejorase la productividad o calidad, podría causar el efecto contrario si diezma la moral al percibirse como algo pesado, raro o molesto por mera falta de costumbre. La misma historia del TDD.
Point it out como posible ejemplo de especificación visual
Puedes seguir el desarrollo de Point it out! en github, donde verás que hay mucho de lo hablado en este artículo llevado a un nivel enfermizo. Es un proyecto pensado para experimentar y debatir acerca de las posibilidades de la especificación visual.
Si clonas y ejecutas el proyecto en modo de desarrollo podrás encontrar una jerarquía de especificaciones bastante intensa para al menos dos formas y dos tamaños de objetivos. Aquí una screenshot: