Playwright: visual testing — toHaveScreenshot(), golden files y el diff que lo muestra todo
Golden files, diff de 11,964 píxeles, tolerancia con maxDiffPixels y strict mode con SlickGrid. Lo que en Selenium necesita Ashot, acá es nativo.
Contexto: qué detecta un visual test que un test funcional no
Este post documenta visual testing en Playwright: comparar screenshots automáticamente para detectar cambios visuales que un test funcional no detecta. Es parte de mi serie de Playwright + TypeScript.
En el post anterior hice network interception — interceptar, mockear y bloquear requests HTTP. Tests funcionales: ¿la grilla carga? ¿tiene 91 filas? ¿el filtro funciona?
Pero hay otra categoría de problemas que esos tests no detectan. Alguien cambia un CSS y una columna se desalinea. Un font no carga y el texto se ve diferente. Un padding cambia y el layout se rompe. Un color cambia de #333 a #000 y nadie se entera.
Tests funcionales verifican qué muestra la página. Visual testing verifica cómo se ve.
toHaveScreenshot() toma un screenshot de la página (o de un elemento), lo compara pixel a pixel contra una imagen de referencia (golden file), y falla si hay diferencias. Si algo cambió visualmente, el reporte te muestra exactamente qué cambió, dónde y cuánto.
¿Y en Selenium?
No existe. No hay método nativo para comparar screenshots. Si querías algo así, necesitabas librerías externas como Ashot, configurar comparación de imágenes manualmente, definir thresholds vos mismo. Otro mundo.
En Playwright es una assertion más: await expect(page).toHaveScreenshot('nombre.png').
Test 1: screenshot de página completa
Creé tests/learning/visual-testing.spec.ts:

Primera ejecución: fallo esperado

Falló. "A snapshot doesn't exist." Esto es esperado — Playwright necesita una imagen de referencia para comparar y todavía no existe.
Lo que hizo fue tomar el screenshot actual y guardarlo en test-results/. Pero no lo guardó como referencia. Para eso:
npx playwright test visual-testing --project=chromium --update-snapshots
--update-snapshots toma el screenshot actual como la imagen de referencia. Playwright la guarda en visual-testing.spec.ts-snapshots/grilla-clientes-chromium-win32.png. Ese nombre incluye el browser y la plataforma — porque el mismo sitio puede verse diferente en Chromium vs Firefox vs WebKit, y en Windows vs Linux vs Mac.

Segunda ejecución: verde
Ahora sin --update-snapshots:
npx playwright test visual-testing --project=chromium

Pasó. El screenshot actual es idéntico a la referencia. Pixel a pixel, nada cambió.
Pero eso es el caso fácil. Lo interesante es cuando algo cambia.
Test 2: forzar diff con route.fulfill()
Para forzar un diff real, route.fulfill() del post anterior inyecta datos inventados en la grilla:

La golden tiene 91 clientes reales. Este test muestra 2 inventados. Si toHaveScreenshot() funciona, tiene que fallar.
El diff

11,964 píxeles diferentes. Ratio 0.02 de todos los píxeles de la imagen.
Algo que se ve en el call log: Playwright no falla en la primera toma. Toma el screenshot, detecta diferencia, espera 100ms, toma otro, compara de nuevo. Si sigue diferente, ahí falla. Esto es para evitar falsos positivos por animaciones o renders parciales. También desactiva CSS animations y espera que los fonts carguen antes de comparar.

El reporte genera 3 imágenes: Expected (la golden), Actual (lo que capturó), y Diff (los píxeles diferentes marcados en color). Además tiene vistas Side by side y Slider para comparar visualmente.
Esto es lo que hace poderoso al visual testing: no solo te dice "falló", te muestra exactamente dónde y cuánto.
Error real: strict mode violation con SlickGrid
Quise hacer un screenshot solo de la grilla, no de toda la página. Usé .slick-viewport:
const grilla = page.locator('.slick-viewport');
Error: strict mode violation. .slick-viewport resolvió a 9 elementos.

SlickGrid crea múltiples viewports: sg-top, sg-body, sg-bottom, cada uno con sg-start, sg-main, sg-end. Son 9 combinaciones. Este mismo patrón me apareció en posts anteriores con SlickGrid — siempre hay que ser específico con los selectores.
El fix:
const grilla = page.locator('.slick-viewport.sg-body.sg-main');
El viewport principal donde están los datos de la grilla.
Test 3: screenshot de un elemento específico
Con el selector corregido:

La diferencia con expect(page) es que expect(grilla) solo captura ese elemento. La golden es más chica, más enfocada, y más estable — cambios en el sidebar o el header no la afectan.
maxDiffPixels: cuánta diferencia aceptar
El test del mock falló con 11,964 píxeles de diferencia. ¿Qué pasa si le digo a Playwright "aceptá hasta 15,000 píxeles de diferencia"?

Pasó. 11,964 < 15,000. El diff existe pero está dentro del margen que definí.

maxDiffPixels es útil cuando:
- Hay diferencias de rendering entre corridas (anti-aliasing, sub-pixel rendering)
- Elementos dinámicos que cambian ligeramente (timestamps, cursors)
- Querés capturar cambios grandes pero ignorar variaciones menores
Otra opción es maxDiffPixelRatio — en vez de cantidad absoluta, un porcentaje del total de píxeles. Y threshold controla la sensibilidad por pixel individual (0 = exacto, 1 = cualquier diferencia es aceptable).
La trampa de las golden files compartidas
Hay una trampa con las golden files compartidas. Cuando corrí --update-snapshots, Playwright regeneró la golden grilla-clientes.png basándose en el orden de ejecución. El test con datos reales (91 clientes) y el test con datos mockeados (2 clientes) usan el mismo nombre de golden.
El que corrió último durante el update pisó al otro.
Resultado: al correr sin --update-snapshots, el test de datos reales pasó (porque la golden era de datos reales) y el test de mock falló (porque comparaba 2 clientes contra la golden de 91).
La lección: cada test que produce output visual diferente necesita su propia golden file con nombre único. Compartir nombres entre tests que renderizan cosas distintas genera resultados impredecibles que dependen del orden de ejecución.
En este spec es intencional — el test se llama "debe fallar visual test" porque quiero demostrar el diff. Pero en un framework de producción, cada test necesita su propio nombre de golden.
Visual regression en e2e/
Los tests de learning/ exploran el concepto: mocks, tolerancia, fallas forzadas. Para e2e/ creé tests de visual regression reales — verificar que la grilla se ve como se espera con datos reales.
tests/e2e/visual-regression.spec.ts:

La diferencia con los tests de learning: acá uso fixtures (dashboardPage, clientesPage), la navegación es por beforeEach como en el resto de e2e, y las golden files se guardan en su propia carpeta de snapshots.

Si mañana alguien cambia un CSS, un font, un padding, un color — estos tests fallan y el diff muestra exactamente qué cambió.
Cómo funciona toHaveScreenshot() internamente
Lo que hace Playwright cuando ejecuta toHaveScreenshot():
- Desactiva CSS animations (para evitar capturas en medio de una transición)
- Espera que los fonts carguen (para que el texto se renderice correctamente)
- Toma el screenshot
- Compara contra la golden file pixel a pixel
- Si hay diferencia, espera 100ms y toma otro screenshot
- Si sigue diferente, falla y genera 3 imágenes: Expected, Actual y Diff
Esto se ve en el call log del reporte:
- disabled all CSS animations
- waiting for fonts to load...
- fonts loaded
- 11964 pixels (ratio 0.02 of all image pixels) are different.
- waiting 100ms before taking screenshot
- captured a stable screenshot
- 11964 pixels (ratio 0.02 of all image pixels) are different.
No es una comparación cruda. Playwright hace esfuerzo para que el screenshot sea estable antes de comparar.
Contraste con Selenium
| Concepto | Selenium | Playwright |
|---|---|---|
| Screenshot de página | driver.getScreenshotAs() — solo captura, no compara |
toHaveScreenshot() — captura + compara + diff |
| Comparación pixel a pixel | Ashot o librerías externas | Nativo |
| Golden files | Gestión manual | Automática por spec, browser y plataforma |
| Diff visual | Implementación propia | Generado automáticamente en el reporte |
| Tolerancia | Configurar manualmente en la librería | maxDiffPixels, maxDiffPixelRatio, threshold |
| Screenshot de elemento | Ashot + coordenadas | expect(locator).toHaveScreenshot() |
| Estabilización | Implementar waits manualmente | Desactiva animations, espera fonts, reintenta |
| Update de referencia | Manual: reemplazar archivos | --update-snapshots |
En Selenium, visual testing es un proyecto aparte. En Playwright es una assertion.
Los tests finales
learning/visual-testing.spec.ts — 4 tests explorando el concepto:
- Screenshot de página completa — golden file, comparación pixel a pixel
- Mock con route.fulfill() — diff visual de 11,964 píxeles
- Screenshot de elemento — solo la grilla, selector específico para SlickGrid
- Mock con maxDiffPixels — misma diferencia pero dentro de tolerancia
e2e/visual-regression.spec.ts — 2 tests de producción:
- Página completa de clientes — layout general
- Contenido de la grilla — datos de la tabla

Estado actual
6 tests de visual testing entre learning y e2e. Golden files generadas para Chromium en Windows. toHaveScreenshot() comparando pixel a pixel, generando diffs visuales cuando algo cambia, con opciones de tolerancia para manejar variaciones esperadas.
Próximo post: CI/CD con GitHub Actions — configurar la suite para correr en push/PR y publicar el HTML reporter en GitHub Pages. El ciclo completo del framework.
—
🔗 Todo el código de esta serie está en: github.com/cesarbeassuarez/playwright-typescript-framework