agentsclimarketplace

Browser automation

Skill JPeetz/agent-skills/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.

Install
npx -y skills add JPeetz/agent-skills --skill browser-automation

Assembled 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

TaskPatternSelection 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 fieldpage.get_by_label("Email").fill("[email protected]")Prefer labels
Click & wait for resultawait expect(page.get_by_text("Success")).to_be_visible()Auto-retries 5s
Wait without sleepingpage.wait_for_selector("[data-testid='result']", state="visible")Never time.sleep()
Screenshotpage.screenshot(full_page=True)Redact PII before saving
Navigate SPA pagespage.goto(url, wait_until="networkidle")React/Vue/Angular
CI/DockerAdd args=["--no-sandbox"] to launch()Required on Linux containers
Handle errorsUse context managers (async with blocks)Auto-cleanup on exceptions
Wait for API responseasync with page.expect_response(...)No brittle sleeps
Respectful scrapingRespectfulScraper with 1-3s delayCheck 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

  1. time.sleep(N) — brittle, slow, and flaky

    • Instead: page.wait_for_selector(), page.wait_for_load_state(), expect().to_be_visible()
  2. Hardcoding credentials in scripts

    • Instead: os.environ["TEST_PASSWORD"], .env files, or CI secrets
  3. Committing screenshots with PII to version control

    • Instead: Redact sensitive fields, use clip parameter, or skip screenshots in CI
  4. Scraping without rate limits — you'll get IP-banned

    • Instead: Use RespectfulScraper pattern, 1+ second delays, respect robots.txt
  5. 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
  6. 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.
  7. Forgetting --no-sandbox in Docker/CI

    • Add args=["--no-sandbox"] to every launch() call. Without it, Chromium refuses to start.
  8. 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.
  9. Not handling cookie banners or modals before interacting with page content

    • Always dismiss cookie consents, accept dialogs, or close overlays before interacting.
  10. Leaving browser processes open on script error

    • Always use context managers (async with / with blocks) — they clean up even on exceptions.

✅ Debugging Checklist (when things go wrong)

  • Did you wait for the element to be visible before interacting?
  • Is the selector valid? Test with playwright codegen to verify.
  • Are you using the right wait_until strategy 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 finally block 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=True by default. Use headless=False when 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 typewait_untilAdditional wait
Server-rendered (MPA)domcontentloadedNone usually
SPA / React / Vuenetworkidlewait_for_selector on key element
Streaming / WebSocketdomcontentloadedWait for specific content or state
Slow third-party embedsloadIgnore 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

ErrorLikely causeFix
TimeoutError on gotoSlow page, network issueIncrease timeout, add retry, check URL
TimeoutError on selectorElement not rendered yetWait for visibility, check selector
Error: strict mode violationSelector matches multiple elementsUse .first, .nth(), or refine selector
Error: Target closedPage/browser was closed earlyCheck cleanup order, use context managers
Error: Element is not visibleElement exists in DOM but hiddenWait for state="visible"
net::ERR_CONNECTION_REFUSEDSite is downRetry, 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:

  1. Delay between requests — at least 1 second between page loads, longer for small sites
  2. Respect robots.txt — check if scraping is allowed
  3. Set a user agent that identifies your automation — don't impersonate real browsers
  4. Limit concurrent connections — no more than 2-3 parallel pages to the same domain
  5. Stop on error patterns — if you get 429s, 403s, or captchas, back off
  6. 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_screenshot pattern for visual feedback

Cursor

  • Native Python execution, both sync and async work
  • .cursor/rules can 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.py for execution

OpenClaw

  • Install via pip install playwright in 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-check comments 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

See also the companion reference files in this skill:

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.