## Decision Tree: Choosing Your Approach
```
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 → Use server management helper
│ Then 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
```
## Core Testing Patterns
### Basic Playwright Script
```python
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')
page.wait_for_load_state('networkidle')
browser.close()
```
### Server Lifecycle Management
For applications where you need to start the server:
**Single Server:**
```bash
python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.py
```
**Multiple Servers (Backend + Frontend):**
```bash
python scripts/with_server.py \
--server "cd backend && python server.py" --port 3000 \
--server "cd frontend && npm run dev" --port 5173 \
-- python your_automation.py
```
### Reconnaissance-Then-Action Pattern
When testing dynamic applications, always inspect first:
```python
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')
page.wait_for_load_state('networkidle')
page.screenshot(path='/tmp/inspect.png', full_page=True)
content = page.content()
buttons = page.locator('button').all()
print(f"Found {len(buttons)} buttons:")
for btn in buttons:
print(f" - {btn.inner_text()}")
page.click('button:has-text("Submit")')
browser.close()
```
## Advanced Techniques
### Element Discovery
```python
def discover_interactive_elements(page):
"""Find all interactive elements on the page"""
elements = {
'buttons': page.locator('button').all(),
'links': page.locator('a').all(),
'inputs': page.locator('input').all(),
'selects': page.locator('select').all(),
'textareas': page.locator('textarea').all()
}
report = []
for element_type, items in elements.items():
report.append(f"\n{element_type.upper()} ({len(items)} found):")
for item in items[:10]: # Limit output
try:
text = item.inner_text() or item.get_attribute('placeholder') or item.get_attribute('aria-label')
report.append(f" - {text[:50] if text else '[no text]'}")
except:
report.append(" - [element not accessible]")
return "\n".join(report)
```
### Console Log Capture
```python
def capture_console_logs(page):
"""Capture browser console logs during automation"""
logs = []
def handle_console(msg):
logs.append({
'type': msg.type,
'text': msg.text,
'location': msg.location
})
page.on('console', handle_console)
return logs
```
### Form Interaction
```python
def fill_form(page, form_data: dict):
"""Fill a form with provided data"""
for field_name, value in form_data.items():
selectors = [
f'input[name="{field_name}"]',
f'input[id="{field_name}"]',
f'textarea[name="{field_name}"]',
f'select[name="{field_name}"]'
]
for selector in selectors:
try:
element = page.locator(selector)
if element.count() > 0:
if element.get_attribute('type') == 'checkbox':
if value:
element.check()
else:
element.uncheck()
elif selector.startswith('select'):
element.select_option(value)
else:
element.fill(value)
break
except:
continue
```
### Visual Testing
```python
def visual_comparison(page, baseline_path: str, threshold: float = 0.1):
"""Compare current page to baseline screenshot"""
import cv2
import numpy as np
from pathlib import Path
current_path = '/tmp/current_screenshot.png'
page.screenshot(path=current_path, full_page=True)
baseline = cv2.imread(baseline_path)
current = cv2.imread(current_path)
if baseline.shape != current.shape:
return {'match': False, 'reason': 'Different dimensions'}
diff = cv2.absdiff(baseline, current)
diff_ratio = np.sum(diff) / (baseline.shape[0] * baseline.shape[1] * baseline.shape[2] * 255)
return {
'match': diff_ratio < threshold,
'difference_ratio': diff_ratio,
'diff_image': '/tmp/diff.png' if diff_ratio >= threshold else None
}
```
## Common Pitfalls
### ❌ Don't Do This
```python
page.goto('http://localhost:5173')
content = page.content() # May get incomplete HTML!
```
### ✅ Do This Instead
```python
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle') # Wait for JS to execute
content = page.content() # Now we get the complete DOM
```
## Selector Best Practices
```python
page.locator('text=Submit') # Text content
page.locator('role=button[name="Submit"]') # ARIA role
page.locator('#submit-button') # ID
page.locator('[data-testid="submit"]') # Test ID
page.locator('button.primary') # CSS class
page.locator('div > div > button') # Structure-dependent
page.locator('.css-1a2b3c') # Generated class names
```
## Best Practices
1. **Always Use sync_playwright()** for synchronous scripts
2. **Wait for networkidle** before inspecting dynamic apps
3. **Close browser** when automation completes
4. **Use descriptive selectors**: text=, role=, CSS, or IDs
5. **Add appropriate waits**: wait_for_selector() or wait_for_timeout()
6. **Capture screenshots** at key points for debugging
7. **Handle errors gracefully** with try/except blocks
## Example: Full E2E Test
```python
from playwright.sync_api import sync_playwright
def test_user_registration():
"""Complete E2E test for user registration flow"""
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
try:
page.goto('http://localhost:5173/register')
page.wait_for_load_state('networkidle')
page.fill('input[name="email"]', 'test@example.com')
page.fill('input[name="password"]', 'SecurePass123!')
page.fill('input[name="confirmPassword"]', 'SecurePass123!')
page.screenshot(path='/tmp/before-submit.png')
page.click('button[type="submit"]')
page.wait_for_url('**/dashboard**', timeout=10000)
welcome = page.locator('text=Welcome')
assert welcome.is_visible()
page.screenshot(path='/tmp/success.png')
print("✅ Registration test passed!")
except Exception as e:
page.screenshot(path='/tmp/failure.png')
print(f"❌ Test failed: {e}")
raise
finally:
browser.close()
if __name__ == "__main__":
test_user_registration()
```
## Related Resources
- [Playwright Python Docs](https://playwright.dev/python/)
- [Playwright Selectors](https://playwright.dev/python/docs/selectors)
- [Playwright Best Practices](https://playwright.dev/python/docs/best-practices)
- [Testing Library](https://testing-library.com/)