<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Junior Diaz]]></title><description><![CDATA[Junior Diaz]]></description><link>https://blog.juniordiazbriceno.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Junior Diaz</title><link>https://blog.juniordiazbriceno.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 18 Jun 2026 03:53:26 GMT</lastBuildDate><atom:link href="https://blog.juniordiazbriceno.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Pruebas de Regresión Visual con Playwright: Detectando Cambios en la UI Automáticamente]]></title><description><![CDATA[Serie: Calidad y Accesibilidad en Aplicaciones Web — Artículo 1 de 3

Introducción
Cuando probamos aplicaciones web, solemos enfocarnos en la funcionalidad — pero ¿cómo garantizamos que la interfaz de]]></description><link>https://blog.juniordiazbriceno.dev/pruebas-de-regresi-n-visual-con-playwright-detectando-cambios-en-la-ui-autom-ticamente</link><guid isPermaLink="true">https://blog.juniordiazbriceno.dev/pruebas-de-regresi-n-visual-con-playwright-detectando-cambios-en-la-ui-autom-ticamente</guid><category><![CDATA[playwright]]></category><category><![CDATA[Testing]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[test-automation]]></category><category><![CDATA[QA]]></category><dc:creator><![CDATA[Junior Diaz Briceño]]></dc:creator><pubDate>Thu, 11 Jun 2026 01:59:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69fd57199f93a850a45529cb/4fc8c224-06b1-4777-948a-c637a6002247.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Serie: Calidad y Accesibilidad en Aplicaciones Web — Artículo 1 de 3</em></p>
<hr />
<h2>Introducción</h2>
<p>Cuando probamos aplicaciones web, solemos enfocarnos en la funcionalidad — pero ¿cómo garantizamos que la interfaz de usuario se vea correcta después de cada despliegue?</p>
<p>Ahí es donde entran las pruebas visuales.</p>
<p>Las pruebas visuales comparan automáticamente capturas de pantalla de la UI entre ejecuciones, ayudándonos a detectar cambios inesperados en el diseño, elementos faltantes o regresiones de estilos. Un cambio de padding, un botón que se achicó, un componente que se desplazó dos píxeles — ese tipo de cosas que pasan desapercibidas en code review pero que el equipo de UX y QA necesita detectar antes de que lleguen a producción.</p>
<p>Aunque existen herramientas dedicadas como Percy de BrowserStack que ofrecen capacidades avanzadas de comparación visual, tienen un límite de screenshots. La funcionalidad nativa de Playwright no tiene ese costo directo — los snapshots viven en tu repositorio. El "costo" real es el tamaño en disco y en tu historial de Git, algo que puedes gestionar con una estrategia de almacenamiento adecuada. Para equipos y clientes que priorizan herramientas open source, esto es un argumento sólido: obtienes comparación visual, evidencia histórica, soporte multi-browser y multi-breakpoint, todo dentro de la misma herramienta que ya usas para tus pruebas funcionales.</p>
<p>En este artículo voy a mostrarte cómo configurar pruebas de regresión visual con Playwright usando ejemplos reales de AutoCatalog, un catálogo de autos que construí en Next.js + TypeScript para experimentar con estos patrones.</p>
<p>Al finalizar, podrás:</p>
<ul>
<li><p>Capturar capturas base (baselines) de los componentes clave de la UI</p>
</li>
<li><p>Compararlas automáticamente en futuras ejecuciones</p>
</li>
<li><p>Detectar cambios visuales no deseados de forma temprana en tu pipeline de CI</p>
</li>
</ul>
<hr />
<h2>⚙️ Prerrequisitos</h2>
<p>Antes de comenzar, asegúrate de contar con:</p>
<ul>
<li><p>Node.js (versión LTS recomendada)</p>
</li>
<li><p>Un proyecto de Playwright ya configurado:</p>
</li>
</ul>
<pre><code class="language-bash">pnpm create playwright
</code></pre>
<ul>
<li>Conocimiento básico de pruebas en Playwright y del patrón Page Object Model (POM)</li>
</ul>
<hr />
<h2>Configuración de pruebas visuales</h2>
<h3>1. Aserciones básicas de capturas de pantalla</h3>
<p>Playwright incluye soporte nativo para comparaciones visuales mediante <code>expect(page).toHaveScreenshot()</code> o <code>expect(element).toHaveScreenshot()</code>.</p>
<p>La primera vez que ejecutas una prueba, Playwright guarda una imagen base (baseline). En ejecuciones posteriores, compara la nueva captura con la base y señala cualquier diferencia.</p>
<p>Para ilustrar el valor real de esto: imagina que alguien modifica el tamaño o el padding del botón "Add Product" — un cambio pequeño que fácilmente pasa desapercibido en code review. La prueba de regresión visual lo detecta de inmediato.</p>
<pre><code class="language-typescript">test('Product Management - Default layout page', async ({ page }) =&gt; {
    await test.step('Capture full-page baseline', async () =&gt; {
        await expect(page).toHaveScreenshot('manage_default_layout.png')
    })
})
</code></pre>
<p>Si el layout cambia, Playwright genera una imagen con las diferencias (diff) y el reporte incluye un slider interactivo para comparar el antes y el después:</p>
<blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/69fd57199f93a850a45529cb/f1300e42-7cc5-4906-9934-6432064dc450.gif" alt="Playwright visual regression report showing slider comparison — juniordiazbriceno.dev" style="display:block;margin:0 auto" />

<p>📸 El slider del reporte de Playwright permite comparar visualmente el baseline con el estado actual — ideal para detectar cambios sutiles de spacing o tamaño.</p>
</blockquote>
<hr />
<h3>2. Pruebas visuales a nivel de componente</h3>
<p>En lugar de capturar páginas completas, puedes enfocarte en componentes específicos o modales. Esto hace que los tests sean más estables: un cambio en el header no rompe la prueba del modal.</p>
<pre><code class="language-typescript">test('Product Management - Add Product modal default layout matches baseline', async ({ page }) =&gt; {
    await test.step('Open Add Product modal', async () =&gt; {
        await managePage.openAddProductModal()
        await managePage.expectAddProductModalVisible()
    })

    await test.step('Capture modal component screenshot', async () =&gt; {
        const productModal = managePage.modalTitle.locator('..')
        await expect(productModal).toHaveScreenshot(
            'add_product_modal_default.png'
        )
    })
})
</code></pre>
<p>El scope del screenshot es el modal en sí — no la página completa. Así, la prueba verifica que el layout, espaciado y elementos del modal se mantengan consistentes sin depender del resto de la UI.</p>
<blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/69fd57199f93a850a45529cb/cece800e-cb56-403d-8694-c4814158d04e.png" alt="Add Product modal snapshot baseline — AutoCatalog demo sit" style="display:block;margin:0 auto" />

<p>📸 Snapshot del modal Add Product — la prueba verifica que el layout, espaciado y elementos del modal se mantengan consistentes.</p>
</blockquote>
<hr />
<h3>3. Captura de snapshots en estados clave de la UI</h3>
<p>Las pruebas visuales son más efectivas cuando capturas estados relevantes de la interfaz, no cada interacción.</p>
<h4>El carrusel de la home: el problema del contenido dinámico</h4>
<p>Verificamos que el diseño de la home se vea correcto sin ruido generado por animaciones. El carrusel muestra imágenes en posiciones aleatorias en cada ejecución — sin <code>mask</code>, el test falla constantemente aunque el layout esté perfecto.</p>
<pre><code class="language-typescript">test('Home - Full page screenshot with carousel masked — stable test', async ({ page }) =&gt; {
    await test.step('Wait for carousels to be visible', async () =&gt; {
        await homePage.expectCarouselsVisible()
    })

    await test.step('Capture full page with all carousels masked', async () =&gt; {
        const allCarousels = await homePage.getAllCarousels()
        await expect(page).toHaveScreenshot('home_with_mask.png', {
            mask: allCarousels,
        })
    })
})
</code></pre>
<blockquote>
<p>👉 La opción <code>mask: Locator[]</code> ignora las áreas que cambian dinámicamente, como el carrusel.</p>
</blockquote>
<p>Veamos qué ocurre en ambos casos:</p>
<blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/69fd57199f93a850a45529cb/fe23fed3-d05e-43d7-b719-81089ce19a54.gif" alt="Sin mask: diff report with red highlights showing carousel differences — juniordiazbriceno.dev" style="display:block;margin:0 auto" />

<p>📸 <em>Sin mask: el test falla porque el carrusel muestra imágenes diferentes entre ejecuciones — Playwright detecta las diferencias y las marca en rojo.</em></p>
</blockquote>
<blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/69fd57199f93a850a45529cb/f74d2a62-ce3a-4fa1-906a-8524bb496cba.png" alt="Con mask aplicado: test passing with magenta masked areas — juniordiazbriceno.dev" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69fd57199f93a850a45529cb/f4649076-3cf4-4115-958d-c86d8f3f6141.png" alt="Reporte de Playwright mostrando el test aprobado con mask aplicado — 1 passed, 0 failed — juniordiazbriceno.dev" style="display:block;margin:0 auto" />

<p>📸 <em>Con mask aplicado: las áreas dinámicas son ignoradas y el test pasa de forma consistente.</em></p>
</blockquote>
<hr />
<h3>4. Gestión de imágenes base</h3>
<p>La primera vez que ejecutas las pruebas, Playwright guarda las capturas base en:</p>
<pre><code class="language-plaintext">/tests/screenshots/
</code></pre>
<p>Si un cambio visual es intencional, actualiza los snapshots con:</p>
<pre><code class="language-bash">pnpm exec playwright test --update-snapshots
</code></pre>
<blockquote>
<p>⚠️ <strong>Nota para CI:</strong> Evita ejecutar <code>--update-snapshots</code> de forma automática en tu pipeline. Reserva este comando para actualizaciones manuales y revisadas. Considera crear un workflow separado o protegerlo detrás de una confirmación explícita.</p>
</blockquote>
<p>Para revisar las diferencias:</p>
<ol>
<li><p>Abre la carpeta <code>test-results</code></p>
</li>
<li><p>Compara las imágenes: base, actual y diff</p>
</li>
<li><p>Aprueba o rechaza los cambios durante la revisión de código</p>
</li>
</ol>
<hr />
<h2>Buenas prácticas</h2>
<h3>1. Organiza las pruebas por funcionalidad</h3>
<p>Agrupa las pruebas visuales lógicamente usando <code>test.describe()</code>:</p>
<pre><code class="language-typescript">test.describe('@ProductManagement @Visual', () =&gt; {
    test.describe('@LayoutBaseline', () =&gt; {
        // Prueba del layout completo
    })

    test.describe('@ModalBaseline', () =&gt; {
        // Pruebas de componentes modales
    })
})
</code></pre>
<h3>2. Usa nombres descriptivos para los screenshots</h3>
<p>Nombra las capturas de forma clara para indicar qué están probando:</p>
<p>✅ <code>manage_default_layout.png</code><br />✅ <code>add_product_modal_masked_dropdowns.png</code><br />❌ <code>screenshot1.png</code></p>
<h3>3. Prepara correctamente el estado de la prueba</h3>
<p>Asegúrate de que la aplicación esté en el estado correcto antes de capturar una pantalla:</p>
<pre><code class="language-typescript">test.beforeEach(async ({ page }) =&gt; {
    poManager = new POManager(page)
    managePage = poManager.getManagePage()
    await managePage.navigateToManage()
    await managePage.expectManagePageLoaded()
})
</code></pre>
<h3>4. Usa <code>test.step</code> con snapshots</h3>
<p>Es posible tomar snapshots dentro de <code>test.step</code>, lo que permite capturar múltiples estados en un mismo test. La ventaja es que el test queda más cohesivo y el reporte de Playwright muestra exactamente qué paso falló. La contrapartida: si un step falla, los siguientes no se ejecutan — en algunos casos es mejor separar los tests.</p>
<pre><code class="language-typescript">test('Add Product modal — múltiples estados', async ({ page }) =&gt; {
    await test.step('Open modal', async () =&gt; {
        await managePage.openAddProductModal()
        await managePage.expectAddProductModalVisible()
    })

    await test.step('Capture default state', async () =&gt; {
        await expect(productModal).toHaveScreenshot('modal_default.png')
    })

    await test.step('Capture focus on submit button', async () =&gt; {
        await managePage.activateTabKeyboard()
        await managePage.focusModalSubmitButton()
        await expect(productModal).toHaveScreenshot('modal_submit_focus.png')
    })
})
</code></pre>
<h3>5. Prueba diseños responsivos</h3>
<p>Verifica que los layouts responsivos funcionen correctamente en distintos tamaños de pantalla:</p>
<pre><code class="language-typescript">test('Home - mobile layout', async ({ page }) =&gt; {
    await page.setViewportSize({ width: 375, height: 667 })
    await expect(page).toHaveScreenshot('home-mobile.png')
})
</code></pre>
<hr />
<h2>🧠 Solución de problemas frecuentes</h2>
<table>
<thead>
<tr>
<th>Problema</th>
<th>Solución</th>
</tr>
</thead>
<tbody><tr>
<td>Diferencias mínimas de píxeles entre ejecuciones</td>
<td>Diferencias mínimas de píxeles entre ejecuciones → Identifica la causa raíz: usa <code>mask</code> para contenido dinámico, <code>addStyleTag</code> para scrollbars, o espera explícita para elementos que aún están renderizando. Evita <code>maxDiffPixelRatio</code> — acepta píxeles incorrectos como válidos y puede ocultar regresiones reales.</td>
</tr>
<tr>
<td>Contenido dinámico (carruseles, timestamps, animaciones)</td>
<td>Usa la opción <code>mask</code> para excluirlo del snapshot</td>
</tr>
<tr>
<td>Falsos positivos en CI</td>
<td>Asegura resolución de pantalla y versión del navegador consistentes entre entornos</td>
</tr>
<tr>
<td>Scrollbars que generan diffs</td>
<td>Ocúltalos con <code>addStyleTag</code> antes del screenshot</td>
</tr>
</tbody></table>
<blockquote>
<p>⚠️ Sobre <code>maxDiffPixelRatio</code>: Aunque Playwright ofrece esta opción para tolerar diferencias mínimas de píxeles, úsala con precaución. Un valor de <code>0.01</code> puede parecer pequeño, pero en una pantalla de 1280×720 equivale a más de 9.000 píxeles — suficiente para ocultar un botón desalineado o un cambio de color. En la mayoría de los casos, la solución correcta es eliminar la fuente de inestabilidad, no tolerarla.</p>
</blockquote>
<hr />
<h2>🎯 Conclusión</h2>
<p>Al combinar las pruebas funcionales de Playwright con aserciones visuales, puedes detectar y prevenir con confianza cambios no deseados en la UI.</p>
<p>Este enfoque le da a tu equipo de QA y desarrollo una capa de seguridad adicional: garantizando que tanto el comportamiento como la apariencia de la aplicación se mantengan consistentes con el tiempo.</p>
<p><strong>Puntos clave:</strong></p>
<ul>
<li><p>Usa <code>toHaveScreenshot()</code> para capturar y comparar estados de la UI</p>
</li>
<li><p>Enmascara elementos dinámicos para obtener diffs confiables</p>
</li>
<li><p>Versiona las imágenes base y revisa los cambios a través del proceso de revisión de código</p>
</li>
<li><p>Evita actualizar snapshots automáticamente en CI sin una revisión explícita</p>
</li>
<li><p>Usa <code>test.step</code> para organizar tests con múltiples capturas y obtener reportes más granulares</p>
</li>
</ul>
<hr />
<p>Este es el primero de una serie sobre QA automation — próximamente:</p>
<ul>
<li><p><em>Automatizando Pruebas de Accesibilidad Web: Combinando axe-core y Playwright</em></p>
</li>
<li><p><em>WCAG 2.4.7 Foco Visible: Pruebas de Regresión Visual con Playwright</em></p>
</li>
</ul>
<hr />
<p>¿Tu equipo tiene cobertura de regresión visual? Si quieres explorar cómo implementar este tipo de testing en tu proyecto, <a href="https://form.typeform.com/to/pUJwPnOw?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=visual-testing&amp;utm_content=es">cuéntame sobre tu equipo aquí</a>.</p>
<hr />
<h2>📚 Lecturas adicionales</h2>
<ul>
<li><p><a href="https://playwright.dev/docs/screenshots">Documentación de comparaciones visuales de Playwright</a></p>
</li>
<li><p><a href="https://playwright.dev/docs/test-assertions">Aserciones de pruebas en Playwright</a></p>
</li>
</ul>
]]></content:encoded></item></channel></rss>