Claude Code Plugins

Community-maintained marketplace

Feedback

playwright-e2e-testing

@ils15/copilot-global-config
0
0

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

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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

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

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)

# 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

# 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

# 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

# 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

# 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

# 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

# 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