webapp-testing

Installation
SKILL.md

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 builddist/

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:

# Take screenshot to see current state
page.screenshot(path="screenshot.png")

# Get page title
print(page.title())

# Wait for dynamic content
page.wait_for_load_state('networkidle')

# Inspect DOM structure
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')

            # Your test logic here

        finally:
            browser.close()

if __name__ == "__main__":
    test_page()

Waiting for Dynamic Content

# Wait for network to settle (React hydration)
page.wait_for_load_state('networkidle')

# Wait for specific element
page.wait_for_selector('[data-testid="charity-card"]')

# Wait for text to appear
page.wait_for_selector('text=Loading...', state='hidden')

Element Selection

Prefer readable selectors:

# By role (best for accessibility)
page.get_by_role("button", name="Toggle theme")

# By text content
page.get_by_text("View Details")

# By test ID (if available)
page.get_by_test_id("charity-card")

# By CSS (fallback)
page.locator(".charity-card")

# By combined selectors
page.locator("article").filter(has_text="Islamic Relief")

Common Actions

# Click
page.get_by_role("button", name="Submit").click()

# Type
page.get_by_label("Email").fill("test@example.com")

# Select dropdown
page.get_by_role("combobox").select_option("option-value")

# Hover
page.get_by_text("Menu").hover()

# Screenshot
page.screenshot(path="test-result.png", full_page=True)

Assertions

from playwright.sync_api import expect

# Element is visible
expect(page.get_by_text("Welcome")).to_be_visible()

# Element has text
expect(page.locator("h1")).to_have_text("Charity Evaluations")

# Element count
expect(page.locator(".charity-card")).to_have_count(10)

# URL
expect(page).to_have_url("http://localhost:5173/charity/123")

# Title
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')

        # Check initial state
        body = page.locator("body")
        initial_bg = body.evaluate("el => getComputedStyle(el).backgroundColor")

        # Toggle theme
        page.get_by_role("button", name="Toggle theme").click()

        # Verify change
        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()

        # Mobile viewport
        page = browser.new_page(viewport={"width": 375, "height": 667})
        page.goto("http://localhost:5173")
        page.wait_for_load_state('networkidle')

        # Check mobile menu is present
        expect(page.get_by_role("button", name="Menu")).to_be_visible()

        # Desktop navbar should be hidden
        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')

        # Find first charity card and click
        first_card = page.locator(".charity-card").first
        charity_name = first_card.locator("h3").text_content()
        first_card.click()

        # Should navigate to detail page
        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()

        # Capture console errors
        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')

        # Navigate through pages
        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

# Run with browser visible
browser = p.chromium.launch(headless=False, slow_mo=500)

# Pause for manual inspection
page.pause()

# Screenshot on failure
try:
    # test code
except Exception as e:
    page.screenshot(path="failure.png")
    raise

Network Inspection

# Log all requests
page.on("request", lambda req: print(f">> {req.method} {req.url}"))
page.on("response", lambda res: print(f"<< {res.status} {res.url}"))

# Wait for specific API call
with page.expect_response("**/api/charities") as response_info:
    page.goto("http://localhost:5173")
response = response_info.value
print(response.json())

Trace Recording

# Record trace for debugging
context = browser.new_context()
context.tracing.start(screenshots=True, snapshots=True)

page = context.new_page()
# ... run tests ...

context.tracing.stop(path="trace.zip")
# View with: npx playwright show-trace trace.zip

Server Management

Start Dev Server for Testing

# Terminal 1: Start server
cd website && npm run dev

# Terminal 2: Run tests
python test_script.py

Programmatic Server Start

import subprocess
import time

def with_server(test_func):
    """Decorator to start/stop dev server"""
    def wrapper():
        # Start server
        server = subprocess.Popen(
            ["npm", "run", "dev"],
            cwd="website",
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE
        )
        time.sleep(5)  # Wait for server to start

        try:
            test_func()
        finally:
            server.terminate()
            server.wait()

    return wrapper

@with_server
def test_homepage():
    # Test runs with server available
    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')  # All requests done
page.wait_for_load_state('domcontentloaded')  # DOM ready
page.wait_for_selector('selector')  # Element exists
page.wait_for_timeout(1000)  # Hard wait (avoid if possible)

Screenshot Options

page.screenshot(
    path="screenshot.png",
    full_page=True,          # Entire scrollable page
    # OR
    clip={"x": 0, "y": 0, "width": 500, "height": 500}  # Specific area
)

Test Checklist

Before shipping:

  • Homepage loads without console errors
  • Theme toggle works (light ↔ dark)
  • Mobile responsive (375px viewport)
  • Tablet responsive (768px viewport)
  • Charity cards render correctly
  • Charity detail pages load
  • Navigation works (all links)
  • Images load (no broken images)
  • No accessibility errors (axe-core)
Related skills

More from uabbasi/good-measure-giving

Installs
1
First Seen
Mar 30, 2026