Page Object Model
Learn the Page Object Model (POM) design pattern for writing maintainable and scalable Selenium tests.
Selenium 3 & 4 Stable
The Page Object Model (POM) is a design pattern that creates an object-oriented representation of web pages. It separates test logic from page structure, making tests more maintainable and readable.
Why Use Page Objects?
Without POM, tests become:
- Hard to maintain: Locators scattered across test files
- Brittle: One UI change breaks multiple tests
- Repetitive: Same interactions written in every test
- Hard to read: Tests filled with low-level Selenium calls
With POM:
- Locators centralized: One place to update when UI changes
- Reusable methods: Write interaction logic once
- Readable tests: Tests describe what happens, not how
- Easier debugging: Clear separation of concerns
Basic Page Object
Login Page Object
Selenium 3 & 4 Stable
public class LoginPage { private WebDriver driver; private WebDriverWait wait;
// Locators private By usernameField = By.id("username"); private By passwordField = By.id("password"); private By loginButton = By.id("login-btn"); private By errorMessage = By.className("error-message");
public LoginPage(WebDriver driver) { this.driver = driver; this.wait = new WebDriverWait(driver, Duration.ofSeconds(10)); }
public void enterUsername(String username) { WebElement field = wait.until( ExpectedConditions.visibilityOfElementLocated(usernameField) ); field.clear(); field.sendKeys(username); }
public void enterPassword(String password) { WebElement field = driver.findElement(passwordField); field.clear(); field.sendKeys(password); }
public void clickLogin() { wait.until(ExpectedConditions.elementToBeClickable(loginButton)).click(); }
public String getErrorMessage() { return wait.until( ExpectedConditions.visibilityOfElementLocated(errorMessage) ).getText(); }
// Fluent method for complete login public DashboardPage loginAs(String username, String password) { enterUsername(username); enterPassword(password); clickLogin(); return new DashboardPage(driver); }}class LoginPage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10)
# Locators self._username_field = (By.ID, "username") self._password_field = (By.ID, "password") self._login_button = (By.ID, "login-btn") self._error_message = (By.CLASS_NAME, "error-message")
def enter_username(self, username): field = self.wait.until( EC.visibility_of_element_located(self._username_field) ) field.clear() field.send_keys(username)
def enter_password(self, password): field = self.driver.find_element(*self._password_field) field.clear() field.send_keys(password)
def click_login(self): self.wait.until( EC.element_to_be_clickable(self._login_button) ).click()
def get_error_message(self): return self.wait.until( EC.visibility_of_element_located(self._error_message) ).text
# Fluent method for complete login def login_as(self, username, password): self.enter_username(username) self.enter_password(password) self.click_login() return DashboardPage(self.driver)class LoginPage { constructor(driver) { this.driver = driver;
// Locators this.usernameField = By.id('username'); this.passwordField = By.id('password'); this.loginButton = By.id('login-btn'); this.errorMessage = By.className('error-message'); }
async enterUsername(username) { const field = await this.driver.wait( until.elementLocated(this.usernameField), 10000 ); await field.clear(); await field.sendKeys(username); }
async enterPassword(password) { const field = await this.driver.findElement(this.passwordField); await field.clear(); await field.sendKeys(password); }
async clickLogin() { const button = await this.driver.wait( until.elementLocated(this.loginButton), 10000 ); await this.driver.wait(until.elementIsEnabled(button), 10000); await button.click(); }
async getErrorMessage() { const element = await this.driver.wait( until.elementLocated(this.errorMessage), 10000 ); return await element.getText(); }
// Fluent method for complete login async loginAs(username, password) { await this.enterUsername(username); await this.enterPassword(password); await this.clickLogin(); return new DashboardPage(this.driver); }}public class LoginPage{ private IWebDriver driver; private WebDriverWait wait;
// Locators private By usernameField = By.Id("username"); private By passwordField = By.Id("password"); private By loginButton = By.Id("login-btn"); private By errorMessage = By.ClassName("error-message");
public LoginPage(IWebDriver driver) { this.driver = driver; this.wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10)); }
public void EnterUsername(string username) { var field = wait.Until( ExpectedConditions.ElementIsVisible(usernameField) ); field.Clear(); field.SendKeys(username); }
public void EnterPassword(string password) { var field = driver.FindElement(passwordField); field.Clear(); field.SendKeys(password); }
public void ClickLogin() { wait.Until( ExpectedConditions.ElementToBeClickable(loginButton) ).Click(); }
public string GetErrorMessage() { return wait.Until( ExpectedConditions.ElementIsVisible(errorMessage) ).Text; }
// Fluent method for complete login public DashboardPage LoginAs(string username, string password) { EnterUsername(username); EnterPassword(password); ClickLogin(); return new DashboardPage(driver); }}Using Page Objects in Tests
Test Using Page Objects
Selenium 3 & 4 Stable
public class LoginTest { private WebDriver driver; private LoginPage loginPage;
@BeforeEach void setup() { driver = new ChromeDriver(); driver.get("https://example.com/login"); loginPage = new LoginPage(driver); }
@Test void successfulLogin() { DashboardPage dashboard = loginPage.loginAs("user", "pass123");
assertTrue(dashboard.isWelcomeMessageDisplayed()); assertEquals("Welcome, user!", dashboard.getWelcomeMessage()); }
@Test void invalidCredentials() { loginPage.enterUsername("invalid"); loginPage.enterPassword("wrong"); loginPage.clickLogin();
assertEquals("Invalid credentials", loginPage.getErrorMessage()); }
@AfterEach void teardown() { driver.quit(); }}class TestLogin: def setup_method(self): self.driver = webdriver.Chrome() self.driver.get("https://example.com/login") self.login_page = LoginPage(self.driver)
def test_successful_login(self): dashboard = self.login_page.login_as("user", "pass123")
assert dashboard.is_welcome_message_displayed() assert dashboard.get_welcome_message() == "Welcome, user!"
def test_invalid_credentials(self): self.login_page.enter_username("invalid") self.login_page.enter_password("wrong") self.login_page.click_login()
assert self.login_page.get_error_message() == "Invalid credentials"
def teardown_method(self): self.driver.quit()describe('Login', function() { let driver; let loginPage;
beforeEach(async function() { driver = await new Builder().forBrowser('chrome').build(); await driver.get('https://example.com/login'); loginPage = new LoginPage(driver); });
it('should login successfully', async function() { const dashboard = await loginPage.loginAs('user', 'pass123');
expect(await dashboard.isWelcomeMessageDisplayed()).toBe(true); expect(await dashboard.getWelcomeMessage()).toBe('Welcome, user!'); });
it('should show error for invalid credentials', async function() { await loginPage.enterUsername('invalid'); await loginPage.enterPassword('wrong'); await loginPage.clickLogin();
expect(await loginPage.getErrorMessage()).toBe('Invalid credentials'); });
afterEach(async function() { await driver.quit(); });});[TestClass]public class LoginTest{ private IWebDriver driver; private LoginPage loginPage;
[TestInitialize] public void Setup() { driver = new ChromeDriver(); driver.Navigate().GoToUrl("https://example.com/login"); loginPage = new LoginPage(driver); }
[TestMethod] public void SuccessfulLogin() { DashboardPage dashboard = loginPage.LoginAs("user", "pass123");
Assert.IsTrue(dashboard.IsWelcomeMessageDisplayed()); Assert.AreEqual("Welcome, user!", dashboard.GetWelcomeMessage()); }
[TestMethod] public void InvalidCredentials() { loginPage.EnterUsername("invalid"); loginPage.EnterPassword("wrong"); loginPage.ClickLogin();
Assert.AreEqual("Invalid credentials", loginPage.GetErrorMessage()); }
[TestCleanup] public void Teardown() { driver.Quit(); }}Page Object Structure
Recommended project structure:
src/test/├── pages/│ ├── BasePage.java│ ├── LoginPage.java│ ├── DashboardPage.java│ └── components/│ ├── Header.java│ └── Footer.java├── tests/│ ├── LoginTest.java│ └── DashboardTest.java└── utils/ ├── DriverFactory.java └── TestConfig.javaBase Page Class
Base Page Pattern
Selenium 3 & 4 Stable
public abstract class BasePage { protected WebDriver driver; protected WebDriverWait wait;
public BasePage(WebDriver driver) { this.driver = driver; this.wait = new WebDriverWait(driver, Duration.ofSeconds(10)); }
protected WebElement waitForVisible(By locator) { return wait.until(ExpectedConditions.visibilityOfElementLocated(locator)); }
protected WebElement waitForClickable(By locator) { return wait.until(ExpectedConditions.elementToBeClickable(locator)); }
protected void click(By locator) { waitForClickable(locator).click(); }
protected void type(By locator, String text) { WebElement element = waitForVisible(locator); element.clear(); element.sendKeys(text); }
protected String getText(By locator) { return waitForVisible(locator).getText(); }
protected boolean isDisplayed(By locator) { try { return driver.findElement(locator).isDisplayed(); } catch (NoSuchElementException e) { return false; } }}class BasePage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10)
def wait_for_visible(self, locator): return self.wait.until(EC.visibility_of_element_located(locator))
def wait_for_clickable(self, locator): return self.wait.until(EC.element_to_be_clickable(locator))
def click(self, locator): self.wait_for_clickable(locator).click()
def type(self, locator, text): element = self.wait_for_visible(locator) element.clear() element.send_keys(text)
def get_text(self, locator): return self.wait_for_visible(locator).text
def is_displayed(self, locator): try: return self.driver.find_element(*locator).is_displayed() except NoSuchElementException: return Falseclass BasePage { constructor(driver) { this.driver = driver; this.timeout = 10000; }
async waitForVisible(locator) { const element = await this.driver.wait( until.elementLocated(locator), this.timeout ); await this.driver.wait(until.elementIsVisible(element), this.timeout); return element; }
async waitForClickable(locator) { const element = await this.driver.wait( until.elementLocated(locator), this.timeout ); await this.driver.wait(until.elementIsEnabled(element), this.timeout); return element; }
async click(locator) { const element = await this.waitForClickable(locator); await element.click(); }
async type(locator, text) { const element = await this.waitForVisible(locator); await element.clear(); await element.sendKeys(text); }
async getText(locator) { const element = await this.waitForVisible(locator); return await element.getText(); }
async isDisplayed(locator) { try { const element = await this.driver.findElement(locator); return await element.isDisplayed(); } catch (e) { return false; } }}public abstract class BasePage{ protected IWebDriver driver; protected WebDriverWait wait;
public BasePage(IWebDriver driver) { this.driver = driver; this.wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10)); }
protected IWebElement WaitForVisible(By locator) { return wait.Until(ExpectedConditions.ElementIsVisible(locator)); }
protected IWebElement WaitForClickable(By locator) { return wait.Until(ExpectedConditions.ElementToBeClickable(locator)); }
protected void Click(By locator) { WaitForClickable(locator).Click(); }
protected void Type(By locator, string text) { var element = WaitForVisible(locator); element.Clear(); element.SendKeys(text); }
protected string GetText(By locator) { return WaitForVisible(locator).Text; }
protected bool IsDisplayed(By locator) { try { return driver.FindElement(locator).Displayed; } catch (NoSuchElementException) { return false; } }}Best Practices
- One page class per page/section: Don’t create god objects
- Return page objects from navigation methods: Enables fluent chains
- Keep locators private: External code shouldn’t know implementation details
- No assertions in page objects: Page objects provide data, tests make assertions
- Use meaningful method names:
clickLoginButton()notclick() - Handle waits internally: Callers shouldn’t worry about synchronization
Next Steps
- Page Factory - Annotation-based element initialization
- Data-Driven Testing - External test data management
- pytest Integration - Python test framework setup
- TestNG and JUnit - Java test framework setup