Implement Page Object Model (POM)
When to Use
- Building web UI test automation
- Reducing locator hardcoding in test files
- Improving test maintainability and reusability
- Centralizing page element definitions
- Enabling cross-browser compatibility
Procedure
1. Create Base Page Class
pages/base_page.py
from utils.logger import get_logger
logger = get_logger(__name__)
class PlaywrightBasePage:
"""Base page class with common Playwright methods for all pages."""
def __init__(self, page, timeout=10000):
self.page = page
self.timeout = timeout
def goto(self, url):
self.page.goto(url)
logger.info(f"Navigated to: {url}")
def click(self, selector):
self.page.click(selector)
logger.info(f"Clicked: {selector}")
def fill(self, selector, text):
self.page.fill(selector, text)
def get_text(self, selector):
return self.page.text_content(selector)
def is_visible(self, selector):
return self.page.is_visible(selector)
def is_enabled(self, selector):
return self.page.is_enabled(selector)
def wait_for_selector(self, selector):
self.page.wait_for_selector(selector)
def wait_for_load_state(self, state="networkidle"):
self.page.wait_for_load_state(state)
def select_option(self, selector, value):
self.page.select_option(selector, value)
def check_checkbox(self, selector):
self.page.check(selector)
def screenshot(self, filename):
self.page.screenshot(path=f"reports/screenshots/{filename}.png")
def get_page_title(self):
return self.page.title()
def get_current_url(self):
return self.page.url
2. Create Specific Page Classes
pages/login_page.py
from pages.base_page import PlaywrightBasePage
from utils.logger import get_logger
logger = get_logger(__name__)
class LoginPage(PlaywrightBasePage):
"""Login page object."""
USERNAME_INPUT = "#username"
PASSWORD_INPUT = "#password"
LOGIN_BUTTON = "#loginBtn"
ERROR_MESSAGE = ".error-message"
FORGOT_PWD_LINK = "text=Forgot Password?"
def enter_username(self, username):
self.fill(self.USERNAME_INPUT, username)
def enter_password(self, password):
self.fill(self.PASSWORD_INPUT, password)
def click_login(self):
self.click(self.LOGIN_BUTTON)
def login(self, username, password):
logger.info(f"Logging in as: {username}")
self.enter_username(username)
self.enter_password(password)
self.click_login()
self.wait_for_load_state("networkidle")
def get_error_message(self):
if self.is_visible(self.ERROR_MESSAGE):
return self.get_text(self.ERROR_MESSAGE)
return ""
def is_error_displayed(self):
return self.is_visible(self.ERROR_MESSAGE)
def click_forgot_password(self):
self.click(self.FORGOT_PWD_LINK)
pages/dashboard_page.py
from pages.base_page import PlaywrightBasePage
from utils.logger import get_logger
logger = get_logger(__name__)
class DashboardPage(PlaywrightBasePage):
"""Dashboard page object."""
WELCOME_MESSAGE = "h1"
USER_PROFILE = "#userProfile"
LOGOUT_BUTTON = "#logoutBtn"
NAV_MENU = "#navMenu"
def get_welcome_message(self):
return self.get_text(self.WELCOME_MESSAGE)
def is_user_logged_in(self):
return self.is_visible(self.USER_PROFILE)
def click_logout(self):
self.click(self.LOGOUT_BUTTON)
logger.info("Logout clicked")
def navigate_to(self, menu_item):
self.click(f"nav >> text={menu_item}")
logger.info(f"Navigated to: {menu_item}")
3. Using POM in Tests
tests/ui/test_login.py
import pytest
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage
class TestLogin:
@pytest.fixture(autouse=True)
def setup(self, page):
self.page = page.page
self.page.goto("https://app.example.com/login")
@pytest.mark.smoke
def test_successful_login(self):
login = LoginPage(self.page)
login.login("user@example.com", "password123")
dashboard = DashboardPage(self.page)
assert dashboard.is_user_logged_in()
@pytest.mark.regression
def test_invalid_credentials(self):
login = LoginPage(self.page)
login.login("user@example.com", "wrongpass")
assert login.is_error_displayed()
def test_forgot_password_link(self):
login = LoginPage(self.page)
login.click_forgot_password()
assert "forgot" in login.get_current_url()
Best Practices
- All selectors live in ONE class — single place to update
- Pages expose actions, not raw selectors
- Tests never use raw locators directly
- Base page holds shared Playwright methods
- One class per page