jpeetz/agent-skills

browser-automation

Browser automation with Playwright — testing, scraping, monitoring, form submission, screenshot capture, and multi-page interaction flows.

Vedi sorgente

Come usare

  1. Copia il comando e controlla il pacchetto prima di installarlo nel tuo ambiente agente.
  2. Controlla strumenti, credenziali e ambienti richiesti nel documento originale.
  3. Descrivi input e risultato desiderato; confronta l’output con le fonti.
npx skills add https://github.com/jpeetz/agent-skills --skill browser-automation

Prima di iniziare

  • L’installazione non include abbonamenti, quote API o accesso ai file.
  • Stelle e installazioni indicano attività, non un test indipendente di qualità o sicurezza.
Documento Skill originale

Contenuto dal repository con titoli, esempi, codice, tabelle, link e immagini preservati.

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("user@example.com")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()
  1. Hardcoding credentials in scripts
  • Instead: os.environ["TEST_PASSWORD"], .env files, or CI secrets
  1. Committing screenshots with PII to version control
  • Instead: Redact sensitive fields, use clip parameter, or skip screenshots in CI
  1. Scraping without rate limits — you'll get IP-banned
  • Instead: Use RespectfulScraper pattern, 1+ second delays, respect robots.txt
  1. 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
  1. 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.
  1. Forgetting `--no-sandbox` in Docker/CI
  • Add args=["--no-sandbox"] to every launch() call. Without it, Chromium refuses to start.
  1. 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.
  1. Not handling cookie banners or modals before interacting with page content
  • Always dismiss cookie consents, accept dialogs, or close overlays before interacting.
  1. 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

python
# 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

python
# 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:

python
# 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("user@example.com")

# 5. Placeholder
page.get_by_placeholder("Search...").fill("query")

# 6. CSS/XPath (last resort)
page.locator("form#login input[name='email']").fill("user@example.com")

Common interaction patterns:

python
# Form filling
await page.get_by_label("Email").fill("user@example.com")
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:

python
# 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

python
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("user@example.com")
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

python
# 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

python
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

python
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

python
# ✅ 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 = "admin@company.com"  # 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

python
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

python
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

python
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

python
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

python
# 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)

python
# 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

  • 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:

dallo stesso repository

Altri Skills

Tutti gli Skills
jpeetz
Community

accessibility-compliance-audit

Comprehensive web accessibility (a11y) audit and compliance skill. Audits HTML, React, Vue, and Angular codebases against WCAG 2.2 AA standards. Activates when users ask for "accessibility audit", "a11y check", "WCAG compliance", "ADA compliance", "screen reader testing", "keyboard navigation audit", "color contrast check", "accessibility fix", or "make this accessible". Detects violations, explains impact, and generates fix-ready code. Covers semantic HTML, ARIA authoring, focus management, col

installazioni
1
GitHub Stars
14
Aggiornato
21 ott
jpeetz
Community

agent-n-video-pipeline

Use this skill when producing a fully-local "Agent N presents a message" video on the 16GB Apple-Silicon M5 executor: Kokoro mature TTS → LTX-2.3 MLX A2V talking render with native lip sync → karaoke captions → trim → QA → X caption. Replaces the old cloud Herald pipeline (grok-imagine + volcengine) with zero cloud cost. Activates for Herald release videos, Agent N announcements, and newsletter video messages.

installazioni
1
GitHub Stars
14
Aggiornato
21 ott
jpeetz
Community

anti-over-engineering

Use when AI agents over-engineer code beyond user requests.

installazioni
1
GitHub Stars
14
Aggiornato
21 ott
jpeetz
Community

api-and-interface-design

Use this skill when designing APIs, module boundaries, or any public interface. Create stable, well-documented interfaces that are hard to misuse — REST endpoints, GraphQL schemas, module contracts, component props. Covers contract-first design, error semantics, validation boundaries, idempotency, and consistent naming.

installazioni
1
GitHub Stars
14
Aggiornato
21 ott