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.
Install
npx skillscat add yldgio/codereview-skills/webapp-testing Install via the SkillsCat registry.
Web Application Testing
To test local web applications, write native Python Playwright scripts.
Security Notice (Critical)
IMPORTANT: Input sanitization is required for safe testing.
- 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 selectorsExample: 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') # CRITICAL: Wait for JS
# Take screenshot for inspection
page.screenshot(path='/tmp/inspect.png', full_page=True)
# Find and click a button
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
networkidleon 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()orpage.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')
# ... test actions ...
except PlaywrightTimeout as e:
print(f"Timeout error: {e}")
# Capture screenshot for debugging
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:
# Using context managers (recommended)
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context()
page = context.new_page()
# ... test code ...
# Browser closes automatically
# Or explicit cleanup
with sync_playwright() as p:
browser = None
try:
browser = p.chromium.launch()
# ... test code ...
finally:
if browser:
browser.close()Browser Console Logs
Capture and view browser logs for debugging:
# Listen to console messages
page.on('console', lambda msg: print(f'Browser console: {msg.text}'))
# Listen to page errors
page.on('pageerror', lambda err: print(f'Page error: {err}'))
# View network activity
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:
# Set up test data before tests
def setup_test_data():
# Clear localStorage
page.evaluate('() => localStorage.clear()')
# Set initial state
page.evaluate('''() => {
localStorage.setItem('user', JSON.stringify({id: 1, name: 'Test User'}))
}''')
# Use fixtures for reproducible tests
@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 pageMulti-Page and Session Testing
Handle authentication and navigation:
# Persist authentication state
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context()
page = context.new_page()
# Login
page.goto('http://localhost:3000/login')
page.fill('[name="email"]', 'user@test.com')
page.fill('[name="password"]', 'password')
page.click('button[type="submit"]')
# Save authentication state
context.storage_state(path='/tmp/auth.json')
# Reuse authentication in new context
context2 = browser.new_context(storage_state='/tmp/auth.json')
page2 = context2.new_page()
page2.goto('http://localhost:3000/dashboard') # Already authenticatedAccessibility Testing
Check for accessibility issues:
# Test keyboard navigation
page.keyboard.press('Tab')
expect(page.locator('button:focus')).to_be_visible()
# Check for proper ARIA labels
expect(page.locator('button[aria-label="Close dialog"]')).to_be_visible()
# Verify semantic HTML
heading = page.locator('h1')
expect(heading).to_be_visible()
# Test with different browsers for cross-browser compatibility
for browser_type in [p.chromium, p.firefox, p.webkit]:
browser = browser_type.launch()
# ... run tests ...
browser.close()Selector Examples
# By text content
page.click('text=Sign In')
# By role
page.click('role=button[name="Submit"]')
# By CSS selector
page.click('.submit-button')
page.click('#login-form button[type="submit"]')
# By test ID (recommended)
page.click('[data-testid="submit-btn"]')Assertions
from playwright.sync_api import expect
# Check element is visible
expect(page.locator('.success-message')).to_be_visible()
# Check text content
expect(page.locator('h1')).to_have_text('Welcome')
# Check URL
expect(page).to_have_url('http://localhost:3000/dashboard')