webapp-testing
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and managing server p
SKILL.mdskills/webapp-testing/SKILL.md
GitHubWeb Application Testing - Playwright Workflows & Standards
A professional testing framework using Playwright and server lifecycles to validate web applications with speed and reliability.
Progressive Disclosure & Helper Utilities
This skill relies on local utility scripts in scripts/ and examples in examples/. Rather than parsing or reading their code directly, execute them as black boxes:
- Server Manager:
scripts/with_server.pymanages one or more server lifecycles during testing. Executepython scripts/with_server.py --helpto verify CLI args. - Reference Implementations: Refer to files under
examples/(likeelement_discovery.pyorconsole_logging.py) to copy boilerplate for common selector and logging setups.
Freedom Calibration & Constraints
- Constraint Level: Medium
- High Rigidity: All browser processes must run headlessly (
headless=True), and elements must be interacted with using explicit wait states (no rawtime.sleep). - Medium Rigidity: Usage of the
with_server.pyrunner to run tests in CI or headless local servers. - High Freedom: Selection of assertion libraries, DOM traversal patterns, and custom reporting structures.
- High Rigidity: All browser processes must run headlessly (
Decision Tree: Choosing the Server & Automation Framework
What type of web application target is being tested?
├─ Pure Static HTML File (No Server)
│ └─ Open file directly using local path syntax
│ └─ Code: page.goto("file:///absolute/path/to/index.html")
├─ Single-Process Server Webapp (Node/Vite, Flask, Django)
│ └─ Run test script wrapped with single process wrapper
│ └─ Command: python scripts/with_server.py --server "npm run dev" --port 5173 -- python test_script.py
└─ Multi-Service Environment (Separate Frontend and Backend API)
└─ Run test script wrapping both processes
└─ Command: python scripts/with_server.py --server "npm run backend" --port 8000 --server "npm run frontend" --port 3000 -- python test_script.py
Professional Mindset & Design Principles
- Determinism (Anti-Flakiness): Avoid static waits at all costs. Web applications load asynchronously. Always pair actions (clicks, navigation) with expectation states (locator visible, network idle).
- Reconnaissance-then-Action: When debugging headless test failures:
- Capture full-page screenshots to check layout states.
- Inspect console logs programmatically to catch uncaught JavaScript exceptions.
- Dump DOM content to look for text fragments when element matches fail.
- Clean Teardowns: Always close pages, contexts, and browsers within
try-finallyblocks or context managers to prevent orphaned browser processes from hogging system memory.
Core Automation Patterns
1. Robust Script Boilerplate (With Error Capturing & Logs)
from playwright.sync_api import sync_playwright
def run_test():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_page()
# Capture console messages
context.on("console", lambda msg: print(f"CONSOLE [{msg.type}]: {msg.text}"))
try:
# Navigate to local server
context.goto("http://localhost:5173")
context.wait_for_load_state("networkidle")
# Assert element visibility before acting
button = context.locator("button#submit-action")
button.wait_for(state="visible", timeout=5000)
button.click()
# Verify result state
result = context.locator(".success-message")
result.wait_for(state="visible", timeout=5000)
assert "Saved successfully" in result.inner_text()
except Exception as e:
# Capture diagnostic screenshot on failure
context.screenshot(path="error_diagnostic.png", full_page=True)
print(f"Test Execution Failed: {e}")
raise e
finally:
browser.close()
if __name__ == "__main__":
run_test()
Critical Anti-Patterns (NEVER List)
| Anti-Pattern | Description | Alternative / Solution |
|---|---|---|
NEVER use static time.sleep() |
Hardcoded pauses slow down suites and cause flaky tests under varying CPU loads. | Use locator.wait_for(state='visible') or page.wait_for_selector(). |
NEVER launch in headed mode (headless=False) |
Running headed browsers in containerized, CI, or headless CLI environments throws display crashes. | Always use p.chromium.launch(headless=True). |
| NEVER ignore page errors / console logs | Silent UI crashes (uncaught JS exceptions) leave tests failing without context. | Bind console events context.on("console", ...) or page error listeners. |
| NEVER make assertions on unstable DOM states | Checking inner text immediately after click without waiting for load causes race conditions. | Wait for the specific element containing the expected text to load. |
| NEVER hardcode server setup script within tests | Bundling server start commands (like subprocess startup) in testing scripts makes processes leak on crash. | Delegate server lifecycle to scripts/with_server.py. |
Common Error Scenarios & Fallbacks
Scenario 1: Port conflict / Server address already in use
- Root Cause: A previously run server crashed without releasing the port, or another process is occupying the required testing port.
- Fallback:
- Kill the process running on that port manually (e.g.,
fuser -k 5173/tcporkill -9 $(lsof -t -i:5173)). - Configure a dynamic port or pass an alternate port to the test suite script and server launcher:
python scripts/with_server.py --server "npm run dev -- --port 5174" --port 5174 -- python test_script.py
- Kill the process running on that port manually (e.g.,
Scenario 2: TimeoutError: waiting for selector/locator
- Root Cause: Element is dynamically generated, slow API response, or selector is incorrect.
- Fallback:
- Increase the wait timeout threshold to confirm if it's a speed issue:
button.wait_for(state="visible", timeout=15000). - Take a screenshot right before the failure to inspect what is rendered:
page.screenshot(path="timeout_debug.png"). - Verify if the selector text/ID matches the current DOM snapshot.
- Increase the wait timeout threshold to confirm if it's a speed issue:
Scenario 3: Click Intercepted (element not clickable)
- Root Cause: A loading spinner, modal dialog overlay, or floating navbar is physically blocking the target element.
- Fallback:
- Wait for the blocking element to become hidden:
page.locator(".spinner").wait_for(state="hidden"). - Force the click event directly via JavaScript (bypasses physical intersection checks):
page.locator("button#submit").click(force=True)
- Wait for the blocking element to become hidden:
Memory Sync
After completing key technical findings, architectural decisions, code refactorings, or risk assessments, you MUST trigger the local memory capture.
- Save the final summary or artifact as a Markdown file in the project directory.
- Invoke the capture script:
python3 ~/memory_system/capture_knowledge.py <file_path> - This ensures that new learnings, policies, and technical snippets are automatically routed to the correct local storage (OKF or ChromaDB).