The Playwright Page Object Model is the standard way to keep your large test suites from devolving into an unmanageable mess of duplicated code and brittle selectors.

Let’s see it in action. Imagine we’re testing a simple login form.

# login_page.py
from playwright.sync_api import Page

class LoginPage:
    def __init__(self, page: Page):
        self.page = page
        self.username_input = page.locator("#username")
        self.password_input = page.locator("#password")
        self.login_button = page.locator("button[type='submit']")
        self.error_message = page.locator(".error-message")

    def navigate(self):
        self.page.goto("/login")

    def enter_username(self, username: str):
        self.username_input.fill(username)

    def enter_password(self, password: str):
        self.password_input.fill(password)

    def click_login(self):
        self.login_button.click()

    def login(self, username: str, password: str):
        self.enter_username(username)
        self.enter_password(password)
        self.click_login()

    def get_error_message(self) -> str:
        return self.error_message.text_content()

# test_login.py
from playwright.sync_api import Page
from .login_page import LoginPage

def test_valid_login(page: Page):
    login_page = LoginPage(page)
    login_page.navigate()
    login_page.login("testuser", "password123")
    # Assertions for successful login would go here
    assert page.url == "/dashboard"

def test_invalid_login(page: Page):
    login_page = LoginPage(page)
    login_page.navigate()
    login_page.login("wronguser", "wrongpass")
    assert login_page.get_error_message() == "Invalid credentials"

This structure separates what to test (your test_login.py file) from how to interact with the UI elements (your login_page.py file). The LoginPage class encapsulates all the logic for interacting with the login page.

The core problem the Page Object Model (POM) solves is the "selector sprawl." Without it, you’d find yourself repeating selectors like page.locator("#username") across dozens of test files. If the ID of the username field changes from #username to #user-id, you’d have to find and update every single instance. With POM, you change it in one place: the LoginPage class.

Here’s how it breaks down internally:

  1. Locators: Each Page Object defines its UI elements using Playwright’s page.locator() method. These locators are the stable references to your DOM elements, independent of specific test steps.
  2. Actions: The Page Object then exposes methods that perform actions on these locators. Instead of a test file calling page.locator("#username").fill("user"), the test file calls login_page.enter_username("user"). This abstracts away the implementation detail of how the username is entered.
  3. State: Page Objects can also expose methods to retrieve information about the page’s state, like get_error_message(). This keeps tests cleaner by not requiring them to directly interact with potentially complex DOM queries.

The mental model you build is one of distinct "pages" or "components" of your application. Each Page Object represents a single, coherent unit of your UI. When you need to interact with the login screen, you grab an instance of LoginPage. When you need to interact with the dashboard, you’d create a DashboardPage object. This modularity is key to scaling.

A common misconception is that Page Objects are just fancy wrappers for selectors. They are much more. They are about encapsulating behavior and state related to a specific part of the UI, not just the elements themselves. This means that if entering a username involves a complex sequence of events (e.g., waiting for an autocomplete to appear and then selecting an item), that entire sequence is encapsulated within the enter_username method. The test just calls enter_username, and the Page Object handles the underlying complexity.

This approach allows you to refactor your UI elements without breaking your tests, as long as the public methods of the Page Object remain the same. Your tests become more readable, focusing on the user’s journey and expected outcomes rather than the minutiae of DOM manipulation.

The next step in organizing large test suites is often understanding how to manage dependencies between Page Objects and how to handle shared components that appear across multiple pages.

Want structured learning?

Take the full Playwright course →