Playwright en GitHub Actions
Publicado el
Playwright en GitHub Actions: E2E y regresión visual en Deploy Previews
Seguro que te ha pasado: abres un pull request, Netlify te genera un Deploy Preview, todo parece “verde”… y al hacer merge alguien descubre que se ha roto un flujo crítico (login, checkout, formulario, navegación, etc.).
La buena noticia es que, si ya usas Astro + Netlify, tienes el escenario perfecto para automatizar:
- Tests E2E reales (sobre el entorno de preview, no sobre mocks).
- Regresión visual con snapshots (capturas comparadas en cada PR).
- Reportes, trazas y capturas como artefactos descargables en GitHub Actions.
- Y un “semáforo” que bloquea el merge si algo se rompe.

1. El patrón: “PR → Deploy Preview → Playwright → merge seguro”
En proyectos modernos, el Deploy Preview no es solo “para que lo vea producto”: es tu entorno de QA automático.
La idea clave es esta:
- Abres PR → Netlify construye un Deploy Preview.
- GitHub Actions espera a que el preview esté “ready”.
- Playwright ejecuta:
- E2E (flujos críticos).
- Visual regression (capturas clave).
- Si falla algo, se suben pruebas:
- HTML report.
- Trace Viewer (muy útil para depurar).
- Capturas/vídeos (si los activas).
- El check queda rojo → no hay merge (si lo marcas como “required” en branch protection).
“Si un PR no puede demostrar que funciona en un Deploy Preview, no debería llegar a producción por accidente.”
2. Preparar Playwright para probar un Deploy Preview (sin servidor local)
Cuando pruebas sobre un Deploy Preview, no necesitas levantar astro dev en CI. Solo necesitas que Playwright use como baseURL la URL del preview.
2.1 Dependencias y scripts mínimos
Instala Playwright:
npm i -D @playwright/test
npx playwright install
Añade scripts útiles en package.json:
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:update": "playwright test --update-snapshots"
}
}
2.2 Config recomendada: baseURL por variable + trazas en CI
Ejemplo de playwright.config.ts (ajústalo a tu repo):
import { defineConfig, devices } from '@playwright/test';
const isCI = !!process.env.CI;
export default defineConfig({
testDir: './tests/e2e',
// Importante: en CI solemos permitir 1-2 reintentos controlados para reducir flakiness.
retries: isCI ? 2 : 0,
// Manténlo razonable para no “tapar” errores reales.
timeout: 60_000,
expect: { timeout: 10_000 },
// Reporte HTML (en CI lo subiremos como artefacto)
reporter: [
['list'],
['html', { open: 'never', outputFolder: 'playwright-report' }],
],
use: {
// La URL vendrá del workflow: BASE_URL = Deploy Preview
baseURL: process.env.BASE_URL,
// “Pruebas” para depurar: trazas/capturas solo si falla
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: isCI ? 'on-first-retry' : 'retain-on-failure',
// Recomendable para estabilidad visual
viewport: { width: 1280, height: 720 },
// Si tu app usa mucha animación, considera desactivarla en tests (ver sección 5).
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
3. Obtener la URL del Deploy Preview (sin “pegamento” frágil)
Aquí está el punto que más suele fallar: cómo saber qué URL probar.
Tienes dos enfoques principales:
Opción A (recomendada): leer la URL desde el “commit status” de Netlify
Cuando Netlify genera el Deploy Preview, normalmente publica un status en el commit del PR con un target_url apuntando al preview. Eso significa que puedes resolver la URL solo con GITHUB_TOKEN.
Ventajas:
- No necesitas token de Netlify.
- La URL siempre coincide con lo que Netlify ha publicado para ese commit.
- Menos moving parts.
Opción B: usar la API de Netlify (si necesitas más control)
Si quieres filtrar por contexto, rama, etc., puedes llamar a la API de Netlify con NETLIFY_AUTH_TOKEN + NETLIFY_SITE_ID y buscar el deploy más reciente del PR.
Ventajas:
- Control total si tienes flujos complejos (varios sites, deploy contexts, alias, etc.).
- Útil si el status de GitHub no está disponible por tu configuración.
En el workflow de ejemplo iremos con Opción A y dejaremos la B como plan de contingencia.
4. Workflow completo: matriz de navegadores, cachés y artefactos
Este workflow hace lo siguiente:
- Se dispara en
pull_request. - Espera a que el Deploy Preview esté listo (polling del status).
- Ejecuta Playwright por navegador (matriz).
- Sube reportes y evidencias como artefactos.
4.1 Estructura mínima en el repo
Estructura sugerida para E2E + snapshots
No es obligatoria; busca consistencia y revisión sencilla en PRs.
/
- .github/workflows/e2e-deploy-preview.yml
- playwright.config.ts
tests/
e2e/
- smoke.spec.ts
- checkout.visual.spec.ts
4.2 Workflow e2e-deploy-preview.yml
name: E2E (Playwright) on Netlify Deploy Preview
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
concurrency:
group: e2e-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: read
statuses: read
jobs:
e2e:
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
project: [chromium, firefox, webkit]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
# Cache de navegadores de Playwright (acelera muchísimo)
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-ms-playwright-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-ms-playwright-
- name: Install Playwright browsers (with deps)
run: npx playwright install --with-deps
- name: Resolve Netlify Deploy Preview URL
id: preview
uses: actions/github-script@v7
with:
script: |
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
const { owner, repo } = context.repo;
const sha = context.payload.pull_request.head.sha;
// Ajusta si tu status tiene otro patrón; esta heurística suele ser suficiente.
const isPreviewUrl = (u) =>
typeof u === 'string' && (u.includes('deploy-preview') || u.includes('.netlify.app'));
for (let attempt = 1; attempt <= 30; attempt++) {
const { data: statuses } = await github.rest.repos.listCommitStatusesForRef({
owner,
repo,
ref: sha,
per_page: 100,
});
// Preferimos el status que apunte a deploy-preview y esté "success"
const candidate = statuses.find((s) => {
const ctx = (s.context || '').toLowerCase();
return ctx.includes('netlify') && isPreviewUrl(s.target_url);
});
if (candidate?.state === 'success' && isPreviewUrl(candidate.target_url)) {
core.setOutput('url', candidate.target_url);
core.info(`Deploy Preview listo: ${candidate.target_url}`);
return;
}
core.info(
`Intento ${attempt}/30: aún no está listo (status: ${candidate?.state || 'no encontrado'}).`
);
await delay(20_000);
}
core.setFailed(
'No se encontró un Deploy Preview listo. Revisa que Netlify publique el status en el PR o aumenta el tiempo de espera.'
);
- name: Export BASE_URL
run: echo "BASE_URL=${{ steps.preview.outputs.url }}" >> $GITHUB_ENV
- name: Run Playwright tests
env:
PLAYWRIGHT_HTML_OUTPUT_DIR: playwright-report
PLAYWRIGHT_HTML_OPEN: never
run: npx playwright test --project=${{ matrix.project }}
- name: Upload Playwright report (HTML)
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report-${{ matrix.project }}
path: playwright-report
retention-days: 7
- name: Upload test results (traces, screenshots, videos)
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-test-results-${{ matrix.project }}
path: test-results
retention-days: 7
4.3 Hacer que “bloquee el merge”
El workflow por sí solo no bloquea nada. Lo bloquea tu repositorio cuando lo marcas como Required status check en la rama principal (Branch protection).
En la práctica:
- Si el job falla → el check queda rojo.
- Si el check es requerido → no puedes hacer merge.
5. Regresión visual con snapshots: cómo hacerlo bien (y sin falsas alarmas)
La regresión visual es potente, pero solo funciona si la imagen es estable.
5.1 Qué es un snapshot en Playwright
Cuando haces algo como:
import { test, expect } from '@playwright/test';
test('checkout: no se rompe el resumen', async ({ page }) => {
await page.goto('/checkout');
await expect(page.locator('[data-testid="order-summary"]')).toHaveScreenshot();
});
Playwright crea (o compara) una imagen en tu repo. En CI:
- Si la UI cambió, verás diffs.
- Si está igual, pasa.
5.2 Cuándo actualizar snapshots
Actualiza snapshots cuando:
- Has cambiado UI a propósito (rediseño, copy, spacing).
- Has corregido un bug visual y quieres “congelar” el nuevo comportamiento.
No actualices snapshots para “hacer que el CI se calle”. Si el snapshot falla, primero pregunta: ¿es un cambio esperado o un bug?
5.3 Revisión segura de diffs (la parte humana)
En un PR, lo sano es:
- El autor actualiza snapshots localmente (
npm run test:e2e:update) y commitea. - El revisor compara:
- Captura “antes vs después” (si subes artefactos o usas el diff de Playwright).
- Contexto del cambio en el PR.
5.4 Técnicas anti-falsos-positivos
Checklist rápido para estabilizar visual regression:
- Desactiva animaciones en tests (CSS global inyectado).
- Fija
viewporty evita layouts responsive si no es el objetivo. - Evita datos dinámicos (fechas, IDs, “hoy”, contadores).
- Si hay zonas dinámicas inevitables, usa
masko captura un componente concreto (no toda la página). - Usa
toHaveScreenshot()sobre un locator estable (nopageentero, salvo en smoke visual).
Ejemplo de “apagado de animaciones”:
test.beforeEach(async ({ page }) => {
await page.addStyleTag({
content: `
*, *::before, *::after {
transition: none !important;
animation: none !important;
caret-color: transparent !important;
}
`,
});
});
6. Reportes, trazas y capturas: que depurar sea barato
Cuando un test falla en CI, lo caro no es el fallo: es entenderlo.
Por eso, en el workflow subimos:
playwright-report/→ HTML report (resultados navegables).test-results/→ traces, screenshots y vídeos (si los activas).
Consejos prácticos:
trace: 'on-first-retry'suele ser el mejor equilibrio: no genera trazas en verde, pero sí cuando hay flakiness o fallos reales.video: 'retain-on-failure'ayuda mucho en flows complejos (pero pesa).- Ajusta
retention-dayssegún tu uso (7–30 días suele estar bien).

7. Técnicas anti-flakiness que sí merecen la pena
Un CI “rojo por flakiness” se ignora. Y lo que se ignora, deja de proteger.
En Playwright, lo que suele funcionar (sin magia) es:
- Retries controlados: 1–2 en CI, 0 en local.
- Timeouts explícitos (pero no absurdos):
actionTimeout,navigationTimeoutsi tu app lo necesita. - Evitar
waitForTimeout()salvo emergencias: prefiere esperas inteligentes (por ejemplo, esperar a un locator visible). - Aislamiento:
- Tests independientes.
- Datos de prueba predecibles.
- “Smoke” primero:
- Un set pequeño de tests críticos puede ejecutarse en todos los navegadores.
- El resto (más pesado) quizá solo en Chromium o en un pipeline nocturno.
8. ¿Y si quiero algo “no-code” o más fácil?
Tiene sentido quererlo fácil. Especialmente si estás validando un MVP o si todavía no tienes cultura de QA automatizado.
Opciones típicas (sin casarte con un stack):
- QA manual con Deploy Previews: rápido, barato… pero no escala y depende de personas.
- Herramientas SaaS de regresión visual: suelen integrarse con GitHub y te dan diffs “bonitos” sin que montes infraestructura.
- E2E no-code: útiles para smoke tests simples, pero se quedan cortas en flujos complejos o cuando necesitas control fino (datos, auth, mocks parciales, etc.).
Dónde suele estar el equilibrio realista en pymes y equipos pequeños:
- Empieza con un smoke suite (3–10 tests) que bloquee lo crítico.
- Añade regresión visual solo en pantallas “clave” (home, pricing, checkout, panel, etc.).
- Evoluciona a medida que el producto madura.
Y aquí viene la parte importante: montar esto bien no es “meter un YAML”. Es diseñar un sistema que:
- sea estable,
- cueste poco mantener,
- y de verdad proteja releases.
Si lo quieres “hecho y olvidarte”, suele salir mejor que lo monte un profesional: reduces semanas de prueba/error y evitas que el pipeline termine desactivado “porque molesta”.
9. Checklist final: tu semáforo de regresión visual
Antes de darlo por terminado, revisa:
- El check de E2E está marcado como required en la rama principal.
- El workflow espera correctamente a que el Deploy Preview esté “success”.
BASE_URLviene del preview (no de producción, no de localhost).- Tienes un smoke suite pequeño y rápido (lo crítico).
- Los tests no dependen de datos volátiles (fechas, IDs, contenido aleatorio).
- La regresión visual captura locators estables (no toda la página por defecto).
- Animaciones/transiciones desactivadas en tests visuales.
- Trazas/reportes se suben como artefactos siempre (
if: always()). - Retries controlados (1–2) y timeouts razonables.
- Si falla, puedes depurar sin “adivinar” (trace + screenshot + HTML report).
Lecturas recomendadas dentro del blog
Si quieres completar el sistema “de calidad” alrededor del CI:
- CI/CD para Astro y Netlify con GitHub Actions: pipeline completo paso a paso
- Automatiza tus presupuestos de rendimiento y flujos de usuario con Lighthouse CI y GitHub Actions
- Netlify Functions y Edge Functions con Astro: servicios dinámicos sin backend propio
Cierre
Si quieres, puedo ayudarte a diseñar y dejar funcionando este flujo en tu proyecto (Astro + Netlify), con una suite inicial de tests, regresión visual estable y reporting útil para el equipo.
Si te encaja, escríbeme y lo aterrizamos con tu caso real (qué flujos son críticos, qué navegadores importan y qué nivel de cobertura tiene sentido).