Previous: API Testing with WebDriverIO Next: WebDriverIO with Cucumber Integration

Module 6: Page Object Model Implementation

Learning Objectives

6.1 Introduction to Page Object Model

What is the Page Object Model?

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:

  1. Element locators: Selectors for the UI elements on the page
  2. Methods: Actions that can be performed on the page
  3. Verification points: Assertions about the page state

Benefits of the Page Object Model

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 vs. Page Object Model

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());
}

6.2 Implementing Page Objects in WebDriverIO with Java

Page Object Model Structure

Page Object Interaction Flow

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);
    }
}

Using Page Objects in Tests

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());
    }
}

6.3 Advanced Page Object Patterns

Page Factory Pattern

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

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

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

Loadable Component Pattern

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

6.4 Best Practices for Page Objects

1. Keep Page Objects Focused

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) { /* ... */ }
}

2. Use Stable Selectors

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";

3. Encapsulate Element Interactions

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);
}

4. Return Page Objects for Navigation

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
}

5. Include Verification Points

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()

6. Handle Waits Within Page Objects

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
}

6.5 Testing Page Objects

Page objects should be tested to ensure they work correctly.

Unit Testing Page Objects

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();
        }
    }
}

Integration Testing Page Objects

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();
        }
    }
}

Knowledge Checkpoint

Practical Exercise

Exercise 1: Basic Page Object Implementation

  1. Create a new Java project with WebDriverIO and TestNG
  2. Implement the following page objects:
    • BasePage with common methods
    • LoginPage with methods for authentication
    • DashboardPage with methods for dashboard interactions
  3. Write tests that use these page objects to:
    • Test successful login
    • Test failed login with invalid credentials
    • Test navigation between pages
  4. Run the tests and verify they pass

Exercise 2: Advanced Page Object Patterns

  1. Extend your project to implement:
    • A HeaderComponent that can be used across multiple pages
    • Fluent page objects with method chaining
    • The Loadable Component pattern for at least one page
  2. Refactor your tests to use these advanced patterns
  3. Add tests for edge cases:
    • Timeout handling
    • Error states
    • Dynamic content loading
  4. Run the tests and verify they pass

Further Reading

Next Steps

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.

Previous: API Testing with WebDriverIO Next: WebDriverIO with Cucumber Integration