Agent skill

playwright-e2e-testing

Write end-to-end tests with Playwright for web applications. Includes fixtures, page objects, test templates, visual regression testing, and accessibility audits.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/playwright-e2e-testing

SKILL.md

Playwright E2E Testing Skill

When to Use

Use this skill when:

  • Testing user workflows (sign up, login, checkout)
  • Verifying form submission and validation
  • Testing responsive design across devices
  • Running visual regression tests
  • Checking accessibility (WCAG AA)
  • Testing dynamic content and API interactions
  • Creating smoke tests for CI/CD

Setup

Install Playwright

bash
pip install pytest-playwright
playwright install  # Download browsers (chromium, firefox, webkit)

Project Structure

tests/
  ├── conftest.py              # Pytest fixtures
  ├── e2e/
  │   ├── test_homepage.py
  │   ├── test_product_search.py
  │   └── test_checkout.py
  └── pages/
      ├── base_page.py
      ├── homepage.py
      ├── product_page.py
      └── checkout_page.py

Code Patterns

1. Page Object Model (POM)

python
# tests/pages/base_page.py
from playwright.async_api import Page, expect

class BasePage:
    """Base page class with common methods"""
    
    def __init__(self, page: Page):
        self.page = page
    
    async def goto(self, url: str):
        """Navigate to URL"""
        await self.page.goto(url)
    
    async def wait_for_element(self, selector: str, timeout: int = 5000):
        """Wait for element to appear"""
        await self.page.locator(selector).wait_for(timeout=timeout)
    
    async def click(self, selector: str):
        """Click element"""
        await self.page.locator(selector).click()
    
    async def fill(self, selector: str, text: str):
        """Fill input field"""
        await self.page.locator(selector).fill(text)
    
    async def get_text(self, selector: str) -> str:
        """Get element text"""
        return await self.page.locator(selector).text_content()
    
    async def expect_visible(self, selector: str):
        """Assert element is visible"""
        await expect(self.page.locator(selector)).to_be_visible()
    
    async def expect_text(self, selector: str, text: str):
        """Assert element contains text"""
        await expect(self.page.locator(selector)).to_contain_text(text)
    
    async def screenshot(self, name: str):
        """Take screenshot for visual regression"""
        await self.page.screenshot(path=f"tests/screenshots/{name}.png")

2. Page Object for Product Search

python
# tests/pages/product_page.py
from playwright.async_api import Page
from tests.pages.base_page import BasePage

class ProductPage(BasePage):
    """Product search and listing page"""
    
    # Selectors
    SEARCH_INPUT = 'input[placeholder="Buscar ofertas"]'
    SEARCH_BUTTON = 'button[type="submit"]'
    PRODUCT_CARD = '.product-card'
    PRODUCT_TITLE = '.product-title'
    PRODUCT_PRICE = '.product-price'
    PRODUCT_RATING = '.rating'
    SORT_DROPDOWN = 'select[name="sort"]'
    CATEGORY_FILTER = 'input[name="category"]'
    
    async def search(self, query: str):
        """Search for products"""
        await self.fill(self.SEARCH_INPUT, query)
        await self.click(self.SEARCH_BUTTON)
        await self.page.wait_for_load_state("networkidle")
    
    async def get_product_count(self) -> int:
        """Count visible products"""
        return await self.page.locator(self.PRODUCT_CARD).count()
    
    async def get_first_product_title(self) -> str:
        """Get first product title"""
        return await self.get_text(f"{self.PRODUCT_CARD}:first-child {self.PRODUCT_TITLE}")
    
    async def click_product(self, index: int = 0):
        """Click product by index"""
        products = self.page.locator(self.PRODUCT_CARD)
        await products.nth(index).click()
    
    async def filter_by_category(self, category: str):
        """Filter products by category"""
        await self.click(f'{self.CATEGORY_FILTER}[value="{category}"]')
        await self.page.wait_for_load_state("networkidle")
    
    async def sort_by(self, sort_type: str):
        """Sort products"""
        # sort_type: 'price-low-high', 'rating', 'newest'
        await self.page.select_option(self.SORT_DROPDOWN, sort_type)
        await self.page.wait_for_load_state("networkidle")
    
    async def get_price_range(self) -> tuple:
        """Get min/max price from current results"""
        prices = []
        price_elements = await self.page.locator(self.PRODUCT_PRICE).all()
        
        for element in price_elements:
            text = await element.text_content()
            # Parse "R$ 99,99" → 99.99
            price = float(text.replace("R$", "").replace(",", ".").strip())
            prices.append(price)
        
        return (min(prices), max(prices)) if prices else (0, 0)

3. Test Fixtures

python
# tests/conftest.py
import pytest
from playwright.async_api import async_playwright, Browser, BrowserContext, Page
from tests.pages.product_page import ProductPage

BASE_URL = "http://localhost:3000"

@pytest.fixture(scope="session")
async def browser() -> Browser:
    """Create browser session"""
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        yield browser
        await browser.close()

@pytest.fixture
async def context(browser: Browser) -> BrowserContext:
    """Create browser context"""
    context = await browser.new_context()
    yield context
    await context.close()

@pytest.fixture
async def page(context: BrowserContext) -> Page:
    """Create page object"""
    return await context.new_page()

@pytest.fixture
async def product_page(page: Page) -> ProductPage:
    """Instantiate ProductPage"""
    product_page = ProductPage(page)
    await product_page.goto(BASE_URL)
    return product_page

# Async test marker
def pytest_collection_modifyitems(items):
    for item in items:
        item.add_marker(pytest.mark.asyncio)

4. E2E Test Examples

python
# tests/e2e/test_product_search.py
import pytest
from tests.pages.product_page import ProductPage

class TestProductSearch:
    """Test product search functionality"""
    
    @pytest.mark.asyncio
    async def test_search_returns_results(self, product_page: ProductPage):
        """Should return products for valid search"""
        await product_page.search("smartphone")
        
        count = await product_page.get_product_count()
        assert count > 0, "Should return at least one product"
        
        title = await product_page.get_first_product_title()
        assert "smartphone" in title.lower(), "Product should match search query"
    
    @pytest.mark.asyncio
    async def test_search_no_results(self, product_page: ProductPage):
        """Should handle empty results"""
        await product_page.search("xyzabc123nonexistent")
        
        count = await product_page.get_product_count()
        assert count == 0, "Should return no products"
        
        # Verify "no results" message
        await product_page.expect_text('.no-results', "Nenhum produto encontrado")
    
    @pytest.mark.asyncio
    async def test_filter_by_category(self, product_page: ProductPage):
        """Should filter products by category"""
        await product_page.filter_by_category("electronics")
        
        count = await product_page.get_product_count()
        assert count > 0, "Should return electronics products"
    
    @pytest.mark.asyncio
    async def test_sort_by_price(self, product_page: ProductPage):
        """Should sort products by price (low to high)"""
        await product_page.search("smartphone")
        await product_page.sort_by("price-low-high")
        
        min_price, max_price = await product_page.get_price_range()
        assert min_price <= max_price, "Prices should be in ascending order"
    
    @pytest.mark.asyncio
    async def test_pagination(self, product_page: ProductPage):
        """Should paginate results"""
        await product_page.search("teclado")
        
        # Get products on page 1
        count_page1 = await product_page.get_product_count()
        
        # Go to page 2
        await product_page.click('a[aria-label="Next page"]')
        await product_page.page.wait_for_load_state("networkidle")
        
        count_page2 = await product_page.get_product_count()
        assert count_page1 > 0 and count_page2 > 0, "Both pages should have products"

5. Visual Regression Testing

python
# tests/e2e/test_visual_regression.py
import pytest

class TestVisualRegression:
    """Test visual consistency across changes"""
    
    @pytest.mark.asyncio
    async def test_homepage_visual(self, product_page: ProductPage):
        """Compare homepage visual appearance"""
        await product_page.goto("http://localhost:3000")
        
        # Compare with baseline screenshot
        await product_page.page.expect_screenshot(
            name="homepage.png",
            mask_locator='[aria-label="Last updated"]'  # Ignore dynamic elements
        )
    
    @pytest.mark.asyncio
    async def test_product_card_visual(self, product_page: ProductPage):
        """Compare product card design"""
        await product_page.search("monitor")
        
        product_element = product_page.page.locator('.product-card').first
        
        await expect(product_element).to_have_screenshot("product-card.png")

6. Accessibility Testing

python
# tests/e2e/test_accessibility.py
import pytest
from playwright.async_api import expect

class TestAccessibility:
    """Test WCAG AA compliance"""
    
    @pytest.mark.asyncio
    async def test_form_labels(self, page):
        """All inputs should have associated labels"""
        await page.goto("http://localhost:3000")
        
        inputs = await page.locator('input').all()
        
        for input_elem in inputs:
            # Check for associated label
            input_id = await input_elem.get_attribute("id")
            
            if input_id:
                label = page.locator(f'label[for="{input_id}"]')
                await expect(label).to_be_visible()
    
    @pytest.mark.asyncio
    async def test_button_contrast(self, page):
        """Buttons should have sufficient color contrast"""
        await page.goto("http://localhost:3000")
        
        buttons = await page.locator('button').all()
        
        for button in buttons:
            # This would require a contrast checking library
            # Example: check computed styles
            color = await button.evaluate("el => window.getComputedStyle(el).color")
            bg_color = await button.evaluate("el => window.getComputedStyle(el).backgroundColor")
            
            # Verify contrast ratio >= 4.5:1
            # Use contrast checking library for precise calculation
    
    @pytest.mark.asyncio
    async def test_keyboard_navigation(self, page):
        """Page should be navigable with keyboard"""
        await page.goto("http://localhost:3000")
        
        # Tab through interactive elements
        await page.keyboard.press("Tab")
        
        # Check focus is visible
        focused = await page.evaluate("document.activeElement")
        assert focused is not None, "Focus should be visible after Tab"
    
    @pytest.mark.asyncio
    async def test_mobile_responsive(self, browser):
        """Test on mobile viewport (375x667)"""
        context = await browser.new_context(
            viewport={"width": 375, "height": 667}
        )
        page = await context.new_page()
        
        await page.goto("http://localhost:3000")
        
        # Verify content is readable
        await expect(page.locator('h1')).to_be_visible()
        await expect(page.locator('nav')).to_be_visible()
        
        await context.close()

7. Running Tests

bash
# Run all tests
pytest tests/e2e/

# Run specific test file
pytest tests/e2e/test_product_search.py

# Run with verbose output
pytest -v tests/e2e/

# Run in headed mode (see browser)
pytest tests/e2e/ --headed

# Run specific test
pytest tests/e2e/test_product_search.py::TestProductSearch::test_search_returns_results

# Generate HTML report
pytest tests/e2e/ --html=report.html

# Run with screenshots on failure
pytest tests/e2e/ --screenshot=only-on-failure

Best Practices

✅ Use Page Object Model for maintainability
Wait for elements properly (not sleep)
Test user flows, not implementation details
Mock external APIs when possible
Use fixtures for setup/teardown
Name tests clearly (test_search_returns_results)
Keep tests independent (no test order dependency)
Screenshot baselines for visual regression
Check accessibility in every test

Related Files

References

Didn't find tool you were looking for?

Be as detailed as possible for better results