DEV Community

Karan Sharma
Karan Sharma

Posted on

Structuring Playwright Tests with the Page Object Model in Python

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Line by line explanation

  • from playwright.sync_api import Page, expect: imports the Page type hint and the expect assertion 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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)