Skip to main content
skill CX Codex

webapp-testing · bobmatnyc — Claude Code skill: what it does, how to install

A Playwright testing reference for coding agents. It provides selector hierarchies, wait strategies, and form interaction patterns.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add bobmatnyc/claude-mpm-skills --skill webapp-testing -a claude-code
Open on GitHub

What webapp-testing does

This skill injects Playwright automation patterns into the agent context. It covers selector definitions, network state waiting such as networkidle, and assertions using Python examples.

The guide ranks selector reliability directly. It treats data-testid and ARIA role attributes as the safest choices, while XPath is flagged as fragile and worth avoiding. Inside are also patterns for file uploads via set_input_files, dropdown handling, and built-in auto-waiting mechanisms.

To run these tests, your project needs Playwright installed in your test environment.

What it helps with

  • select stable data-testid and ARIA role locators for UI elements
  • configure networkidle or custom load state waits for dynamic pages
  • automate file uploads and dropdown selections in Playwright
  • write visibility and text assertions with expect

Example prompts for the agent

  • Write a Playwright test for the login flow using role-based selectors
  • Add a wait for network idle and check that the submit button is visible
  • Create an upload test using set_input_files and verify the success alert

How to install webapp-testing

1Skills CLI (Vercel)

npx skills add bobmatnyc/claude-mpm-skills --skill webapp-testing -a claude-code

2Manually: put the files in place

~/.claude/skills/webapp-testing/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/webapp-testing/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about webapp-testing

How do I install webapp-testing in Claude Code?

Run npx skills add bobmatnyc/claude-mpm-skills --skill webapp-testing -a claude-code in your terminal.

What does webapp-testing do?

It teaches the agent Playwright automation conventions. The model learns to prioritize semantic locators and native wait conditions over brittle timeouts.

Which selector types are prioritized?

The guide ranks data-testid attributes highest, followed by semantic role selectors. Plain CSS classes and XPath rank lowest due to fragility.

Source text

SKILL.md · MIT
---
name: webapp-testing
description: "Comprehensive web application testing patterns with Playwright selectors, wait strategies, and best practices"
user-invocable: false
disable-model-invocation: true
progressive_disclosure:
  entry_point:
    summary: "Comprehensive web application testing patterns with Playwright selectors, wait strategies, and best practices"
    when_to_use: "When writing tests, implementing webapp-testing-patterns, or ensuring code quality."
    quick_start: "1. Review the core concepts below. 2. Apply patterns to your use case. 3. Follow best practices for implementation."
---
# Playwright Patterns Reference

Complete guide to Playwright automation patterns, selectors, and best practices.

## Table of Contents

- [Selectors](#selectors)
- [Wait Strategies](#wait-strategies)
- [Element Interactions](#element-interactions)
- [Assertions](#assertions)
- [Test Organization](#test-organization)
- [Network Interception](#network-interception)
- [Screenshots and Videos](#screenshots-and-videos)
- [Debugging](#debugging)
- [Parallel Execution](#parallel-execution)

## Selectors

### Text Selectors
Most readable and maintainable approach when text is unique:

```python
page.click('text=Login')
page.click('text="Sign Up"')  # Exact match
page.click('text=/log.*in/i')  # Regex, case-insensitive
```

### Role-Based Selectors
Semantic selectors based on ARIA roles:

```python
page.click('role=button[name="Submit"]')
page.fill('role=textbox[name="Email"]', '[email protected]')
page.click('role=link[name="Learn more"]')
page.check('role=checkbox[name="Accept terms"]')
```

### CSS Selectors
Traditional CSS selectors for precise targeting:

```python
page.click('#submit-button')
page.fill('.email-input', '[email protected]')
page.click('button.primary')
page.click('nav > ul > li:first-child')
```

### XPath Selectors
For complex DOM navigation:

```python
page.click('xpath=//button[contains(text(), "Submit")]')
page.click('xpath=//div[@class="modal"]//button[@type="submit"]')
```

### Data Attributes
Best practice for test-specific selectors:

```python
page.click('[data-testid="submit-btn"]')
page.fill('[data-test="email-input"]', '[email protected]')
```

### Chaining Selectors
Combine selectors for precision:

```python
page.locator('div.modal').locator('button.submit').click()
page.locator('role=dialog').locator('text=Confirm').click()
```

### Selector Best Practices

**Priority order (most stable to least stable):**
1. `data-testid` attributes (most stable)
2. `role=` selectors (semantic, accessible)
3. `text=` selectors (readable, but text may change)
4. `id` attributes (stable if not dynamic)
5. CSS classes (less stable, may change with styling)
6. XPath (fragile, avoid if possible)

## Wait Strategies

### Load State Waits
Essential for dynamic applications:

```python
# Wait for network to be idle (most common)
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')

# Wait for DOM to be ready
page.wait_for_load_state('domcontentloaded')

# Wait for full load including images
page.wait_for_load_state('load')
```

### Element Waits
Wait for specific elements before interacting:

```python
# Wait for element to be visible
page.wait_for_selector('button.submit', state='visible')

# Wait for element to be hidden
page.wait_for_selector('.loading-spinner', state='hidden')

# Wait for element to exist in DOM (may not be visible)
page.wait_for_selector('.modal', state='attached')

# Wait for element to be removed from DOM
page.wait_for_selector('.error-message', state='detached')
```

### Timeout Waits
Fixed time delays (use sparingly):

```python
# Wait for animations to complete
page.wait_for_timeout(500)

# Wait for delayed content (better to use wait_for_selector)
page.wait_for_timeout(2000)
```

### Custom Wait Conditions
Wait for JavaScript conditions:

```python
# Wait for custom JavaScript condition
page.wait_for_function('() => document.querySelector(".data").innerText !== "Loading..."')

# Wait for variable to be set
page.wait_for_function('() => window.appReady === true')
```

### Auto-Waiting
Playwright automatically waits for elements to be actionable:

```python
# These automatically wait for element to be:
# - Visible
# - Stable (not animating)
# - Enabled (not disabled)
# - Not obscured by other elements
page.click('button.submit')  # Auto-waits
page.fill('input.email', '[email protected]')  # Auto-waits
```

## Element Interactions

### Clicking
```python
# Basic click
page.click('button.submit')

# Click with options
page.click('button.submit', button='right')  # Right-click
page.click('button.submit', click_count=2)  # Double-click
page.click('button.submit', modifiers=['Control'])  # Ctrl+click

# Force click (bypass actionability checks)
page.click('button.submit', force=True)
```

### Filling Forms
```python
# Text inputs
page.fill('input[name="email"]', '[email protected]')
page.type('input[name="search"]', 'query', delay=100)  # Type with delay

# Clear then fill
page.fill('input[name="email"]', '')
page.fill('input[name="email"]', '[email protected]')

# Press keys
page.press('input[name="search"]', 'Enter')
page.press('input[name="text"]', 'Control+A')
```

### Dropdowns and Selects
```python
# Select by label
page.select_option('select[name="country"]', label='United States')

# Select by value
page.select_option('select[name="country"]', value='us')

# Select by index
page.select_option('select[name="country"]', index=2)

# Select multiple options
page.select_option('select[multiple]', ['option1', 'option2'])
```

### Checkboxes and Radio Buttons
```python
# Check a checkbox
page.check('input[type="checkbox"]')

# Uncheck a checkbox
page.uncheck('input[type="checkbox"]')

# Check a radio button
page.check('input[value="option1"]')

# Toggle checkbox
if page.is_checked('input[type="checkbox"]'):
    page.uncheck('input[type="checkbox"]')
else:
    page.check('input[type="checkbox"]')
```

### File Uploads
```python
# Upload single file
page.set_input_files('input[type="file"]', '/path/to/file.pdf')

# Upload multiple files
page.set_input_files('input[type="file"]', ['/path/to/file1.pdf', '/path/to/file2.pdf'])

# Clear file input
page.set_input_files('input[type="file"]', [])
```

### Hover and Focus
```python
# Hover over element
page.hover('button.tooltip-trigger')

# Focus element
page.focus('input[name="email"]')

# Blur element
page.evaluate('document.activeElement.blur()')
```

## Assertions

### Element Visibility
```python
from playwright.sync_api import expect

# Expect element to be visible
expect(page.locator('button.submit')).to_be_visible()

# Expect element to be hidden
expect(page.locator('.error-message')).to_be_hidden()
```

### Text Content
```python
# Expect exact text
expect(page.locator('.title')).to_have_text('Welcome')

# Expect partial text
expect(page.locator('.message')).to_contain_text('success')

# Expect text matching pattern
expect(page.locator('.code')).to_have_text(re.compile(r'\d{6}'))
```

### Element State
```python
# Expect element to be enabled/disabled
expect(page.locator('button.submit')).to_be_enabled()
expect(page.locator('button.submit')).to_be_disabled()

# Expect checkbox to be checked
expect(page.locator('input[type="checkbox"]')).to_be_checked()

# Expect element to be editable
expect(page.locator('input[name="email"]')).to_be_editable()
```

### Attributes and Values
```python
# Expect attribute value
expect(page.locator('img')).to_have_attribute('src', '/logo.png')

# Expect CSS class
expect(page.locator('button')).to_have_class('btn-primary')

# Expect input value
expect(page.locator('input[name="email"]')).to_have_value('[email protected]')
```

### Count and Collections
```python
# Expect specific count
expect(page.locator('li')).to_have_count(5)

# Get all elements and assert
items = page.locator('li').all()
assert len(items) == 5
```

## Test Organization

### Basic Test Structure
```python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()

    # Test logic here
    page.goto('http://localhost:3000')
    page.wait_for_load_state('networkidle')

    browser.close()
```

### Using Pytest (Recommended)
```python
import pytest
from playwright.sync_api import sync_playwright

@pytest.fixture(scope="session")
def browser():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        yield browser
        browser.close()

@pytest.fixture
def page(browser):
    page = browser.new_page()
    yield page
    page.close()

def test_login(page):
    page.goto('http://localhost:3000')
    page.fill('input[name="email"]', '[email protected]')
    page.fill('input[name="password"]', 'password123')
    page.click('button[type="submit"]')
    expect(page.locator('.welcome-message')).to_be_visible()
```

### Test Grouping with Describe Blocks
```python
class TestAuthentication:
    def test_successful_login(self, page):
        # Test successful login
        pass

    def test_failed_login(self, page):
        # Test failed login
        pass

    def test_logout(self, page):
        # Test logout
        pass
```

### Setup and Teardown
```python
@pytest.fixture(autouse=True)
def setup_and_teardown(page):
    # Setup - runs before each test
    page.goto('http://localhost:3000')
    page.wait_for_load_state('networkidle')

    yield  # Test runs here

    # Teardown - runs after each test
    page.evaluate('localStorage.clear()')
```

## Network Interception

### Mock API Responses
```python
# Intercept and mock API response
def handle_route(route):
    route.fulfill(
        status=200,
        body='{"success": true, "data": "mocked"}',
        headers={'Content-Type': 'application/json'}
    )

page.route('**/api/data', handle_route)
page.goto('http://localhost:3000')
```

### Block Resources
```python
# Block images and stylesheets for faster tests
page.route('**/*.{png,jpg,jpeg,gif,svg,css}', lambda route: route.abort())
```

### Wait for Network Responses
```python
# Wait for specific API call
with page.expect_response('**/api/users') as response_info:
    page.click('button.load-users')
response = response_info.value
assert response.status == 200
```

## Screenshots and Videos

### Screenshots
```python
# Full page screenshot
page.screenshot(path='/tmp/screenshot.png', full_page=True)

# Element screenshot
page.locator('.modal').screenshot(path='/tmp/modal.png')

# Screenshot with custom dimensions
page.set_viewport_size({'width': 1920, 'height': 1080})
page.screenshot(path='/tmp/desktop.png')
```

### Video Recording
```python
browser = p.chromium.launch(headless=True)
context = browser.new_context(record_video_dir='/tmp/videos/')
page = context.new_page()

# Perform actions...

context.close()  # Video saved on close
```

## Debugging

### Pause Execution
```python
page.pause()  # Opens Playwright Inspector
```

### Console Logs
```python
def handle_console(msg):
    print(f"[{msg.type}] {msg.text}")

page.on("console", handle_console)
```

### Slow Motion
```python
browser = p.chromium.launch(headless=False, slow_mo=1000)  # 1 second delay
```

### Verbose Logging
```python
# Set DEBUG environment variable
# DEBUG=pw:api python test.py
```

## Parallel Execution

### Pytest Parallel
```bash
# Install pytest-xdist
pip install pytest-xdist

# Run tests in parallel
pytest -n auto  # Auto-detect CPU cores
pytest -n 4     # Run with 4 workers
```

### Browser Context Isolation
```python
# Each test gets isolated context (cookies, localStorage, etc.)
@pytest.fixture
def context(browser):
    context = browser.new_context()
    yield context
    context.close()

@pytest.fixture
def page(context):
    return context.new_page()
```

© bobmatnyc · MIT · bobmatnyc/claude-mpm-skills

Similar skills

All skills for Testing and QA →

webapp-testing by other authors comes first.

From the same set125

local-llm-ops A skill for running local models through Ollama on Apple Silicon: bring up the service, pull models, start chat sessions and benchmarks. CX Codex +1 448installs golang-testing-strategies A skill for writing Go tests: table-driven cases, testify assertions, gomock mocks, benchmarks and CI coverage. Useful when a project's test suite needs structure. CX Codex +1 443installs sveltekit A skill that tells the agent how SvelteKit works: file-based routing, load functions, form actions and deployment adapters. CX Codex +1 441installs wordpress-advanced-architecture A skill for advanced WordPress work: custom REST routes, WP-CLI commands, transient and object caching. CX Codex +1 441installs golang-http-frameworks Provides architecture patterns and routing guidance for Go REST APIs using net/http, Chi, Gin, Echo, or Fiber. Useful when designing endpoints, middleware, and httptest suites. CX Codex +1 440installs digitalocean-management A skill for Claude, Codex and Cursor that walks through monitoring, uptime checks and Projects on DigitalOcean. Useful when you need alerts and operational visibility for droplets and clusters. CX Codex +1 438installs flask A skill that hands your agent ready Flask patterns: app factory, blueprints, request and response handling. Useful when you're building a REST API or microservice in Python. CX Codex +1 437installs hono-rpc A skill that walks through Hono RPC: export AppType on the server, call hc<AppType> on the client, skip codegen entirely. CX Codex +1 437installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor