The Page Object Model (POM) is a design pattern that creates an abstraction of the UI page with which the tests interact. Each web page in the application is represented by a corresponding page class. The page class contains:
Benefit | Description | Example |
---|---|---|
Reduced Duplication | Element locators are defined once | Instead of repeating $("#login-button") in multiple tests, define it once in the LoginPage class |
Improved Maintenance | When the UI changes, only the page object needs to be updated | If the login button ID changes, update only the LoginPage class, not all tests |
Better Readability | Tests express intent rather than implementation details | loginPage.login(username, password) vs. complex element interactions |
Enhanced Reusability | Page objects can be used across multiple tests | The same LoginPage can be used in authentication tests, security tests, etc. |
Separation of Concerns | Test logic is separated from page interactions | Tests focus on business scenarios while page objects handle UI interactions |
Traditional Approach:
@Test
public void testLogin() {
browser.url("https://example.com/login");
browser.findElement("#username").setValue("testuser");
browser.findElement("#password").setValue("password");
browser.findElement("#login-button").click();
boolean isDashboardVisible = browser.findElement(".dashboard").isDisplayed();
assertTrue(isDashboardVisible);
}
@Test
public void testInvalidLogin() {
browser.url("https://example.com/login");
browser.findElement("#username").setValue("testuser");
browser.findElement("#password").setValue("wrongpassword");
browser.findElement("#login-button").click();
boolean isErrorVisible = browser.findElement(".error-message").isDisplayed();
assertTrue(isErrorVisible);
}
Page Object Model Approach:
@Test
public void testLogin() {
LoginPage loginPage = new LoginPage(browser);
loginPage.open();
DashboardPage dashboardPage = loginPage.login("testuser", "password");
assertTrue(dashboardPage.isDisplayed());
}
@Test
public void testInvalidLogin() {
LoginPage loginPage = new LoginPage(browser);
loginPage.open();
loginPage.loginExpectingError("testuser", "wrongpassword");
assertTrue(loginPage.isErrorMessageDisplayed());
}
public class BasePage {
protected WebDriverIO browser;
public BasePage(WebDriverIO browser) {
this.browser = browser;
}
// Common methods for all pages
public String getPageTitle() {
return browser.getTitle();
}
public void waitForPageLoad() {
browser.waitUntil(() ->
browser.executeScript("return document.readyState").equals("complete"),
10000,
"Page did not load completely after 10 seconds"
);
}
}
public class LoginPage extends BasePage {
// Element locators
private static final String USERNAME_INPUT = "#username";
private static final String PASSWORD_INPUT = "#password";
private static final String LOGIN_BUTTON = "#login-button";
private static final String ERROR_MESSAGE = ".error-message";
public LoginPage(WebDriverIO browser) {
super(browser);
}
public void open() {
browser.url("https://example.com/login");
waitForPageLoad();
}
public DashboardPage login(String username, String password) {
enterUsername(username);
enterPassword(password);
clickLoginButton();
// Return the next page object
return new DashboardPage(browser);
}
public void loginExpectingError(String username, String password) {
enterUsername(username);
enterPassword(password);
clickLoginButton();
// Stay on the same page, expecting an error
}
public void enterUsername(String username) {
browser.findElement(USERNAME_INPUT).setValue(username);
}
public void enterPassword(String password) {
browser.findElement(PASSWORD_INPUT).setValue(password);
}
public void clickLoginButton() {
browser.findElement(LOGIN_BUTTON).click();
}
public boolean isErrorMessageDisplayed() {
return browser.findElement(ERROR_MESSAGE).isDisplayed();
}
public String getErrorMessage() {
return browser.findElement(ERROR_MESSAGE).getText();
}
}
public class DashboardPage extends BasePage {
// Element locators
private static final String DASHBOARD_CONTAINER = ".dashboard";
private static final String WELCOME_MESSAGE = ".welcome-message";
private static final String LOGOUT_BUTTON = "#logout";
public DashboardPage(WebDriverIO browser) {
super(browser);
waitForPageLoad();
}
public boolean isDisplayed() {
return browser.findElement(DASHBOARD_CONTAINER).isDisplayed();
}
public String getWelcomeMessage() {
return browser.findElement(WELCOME_MESSAGE).getText();
}
public LoginPage logout() {
browser.findElement(LOGOUT_BUTTON).click();
return new LoginPage(browser);
}
}
public class BaseTest {
protected WebDriverIO browser;
@BeforeMethod
public void setup() {
Options options = new Options();
options.setBrowser("chrome");
browser = new WebDriverIO(options);
}
@AfterMethod
public void teardown() {
if (browser != null) {
browser.deleteSession();
}
}
}
public class LoginTest extends BaseTest {
private LoginPage loginPage;
@BeforeMethod
public void setupLoginPage() {
loginPage = new LoginPage(browser);
loginPage.open();
}
@Test
public void testSuccessfulLogin() {
DashboardPage dashboardPage = loginPage.login("testuser", "password");
assertTrue(dashboardPage.isDisplayed());
assertEquals("Welcome, Test User!", dashboardPage.getWelcomeMessage());
}
@Test
public void testInvalidPassword() {
loginPage.loginExpectingError("testuser", "wrongpassword");
assertTrue(loginPage.isErrorMessageDisplayed());
assertEquals("Invalid username or password", loginPage.getErrorMessage());
}
@Test
public void testEmptyUsername() {
loginPage.loginExpectingError("", "password");
assertTrue(loginPage.isErrorMessageDisplayed());
assertEquals("Username is required", loginPage.getErrorMessage());
}
}
The Page Factory pattern uses annotations to initialize page elements, reducing boilerplate code.
public class LoginPage extends BasePage {
@FindBy(css = "#username")
private WebElement usernameInput;
@FindBy(css = "#password")
private WebElement passwordInput;
@FindBy(css = "#login-button")
private WebElement loginButton;
@FindBy(css = ".error-message")
private WebElement errorMessage;
public LoginPage(WebDriverIO browser) {
super(browser);
PageFactory.initElements(browser, this);
}
// Methods using the initialized elements
public void enterUsername(String username) {
usernameInput.setValue(username);
}
// Other methods...
}
Fluent page objects use method chaining for more readable test code.
public class LoginPage extends BasePage {
// Element locators and constructor...
public LoginPage enterUsername(String username) {
browser.findElement(USERNAME_INPUT).setValue(username);
return this;
}
public LoginPage enterPassword(String password) {
browser.findElement(PASSWORD_INPUT).setValue(password);
return this;
}
public DashboardPage clickLoginButton() {
browser.findElement(LOGIN_BUTTON).click();
return new DashboardPage(browser);
}
// Usage in test
// dashboardPage = loginPage.enterUsername("user").enterPassword("pass").clickLoginButton();
}
Component objects represent reusable UI components that appear on multiple pages.
public class HeaderComponent {
private WebDriverIO browser;
private String rootSelector;
public HeaderComponent(WebDriverIO browser, String rootSelector) {
this.browser = browser;
this.rootSelector = rootSelector;
}
public void clickLogo() {
browser.findElement(rootSelector + " .logo").click();
}
public void openUserMenu() {
browser.findElement(rootSelector + " .user-menu").click();
}
public void search(String query) {
browser.findElement(rootSelector + " .search-input").setValue(query);
browser.findElement(rootSelector + " .search-button").click();
}
}
public class BasePage {
protected WebDriverIO browser;
protected HeaderComponent header;
public BasePage(WebDriverIO browser) {
this.browser = browser;
this.header = new HeaderComponent(browser, "header");
}
// Common methods...
}
The Loadable Component pattern ensures a page is fully loaded before interacting with it.
public abstract class LoadablePage<T extends LoadablePage<T>> {
protected WebDriverIO browser;
public LoadablePage(WebDriverIO browser) {
this.browser = browser;
}
/**
* Navigate to the page
*/
public abstract T open();
/**
* Check if the page is loaded
*/
public abstract boolean isLoaded();
/**
* Wait until the page is loaded
*/
public T waitUntilLoaded() {
browser.waitUntil(this::isLoaded, 10000, "Page did not load in 10 seconds");
return (T) this;
}
/**
* Open the page and wait until it's loaded
*/
public T openAndWait() {
return open().waitUntilLoaded();
}
}
public class LoginPage extends LoadablePage<LoginPage> {
private static final String URL = "https://example.com/login";
private static final String USERNAME_INPUT = "#username";
public LoginPage(WebDriverIO browser) {
super(browser);
}
@Override
public LoginPage open() {
browser.url(URL);
return this;
}
@Override
public boolean isLoaded() {
try {
return browser.getUrl().contains("/login") &&
browser.findElement(USERNAME_INPUT).isDisplayed();
} catch (Exception e) {
return false;
}
}
// Other methods...
}
Each page object should represent a single page or component with a clear responsibility.
Good Practice:
// Separate page objects for different pages
public class LoginPage extends BasePage { /* ... */ }
public class DashboardPage extends BasePage { /* ... */ }
public class ProfilePage extends BasePage { /* ... */ }
Bad Practice:
// One page object handling multiple pages
public class ApplicationPage extends BasePage {
public void login(String username, String password) { /* ... */ }
public void viewDashboard() { /* ... */ }
public void editProfile(String name, String email) { /* ... */ }
}
Choose selectors that are less likely to change with UI updates.
Good Practice:
// Using data attributes or IDs
private static final String LOGIN_BUTTON = "[data-test-id='login-button']";
private static final String USERNAME_INPUT = "#username";
Bad Practice:
// Using brittle selectors based on structure or styling
private static final String LOGIN_BUTTON = "form > div:nth-child(3) > button.btn-primary";
private static final String USERNAME_INPUT = "input.form-control:first-child";
Hide the details of element interactions behind meaningful methods.
Good Practice:
// Methods express intent
public void login(String username, String password) {
enterUsername(username);
enterPassword(password);
clickLoginButton();
}
Bad Practice:
// Exposing element interactions directly
public WebElement getUsernameInput() {
return browser.findElement(USERNAME_INPUT);
}
public WebElement getPasswordInput() {
return browser.findElement(PASSWORD_INPUT);
}
public WebElement getLoginButton() {
return browser.findElement(LOGIN_BUTTON);
}
When an action navigates to a new page, return the corresponding page object.
Good Practice:
public DashboardPage login(String username, String password) {
enterUsername(username);
enterPassword(password);
clickLoginButton();
return new DashboardPage(browser);
}
Bad Practice:
public void login(String username, String password) {
enterUsername(username);
enterPassword(password);
clickLoginButton();
// No return value, caller doesn't know what page we're on
}
Add methods to verify the state of the page.
Good Practice:
public boolean isErrorMessageDisplayed() {
return browser.findElement(ERROR_MESSAGE).isDisplayed();
}
public String getErrorMessage() {
return browser.findElement(ERROR_MESSAGE).getText();
}
Bad Practice:
// No verification methods, tests must access elements directly
// Test: browser.findElement(".error-message").isDisplayed()
Encapsulate wait logic within the page object methods.
Good Practice:
public DashboardPage login(String username, String password) {
enterUsername(username);
enterPassword(password);
clickLoginButton();
// Wait for navigation to complete
browser.waitUntil(() ->
browser.getUrl().contains("/dashboard"),
5000,
"Navigation to dashboard failed"
);
return new DashboardPage(browser);
}
Bad Practice:
public void login(String username, String password) {
enterUsername(username);
enterPassword(password);
clickLoginButton();
// No waiting, test might fail if navigation is slow
}
Page objects should be tested to ensure they work correctly.
public class LoginPageTest {
private WebDriverIO browser;
private LoginPage loginPage;
@BeforeMethod
public void setup() {
// Setup browser and page
Options options = new Options();
options.setBrowser("chrome");
browser = new WebDriverIO(options);
loginPage = new LoginPage(browser);
}
@Test
public void testPageElements() {
loginPage.open();
// Verify all required elements are present
assertTrue(browser.findElement("#username").isDisplayed());
assertTrue(browser.findElement("#password").isDisplayed());
assertTrue(browser.findElement("#login-button").isDisplayed());
}
@Test
public void testEnterUsername() {
loginPage.open();
// Test the enterUsername method
loginPage.enterUsername("testuser");
// Verify the username was entered
assertEquals("testuser", browser.findElement("#username").getValue());
}
// More tests for other methods...
@AfterMethod
public void teardown() {
if (browser != null) {
browser.deleteSession();
}
}
}
public class PageObjectIntegrationTest {
private WebDriverIO browser;
@BeforeMethod
public void setup() {
Options options = new Options();
options.setBrowser("chrome");
browser = new WebDriverIO(options);
}
@Test
public void testPageNavigation() {
// Test navigation between pages
LoginPage loginPage = new LoginPage(browser);
loginPage.open();
DashboardPage dashboardPage = loginPage.login("testuser", "password");
assertTrue(dashboardPage.isDisplayed());
ProfilePage profilePage = dashboardPage.navigateToProfile();
assertTrue(profilePage.isDisplayed());
dashboardPage = profilePage.navigateToDashboard();
assertTrue(dashboardPage.isDisplayed());
}
@AfterMethod
public void teardown() {
if (browser != null) {
browser.deleteSession();
}
}
}
In the next module, we'll explore WebDriverIO with Cucumber integration, learning how to combine the power of behavior-driven development with the Page Object Model for even more maintainable and business-focused test automation.