toHaveScreenshot() crea una captura de pantalla de referencia en la primera ejecución y falla en cualquier ejecución posterior donde la diferencia de píxeles supere un umbral. El primer fallo más común en CI no tiene nada que ver con una regresión real: las referencias generadas en macOS no coinciden con lo que renderiza el CI en Linux, porque el hinting de fuentes y el renderizado sub-píxel difieren entre plataformas. Este artículo cubre la configuración de umbrales, el enmascarado de contenido dinámico como timestamps y avatares, la generación de referencias compatibles con Linux usando la imagen Docker de Playwright, y cuándo una herramienta comercial como Applitools vale el costo frente al enfoque integrado.

Qué es el testing de regresión visual

Un test de regresión visual captura una captura de pantalla de una página o elemento, la guarda como referencia, y luego compara cada ejecución futura contra esa referencia píxel por píxel. Si la diferencia supera un umbral configurable, el test falla y muestra exactamente qué píxeles cambiaron.

La distinción con una captura de pantalla normal es importante. Tomar una captura con page.screenshot() solo guarda un archivo. Nunca falla. No te dice nada sobre si la página se ve correcta. El testing de regresión visual requiere una referencia (la imagen acordada de "así es como debería verse") y una comparación automatizada contra esa referencia en cada ejecución.

El valor es real. Atrapás regresiones de layout que ninguna aserción funcional detectaría: un cambio de CSS que mueve un modal cinco píxeles a la izquierda, un bug de z-index que oculta un dropdown detrás de un banner, una implementación de modo oscuro que invierte accidentalmente un logo. Estos son los bugs que se cuelan en el code review porque quienes revisan se enfocan en la lógica, no en los píxeles.

El desafío también es real. Las capturas son sensibles. Una diferencia de un píxel en el antialiasing entre macOS y Linux, un timestamp dinámico en la página, un anuncio con contenido rotativo: cualquiera de estos genera falsos positivos. Gestionar ese ruido es la mayor parte del trabajo práctico en el testing de regresión visual.

toHaveScreenshot(): la aserción integrada

La aserción visual de Playwright es expect(locator).toHaveScreenshot() o expect(page).toHaveScreenshot(). Puedes capturar la página completa o acotar la captura a cualquier locator.

// tests/visual/homepage.spec.ts
import { test, expect } from '@playwright/test';

test('la homepage coincide con la referencia', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');

  // Captura de página completa
  await expect(page).toHaveScreenshot('homepage.png');
});

test('el botón de login coincide con la referencia', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');

  // Captura acotada a un elemento específico
  const loginButton = page.getByRole('button', { name: 'Login' });
  await expect(loginButton).toHaveScreenshot('login-button.png');
});

El argumento de nombre ('homepage.png') es opcional. Si lo omites, Playwright genera un nombre automáticamente a partir del título del test y un contador. Proveer un nombre explícito hace que los archivos de referencia sean más fáciles de encontrar y entender al revisarlos después.

En la primera ejecución, no hay referencia con qué comparar. Playwright crea una.

Generar capturas de referencia en la primera ejecución

Ejecuta los tests por primera vez y verás errores así:

Error: A snapshot doesn't exist at tests/visual/homepage.spec.ts-snapshots/homepage-chromium-darwin.png, writing actual.

Es lo esperado. Playwright te dice que escribió el archivo de referencia y te pide que lo revises y lo confirmes en el repositorio. El test falla en la primera ejecución a propósito. Playwright no crea una referencia silenciosamente sin que lo sepas.

Después de la primera ejecución, el proyecto tendrá un directorio de snapshots:

tests/
  visual/
    homepage.spec.ts
    homepage.spec.ts-snapshots/
      homepage-chromium-darwin.png
      homepage-chromium-linux.png
      login-button-chromium-darwin.png

Revisa esas imágenes. Si se ven correctas, confírmalas en el repositorio. Ahora son la referencia. Cada ejecución posterior compara contra estos archivos commiteados.

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  // Dónde se guardan los archivos de snapshot. Por defecto, junto al archivo spec.
  snapshotDir: './tests/__snapshots__',
  use: {
    baseURL: 'https://lab.becomeqa.com',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

Puedes centralizar todos los snapshots en un único directorio con snapshotDir en la config, algo que algunos equipos prefieren para una organización más limpia del repositorio.

Actualizar referencias con --update-snapshots

La app cambia. El diseño cambia. Cuando un cambio visual es intencional, hay que actualizar la referencia:

npx playwright test --update-snapshots

Esto sobreescribe todos los snapshots existentes con nuevas capturas. Cada test que se ejecutó tendrá su estado actual como nueva referencia.

Si solo quieres actualizar los snapshots de un archivo de tests:

npx playwright test tests/visual/homepage.spec.ts --update-snapshots

O para un test específico por nombre:

npx playwright test --update-snapshots -g "la homepage coincide con la referencia"

Trata --update-snapshots con la misma precaución que git push --force. Ejecutarlo descuidadamente sobreescribe referencias legítimas con estados rotos. Siempre revisa las imágenes actualizadas antes de commitearlas. En CI, el flag nunca debe activarse automáticamente. Solo debe ejecutarse en respuesta a una acción deliberada del desarrollador.

Después de actualizar, confirmas los archivos .png modificados en el repositorio. El diff en el code review mostrará las imágenes antes y después, que es exactamente el lugar correcto para detectar cambios visuales no deseados.

Configurar umbrales de comparación

La comparación píxel perfecto funciona bien en un entorno controlado y genera ruido constante en cualquier otro lugar. Playwright te da tres opciones de umbral para gestionar la sensibilidad.

test('la tarjeta de producto coincide con la referencia', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com/products');

  const productCard = page.locator('.product-card').first();

  await expect(productCard).toHaveScreenshot('product-card.png', {
    // Número máximo de píxeles que pueden diferir
    maxDiffPixels: 100,

    // Proporción máxima de píxeles diferentes (0 a 1). 0.01 = 1% del total
    maxDiffPixelRatio: 0.01,

    // Umbral de diferencia de color por píxel (0 a 1). Mayor = más tolerante
    threshold: 0.2,
  });
});

threshold controla cuánto debe diferir un píxel para contarse como "diferente". El valor por defecto es 0.2, que maneja diferencias menores de antialiasing y renderizado sub-píxel entre plataformas. Súbelo a 0.3 o 0.4 en componentes con muchas curvas o degradados donde el renderizado varía levemente según la plataforma. maxDiffPixels es un conteo absoluto. Úsalo para componentes pequeños y acotados donde sabes que algunos píxeles pueden variar (renderizado de íconos, radio de borde), pero un desplazamiento de 50 píxeles siempre debe fallar. maxDiffPixelRatio es un porcentaje del total de píxeles. Úsalo para capturas de página completa donde el conteo total de píxeles es grande. maxDiffPixels: 100 en una página de 1920x1080 es extremadamente estricto, pero maxDiffPixelRatio: 0.001 da una tolerancia razonable.

Puedes establecer valores por defecto en playwright.config.ts para no repetir los mismos umbrales en cada test:

// playwright.config.ts
export default defineConfig({
  expect: {
    toHaveScreenshot: {
      maxDiffPixels: 100,
      threshold: 0.2,
    },
  },
});

Los tests individuales pueden seguir sobreescribiendo estos valores si necesitan una sensibilidad diferente.

Enmascarar contenido dinámico

El contenido dinámico es la mayor fuente de falsos positivos en los tests de regresión visual. Un timestamp que se actualiza cada segundo, un avatar de usuario cargado desde un CDN, un banner publicitario rotativo: cualquiera de estos genera un diff en cada ejecución.

La opción mask de Playwright acepta un array de locators. Esas regiones se pintan con un color sólido antes de que se realice la comparación.

test('el dashboard coincide con la referencia', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com/dashboard');

  await expect(page).toHaveScreenshot('dashboard.png', {
    mask: [
      // Enmascarar el timestamp "Última actualización" del encabezado
      page.locator('[data-testid="last-updated-timestamp"]'),

      // Enmascarar el avatar del usuario (diferente para cada usuario)
      page.locator('[data-testid="user-avatar"]'),

      // Enmascarar los contenedores de anuncios de terceros
      page.locator('.ad-container'),
    ],
    // Personalizar el color de la máscara (por defecto es magenta)
    maskColor: '#FF00FF',
  });
});

Las regiones enmascaradas aparecen en la comparación como un bloque sólido de color. La comparación sigue ejecutándose sobre toda la captura. Las áreas enmascaradas siempre coinciden porque tanto la captura real como la esperada tienen la misma máscara aplicada.

Agrega atributos data-testid al contenido dinámico específicamente para poder enmascararlo de forma confiable en los tests visuales. Seleccionar por nombre de clase funciona, pero los nombres de clase cambian. Un data-testid="user-avatar" es estable y comunica claramente su propósito a quien lee el test.

Para animaciones, puedes usar animations: 'disabled' para detener las animaciones CSS antes de tomar la captura:

test('la sección hero animada coincide con la referencia', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');

  await expect(page).toHaveScreenshot('hero.png', {
    animations: 'disabled',
  });
});

Esto congela las transiciones y animaciones CSS en su estado inicial, lo que hace que los componentes animados sean deterministas. Para animaciones controladas por JavaScript que no usan transiciones CSS, puede que necesites esperar a que la animación termine o agregar un waitForLoadState('networkidle') antes de la aserción.

Nombres de snapshots y organización multiplataforma

Mira el nombre de archivo que genera Playwright: homepage-chromium-darwin.png. El navegador y el sistema operativo están incorporados en el nombre. No es casualidad.

La misma página renderizada en Chromium en macOS frente a Chromium en Linux produce píxeles sutilmente diferentes. El hinting de fuentes, el renderizado sub-píxel y pequeñas diferencias en cómo cada SO compone los gráficos hacen que no puedas compartir una sola imagen de referencia entre plataformas. Playwright maneja esto creando referencias separadas para cada combinación navegador/SO.

tests/__snapshots__/
  homepage.spec.ts/
    homepage-chromium-darwin.png   (Chrome en macOS)
    homepage-chromium-linux.png    (Chrome en Linux)
    homepage-firefox-linux.png     (Firefox en Linux)
    homepage-webkit-darwin.png     (Safari en macOS)

Controlas el patrón de nombres con la opción snapshotPathTemplate en playwright.config.ts:

// playwright.config.ts
export default defineConfig({
  snapshotPathTemplate:
    '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}-{projectName}-{platform}{ext}',
});

Los tokens disponibles son:

  • {arg}: el nombre que pasaste a toHaveScreenshot()
  • {projectName}: el nombre del proyecto en la config (ej.: chromium, firefox)
  • {platform}: el SO (darwin, linux, win32)
  • {testFileName}: el nombre del archivo spec sin extensión
  • {snapshotDir}: el directorio base de snapshots

Deja {platform} en la plantilla. Quitarlo e intentar compartir una referencia entre sistemas operativos es el error más frecuente al configurar tests visuales por primera vez, y genera fallos continuos en CI.

Ejecutar tests visuales en CI

Ejecutar tests visuales en CI revela el problema multiplataforma de inmediato. Las referencias se generaron en macOS. El pipeline de CI corre en Linux. Los snapshots no coinciden.

La solución más limpia es generar las referencias dentro del mismo contenedor Docker que usa el CI. Playwright provee imágenes Docker oficiales:

# .github/workflows/visual-tests.yml
name: Tests Visuales

on: [push, pull_request]

jobs:
  visual:
    runs-on: ubuntu-latest
    container:
      image: mcr.microsoft.com/playwright:v1.44.0-jammy

    steps:
      - uses: actions/checkout@v4

      - name: Instalar dependencias
        run: npm ci

      - name: Ejecutar tests visuales
        run: npx playwright test tests/visual/

      - name: Subir reporte de diff ante fallos
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-visual-report
          path: playwright-report/
          retention-days: 7

Cuando los tests fallan en CI, el reporte subido contiene la captura real, la referencia esperada y una imagen de diff que resalta exactamente qué píxeles cambiaron. Así distinguís una regresión visual real de una diferencia de entorno.

Para generar referencias de Linux desde una computadora macOS sin cambiar a Linux, ejecuta el contenedor Docker de Playwright en local:

# Generar referencias compatibles con Linux desde tu Mac
docker run --rm \
  -v "$(pwd):/work" \
  -w /work \
  mcr.microsoft.com/playwright:v1.44.0-jammy \
  npx playwright test tests/visual/ --update-snapshots

Esto escribe nuevos archivos de snapshot *-linux.png que coincidirán con lo que produce el CI. Commitéalos y los fallos de CI por diferencias de plataforma desaparecen.

Un patrón común en CI es ejecutar los tests visuales en un job o proyecto separado, condicionado a que la suite de tests funcionales pase primero. Los tests visuales son más lentos que los funcionales y sus fallos son más ruidosos, así que mantenerlos en un paso separado del pipeline evita que bloqueen el feedback rápido sobre regresiones funcionales:

// playwright.config.ts
export default defineConfig({
  projects: [
    // Los tests funcionales se ejecutan primero
    {
      name: 'functional',
      testMatch: 'tests/functional/**/*.spec.ts',
    },
    // Los tests visuales se ejecutan después de los funcionales
    {
      name: 'visual',
      testMatch: 'tests/visual/**/*.spec.ts',
      dependencies: ['functional'],
    },
  ],
});

Playwright integrado vs Applitools y Percy

El testing visual integrado de Playwright cubre mucho terreno. Pero herramientas comerciales como Applitools Eyes y Percy existen por razones que vale la pena entender.

La limitación central del enfoque integrado es la gestión de snapshots. Cada imagen de referencia vive en el repositorio. Un proyecto con 50 tests visuales, 3 navegadores y 2 plataformas genera 300 archivos PNG. A medida que crecen los casos de prueba, el repositorio crece también. Revisar cambios visuales en un pull request significa ver diffs de imágenes en la interfaz de GitHub, que funciona pero no es ideal para imágenes grandes o cambios sutiles.

Applitools y Percy resuelven esto con almacenamiento en la nube para las referencias, interfaces de revisión dedicadas para diffs visuales, comparación inteligente basada en IA que distingue desplazamientos de layout de cambios de contenido, y flujos de trabajo en equipo para aprobar o rechazar cambios visuales.

| | Playwright integrado | Applitools / Percy |

|---|---|---|

| Costo | Gratis | Pago (hay nivel gratuito) |

| Configuración | Minutos | Minutos + API key |

| Almacenamiento de referencias | Repositorio git | Nube |

| Interfaz de revisión de diffs | Reporte HTML de Playwright | Interfaz cloud dedicada |

| Comparación con IA | No | Sí (Applitools) |

| Referencias multiplataforma | Archivos separados por navegador/SO | Unificadas con normalización |

| Snapshots en CI | Requiere imagen Docker coincidente | Lo gestiona el servicio |

Para un proyecto individual o equipo pequeño, el enfoque integrado es el punto de partida correcto. Es gratis, rápido de configurar y maneja bien los casos comunes. El flujo con Docker gestiona el problema multiplataforma de forma adecuada una vez que lo configuraste.

Para equipos más grandes, especialmente cuando varias personas necesitan revisar y aprobar cambios visuales, la fricción de gestionar archivos PNG en git y revisar diffs en GitHub se vuelve real. Ahí es cuando un servicio dedicado empieza a justificar su costo. Pagas tanto por el flujo de revisión como por la tecnología de comparación.

Applitools también ofrece una integración con Playwright que reemplaza toHaveScreenshot() con las llamadas eyes.check() de Applitools, así que migrar es cuestión de actualizar un import y cambiar la llamada a la aserción, no de reescribir los tests.

Preguntas frecuentes

¿Cómo ejecuto solo los tests visuales sin correr toda la suite?

Usa el flag --grep u organiza los tests visuales en su propio directorio y apunta Playwright ahí: npx playwright test tests/visual/. Si usas proyectos en la config, npx playwright test --project=visual ejecuta solo el proyecto visual.

Los snapshots siguen fallando por un spinner de carga que aparece a veces. ¿Qué hago?

Espera a que el spinner desaparezca antes de hacer la aserción: await page.locator('[data-testid="loading-spinner"]').waitFor({ state: 'hidden' }). También puedes enmascararlo. El enfoque de máscara es más robusto. Si los tiempos cambian, una máscara sigue funcionando, pero un waitFor con un timeout ajustado puede que no.

¿Puedo usar toHaveScreenshot() para tests en viewport móvil?

Sí. Configura el viewport en la config del proyecto o en el test: await page.setViewportSize({ width: 375, height: 812 }). Playwright tratará las capturas en móvil y escritorio como referencias separadas si se capturan en tests o proyectos distintos.

¿Cuántos tests visuales debería escribir?

Menos de los que piensas. Los tests visuales son mejores para componentes y páginas donde el resultado visual es genuinamente parte de la especificación: los estados de botones de un sistema de diseño, una visualización de datos, una vista previa de exportación PDF. Intentar cubrir cada página visualmente genera una carga de mantenimiento que los equipos suelen abandonar en pocos meses.

¿Puedo capturar un componente de forma aislada sin navegar a una página?

No directamente con Playwright. Es una herramienta basada en navegador que opera sobre páginas completas. Para testing visual de componentes en aislamiento, Storybook con Chromatic es la herramienta más apropiada. Los tests visuales de Playwright funcionan mejor a nivel de integración: páginas reales en un navegador real.

→ See also: Fixtures Personalizados en Playwright: El Patrón que Hace los Tests Legibles | Archivo de Configuración de Playwright Explicado: Todas las Opciones | Depurando Tests Inestables: Una Guía Práctica | Testing Visual de Regresión con IA: Más Allá de las Capturas Pixel-Perfectas | Pruebas Entre Navegadores con Playwright: Chrome, Firefox, Safari