Playwright — Page Object Model
POM encapsulates page structure and interactions in classes. Tests stay readable; locators live in one place.
Basic Page Object
from playwright.sync_api import Page, expect
class LoginPage:
"""Encapsulates login page locators and actions."""
def __init__(self, page: Page) -> None:
self.page = page
# define locators as properties — centralized, easy to update
self.email_input = page.get_by_label("Email")
self.password_input = page.get_by_label("Password")
self.submit_button = page.get_by_role("button", name="Sign in")
self.error_message = page.get_by_role("alert")
def navigate(self) -> None:
self.page.goto("/login")
def login(self, email: str, password: str) -> None:
self.email_input.fill(email)
self.password_input.fill(password)
self.submit_button.click()
def expect_error(self, text: str) -> None:
expect(self.error_message).to_contain_text(text)
Using in Tests
from playwright.sync_api import Page, expect
def test_successful_login(page: Page):
login_page = LoginPage(page)
login_page.navigate()
login_page.login("alice@example.com", "correct-password")
expect(page).to_have_url("/dashboard")
def test_wrong_password(page: Page):
login_page = LoginPage(page)
login_page.navigate()
login_page.login("alice@example.com", "wrong")
login_page.expect_error("Invalid credentials")
Page Object as Fixture
import pytest
from playwright.sync_api import Page
@pytest.fixture
def login_page(page: Page) -> LoginPage:
"""Provides a LoginPage already navigated to /login."""
lp = LoginPage(page)
lp.navigate()
return lp
def test_login_success(login_page: LoginPage, page: Page):
login_page.login("alice@example.com", "secret")
expect(page).to_have_url("/dashboard")
Component Object
For reusable UI components (navbar, sidebar, modal, table row):
from playwright.sync_api import Page, Locator, expect
class Navbar:
"""Reusable navbar component — shared across multiple page objects."""
def __init__(self, page: Page) -> None:
self.page = page
self.root = page.get_by_role("navigation")
self.user_menu = self.root.get_by_role("button", name="Account")
self.logout_button = self.root.get_by_role("menuitem", name="Logout")
def open_user_menu(self) -> None:
self.user_menu.click()
def logout(self) -> None:
self.open_user_menu()
self.logout_button.click()
class DashboardPage:
"""Composes Navbar component into the page object."""
def __init__(self, page: Page) -> None:
self.page = page
self.navbar = Navbar(page) # reuse component
self.heading = page.get_by_role("heading", level=1)
def navigate(self) -> None:
self.page.goto("/dashboard")
def expect_loaded(self) -> None:
expect(self.heading).to_have_text("Dashboard")
Table / List Pattern
from playwright.sync_api import Page, Locator, expect
class UsersTable:
def __init__(self, page: Page) -> None:
self.page = page
self.rows = page.get_by_role("row")
def row_by_name(self, name: str) -> Locator:
"""Find a specific row by visible user name."""
return self.rows.filter(has_text=name)
def delete_user(self, name: str) -> None:
row = self.row_by_name(name)
row.get_by_role("button", name="Delete").click()
def expect_user_visible(self, name: str) -> None:
expect(self.row_by_name(name)).to_be_visible()
def expect_user_count(self, count: int) -> None:
# +1 for header row in most tables
expect(self.rows).to_have_count(count + 1)
Project Structure
tests/
├── conftest.py # shared fixtures (auth, base_url)
├── pages/
│ ├── __init__.py
│ ├── login_page.py
│ ├── dashboard_page.py
│ └── users_page.py
├── components/
│ ├── __init__.py
│ ├── navbar.py
│ └── modal.py
├── test_login.py
├── test_dashboard.py
└── test_users.py
Best Practices
| Practice | Why |
|---|---|
Define locators in __init__ |
Centralized, easy to find and update |
Use get_by_role / get_by_label |
Resilient to DOM changes |
Return self from actions for chaining |
Optional fluent API |
Keep assertions in test or expect_* methods |
Clear test intent |
| Compose component objects into pages | DRY, reusable across pages |
| One page object per page/view | Maps 1:1 to the application |
| Fixture for page object creation | Keeps test code clean |