references/api-reference.md
# Playwright Python API Reference
Quick reference for common Playwright operations. For complete documentation, see [Playwright Python docs](https://playwright.dev/python/docs/api/class-playwright).
## Browser Launch
```python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# Launch browsers
browser = p.chromium.launch() # Default: headless=True
browser = p.firefox.launch()
browser = p.webkit.launch()
# Launch options
browser = p.chromium.launch(
headless=False, # Show browser UI
slow_mo=100, # Slow down actions by 100ms
args=["--disable-dev-shm-usage"], # Chrome flags
channel="chrome", # Use installed Chrome
)
```
## Browser Context
```python
# Create context with options
context = browser.new_context(
viewport={"width": 1280, "height": 720},
user_agent="Custom UA",
locale="en-US",
timezone_id="America/New_York",
geolocation={"latitude": 40.7128, "longitude": -74.0060},
permissions=["geolocation"],
color_scheme="dark", # or "light", "no-preference"
device_scale_factor=2, # Retina
is_mobile=True,
has_touch=True,
)
# Use device emulation
iphone = p.devices["iPhone 13"]
context = browser.new_context(**iphone)
# Reuse authentication state
context = browser.new_context(storage_state="auth.json")
```
## Page Navigation
```python
page = context.new_page()
# Navigation
page.goto("https://example.com")
page.goto(url, wait_until="domcontentloaded") # or "load", "networkidle"
page.go_back()
page.go_forward()
page.reload()
# Wait for state
page.wait_for_load_state("networkidle")
page.wait_for_url("**/login")
# Timeouts
page.set_default_timeout(30_000) # All operations
page.set_default_navigation_timeout(60_000) # Navigation only
```
## Modern Locator API (Preferred)
```python
# Semantic locators - ALWAYS prefer these
page.get_by_role("button", name="Submit")
page.get_by_role("link", name="Sign up")
page.get_by_role("textbox", name="Email")
page.get_by_role("checkbox", name="Accept terms")
page.get_by_label("Email")
page.get_by_placeholder("Enter email")
page.get_by_text("Welcome")
page.get_by_text("Welcome", exact=True) # Exact match
page.get_by_alt_text("Profile picture")
page.get_by_title("Settings")
page.get_by_test_id("submit-btn") # data-testid attribute
# Locator combinators
btn = page.get_by_role("button", name="New")
dialog = page.get_by_text("Confirm")
btn.or_(dialog).first.click() # Match either
page.get_by_role("button").and_(
page.get_by_title("Subscribe")
).click() # Match both
# Filtering
page.locator("tr").filter(has_text="Active").first.click()
page.locator("li").filter(
has=page.get_by_role("button", name="Edit")
).click()
```
## CSS/XPath Locators (Fallback)
```python
# CSS selectors
page.locator("button.primary")
page.locator("#submit")
page.locator("[data-testid='submit']")
page.locator("form >> button") # Chaining
# XPath
page.locator("xpath=//button[@type='submit']")
# Text matching
page.locator("text=Click me")
page.locator("text=/click/i") # Regex, case insensitive
```
## Actions
```python
# Click
locator.click()
locator.click(button="right") # Right-click
locator.click(click_count=2) # Double-click
locator.click(modifiers=["Shift"])
locator.click(force=True) # Skip actionability checks
locator.click(position={"x": 10, "y": 10})
# Hover
locator.hover()
# Input
locator.fill("value") # Clear and type
locator.type("value") # Type char by char
locator.press("Enter")
locator.press("Control+a")
locator.clear()
# Checkboxes/Radio
locator.check()
locator.uncheck()
locator.set_checked(True)
# Select dropdown
locator.select_option("value")
locator.select_option(label="Option text")
locator.select_option(index=2)
# File upload
locator.set_input_files("file.pdf")
locator.set_input_files(["file1.pdf", "file2.pdf"])
# Drag and drop
locator.drag_to(target_locator)
# Focus/blur
locator.focus()
locator.blur()
# Scroll
locator.scroll_into_view_if_needed()
page.mouse.wheel(0, 500) # Scroll down
```
## Waiting
```python
# Wait for element
locator.wait_for() # visible by default
locator.wait_for(state="visible")
locator.wait_for(state="hidden")
locator.wait_for(state="attached")
locator.wait_for(state="detached")
locator.wait_for(timeout=5000)
# Wait for conditions
page.wait_for_selector("selector")
page.wait_for_url("**/success")
page.wait_for_function("() => window.ready")
page.wait_for_timeout(1000) # Avoid if possible
# Wait for network
with page.expect_response("**/api/data") as response_info:
page.click("button")
response = response_info.value
```
## Extracting Content
```python
# Text
locator.text_content() # Raw text
locator.inner_text() # Visible text
locator.inner_html() # Inner HTML
locator.all_text_contents() # All matches
# Attributes
locator.get_attribute("href")
locator.get_attribute("value")
# Input values
locator.input_value()
# Count and existence
locator.count()
locator.is_visible()
locator.is_enabled()
locator.is_checked()
# Multiple elements
for item in locator.all():
print(item.text_content())
```
## Screenshots
```python
# Page screenshot
page.screenshot(path="/tmp/screenshot.png")
page.screenshot(path="/tmp/full.png", full_page=True)
page.screenshot(type="jpeg", quality=80)
# Element screenshot
locator.screenshot(path="/tmp/element.png")
# Return bytes (no file)
bytes_data = page.screenshot()
```
## JavaScript Execution
```python
# Evaluate expression
title = page.evaluate("document.title")
count = page.evaluate("document.querySelectorAll('a').length")
# Evaluate with arguments
result = page.evaluate("([a, b]) => a + b", [1, 2])
# Evaluate function
result = page.evaluate("""
() => {
return window.localStorage.getItem('key');
}
""")
# Evaluate on element
href = locator.evaluate("el => el.href")
```
## Network
```python
# Intercept requests
def handle_route(route):
if "ads" in route.request.url:
route.abort()
else:
route.continue_()
page.route("**/*", handle_route)
# Mock API response
page.route("**/api/user", lambda route: route.fulfill(
status=200,
content_type="application/json",
body='{"name": "Test User"}'
))
# Wait for response
with page.expect_response("**/api/data") as response_info:
page.click("button")
data = response_info.value.json()
```
## Frames and Popups
```python
# Frames
frame = page.frame(name="frame-name")
frame = page.frame_locator("#iframe").locator("button")
frame.click()
# Popups
with page.expect_popup() as popup_info:
page.click("a[target='_blank']")
popup = popup_info.value
popup.wait_for_load_state()
print(popup.title())
```
## Tracing
```python
# Start tracing
context.tracing.start(screenshots=True, snapshots=True, sources=True)
# Your automation...
# Stop and save
context.tracing.stop(path="trace.zip")
# View trace
# uv run --with playwright playwright show-trace trace.zip
```
## Clock API (Time Mocking)
```python
import datetime
# Install fake timers
page.clock.install(time=datetime.datetime(2024, 12, 10, 8, 0, 0))
page.goto("https://example.com")
# Fast forward
page.clock.fast_forward(1000) # 1 second
page.clock.fast_forward("30:00") # 30 minutes
# Pause at specific time
page.clock.pause_at(datetime.datetime(2024, 12, 10, 10, 0, 0))
# Resume normal flow
page.clock.resume()
```
## ARIA Snapshots
```python
from playwright.sync_api import expect
# Get ARIA snapshot (YAML format)
snapshot = page.get_by_role("navigation").aria_snapshot()
print(snapshot)
# Assert ARIA structure
expect(page.locator("nav")).to_match_aria_snapshot('''
- navigation:
- link "Home"
- link "About"
- link "Contact"
''')
```
## Session Storage
```python
# Save auth state after login
context.storage_state(path="auth.json")
# Reuse in new context
context = browser.new_context(storage_state="auth.json")
```
## Common Patterns
### Login with session reuse
```python
# First time: login and save state
page.goto("https://example.com/login")
page.get_by_label("Email").fill("user@example.com")
page.get_by_label("Password").fill("password")
page.get_by_role("button", name="Sign in").click()
page.wait_for_url("**/dashboard")
context.storage_state(path="auth.json")
# Subsequent runs: reuse state
context = browser.new_context(storage_state="auth.json")
page = context.new_page()
page.goto("https://example.com/dashboard") # Already logged in
```
### Handle dynamic content
```python
# Wait for network idle
page.goto(url, wait_until="networkidle")
# Wait for specific element
page.get_by_text("Loaded").wait_for()
# Wait for element count
page.locator(".item").nth(9).wait_for() # Wait for 10 items
```
### Retry on failure
```python
from playwright.sync_api import expect
# Built-in retry with expect
expect(locator).to_be_visible(timeout=10_000)
expect(locator).to_have_text("Success")
expect(locator).to_have_count(5)
```
references/custom-scripts.md
# Custom Playwright Scripts Guide
How to write custom automation scripts using the Playwright skill.
## Script Template
Save this template to `/tmp/my-automation.py`:
```python
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = ["playwright==1.56.0"]
# ///
"""
Custom Playwright automation script.
Run with: uv run /tmp/my-automation.py
"""
import argparse
import os
import sys
from datetime import datetime
from playwright.sync_api import sync_playwright
# Configuration from environment
HEADLESS = os.getenv("HEADLESS", "0").lower() in ("1", "true", "yes")
SLOW_MO = int(os.getenv("SLOW_MO", "0"))
TRACE = os.getenv("TRACE", "0").lower() in ("1", "true", "yes")
def parse_viewport(value: str) -> dict | None:
"""Parse viewport string like '1280x720' into dict."""
if not value:
return None
try:
w, h = value.lower().split("x", 1)
return {"width": int(w), "height": int(h)}
except Exception:
return None
def main() -> int:
parser = argparse.ArgumentParser(description="My automation script")
parser.add_argument("--url", default="https://example.com", help="Target URL")
parser.add_argument("-o", "--output", help="Screenshot output path")
args = parser.parse_args()
viewport = parse_viewport(os.getenv("VIEWPORT", "")) or {"width": 1280, "height": 720}
with sync_playwright() as p:
browser = p.chromium.launch(headless=HEADLESS, slow_mo=SLOW_MO)
context = browser.new_context(viewport=viewport)
if TRACE:
context.tracing.start(screenshots=True, snapshots=True, sources=True)
page = context.new_page()
page.set_default_timeout(15_000)
page.set_default_navigation_timeout(30_000)
try:
# ========================================
# YOUR AUTOMATION CODE HERE
# ========================================
page.goto(args.url, wait_until="networkidle")
print(f"Title: {page.title()}")
# Example: Click a button
# page.get_by_role("button", name="Submit").click()
# Example: Fill a form
# page.get_by_label("Email").fill("test@example.com")
# Example: Extract data
# links = page.locator("a").all_text_contents()
# print(links)
# ========================================
if args.output:
page.screenshot(path=args.output, full_page=True)
print(f"Screenshot: {args.output}")
return 0
except Exception as e:
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
error_shot = f"/tmp/error-{ts}.png"
try:
page.screenshot(path=error_shot)
print(f"Error screenshot: {error_shot}", file=sys.stderr)
except Exception:
pass
print(f"Error: {e}", file=sys.stderr)
return 1
finally:
if TRACE:
trace_path = "/tmp/trace.zip"
context.tracing.stop(path=trace_path)
print(f"Trace: {trace_path}")
context.close()
browser.close()
if __name__ == "__main__":
sys.exit(main())
```
## Running Scripts
```bash
# Basic run
uv run /tmp/my-automation.py
# With arguments
uv run /tmp/my-automation.py --url https://example.com
# With environment variables
HEADLESS=1 uv run /tmp/my-automation.py
SLOW_MO=500 uv run /tmp/my-automation.py
TRACE=1 uv run /tmp/my-automation.py
```
## Common Patterns
### Login Script
```python
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = ["playwright==1.56.0"]
# ///
"""Login and save session state."""
import os
import sys
from playwright.sync_api import sync_playwright
def main() -> int:
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
try:
page.goto("https://example.com/login")
# Fill login form
page.get_by_label("Email").fill(os.environ["EMAIL"])
page.get_by_label("Password").fill(os.environ["PASSWORD"])
page.get_by_role("button", name="Sign in").click()
# Wait for successful login
page.wait_for_url("**/dashboard")
print("Login successful!")
# Save session for reuse
context.storage_state(path="/tmp/auth.json")
print("Session saved to /tmp/auth.json")
return 0
except Exception as e:
page.screenshot(path="/tmp/login-error.png")
print(f"Error: {e}", file=sys.stderr)
return 1
finally:
browser.close()
if __name__ == "__main__":
sys.exit(main())
```
Run with:
```bash
EMAIL=user@example.com PASSWORD=secret uv run /tmp/login.py
```
### Scraping Script
```python
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = ["playwright==1.56.0"]
# ///
"""Scrape product data from a website."""
import json
import sys
from playwright.sync_api import sync_playwright
def main() -> int:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
try:
page.goto("https://example.com/products")
page.wait_for_selector(".product-card")
# Extract product data
products = page.evaluate("""
() => Array.from(document.querySelectorAll('.product-card')).map(card => ({
name: card.querySelector('.name')?.textContent?.trim(),
price: card.querySelector('.price')?.textContent?.trim(),
url: card.querySelector('a')?.href
}))
""")
print(json.dumps(products, indent=2))
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
finally:
browser.close()
if __name__ == "__main__":
sys.exit(main())
```
### Form Submission with Wait
```python
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = ["playwright==1.56.0"]
# ///
"""Submit a form and wait for response."""
import sys
from playwright.sync_api import sync_playwright
def main() -> int:
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
try:
page.goto("https://example.com/contact")
# Fill form
page.get_by_label("Name").fill("John Doe")
page.get_by_label("Email").fill("john@example.com")
page.get_by_label("Message").fill("Hello, this is a test message.")
# Submit and wait for response
with page.expect_navigation():
page.get_by_role("button", name="Send").click()
# Check for success message
if page.get_by_text("Thank you").is_visible():
print("Form submitted successfully!")
return 0
else:
print("Submission may have failed")
page.screenshot(path="/tmp/form-result.png")
return 1
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
finally:
browser.close()
if __name__ == "__main__":
sys.exit(main())
```
### Multi-Page Navigation
```python
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = ["playwright==1.56.0"]
# ///
"""Navigate through multiple pages and collect data."""
import json
import sys
from playwright.sync_api import sync_playwright
def main() -> int:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
all_items = []
try:
page.goto("https://example.com/items")
while True:
# Wait for items to load
page.wait_for_selector(".item")
# Extract items from current page
items = page.locator(".item").all_text_contents()
all_items.extend(items)
print(f"Collected {len(items)} items from page")
# Check for next page
next_btn = page.get_by_role("link", name="Next")
if next_btn.count() == 0 or not next_btn.is_enabled():
break
next_btn.click()
page.wait_for_load_state("networkidle")
print(f"\nTotal items: {len(all_items)}")
print(json.dumps(all_items, indent=2))
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
finally:
browser.close()
if __name__ == "__main__":
sys.exit(main())
```
### Screenshot with Auth
```python
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = ["playwright==1.56.0"]
# ///
"""Take authenticated screenshot using saved session."""
import sys
from playwright.sync_api import sync_playwright
def main() -> int:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
# Reuse saved auth state
context = browser.new_context(storage_state="/tmp/auth.json")
page = context.new_page()
try:
page.goto("https://example.com/dashboard")
page.wait_for_load_state("networkidle")
page.screenshot(path="/tmp/dashboard.png", full_page=True)
print("Screenshot saved to /tmp/dashboard.png")
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
finally:
context.close()
browser.close()
if __name__ == "__main__":
sys.exit(main())
```
## Adding Dependencies
Add Python packages to the script metadata:
```python
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "playwright==1.56.0",
# "pandas",
# "beautifulsoup4",
# ]
# ///
import pandas as pd
from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright
```
## Debugging Tips
### Enable tracing
```bash
TRACE=1 uv run /tmp/my-script.py
uv run --with playwright playwright show-trace /tmp/trace.zip
```
### Use headed mode with slow-mo
```bash
HEADLESS=0 SLOW_MO=500 uv run /tmp/my-script.py
```
### Add page.pause() for debugging
```python
page.goto("https://example.com")
page.pause() # Opens Playwright Inspector
page.get_by_role("button", name="Submit").click()
```
### Screenshot on error
```python
try:
# automation code
except Exception as e:
page.screenshot(path="/tmp/error.png")
raise
```
## CI/Docker Configuration
For running in containers:
```python
browser = p.chromium.launch(
headless=True,
args=[
"--disable-dev-shm-usage",
"--no-sandbox",
"--disable-gpu",
]
)
```
Environment variables for containers:
```bash
PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
XDG_CACHE_HOME=/tmp/.cache
```
references/selectors.md
# Playwright Selector Guide
Best practices for selecting elements in Playwright.
## Selector Priority (Most to Least Preferred)
1. **Role-based** - `get_by_role()` - Most stable, accessibility-aware
2. **Label/Text** - `get_by_label()`, `get_by_text()` - User-visible
3. **Test ID** - `get_by_test_id()` - Explicitly for testing
4. **CSS/XPath** - `locator()` - Fallback only
## Role-Based Selectors (Preferred)
```python
# Buttons
page.get_by_role("button", name="Submit")
page.get_by_role("button", name="Submit", exact=True) # Exact match
page.get_by_role("button", name=/submit/i) # Regex
# Links
page.get_by_role("link", name="Sign up")
page.get_by_role("link", name="Home")
# Form elements
page.get_by_role("textbox", name="Email")
page.get_by_role("checkbox", name="Accept terms")
page.get_by_role("radio", name="Option A")
page.get_by_role("combobox", name="Country") # Select dropdown
page.get_by_role("spinbutton", name="Quantity") # Number input
page.get_by_role("slider", name="Volume")
# Structure
page.get_by_role("heading", name="Welcome", level=1) # h1
page.get_by_role("heading", level=2) # Any h2
page.get_by_role("list")
page.get_by_role("listitem")
page.get_by_role("navigation")
page.get_by_role("main")
page.get_by_role("article")
# Tables
page.get_by_role("table")
page.get_by_role("row")
page.get_by_role("cell", name="Price")
page.get_by_role("columnheader", name="Name")
# Dialogs
page.get_by_role("dialog")
page.get_by_role("alertdialog")
# Media
page.get_by_role("img", name="Logo")
```
## Common ARIA Roles Reference
| Role | HTML Examples |
|------|--------------|
| `button` | `<button>`, `<input type="button">` |
| `link` | `<a href>` |
| `textbox` | `<input type="text">`, `<textarea>` |
| `checkbox` | `<input type="checkbox">` |
| `radio` | `<input type="radio">` |
| `combobox` | `<select>` |
| `listbox` | `<select>`, `<ul role="listbox">` |
| `option` | `<option>` |
| `heading` | `<h1>` - `<h6>` |
| `list` | `<ul>`, `<ol>` |
| `listitem` | `<li>` |
| `table` | `<table>` |
| `row` | `<tr>` |
| `cell` | `<td>` |
| `navigation` | `<nav>` |
| `main` | `<main>` |
| `article` | `<article>` |
| `dialog` | `<dialog>` |
| `img` | `<img>` |
## Text-Based Selectors
```python
# By visible label (for form elements)
page.get_by_label("Email address")
page.get_by_label("Password")
page.get_by_label(/email/i) # Regex
# By placeholder
page.get_by_placeholder("Enter your email")
page.get_by_placeholder("Search...")
# By visible text
page.get_by_text("Click here")
page.get_by_text("Click here", exact=True) # Exact match
page.get_by_text(/click/i) # Regex, case insensitive
# By alt text (images)
page.get_by_alt_text("Company Logo")
page.get_by_alt_text("User avatar")
# By title attribute
page.get_by_title("Close dialog")
page.get_by_title("Settings")
```
## Test ID Selectors
```python
# Using data-testid attribute (configurable)
page.get_by_test_id("submit-button")
page.get_by_test_id("user-email-input")
page.get_by_test_id("modal-close")
# HTML: <button data-testid="submit-button">Submit</button>
```
Configure custom test ID attribute:
```python
# In browser context
context = browser.new_context()
context.set_default_test_id_attribute("data-test")
# Now matches data-test instead of data-testid
page.get_by_test_id("my-element")
# Matches: <div data-test="my-element">
```
## Combining Locators
### OR - Match Either
```python
# Click whichever appears first
new_button = page.get_by_role("button", name="New")
create_button = page.get_by_role("button", name="Create")
new_button.or_(create_button).click()
# With first() for reliability
new_button.or_(create_button).first.click()
```
### AND - Match Both Conditions
```python
# Button that also has specific title
page.get_by_role("button").and_(
page.get_by_title("Subscribe")
).click()
# Link with specific class (rare use case)
page.get_by_role("link").and_(
page.locator(".external")
).click()
```
### Filter - Narrow Down Results
```python
# Table rows containing text
page.locator("tr").filter(has_text="Active").click()
page.locator("tr").filter(has_text=/active/i).click()
# List items containing a button
page.locator("li").filter(
has=page.get_by_role("button", name="Edit")
).click()
# Combine filters
page.locator("tr").filter(has_text="Active").filter(
has=page.get_by_role("button", name="Delete")
).first.click()
# NOT filter (exclude)
page.locator("li").filter(has_not_text="Archived").all()
page.locator("div").filter(
has_not=page.get_by_role("button")
).all()
```
## CSS Selectors (Fallback)
```python
# Basic CSS
page.locator("button")
page.locator(".btn-primary")
page.locator("#submit")
page.locator("[type='submit']")
# Attribute selectors
page.locator("[data-value='123']")
page.locator("[href*='login']") # Contains
page.locator("[href^='https']") # Starts with
page.locator("[href$='.pdf']") # Ends with
# Combinators
page.locator("form button") # Descendant
page.locator("form > button") # Direct child
page.locator("input + label") # Adjacent sibling
page.locator("input ~ button") # General sibling
# Pseudo-classes
page.locator("li:first-child")
page.locator("li:last-child")
page.locator("li:nth-child(3)")
page.locator("button:not(.disabled)")
page.locator("input:enabled")
page.locator("option:checked")
```
## XPath Selectors (Last Resort)
```python
# XPath with prefix
page.locator("xpath=//button[@type='submit']")
page.locator("xpath=//div[contains(@class, 'modal')]")
page.locator("xpath=//a[text()='Click me']")
page.locator("xpath=//input[@name='email']/following-sibling::button")
```
## Chaining Locators
```python
# Find within
modal = page.locator(".modal")
modal.get_by_role("button", name="Close").click()
# Multiple levels
page.locator("form").locator("fieldset").get_by_label("Email").fill("test@example.com")
# Frame locator
frame = page.frame_locator("#iframe")
frame.get_by_role("button", name="Submit").click()
```
## Selecting Multiple Elements
```python
# Count elements
count = page.locator("li").count()
# Get all elements
items = page.locator("li").all()
for item in items:
print(item.text_content())
# Get specific by index
page.locator("li").first.click()
page.locator("li").last.click()
page.locator("li").nth(2).click() # 0-indexed
# Get all text contents
texts = page.locator("li").all_text_contents()
```
## Best Practices
### DO
```python
# Prefer semantic, user-facing locators
page.get_by_role("button", name="Submit")
page.get_by_label("Email")
page.get_by_text("Welcome back")
page.get_by_test_id("checkout-button")
```
### DON'T
```python
# Avoid fragile selectors
page.locator("#root > div > div:nth-child(3) > button") # Brittle
page.locator(".sc-hKgILt") # Generated class
page.locator("[class*='Button__StyledButton']") # CSS-in-JS
page.locator("xpath=//div[3]/span[2]/button") # Position-based
```
### Debugging Selectors
```python
# Check if selector matches
if page.locator("button").count() > 0:
print("Found buttons")
# Highlight element (headed mode)
locator = page.get_by_role("button", name="Submit")
locator.highlight()
# Get selector playground
page.pause() # Opens Playwright Inspector
```
## Common Selector Patterns
### Login form
```python
page.get_by_label("Email").fill("user@example.com")
page.get_by_label("Password").fill("secret")
page.get_by_role("button", name="Sign in").click()
```
### Search
```python
page.get_by_placeholder("Search...").fill("query")
page.get_by_role("button", name="Search").click()
# or
page.get_by_placeholder("Search...").press("Enter")
```
### Table row actions
```python
# Click Edit button in row containing "John"
page.locator("tr").filter(has_text="John").get_by_role("button", name="Edit").click()
```
### Modal dialog
```python
dialog = page.get_by_role("dialog")
dialog.get_by_role("button", name="Confirm").click()
```
### Navigation menu
```python
page.get_by_role("navigation").get_by_role("link", name="Settings").click()
```
references/troubleshooting.md
# Playwright Troubleshooting Guide
Common issues and solutions when using Playwright.
## Browser Installation Issues
> **Claude: Do not run browser installation commands directly.** Suggest these commands to the user and let them run manually.
### "Executable doesn't exist" / "Browser not found"
**Cause**: Playwright browser binaries not installed.
**Solution** (suggest to user, do not run directly):
```bash
# Install Chromium (recommended, ~200MB)
uv run --with playwright playwright install chromium
# Install all browsers
uv run --with playwright playwright install
# Install with system dependencies (Linux)
uv run --with playwright playwright install --with-deps chromium
```
### "Missing system dependencies" (Linux)
**Cause**: Required system libraries not installed.
**Solution**:
```bash
# Install dependencies automatically
uv run --with playwright playwright install-deps chromium
# Or manually on Ubuntu/Debian
sudo apt-get install libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
libgbm1 libasound2
```
### Browser location not detected
**Check installed browsers**:
```bash
uv run /path/to/plugins/playwright/scripts/check_setup.py
```
**Browser cache locations**:
- macOS: `~/Library/Caches/ms-playwright/`
- Linux: `~/.cache/ms-playwright/`
- Windows: `%USERPROFILE%\AppData\Local\ms-playwright\`
**Custom browser path**:
```python
browser = p.chromium.launch(
executable_path="/path/to/chromium"
)
```
## Timeout Issues
### "Timeout waiting for element"
**Causes**:
- Element doesn't exist
- Element is hidden or not rendered
- Wrong selector
- Page still loading
**Solutions**:
```python
# 1. Increase timeout
page.set_default_timeout(60_000)
locator.click(timeout=30_000)
# 2. Wait for network idle
page.goto(url, wait_until="networkidle")
# 3. Wait for element explicitly
page.get_by_role("button", name="Submit").wait_for(state="visible")
# 4. Check element exists
if page.get_by_role("button", name="Submit").count() > 0:
page.get_by_role("button", name="Submit").click()
else:
print("Button not found")
```
### "Navigation timeout"
**Causes**:
- Slow page load
- Page waiting for external resources
- Infinite redirects
**Solutions**:
```python
# 1. Increase navigation timeout
page.set_default_navigation_timeout(120_000)
# 2. Use different wait strategy
page.goto(url, wait_until="domcontentloaded") # Faster than "load"
# 3. Don't wait for all network activity
page.goto(url, wait_until="commit")
```
## Element Interaction Issues
### "Element is not visible"
**Solutions**:
```python
# 1. Scroll into view
element = page.get_by_role("button", name="Submit")
element.scroll_into_view_if_needed()
element.click()
# 2. Force click (skip visibility check)
element.click(force=True)
# 3. Wait for visibility
element.wait_for(state="visible")
element.click()
```
### "Element is not enabled"
**Cause**: Button or input is disabled.
**Solutions**:
```python
# 1. Wait for element to be enabled
page.get_by_role("button", name="Submit").wait_for(state="attached")
page.wait_for_function("document.querySelector('button').disabled === false")
page.get_by_role("button", name="Submit").click()
# 2. Check element state
btn = page.get_by_role("button", name="Submit")
if btn.is_enabled():
btn.click()
```
### "Element is detached from DOM"
**Cause**: Page updated and element was removed/replaced.
**Solution**:
```python
# Re-query the element
page.get_by_role("button", name="Submit").click() # Always fresh query
```
### "Multiple elements match selector"
**Solutions**:
```python
# 1. Use more specific selector
page.get_by_role("button", name="Submit", exact=True)
# 2. Use first/last/nth
page.locator("button").first.click()
page.locator("button").nth(2).click()
# 3. Filter results
page.locator("button").filter(has_text="Submit").click()
```
## Frame and Popup Issues
### "Element not found" (in iframe)
**Solution**:
```python
# Locate frame first
frame = page.frame_locator("#iframe")
frame.get_by_role("button", name="Submit").click()
# Or by name
frame = page.frame(name="content-frame")
frame.locator("button").click()
```
### Popup window not captured
**Solution**:
```python
# Wait for popup before clicking
with page.expect_popup() as popup_info:
page.get_by_role("link", name="Open").click()
popup = popup_info.value
popup.wait_for_load_state()
print(popup.title())
```
## Headless Mode Issues
### Page renders differently in headless mode
**Causes**:
- Different viewport
- Missing fonts
- GPU rendering differences
**Solutions**:
```python
# 1. Set explicit viewport
context = browser.new_context(
viewport={"width": 1920, "height": 1080}
)
# 2. Debug in headed mode first
browser = p.chromium.launch(headless=False)
# 3. Use device emulation for consistency
iphone = p.devices["iPhone 13"]
context = browser.new_context(**iphone)
```
### "Page crash" in headless
**Cause**: Often memory or resource issues.
**Solution** (especially in Docker):
```python
browser = p.chromium.launch(
headless=True,
args=[
"--disable-dev-shm-usage",
"--disable-gpu",
"--no-sandbox",
"--single-process",
]
)
```
## Network Issues
### SSL certificate errors
**Solution**:
```python
context = browser.new_context(
ignore_https_errors=True
)
```
### Requests blocked by CORS
**Cause**: Browser enforces CORS, unlike requests library.
**Solution**: This is expected browser behavior. To bypass:
```python
# Intercept and modify response headers
page.route("**/*", lambda route: route.continue_(
headers={**route.request.headers, "Access-Control-Allow-Origin": "*"}
))
```
### Page blocked by bot detection
**Solutions**:
```python
# 1. Use stealth settings
context = browser.new_context(
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...",
viewport={"width": 1920, "height": 1080},
locale="en-US",
)
# 2. Use slow_mo to appear more human
browser = p.chromium.launch(slow_mo=100)
# 3. Add random delays
import random
import time
time.sleep(random.uniform(0.5, 1.5))
```
## File Downloads
### Download not triggered
**Solution**:
```python
# Wait for download
with page.expect_download() as download_info:
page.get_by_role("link", name="Download").click()
download = download_info.value
download.save_as("/tmp/file.pdf")
print(f"Downloaded: {download.path()}")
```
### Downloads blocked
**Solution**:
```python
context = browser.new_context(
accept_downloads=True
)
```
## Performance Issues
### Script runs slowly
**Solutions**:
```python
# 1. Use headless mode
browser = p.chromium.launch(headless=True)
# 2. Disable images/CSS for faster loads
context = browser.new_context()
await page.route("**/*.{png,jpg,jpeg,gif,webp,css}", lambda route: route.abort())
# 3. Use networkidle only when necessary
page.goto(url, wait_until="domcontentloaded") # Faster
# 4. Close browser when done
browser.close()
```
### Memory leaks
**Solution**:
```python
# Always use context manager
with sync_playwright() as p:
browser = p.chromium.launch()
# ... automation ...
browser.close() # Explicit close
# Or try/finally
try:
browser = p.chromium.launch()
# ...
finally:
browser.close()
```
## Debugging Techniques
### Use Playwright Inspector
```python
page.pause() # Opens inspector
```
### Enable verbose logging
```bash
DEBUG=pw:api uv run /tmp/script.py
```
### Take screenshots on failure
```python
try:
# automation code
except Exception as e:
page.screenshot(path="/tmp/error.png")
raise
```
### Record trace for debugging
```python
context.tracing.start(screenshots=True, snapshots=True, sources=True)
# ... automation ...
context.tracing.stop(path="/tmp/trace.zip")
```
View trace:
```bash
uv run --with playwright playwright show-trace /tmp/trace.zip
```
### Console log from page
```python
page.on("console", lambda msg: print(f"Console: {msg.text}"))
page.on("pageerror", lambda err: print(f"Page error: {err}"))
```
## Docker/CI Specific
### Running in Docker
```dockerfile
FROM mcr.microsoft.com/playwright/python:v1.56.0
WORKDIR /app
COPY script.py .
CMD ["python", "script.py"]
```
### GitHub Actions
```yaml
- name: Install Playwright
run: |
pip install playwright==1.56.0
playwright install chromium --with-deps
```
### Environment variables for CI
```bash
export PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
export XDG_CACHE_HOME=/tmp/.cache
```
SKILL.md
---
name: playwright
description: Browser automation with Playwright for Python. Use when testing websites, taking screenshots, filling forms, scraping web content, or automating browser interactions. Triggers on browser, web testing, screenshots, selenium, puppeteer, or playwright.
---
# Playwright Browser Automation
## Overview
Playwright enables browser automation for web testing, screenshots, form filling, and scraping. This skill uses Python with `uv` for self-contained scripts that require no global installation.
## Prerequisites
- Python 3.10+
- [uv](https://docs.astral.sh/uv/) package manager
- Playwright browser binaries (one-time setup)
## Setup (First Time Only)
> **Claude: Do not run browser installation commands directly.** Suggest these commands to the user and let them run manually. This is a one-time setup that downloads ~200MB of browser binaries.
Suggest the user run:
```bash
# Install Chromium (recommended, ~200MB)
uv run --with playwright playwright install chromium
# Or install all browsers
uv run --with playwright playwright install
```
To verify installation:
```bash
uv run /path/to/plugins/playwright/scripts/check_setup.py
```
## Quick Start
Take a screenshot of any URL:
```bash
uv run /path/to/plugins/playwright/scripts/screenshot.py https://example.com
```
Output: `/tmp/screenshot-{timestamp}.png`
## Common Patterns
### Take a Screenshot
```bash
# Default (visible browser)
uv run scripts/screenshot.py https://example.com
# Full page, headless
uv run scripts/screenshot.py https://example.com --full-page --headless
# Custom output path
uv run scripts/screenshot.py https://example.com -o /tmp/my-shot.png
```
### Navigate and Extract Content
```bash
# Get page title and URL
uv run scripts/navigate.py https://example.com
# Extract all links as JSON
uv run scripts/navigate.py https://example.com --links
# Get page text content
uv run scripts/navigate.py https://example.com --text
```
### Fill and Submit Forms
```bash
uv run scripts/fill_form.py https://example.com/login \
--field "email=test@example.com" \
--field "password=secret123" \
--submit
```
### Execute JavaScript
```bash
uv run scripts/evaluate.py https://example.com "document.title"
uv run scripts/evaluate.py https://example.com "document.querySelectorAll('a').length"
```
## Writing Custom Scripts
Save this template to `/tmp/my-automation.py`:
```python
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = ["playwright==1.56.0"]
# ///
"""Custom Playwright automation script."""
import os
import sys
from playwright.sync_api import sync_playwright
HEADLESS = os.getenv("HEADLESS", "0").lower() in ("1", "true", "yes")
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=HEADLESS)
page = browser.new_page()
try:
page.goto("https://example.com")
print(f"Title: {page.title()}")
# Use semantic locators (preferred)
page.get_by_role("button", name="Submit").click()
page.get_by_label("Email").fill("test@example.com")
# Screenshot
page.screenshot(path="/tmp/result.png")
except Exception as e:
page.screenshot(path="/tmp/error.png")
print(f"Error: {e}", file=sys.stderr)
return 1
finally:
browser.close()
return 0
if __name__ == "__main__":
sys.exit(main())
```
Run with:
```bash
uv run /tmp/my-automation.py
```
## Modern Locator API
**Prefer semantic locators over CSS selectors:**
```python
# PREFERRED: Semantic locators (accessible, stable)
page.get_by_role("button", name="Submit").click()
page.get_by_label("Email").fill("user@example.com")
page.get_by_placeholder("Search...").fill("query")
page.get_by_text("Welcome back").wait_for()
page.get_by_test_id("submit-btn").click()
# AVOID: Raw CSS selectors (fragile)
page.locator("button.btn-primary").click() # Don't use
```
**Combine locators:**
```python
# OR: Match either
page.get_by_role("button", name="New").or_(
page.get_by_text("Create")
).click()
# Filter: Narrow down
page.locator("tr").filter(has_text="Active").first.click()
```
## Quick Reference
| Operation | Code |
|-----------|------|
| Navigate | `page.goto("https://url")` |
| Click | `page.get_by_role("button", name="X").click()` |
| Fill input | `page.get_by_label("Email").fill("value")` |
| Get text | `page.get_by_role("heading").text_content()` |
| Screenshot | `page.screenshot(path="/tmp/shot.png")` |
| Wait | `page.get_by_text("Loaded").wait_for()` |
| Evaluate JS | `page.evaluate("document.title")` |
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `HEADLESS` | Run browser headless | `0` (headed) |
| `SLOW_MO` | Slow down actions (ms) | `0` |
| `VIEWPORT` | Browser viewport | `1280x720` |
| `TRACE` | Enable tracing | `0` (off) |
Example:
```bash
HEADLESS=1 SLOW_MO=250 uv run scripts/screenshot.py https://example.com
```
## Tracing for Debugging
Enable tracing to debug complex automations:
```python
context.tracing.start(screenshots=True, snapshots=True, sources=True)
# ... your automation ...
context.tracing.stop(path="/tmp/trace.zip")
```
View the trace:
```bash
uv run --with playwright playwright show-trace /tmp/trace.zip
```
## Troubleshooting
### "Browser not found"
Suggest the user install browser binaries (do not run directly):
```bash
uv run --with playwright playwright install chromium
```
### "Timeout waiting for element"
Use proper waiting strategies:
```python
# Wait for element to be visible
page.get_by_text("Loaded").wait_for(state="visible")
# Wait for network idle
page.goto(url, wait_until="networkidle")
```
### "Element not interactable"
Ensure element is visible and scroll into view:
```python
element = page.get_by_role("button", name="Submit")
element.scroll_into_view_if_needed()
element.click()
```
### Headless mode issues
Debug with headed mode:
```bash
HEADLESS=0 uv run scripts/screenshot.py https://example.com
```
### Container/CI Issues
Use these Chromium flags:
```python
browser = p.chromium.launch(
headless=True,
args=["--disable-dev-shm-usage", "--no-sandbox"]
)
```
## Advanced Usage
For comprehensive documentation, see:
- [references/api-reference.md](references/api-reference.md) - Full API reference
- [references/selectors.md](references/selectors.md) - Selector patterns
- [references/custom-scripts.md](references/custom-scripts.md) - Script templates
- [references/troubleshooting.md](references/troubleshooting.md) - Common issues