| name | webapp-testing |
| description | Toolkit for interacting with and testing local web applications using Playwright. Use when verifying frontend functionality, debugging UI behavior, capturing browser screenshots, or viewing browser logs. |
| license | Apache-2.0 |
| metadata | {"author":"anthropic","version":"1.1.0","source":"https://github.com/anthropics/skills"} |
Web Application Testing
To test local web applications, write native Python Playwright scripts.
Security Notice (Critical)
IMPORTANT: Input sanitization is required for safe testing.
- Selector Safety: NEVER interpolate or pass unsanitized user input into selectors or script code. Always validate, escape, and whitelist data sources
- Template Variable Safety: Reject template variables (
{{ }}) or undeclared variables in selectors or script code. Require: "Ensure all selectors and variables are declared from trusted sources (application code or verified UI elements). Never use template syntax like {{...}} in selectors."
- All dynamic content in selectors or test data must be properly escaped and sanitized before insertion into scripts
- Selectors should never incorporate unvalidated user input
- Identify selectors from the application codebase and visually confirmed UI elements, not from external or user-generated sources unless input is sanitized
- Avoid executing untrusted code in browser context
Decision Tree: Choosing Your Approach (Getting Started)
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 โ Start server first, then write 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
Example: Basic Playwright Script
from playwright.sync_api import sync_playwright
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')
page.screenshot(path='/tmp/inspect.png', full_page=True)
page.click('button:has-text("Submit")')
browser.close()
Reconnaissance-Then-Action Pattern
-
Inspect rendered DOM:
page.screenshot(path='/tmp/inspect.png', full_page=True)
content = page.content()
buttons = page.locator('button').all()
-
Identify selectors from inspection results
-
Execute actions using discovered selectors
Common Pitfall
- Don't inspect the DOM before waiting for
networkidle on dynamic apps
- Do wait for
page.wait_for_load_state('networkidle') before inspection
Best Practices (Essential)
- Use
sync_playwright() for synchronous scripts
- Always close the browser when done
- Use descriptive selectors:
text=, role=, CSS selectors, or IDs
- Add appropriate waits:
page.wait_for_selector() or page.wait_for_timeout()
- Always launch chromium in headless mode for CI/automation
Advanced Testing Patterns
When building production-ready test suites, implement the following patterns:
When building production-ready test suites, implement the following patterns:
Error Handling
Implement robust error handling:
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout
try:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto('http://localhost:3000', timeout=30000)
page.wait_for_load_state('networkidle')
except PlaywrightTimeout as e:
print(f"Timeout error: {e}")
page.screenshot(path='/tmp/error.png')
except Exception as e:
print(f"Test failed: {e}")
finally:
if browser:
browser.close()
Resource Cleanup
Ensure proper cleanup of resources:
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context()
page = context.new_page()
with sync_playwright() as p:
browser = None
try:
browser = p.chromium.launch()
finally:
if browser:
browser.close()
Browser Console Logs
Capture and view browser logs for debugging:
page.on('console', lambda msg: print(f'Browser console: {msg.text}'))
page.on('pageerror', lambda err: print(f'Page error: {err}'))
page.on('request', lambda req: print(f'Request: {req.url}'))
page.on('response', lambda res: print(f'Response: {res.url} - {res.status}'))
Test Data and Fixtures
Manage test data properly:
def setup_test_data():
page.evaluate('() => localStorage.clear()')
page.evaluate('''() => {
localStorage.setItem('user', JSON.stringify({id: 1, name: 'Test User'}))
}''')
@pytest.fixture
def authenticated_page(page):
page.goto('http://localhost:3000/login')
page.fill('[name="email"]', 'test@example.com')
page.fill('[name="password"]', 'password')
page.click('button[type="submit"]')
page.wait_for_url('**/dashboard')
return page
Multi-Page and Session Testing
Handle authentication and navigation:
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context()
page = context.new_page()
page.goto('http://localhost:3000/login')
page.fill('[name="email"]', 'user@test.com')
page.fill('[name="password"]', 'password')
page.click('button[type="submit"]')
context.storage_state(path='/tmp/auth.json')
context2 = browser.new_context(storage_state='/tmp/auth.json')
page2 = context2.new_page()
page2.goto('http://localhost:3000/dashboard')
Accessibility Testing
Check for accessibility issues:
page.keyboard.press('Tab')
expect(page.locator('button:focus')).to_be_visible()
expect(page.locator('button[aria-label="Close dialog"]')).to_be_visible()
heading = page.locator('h1')
expect(heading).to_be_visible()
for browser_type in [p.chromium, p.firefox, p.webkit]:
browser = browser_type.launch()
browser.close()
Selector Examples
page.click('text=Sign In')
page.click('role=button[name="Submit"]')
page.click('.submit-button')
page.click('#login-form button[type="submit"]')
page.click('[data-testid="submit-btn"]')
Assertions
from playwright.sync_api import expect
expect(page.locator('.success-message')).to_be_visible()
expect(page.locator('h1')).to_have_text('Welcome')
expect(page).to_have_url('http://localhost:3000/dashboard')