WebApp Testing (Anthropic): автопилот для UI-тестирования
Когда нужно протестировать local веб-приложение, Claude Code с WebApp Testing скиллом от Anthropic превращается в QA-инженера с Playwright под капотом.
Что за магия
WebApp Testing — скилл для interacting с локальными веб-приложениями через Playwright. Claude Code может запустить сервер, открыть браузер, кликать по UI, делать скриншоты и проверять functionality.
Возможности:
- Server lifecycle management** — автоматически стартует/останавливает сервера
- UI interaction** — clicks, form filling, navigation
- Visual verification** — скриншоты full-page или specific элементов
- Console monitoring** — captures browser logs для debugging
- Multi-server support** — backend + frontend одновременно
Архитектура: Native Python Playwright scripts + helper utilities
Decision Tree: выбор подхода
User task → Is it static HTML?
├─ Yes → Read HTML file directly to identify selectors
│ ├─ Success → Write Playwright script using selectors
│ └─ Fails/Incomplete → Treat as dynamic (below)
│
└─ No (dynamic webapp) → Is the server already running?
├─ No → Run: python scripts/with_server.py --help
│ Then use the helper + write simplified Playwright script
│
└─ Yes → Reconnaissance-then-action:
1. Navigate and wait for networkidle
2. Take screenshot or inspect DOM
3. Identify selectors from rendered state
4. Execute actions with discovered selectorsЛогика простая: static HTML → прямое чтение файлов, dynamic apps → Playwright automation.
Helper Scripts: черные ящики
Скилл находится: /skills/webapp-testing/
Ключевое правило: ALWAYS run scripts with --help first. НЕ читай source код — they're designed как black-box utilities.
Основной helper:
- scripts/with_server.py — manages server lifecycle (supports multiple servers)
Reference examples:
- examples/element_discovery.py — discovering buttons, links, inputs
- examples/static_html_automation.py — file:// URLs для local HTML
- examples/console_logging.py — capturing console logs
Single Server Management
Basic usage:
# Сначала --help для понимания опций
python scripts/with_server.py --help
# Запуск с автоматическим server management
python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.pyЧто происходит:
- with_server.py запускает npm run dev
- Ждёт пока сервер поднимется на порту 5173
- Выполняет your_automation.py
- Автоматически останавливает сервер
Automation script example:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# ALWAYS headless mode для автоматизации
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Сервер уже running благодаря with_server.py
page.goto('http://localhost:5173')
# CRITICAL: Wait for JS to execute
page.wait_for_load_state('networkidle')
# ... your automation logic
browser.close()Multi-Server Support
Backend + Frontend setup:
python scripts/with_server.py \
--server "cd backend && python server.py" --port 3000 \
--server "cd frontend && npm run dev" --port 5173 \
-- python test_full_stack.pyUse case: тестирование full-stack приложения где frontend делает API calls к backend.
Test script example:
from playwright.sync_api import sync_playwright
def test_full_stack():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Frontend server
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle')
# Test API integration
page.click('button:text("Load Data")')
page.wait_for_selector('.data-loaded')
# Verify API response rendered
assert 'Data from API' in page.text_content('.api-data')
browser.close()
print("✓ Full-stack test passed!")
test_full_stack()Reconnaissance-Then-Action Pattern
Основная methodология: сначала explore, потом automate.
Step 1: Visual reconnaissance
from playwright.sync_api import sync_playwright
def inspect_page():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle') # CRITICAL!
# Full page screenshot для понимания layout
page.screenshot(path='/tmp/page_overview.png', full_page=True)
# Inspect DOM structure
content = page.content()
print(content[:1000]) # First 1000 chars
# Find all clickable elements
buttons = page.locator('button').all()
links = page.locator('a').all()
print(f"Found {len(buttons)} buttons, {len(links)} links")
browser.close()
inspect_page()Step 2: Selector discovery
def discover_selectors():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
# Find elements by text content
login_button = page.locator('button:text("Login")')
nav_links = page.locator('nav a')
# Find form elements
email_input = page.locator('input[type="email"]')
password_input = page.locator('input[type="password"]')
# Test selectors exist
print(f"Login button exists: {login_button.is_visible()}")
print(f"Found {nav_links.count()} nav links")
browser.close()
discover_selectors()Step 3: Action execution
def execute_test_flow():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
# Execute discovered actions
page.fill('input[type="email"]', 'test@example.com')
page.fill('input[type="password"]', 'password123')
page.click('button:text("Login")')
# Wait for result and verify
page.wait_for_selector('.dashboard')
assert page.is_visible('.user-info')
print("✓ Login flow successful!")
browser.close()
execute_test_flow()Advanced Patterns
Console log monitoring:
def test_with_console_monitoring():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Capture console messages
console_messages = []
page.on('console', lambda msg: console_messages.append(msg.text))
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
# Trigger actions that might cause JS errors
page.click('.problematic-button')
# Check for errors
errors = [msg for msg in console_messages if 'error' in msg.lower()]
if errors:
print(f"❌ Found {len(errors)} console errors:")
for error in errors:
print(f" {error}")
else:
print("✓ No console errors detected")
browser.close()
test_with_console_monitoring()Screenshot comparison:
def visual_regression_test():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
# Take screenshot of specific component
component = page.locator('.main-dashboard')
component.screenshot(path='/tmp/dashboard_current.png')
# Compare with baseline (manual process)
print("📷 Screenshot saved for manual comparison")
browser.close()
visual_regression_test()Form automation:
def test_complex_form():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto('http://localhost:3000/form')
page.wait_for_load_state('networkidle')
# Fill complex form
page.fill('#firstName', 'John')
page.fill('#lastName', 'Doe')
page.select_option('#country', 'USA')
page.check('#agreeTerms')
page.fill('#comments', 'This is a test comment')
# Submit and verify
page.click('button[type="submit"]')
page.wait_for_selector('.success-message')
success_text = page.text_content('.success-message')
assert 'Form submitted successfully' in success_text
print("✓ Complex form submission successful!")
browser.close()
test_complex_form()Static HTML Testing
Для local HTML files:
def test_static_html():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Use file:// protocol для local files
page.goto('file:///path/to/your/index.html')
# Static HTML loads immediately, no networkidle needed
page.wait_for_timeout(500) # Brief wait for rendering
# Test static functionality
assert 'Welcome' in page.title()
# Click static navigation
page.click('a[href="#about"]')
page.wait_for_timeout(500)
# Check anchor navigation worked
assert '#about' in page.url
browser.close()
test_static_html()Best Practices
1. Always wait for networkidle:
# ❌ Don't inspect before JS executes
page.goto('http://localhost:3000')
page.locator('button') # Might miss dynamically added buttons
# ✅ Wait for complete loading
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle') # CRITICAL
page.locator('button') # Now sees all buttons2. Use descriptive selectors:
# ✅ Good selectors (readable, stable)
page.click('button:text("Submit")')
page.click('[data-testid="login-button"]')
page.click('role=button[name="Save"]')
# ❌ Fragile selectors
page.click('.btn-primary.mt-4') # CSS classes can change
page.click('body > div:nth-child(3) > button') # Position-dependent3. Add appropriate waits:
# Wait for specific element
page.click('button:text("Load Data")')
page.wait_for_selector('.data-table')
# Wait for state change
page.click('.toggle-menu')
page.wait_for_function('() => document.querySelector(".menu").style.display !== "none"')
# Wait for network requests
page.wait_for_load_state('networkidle')4. Handle errors gracefully:
def robust_test():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
try:
page.goto('http://localhost:3000', timeout=10000)
page.wait_for_load_state('networkidle', timeout=5000)
# Test logic here
except Exception as e:
print(f"❌ Test failed: {e}")
page.screenshot(path='/tmp/error_screenshot.png')
finally:
browser.close()
robust_test()Common Use Cases
E2E Login Flow:
def test_login_flow():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Navigate to login
page.goto('http://localhost:3000/login')
page.wait_for_load_state('networkidle')
# Fill credentials
page.fill('#email', 'test@example.com')
page.fill('#password', 'password123')
page.click('button[type="submit"]')
# Verify redirect to dashboard
page.wait_for_url('**/dashboard')
assert 'Dashboard' in page.title()
# Test logout
page.click('.logout-button')
page.wait_for_url('**/login')
print("✓ Complete login/logout flow tested")
browser.close()
test_login_flow()API Integration Testing:
def test_api_integration():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Monitor network requests
requests = []
page.on('request', lambda request: requests.append(request.url))
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
# Trigger API call
page.click('button:text("Fetch Data")')
page.wait_for_selector('.api-data')
# Verify API was called
api_calls = [url for url in requests if '/api/' in url]
assert len(api_calls) > 0, "No API calls detected"
# Verify data displayed
data_text = page.text_content('.api-data')
assert 'data loaded' in data_text.lower()
print(f"✓ API integration test passed ({len(api_calls)} API calls)")
browser.close()
test_api_integration()Заключение
WebApp Testing скилл от Anthropic превращает Claude Code в полноценного QA-инженера:
- Automated server management** — no manual setup
- Multi-environment support** — static HTML + dynamic webapps
- Reconnaissance-driven approach** — explore first, automate second
- Production-grade testing** — console monitoring, screenshots, robust error handling
Результат: Claude Code может comprehensive протестировать любое local веб-приложение от static HTML до complex full-stack setup.
GitHub: https://github.com/anthropics/skills/tree/main/skills/webapp-testing
Этот скилл превращает testing из manual chore в automated workflow, где AI понимает UI и может interaction с ним как human tester.
*Тестирование: когда AI кликает быстрее человека.* 🧪
> Пока нет комментариев