Skip to main content
SeleniumDecoded

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.java

Base 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 False
class 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

  1. One page class per page/section: Don’t create god objects
  2. Return page objects from navigation methods: Enables fluent chains
  3. Keep locators private: External code shouldn’t know implementation details
  4. No assertions in page objects: Page objects provide data, tests make assertions
  5. Use meaningful method names: clickLoginButton() not click()
  6. Handle waits internally: Callers shouldn’t worry about synchronization

Next Steps