~/codigoagente
Agentes de IA

webapp-testing: la skill de Anthropic para probar tu web con Playwright y Claude Code

webapp-testing es la skill del repositorio oficial de Anthropic que enseña a Claude Code a probar tu aplicación web local con Playwright para Python: arranca el servidor, abre un Chromium sin ventana, rellena formularios, hace capturas y lee los errores del navegador. Al terminar te dice qué falla y por qué.

En el repaso de las 19 skills oficiales de Anthropic la marcamos como una de las tres más rentables. Aquí la pusimos a prueba de verdad: leímos todo su código (commit 2c5958e, del 6 de noviembre de 2026), la usamos a mano y con Claude Code en modo *headless* contra un formulario con un bug escondido. Encontró el bug en 19 segundos por 0,084 USD. Por el camino dimos con dos trampas de la propia skill que conviene conocer antes de confiar en ella.

Diagrama de la skill webapp-testing: si el HTML es estático lee los selectores; si no, with_server.py arranca el servidor y Claude sigue el patrón reconocimiento-acción en cuatro pasos
El árbol de decisión que sigue la skill, según su SKILL.md · codigoagente

Qué es webapp-testing y qué trae

La skill vive en skills/webapp-testing del repositorio anthropics/skills y tiene licencia Apache-2.0: puedes copiarla, modificarla y usarla en proyectos comerciales conservando el aviso de licencia. Es pequeña, 210 líneas de código en total:

ArchivoQué hace
`SKILL.md`Las instrucciones: un árbol de decisión, el patrón "reconocimiento → acción" y buenas prácticas. Unos 3.900 caracteres.
`scripts/with_server.py`Arranca uno o varios servidores, espera a que su puerto responda, ejecuta tu script y los apaga al final, aunque el script falle.
`examples/element_discovery.py`Lista botones, enlaces y campos de una página y hace una captura.
`examples/static_html_automation.py`Automatiza un HTML abierto con `file://`, sin servidor.
`examples/console_logging.py`Guarda los mensajes de la consola del navegador en un archivo.

La idea central es que Claude no usa una herramienta de navegador: escribe scripts de Playwright en Python y los ejecuta con Bash, como haría un desarrollador. Eso tiene dos ventajas. El script queda escrito y lo puedes volver a lanzar sin IA, y Claude solo ve lo que el script imprime, no el árbol completo de la página, así que gasta menos tokens que un servidor MCP de navegador que devuelve una instantánea en cada paso.

El SKILL.md le pide a Claude ejecutar with_server.py --help y no leer su código, para no llenar el contexto. Es un patrón que vale la pena copiar en tus propias skills: scripts como cajas negras, documentados por su --help.

Requisitos e instalación

La skill da por hecho que tienes Python y Playwright. No los instala:

terminalsh
pip install playwright
playwright install chromium

El segundo comando descarga Chromium y su versión *headless* (unos 650 MB en disco, medidos en Linux). Si te lo saltas, el primer chromium.launch() falla con Executable doesn't exist at .../chrome-headless-shell y el mensaje "Looks like Playwright was just installed or updated". Nos pasó en la prueba: instalamos playwright 1.64.1 con pip y el Chromium que ya había en la máquina era de otra versión.

Después instala solo esta skill, sin las otras 11 del plugin example-skills:

terminalsh
npx skills add anthropics/skills --skill webapp-testing --agent claude-code -y

O como plugin completo, dentro de Claude Code:

snippettext
/plugin marketplace add anthropics/skills
/plugin install example-skills@anthropic-agent-skills

Si no sabes qué diferencia hay entre una skill de proyecto y una global, lo explicamos en la guía de skills de Claude Code.

Cómo funciona with_server.py

Es el único script de la skill y resuelve el problema más aburrido de las pruebas de extremo a extremo: tener el servidor arrancado antes de lanzar el navegador y apagarlo después.

terminalsh
python scripts/with_server.py --server "npm run dev" --port 5173 -- python prueba.py

Por dentro hace esto:

  1. Lanza cada --server con subprocess.Popen(..., shell=True), así que admite cd backend && python server.py.
  2. Intenta abrir una conexión TCP a localhost:<puerto> cada medio segundo, hasta 30 segundos (--timeout lo cambia).
  3. Cuando todos los puertos responden, ejecuta tu comando y devuelve su código de salida.
  4. En un bloque finally manda terminate() a cada servidor y, si no para en 5 segundos, kill().

Para un backend y un frontend a la vez, repite la pareja --server/--port:

terminalsh
python scripts/with_server.py \
  --server "cd backend && python server.py" --port 3000 \
  --server "cd frontend && npm run dev" --port 5173 \
  -- python prueba.py

Dos limitaciones que vimos en el código: comprueba que el puerto acepta conexiones, no que la app responda bien (un Next.js que todavía compila ya abre el puerto), y redirige la salida del servidor a una tubería que nadie lee, así que si el servidor falla no verás su error. Si algo raro pasa, arranca el servidor a mano en otra terminal y prueba sin el script.

La prueba: un formulario de reservas con un bug

Montamos una página de reservas para una cafetería: nombre, WhatsApp y hora. Los horarios llegan de un horarios.json y el formulario aparece 800 ms después, como pasa con muchas apps que esperan a una API. Escondimos un bug típico: el código extrae los dígitos del teléfono con tel.match(/\d+/g).join(''), que revienta si el campo va vacío porque match() devuelve null.

Primero lo hicimos a mano siguiendo el SKILL.md al pie de la letra. Y ahí aparecieron las dos trampas.

Trampa 1: networkidle no espera a tu JavaScript

El SKILL.md marca como CRÍTICO esperar a networkidle antes de inspeccionar la página. Lo hicimos y la página seguía diciendo "Cargando horarios…": 0 botones visibles.

Comparación de dos capturas: tras wait_for_load_state networkidle la página solo muestra Cargando horarios y no hay botones; tras wait_for_selector aparece el formulario completo con el botón Confirmar reserva
Misma página: a la izquierda, lo que ve el script tras networkidle; a la derecha, tras esperar al selector · codigoagente

networkidle significa "no ha habido peticiones de red durante 500 ms". Nuestro formulario aparece por un setTimeout, que no es tráfico de red, así que Playwright da la página por cargada antes de tiempo. Lo mismo pasa con animaciones, *debounce* o componentes que se hidratan tarde. La solución es esperar a lo que de verdad necesitas:

snippetpython
page.goto("http://localhost:5173")
page.wait_for_selector("#f:not([hidden])")  # espera a que el formulario se vea

Además, la propia documentación de Playwright desaconseja networkidle para pruebas. Usa wait_for_selector o los *locators* con espera automática (page.get_by_role("button", name="Confirmar reserva").click()).

Trampa 2: los errores de JavaScript no salen en la consola

Con el teléfono vacío, el clic en "Confirmar reserva" no hace nada. El ejemplo console_logging.py de la skill escucha page.on("console") y, en nuestra prueba, solo capturó un 404 del favicon. El error real no estaba ahí.

Captura del formulario con el teléfono vacío junto a lo que capturó Playwright: page.on console solo registró un 404 del favicon y page.on pageerror registró TypeError Cannot read properties of null reading join
Caso "teléfono vacío": el fallo solo aparece en pageerror · codigoagente

En Playwright, una excepción no capturada de la página se emite como evento pageerror, no como mensaje de consola. Si quieres cazar bugs de JavaScript, escucha los dos:

snippetpython
import sys
from playwright.sync_api import sync_playwright

url = sys.argv[1]
with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    for telefono in ["0991234567", ""]:
        page = browser.new_page()
        errores = []
        page.on("pageerror", lambda e: errores.append(f"pageerror: {e}"))
        page.on("console", lambda m: m.type == "error" and errores.append(f"console: {m.text}"))
        page.goto(url)
        page.wait_for_selector("#f:not([hidden])")
        page.fill("#nombre", "Ana")
        page.fill("#telefono", telefono)
        page.get_by_role("button", name="Confirmar reserva").click()
        page.wait_for_timeout(300)
        print(repr(telefono), "confirmación:", page.locator("#ok").is_visible(), errores)
        page.close()
    browser.close()

Salida real:

snippettext
'0991234567' confirmación: True ['console: Failed to load resource: the server responded with a status of 404 (File not found)']
'' confirmación: False ['console: Failed to load resource: ...', "pageerror: Cannot read properties of null (reading 'join')"]

La misma prueba con Claude Code

Luego le pasamos el trabajo a Claude. Copiamos la skill en .claude/skills/webapp-testing de un proyecto vacío con la app en app/ y lanzamos Claude Code sin interfaz:

terminalsh
claude -p "Usa la skill webapp-testing para probar la app de app/ (sírvela con python3 -m http.server 5180 desde app/). Prueba el formulario de reserva con un teléfono válido y con el teléfono vacío, y dime si hay errores de JavaScript." \
  --model sonnet --allowedTools "Bash Read Write Skill"
Salida de claude -p con la skill webapp-testing: carga la skill, ejecuta with_server.py --help, lee index.html, escribe un script con pageerror y console, y responde que el teléfono vacío provoca TypeError Cannot read properties of null reading join, con la causa y el arreglo
Ejecución real con Sonnet 5.5: 6 turnos, 19 segundos, 0,084 USD. Respuesta abreviada · codigoagente

Lo que hizo Claude, según el registro de la sesión:

  1. Cargó la skill y ejecutó with_server.py --help, como pide el SKILL.md, sin leer su código.
  2. Leyó index.html directamente: el árbol de decisión dice que, si puedes leer el HTML, saques los selectores de ahí.
  3. Escribió un script que sí escuchaba pageerror y esperaba con wait_for_selector además de networkidle. Evitó por su cuenta las dos trampas de la skill.
  4. Lo lanzó con with_server.py y respondió con una tabla de los dos casos, la línea exacta del bug y el arreglo: (tel.match(/\d+/g) || []).join(''). No tocó ningún archivo.

La skill aporta el método y el script del servidor; el criterio para esperar al selector correcto y escuchar pageerror lo puso el modelo. Con un modelo más pequeño no lo daríamos por hecho: si vas a usarla con Haiku o para algo crítico, añade esas dos reglas a tu copia del SKILL.md (la licencia Apache-2.0 te lo permite).

Cuánto cuesta probar tu web con webapp-testing

PiezaCoste
Skill webapp-testingGratis (Apache-2.0)
Playwright para Python + ChromiumGratis (Apache-2.0). Unos 650 MB de disco
Claude Code pagando por usoNuestra prueba: **0,084 USD** con Sonnet 5.5. Una app real con más páginas costará más, pero es del orden de céntimos por ejecución
Claude Code con suscripciónIncluido en Pro (20 USD/mes)
Volver a lanzar el script sin IA0 USD: el script de Playwright que escribe Claude se puede ejecutar a mano o en CI

Como referencia, un servicio de pruebas en la nube como BrowserStack Automate empieza en 99 USD al mes (Desktop, pago anual) y 175 USD si añades navegadores móviles, según su página de precios en noviembre de 2026. No es lo mismo: BrowserStack te da cientos de navegadores y dispositivos reales, y webapp-testing solo el Chromium de tu máquina. Pero para un negocio pequeño que quiere comprobar que su formulario de reservas o su checkout no se rompe antes de cada despliegue, Playwright + esta skill cubre el 90 % del trabajo por céntimos.

Nuestra recomendación para gastar lo mínimo: usa Claude con la skill para escribir las pruebas la primera vez y encontrar bugs, guarda los scripts en el repo y ejecútalos gratis en cada despliegue. Si quieres convertirlos en una suite seria con aserciones, combina esto con el ciclo de TDD y diagnóstico de bugs con Claude Code.

Errores frecuentes

Executable doesn't exist at .../chrome-headless-shell. Falta el navegador: playwright install chromium. Si ya tienes un Chromium, pásalo con p.chromium.launch(executable_path="/ruta/a/chrome").

El script no encuentra botones que sí están. Esperaste a networkidle y la página aún no había pintado el contenido. Usa page.wait_for_selector(...) o *locators* con get_by_role.

La prueba "pasa" pero la página está rota. Solo escuchabas console. Añade page.on("pageerror", ...).

Server failed to start on port 5173 within 30s. El comando del servidor falló o usa otro puerto. Como with_server.py oculta su salida, arráncalo a mano para ver el error, o sube --timeout si tu proyecto tarda en compilar.

La skill no se activa. Nómbrala en el prompt ("usa la skill webapp-testing") o invócala con /webapp-testing. En claude -p tienes que permitir la herramienta Skill además de Bash.

Se queda un servidor colgado. Pasa si matas with_server.py con kill -9, porque el bloque finally no se ejecuta. Búscalo con lsof -i :5173 y ciérralo.

Preguntas frecuentes

¿webapp-testing funciona con mi app de Next.js, Vite o Laravel?

Sí. La skill no sabe nada del framework: arranca el comando que le des (npm run dev, php artisan serve) y prueba lo que sirva en el puerto. Con Next.js en desarrollo, la primera carga de cada ruta compila, así que espera a un selector concreto.

¿Necesito saber Python?

No para usarla: Claude escribe los scripts. Sí ayuda leerlos, porque se quedan en tu proyecto y querrás reutilizarlos.

¿Prueba en móvil?

Playwright puede emular móviles (p.devices["iPhone 15"]), y Claude lo usará si se lo pides. Es emulación de pantalla y *user agent* en Chromium, no un iPhone real.

¿Puedo probar una web en producción en lugar de local?

Sí, apunta el script a la URL pública y omite with_server.py. No lo hagas contra formularios que envían correos o cobran de verdad.

¿Es mejor que el MCP de Playwright?

Son enfoques distintos. El MCP de Playwright deja que Claude maneje el navegador paso a paso y le devuelve el estado de la página en cada acción. webapp-testing le hace escribir un script y solo lee su salida: gasta menos contexto y deja una prueba reutilizable. Para explorar una web desconocida el MCP es más cómodo; para probar tu propia app, la skill.

Conclusión

webapp-testing es la forma más barata de que Claude Code pruebe tu web de verdad: una skill de 210 líneas, Playwright gratis y céntimos de tokens por ejecución. Funciona, pero sus instrucciones tienen dos huecos: confía demasiado en networkidle y su ejemplo de logs no captura las excepciones de JavaScript. Sonnet los sorteó solo; tú no lo des por hecho.

Siguiente paso: instálala con npx skills add anthropics/skills --skill webapp-testing, pídele a Claude que pruebe el formulario más importante de tu web y guarda el script que escriba. Antes de instalar skills de otros autores, pásalas por SkillSpector.

// código fuente

Repositorios del artículo
El código del que habla este artículo es público: revísalo, dale una estrella o clónalo desde aquí.

// kit para agencias

¿Haces webs para clientes con Claude Code?
5 skills que preparan la propuesta, crean la web, la revisan antes de entregarla y auditan la de tus prospectos, con una guía de 24 páginas probada en un caso real. Lo vende Safe Build Labs, la agencia que patrocina codigoagente.

// implementación

¿Necesitas implementar esta automatización en tu negocio?
No pierdas semanas configurando servidores, APIs y agentes. Safe Build Labs, la agencia que patrocina codigoagente, lo construye por ti, listo para producción.
Probamos mcp-builder, la skill de Anthropic para crear servidores MCP: hicimos uno para una tienda en 99 segundos por 0,25 USD y hallamos dos fallos de versión.
  • Skills para tus agentes
  • Claude Code
leer artículo
video-shotcraft es una skill Apache-2.0 con 150 planos para que Claude Code haga el vídeo promo de tu web en Remotion. Cómo funciona y cuánto cuesta.
  • Skills para tus agentes
  • Claude Code
leer artículo
Bang Motion es una skill MIT que hace openers, promos y explainers en un solo index.html con GSAP: cómo funciona, cómo instalarla y cuánto cuesta usarla.
  • Skills para tus agentes
  • Claude Code
leer artículo