Browser automation
The definitive collection of cross-platform Agent Skills. Compatible with Claude Code, Codex, Cursor, OpenClaw, Gemini CLI, Copilot, Hermes. Curated weekly. Higher quality than any alternative.
npx -y skills add JPeetz/agent-skills --skill browser-automationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Browser automation with Playwright — testing, scraping, monitoring, form submission, screenshot capture, and multi-page interaction flows. Covers setup, navigation, element interaction, assertions, and cleanup. Primary keyword clusters: Playwright browser automation, end-to-end testing with Playwright, web scraping Python Playwright, headless browser testing, automated form submission Playwright, synthetic monitoring browser, screenshot capture automation, CI/CD browser testing, page object model Playwright. Designed for agentic platforms — Claude Code, Codex, Cursor, Gemini CLI, OpenClaw, GitHub Copilot, Windsurf, and OpenCode.
SKILL.md
23.5 KB, as published. Nobody here has run it
Browser Automation — Agent Skill
Production-grade browser automation using Playwright. Covers end-to-end testing, web scraping, synthetic monitoring, form automation, and screenshot capture — with safe defaults, robust selectors, and CI/CD portability.
Quick Reference
| Task | Pattern | Selection Priority |
|---|---|---|
| Launch browser (sync) | sync_playwright() → p.chromium.launch(headless=True) | Default for one-off scripts |
| Launch browser (async) | async_playwright() → await p.chromium.launch() | Required for pytest suites |
| Find element (best) | page.get_by_test_id("foo") | 🥇 data-testid |
| Find element (good) | page.get_by_role("button", name="Submit") | 🥈 role-based |
| Find element (ok) | page.get_by_text("Sign In") | 🥉 user-visible text |
| Find element (last) | page.locator("form#login input") | ⚠️ CSS/XPath fallback |
| Fill a form field | page.get_by_label("Email").fill("[email protected]") | Prefer labels |
| Click & wait for result | await expect(page.get_by_text("Success")).to_be_visible() | Auto-retries 5s |
| Wait without sleeping | page.wait_for_selector("[data-testid='result']", state="visible") | Never time.sleep() |
| Screenshot | page.screenshot(full_page=True) | Redact PII before saving |
| Navigate SPA pages | page.goto(url, wait_until="networkidle") | React/Vue/Angular |
| CI/Docker | Add args=["--no-sandbox"] to launch() | Required on Linux containers |
| Handle errors | Use context managers (async with blocks) | Auto-cleanup on exceptions |
| Wait for API response | async with page.expect_response(...) | No brittle sleeps |
| Respectful scraping | RespectfulScraper with 1-3s delay | Check robots.txt first |
When to Use This Skill
Trigger this skill when the user asks for:
- Browser testing — "write an e2e test for the login flow", "test this form", "check if the dashboard loads", "automate browser regression tests"
- Web scraping — "extract all product prices from this page", "scrape the table data", "get the article text", "crawl product listings from this site"
- Form automation — "fill out this multi-step form", "submit the registration", "bulk-upload via the web UI", "automate this checkout flow"
- Screenshot capture — "take a screenshot of the page", "capture the error state", "full-page screenshot of this blog", "screenshot every page of this site"
- Synthetic monitoring — "check if the site is up and the login works", "monitor this checkout flow every 5 minutes", "set up health-check for the dashboard"
- Multi-page flows — "go through the onboarding wizard", "walk through the purchase funnel", "verify the password-reset flow", "test the entire signup-to-purchase journey"
Do NOT trigger for:
- Asking about browser features without automation intent ("what browsers support WebGPU?")
- General Playwright API questions without a concrete task ("how does page.waitForSelector work?")
- Discussing browser compatibility in the abstract
- Requests to manually test something in a browser
- UI/UX design feedback without automation
- Asking "what's different between Chrome and Firefox rendering?" — factual, no automation
Common Pitfalls & Anti-Patterns
❌ NEVER do these
-
time.sleep(N)— brittle, slow, and flaky- Instead:
page.wait_for_selector(),page.wait_for_load_state(),expect().to_be_visible()
- Instead:
-
Hardcoding credentials in scripts
- Instead:
os.environ["TEST_PASSWORD"],.envfiles, or CI secrets
- Instead:
-
Committing screenshots with PII to version control
- Instead: Redact sensitive fields, use
clipparameter, or skip screenshots in CI
- Instead: Redact sensitive fields, use
-
Scraping without rate limits — you'll get IP-banned
- Instead: Use
RespectfulScraperpattern, 1+ second delays, respectrobots.txt
- Instead: Use
-
Using brittle CSS selectors like
.col-md-4 > div:nth-child(3) > a- Instead: Prioritize
data-testid,get_by_role,get_by_label,get_by_text
- Instead: Prioritize
-
Mixing sync and async Playwright APIs in the same script
- Pick one API and stay consistent.
sync_playwright()for scripts,async_playwright()for test suites.
- Pick one API and stay consistent.
-
Forgetting
--no-sandboxin Docker/CI- Add
args=["--no-sandbox"]to everylaunch()call. Without it, Chromium refuses to start.
- Add
-
Using
page.content()for data extraction instead of.evaluate_all()or.text_content()page.content()returns raw HTML that you then have to parse. Use Playwright's built-in extraction.
-
Not handling cookie banners or modals before interacting with page content
- Always dismiss cookie consents, accept dialogs, or close overlays before interacting.
-
Leaving browser processes open on script error
- Always use context managers (
async with/withblocks) — they clean up even on exceptions.
- Always use context managers (
✅ Debugging Checklist (when things go wrong)
- Did you wait for the element to be visible before interacting?
- Is the selector valid? Test with
playwright codegento verify. - Are you using the right
wait_untilstrategy for your page type (SPA vs MPA)? - If in CI/Docker, did you add
--no-sandbox? - Is there a cookie consent modal blocking interaction?
- Are you behind a proxy/VPN that interferes with browser network?
- Did the page trigger a download dialog? Handle with
page.on("download"). - Is the browser closed too early? Check
finallyblock or context manager exit order.
Workflow
Follow this ordered pipeline for every browser automation task:
1. Setup
# Synchronous (preferred for simple scripts)
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
viewport={"width": 1280, "height": 720},
user_agent="Mozilla/5.0 (compatible; AutomationBot/1.0)"
)
page = context.new_page()
# Async (required for pytest-playwright, larger suites)
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
asyncio.run(main())
Decision points:
- Headless vs headed: Use
headless=Trueby default. Useheadless=Falsewhen the user needs to observe the action or debug a visual issue. - Sync vs async: Use sync for one-off scripts and quick tasks. Use async when writing pytest fixtures, concurrent scrapers, or large test suites.
- Chromium vs Firefox vs WebKit: Default to Chromium for broadest compatibility. Use Firefox/WebKit only when explicitly requested for cross-browser testing.
CI tip: If the user mentions CI/CD, add args=["--no-sandbox"] to launch() for Docker/Linux environments.
2. Navigation
# Basic navigation with timeout
try:
page.goto("https://example.com", wait_until="domcontentloaded", timeout=30000)
except playwright._impl._api_types.TimeoutError:
print("Navigation timed out — site may be down or slow")
raise
# Wait for network idle (SPA-heavy pages)
page.goto("https://spa-app.example.com", wait_until="networkidle")
# Useful post-navigation waits
page.wait_for_load_state("domcontentloaded") # HTML parsed
page.wait_for_load_state("load") # all resources loaded
page.wait_for_load_state("networkidle") # no network for 500ms
Navigation strategies by page type:
| Page type | wait_until | Additional wait |
|---|---|---|
| Server-rendered (MPA) | domcontentloaded | None usually |
| SPA / React / Vue | networkidle | wait_for_selector on key element |
| Streaming / WebSocket | domcontentloaded | Wait for specific content or state |
| Slow third-party embeds | load | Ignore third-party timeouts |
3. Interaction
Selectors — in order of preference:
# 1. data-testid (most stable)
page.click("[data-testid='submit-button']")
# 2. Role-based (accessible)
page.get_by_role("button", name="Submit").click()
# 3. Text content (user-visible stable)
page.get_by_text("Sign In").click()
# 4. Label (forms)
page.get_by_label("Email address").fill("[email protected]")
# 5. Placeholder
page.get_by_placeholder("Search...").fill("query")
# 6. CSS/XPath (last resort)
page.locator("form#login input[name='email']").fill("[email protected]")
Common interaction patterns:
# Form filling
await page.get_by_label("Email").fill("[email protected]")
await page.get_by_label("Password").fill("s3cret")
await page.get_by_role("button", name="Log In").click()
# Dropdown / select
await page.select_option("select#country", value="DE")
# Checkbox / radio
await page.get_by_label("I agree to terms").check()
# File upload
await page.set_input_files("input[type='file']", "/path/to/file.pdf")
# Hover and nested interactions
await page.get_by_text("Products").hover()
await page.get_by_text("New Arrivals").click()
# Keyboard shortcuts
await page.keyboard.press("Enter")
await page.keyboard.press("Control+A")
Wait strategies:
# Never use time.sleep(). Use these instead:
await page.wait_for_selector("[data-testid='result']", state="visible", timeout=10000)
await page.wait_for_function("() => document.querySelector('.spinner') === null")
await page.wait_for_url("**/dashboard**")
await page.wait_for_load_state("networkidle")
# For dynamic content that appears/disappears
await expect(page.get_by_text("Loading...")).to_be_hidden(timeout=15000)
await expect(page.get_by_text("Results")).to_be_visible(timeout=15000)
# For network-triggered updates
async with page.expect_response(lambda r: "/api/results" in r.url):
await page.click("[data-testid='search-button']")
4. Assertion
from playwright.async_api import expect
# Page-level
await expect(page).to_have_title("Dashboard — My App")
await expect(page).to_have_url("https://app.example.com/dashboard")
# Element visibility
await expect(page.get_by_text("Welcome back")).to_be_visible()
await expect(page.locator(".error-banner")).to_be_hidden()
# Content
await expect(page.get_by_test_id("user-name")).to_have_text("John Doe")
await expect(page.get_by_test_id("item-count")).to_contain_text("5")
# Form state
await expect(page.get_by_label("Email")).to_have_value("[email protected]")
await expect(page.get_by_label("Agree")).to_be_checked()
# Screenshot-based verification
screenshot = await page.screenshot(full_page=True)
# For visual regression, combine with pixelmatch or Percy
# Custom assertions for scraping
items = await page.locator(".product-card").count()
assert items >= 10, f"Expected at least 10 products, found {items}"
Assertion retry behavior: Playwright expect auto-retries for up to 5 seconds (configurable). This is usually what you want — it handles async rendering without brittle sleeps.
5. Cleanup
# With context managers (recommended)
async with async_playwright() as p:
async with await p.chromium.launch() as browser:
async with await browser.new_page() as page:
await page.goto("https://example.com")
# ... work ...
# Everything auto-closes at block exit
# Manual cleanup (when not using context managers)
await page.close()
await context.close()
await browser.close()
await p.stop() # playwright instance
Always clean up. Orphaned browser processes leak memory and ports. Context managers are the safest default — they handle cleanup even on exceptions.
Error Handling
import asyncio
from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeout
async def robust_navigation(url: str, retries: int = 2):
"""Navigate with retry logic for flaky networks."""
for attempt in range(retries + 1):
try:
await page.goto(url, wait_until="domcontentloaded", timeout=15000)
return
except PlaywrightTimeout:
if attempt == retries:
raise
print(f"Navigation attempt {attempt + 1} failed, retrying...")
await asyncio.sleep(2 ** attempt) # exponential backoff
# Stale element recovery
try:
await page.click("[data-testid='dynamic-button']")
except PlaywrightTimeout:
# Element may have been removed and re-rendered
await page.wait_for_selector("[data-testid='dynamic-button']", state="attached")
await page.click("[data-testid='dynamic-button']")
# Network failure handling
try:
await page.goto("https://flaky-service.example.com")
except Exception as e:
if "net::ERR_" in str(e):
raise RuntimeError(f"Network error accessing page: {e}")
raise
# Modal/dialog handling (accept before interaction)
page.on("dialog", lambda dialog: dialog.accept())
Common Error Patterns
| Error | Likely cause | Fix |
|---|---|---|
TimeoutError on goto | Slow page, network issue | Increase timeout, add retry, check URL |
TimeoutError on selector | Element not rendered yet | Wait for visibility, check selector |
Error: strict mode violation | Selector matches multiple elements | Use .first, .nth(), or refine selector |
Error: Target closed | Page/browser was closed early | Check cleanup order, use context managers |
Error: Element is not visible | Element exists in DOM but hidden | Wait for state="visible" |
net::ERR_CONNECTION_REFUSED | Site is down | Retry, raise clear error, log for monitoring |
Safety Rules
Rate Limiting & Respectful Scraping
import time
from typing import List
class RespectfulScraper:
"""Scraper with built-in politeness delays."""
def __init__(self, min_delay: float = 1.0, max_delay: float = 3.0):
self.min_delay = min_delay
self.max_delay = max_delay
self._last_request = 0.0
self._jitter = __import__("random").uniform
async def wait(self):
"""Enforce minimum delay between requests."""
elapsed = time.monotonic() - self._last_request
if elapsed < self.min_delay:
await asyncio.sleep(self.min_delay - elapsed + self._jitter(0, self.max_delay - self.min_delay))
self._last_request = time.monotonic()
async def get(self, page, url: str) -> None:
await self.wait()
await page.goto(url)
Golden rules:
- Delay between requests — at least 1 second between page loads, longer for small sites
- Respect
robots.txt— check if scraping is allowed - Set a user agent that identifies your automation — don't impersonate real browsers
- Limit concurrent connections — no more than 2-3 parallel pages to the same domain
- Stop on error patterns — if you get 429s, 403s, or captchas, back off
- Don't scrape auth-walled content without explicit permission
Credential Safety
# ✅ DO: Read from environment variables
EMAIL = os.environ["TEST_EMAIL"]
PASSWORD = os.environ["TEST_PASSWORD"]
# ✅ DO: Read from a .env file (gitignored)
from dotenv import load_dotenv
load_dotenv()
# ❌ DON'T: Hardcode credentials
# EMAIL = "[email protected]" # NEVER DO THIS
# ❌ DON'T: Log credentials
# print(f"Logging in as {EMAIL} with password {PASSWORD}") # NEVER
Screenshot Safety
- Redact sensitive fields before capture when possible
- Never commit screenshots with PII to version control
- Use viewport clipping for targeted captures:
page.screenshot(clip={"x": 0, "y": 0, "width": 800, "height": 600})
Quick-Start Templates
Template: E2E Login Test
import os
from playwright.sync_api import sync_playwright, expect
BASE_URL = os.environ.get("BASE_URL", "https://app.example.com")
EMAIL = os.environ["TEST_EMAIL"]
PASSWORD = os.environ["TEST_PASSWORD"]
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Navigate
page.goto(f"{BASE_URL}/login", wait_until="networkidle")
# Fill & submit
page.get_by_label("Email").fill(EMAIL)
page.get_by_label("Password").fill(PASSWORD)
page.get_by_role("button", name="Sign In").click()
# Assert redirect
expect(page).to_have_url(f"{BASE_URL}/dashboard", timeout=10000)
expect(page.get_by_text("Welcome")).to_be_visible()
browser.close()
print("✅ Login test passed")
Template: Web Scraper
import json
import sys
from playwright.sync_api import sync_playwright
URL = sys.argv[1] if len(sys.argv) > 1 else "https://books.toscrape.com"
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(URL, wait_until="domcontentloaded")
books = page.locator(".product_pod").evaluate_all("""
els => els.map(el => ({
title: el.querySelector('h3 a')?.getAttribute('title') || '',
price: el.querySelector('.price_color')?.textContent || '',
availability: el.querySelector('.availability')?.textContent?.trim() || ''
}))
""")
print(json.dumps(books, indent=2))
browser.close()
Template: Multi-Page Monitoring Flow
import os
import sys
from datetime import datetime, timezone
from playwright.sync_api import sync_playwright
URL = os.environ.get("MONITOR_URL", "https://example.com")
def check_flow() -> bool:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
try:
# Step 1: Homepage loads
page.goto(URL, wait_until="domcontentloaded", timeout=15000)
assert page.title(), "Page has no title"
# Step 2: Search works
page.get_by_placeholder("Search").fill("test")
page.keyboard.press("Enter")
page.wait_for_load_state("networkidle")
# Step 3: Results appear
assert page.locator(".search-results").is_visible(), "No results container"
print(f"[{datetime.now(timezone.utc).isoformat()}] ✅ Flow healthy")
return True
except Exception as e:
print(f"[{datetime.now(timezone.utc).isoformat()}] ❌ Flow failed: {e}")
# Save diagnostic screenshot
page.screenshot(path=f"error-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}.png")
return False
finally:
browser.close()
if __name__ == "__main__":
success = check_flow()
sys.exit(0 if success else 1)
Advanced Patterns
Page Object Model
class LoginPage:
def __init__(self, page):
self.page = page
self.email_input = page.get_by_label("Email")
self.password_input = page.get_by_label("Password")
self.submit_button = page.get_by_role("button", name="Sign In")
self.error_message = page.locator(".alert-error")
async def goto(self):
await self.page.goto("/login", wait_until="networkidle")
async def login(self, email: str, password: str):
await self.email_input.fill(email)
await self.password_input.fill(password)
await self.submit_button.click()
async def get_error(self) -> str:
await self.error_message.wait_for(state="visible")
return await self.error_message.text_content()
Network Interception
# Mock API responses for stable tests
await page.route("**/api/users/**", lambda route: route.fulfill(
status=200,
content_type="application/json",
body='{"id": 1, "name": "Test User"}'
))
# Capture network requests for debugging
requests = []
page.on("request", lambda req: requests.append(f"{req.method} {req.url}"))
page.on("response", lambda res: print(f"{res.status} {res.url}"))
# Wait for specific API call to complete
async with page.expect_response(lambda r: "/api/submit" in r.url) as response_info:
await page.click("[data-testid='submit']")
response = await response_info.value
assert response.status == 200
Visual Regression (Screenshot Diffing)
# Capture and compare screenshots
await page.screenshot(path="baseline.png", full_page=True)
# Use with pixelmatch, Percy, or Chromatic for automated diffing
# Element-level screenshot
await page.locator(".pricing-table").screenshot(path="pricing.png")
# Clip to a specific region (avoid dynamic content)
await page.screenshot(
path="header.png",
clip={"x": 0, "y": 0, "width": 1280, "height": 200}
)
Platform Compatibility Notes
Claude Code (VS Code / CLI)
- Sync API preferred for quick scripts
- Use
subprocess.run(["python", "script.py"])to execute - Install:
pip install playwright && playwright install chromium
Codex
- Async API for concurrent task handling
- Each session gets its own browser context for isolation
- Use the
save_screenshotpattern for visual feedback
Cursor
- Native Python execution, both sync and async work
.cursor/rulescan store common Playwright patterns- Leverage the built-in terminal for
playwright codegen
Gemini CLI
- Well-suited for one-shot scraping and monitoring tasks
- Package scripts as standalone Python files
- Use
gemini run script.pyfor execution
OpenClaw
- Install via
pip install playwrightin the OpenClaw environment - Run scripts through exec tool with PTY for headed mode debugging
- Screenshots are auto-attached in chat output
GitHub Copilot
- Works natively in VS Code with Python extension
- Chat can generate complete Playwright scripts
- Use
// @ts-checkcomments for inline documentation
Windsurf
- Native Python execution in the IDE terminal
- Use sync API for quick tasks; async for larger test suites
- Store reusable POM classes in project workspace
OpenCode
- Execute scripts as standalone Python files
- Prefer async API with context managers for safety
- Install Playwright via
pip install playwright && playwright install
References
- Playwright docs: https://playwright.dev/python/docs/intro
- Selectors guide: https://playwright.dev/python/docs/selectors
- API reference: https://playwright.dev/python/docs/api/class-playwright
- Best practices: https://playwright.dev/python/docs/best-practices
- CI configuration: https://playwright.dev/python/docs/ci
See also the companion reference files in this skill:
references/playwright-patterns.md— POM, fixtures, CI setupreferences/selector-strategies.md— Robust selector hierarchyreferences/browser-testing-workflows.md— Testing patterns