WebApp Testing (Anthropic): автопилот для UI-тестирования

10.03.2026обновлено 6 апр

Когда нужно протестировать 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

Что происходит:

  1. with_server.py запускает npm run dev
  2. Ждёт пока сервер поднимется на порту 5173
  3. Выполняет your_automation.py
  4. Автоматически останавливает сервер

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.py

Use 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 buttons

2. 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-dependent

3. 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 кликает быстрее человека.* 🧪

TG

> Пока нет комментариев