Loadable Component Pattern for Better Page Object Model Architecture

Hi all, in this article I will explain you the loadable component pattern in Page Object Model architecture. In our test automation projects generally, we are doing some prerequisite actions to test some scenarios. For example, when we are testing the login, first we need to open the homepage and go to login page, then start to do login actions. Without the loadable component pattern, we do this test as follows:

 @Test (priority = 0, description="Invalid Login Scenario with wrong username and password.")
    public void invalidLoginTest_InvalidUserNameInvalidPassword ()  {

        //*************PAGE INSTANTIATIONS*************
        HomePage homePage = new HomePage(driver,wait);
        LoginPage loginPage = new LoginPage(driver,wait);

        //*************PAGE METHODS********************
        //Open N11 HomePage
        homePage.goToN11();

        //Go to LoginPage
        homePage.goToLoginPage();

        //Login to N11
        loginPage.loginToN11(wrongUsername, wrongPassword);

        //*************ASSERTIONS***********************
        loginPage.verifyLoginPassword(("E-posta adresiniz veya şifreniz hatalı"));
    }

When we apply the loadable component pattern in our Page Object Model framework, our test class will become like this:

@Test (priority = 2, description="Invalid Login Scenario with wrong username and password.")
    public void invalidLoginTest_InvalidUserNameInvalidPassword () {
        //*************PAGE INSTANTIATIONS*************
        homePage = new HomePage(driver);
        loginPage = new LoginPage(driver, homePage);

        //*************PAGE METHODS********************
        //Open N11 HomePage and Go to Login Page
        loginPage.get();
        
        //*************PAGE METHODS********************
        loginPage.loginToN11(wrongUsername, wrongPassword);

        //*************ASSERTIONS***********************
        loginPage.verifyLoginPassword(("E-posta adresiniz veya şifreniz hatalı"));
    }

As you see above, now we do not need to specify prerequisite actions explicitly. I mean, we do not need to write HomePage class’s methods which opens the login page:

//Open N11 HomePage
homePage.goToN11();

//Go to LoginPage
homePage.goToLoginPage();

So, how can we use Loadable Component Pattern? Let’s start to learn.

First, we need to extend our page classes with LoadableComponent abstract class.

//HomePage Class
public class HomePage extends LoadableComponent<HomePage> {
.....
.....
.....
}

//LoginPage Class
public class LoginPage extends LoadableComponent<LoginPage> {
.....
.....
.....
}

LoadableComponent class’s structure is shown below:

public abstract class LoadableComponent<T extends LoadableComponent<T>> {
    public LoadableComponent() {
    }

    public T get() {
        try {
            this.isLoaded();
            return this;
        } catch (Error var2) {
            this.load();
            this.isLoaded();
            return this;
        }
    }

    protected abstract void load();

    protected abstract void isLoaded() throws Error;
}

It is an abstract class with load(), isLoaded(), and get() methods. When we extend this class, we have to implement load(), and isLoaded() methods and we can use get() method to obtain our class when it is loaded based on our implementations.

After extending LoadableComponent class, now we need to implement load() and isLoaded() methods in HomePage and LoginPage classes. In load () method, we need to declare the actions which load the page and in isLoaded() method, we need to check that the page has been loaded or not. Before this, let’s look at the project structure:

BasePage is our common Page class which comprises of the common functions of all pages.
HomePage is home page class.
LoginPage is login page class.
TestListener is our test listener Class for taking action when test and suite started, finished, failed, skipped, passed.
BaseTest is the common test class.
LoginTest is login test class.
ExtentManager is for ExtentReports.

Let’s look at some classes’ details. First, let’s start with BasePage. We declared common methods such as click, sendText, getText, etc.

package pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.WebDriverWait;

public class BasePage {
    private WebDriver driver;

    //Constructor
    public BasePage (WebDriver driver){
        this.driver = driver;
    }

    //Click Method
    public void click (By elementLocation) {
        driver.findElement(elementLocation).click();
    }

    //Write Text
    public void writeText (By elementLocation, String text) {
        driver.findElement(elementLocation).sendKeys(text);
    }

    //Read Text
    public String readText (By elementLocation) {
        return driver.findElement(elementLocation).getText();
    }
}

HomePage Class:

Here, I implemented load() and isLoaded() methods which belong to LoadableComponent abstract class. I also instantiate BasePage class’s object in the constructor to use its methods via basePage object.

package pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.LoadableComponent;
import org.openqa.selenium.support.ui.WebDriverWait;

import static org.testng.AssertJUnit.assertTrue;

public class HomePage extends LoadableComponent<HomePage> {

    //*********Page Variables*********
    private String baseURL = "https://www.n11.com/";
    private WebDriver driver;
    private WebDriverWait wait;
    private BasePage basePage;

    //*********Constructor*********
    public HomePage (WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, 10);
        basePage = new BasePage(driver);
    }

    //*********Web Elements*********
    By signInButtonBy = By.className("btnSignIn");

    //*********Override LoadableComponent Methods*********
    //We need to go to the page at load method
    @Override
    protected void load() {
        this.driver.get(baseURL);
    }

    //We need to check that the page has been loaded.
    @Override
    protected void isLoaded() throws Error {
        assertTrue("HomePage is not loaded!", driver.getCurrentUrl().contains(baseURL));
    }

    //*********Page Methods*********
    //Go to LoginPage
    public LoginPage goToLoginPage (){
        basePage.click(signInButtonBy);
        return new LoginPage(this.driver, this);
    }

}

LoginPage Class:

Here, I implemented load() and isLoaded() methods which belong to LoadableComponent abstract class. I also instantiate BasePage class’s object in the constructor to use its methods via basePage object. I also use HomePage class’s goToLoginPage() method at load() block. In order to reach that method, I need to get that class as parent. I assigned the parent object in the constructor and use it at load() method.

parent.get() -> gets the HomePage class

and then, I used its goToLoginPage() method as -> parent.get().goToLoginPage()

In this way, I can go to LoginPage() class.

After, going to LoginPage, we need to check that the page is loaded? We can check this at isLoaded() method.

assertTrue(“LoginPage is not loaded!”, driver.getCurrentUrl().contains(loginURL));

package pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.LoadableComponent;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;

import static org.testng.AssertJUnit.assertTrue;

public class LoginPage extends LoadableComponent<LoginPage> {

    //*********Page Variables*********
    private WebDriver driver;
    private WebDriverWait wait;
    private BasePage page;
    private LoadableComponent<HomePage> parent;
    private final String loginURL = "https://www.n11.com/giris-yap";

    //*********Constructor*********
    public LoginPage (WebDriver driver, LoadableComponent<HomePage> parent) {
         this.driver = driver;
         this.wait = new WebDriverWait(driver,10);
         page = new BasePage(this.driver);
         this.parent = parent;
    }

    //*********Web Elements*********
    By usernameBy = By.id("email");
    By passwordBy = By.id("password");
    By loginButtonBy = By.id("loginButton");
    By errorMessageUsernameBy = By.cssSelector("#loginForm .error:nth-of-type(1) .errorMessage");
    By errorMessagePasswordBy = By.cssSelector("#loginForm .error:nth-of-type(2) .errorText");

    //*********Override LoadableComponent Methods*********
    //We need to go to the page at load method
    @Override
    protected void load() {
        parent.get().goToLoginPage();
    }

    //We need to check that the page has been loaded.
    @Override
    protected void isLoaded() throws Error {
        assertTrue("LoginPage is not loaded!", driver.getCurrentUrl().contains(loginURL));
    }

    //*********Page Methods*********
    public void loginToN11 (String username, String password){
        //Enter Username(Email)
        page.writeText(usernameBy,username);
        //Enter Password
        page.writeText(passwordBy, password);
        //Click Login Button
        page.click(usernameBy); //In order to click right, this line needed. Site related.
        page.click(loginButtonBy);
    }

    //Verify Username Condition
    public void verifyLoginUserName (String expectedText) {
        wait.until(ExpectedConditions.visibilityOfElementLocated(errorMessageUsernameBy));
        Assert.assertEquals(page.readText(errorMessageUsernameBy), expectedText);
    }

    //Verify Password Condition
    public void verifyLoginPassword (String expectedText) {
        wait.until(ExpectedConditions.visibilityOfElementLocated(errorMessagePasswordBy));
        Assert.assertEquals(page.readText(errorMessagePasswordBy), expectedText);
    }
}

Now, let’s go on BaseTest class. In this class, we can declare the before and after operations at class level.

BaseTest Class:

package tests;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;

public class BaseTest {
    public WebDriver driver;
    public WebDriverWait wait;

    @BeforeClass(description = "Class Level Setup!")
    public void classLevelSetup () {
        //Create a Chrome driver. All test classes use this.
        driver = new ChromeDriver();

        //Create a wait. All test classes use this.
        wait = new WebDriverWait(driver,15);

        //Maximize Window
        driver.manage().window().maximize();
    }

    @AfterClass (description = "Class Level Teardown!")
    public void classLevelTeardown () {
        driver.quit();
    }
}

and now, it is time to write a test method.

LoginTest Class:

Here, I did comon prerequisites for each test in @BeforeMethod block. I can reach the loginPage with below line:

loginPage.get();

This method, firsts call’s LoginPage class’s load() method. In this method, parent.get() part calls HomePage class’s load() method and gets the HomePage class then calls its goToLoginPage() method to load LoginPage().

@Override
protected void load() {
parent.get().goToLoginPage();
}

In @Test blocks we can only call LoginTest page’s methods to fulfill login operation and test scenarios. In this way, we don’t need to explicity call HomePage class’s methods to go/open/load LoginPage.

package tests;

import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import pages.HomePage;
import pages.LoginPage;

public class LoginTests extends BaseTest {

    //Test Data
    private String wrongUsername = "[email protected]";
    private String wrongPassword = "11122233444";
    private HomePage homePage;
    private LoginPage loginPage;

    @BeforeMethod(description = "Method Level Setup!")
    public void methodLevelSetup() {
        //*************PAGE INSTANTIATIONS*************
        homePage = new HomePage(driver);
        loginPage = new LoginPage(driver, homePage);

        //*************PAGE METHODS********************
        //Open N11 HomePage and Go to Login Page
        loginPage.get();
    }

    @Test (priority = 2, description="Invalid Login Scenario with wrong username and password.")
    public void invalidLoginTest_InvalidUserNameInvalidPassword () {
        //*************PAGE METHODS********************
        loginPage.loginToN11(wrongUsername, wrongPassword);

        //*************ASSERTIONS***********************
        loginPage.verifyLoginPassword(("E-posta adresiniz veya şifreniz hatalı"));
    }

    @Test (priority = 1, description="Invalid Login Scenario with empty username and password.")
    public void invalidLoginTest_EmptyUserEmptyPassword ()  {
        //*************PAGE INSTANTIATIONS*************
        loginPage.loginToN11("","");

        //*************ASSERTIONS***********************
        loginPage.verifyLoginUserName("Lütfen e-posta adresinizi girin.");
        loginPage.verifyLoginPassword("Bu alanın doldurulması zorunludur.");
    }
}

I do not want to share here ExtentManager and TestListener classes. They are not related to the Loadable Component pattern. But, you can find the full implemented project here: https://github.com/swtestacademy/loadablecomponentexample

You can also learn the SlowLoadableComponent Pattern via this article: http://www.swtestacademy.com/slowloadablecomponent-pattern/

[fusion_widget_area name=”avada-custom-sidebar-seleniumwidget” title_size=”” title_color=”” background_color=”” padding_top=”” padding_right=”” padding_bottom=”” padding_left=”” hide_on_mobile=”small-visibility,medium-visibility,large-visibility” class=”” id=””][/fusion_widget_area]

Thanks.
Onur Baskirt

1 thought on “Loadable Component Pattern for Better Page Object Model Architecture”

  1. In my opinion, using Generic here is too much. Maybe we need two abstract method load() and isLoaded() for page model classes then we implement these two method accordingly to their intention.

    Reply

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.