skills/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. Activate on: Playwright, webapp testing, browser automation, E2E testing, UI testing. NOT for API-only testing without browser, unit tests, or mobile app testing.
npx skillsauth add curiositech/windags-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.
Write native Python Playwright scripts to test local web applications.
✅ Use for:
❌ NOT for:
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 → Start server first, then run Playwright
│
└─ Yes → Reconnaissance-then-action:
1. Navigate and wait for networkidle
2. Take screenshot or inspect DOM
3. Identify selectors from rendered state
4. Execute actions with discovered selectors
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True) # Always headless
page = browser.new_page()
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle') # CRITICAL for SPAs
# ... your test logic
browser.close()
Step 1: Inspect rendered DOM
page.screenshot(path='/tmp/inspect.png', full_page=True)
content = page.content()
buttons = page.locator('button').all()
Step 2: Identify selectors from inspection results
Step 3: Execute actions using discovered selectors
Role-based (best for accessibility):
page.get_by_role("button", name="Submit")
page.get_by_role("textbox", name="Email")
Text-based (readable, but fragile to copy changes):
page.get_by_text("Sign In")
page.get_by_label("Password")
Test IDs (stable, explicit):
page.get_by_test_id("login-button")
CSS selectors (last resort):
page.locator(".btn-primary")
page.locator("#submit-form")
Symptom: Tests pass locally, fail in CI; elements not found
Problem: Modern SPAs load content dynamically after initial page load
Solution:
# ❌ Wrong
page.goto('http://localhost:3000')
page.click('button') # Element may not exist yet
# ✅ Correct
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
page.click('button')
Symptom: time.sleep(3) scattered throughout tests
Problem: Slow, unreliable, doesn't adapt to actual page state
Solution:
# ❌ Wrong
time.sleep(5)
page.click('.dynamic-button')
# ✅ Correct
page.wait_for_selector('.dynamic-button', state='visible')
page.click('.dynamic-button')
Symptom: Empty page content, missing elements in static analysis
Problem: Reading HTML before client-side rendering completes
Solution: Always wait for networkidle on dynamic apps before inspection
# Wait for element to appear
page.wait_for_selector('#my-element')
# Wait for element to be visible
page.wait_for_selector('#my-element', state='visible')
# Wait for element to be hidden
page.wait_for_selector('#my-element', state='hidden')
# Wait for navigation
page.wait_for_url('**/dashboard')
# Wait for network idle (all requests complete)
page.wait_for_load_state('networkidle')
# Custom wait with timeout
page.wait_for_function('document.querySelector(".loaded")')
# Full page screenshot
page.screenshot(path='/tmp/full.png', full_page=True)
# Element screenshot
page.locator('#header').screenshot(path='/tmp/header.png')
# Before/after comparison
page.screenshot(path='/tmp/before.png')
# ... perform action ...
page.screenshot(path='/tmp/after.png')
# Capture all console messages
messages = []
page.on('console', lambda msg: messages.append({
'type': msg.type,
'text': msg.text
}))
# Filter errors only
page.on('console', lambda msg:
print(f'ERROR: {msg.text}') if msg.type == 'error' else None
)
# Fill form fields
page.fill('#email', '[email protected]')
page.fill('#password', 'secret123')
# Select dropdown
page.select_option('#country', 'US')
# Check checkbox
page.check('#terms')
# Submit form
page.click('button[type="submit"]')
# Verify submission
page.wait_for_url('**/success')
from playwright.sync_api import expect
# Element assertions
expect(page.locator('#title')).to_have_text('Welcome')
expect(page.locator('#count')).to_have_text('5')
expect(page.locator('.error')).to_be_hidden()
expect(page.locator('#submit')).to_be_enabled()
# Page assertions
expect(page).to_have_url('http://localhost:3000/dashboard')
expect(page).to_have_title('My App')
# Handle popup windows
with page.expect_popup() as popup_info:
page.click('#open-popup')
popup = popup_info.value
popup.wait_for_load_state()
# Handle new tabs
with context.expect_page() as new_page_info:
page.click('a[target="_blank"]')
new_page = new_page_info.value
tests/
├── conftest.py # Shared fixtures
├── test_login.py # Login flows
├── test_dashboard.py # Dashboard features
├── test_forms.py # Form submissions
└── screenshots/ # Visual artifacts
# Run single test file
python -m pytest tests/test_login.py
# Run with browser visible (debugging)
PWDEBUG=1 python -m pytest tests/test_login.py
# Generate trace for debugging
python -m pytest --tracing=on tests/test_login.py
sync_playwright() for synchronous scriptswait_for_selector(), wait_for_load_state()This skill encodes: Playwright best practices | Selector strategies | Wait patterns | Anti-pattern prevention | E2E testing workflows
data-ai
license: Apache-2.0 NOT for unrelated tasks outside this domain.
development
Use when designing caching strategies (cache-aside, write-through, write-behind), implementing distributed locks, building rate limiters, leaderboards, real-time streams (XADD/consumer groups), pub/sub, or tuning eviction policies. Triggers: thundering-herd on cache miss, dogpile on key expiry, Redlock vs SET-NX-PX choice, sliding-window rate limiter, hot-key on a single cluster slot, big-key blowup, MULTI/EXEC across slots, KEYS in production. NOT for Redis Cluster operations/admin (different domain), embedded KV (SQLite, leveldb), in-process LRU caches, or Memcached.
tools
Drawing the `'use client'` boundary correctly in React Server Components apps (Next.js App Router, RSC frameworks) — leaf-pushing, slot composition, serialization rules, and environment poisoning prevention. Grounded in react.dev and Next.js 16 docs.
development
Use when designing rate limiting for an API, choosing between token bucket / sliding window / leaky bucket / fixed window, implementing it in Redis, deciding edge (Cloudflare/Upstash) vs origin enforcement, sizing per-user vs per-IP vs per-endpoint quotas, returning the right 429 response with Retry-After, or fixing the boundary-burst bug in fixed-window limiters. Triggers: 429 too many requests, INCR + EXPIRE, ZADD + ZREMRANGEBYSCORE + ZCARD, X-RateLimit-Remaining header, Cloudflare WAF rate limiting rules, Upstash @upstash/ratelimit, leaky bucket shaping vs policing, distributed rate limiter consistency. NOT for DDoS mitigation specifically (different scale), CAPTCHA / bot management, full WAF design, or per-user quota billing.