Page Object Model (POM)
Concept
POM encapsulates all interactions with a UI page or component behind a dedicated class. Tests call methods on the page object; the page object handles locators and browser actions.
Test → PageObject.do_action() → Browser / API
Test → PageObject.get_state() → Browser / API
Responsibilities
Element Locators
- Defined once per page object, never repeated across tests
- Prefer stable locators:
data-testid, ARIA roles, label text - Avoid: CSS class names, XPath by position, auto-generated IDs
class LoginPage:
USERNAME = '[data-testid="username-input"]'
PASSWORD = '[data-testid="password-input"]'
SUBMIT = '[data-testid="submit-btn"]'
ERROR = '[data-testid="error-message"]'
Actions
- One method = one user intent
- Return
selffor chaining or a new page object after navigation - No assertions inside page objects — pages describe capability, not expectations
class LoginPage:
def __init__(self, page: Page) -> None:
self._page = page
def fill_credentials(self, username: str, password: str) -> "LoginPage":
self._page.fill(self.USERNAME, username)
self._page.fill(self.PASSWORD, password)
return self
def submit(self) -> "DashboardPage":
self._page.click(self.SUBMIT)
return DashboardPage(self._page)
def get_error(self) -> str:
return self._page.inner_text(self.ERROR)
State / Query Methods
- Read-only methods that return values for assertion in tests
- Naming:
get_*,is_*,has_*
Composition Over Inheritance
Large pages decompose into components:
LoginPage
├── HeaderComponent
├── LoginFormComponent
└── FooterComponent
Each component owns its locators and actions. The page object composes them.
Risks
| Risk | Description | Mitigation |
|---|---|---|
| God page object | One class with 50+ methods | Split by component |
| Tight coupling to UI | Tests break on every redesign | Use data-testid, not visual selectors |
| Assertions in POM | Logic leaks from tests to pages | Move all assertions to test body |
| Nested navigation | Page returns wrong type after action | Use typed return annotations, verify in tests |
| Shared mutable state | Race conditions in parallel runs | Each test gets a fresh page instance |
When to Use
| Scenario | Use POM |
|---|---|
| Playwright / Selenium E2E suite | Yes |
| Repeated UI flows across tests | Yes |
| Single one-off smoke test | No — direct API call preferred |
| API-only testing | No |
POM vs Direct API
| Criterion | POM | Direct API |
|---|---|---|
| Setup cost | Medium | Low |
| Reusability | High | Low |
| Maintenance | Centralised | Scattered |
| Speed | Slow (browser) | Fast |
For regression and user journey coverage, use POM. For data setup and teardown, prefer direct API or service calls.