AgentSkillsCN

Testing Webapps

Playwright工具包,可用于与本地Web应用进行交互并开展测试。支持验证前端功能、调试UI行为、截取浏览器屏幕截图,以及查看浏览器日志。

SKILL.md
--- frontmatter
name: Testing Webapps
description: Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
license: Complete terms in LICENSE.txt

Web Application Testing

Write native Python Playwright scripts to test local webapps.

Helper: scripts/with_server.py manages server lifecycle. Run with --help first.

Approach

Static HTML: Read file → identify selectors → write script

Dynamic webapp:

  • Server not running: Use with_server.py
  • Server running: Navigate → wait networkidle → inspect → act

Server Management

bash
# Single server
python scripts/with_server.py --server "npm run dev" --port 5173 -- python automation.py

# Multiple servers
python scripts/with_server.py \
  --server "cd backend && python server.py" --port 3000 \
  --server "cd frontend && npm run dev" --port 5173 \
  -- python automation.py

Script Patterns

Automation:

python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.goto('http://localhost:5173')
    page.wait_for_load_state('networkidle') # CRITICAL for dynamic apps
    # automation logic here
    browser.close()

Reconnaissance:

python
page.screenshot(path='/tmp/inspect.png', full_page=True)
page.content() # Get HTML
page.locator('button').all() # Find elements

Headless Mode + Trace Viewer (Recommended for macOS)

Problem: Headed mode steals window focus on macOS, disrupting workflow.

Solution: Run headless with trace recording:

python
import os
headless = os.getenv('HEADED') != '1'  # Default headless, override with HEADED=1

browser = p.chromium.launch(headless=headless)
context = browser.new_context()
context.tracing.start(screenshots=True, snapshots=True, sources=True)
page = context.new_page()

# ... test code ...

# Save trace on completion
context.tracing.stop(path="/tmp/trace_testname_SUCCESS.zip")

Debug traces:

bash
playwright show-trace /tmp/trace_testname_SUCCESS.zip

Why better than headed: Step through at your own pace, inspect DOM at any point, see network requests, no window disruption.

Selector Best Practices

Emoji-safe text matching:

python
# ❌ Fails with emoji
page.locator('text="Mission Control"')

# ✅ Works with "Mission Control 🚀"
page.locator('text=/Mission Control/')

Button-specific selectors:

python
# ❌ Too generic, matches any text
page.locator('text="Create World"')

# ✅ Specific to buttons
page.locator('button:has-text("Create World")')

Form field specificity:

python
# ❌ Fragile, matches wrong element
page.locator('textarea').first

# ✅ Specific placeholder
page.locator('textarea[placeholder*="description"]')
page.locator('input[type="text"]').nth(2)  # If index matters

Wait for both visible AND enabled:

python
button = page.locator('button:has-text("Submit")')
expect(button).to_be_visible(timeout=5000)
expect(button).to_be_enabled(timeout=5000)  # Critical for form buttons!
button.click()

Form Testing Pattern

Rule: Fill → Wait for enabled → Click

Wrong order (causes timeouts):

python
# ❌ Button is disabled, causes "element not enabled" timeout
button.click()
textarea.fill("content")

Correct order:

python
# ✅ Button becomes enabled after fill
textarea = page.locator('textarea[placeholder="description"]')
expect(textarea).to_be_visible(timeout=5000)
textarea.fill("content")

button = page.locator('button:has-text("Submit")')
expect(button).to_be_enabled(timeout=5000)  # Now enabled
button.click()

Why: Most forms disable submit buttons until validation passes. Always fill first.

Test Setup: Database State

Pattern for clean test runs:

bash
# Reset database before tests
rm -f backend/database.db
cd backend && python -c "from src.database import init_db; import asyncio; asyncio.run(init_db())"

In test runner:

python
from pathlib import Path
import subprocess

def setup_clean_database():
    """Reset database to clean state."""
    db_path = Path("backend/database.db")
    if db_path.exists():
        db_path.unlink()
    subprocess.run([
        "python", "-c",
        "from src.database import init_db; import asyncio; asyncio.run(init_db())"
    ], cwd="backend")

Why: Prevents UUID conflicts, UNIQUE constraint violations, and flaky tests from stale data.

Debugging Triad: Screenshot + Trace + Console

Always capture all three:

python
# Setup
context.tracing.start(screenshots=True, snapshots=True, sources=True)
logs = []
page.on("console", lambda msg: logs.append(f"[{msg.type}] {msg.text}"))

# During test - take screenshots at key steps
page.screenshot(path='/tmp/test_step1.png')

# On failure
context.tracing.stop(path="/tmp/trace_FAILED.zip")
print(f"Console logs (last 20):")
for log in logs[-20:]:
    print(f"  {log}")

Why each matters:

  • Screenshots: Visual state at failure point
  • Trace: Full interaction timeline, DOM snapshots, network activity
  • Console: React errors, API failures, JavaScript warnings

Debugging workflow:

  1. Check console logs for errors first (fastest)
  2. View screenshot to understand visual state
  3. Open trace with playwright show-trace to step through and inspect DOM

Troubleshooting

"Fix doesn't work" - Tests still fail after code change

Symptom: Fixed a bug but tests still fail with same error.

Causes & Solutions:

  1. Frontend hot reload hasn't applied changes

    • Verify file: grep "new code" file.jsx
    • Check dev server console for reload confirmation
    • Hard restart: Kill dev server, npm run dev
  2. Browser cache

    • Use page.goto(..., wait_until='networkidle')
    • Or clear: context.clear_cookies()

Generic selectors match wrong elements

Symptom: textarea.first or button.last fails unexpectedly or matches wrong element.

Cause: DOM structure changed or multiple matching elements exist.

Solution: Use attribute selectors:

python
# ❌ Fragile - depends on DOM order
page.locator('textarea').first

# ✅ Robust - matches specific element
page.locator('textarea[placeholder="World description"]')
page.locator('button:has-text("Create")').first  # If multiple, be specific

"Element not enabled" timeouts

Symptom: page.click() times out with "element is not enabled".

Cause: Trying to click button before form validation passes.

Solution: Fill form first, then wait for enabled:

python
# Fill all required fields first
input1.fill("value1")
input2.fill("value2")

# Then wait for button to enable
button = page.locator('button:has-text("Submit")')
expect(button).to_be_enabled(timeout=5000)
button.click()

Critical Rules

  • Always page.wait_for_load_state('networkidle') before DOM inspection
  • Default to headless with trace recording for debugging without window disruption
  • Fill forms before clicking submit buttons (they're usually disabled)
  • Use specific selectors with attributes, not generic .first/.last
  • Capture triad: screenshots + trace + console logs for debugging
  • Close browser when done
  • See examples/ for more patterns