webapp-testing/SKILL.md
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.
npx skillsauth add szweibel/claude-skills webapp-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Test local web applications using Python and Playwright with battle-tested helper scripts.
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:3000')
page.wait_for_load_state('networkidle') # CRITICAL!
# Interact with page
page.click('button:has-text("Submit")')
assert page.locator('h1').text_content() == 'Success'
browser.close()
User task → Is it static HTML?
├─ Yes → Read HTML file directly to identify selectors
│ ├─ Success → Write Playwright script using selectors
│ └─ Fails/Incomplete → Treat as dynamic (below)
│
└─ No (dynamic webapp) → Is the server already running?
├─ No → Use scripts/with_server.py to manage lifecycle
│ 1. Run: python scripts/with_server.py --help
│ 2. Write Playwright script (server managed automatically)
│
└─ Yes → Reconnaissance-then-action pattern:
1. Navigate and wait for networkidle
2. Take screenshot or inspect DOM
3. Identify selectors from rendered state
4. Execute actions with discovered selectors
Purpose: Start servers, run tests, automatically clean up
Always run with --help first to see current usage. These scripts are black boxes - use them without reading the source.
Single server:
python scripts/with_server.py --server "npm run dev" --port 5173 -- python test.py
Multiple servers (backend + frontend):
python scripts/with_server.py \
--server "cd backend && python server.py" --port 3000 \
--server "cd frontend && npm run dev" --port 5173 \
-- python test.py
Your test script only contains Playwright logic:
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') # Server already running!
page.wait_for_load_state('networkidle')
# ... test logic
browser.close()
Critical for dynamic apps: Discover selectors from rendered state, then act.
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle') # CRITICAL!
# Take screenshot
page.screenshot(path='/tmp/inspect.png', full_page=True)
# Get rendered HTML
content = page.content()
# Discover elements
buttons = page.locator('button').all()
for btn in buttons:
print(f"Button: {btn.text_content()}")
From inspection, find reliable selectors:
text="Login"role=button[name="Submit"]button.primary, #login-form[data-testid="submit-btn"]page.click('button:has-text("Login")')
page.fill('input[name="username"]', 'admin')
page.select_option('select#country', 'US')
page.fill('input[name="email"]', '[email protected]')
page.fill('input[name="password"]', 'secret123')
page.click('button[type="submit"]')
page.wait_for_load_state('networkidle')
# Verify result
success_msg = page.locator('.success-message').text_content()
assert 'Welcome' in success_msg
# Wait for specific element
page.wait_for_selector('.loading-spinner', state='hidden')
page.wait_for_selector('.data-loaded')
# Wait for network idle
page.wait_for_load_state('networkidle')
# Custom timeout
page.wait_for_selector('div.results', timeout=10000) # 10 seconds
logs = []
page.on('console', lambda msg: logs.append(f"{msg.type}: {msg.text}"))
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
# Check for errors
errors = [log for log in logs if 'error' in log.lower()]
print(f"Found {len(errors)} errors:", errors)
# Full page screenshot
page.screenshot(path='full-page.png', full_page=True)
# Element screenshot
element = page.locator('.dashboard')
element.screenshot(path='dashboard.png')
# On failure
try:
page.click('button.does-not-exist')
except:
page.screenshot(path='/tmp/error.png')
raise
text="Exact Text" or :has-text("Partial")role=button[name="Submit"][data-testid="login-form"]#unique-element-id.specific-class (avoid generic names)# Good - Semantic, resilient
page.click('text="Log In"')
page.click('role=button[name="Save Changes"]')
page.click('[data-testid="checkout-button"]')
# Avoid - Brittle
page.click('div > div:nth-child(3) > button') # Too fragile
page.click('.btn-primary') # Too generic
networkidle - Dynamic content not loaded yetdiv > div > div > button breaks easily--help instead, avoid context pollutionnetworkidle before inspectingfinally blocks# ❌ Fails
page.click('button') # Too many matches or not loaded
# ✅ Fix
page.wait_for_selector('button.submit')
page.click('button.submit')
page.wait_for_load_state('networkidle')# ❌ Flaky
page.goto('http://localhost:3000')
page.click('button') # May click before page loads
# ✅ Stable
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
page.wait_for_selector('button:has-text("Start")')
page.click('button:has-text("Start")')
lsof -i :3000with_server.pyThe examples/ directory contains working demonstrations:
with_server.py handles lifecycle managementheadless=True for CI/automationtime.sleep()finally blocks# Browser setup
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Navigation
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
# Finding elements
page.locator('button')
page.locator('text="Submit"')
page.locator('role=button[name="OK"]')
# Actions
page.click('button')
page.fill('input', 'value')
page.select_option('select', 'option1')
page.check('input[type="checkbox"]')
# Waiting
page.wait_for_selector('.element')
page.wait_for_load_state('networkidle')
page.wait_for_timeout(1000) # milliseconds
# Assertions
assert page.locator('h1').text_content() == 'Welcome'
assert page.locator('.error').count() == 0
# Cleanup
browser.close()
examples/ for working code--help for usagedevelopment
Comprehensive spreadsheet creation, editing, and analysis with support for formulas, formatting, data analysis, and visualization. When Claude needs to work with spreadsheets (.xlsx, .xlsm, .csv, .tsv, etc) for: (1) Creating new spreadsheets with formulas and formatting, (2) Reading or analyzing data, (3) Modify existing spreadsheets while preserving formulas, (4) Data analysis and visualization in spreadsheets, or (5) Recalculating formulas
development
Use OCLC WorldCat APIs to search for books and scholarly materials, retrieve bibliographic metadata, check library holdings worldwide, and get classification data. Use when working with ISBNs, DOIs, OCLC numbers, library catalogs, or institutional holdings.
development
Complete guide for Svelte 5 runes ($state, $derived, $effect, $props, $bindable). Use for any Svelte 5 project or when code contains $ prefixed runes. Essential for reactive state management, computed values, side effects, and component props. Covers migration from Svelte 4 reactive statements.
tools
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.