Skip to main content

Page Object Model

The Page Object Model (POM) wraps each page (or component) of your app in a Python class. Tests interact with the page through that class rather than calling Selenium directly — so when the UI changes, you update one place, not every test.

Without POM (Fragile)

def test_login_success(driver):
    driver.get("https://app.example.com/login")
    driver.find_element(By.ID, "email").send_keys("user@test.com")
    driver.find_element(By.ID, "password").send_keys("secret")
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
    assert driver.current_url.endswith("/dashboard")

def test_login_wrong_password(driver):
    driver.get("https://app.example.com/login")
    driver.find_element(By.ID, "email").send_keys("user@test.com")
    driver.find_element(By.ID, "password").send_keys("wrong")
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
    error = driver.find_element(By.CSS_SELECTOR, ".error-msg")
    assert "Invalid" in error.text

If the submit button's selector changes, you fix it in both tests (and every other test that logs in).

With POM (Maintainable)

# pages/login_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class LoginPage:
    URL = "https://app.example.com/login"

    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)

    def open(self):
        self.driver.get(self.URL)
        return self

    def login(self, email, password):
        self.driver.find_element(By.ID, "email").send_keys(email)
        self.driver.find_element(By.ID, "password").send_keys(password)
        self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
        return self

    def error_message(self):
        el = self.wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".error-msg")))
        return el.text
# tests/test_login.py
from pages.login_page import LoginPage

def test_login_success(driver):
    page = LoginPage(driver).open()
    page.login("user@test.com", "secret")
    assert driver.current_url.endswith("/dashboard")

def test_login_wrong_password(driver):
    page = LoginPage(driver).open()
    page.login("user@test.com", "wrong")
    assert "Invalid" in page.error_message()

Project Structure

tests/
├── conftest.py          # shared fixtures (driver setup/teardown)
├── pages/
   ├── login_page.py
   ├── dashboard_page.py
   └── checkout_page.py
└── tests/
    ├── test_login.py
    ├── test_checkout.py
    └── test_search.py

Page Object Pattern Visualiser

POM structure
Ctrl+Enter
HTML
CSS
JS
Preview