# Web Application Testing 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:** ```bash python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.py ``` **Multiple servers (e.g., backend + frontend):** ```bash 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): ```python 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 1. **Inspect rendered DOM**: ```python page.screenshot(path='/tmp/inspect.png', full_page=True) content = page.content() page.locator('button').all() ``` 2. **Identify selectors** from inspection results 3. **Execute actions** using discovered selectors ## Common Pitfall **Don't** inspect the DOM before waiting for `networkidle` on dynamic apps **Do** wait for `page.wait_for_load_state('networkidle')` before inspection ## Best Practices - **Use bundled scripts as black boxes** - To accomplish a task, consider whether one of the scripts available in `scripts/` can help. These scripts handle common, complex workflows reliably without cluttering the context window. Use `--help` to see usage, then invoke directly. - Use `sync_playwright()` for synchronous scripts - Always close the browser when done - Use descriptive selectors: `text=`, `role=`, CSS selectors, or IDs - Add appropriate waits: `page.wait_for_selector()` or `page.wait_for_timeout()` ## Helper Script: with_server.py Starts one or more servers, waits for them to be ready, runs a command, then cleans up. ```python #!/usr/bin/env python3 import subprocess import socket import time import sys import argparse def is_server_ready(port, timeout=30): """Wait for server to be ready by polling the port.""" start_time = time.time() while time.time() - start_time < timeout: try: with socket.create_connection(('localhost', port), timeout=1): return True except (socket.error, ConnectionRefusedError): time.sleep(0.5) return False def main(): parser = argparse.ArgumentParser(description='Run command with one or more servers') parser.add_argument('--server', action='append', dest='servers', required=True, help='Server command (can be repeated)') parser.add_argument('--port', action='append', dest='ports', type=int, required=True, help='Port for each server (must match --server count)') parser.add_argument('--timeout', type=int, default=30, help='Timeout in seconds per server (default: 30)') parser.add_argument('command', nargs=argparse.REMAINDER, help='Command to run after server(s) ready') args = parser.parse_args() # Remove the '--' separator if present if args.command and args.command[0] == '--': args.command = args.command[1:] if not args.command: print("Error: No command specified to run") sys.exit(1) # Parse server configurations if len(args.servers) != len(args.ports): print("Error: Number of --server and --port arguments must match") sys.exit(1) servers = [] for cmd, port in zip(args.servers, args.ports): servers.append({'cmd': cmd, 'port': port}) server_processes = [] try: # Start all servers for i, server in enumerate(servers): print(f"Starting server {i+1}/{len(servers)}: {server['cmd']}") # Use shell=True to support commands with cd and && process = subprocess.Popen( server['cmd'], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) server_processes.append(process) # Wait for this server to be ready print(f"Waiting for server on port {server['port']}...") if not is_server_ready(server['port'], timeout=args.timeout): raise RuntimeError(f"Server failed to start on port {server['port']} within {args.timeout}s") print(f"Server ready on port {server['port']}") print(f"\nAll {len(servers)} server(s) ready") # Run the command print(f"Running: {' '.join(args.command)}\n") result = subprocess.run(args.command) sys.exit(result.returncode) finally: # Clean up all servers print(f"\nStopping {len(server_processes)} server(s)...") for i, process in enumerate(server_processes): try: process.terminate() process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill() process.wait() print(f"Server {i+1} stopped") print("All servers stopped") if __name__ == '__main__': main() ``` ## Example: Element Discovery Discovering buttons, links, and inputs on a page: ```python from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() # Navigate to page and wait for it to fully load page.goto('http://localhost:5173') page.wait_for_load_state('networkidle') # Discover all buttons on the page buttons = page.locator('button').all() print(f"Found {len(buttons)} buttons:") for i, button in enumerate(buttons): text = button.inner_text() if button.is_visible() else "[hidden]" print(f" [{i}] {text}") # Discover links links = page.locator('a[href]').all() print(f"\nFound {len(links)} links:") for link in links[:5]: # Show first 5 text = link.inner_text().strip() href = link.get_attribute('href') print(f" - {text} -> {href}") # Discover input fields inputs = page.locator('input, textarea, select').all() print(f"\nFound {len(inputs)} input fields:") for input_elem in inputs: name = input_elem.get_attribute('name') or input_elem.get_attribute('id') or "[unnamed]" input_type = input_elem.get_attribute('type') or 'text' print(f" - {name} ({input_type})") # Take screenshot for visual reference page.screenshot(path='/tmp/page_discovery.png', full_page=True) print("\nScreenshot saved to /tmp/page_discovery.png") browser.close() ``` ## Example: Static HTML Automation Using file:// URLs for local HTML files: ```python from playwright.sync_api import sync_playwright import os html_file_path = os.path.abspath('path/to/your/file.html') file_url = f'file://{html_file_path}' with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page(viewport={'width': 1920, 'height': 1080}) # Navigate to local HTML file page.goto(file_url) # Take screenshot page.screenshot(path='/tmp/static_page.png', full_page=True) # Interact with elements page.click('text=Click Me') page.fill('#name', 'John Doe') page.fill('#email', 'john@example.com') # Submit form page.click('button[type="submit"]') page.wait_for_timeout(500) # Take final screenshot page.screenshot(path='/tmp/after_submit.png', full_page=True) browser.close() ``` ## Example: Console Log Capture Capturing console logs during browser automation: ```python from playwright.sync_api import sync_playwright url = 'http://localhost:5173' # Replace with your URL console_logs = [] with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page(viewport={'width': 1920, 'height': 1080}) # Set up console log capture def handle_console_message(msg): console_logs.append(f"[{msg.type}] {msg.text}") print(f"Console: [{msg.type}] {msg.text}") page.on("console", handle_console_message) # Navigate to page page.goto(url) page.wait_for_load_state('networkidle') # Interact with the page (triggers console logs) page.click('text=Dashboard') page.wait_for_timeout(1000) browser.close() # Save console logs to file with open('/tmp/console.log', 'w') as f: f.write('\n'.join(console_logs)) print(f"\nCaptured {len(console_logs)} console messages") ```