Testing Webapps
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.
What this skill does
# 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 patternsRelated in Design
contribute
IncludedLocal-only OSS contribution command center. Auto-refreshes the user's in-flight PR and issue state on invoke so conversations start with full context — no need to brief Claude on what's in flight. Helps the user find issues to contribute to on GitHub, builds per-repo dossiers of what each upstream expects (CLA, DCO, branch convention, AI policy, draft-first, review bots, issue templates), runs deterministic gates before any external action so AI-assisted contributions don't reach maintainers as slop. State is markdown-only: candidate files at ~/.contribute-system/candidates/, repo dossiers at ~/.contribute-system/research/, append-only event log at ~/.contribute-system/log.jsonl. No database, no cloud calls. Use when the user asks about their PRs / issues / contributions, wants to find new work to take on, claim an issue, build/refresh a repo's dossier, or draft a Design Issue or PR. Trigger with "/contribute", "what's my PR status", "find a contribution", "claim issue X", "draft a Design Issue for Y", "refresh dossier for Z".
architectural-analysis
IncludedUser-triggered deep architectural analysis of a codebase or scoped subtree across eight modes — information architecture, data flow, integration points, UI surfaces, interaction patterns, data model, control flow, and failure modes. This skill should be used when the user asks to "diagram this codebase," "map the architecture," "show the data flow," "give me an ERD," "trace control flow," "find the integration points," "verify the layout pattern," "audit the UX architecture," or any similar request whose primary deliverable is mermaid diagrams plus cited reports under docs/architecture/. Dispatches haiku/sonnet sub-agents in parallel for per-mode exploration, then verifies every citation mechanically before any node lands in a diagram. Not for one-off prose explanations of code (use code-explanation) or for high-level system design from scratch (use system-design).
mcp
IncludedModel Context Protocol (MCP) server development and tool management. Languages: Python, TypeScript. Capabilities: build MCP servers, integrate external APIs, discover/execute MCP tools, manage multi-server configs, design agent-centric tools. Actions: create, build, integrate, discover, execute, configure MCP servers/tools. Keywords: MCP, Model Context Protocol, MCP server, MCP tool, stdio transport, SSE transport, tool discovery, resource provider, prompt template, external API integration, Gemini CLI MCP, Claude MCP, agent tools, tool execution, server config. Use when: building MCP servers, integrating external APIs as MCP tools, discovering available MCP tools, executing MCP capabilities, configuring multi-server setups, designing tools for AI agents.
react-native-skia
IncludedDesign, build, debug, and optimise high-polish animated graphics in React Native or Expo using @shopify/react-native-skia, Reanimated, and Gesture Handler. Use when the user wants canvas-driven UI, shaders, paths, rich text, image filters, sprite fields, Skottie, video frames, snapshots, web CanvasKit setup, or performance tuning for custom motion-heavy elements such as loaders, hero art, cards, charts, progress indicators, particle systems, or gesture-driven surfaces. Also use when the user asks for fluid, glow, glass, blob, parallax, 60fps/120fps, or GPU-friendly animated effects in React Native, even if they do not explicitly say "Skia". Do not use for ordinary form/layout work with standard views.
plaid
IncludedProduct Led AI Development — guides founders from idea to launched product. Six capabilities: Idea (discover a product idea), Validate (pressure-test the idea against fatal flaws, problem reality, competition, and 2-week MVP feasibility), Plan (vision intake + document generation), Design (translate image references into a design.md spec), Launch (go-to-market strategy), and Build (roadmap execution). Use when someone says "PLAID", "plaid idea", "help me find an idea", "product idea", "idea from my business", "idea from my expertise", "plaid validate", "validate my idea", "pressure-test", "is this idea good", "find fatal flaws", "validate the problem", "plan a product", "define my vision", "generate a PRD", "product strategy", "plaid design", "design from image", "translate image to design", "create design.md", "extract design tokens", "plaid launch", "go-to-market", "launch plan", "GTM strategy", "launch playbook", "plaid build", "build the app", "start building", or "execute the roadmap".
nextjs-framer-motion-animations
IncludedAdds production-safe Motion for React or Framer Motion animations to Next.js apps, including reveal, hover and tap micro-interactions, whileInView, stagger, AnimatePresence, layout and layoutId transitions, reorder, scroll-linked UI, and lightweight route-content transitions. Use when the user asks to add, refactor, or debug Motion or Framer Motion in App Router or Pages Router codebases, especially around server/client boundaries, reduced motion, LazyMotion, bundle size, hydration, or route transitions. Avoid for GSAP-style timelines, WebGL or 3D scenes, heavy scroll storytelling, or CSS-only effects unless Motion is explicitly requested.