| name | playwright-tests |
| description | Write Playwright E2E tests for user flows and browser interactions. Use when testing HTMX, user journeys, or when the user mentions E2E, Playwright, or browser testing. |
| allowed-tools | Read, Grep, Glob |
Playwright E2E Testing
This Skill helps write end-to-end tests for browser-required behavior.
When to Use This Skill
Use this Skill when:
- Testing user flows - Login, enrollment, multi-step processes
- Testing HTMX interactions - Dynamic updates, partial swaps
- Testing JavaScript behavior - Alpine.js, modals, interactive elements
- Integration testing across pages - Navigation, full user journeys
- User mentions "E2E", "Playwright", "browser testing", "end-to-end"
- Visual verification needed - Layout, responsive behavior
When to Use Playwright
Use Playwright for:
- User flows - Login, enrollment, course navigation
- HTMX interactions - Dynamic updates, partial swaps
- JavaScript behavior - Alpine.js interactions, modals
- Integration across pages - Multi-step processes
- Visual verification - Layout, responsive design
When NOT to Use Playwright
Use pytest instead for:
- Model logic and methods
- View responses and context
- Template rendering
- Database operations
- Utility functions
- API endpoints
Rule: If it can be tested with pytest, test it with pytest. Playwright is for browser-required behavior only.
Setup
uv add --dev playwright
playwright install
pytest tests/e2e/
pytest tests/e2e/test_enrollment.py
Test Structure
import pytest
from django.urls import reverse
@pytest.mark.playwright
def test_user_enrollment_flow(page, live_server):
"""Test user can enroll in a course."""
url = reverse('courses:list')
page.goto(f"{live_server.url}{url}")
page.click('text="Enroll"')
page.wait_for_selector('.success-message')
assert page.is_visible('text="Enrolled"')
Key Rules
- Only use Playwright for browser-required behavior — if it can be tested with pytest, use pytest instead
- Mark all tests with
@pytest.mark.playwright
- Use
page and live_server fixtures
- Use
reverse() for URLs, never hardcode
- Prefer semantic selectors (
text="Submit") over CSS selectors
- Wait for elements with
wait_for_selector() for dynamic/HTMX content
- Test location:
tests/e2e/
Best Practices
- Mark with @pytest.mark.playwright - Required for all Playwright tests
- Test real user behavior - Click, type, navigate like a user
- Wait for elements - Use
wait_for_selector() for dynamic content
- Use semantic selectors - Text content over CSS classes:
'text="Enroll"' not '.btn-enroll'
- Test happy paths first - Core user journeys
- Keep tests independent - Each test should setup/teardown its own data
- Use live_server fixture - Django test server integration
- Use reverse() for URLs - Never hardcode URLs:
reverse('app:view') not '/app/view/'
- Don't test what pytest can - Avoid testing backend logic
Selectors
page.click('text="Submit"')
page.click('role=button[name="Submit"]')
page.click('.form > .btn-submit')
HTMX Testing
page.click('button[hx-get="/more"]')
page.wait_for_selector('#content .new-item')
assert page.locator('.item').count() == 5
Test Organization
tests/
└── e2e/
├── conftest.py # Playwright fixtures
├── test_enrollment.py # Enrollment flows
└── test_course_nav.py # Course navigation
Key Differences from Pytest
- Scope: Browser interactions vs. backend logic
- Speed: Slower (use sparingly)
- Fixtures:
page, live_server vs. client, user
- Assertions: Visible elements vs. data/responses
Use Playwright to complement pytest, not replace it.