Tag: Playwright

  • Tests E2E autoreparables con Playwright: diagnostica, no parchees

    Tests E2E autoreparables con Playwright: diagnostica, no parchees

    El canal de Slack se llamaba #e2e-alerts. Llevaba cinco meses en silencio, y no porque no llegaran alertas: es que las once personas del equipo lo tenían silenciado.

    Ciento ochenta tests de Playwright. Entre veinte y cuarenta en rojo cada mañana. El ritual era mirar por encima, decir "es flaky" y relanzar el job.

    Cuando entré en ese proyecto me pidieron exactamente lo que estás pensando: montar tests E2E autoreparables, un agente que arreglara solo lo que se rompiera cada noche. Dije que no. Esa negativa es la mitad de este post.

    Antes de decir que no me senté a mirar por qué estaba rojo. Casi todo venía del mismo sitio: un refactor del design system que había renombrado clases CSS, y ciento y pico selectores apuntando a esas clases.

    Pero había un test distinto. El de pagar con código de descuento. Alguien lo había "arreglado" tres semanas antes cambiando getByRole('button', { name: 'Pagar pedido' }) por locator('button').nth(3).

    Verde. Precioso.

    El cuarto botón de esa página era "Seguir comprando". El de pagar llevaba nueve días deshabilitado para cualquier usuario que aplicara un descuento. Nueve días sin que nadie con un cupón pudiera pagar.

    El test lo sabía. Y una persona lo calló a mano en cuarenta segundos.

    Ahora imagina eso mismo, cien veces por semana, hecho por un agente que no firma nada y al que nadie revisa.

    Qué es un test E2E autoreparable (y qué no lo es)

    Aquí está la definición con la que trabajo:

    Un test E2E autoreparable es aquel que, cuando falla, dispara un agente que investiga la traza de ejecución, clasifica la causa y propone un parche revisable con evidencia adjunta. No es el que se modifica a sí mismo hasta ponerse verde.

    En inglés se conoce como self-healing test, y ahí está justo el malentendido: healing no significa que el test se reescriba solo.

    La diferencia no es de matiz. Es de propósito.

    Un test existe para emitir una señal: esto funciona, esto no. Si le das a un agente permiso para editar el test hasta que pase, has construido una máquina de decir "verde". Y una máquina de decir verde vale exactamente cero.

    El valor no está en el auto-arreglo. Está en el auto-diagnóstico. La parte cara de mantener una suite E2E no es escribir el parche —son tres caracteres en un locator—, es averiguar cuál de los cuarenta tests rojos merece un parche y cuáles están gritando que la aplicación se rompió.

    Eso es lo que un agente hace bien. Lo otro es lo que lo hace peligroso.

    No se rompe el test, se rompe el acoplamiento

    Los tests E2E no se degradan solos. Lo que se degrada es el contrato implícito entre tu test y el DOM.

    Escribiste .checkout__actions > button.btn-primary. Nadie firmó que esa clase fuera estable. Un martes alguien migró el botón a otro componente y tu test se enteró en producción.

    En las suites que he auditado, la inmensa mayoría de los rojos son fallos de localización, no de comportamiento: el elemento sigue ahí, hace lo mismo, y el test ya no sabe encontrarlo. Es mi impresión revisando proyectos, no un estudio —pero haz el recuento en tu propia suite y apuesto a que te sale parecido.

    Esa asimetría es la que hace viable un agente. Y también la que lo vuelve inútil si no distingue el resto.

    Antes del agente: locators de Playwright que se puedan reparar

    Si tus locators son frágiles, el agente no reduce la deuda. La automatiza.

    La estrategia de locators de Playwright está construida sobre lo que el usuario percibe, no sobre la implementación. Ese es el punto: un locator basado en rol accesible sobrevive a un refactor de estilos y muere cuando cambia lo que el usuario ve —que es justo cuando quieres que muera.

    // ❌ Se rompe con cualquier refactor de CSS. El agente no puede saber si es grave.
    await page.locator('.checkout__actions > button.btn-primary').click();
    
    // ✅ Se rompe solo cuando cambia lo que el usuario percibe.
    await page.getByRole('button', { name: 'Pagar pedido' }).click();
    await page.getByLabel('Código de descuento').fill('BLACKFRIDAY');
    await expect(page.getByTestId('order-summary')).toContainText('49,90 €');
    

    El orden que sigo es siempre el mismo: getByRole primero, getByLabel para formularios, y getByTestId solo cuando no hay semántica que agarrar —listas virtualizadas, tablas sin cabecera accesible, componentes de terceros.

    Y el atributo de test se declara en la config, no se improvisa por fichero:

    // playwright.config.ts
    import { defineConfig } from '@playwright/test';
    
    export default defineConfig({
      retries: 2,
      reporter: [['html', { open: 'never' }]],
      use: {
        testIdAttribute: 'data-test',
        trace: 'on-first-retry',
      },
    });
    

    Hay un efecto secundario que casi nadie cuenta: escribir los tests por rol accesible te obliga a que la aplicación tenga roles accesibles. Es la misma palanca que uso cuando pongo a un agente a auditar usabilidad con Playwright MCP. El árbol de accesibilidad deja de ser un extra de compliance y pasa a ser la API contra la que testeas.

    La traza es la única señal que el agente puede usar

    Sin traza, el agente lee un mensaje de timeout y adivina. Con traza, compara.

    Esa línea de la config —trace: 'on-first-retry'— es la que separa un diagnóstico de una alucinación. Playwright graba el reintento completo: acciones, peticiones de red, consola y snapshots del DOM antes y después de cada paso. El archivo se guarda dentro de test-results/ y se abre con npx playwright show-trace. La documentación del Trace Viewer explica el formato entero. Verificado con Playwright 1.63, la versión estable en septiembre de 2026.

    Un humano abre esa traza y ve la película. Un agente necesita algo más masticado, porque el DOM crudo de una app real son decenas de miles de tokens de ruido. En las dos páginas donde lo he medido, el árbol de accesibilidad ocupaba entre 2,5 y 28 veces menos que el HTML crudo.

    Lo que le doy es el árbol de accesibilidad en el momento del fallo, que es la misma abstracción sobre la que están escritos los locators. ariaSnapshot() está disponible desde Playwright 1.49; en versiones anteriores tendrás que serializar el árbol a mano.

    Aquí conviene ser honesto: Playwright 1.63 ya escribe por su cuenta un test-results/<test>/error-context.md con una sección # Page snapshot que contiene ese mismo árbol, así que una parte de esto la tienes gratis. El fixture sigue mereciendo la pena por lo que añade encima: la URL exacta del fallo, el nombrado que decides tú y los adjuntos visibles en el reporte HTML, que es lo que después consume el pipeline del agente.

    Este es el fixture:

    // tests/fixtures/diagnostico.ts
    import { test as base, expect } from '@playwright/test';
    
    export const test = base.extend<{ diagnostico: void }>({
      diagnostico: [
        async ({ page }, use, testInfo) => {
          await use();
    
          if (testInfo.status === testInfo.expectedStatus) return;
    
          // Árbol de accesibilidad real en el instante del fallo
          const real = await page.locator('body').ariaSnapshot();
          await testInfo.attach('aria-real.yml', { body: real, contentType: 'text/yaml' });
          await testInfo.attach('url-fallo.txt', { body: page.url(), contentType: 'text/plain' });
        },
        { auto: true },
      ],
    });
    
    export { expect };
    

    Con ese YAML adjunto en el reporte, el prompt del agente deja de ser "arregla este test" y pasa a ser una comparación: esperaba un button con nombre "Pagar pedido"; en el árbol real hay un button con nombre "Confirmar pedido" en la misma posición. Eso ya no es adivinar. Es diffear dos estructuras.

    Y si el elemento no aparece en el árbol bajo ningún nombre, el agente tiene que saber que eso significa algo completamente distinto.

    El triaje de un test E2E autoreparable: tres fallos distintos

    Esta es la sección que sostiene todo lo demás. Un agente que no separa estos tres casos te borra la señal de la suite entera.

    Tipo de fallo Evidencia que lo identifica Qué puede hacer el agente
    A. El DOM cambió, el test no El elemento existe en el árbol con otro nombre, rol o posición. El commit sospechoso toca marcado o estilos. Fallan varios tests que comparten componente. Proponer el parche del locator en un PR. Es el único caso reparable.
    B. El comportamiento cambió a propósito El elemento ya no existe porque el flujo cambió: un paso nuevo, otra ruta, un campo eliminado. El commit toca lógica, no marcado. El fallo encaja con una feature reciente. Nada. Abre un issue con el diagnóstico y etiqueta al dueño de la feature. Actualizar ese test es una decisión de producto.
    C. La aplicación está rota El locator es correcto y el elemento está ahí, pero la acción falla, el estado final es otro o hay 500 en la traza de red. Suele caer un solo flujo crítico. Prohibido tocar el test. Escalar con la traza adjunta. El test está haciendo su trabajo.

    Las tres preguntas que resuelven casi todos los casos:

    ¿El commit tocó marcado o lógica? Un cambio en plantillas y estilos apunta a A. Un cambio en servicios, rutas o estado apunta a B o C.

    ¿Falla un test o fallan veinte? Veinte tests que comparten componente es A casi seguro. Uno solo, en el flujo de pago, con el resto en verde, es C hasta que se demuestre lo contrario.

    ¿El elemento existe con otro nombre o no existe? Existe con otro nombre → A. No aparece en el árbol → B o C, y el agente no decide cuál.

    Fíjate en que ninguna de las tres se responde mirando el test. Se responden cruzando la traza con el diff del commit. Por eso el agente necesita acceso al repositorio, y solo de lectura.

    El agente abre un PR. No commitea.

    El flujo que uso es aburrido, y por eso funciona.

    El job nocturno corre la suite. Si hay rojos, un segundo job lanza el agente con tres entradas: el reporte HTML con sus adjuntos, la traza de cada fallo y el diff de los commits desde el último run verde. Relanzar solo lo caído con npx playwright test --last-failed ahorra minutos de CI.

    El agente no ejecuta el navegador. Lee artefactos. Eso elimina de golpe toda la clase de problemas que aparecen cuando pones a un agente a conducir el navegador en vivo: esperas mal calculadas, referencias caducadas, estado que se le escapa.

    Su salida es una pull request con cuatro cosas: diagnóstico en prosa, clasificación A/B/C con la evidencia que la justifica, el diff del locator y la traza enlazada.

    Y una regla que no se negocia: un locator por test, un test por commit dentro del PR. Si el parche necesita tocar tres líneas para que pase, no es un parche de localización: es un cambio de comportamiento disfrazado. Se rechaza.

    Un humano mergea. Siempre.

    Esa puerta es el mismo mecanismo que describo en la revisión por contrato del código que genera la IA: el agente no gana el derecho a escribir en main por haber acertado ochenta veces seguidas. Si quieres el método entero, con las cláusulas y los límites escritos, está en el ebook gratuito de Revisión por Contrato.

    Los guardarraíles que impiden que la suite se convierta en teatro

    Tres guardarraíles bastan para que un agente de reparación no degrade la suite: que no toque asserts, un tope de cinco tests parcheados por PR y una métrica de bugs escapados a producción. Con eso vas servido.

    El agente no toca asserts. Solo locators y esperas. Un expect(...).toBeVisible() que muta a toBeHidden(), un toContainText con otro importe, un assert borrado: eso es cambiar la definición de correcto, y no es su trabajo. Lo hago cumplir con un check en CI que rechaza el PR si el diff cambia el matcher (toBeVisible, toContainText…) o el valor esperado. El locator que vive dentro de un expect(...) sí se puede tocar; la afirmación sobre qué es correcto, no.

    Tope de tests reparados por PR. Cinco. Si el agente quiere arreglar cuarenta, no ha encontrado cuarenta fallos de localización: ha encontrado un cambio estructural que necesita a una persona pensando diez minutos.

    La métrica que delata el desastre. Cuenta dos números cada mes: locators parcheados por el agente y bugs escapados a producción en flujos que la suite cubre. Si el primero sube y el segundo también, el agente no está manteniendo la suite. La está silenciando.

    Ese segundo número no sale de los tests. Sale de tener observabilidad de verdad, más allá del code review. Sin él pilotas a ciegas con un copiloto que te asegura que todo va bien.

    Un apunte de coste: los fallos de tipo B —comportamiento que cambió a propósito— se detectan mucho más barato una capa por debajo, en tests de componente. Si trabajas con Angular, ese nivel es el que cubro en el curso de Testing en Angular. Cuanto mejor sea tu capa de componente, menos ruido le llega al agente arriba.

    Qué hacer el lunes

    No montes el agente. Todavía no.

    Coge los diez tests que más han cambiado en los últimos tres meses según git log. Son tus diez tests más frágiles. Reescribe sus locators con getByRole y getByLabel, y activa trace: 'on-first-retry' en la config.

    Eso es una tarde. Y probablemente te baje el ruido más que cualquier agente.

    La semana siguiente, cuando algo se ponga rojo, abre la traza y clasifícalo a mano: A, B o C. Hazlo diez veces. Lo que aprendas en esas diez clasificaciones es literalmente el prompt del agente —y no lo puedes escribir antes de haberlo hecho tú.

    Cuando llegue el momento de montarlo, el patrón de agente que produce artefactos revisables en lugar de commits directos es el que enseño en Construye con IA, de la idea al producto con Claude Code.

    Y quédate con esto: el objetivo nunca fue tener la suite en verde. Era saber cuándo dejar de confiar en ella.

    Preguntas frecuentes

    ¿Qué es un test E2E autoreparable exactamente?

    Es un test que, al fallar, dispara un agente que investiga la traza de ejecución, clasifica la causa y propone un parche revisable con la evidencia adjunta. La palabra "autoreparable" describe el diagnóstico automático, no la escritura automática en la rama principal. Si el agente puede modificar el test hasta que pase sin que nadie lo apruebe, lo que tienes no es una suite que se cura sola: es una suite que ha dejado de informarte.

    ¿Por qué no dejar que el agente commitee el arreglo directamente si acierta casi siempre?

    Porque el coste del error no es simétrico. Cien parches correctos te ahorran unos minutos cada uno. Un solo parche incorrecto sobre un fallo de tipo C —la aplicación rota— borra la única señal que tenías de un bug en producción, y encima deja el CI en verde. Cuando ese bug aparezca, tu primer instinto será descartar el área que cubren los tests, precisamente porque estaban pasando.

    ¿Cómo distingue el agente entre un cambio de DOM y una regresión real?

    Cruzando tres evidencias: si el elemento sigue existiendo en el árbol de accesibilidad bajo otro nombre o rol, si el commit sospechoso tocó marcado o lógica, y si el fallo está aislado o afecta a varios tests que comparten componente. Elemento presente con otro nombre, más commit de marcado, más fallo en grupo, apunta a cambio de DOM. Elemento presente, locator correcto y acción que falla apunta a regresión, y ahí el agente no toca nada.

    ¿Sirve esto para tests flaky por timing y no por locators?

    Parcialmente. La traza deja ver si el fallo fue una espera insuficiente o una condición de carrera, y el agente puede proponer sustituir una espera fija por una aserción con reintento automático, que es la forma correcta de esperar en Playwright. Pero el flaky por datos compartidos entre tests o por estado sucio del entorno no se arregla en el test: se arregla aislando los datos de cada ejecución, y eso es trabajo de arquitectura, no de parche.

    ¿Cuánto cuesta esto en tokens si la suite es grande?

    Depende de qué le pases, y ahí está el truco. Si le mandas el DOM crudo de cada fallo, el coste se dispara y además el diagnóstico empeora por el ruido. Pasándole solo el árbol de accesibilidad recortado, la URL y el diff de los commits relevantes, el contexto por fallo se queda en unos pocos miles de tokens. El tope de cinco tests por PR funciona también como tope de gasto.

    ¿Puedo aplicar el mismo triaje sin agente, a mano?

    Sí, y deberías empezar por ahí. La tabla de A/B/C es una herramienta de proceso antes que de IA: obliga al equipo a justificar por escrito por qué un test rojo se convierte en verde. He visto equipos bajar el ruido de su suite a la mitad solo con esa regla y cero automatización. El agente acelera un criterio que ya funciona; no lo inventa.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Astro 7.3: el candado que bloqueaba tus tests E2E en preview

    Astro 7.3: el candado que bloqueaba tus tests E2E en preview

    Actualizas Astro. Lanzas la suite de Playwright. Un servidor de preview arranca bien y los demás se caen con un error que no habla de tus tests: habla de un lockfile.

    Tú no habías puesto ningún lockfile. Y el commit de esa noche no tocaba nada de testing.

    Ese error tiene una historia detrás, y la historia no va de ti. Va de los agentes de IA.

    Astro 7.3.0 se publicó el 3 de septiembre de 2026 con tres cambios pequeños: el flag --ignore-lock llega a astro preview, el logger de runtime de Astro se pasa a los image services y cache providers custom, y @astrojs/cloudflare 14.3.0 incorpora un helper finalize() para worker entrypoints propios. No hay breaking changes. Ninguno de los tres cambia lo que puedes construir con Astro; los tres cambian cuánto tiempo pierdes cuando algo se rompe.

    Todo lo que cuento aquí sale de las notas oficiales de la release de Astro 7.3, del changelog de astro 7.3.0, del changelog de @astrojs/cloudflare y de la referencia de CLI de Astro.

    Vamos con el primero, que es el que más gente va a notar hoy mismo.


    --ignore-lock llega a astro preview: el arreglo que se nota en el CI

    astro preview levanta un servidor sobre tu build de producción. Es contra ese servidor contra el que corren tus tests E2E, porque probar contra astro dev es probar otra cosa: otro pipeline, otros assets, otro comportamiento.

    Desde Astro 7.2, ese comando escribe un lockfile en .astro/preview.json — el mismo mecanismo que astro dev usa en .astro/dev.json desde Astro 7.0. Un servidor de preview por proyecto y punto: si intentas levantar el segundo, no arranca.

    Astro 7.3 devuelve la escotilla:

    npx astro preview --port 4322 --ignore-lock
    

    El flag se salta la comprobación del lockfile y te deja arrancar tantos servidores de preview como quieras sobre el mismo proyecto, cada uno en su puerto. En Playwright, que es el caso de uso que el propio equipo de Astro menciona en las notas, se traduce en esto:

    // playwright.config.ts
    import { defineConfig } from '@playwright/test';
    
    export default defineConfig({
      webServer: [
        {
          command: 'npx astro preview --port 4321 --ignore-lock',
          url: 'http://localhost:4321',
          reuseExistingServer: !process.env.CI,
        },
        {
          command: 'npx astro preview --port 4322 --ignore-lock',
          url: 'http://localhost:4322',
          reuseExistingServer: !process.env.CI,
        },
      ],
    });
    

    Hay un detalle que conviene leer dos veces. Las notas oficiales avisan de que estas instancias corren de forma independiente, así que están pensadas para servidores rápidos y de usar y tirar, no para los que gestionas con astro preview stop o astro preview status. Traducido: si los arrancas a mano, los matas a mano. Dentro de Playwright da igual, porque el teardown del webServer se encarga.

    Y un aviso que no está en el anuncio de la release pero sí en la referencia del CLI: --ignore-lock no se puede combinar con --background ni con --force, porque los dos dependen del lockfile. Si los juntas, Astro lanza un error.

    Lo que convierte eso en un problema real: Astro activa el modo background por su cuenta cuando detecta que quien ejecuta el comando es un agente de IA. Traducido: si lanzas la suite desde dentro de una sesión de Claude Code o de Cursor, tu --ignore-lock puede petar sin que hayas escrito --background en ninguna parte.

    La salida está documentada y es una variable de entorno:

    ASTRO_PREVIEW_BACKGROUND=0 npx astro preview --port 4322 --ignore-lock
    

    Si lanzas Playwright desde tu terminal, esto no te afecta. Si lo lanza un agente por ti, ponla en el webServer y te ahorras el segundo día de depuración.

    Y hay un segundo escenario, menos obvio y más frecuente de lo que parece: no necesitas dos suites en paralelo para chocar con el candado. Basta con que algo ya lo tenga cogido. Un preview huérfano de la ejecución anterior. O tu agente de IA, que dejó uno levantado en segundo plano mientras trabajaba.

    Esto último no es hipotético, y aquí es donde la release se pone interesante.


    El candado no lo pusieron por ti

    Astro 7 introdujo el lockfile en astro dev con un motivo explícito, y lo dicen ellos con todas las letras en su blog: para que los agentes de IA no levantaran servidores de desarrollo duplicados del mismo proyecto sin darse cuenta. Un problema que hace tres años no existía.

    La secuencia completa es esta:

    Versión Qué pasó
    Astro 7.0 Llega el lockfile a astro dev para frenar servidores duplicados de agentes de IA
    Astro 7.1 Se añade --ignore-lock a astro dev para quien sí quiere varios a propósito
    Astro 7.2 El lockfile se extiende a astro preview, pero sin escotilla
    Astro 7.3 --ignore-lock llega también a astro preview

    Si te interesa el contexto de dónde salió todo esto, lo conté cuando salió la major en el repaso de novedades de Astro v7.

    Aquí está la lección, y va mucho más allá de Astro.

    Tus herramientas están empezando a poner defaults pensados para un agente, no para ti. El lockfile es una decisión sensata cuando quien ejecuta el comando es un modelo que no recuerda si ya levantó el servidor hace diez minutos. Es una decisión molesta cuando quien lo ejecuta eres tú, con un playwright.config.ts delante y muy claro lo que quieres.

    Astro lo ha resuelto bien: default seguro para el agente, flag explícito para el humano. Pero el patrón se va a repetir en todo tu stack, y la próxima vez a lo mejor no te dan el flag el mismo trimestre.

    Por eso insisto tanto en esto cuando trabajo con agentes en el curso de Construye con IA: tienes que saber qué procesos deja vivos tu agente. No porque vaya a romper nada grave, sino porque el día que tu CI falle por un lockfile vas a perder dos horas buscando el bug en tu código.

    Y ya que estamos en testing: el E2E es la capa cara. Casi todo lo que quieres verificar debería estar cubierto más abajo, donde una prueba cuesta milisegundos — lo desarrollé en pruebas unitarias ultrarrápidas con Vitest.


    El logger de runtime llega a los image services y a los cache providers

    Segundo cambio. Menos vistoso, y sin embargo es el que arregla un problema de higiene real.

    Si mantienes un image service custom —el típico wrapper sobre Cloudinary, imgproxy o tu propio CDN— hasta ahora tu única forma de avisar de algo era console.warn(). Con la consecuencia obvia: ese warning se escupía siempre. Ignoraba el nivel de log del proyecto, ignoraba --silent y ensuciaba la salida del build igual en local que en CI.

    En Astro 7.3, los image services reciben el logger de Astro como argumento adicional en transform():

    import type { LocalImageService } from 'astro';
    
    const service: LocalImageService = {
      // ...
      async transform(inputBuffer, transform, imageConfig, logger) {
        logger.warn(`No se pudo optimizar "${transform.src}". Se devuelve sin tocar.`);
        return { data: inputBuffer, format: 'png' };
      },
    };
    

    Y los cache providers lo reciben dentro del contexto que se pasa a onRequest():

    import type { CacheProvider } from 'astro';
    
    const provider: CacheProvider = {
      name: 'my-cache',
      async onRequest({ request, url, logger }, next) {
        logger.warn(`Caché omitida en ${url.pathname}: la respuesta define una cookie.`);
        return next();
      },
      // ...
    };
    

    El servicio Sharp integrado y el provider memoryCache() ya están migrados. Sharp lo usa para avisar de formatos de origen raros o no soportados; memoryCache(), para avisar de respuestas que se salta y de fallos en la revalidación en segundo plano.

    La ganancia no es que ahora "haya logs". Es que tus warnings viajan por el mismo canal que los de Astro y respetan la configuración del proyecto. Si mantienes una integración propia, es un cambio de dos líneas.

    Y sí, esto entra de lleno en la conversación de rendimiento: el image service es una de las piezas que decide el peso real de tus páginas, igual que las Server Islands que analicé en sitios web ultrarrápidos con Astro.


    finalize() en @astrojs/cloudflare: el bug silencioso de las cookies

    Tercer cambio, el más de nicho y el más peligroso de los tres si te toca.

    Va para quien despliega en Cloudflare con un worker entrypoint propio en lugar del que genera el adapter. Ese pipeline manual funciona, pero se salta un paso: aplicar las cookies y los defaults de caché del CDN de Cloudflare a la respuesta que devuelve astro/fetch.

    Un bug de esos que no rompe el build. Simplemente un día descubres que las cookies de sesión no llegan.

    @astrojs/cloudflare 14.3.0, publicada el 3 de septiembre de 2026 —el mismo día que Astro 7.3—, añade finalize() para cerrar ese hueco. Recibe el FetchState y la respuesta del pipeline, y devuelve la respuesta ya con las cookies y la caché aplicadas:

    // src/worker.ts
    import { astro, FetchState } from 'astro/fetch';
    import { cf, finalize } from '@astrojs/cloudflare/fetch';
    
    export default {
      async fetch(request: Request, env: Env, context: ExecutionContext) {
        const state = new FetchState(request);
        const asset = await cf(state, env, context);
        if (asset) return asset;
    
        return finalize(state, await astro(state));
      },
    };
    

    Si usas Hono, no tienes que hacer nada: el middleware @astrojs/cloudflare/hono aplica esas cabeceras por su cuenta.

    El resto de la release te ahorra tiempo. Esta te ahorra un incidente.


    ¿Te toca actualizar a Astro 7.3?

    No hay breaking changes, así que la pregunta no es si puedes, es si ganas algo.

    Tu situación Qué hacer Por qué
    Tienes E2E con Playwright sobre astro preview Actualiza hoy Es la diferencia entre una suite que arranca y una que muere en el webServer
    Trabajas a diario con un agente de IA en el proyecto Actualiza hoy Dejas de pelearte con él por el mismo lockfile
    Mantienes un image service o cache provider custom Actualiza y cambia dos líneas Tus warnings pasan a respetar el nivel de log y --silent
    Despliegas en Cloudflare con worker entrypoint propio Sube @astrojs/cloudflare a 14.3.0 finalize() te quita el bug silencioso de las cookies
    Sitio estático, sin E2E, sin adapter Actualiza sin prisa No hay riesgo, pero tampoco premio

    Si vienes de la rama 6.x, el salto que de verdad cambió cómo se construye con Astro no es este: fue el de Server Islands y Actions en 6.2. Astro 7.3 es mantenimiento fino sobre esa base.


    Lo que yo haría esta semana

    Abre tu playwright.config.ts y mira si el command del webServer llama a astro preview. Si es que sí, añade --ignore-lock y actualiza. Son treinta segundos y te ahorras el día en que el CI falle por un lockfile que tú no pusiste.

    El resto puede esperar al próximo sprint.

    Pero quédate con la idea de fondo, porque vale más que los tres cambios juntos: tu tooling ha empezado a asumir que quien escribe los comandos es un agente. Los defaults se están moviendo hacia ahí. Cuando algo se rompa de forma rara en tu pipeline, esa es la primera hipótesis que deberías poner sobre la mesa, y ya no la última.

    De esto discutimos bastante en Dominicode Labs, porque casi todos los que estamos metiendo agentes en el flujo diario nos hemos comido alguna versión de este mismo problema.


    Preguntas frecuentes

    ¿Qué hace exactamente –ignore-lock en astro preview?

    Se salta la comprobación del lockfile que Astro 7.2 añadió a astro preview, de modo que puedes tener varios servidores de preview del mismo proyecto corriendo a la vez en puertos distintos. El flag ya existía en astro dev desde Astro 7.1; la 7.3 lo lleva también a preview.

    ¿Por qué astro preview –ignore-lock me da error?

    Porque --ignore-lock no se puede combinar con --background ni con --force: las dos opciones dependen del lockfile, así que Astro lanza un error. Y cuando Astro detecta que el comando lo ejecuta un agente de IA, activa el modo background automáticamente, aunque tú no hayas pasado --background. Si te ocurre, desactiva ese automatismo con la variable de entorno ASTRO_PREVIEW_BACKGROUND=0 delante del comando.

    ¿Por qué existe ese lockfile si nadie lo pidió?

    Porque Astro 7 lo introdujo para que los agentes de IA no levantaran servidores duplicados del mismo proyecto sin darse cuenta. Es un default pensado para un ejecutor que no recuerda si ya arrancó el servidor hace diez minutos, y por eso convive mal con flujos donde tú quieres varios servidores a propósito.

    ¿Los servidores lanzados con –ignore-lock se gestionan con astro preview stop?

    No. Las notas oficiales avisan de que esas instancias corren de forma independiente y están pensadas para servidores rápidos y de usar y tirar, no para los que administras con astro preview stop o astro preview status. Si los arrancas a mano, los cierras a mano; dentro de Playwright se encarga el teardown del webServer.

    ¿Tengo que tocar mi image service custom para aprovechar el logger?

    Solo si quieres. El cambio es aditivo: el logger llega como argumento adicional en transform() y dentro del contexto de onRequest() en los cache providers, así que tu código actual sigue funcionando. Cambiar console.warn() por logger.warn() es lo que hace que tus avisos respeten el nivel de log configurado y el flag --silent.

    ¿Necesito llamar a finalize() si uso Hono en Cloudflare?

    No. El middleware @astrojs/cloudflare/hono aplica esas cabeceras automáticamente. finalize() está pensado para quien monta un worker entrypoint propio sobre astro/fetch y necesita aplicar a mano las cookies y los defaults de caché del CDN de Cloudflare.

    ¿En qué versión de @astrojs/cloudflare está finalize()?

    En @astrojs/cloudflare 14.3.0, publicada el 3 de septiembre de 2026 junto a Astro 7.3. El adapter se versiona aparte de astro: subir astro a 7.3 no actualiza el adapter, tienes que subir los dos paquetes.

    ¿Hay breaking changes al actualizar a Astro 7.3?

    No. Es una release menor de mantenimiento sobre la 7.2: un flag nuevo, un logger que se propaga a APIs de extensión y un helper añadido en @astrojs/cloudflare 14.3. La actualización del adapter de Cloudflare va aparte de la de astro, así que revisa que subes las dos si estás en ese escenario.


    Si prefieres ver este tipo de análisis en vídeo, con el proyecto delante, lo publico en el canal de YouTube de Dominicode.

    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Agentes de IA en el navegador: por qué fallan y cómo arreglarlo

    Agentes de IA en el navegador: por qué fallan y cómo arreglarlo

    Tenía un agente que hacía una cosa aburrida y la hacía bien: leer una cola, crear el recurso por API, comprobar la respuesta, seguir. Si algo fallaba, lo repetía. Lo dejé corriendo toda una tarde sin mirarlo.

    Luego apunté ese mismo agente al navegador, porque el proveedor de turno no tenía API. Misma tarea, mismo bucle, misma cabeza: así se montan hoy casi todos los agentes de IA en el navegador. Y ahí aparece el clásico. El clic de "Confirmar" parece no responder, el bucle reintenta, y al otro lado quedan dos pedidos idénticos.

    No fue un bug del modelo. Fue el bucle haciendo exactamente lo que le pedí: reintentar. Hereda una política diseñada para un mundo donde reintentar es gratis, y el navegador es justo el sitio donde deja de serlo.

    Esto no es un tutorial. Si quieres montarlo y verlo funcionando, el cómo está en automatizar la usabilidad con agentes de IA. Aquí va lo otro: por qué se rompe cuando lo pones a trabajar de verdad. Se rompe siempre por los mismos tres sitios —las esperas, el estado y las acciones que no se pueden deshacer— y ninguno de los tres es culpa del modelo.

    Los agentes de IA en el navegador heredan un bucle que no es suyo

    Un agente es un bucle: observa, decide, actúa, vuelve a observar. Lo conté con detalle en qué es el agentic loop, y todo lo que envuelve al modelo —tools, memoria, política de errores— es lo que llamamos harness.

    Ese harness lleva dentro una suposición que casi nadie hace explícita: si una acción falla, se puede volver a intentar.

    Con una API la suposición se sostiene. El POST devolvió timeout, no sabes si llegó, lo repites. Si la API es decente tienes una clave de idempotencia. Si no, tienes un staging donde da igual. El coste de un reintento es latencia.

    En el navegador se cae. La acción ya salió de tu sistema: un pedido confirmado, un correo enviado, un registro borrado, una publicación en una cuenta real. Y tu bucle no recibe ningún código de estado: recibe una página repintada. Saber si aquello se completó es un paso aparte, que tienes que pedir tú. Casi nadie lo pide.

    Y aquí está el detalle que provoca el desastre: nadie cambia el bucle al cambiar de herramienta. Sustituyes http_request por browser_click en la lista de tools y sigues con la misma política de reintentos y la misma tolerancia a errores.

    Fallo 1: las esperas. Clicable no significa listo

    Playwright, antes de actuar, comprueba que el elemento sea accionable. Son los actionability checks, y la documentación oficial los define así:

    • Visible: tiene un bounding box no vacío y no tiene visibility: hidden.
    • Stable: ha mantenido el mismo bounding box durante al menos dos frames de animación consecutivos.
    • Receives Events: es el hit target del evento de puntero en el punto de la acción.
    • Enabled: no está deshabilitado.

    Lee esas cuatro definiciones otra vez. Son geometría y DOM. Ninguna dice nada sobre si tu aplicación tiene sentido.

    Un botón "Confirmar pedido" puede estar visible, quieto durante dos frames, habilitado y ser el hit target mientras el precio final todavía se recalcula en el backend. Pasa los cuatro checks. El clic es prematuro. Playwright no ha mentido: nunca prometió esperar a que la app estuviera lista, solo a que el elemento fuera clicable.

    Hay más. No todas las acciones piden lo mismo:

    Acción Comprobaciones
    click(), dblclick(), check(), uncheck(), tap() Visible, Stable, Receives Events, Enabled
    fill(), clear() Visible, Enabled, Editable
    selectOption() Visible, Enabled
    hover(), dragTo() Visible, Stable, Receives Events
    press(), pressSequentially(), focus(), blur(), dispatchEvent(), setInputFiles() Ninguna

    Fíjate en dos cosas. fill() no espera a Stable: comprueba que el campo sea visible, esté habilitado y no sea de solo lectura, pero puedes escribir en un input que se está moviendo. Y hay un grupo entero de acciones sin ninguna comprobación, incluida dispatchEvent().

    Ese grupo importa porque es la salida de emergencia. Cuando el clic normal "no funciona", el camino que aparece es disparar el evento a mano. En código de Playwright, locator.dispatchEvent('click'). Desde MCP no existe una tool equivalente: lo que hay es browser_evaluate, que ejecuta tu JavaScript sobre el elemento, y browser_run_code_unsafe, que la propia documentación describe como equivalente a ejecución remota de código.

    Tres caminos distintos y el mismo efecto: ninguno pasa por los actionability checks.

    El agente cree que ha encontrado una solución ingeniosa. Lo que ha hecho es quitar las comprobaciones que le impedían hacer clic demasiado pronto.

    La corrección no es esperar más. Es esperar a otra cosa: a la condición de negocio, no al elemento.

    // Espera al elemento. Pasa los cuatro checks y hace clic.
    await page.getByRole('button', { name: 'Confirmar pedido' }).click();
    
    // Espera a que la aplicación esté lista, y entonces hace clic.
    await expect(page.getByTestId('gastos-envio')).toHaveText(/\d/);
    await expect(page.getByTestId('spinner-total')).toBeHidden();
    await page.getByRole('button', { name: 'Confirmar pedido' }).click();
    

    Ojo con el orden: toBeHidden() en primera posición pasaría antes de que el spinner llegue a aparecer. Solo es seguro después de una aserción positiva.

    Con Playwright MCP el equivalente es browser_wait_for sobre un texto que solo existe cuando el backend ya respondió. No "Total" —que está en el DOM desde el primer render—, sino "Envío calculado" o el importe con su formato final.

    Esperar por estado y no por píxeles es la misma disciplina que separa una suite de tests fiable de una que falla los martes. La trabajo a fondo en el curso de Testing en Angular, y se traslada tal cual a los agentes.

    Fallo 2: el estado. El agente decide sobre una foto caducada

    En el fallo anterior el elemento no estaba listo. Aquí el elemento está listo, pero el agente mira una foto de hace tres segundos. La carrera es la misma; la corrección, no.

    Playwright MCP no le manda screenshots al modelo. Le manda accessibility snapshots: un árbol estructurado de elementos accesibles con refs para interactuar, del tipo e5.

    Es la decisión correcta. Playwright documenta un coste aproximado de 200-400 tokens por snapshot frente a 3.000-5.000 de un screenshot para un modelo de visión. Barato, textual, determinista.

    Pero la letra pequeña está en la misma página: los refs son estables dentro de un único snapshot —el mismo elemento mantiene el mismo ref hasta que la página cambia— y quedan invalidados cuando la página cambia.

    Traducción: el snapshot es una fotografía. Y entre la fotografía y el clic hay una llamada a un LLM que tarda segundos.

    En esos segundos tu SPA puede recibir un evento por WebSocket, reordenar una lista, cerrar un modal o repintar una tabla. Si el nodo desapareció, el ref deja de resolver y la tool falla: molesto, pero visible.

    El caso feo es el otro. Si tu framework recicla el nodo en vez de recrearlo —listas sin key o sin trackBy, scroll virtual—, el e5 que en la foto era "Eliminar borrador" de la fila 3 sigue vivo y ahora es el de la fila 4. Resuelve, el clic sale, y borras lo que no era. El agente no actúa sobre la página: actúa sobre su recuerdo de la página.

    Un locator de Playwright se resuelve en el instante de actuar. Un ref se resolvió antes de que el modelo empezara a pensar. Toda la diferencia está ahí.

    Hay un segundo efecto del estado que casi nadie cuenta, y es el que más planes rompe.

    No puedes paralelizar agentes de navegador como paralelizas agentes de API. Playwright MCP arranca por defecto con un perfil persistente —ahí viven las sesiones y las cookies— y la documentación es tajante: un perfil persistente solo lo puede usar una instancia de navegador a la vez, así que varios clientes MCP concurrentes que compartan el mismo workspace entrarán en conflicto.

    Con la configuración por defecto, ese perfil es un singleton. Lanzar diez subagentes contra la misma cuenta no te da diez veces el rendimiento: te da un conflicto, o peor, diez agentes pisándose la sesión.

    La propia documentación da dos salidas: arrancar cada cliente extra con --isolated, o apuntarlo a un --user-data-dir distinto. La segunda te conserva la sesión guardada. La primera arranca limpia y pierde todo su storage al cerrar el navegador, así que le inyectas el estado inicial con --storage-state.

    Para un agente que reintenta, quédate con la primera. Pierdes comodidad y ganas algo más valioso: poder tirar el estado y repetir el intento desde cero.

    Fallo 3: el bucle no distingue lo que se puede deshacer

    Divide en dos columnas todo lo que hace tu agente.

    Idempotente: navegar, leer, hacer scroll, sacar un snapshot, abrir una pestaña, leer la consola. Repetirlo cien veces no cambia el mundo. Como mucho gasta tokens.

    Con efecto externo: enviar el formulario, confirmar el pago, borrar el registro, publicar el post, invitar al usuario. Repetirlo cambia el mundo, y a veces cambia el mundo de otra persona.

    Para el harness las dos son idénticas. browser_click sobre "Volver" y browser_click sobre "Confirmar pedido" son la misma tool call con distinto ref. El modelo cree que sabe la diferencia; el bucle, que es quien decide reintentar, no la sabe. Sobre los límites reales de un bucle en producción escribí en el loop del agente en producción.

    La confirmación de que esto es un problema real no la pongo yo. La pone Anthropic.

    Claude for Chrome, en beta para planes de pago, navega, hace clic y rellena formularios en tu navegador. El usuario puede pre-aprobar por sitio las acciones que le permite. Y aun así, el producto sigue preguntando antes de ciertas acciones irreversibles o potencialmente dañinas, como hacer una compra.

    Si el reintento fuera seguro, ese gate no existiría. Nadie construye una interrupción humana específica alrededor de "comprar" por gusto: la construye porque comprar dos veces no se arregla razonando mejor. Es el mismo principio de aprobación previa del que hablé en computer use con Claude Code.

    El navegador viene con tus credenciales dentro

    La documentación de Playwright MCP tiene una frase que deberías pegar en la pared antes de darle un navegador a un agente:

    Playwright MCP is not a security boundary.
    (Playwright MCP no es una frontera de seguridad.)

    No es un aviso legal. Es una descripción exacta de lo que estás montando. Ese navegador lleva las sesiones del usuario real, sus cookies, sus tokens y su banca abierta en otra pestaña. Y cualquier texto que la página muestre entra en el contexto del modelo.

    Anthropic lo midió sobre su propia extensión. En el anuncio del piloto de Claude in Chrome, de agosto de 2025: 123 casos de prueba sobre 29 escenarios de ataque, 23,6% de éxito de la inyección de prompts sin mitigaciones y 11,2% con las defensas activadas en modo autónomo.

    Fíjate en que el segundo número no es cero. Algo más de uno de cada diez intentos seguía pasando, en la configuración de quien fabrica el modelo y tiene todos los incentivos para que no pase.

    Darle un navegador a un agente no es añadir una tool más al array. Es una decisión de arquitectura sobre qué credenciales pones al alcance de un bucle que, por diseño, insiste.

    Qué hacer con esto hoy

    1. Afirma el estado, no esperes al elemento. Antes de cada acción con efecto, exige una condición de negocio verificable: un texto que solo aparece cuando el backend respondió, un importe con formato final, un contador que cuadra. Si no sabes escribir esa aserción, tu agente tampoco sabe si la app está lista.

    2. Clasifica tus acciones y cierra las pocas irreversibles con un gate. No pidas aprobación para todo: eso degenera en aprobar en automático en tres días. Pídela solo en la columna corta. En Claude Code se implementa con hooks, como lo monté en hooks como guardrails.

    3. Usa una clave de idempotencia cuando la aplicación te la dé. Un identificador de operación en el formulario, un borrador con ID estable, un endpoint que acepta la misma referencia dos veces. Si controlas la app de destino, es media hora de trabajo y elimina la clase entera de fallo.

    4. Trabaja con perfil aislado y --storage-state. Estado desechable significa reintentos limpios. Estado persistente significa que el segundo intento arranca desde la basura del primero.

    5. Verifica después de actuar, no solo antes. browser_network_requests y browser_console_messages te dicen si la petición salió y si la app se quejó. Actuar a ciegas y volver a intentar es exactamente cómo se duplican pedidos.

    Diseñar así el bucle —qué se reintenta, qué se para, qué se verifica— es lo que separa un agente de demo de uno que dejas suelto, y es lo que trabajamos en el curso Construye con IA.

    El bucle no va a distinguir por ti entre leer y comprar. Esa frontera la dibujas tú, hoy, antes de la próxima ejecución. Si quieres ver estos patrones aplicados sobre proyectos reales y con el código delante, en Dominicode Labs es donde los montamos.

    Preguntas frecuentes

    ¿Por qué mi agente funciona bien contra APIs y falla en el navegador?

    Porque el bucle está construido sobre la idea de que una acción fallida se puede repetir. Con una API eso suele ser cierto y barato. En el navegador, la acción ya produjo un efecto fuera de tu sistema —un pedido, un correo, un borrado— y confirmar si se completó exige un paso extra que el bucle no da solo. Cambiaste la herramienta, no la política de reintentos.

    Si Playwright ya espera a que el elemento sea accionable, ¿por qué hace clic antes de tiempo?

    Porque sus comprobaciones son geométricas y de DOM: visible, estable, hit target y habilitado. Estable significa que el bounding box no cambió durante dos frames de animación, no que los datos hayan cargado. Un botón puede pasar los cuatro checks mientras el backend todavía calcula. Clicable no es lo mismo que listo.

    ¿Cómo hago que un agente espere a que la aplicación esté lista de verdad?

    Espera por una condición de negocio, no por un elemento: un texto que solo se renderiza cuando llegó la respuesta, un importe con su formato final, un estado que pasó de "calculando" a un valor. Con Playwright MCP, browser_wait_for sobre ese texto. Regla práctica: si el marcador que esperas ya está en el DOM en el primer render, no te sirve.

    ¿Puedo lanzar en paralelo varios agentes de IA en el navegador?

    No con la configuración por defecto. Playwright MCP usa un perfil persistente y su documentación advierte de que solo lo puede usar una instancia de navegador a la vez, así que varios clientes MCP concurrentes en el mismo workspace entran en conflicto. La salida documentada es arrancar cada cliente extra con --isolated —con --storage-state si hace falta sesión inicial— o apuntarlo a un --user-data-dir distinto si quieres conservar la sesión. Y, normalmente, cuentas distintas.

    ¿Qué acciones de un agente de navegador deberían pedir aprobación humana?

    Solo las que no se pueden deshacer: pagos, envíos definitivos, borrados, publicaciones e invitaciones. Navegar, leer o sacar snapshots no necesitan gate. Es la línea que sigue Claude for Chrome, que pre-aprueba por sitio y aun así vuelve a preguntar antes de acciones irreversibles como una compra. Si pides confirmación para todo, en tres días la darás en automático.

    ¿Es mejor accessibility snapshot o screenshot para un agente de navegador?

    El snapshot, casi siempre. Playwright documenta un coste aproximado de 200-400 tokens por snapshot frente a 3.000-5.000 de un screenshot para un modelo de visión, y además da refs con los que interactuar. Su límite es otro: esos refs solo son estables dentro de ese snapshot y se invalidan cuando la página cambia. El screenshot sigue disponible como tool por defecto (browser_take_screenshot) para lo que el árbol de accesibilidad no expresa, como un canvas o un mapa. Lo que habilita --caps=vision no es la captura: es poder interactuar por coordenadas sobre ella.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Cómo automatizar usabilidad con agentes de IA en tu app

    Cómo automatizar usabilidad con agentes de IA en tu app

    Un cliente me enseñó su aplicación de onboarding hace unos meses. Cinco pasos. Diseño limpio. Todo funcional en los tests.

    La tasa de abandono era del 68% en el paso tres.

    Revisamos los tests de integración: pasaban todos. El formulario enviaba datos correctamente. La validación funcionaba. El CI estaba en verde. Y aun así, casi siete de cada diez usuarios salían del flujo antes de terminar.

    El problema no era que la aplicación no funcionara. Era que nadie había auditado cómo se usaba.

    Ahí está la diferencia que muchos developers no distinguen hasta que lo ven en métricas reales: una cosa es que el código haga lo que debe, y otra muy distinta es que el usuario pueda usarlo sin fricción. La primera la resuelves con tests. La segunda la resuelves con automatizar usabilidad con agentes de IA — que es exactamente lo que vamos a ver aquí.

    Automatizar usabilidad con agentes de IA consiste en delegar la auditoría de accesibilidad, flujos de usuario y fricciones de interfaz a un agente — como Claude Code con MCP de Playwright — que controla el navegador de forma programática, navega flujos reales y genera un reporte estructurado de hallazgos sin intervención manual.


    Testing funcional vs. testing de usabilidad

    Un test funcional pregunta: ¿el botón de "Enviar" llama a la función correcta?

    Un test de usabilidad pregunta: ¿el usuario sabe que tiene que hacer clic ahí? ¿Lo ve? ¿Entiende qué va a pasar después?

    Son preguntas distintas y se responden con herramientas distintas.

    Los tests funcionales los escribes tú, los corre un CI, comprueban comportamiento esperado. Eso ya lo sabes hacer. Lo que un agente de IA aporta es la capacidad de navegar tu aplicación como si fuera un usuario — explorar rutas no documentadas, detectar elementos sin etiquetas accesibles, medir cuánto tarda en responder una pantalla, o identificar que un campo de error aparece debajo del scroll y el usuario nunca lo ve.

    No te reemplaza un test de usabilidad con personas reales. Pero te da un nivel de auditoría automatizada que antes no existía — y que tú nunca harías manualmente en cada PR. Si quieres ver cómo encaja esto en un pipeline completo de desarrollo, tienes la explicación en el post sobre automatizar el proceso de desarrollo con IA.


    Qué puede detectar un agente

    Cuando conectas Claude Code con el MCP de Playwright o Chrome DevTools, el agente puede controlar el navegador de forma programática. Eso significa que puede:

    • Navegar a cualquier ruta de tu aplicación
    • Hacer clic en elementos, rellenar formularios, desplazarse por la página
    • Leer el DOM y el árbol de accesibilidad (el que usa un lector de pantalla)
    • Medir tiempos de respuesta entre interacciones
    • Ejecutar axe-core para detectar violaciones WCAG
    • Capturar screenshots en cada paso del flujo
    • Generar un reporte estructurado con los hallazgos

    Lo que un agente detecta bien:

    1. Accesibilidad técnica: imágenes sin alt, botones sin aria-label, contraste insuficiente entre texto y fondo, formularios sin etiquetas asociadas, skip navigation ausente, foco de teclado atrapado en un componente modal.
    2. Fricciones estructurales: mensajes de error fuera del viewport, campos obligatorios que no se identifican como tales hasta el submit, pasos de onboarding que no guardan el progreso si el usuario recarga.
    3. Performance percibida: cuánto tarda en aparecer el primer elemento interactivo después de una navegación, si hay loaders sin indicación de progreso, si el layout shift hace que el usuario haga clic en el elemento equivocado.
    4. Cobertura de flujos: si una ruta de error (credenciales incorrectas, sesión expirada, red caída) termina en una pantalla sin instrucciones claras.

    Cómo configurar el agente para automatizar la auditoría de usabilidad

    La combinación que funciona en producción: Claude Code + MCP de Playwright.

    Para instalarlo: npx @playwright/mcp@latest. Una vez activo en tu configuración de Claude Code, el agente tiene acceso a las herramientas de control del navegador sin que escribas una línea de Playwright.

    El MCP de Playwright expone herramientas al agente para controlar el navegador: browser_navigate, browser_click, browser_type, browser_snapshot, browser_evaluate. Claude puede encadenar esas herramientas para ejecutar flujos completos.

    Un ejemplo de instrucción al agente para auditar el onboarding de una app:

    ## Tarea: Auditoría de usabilidad — flujo de onboarding
    
    URL base: http://localhost:4200
    
    Flujo a auditar:
    1. Navega a /register
    2. Rellena el formulario con datos válidos: nombre, email, contraseña
    3. Haz clic en "Crear cuenta"
    4. Completa los pasos del onboarding hasta llegar al dashboard
    
    En cada paso:
    - Captura un screenshot
    - Extrae el árbol de accesibilidad del contenido principal
    - Identifica elementos interactivos sin aria-label o sin texto visible
    - Mide el tiempo hasta que el siguiente paso es interactivo
    - Detecta si hay mensajes de error o advertencia y si son visibles sin scroll
    
    Al terminar, genera un reporte en formato JSON con esta estructura:
    {
      "paso": string,
      "url": string,
      "tiempo_carga_ms": number,
      "violaciones_accesibilidad": [],
      "fricciones_detectadas": [],
      "screenshot": string
    }
    

    El agente ejecuta eso de forma autónoma. Navega, interactúa, observa, y vuelve con un reporte estructurado.


    Accesibilidad automática con axe-core

    Para violaciones WCAG, la integración más sólida es axe-core. El agente puede ejecutarlo sobre cualquier página activa en el navegador mediante browser_evaluate:

    // Si axe-core no está en el bundle de la app, inyectarlo primero:
    // await page.addScriptTag({ url: 'https://cdn.jsdelivr.net/npm/axe-core/axe.min.js' });
    
    // Ejecutar la auditoría en el contexto de la página
    const results = await axe.run();
    return {
      violaciones: results.violations.map(v => ({
        impacto: v.impact,
        descripcion: v.description,
        elementos: v.nodes.map(n => n.target)
      }))
    };
    

    Lo que devuelve axe-core son violaciones categorizadas por impacto: critical, serious, moderate, minor. El agente puede filtrar solo las críticas, agregar el selector del elemento afectado, y generar una lista accionable para el developer.

    Esto detecta cosas como:

    • Contraste de color insuficiente (ratio menor a 4.5:1 para texto normal)
    • Imágenes sin atributo alt o con alt vacío en imágenes informativas
    • Elementos <div> y <span> usados como botones sin rol ARIA
    • Formularios sin <label> asociado o con placeholder como único identificador
    • Encabezados fuera de jerarquía (<h4> después de <h2> sin <h3>)

    Esta es una de las capacidades que trabajamos en detalle en el curso Construye con IA: cómo delegar auditorías estructuradas al agente para que el developer se centre en las decisiones de producto, no en el checklist técnico.


    El reporte de hallazgos

    Un agente que navega y detecta problemas no sirve de nada si los hallazgos terminan en un log de consola que nadie lee.

    El formato que mejor funciona para integrar en un workflow de desarrollo es un JSON estructurado que puedas convertir en un issue de GitHub, una tarea en Linear, o un comentario en un PR.

    Estructura mínima de reporte que el agente genera:

    {
      "auditoria": {
        "fecha": "2026-06-17",
        "url_base": "https://app.ejemplo.com",
        "flujo": "onboarding",
        "duracion_total_ms": 8420
      },
      "resumen": {
        "violaciones_criticas": 3,
        "violaciones_serias": 7,
        "fricciones_detectadas": 4,
        "pasos_con_retraso": 2
      },
      "hallazgos": [
        {
          "paso": "registro",
          "tipo": "accesibilidad",
          "impacto": "critical",
          "descripcion": "Campo de contraseña sin label asociado. Solo usa placeholder.",
          "selector": "#password-input",
          "referencia_wcag": "1.3.1"
        },
        {
          "paso": "paso-2-perfil",
          "tipo": "friccion",
          "impacto": "serious",
          "descripcion": "Mensaje de validación aparece 280px por debajo del campo en mobile. No visible sin scroll.",
          "selector": ".validation-message",
          "screenshot": "paso-2-error-state.png"
        }
      ]
    }
    

    Este reporte lo puedes consumir directamente en tu pipeline de CI, enviarlo a un webhook de Slack, o procesarlo con otro agente que abra los issues correspondientes.


    Agentes IA vs Lighthouse

    Capacidad Lighthouse Agente IA + MCP Playwright
    Métricas de rendimiento (LCP, CLS, FID) ✅ ❌
    Accesibilidad estática (axe-core) ✅ ✅ más granular
    Flujos interactivos multipaso ❌ ✅
    Estados de error y modales ❌ ✅
    Reporte adaptado al contexto del proyecto ❌ ✅ JSON estructurado
    Requiere código de automatización ❌ ❌ lenguaje natural
    Integración en CI/CD ✅ ✅

    Lighthouse mide el estado de una página en un instante. Un agente mide cómo un usuario real la recorre.


    Lo que el agente no puede hacer

    Esto es importante. Un agente mide lo que puede observar en el DOM y en el comportamiento de la interfaz. No puede medir lo que ocurre dentro del usuario.

    No detecta:

    • Frustración emocional. Si el flujo es técnicamente correcto pero genera ansiedad porque el lenguaje es frío o las instrucciones son ambiguas, el agente no lo sabe.
    • Preferencias estéticas. El contraste puede pasar el ratio WCAG y aun así resultar incómodo visualmente en contextos específicos.
    • Contexto cultural. Un ícono que es intuitivo para un usuario europeo puede no serlo para un usuario latinoamericano. El agente no tiene ese mapa cultural.
    • Carga cognitiva subjetiva. Puede detectar que hay ocho campos en un formulario, pero no puede decirte si eso es demasiado para tu audiencia específica.
    • Microcopy y confianza. El texto de un CTA puede ser técnicamente legible y aun así no generar suficiente confianza para que el usuario haga clic.

    Esas decisiones siguen siendo tuyas — o del diseñador, o del researcher de UX. Lo que el agente elimina es el trabajo de auditoría técnica repetitiva que de otra forma no harías en cada ciclo de desarrollo.


    Cómo integrarlo en tu workflow

    El patrón que funciona sin complicar el pipeline:

    1. Local, bajo demanda: el developer lanza la auditoría sobre la rama antes de abrir el PR. El agente revisa el flujo afectado por el cambio.
    2. En CI, sobre entornos de preview: cada PR despliega a un entorno de preview (Vercel, Netlify, Railway), y el agente audita ese entorno de forma automática antes del merge.
    3. Semanal, sobre producción: un job programado lanza la auditoría completa sobre la app en producción y genera un reporte que llega al equipo.

    El tercer nivel es el más valioso a largo plazo: detecta regresiones de accesibilidad que se cuelan en producción sin que nadie las vea en los tests unitarios.

    El paso de code review automático antes del PR — que complementa esta auditoría de usabilidad — lo explico en detalle en el post sobre agentic code review con Claude Code.

    Si quieres ver cómo construir este tipo de pipelines con agentes desde cero, en Dominicode Labs tenemos proyectos completos que aplican exactamente este enfoque — desde la configuración del MCP hasta la generación del reporte final.


    FAQ

    ¿Necesito conocer Playwright para esto?
    No necesitas escribir código Playwright. El MCP abstrae las herramientas de control del navegador y el agente las usa directamente. Basta con que describas el flujo que quieres auditar en lenguaje natural.

    ¿axe-core cubre todos los criterios WCAG?
    Cubre los criterios que son detectables automáticamente — menos de la mitad de los criterios de WCAG 2.1. El resto requiere evaluación humana. Pero ese 30-40% incluye los problemas más comunes y los más graves.

    ¿El agente puede auditar aplicaciones con autenticación?
    Sí. Puedes darle al agente las credenciales de una cuenta de prueba, o configurar el MCP para que arranque el navegador con una sesión ya autenticada. El agente navega como un usuario real, incluyendo el flujo de login.

    ¿Qué diferencia hay entre esto y Lighthouse?
    Lighthouse audita métricas de performance, SEO básico y accesibilidad en un snapshot estático. Un agente con MCP de Playwright puede auditar flujos interactivos completos — formularios multipaso, modales, estados de error, interacciones con el teclado — y generar reportes adaptados a tu contexto específico, no a un checklist genérico.

    ¿Puedo usar esto con cualquier framework frontend?
    Sí. El agente interactúa con el navegador, no con el framework. Funciona igual con Angular, React, Vue o cualquier app renderizada en el cliente o en el servidor.

    La próxima vez que un cliente te muestre métricas de abandono con todos los tests en verde, ya sabes qué está pasando — y cómo resolverlo.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.