webapp-testing
Toolkit for interacting with and testing local web applications using Playwright. Use when you need to verify a frontend change actually renders/behaves correctly (not just that the code compiles), wh
SKILL.mdskills/webapp-testing/SKILL.md
GitHubWeb Application Testing
Auth flows: See the
playwright-authskill for login flow contracts, storage state persistence, POM vs fixture patterns, and multi-role auth.
To test local web applications, write native Python Playwright scripts.
Helper Scripts Available:
scripts/with_server.py- Manages server lifecycle (supports multiple servers)
Always run scripts with --help first to see usage. DO NOT read the source until you try running the script first and find that a customized solution is abslutely necessary. These scripts can be very large and thus pollute your context window. They exist to be called directly as black-box scripts rather than ingested into your context window.
Decision Tree: Choosing Your Approach
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 → Run: python scripts/with_server.py --help
│ Then use the helper + write simplified Playwright script
│
└─ 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
Example: Using with_server.py
To start a server, run --help first, then use the helper:
Single server:
python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.py
Multiple servers (e.g., backend + frontend):
python scripts/with_server.py \
--server "cd backend && python server.py" --port 3000 \
--server "cd frontend && npm run dev" --port 5173 \
-- python your_automation.py
To create an automation script, include only Playwright logic (servers are managed automatically):
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True) # Always launch chromium in headless mode
page = browser.new_page()
page.goto('http://localhost:5173') # Server already running and ready
page.wait_for_load_state('networkidle') # CRITICAL: Wait for JS to execute
# ... your automation logic
browser.close()
Reconnaissance-Then-Action Pattern
Inspect rendered DOM:
page.screenshot(path='/tmp/inspect.png', full_page=True) content = page.content() page.locator('button').all()Identify selectors from inspection results
Execute actions using discovered selectors
NEVER
- NEVER inspect the DOM before
page.wait_for_load_state('networkidle')on a dynamic app — client-rendered content isn't there yet; selectors resolved pre-render silently miss elements that appear a beat later, and the script "works" against a half-loaded page. - NEVER read a bundled script's source before running it with
--help—with_server.pyand its siblings are designed to be called as black boxes. Reading the source first burns context on server-lifecycle internals you don't need and won't reuse; only fall back to reading source after--helpproves the script genuinely can't do what the task needs. - NEVER launch a headed browser for scripted automation — always
headless=True. Headed mode is for interactive human debugging only; leaving it on silently breaks on a headless CI/sandbox box with no display. - NEVER let a script exit without
browser.close()on every path — an uncaught exception before close() leaks the Chromium process. Once a script does more than a single glance-and-exit, wrap the automation body intry/finally(or a context manager), not a bare linear script. - NEVER hand-roll a
sleep(N)beforepage.goto()to wait for a dev server to come up —scripts/with_server.pyalready solves server-readiness polling; a fixed sleep is a bet on how fast the box happens to be that day and fails first under load. - NEVER default to
page.wait_for_timeout()as the wait strategy — it's a last resort for genuinely non-deterministic timing (an animation, a debounce), not a substitute forwait_for_load_state()/wait_for_selector()/ waiting on a specific response. A fixed timeout that passes locally becomes a flaky coin-flip the moment the machine is under load. - NEVER trust selectors read from a dynamic app's initial/static markup — client-rendered
content can diverge from what's in the raw HTML. Resolve selectors from the rendered DOM
(post-
networkidlescreenshot orpage.content()), never from a source read alone.
Reference Files
- examples/ - Examples showing common patterns. Each is a scenario-gated trigger, not a
bare-filename menu — open the specific file when its scenario matches, skip the rest:
element_discovery.py- MANDATORY: read this when you don't yet know the selectors for a rendered page — before writing actions against buttons/links/inputs you haven't inspected, or when a prior selector guess came back empty/wrong. Shows how to enumerate all buttons, links, and input fields on a live page so you pick selectors from what's actually rendered instead of guessing from source.static_html_automation.py- MANDATORY: read this when the target is a static HTML file with no dev server — i.e. the Decision Tree above routed you to "static HTML" branch. Showsfile://URL construction and the fixed 1920x1080 viewport convention for deterministic screenshots of local files.console_logging.py- MANDATORY: read this when debugging a runtime JS error, an unexpected client-side warning, or any bug report that says "check the console" — not for routine automation runs. Shows thepage.on("console", ...)handler pattern for capturing and persisting console output during a scripted interaction.