| name | webapp-testing |
| description | Comprehensive web application testing patterns with Playwright selectors, wait strategies, and best practices |
| user-invocable | false |
| disable-model-invocation | true |
| progressive_disclosure | {"entry_point":{"summary":"Comprehensive web application testing patterns with Playwright selectors, wait strategies, and best practices","when_to_use":"When writing tests, implementing webapp-testing-patterns, or ensuring code quality.","quick_start":"1. Review the core concepts below. 2. Apply patterns to your use case. 3. Follow best practices for implementation."}} |
Playwright Patterns Reference
Complete guide to Playwright automation patterns, selectors, and best practices.
Table of Contents
Selectors
Text Selectors
Most readable and maintainable approach when text is unique:
page.click('text=Login')
page.click('text="Sign Up"')
page.click('text=/log.*in/i')
Role-Based Selectors
Semantic selectors based on ARIA roles:
page.click('role=button[name="Submit"]')
page.fill('role=textbox[name="Email"]', 'user@example.com')
page.click('role=link[name="Learn more"]')
page.check('role=checkbox[name="Accept terms"]')
CSS Selectors
Traditional CSS selectors for precise targeting:
page.click('#submit-button')
page.fill('.email-input', 'user@example.com')
page.click('button.primary')
page.click('nav > ul > li:first-child')
XPath Selectors
For complex DOM navigation:
page.click('xpath=//button[contains(text(), "Submit")]')
page.click('xpath=//div[@class="modal"]//button[@type="submit"]')
Data Attributes
Best practice for test-specific selectors:
page.click('[data-testid="submit-btn"]')
page.fill('[data-test="email-input"]', 'test@example.com')
Chaining Selectors
Combine selectors for precision:
page.locator('div.modal').locator('button.submit').click()
page.locator('role=dialog').locator('text=Confirm').click()
Selector Best Practices
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)
- CSS classes (less stable, may change with styling)
- XPath (fragile, avoid if possible)
Wait Strategies
Load State Waits
Essential for dynamic applications:
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
page.wait_for_load_state('domcontentloaded')
page.wait_for_load_state('load')
Element Waits
Wait for specific elements before interacting:
page.wait_for_selector('button.submit', state='visible')
page.wait_for_selector('.loading-spinner', state='hidden')
page.wait_for_selector('.modal', state='attached')
page.wait_for_selector('.error-message', state='detached')
Timeout Waits
Fixed time delays (use sparingly):
page.wait_for_timeout(500)
page.wait_for_timeout(2000)
Custom Wait Conditions
Wait for JavaScript conditions:
page.wait_for_function('() => document.querySelector(".data").innerText !== "Loading..."')
page.wait_for_function('() => window.appReady === true')
Auto-Waiting
Playwright automatically waits for elements to be actionable:
page.click('button.submit')
page.fill('input.email', 'test@example.com')
Element Interactions
Clicking
page.click('button.submit')
page.click('button.submit', button='right')
page.click('button.submit', click_count=2)
page.click('button.submit', modifiers=['Control'])
page.click('button.submit', force=True)
Filling Forms
page.fill('input[name="email"]', 'user@example.com')
page.type('input[name="search"]', 'query', delay=100)
page.fill('input[name="email"]', '')
page.fill('input[name="email"]', 'new@example.com')
page.press('input[name="search"]', 'Enter')
page.press('input[name="text"]', 'Control+A')
Dropdowns and Selects
page.select_option('select[name="country"]', label='United States')
page.select_option('select[name="country"]', value='us')
page.select_option('select[name="country"]', index=2)
page.select_option('select[multiple]', ['option1', 'option2'])
Checkboxes and Radio Buttons
page.check('input[type="checkbox"]')
page.uncheck('input[type="checkbox"]')
page.check('input[value="option1"]')
if page.is_checked('input[type="checkbox"]'):
page.uncheck('input[type="checkbox"]')
else:
page.check('input[type="checkbox"]')
File Uploads
page.set_input_files('input[type="file"]', '/path/to/file.pdf')
page.set_input_files('input[type="file"]', ['/path/to/file1.pdf', '/path/to/file2.pdf'])
page.set_input_files('input[type="file"]', [])
Hover and Focus
page.hover('button.tooltip-trigger')
page.focus('input[name="email"]')
page.evaluate('document.activeElement.blur()')
Assertions
Element Visibility
from playwright.sync_api import expect
expect(page.locator('button.submit')).to_be_visible()
expect(page.locator('.error-message')).to_be_hidden()
Text Content
expect(page.locator('.title')).to_have_text('Welcome')
expect(page.locator('.message')).to_contain_text('success')
expect(page.locator('.code')).to_have_text(re.compile(r'\d{6}'))
Element State
expect(page.locator('button.submit')).to_be_enabled()
expect(page.locator('button.submit')).to_be_disabled()
expect(page.locator('input[type="checkbox"]')).to_be_checked()
expect(page.locator('input[name="email"]')).to_be_editable()
Attributes and Values
expect(page.locator('img')).to_have_attribute('src', '/logo.png')
expect(page.locator('button')).to_have_class('btn-primary')
expect(page.locator('input[name="email"]')).to_have_value('user@example.com')
Count and Collections
expect(page.locator('li')).to_have_count(5)
items = page.locator('li').all()
assert len(items) == 5
Test Organization
Basic Test Structure
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')
browser.close()
Using Pytest (Recommended)
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"]', 'user@example.com')
page.fill('input[name="password"]', 'password123')
page.click('button[type="submit"]')
expect(page.locator('.welcome-message')).to_be_visible()
Test Grouping with Describe Blocks
class TestAuthentication:
def test_successful_login(self, page):
pass
def test_failed_login(self, page):
pass
def test_logout(self, page):
pass
Setup and Teardown
@pytest.fixture(autouse=True)
def setup_and_teardown(page):
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
yield
page.evaluate('localStorage.clear()')
Network Interception
Mock API Responses
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 Resources
page.route('**/*.{png,jpg,jpeg,gif,svg,css}', lambda route: route.abort())
Wait for Network Responses
with page.expect_response('**/api/users') as response_info:
page.click('button.load-users')
response = response_info.value
assert response.status == 200
Screenshots and Videos
Screenshots
page.screenshot(path='/tmp/screenshot.png', full_page=True)
page.locator('.modal').screenshot(path='/tmp/modal.png')
page.set_viewport_size({'width': 1920, 'height': 1080})
page.screenshot(path='/tmp/desktop.png')
Video Recording
browser = p.chromium.launch(headless=True)
context = browser.new_context(record_video_dir='/tmp/videos/')
page = context.new_page()
context.close()
Debugging
Pause Execution
page.pause()
Console Logs
def handle_console(msg):
print(f"[{msg.type}] {msg.text}")
page.on("console", handle_console)
Slow Motion
browser = p.chromium.launch(headless=False, slow_mo=1000)
Verbose Logging
Parallel Execution
Pytest Parallel
pip install pytest-xdist
pytest -n auto
pytest -n 4
Browser Context Isolation
@pytest.fixture
def context(browser):
context = browser.new_context()
yield context
context.close()
@pytest.fixture
def page(context):
return context.new_page()