webapp-testing
npx skills add https://github.com/5dlabs/cto --skill webapp-testing
Agent 安装分布
Skill 文档
Web Application Testing
Test local web applications using Playwright with a systematic approach.
Functional vs Visual Testing
CRITICAL: Choose the right approach based on what you’re verifying.
| Testing Type | Method | Returns | Use For |
|---|---|---|---|
| Functional | Accessibility tree / DOM inspection | Text (parseable) | Button exists, text appears, form works, element state |
| Visual | Screenshot | Image (not parseable) | Layout, colors, styling, animations, visual regression |
Functional Testing (Checking Behavior)
Use DOM inspection or accessibility tree queries when verifying behavior:
# Get all interactive elements
buttons = page.locator('button').all()
for btn in buttons:
print(btn.text_content()) # Agent CAN read and verify this
# Verify element exists and has correct state
assert page.locator('text=Submit').is_visible()
assert page.locator('#email').input_value() == 'test@example.com'
For MCP browser tools: Use take_snapshot which returns the accessibility tree as text.
Visual Testing (Checking Appearance)
Use screenshots ONLY when verifying appearance:
# Capture for visual comparison
page.screenshot(path='dashboard.png', full_page=True)
For MCP browser tools: Use take_screenshot when checking layout, colors, or styling.
Common Mistake
# BAD: Taking screenshot to verify button exists
page.screenshot(path='check.png') # Agent cannot "read" this image!
# GOOD: Use DOM/accessibility to verify button exists
assert page.locator('text=Submit').is_visible()
Decision Tree: Choose Your Approach
User task â Is it static HTML?
â
ââ Yes â Read HTML file directly to identify selectors
â ââ Write Playwright script using discovered selectors
â
ââ No (dynamic webapp) â Is the server already running?
â
ââ No â Start server first, then test
â (use process management or test framework)
â
ââ Yes â Use Reconnaissance-Then-Action pattern:
1. Navigate and wait for networkidle
2. Take screenshot or inspect DOM
3. Identify selectors from rendered state
4. Execute actions with discovered selectors
Reconnaissance-Then-Action Pattern
For dynamic apps, always inspect before acting:
Step 1: Navigate and Wait
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:5173')
# CRITICAL: Wait for JS to execute
page.wait_for_load_state('networkidle')
Step 2: Inspect Rendered DOM
# Option A: Screenshot for visual inspection
page.screenshot(path='/tmp/inspect.png', full_page=True)
# Option B: Get page content
content = page.content()
# Option C: Find specific elements
buttons = page.locator('button').all()
for btn in buttons:
print(btn.text_content())
Step 3: Identify Selectors
From inspection results, determine the right selectors:
| Selector Type | Example | When to Use |
|---|---|---|
| Text | text=Submit |
Visible button/link text |
| Role | role=button[name="Submit"] |
Accessibility-friendly |
| CSS | #submit-btn, .primary-action |
Unique IDs or classes |
| Data attributes | [data-testid="submit"] |
Test-specific attributes |
Step 4: Execute Actions
# Now interact with discovered selectors
page.locator('text=Submit').click()
page.locator('#email').fill('test@example.com')
page.locator('form').press('Enter')
Common Pitfall
â Don’t inspect DOM before waiting for networkidle on dynamic apps
â
Do wait for page.wait_for_load_state('networkidle') before inspection
Without this wait, you’ll see the initial HTML before JavaScript renders the actual UI.
Multi-Server Testing
When testing apps with separate frontend and backend:
import subprocess
import time
# Start backend
backend = subprocess.Popen(['python', 'server.py'], cwd='backend/')
# Start frontend
frontend = subprocess.Popen(['npm', 'run', 'dev'], cwd='frontend/')
# Wait for servers to be ready
time.sleep(5) # Or use health check polling
try:
# Run your tests
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto('http://localhost:5173')
# ... test logic
finally:
backend.terminate()
frontend.terminate()
Testing Patterns
Form Testing
# Fill form
page.locator('#name').fill('Test User')
page.locator('#email').fill('test@example.com')
page.locator('select#country').select_option('US')
page.locator('input[type="checkbox"]').check()
# Submit and verify
page.locator('button[type="submit"]').click()
page.wait_for_selector('.success-message')
assert page.locator('.success-message').is_visible()
Navigation Testing
# Click link and verify navigation
page.locator('text=About').click()
page.wait_for_url('**/about')
assert 'About' in page.title()
API Response Testing
# Intercept network requests
with page.expect_response('**/api/users') as response_info:
page.locator('button.load-users').click()
response = response_info.value
assert response.status == 200
data = response.json()
assert len(data['users']) > 0
Visual Regression
# Compare screenshots
page.goto('http://localhost:5173/dashboard')
page.wait_for_load_state('networkidle')
# Take screenshot for comparison
page.screenshot(path='dashboard-current.png', full_page=True)
# Compare with baseline (use image diff tool)
Console & Error Monitoring
# Capture console messages
console_messages = []
page.on('console', lambda msg: console_messages.append(msg.text))
# Capture errors
errors = []
page.on('pageerror', lambda err: errors.append(str(err)))
# Run test
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle')
# Check for issues
assert len(errors) == 0, f"Page errors: {errors}"
assert not any('error' in msg.lower() for msg in console_messages)
Wait Strategies
| Method | Use When |
|---|---|
wait_for_load_state('networkidle') |
Initial page load, SPA navigation |
wait_for_selector('.element') |
Waiting for specific element to appear |
wait_for_url('**/path') |
After navigation actions |
wait_for_response('**/api/**') |
After triggering API calls |
wait_for_timeout(1000) |
Last resort for timing-dependent UIs |
Best Practices
- Always launch headless:
browser = p.chromium.launch(headless=True) - Always close browser: Use
withcontext or explicitbrowser.close() - Use descriptive selectors: Prefer
text=,role=, ordata-testid=over fragile CSS - Add appropriate waits: Don’t assume elements are immediately available
- Capture evidence: Screenshot on failures for debugging
- Isolate tests: Each test should set up its own state
Debugging Tips
# Slow down for debugging
browser = p.chromium.launch(headless=False, slow_mo=500)
# Pause for manual inspection
page.pause()
# Get element state
element = page.locator('#my-button')
print(f"Visible: {element.is_visible()}")
print(f"Enabled: {element.is_enabled()}")
print(f"Text: {element.text_content()}")