As a Playwright test suite grows, locators and navigation logic often get duplicated across multiple test files. A small UI change then forces edits in several places, which increases maintenance cost. The Page Object Model (POM) pattern solves this by isolating page structure and interactions into dedicated classes, keeping test functions focused only on flow and assertions.
The problem without POM
def test_search_box_ready():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://www.google.com", wait_until="domcontentloaded")
search_box = page.locator("textarea[name='q']")
expect(search_box).to_be_visible()
browser.close()
If multiple tests reuse this same search box, the locator string gets duplicated across every test. Any markup change then means updating every occurrence.
Project structure with POM
project/
pages/
google_page.py
tests/
test_google_pom.py
Page class implementation
# pages/google_page.py
from playwright.sync_api import Page, expect
class GooglePage:
URL = "https://www.google.com"
def __init__(self, page: Page):
self.page = page
self.search_box = page.locator("textarea[name='q']")
def open(self):
self.page.goto(self.URL, wait_until="domcontentloaded")
def is_search_ready(self):
expect(self.search_box).to_be_visible()
expect(self.search_box).to_be_editable()
Line by line explanation
-
from playwright.sync_api import Page, expect: imports thePagetype hint and theexpectassertion helper. -
class GooglePage:: defines the page object representing the Google homepage. -
URL = "https://www.google.com": stores the target URL as a reusable constant. -
def __init__(self, page: Page):: constructor receives an active Playwright page. -
self.page = page: stores the page reference for reuse in methods. -
self.search_box = page.locator("textarea[name='q']"): defines the search box locator exactly once. -
def open(self):: encapsulates the navigation step. -
self.page.goto(self.URL, wait_until="domcontentloaded"): navigates and waits for DOM readiness. -
def is_search_ready(self):: encapsulates the readiness validation. -
expect(self.search_box).to_be_visible(): assertion for visibility. -
expect(self.search_box).to_be_editable(): assertion for input readiness.
Test implementation using the page object
# tests/test_google_pom.py
from playwright.sync_api import sync_playwright
from pages.google_page import GooglePage
def test_search_box_ready_with_pom():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
google_page = GooglePage(page)
google_page.open()
google_page.is_search_ready()
browser.close()
Line by line explanation
-
from playwright.sync_api import sync_playwright: imports the Playwright sync entry point. -
from pages.google_page import GooglePage: imports the page object. -
def test_search_box_ready_with_pom():: defines the pytest test function. -
with sync_playwright() as p:: starts Playwright with automatic cleanup. -
browser = p.chromium.launch(headless=True): launches Chromium headlessly. -
page = browser.new_page(): opens a new page. -
google_page = GooglePage(page): wraps the page inside the page object. -
google_page.open(): performs navigation via the encapsulated method. -
google_page.is_search_ready(): performs assertions via the encapsulated method. -
browser.close(): closes the browser session.
Running with an HTML report
py -m pytest -q tests/test_google_pom.py --html=playwright_report.html --self-contained-html
Why this pattern matters
- Locators exist in one place only.
- Tests read like flow descriptions rather than raw selector syntax.
- UI changes require editing the page class, not every test.
- New tests can reuse existing page objects directly.
Key takeaway
The Page Object Model turns scattered locators and repeated navigation code into a single reusable abstraction, keeping maintenance cost low as the suite scales.
Without POM vs. with POM
| Aspect | Without POM | With POM |
|---|---|---|
| Locator definition | Repeated inline in every test | Defined once inside the page class |
| Impact of a UI change | Requires editing every affected test | Requires editing only the page class |
| Test readability | Mixed with raw selector syntax | Reads as a sequence of user actions |
| Reuse across tests | Copy-paste of locators and navigation | Import and reuse the same page object |
| Maintenance cost at scale | Grows quickly with suite size | Stays low as the suite grows |
Top comments (0)