Playwright Patterns Reference
Complete guide to Playwright automation patterns, selectors, and best practices.
Table of Contents Selectors Wait Strategies Element Interactions Assertions Test Organization Network Interception Screenshots and Videos Debugging Parallel Execution Selectors Text Selectors
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
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:
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')
Element Waits
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')
Timeout Waits
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)
Custom Wait Conditions
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')
Auto-Waiting
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', 'test@example.com') # Auto-waits
Element Interactions Clicking
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)
Filling Forms
Text inputs
page.fill('input[name="email"]', 'user@example.com') page.type('input[name="search"]', 'query', delay=100) # Type with delay
Clear then fill
page.fill('input[name="email"]', '') page.fill('input[name="email"]', 'new@example.com')
Press keys
page.press('input[name="search"]', 'Enter') page.press('input[name="text"]', 'Control+A')
Dropdowns and Selects
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'])
Checkboxes and Radio Buttons
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"]')
File Uploads
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 and Focus
Hover over element
page.hover('button.tooltip-trigger')
Focus element
page.focus('input[name="email"]')
Blur element
page.evaluate('document.activeElement.blur()')
Assertions Element Visibility 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()
Text Content
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}'))
Element State
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()
Attributes and Values
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('user@example.com')
Count and Collections
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
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()
# Test logic here
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): # Test successful login pass
def test_failed_login(self, page):
# Test failed login
pass
def test_logout(self, page):
# Test logout
pass
Setup and Teardown @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()')
Network Interception Mock API Responses
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 Resources
Block images and stylesheets for faster tests
page.route('*/.{png,jpg,jpeg,gif,svg,css}', lambda route: route.abort())
Wait for Network Responses
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
Screenshots and Videos Screenshots
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')
Video Recording 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
Debugging Pause Execution page.pause() # Opens Playwright Inspector
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) # 1 second delay
Verbose Logging
Set DEBUG environment variable
DEBUG=pw:api python test.py
Parallel Execution Pytest Parallel
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
Browser Context Isolation
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()