Imported from marcocelone/demo-python-ai-agent-project (
.claude/skills/playwright-best-practices/SKILL.md). Install upstream withnpx skills add marcocelone/demo-python-ai-agent-project --skill playwright-best-practices. Copyright stays with the author.
Playwright Test Automation Best Practices for EventHub (Python)
Overview
This document defines the testing standards, patterns, and best practices for writing Playwright E2E tests in the EventHub project using Python + pytest + pytest-playwright. All test automation agents and code reviewers MUST follow these guidelines.
1. Project Test Setup
Config Reference
- Test directory:
./tests - Base URL:
https://eventhub.rahulshettyacademy.com - Timeout: 30s per test, 5s per assertion
- Browser: Chromium only (Desktop Chrome)
- Parallel execution: Disabled by default
- Screenshots: Only on failure
- Video: Retain on failure
File Naming Convention
- Test files:
tests/test_<feature_name>.py - Use descriptive snake_case names:
test_booking_flow.py,test_cross_user_booking.py - Group related tests in the same file using a
class Test<Feature>block
2. Locator Strategy (Priority Order)
Always choose locators in this priority order for reliability and readability:
Priority 1: data-testid (Most Preferred)
page.get_by_test_id("event-card")
page.get_by_test_id("book-now-btn")
Priority 2: Accessibility Roles
page.get_by_role("link", name="Browse Events")
page.get_by_role("button", name="Submit")
Priority 3: Labels and Placeholders
page.get_by_label("Full Name")
page.get_by_label("Password")
page.get_by_placeholder("you@email.com")
page.get_by_placeholder("+91 98765 43210")
Priority 4: Element IDs
page.locator("#login-btn")
page.locator("#customer-email")
page.locator("#check-refund-btn")
Priority 5: CSS Classes (Last Resort)
page.locator(".confirm-booking-btn")
page.locator(".booking-ref")
NEVER Use
- XPath selectors
- Complex CSS chains (
.parent > .child:nth-child(3)) - Index-based selectors without filtering
3. Filtering and Scoping Patterns
Filter cards by content
event_cards = page.get_by_test_id("event-card")
target_card = event_cards.filter(has_text=event_title).first
Filter by nested element
matching_card = booking_cards.filter(
has=page.locator(".booking-ref", has_text=booking_ref)
)
Scope actions to a parent
target_card.get_by_test_id("book-now-btn").click()
4. Assertion Patterns
Visibility Checks
expect(page.get_by_text("Event created!")).to_be_visible()
expect(banner).not_to_be_visible()
URL Assertions
expect(page).to_have_url(re.compile(r"/bookings$"))
expect(page).to_have_url(re.compile(r"/events/\d+"))
Content Assertions
expect(result).to_contain_text("Eligible for refund")
expect(matching_card).to_contain_text(event_title)
Numeric/Value Assertions
assert event_cards.count() == 6
assert seats_after == seats_before - 1
Custom Timeout for Slow Operations
expect(target_card).to_be_visible(timeout=5000)
expect(page.locator("#refund-spinner")).not_to_be_visible(timeout=6000)
5. Test Structure Patterns
Standard Test Structure
import re
import pytest
from playwright.sync_api import Page, expect
BASE_URL = "https://eventhub.rahulshettyacademy.com"
USER_EMAIL = os.getenv("TEST_USER_EMAIL")
USER_PASSWORD = os.getenv("TEST_USER_PASSWORD")
def login(page: Page) -> None:
page.goto(f"{BASE_URL}/login")
page.get_by_placeholder("you@email.com").fill(USER_EMAIL)
page.get_by_label("Password").fill(USER_PASSWORD)
page.locator("#login-btn").click()
expect(page.get_by_role("link", name="Browse Events").first).to_be_visible()
def test_descriptive_name_explaining_what_is_validated(page: Page) -> None:
# Step 1: Setup (login, navigate)
login(page)
# Step 2: Action (interact with UI)
# ...
# Step 3: Assert (verify outcomes)
# ...
Page Object Model (POM)
# pages/login_page.py
import os
from playwright.sync_api import Page, Locator
class LoginPage:
def __init__(self, page: Page) -> None:
self.page = page
self.email_input: Locator = page.get_by_placeholder("you@email.com")
self.password_input: Locator = page.get_by_label("Password")
self.login_btn: Locator = page.locator("#login-btn")
def goto(self) -> None:
self.page.goto("/login")
def login(self, email: str = "", password: str = "") -> None:
email = email or os.getenv("TEST_USER_EMAIL", "")
password = password or os.getenv("TEST_USER_PASSWORD", "")
self.goto()
self.email_input.fill(email)
self.password_input.fill(password)
self.login_btn.click()
POM Rules:
- One page class per page/major component; files in
pages/assnake_case.py - Store locators as instance attributes in
__init__ - Methods represent user actions, not low-level steps
- Keep assertions in test files, not in page objects
- Action methods do not return values unless the return value is needed in tests
Fixtures (conftest.py)
@pytest.fixture
def clean_session(page: Page):
login = LoginPage(page)
bookings = BookingsPage(page)
login.login()
bookings.clear_all()
yield {
"page": page,
"events_page": EventsPage(page),
"bookings_page": bookings,
}
6. API Mocking (Route Interception)
def handle_route(route):
route.fulfill(
status=200,
content_type="application/json",
body=json.dumps(mocked_response),
)
page.route("**/api/events**", handle_route)
7. Test Users
| User | Password | Purpose | |
|---|---|---|---|
| Gmail User | see .env |
see .env |
Primary tester |
| Yahoo User | see .env |
see .env |
Cross-user tests |
8. Dynamic Data Handling
import time
event_title = f"Test Event {int(time.time())}"
9. Wait Strategies
DO: Use auto-waiting with expect
expect(page.get_by_text("Event created!")).to_be_visible() # auto-waits
DO: Use locator.wait_for() in POMs
self.home_link.wait_for()
DON'T: Use arbitrary sleeps
# BAD — never do this
import time; time.sleep(2)
page.wait_for_timeout(2000)
10. Debugging Tips
print(f'Created event: "{event_title}"')
print(f'Booking confirmed. Ref: {booking_ref}')
pytest tests/test_booking_management.py -v --headed # single file, visible browser
pytest -k "tc001" # filter by keyword
pytest --html=report.html --self-contained-html # HTML report
11. Anti-Patterns to Avoid
| Anti-Pattern | Why It's Bad | Do Instead |
|---|---|---|
time.sleep(N) / page.wait_for_timeout(N) |
Flaky, wastes time | Use expect().to_be_visible() |
page.locator("div > span:nth-child(2)") |
Fragile CSS path | Use data-testid |
| Hardcoded booking/event IDs | Tests break when DB changes | Generate data dynamically |
| No assertions after action | Test passes but proves nothing | Always assert outcomes |
| Shared state between tests | Order-dependent failures | Each test self-contained |
| Assertions inside POM methods | Logic leaks into page objects | Keep assertions in test files |