universal/testing/webapp-testing/SKILL.md
Comprehensive web application testing patterns with Playwright selectors, wait strategies, and best practices
npx skillsauth add bobmatnyc/claude-mpm-skills webapp-testing-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Complete guide to Playwright automation patterns, selectors, and best practices.
Most readable and maintainable approach when text is unique:
page.click('text=Login')
page.click('text="Sign Up"') # Exact match
page.click('text=/log.*in/i') # Regex, case-insensitive
Semantic selectors based on ARIA roles:
page.click('role=button[name="Submit"]')
page.fill('role=textbox[name="Email"]', '[email protected]')
page.click('role=link[name="Learn more"]')
page.check('role=checkbox[name="Accept terms"]')
Traditional CSS selectors for precise targeting:
page.click('#submit-button')
page.fill('.email-input', '[email protected]')
page.click('button.primary')
page.click('nav > ul > li:first-child')
For complex DOM navigation:
page.click('xpath=//button[contains(text(), "Submit")]')
page.click('xpath=//div[@class="modal"]//button[@type="submit"]')
Best practice for test-specific selectors:
page.click('[data-testid="submit-btn"]')
page.fill('[data-test="email-input"]', '[email protected]')
Combine selectors for precision:
page.locator('div.modal').locator('button.submit').click()
page.locator('role=dialog').locator('text=Confirm').click()
Priority order (most stable to least stable):
data-testid attributes (most stable)role= selectors (semantic, accessible)text= selectors (readable, but text may change)id attributes (stable if not dynamic)Essential for dynamic applications:
# Wait for network to be idle (most common)
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
# Wait for DOM to be ready
page.wait_for_load_state('domcontentloaded')
# Wait for full load including images
page.wait_for_load_state('load')
Wait for specific elements before interacting:
# Wait for element to be visible
page.wait_for_selector('button.submit', state='visible')
# Wait for element to be hidden
page.wait_for_selector('.loading-spinner', state='hidden')
# Wait for element to exist in DOM (may not be visible)
page.wait_for_selector('.modal', state='attached')
# Wait for element to be removed from DOM
page.wait_for_selector('.error-message', state='detached')
Fixed time delays (use sparingly):
# Wait for animations to complete
page.wait_for_timeout(500)
# Wait for delayed content (better to use wait_for_selector)
page.wait_for_timeout(2000)
Wait for JavaScript conditions:
# Wait for custom JavaScript condition
page.wait_for_function('() => document.querySelector(".data").innerText !== "Loading..."')
# Wait for variable to be set
page.wait_for_function('() => window.appReady === true')
Playwright automatically waits for elements to be actionable:
# These automatically wait for element to be:
# - Visible
# - Stable (not animating)
# - Enabled (not disabled)
# - Not obscured by other elements
page.click('button.submit') # Auto-waits
page.fill('input.email', '[email protected]') # Auto-waits
# Basic click
page.click('button.submit')
# Click with options
page.click('button.submit', button='right') # Right-click
page.click('button.submit', click_count=2) # Double-click
page.click('button.submit', modifiers=['Control']) # Ctrl+click
# Force click (bypass actionability checks)
page.click('button.submit', force=True)
# Text inputs
page.fill('input[name="email"]', '[email protected]')
page.type('input[name="search"]', 'query', delay=100) # Type with delay
# Clear then fill
page.fill('input[name="email"]', '')
page.fill('input[name="email"]', '[email protected]')
# Press keys
page.press('input[name="search"]', 'Enter')
page.press('input[name="text"]', 'Control+A')
# Select by label
page.select_option('select[name="country"]', label='United States')
# Select by value
page.select_option('select[name="country"]', value='us')
# Select by index
page.select_option('select[name="country"]', index=2)
# Select multiple options
page.select_option('select[multiple]', ['option1', 'option2'])
# Check a checkbox
page.check('input[type="checkbox"]')
# Uncheck a checkbox
page.uncheck('input[type="checkbox"]')
# Check a radio button
page.check('input[value="option1"]')
# Toggle checkbox
if page.is_checked('input[type="checkbox"]'):
page.uncheck('input[type="checkbox"]')
else:
page.check('input[type="checkbox"]')
# Upload single file
page.set_input_files('input[type="file"]', '/path/to/file.pdf')
# Upload multiple files
page.set_input_files('input[type="file"]', ['/path/to/file1.pdf', '/path/to/file2.pdf'])
# Clear file input
page.set_input_files('input[type="file"]', [])
# Hover over element
page.hover('button.tooltip-trigger')
# Focus element
page.focus('input[name="email"]')
# Blur element
page.evaluate('document.activeElement.blur()')
from playwright.sync_api import expect
# Expect element to be visible
expect(page.locator('button.submit')).to_be_visible()
# Expect element to be hidden
expect(page.locator('.error-message')).to_be_hidden()
# Expect exact text
expect(page.locator('.title')).to_have_text('Welcome')
# Expect partial text
expect(page.locator('.message')).to_contain_text('success')
# Expect text matching pattern
expect(page.locator('.code')).to_have_text(re.compile(r'\d{6}'))
# Expect element to be enabled/disabled
expect(page.locator('button.submit')).to_be_enabled()
expect(page.locator('button.submit')).to_be_disabled()
# Expect checkbox to be checked
expect(page.locator('input[type="checkbox"]')).to_be_checked()
# Expect element to be editable
expect(page.locator('input[name="email"]')).to_be_editable()
# Expect attribute value
expect(page.locator('img')).to_have_attribute('src', '/logo.png')
# Expect CSS class
expect(page.locator('button')).to_have_class('btn-primary')
# Expect input value
expect(page.locator('input[name="email"]')).to_have_value('[email protected]')
# Expect specific count
expect(page.locator('li')).to_have_count(5)
# Get all elements and assert
items = page.locator('li').all()
assert len(items) == 5
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Test logic here
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
browser.close()
import pytest
from playwright.sync_api import sync_playwright
@pytest.fixture(scope="session")
def browser():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
yield browser
browser.close()
@pytest.fixture
def page(browser):
page = browser.new_page()
yield page
page.close()
def test_login(page):
page.goto('http://localhost:3000')
page.fill('input[name="email"]', '[email protected]')
page.fill('input[name="password"]', 'password123')
page.click('button[type="submit"]')
expect(page.locator('.welcome-message')).to_be_visible()
class TestAuthentication:
def test_successful_login(self, page):
# Test successful login
pass
def test_failed_login(self, page):
# Test failed login
pass
def test_logout(self, page):
# Test logout
pass
@pytest.fixture(autouse=True)
def setup_and_teardown(page):
# Setup - runs before each test
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
yield # Test runs here
# Teardown - runs after each test
page.evaluate('localStorage.clear()')
# Intercept and mock API response
def handle_route(route):
route.fulfill(
status=200,
body='{"success": true, "data": "mocked"}',
headers={'Content-Type': 'application/json'}
)
page.route('**/api/data', handle_route)
page.goto('http://localhost:3000')
# Block images and stylesheets for faster tests
page.route('**/*.{png,jpg,jpeg,gif,svg,css}', lambda route: route.abort())
# Wait for specific API call
with page.expect_response('**/api/users') as response_info:
page.click('button.load-users')
response = response_info.value
assert response.status == 200
# Full page screenshot
page.screenshot(path='/tmp/screenshot.png', full_page=True)
# Element screenshot
page.locator('.modal').screenshot(path='/tmp/modal.png')
# Screenshot with custom dimensions
page.set_viewport_size({'width': 1920, 'height': 1080})
page.screenshot(path='/tmp/desktop.png')
browser = p.chromium.launch(headless=True)
context = browser.new_context(record_video_dir='/tmp/videos/')
page = context.new_page()
# Perform actions...
context.close() # Video saved on close
page.pause() # Opens Playwright Inspector
def handle_console(msg):
print(f"[{msg.type}] {msg.text}")
page.on("console", handle_console)
browser = p.chromium.launch(headless=False, slow_mo=1000) # 1 second delay
# Set DEBUG environment variable
# DEBUG=pw:api python test.py
# Install pytest-xdist
pip install pytest-xdist
# Run tests in parallel
pytest -n auto # Auto-detect CPU cores
pytest -n 4 # Run with 4 workers
# Each test gets isolated context (cookies, localStorage, etc.)
@pytest.fixture
def context(browser):
context = browser.new_context()
yield context
context.close()
@pytest.fixture
def page(context):
return context.new_page()
development
Axum (Rust) web framework patterns for production APIs: routers/extractors, state, middleware, error handling, tracing, graceful shutdown, and testing
development
Optimize web performance using Core Web Vitals, modern patterns (View Transitions, Speculation Rules), and framework-specific techniques
development
Best practices for documenting APIs and code interfaces, eliminating redundant documentation guidance per agent.
development
Comprehensive API design patterns covering REST, GraphQL, gRPC, versioning, authentication, and modern API best practices