| name | webapp-testing |
| description | Test web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing screenshots, and viewing browser logs. Use when testing the website or debugging UI issues. |
Web Application Testing with Playwright
Test the website using Playwright browser automation. Verify functionality, debug UI issues, capture screenshots, and inspect browser state.
When This Skill Activates
- Testing website functionality
- Debugging UI behavior
- Capturing screenshots for documentation
- Verifying responsive design
- Checking dark/light mode
- Testing user flows (navigation, forms)
- Investigating browser console errors
Project Context
Website stack:
- React 19 + Vite 6.2
- Tailwind CSS v4
- Dev server:
npm run dev (typically port 5173)
- Build:
npm run build → dist/
Key pages to test:
- Landing page (
/)
- Charity list
- Individual charity detail pages
- Methodology page
- About page
Theme states:
- Light mode / Dark mode toggle
- Brand variants (amal / third-bucket)
Testing Workflow
1. Reconnaissance First
Before automating, understand the page:
page.screenshot(path="screenshot.png")
print(page.title())
page.wait_for_load_state('networkidle')
print(page.content())
2. Decision Tree
Is the dev server running?
- Yes → Connect directly to
http://localhost:5173
- No → Start it first, then connect
Is content static or dynamic?
- Static HTML → Read files directly for selectors
- Dynamic (React) → Wait for
networkidle before inspecting
What are you testing?
- Visual appearance → Screenshot
- Functionality → Automated interactions
- Errors → Console log capture
Playwright Patterns
Basic Script Structure
from playwright.sync_api import sync_playwright
def test_page():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
try:
page.goto("http://localhost:5173")
page.wait_for_load_state('networkidle')
finally:
browser.close()
if __name__ == "__main__":
test_page()
Waiting for Dynamic Content
page.wait_for_load_state('networkidle')
page.wait_for_selector('[data-testid="charity-card"]')
page.wait_for_selector('text=Loading...', state='hidden')
Element Selection
Prefer readable selectors:
page.get_by_role("button", name="Toggle theme")
page.get_by_text("View Details")
page.get_by_test_id("charity-card")
page.locator(".charity-card")
page.locator("article").filter(has_text="Islamic Relief")
Common Actions
page.get_by_role("button", name="Submit").click()
page.get_by_label("Email").fill("test@example.com")
page.get_by_role("combobox").select_option("option-value")
page.get_by_text("Menu").hover()
page.screenshot(path="test-result.png", full_page=True)
Assertions
from playwright.sync_api import expect
expect(page.get_by_text("Welcome")).to_be_visible()
expect(page.locator("h1")).to_have_text("Charity Evaluations")
expect(page.locator(".charity-card")).to_have_count(10)
expect(page).to_have_url("http://localhost:5173/charity/123")
expect(page).to_have_title("Good Measure Giving")
Test Scenarios
Theme Toggle
def test_theme_toggle():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("http://localhost:5173")
page.wait_for_load_state('networkidle')
body = page.locator("body")
initial_bg = body.evaluate("el => getComputedStyle(el).backgroundColor")
page.get_by_role("button", name="Toggle theme").click()
new_bg = body.evaluate("el => getComputedStyle(el).backgroundColor")
assert initial_bg != new_bg, "Theme should change"
browser.close()
Responsive Design
def test_mobile_view():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 375, "height": 667})
page.goto("http://localhost:5173")
page.wait_for_load_state('networkidle')
expect(page.get_by_role("button", name="Menu")).to_be_visible()
expect(page.locator("nav.desktop-nav")).to_be_hidden()
page.screenshot(path="mobile-view.png")
browser.close()
Charity Card Navigation
def test_charity_navigation():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("http://localhost:5173")
page.wait_for_load_state('networkidle')
first_card = page.locator(".charity-card").first
charity_name = first_card.locator("h3").text_content()
first_card.click()
page.wait_for_load_state('networkidle')
expect(page.locator("h1")).to_contain_text(charity_name)
browser.close()
Console Error Capture
def test_no_console_errors():
errors = []
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.on("console", lambda msg: errors.append(msg.text) if msg.type == "error" else None)
page.goto("http://localhost:5173")
page.wait_for_load_state('networkidle')
page.get_by_text("Methodology").click()
page.wait_for_load_state('networkidle')
browser.close()
assert len(errors) == 0, f"Console errors found: {errors}"
Debugging Tips
Visual Debugging
browser = p.chromium.launch(headless=False, slow_mo=500)
page.pause()
try:
except Exception as e:
page.screenshot(path="failure.png")
raise
Network Inspection
page.on("request", lambda req: print(f">> {req.method} {req.url}"))
page.on("response", lambda res: print(f"<< {res.status} {res.url}"))
with page.expect_response("**/api/charities") as response_info:
page.goto("http://localhost:5173")
response = response_info.value
print(response.json())
Trace Recording
context = browser.new_context()
context.tracing.start(screenshots=True, snapshots=True)
page = context.new_page()
context.tracing.stop(path="trace.zip")
Server Management
Start Dev Server for Testing
cd website && npm run dev
python test_script.py
Programmatic Server Start
import subprocess
import time
def with_server(test_func):
"""Decorator to start/stop dev server"""
def wrapper():
server = subprocess.Popen(
["npm", "run", "dev"],
cwd="website",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
time.sleep(5)
try:
test_func()
finally:
server.terminate()
server.wait()
return wrapper
@with_server
def test_homepage():
pass
Quick Reference
Viewport Sizes
| Device | Width | Height |
|---|
| Mobile | 375 | 667 |
| Tablet | 768 | 1024 |
| Desktop | 1280 | 720 |
| Large | 1920 | 1080 |
Common Waits
page.wait_for_load_state('networkidle')
page.wait_for_load_state('domcontentloaded')
page.wait_for_selector('selector')
page.wait_for_timeout(1000)
Screenshot Options
page.screenshot(
path="screenshot.png",
full_page=True,
clip={"x": 0, "y": 0, "width": 500, "height": 500}
)
Test Checklist
Before shipping: