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.

Reporte Playwright Image mismatch diff 1280x720 píxeles diferentes en rojo naranja, test steps toHaveScreenshot 1.1s failed, links diff actual expected
El diff marca cada pixel que cambió. Toda el área de datos es diferente entre 91 reales y 2 inventados.

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:

VS Code mostrando visual-testing.spec.ts con test screenshot de grilla de clientes, storageState, waitFor slick-row, toHaveScreenshot grilla-clientes.png
El test más simple de visual testing: navegar, esperar la grilla, comparar screenshot.

Primera ejecución: fallo esperado

Terminal Playwright error A snapshot doesn't exist grilla-clientes-chromium-win32.png writing actual, 1 failed 1 passed 20.3s
Primera ejecución: Playwright no tiene referencia. Toma el screenshot actual y avisa que falta la golden file.

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.

Terminal Playwright 2 passed 16 segundos con flag update-snapshots generando golden file de referencia
--update-snapshots toma el screenshot actual como referencia. Ahora Playwright tiene contra qué comparar.

Segunda ejecución: verde

Ahora sin --update-snapshots:

npx playwright test visual-testing --project=chromium
Reporte Playwright test screenshot de grilla de clientes passed 5.2s, navigate 4.2s, waitFor 130ms, toHaveScreenshot 538ms todos verdes
toHaveScreenshot() en 538ms. Navegar, esperar la grilla, capturar, comparar pixel a pixel. Verde.

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:

VS Code test grilla con datos mockeados route.fulfill dos clientes Empresa Inventada SA y Playwright Mock Corp, toHaveScreenshot grilla-clientes.png
route.fulfill() inyectando 2 clientes inventados. Compara contra la golden de 91 reales — tiene que fallar.

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

El diff

Reporte Playwright error toHaveScreenshot failed 11964 pixels ratio 0.02, call log disabled CSS animations fonts loaded, side by side expected vs actual
11,964 píxeles diferentes. Playwright desactivó animations, esperó fonts, reintentó — la diferencia es real.

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.

Reporte Playwright Image mismatch diff 1280x720 píxeles diferentes en rojo naranja, test steps toHaveScreenshot 1.1s failed, links diff actual expected
El diff marca cada pixel que cambió. Toda el área de datos es diferente entre 91 reales y 2 inventados.

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.

Terminal Playwright strict mode violation slick-viewport resolved to 9 elements sg-top sg-body sg-bottom sg-start sg-main sg-end, 1 failed 4 passed
.slick-viewport resolvió a 9 elementos. SlickGrid siempre tiene múltiples viewports — hay que usar .sg-body.sg-main.

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:

VS Code test screenshot solo de la grilla locator slick-viewport.sg-body.sg-main waitFor slick-row toHaveScreenshot grilla-solo.png
Selector corregido: .sg-body.sg-main apunta al viewport principal de datos de SlickGrid.

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"?

VS Code test mock con tolerancia alta route.fulfill dos clientes toHaveScreenshot grilla-clientes.png maxDiffPixels 15000
Misma golden, mismos datos mockeados, pero con tolerancia de 15,000 píxeles. 11,964 < 15,000 — pasa.

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

Reporte HTML Playwright 5 tests 4 passed 1 failed screenshot solo de la grilla rojo, mock tolerancia verde, chromium 28.8s
El strict mode violation en screenshot solo de la grilla. Los otros 4 tests pasaron, incluido el mock con tolerancia.

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:

VS Code visual-regression.spec.ts e2e con beforeEach fixtures, golden file clientes-page-chromium-win32.png preview grilla Serenity 91 clientes
Tests de e2e con fixtures de producción. A la derecha, la golden file: la referencia visual contra la que se compara.

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.

Reporte HTML Playwright 3 passed e2e visual-regression.spec.ts grilla clientes mantiene layout y contenido grilla mantiene layout chromium 7.8s
2 tests de visual regression en producción. Si un CSS cambia, estos tests lo detectan.

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():

  1. Desactiva CSS animations (para evitar capturas en medio de una transición)
  2. Espera que los fonts carguen (para que el texto se renderice correctamente)
  3. Toma el screenshot
  4. Compara contra la golden file pixel a pixel
  5. Si hay diferencia, espera 100ms y toma otro screenshot
  6. 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:

  1. Screenshot de página completa — golden file, comparación pixel a pixel
  2. Mock con route.fulfill() — diff visual de 11,964 píxeles
  3. Screenshot de elemento — solo la grilla, selector específico para SlickGrid
  4. Mock con maxDiffPixels — misma diferencia pero dentro de tolerancia

e2e/visual-regression.spec.ts — 2 tests de producción:

  1. Página completa de clientes — layout general
  2. Contenido de la grilla — datos de la tabla
VS Code visual-regression.spec.ts con dos golden files preview clientes-grilla solo tabla y clientes-page página completa lado a lado
Las dos golden files de e2e: grilla sola (izquierda) y página completa (derecha). Cada una protege un nivel diferente de layout.

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