PageGenerator and LoadableComponent for Better Page Object Model

Hi all, in this tutorial, I will explain to you how to use PageGenerator and LoadableComponent to create a better Page Object Model based Selenium Webdriver test automation projects. By using LoadableComponent pattern, we can easily get the pages without pre-initializations. I described this technique in this article in detail. When we use LoadableComponent pattern with PageGenerator we will easily use the page functions without explicit instantiations.

I will explain this technique on n11.com website. Our test scenario is simple, opening the home page and try to login the site with invalid inputs and check the warning messages. Our main pattern is POM (Page Object Model) and we will construct our project based on POM structure.

First, I want to explain to you PageGenerator class. I called it as “Page” class. In this class, we can create (instantiate) pages. If a page already instantiated with a specific Webdriver instance, we will use the pre-instantiated one instead of the creating a new page instance. So, how to do this?

I will use a ConcurrentHashMap to hold Webdriver and {PageName, PageInstance} pair as shown below. pageHashMap = {Webdriver, {PageName, PageInstance}}

private ConcurrentHashMap<WebDriver, ConcurrentHashMap<String, Object>> pageHashMap = new ConcurrentHashMap<>();

I will have four methods to manage instantiate page instance and save the instantiated page instance with its driver. These are:

setPageHashMap

If a current Webdriver instance is not in the pageHashMap, then this method create a new {Webdriver, {PageName, PageInstance}} pair and put it into the pageHashMap. If current Webdriver exists in the pageHashMap and if the current driver did not instantiate given page before, then put the new PageName and PageInstance into the pageHashMap.

private synchronized void setPageHashMap(WebDriver driver, String pageName, Object obj) {
    if (!pageHashMap.containsKey(driver)) {
        ConcurrentHashMap<String, Object> pageHashMapPageNamePageObj = new ConcurrentHashMap<>();
        pageHashMapPageNamePageObj.put(pageName, obj);
        pageHashMap.put(driver, pageHashMapPageNamePageObj);
    } else {
        if (!pageHashMap.get(driver).containsKey(pageName)) {
            pageHashMap.get(driver).put(pageName, obj);
        }
    }
}

getPageHashMap

It returns the page instance by given driver and page name. If the current driver instance does not exist then it returns null.

private synchronized Object getPageHashMap(WebDriver driver, String pageName) {
    if (pageHashMap.containsKey(driver)) {
        return pageHashMap.get(driver).get(pageName);
    } else {
        return null;
    }
}

createPage

This method gets page name and page class and gets the page instance by using getPageHashMap method. If the page instance is not null, it returns the page instance. If the page instance is null, it first instantiates the page by using current webdriver instance and page class, then it puts the {current driver, {PageName, PageInstance}} pair to the pageHashMap and finally, it returns the Page object by using getPageHashMap method.

//This method creates a new screen with given Screen Name and Screen Class parameters
private synchronized  <TPage extends LoadableComponent<TPage>> TPage createPage (String pageName, Class<TPage> Clazz) {
    TPage tPage = (TPage) getPageHashMap(ThreadLocalDriver.getDriver(), pageName);
    if (tPage != null) {
        return tPage;
    } else {
        tPage = instantiateNewPage(ThreadLocalDriver.getDriver(), Clazz);
        setPageHashMap(ThreadLocalDriver.getDriver(), pageName, tPage.get());
        return (TPage) getPageHashMap(ThreadLocalDriver.getDriver(), pageName);
    }
}

instantiateNewPage

I got this method from PageFactory class and modified it to generate new page instance.

    //This Method Instantiates a new screen. I got this method from PageFactory Class and modify it.
    private static <TPage extends LoadableComponent<TPage>> TPage  instantiateNewPage (WebDriver driver, Class<TPage> Clazz) {
        try {
            try {
                Constructor<TPage> constructor = Clazz.getConstructor(WebDriver.class);
                return constructor.newInstance(driver);
            } catch (NoSuchMethodException var3) {
                return Clazz.newInstance();
            }
        } catch (InstantiationException var4) {
            throw new RuntimeException(var4);
        } catch (IllegalAccessException var5) {
            throw new RuntimeException(var5);
        } catch (InvocationTargetException var6) {
            throw new RuntimeException(var6);
        }
    }

Finally, we can generate page class’s in this way:

public synchronized HomePage homePage() {
    return createPage("homePage", HomePage.class);
}

public synchronized LoginPage loginPage() {
    return createPage("loginPage", LoginPage.class);
}

and when we combine them our page generator class (Page Class) will look like this:

Page Class (Page Generator)

package pages;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.LoadableComponent;
import utilities.ThreadLocalDriver;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.ConcurrentHashMap;

public class Page {

    private ConcurrentHashMap<WebDriver, ConcurrentHashMap<String, Object>> pageHashMap = new ConcurrentHashMap<>();

    private synchronized void setPageHashMap(WebDriver driver, String pageName, Object obj) {
        if (!pageHashMap.containsKey(driver)) {
            ConcurrentHashMap<String, Object> pageHashMapPageNamePageObj = new ConcurrentHashMap<>();
            pageHashMapPageNamePageObj.put(pageName, obj);
            pageHashMap.put(driver, pageHashMapPageNamePageObj);
        } else {
            if (!pageHashMap.get(driver).containsKey(pageName)) {
                pageHashMap.get(driver).put(pageName, obj);
            }
        }
    }

    private synchronized Object getPageHashMap(WebDriver driver, String pageName) {
        if (pageHashMap.containsKey(driver)) {
            return pageHashMap.get(driver).get(pageName);
        } else {
            return null;
        }
    }

    //This method creates a new screen with given Screen Name and Screen Class parameters
    private synchronized  <TPage extends LoadableComponent<TPage>> TPage createPage (String pageName, Class<TPage> Clazz) {
        TPage tPage = (TPage) getPageHashMap(ThreadLocalDriver.getDriver(), pageName);
        if (tPage != null) {
            return tPage;
        } else {
            tPage = instantiateNewPage(ThreadLocalDriver.getDriver(), Clazz);
            setPageHashMap(ThreadLocalDriver.getDriver(), pageName, tPage.get());
            return (TPage) getPageHashMap(ThreadLocalDriver.getDriver(), pageName);
        }
    }

    //This Method Instantiates a new screen. I got this method from PageFactory Class and modify it.
    private static <TPage extends LoadableComponent<TPage>> TPage  instantiateNewPage (WebDriver driver, Class<TPage> Clazz) {
        try {
            try {
                Constructor<TPage> constructor = Clazz.getConstructor(WebDriver.class);
                return constructor.newInstance(driver);
            } catch (NoSuchMethodException var3) {
                return Clazz.newInstance();
            }
        } catch (InstantiationException var4) {
            throw new RuntimeException(var4);
        } catch (IllegalAccessException var5) {
            throw new RuntimeException(var5);
        } catch (InvocationTargetException var6) {
            throw new RuntimeException(var6);
        }
    }

    public synchronized HomePage homePage() {
        return createPage("homePage", HomePage.class);
    }

    public synchronized LoginPage loginPage() {
        return createPage("loginPage", LoginPage.class);
    }
}

BasePage Class

Let’s go on with BasePage class. We will declare the main page operations in this class such as click, write text, and read text, etc.

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

public class BasePage {
    private WebDriver driver;
    public Page page;

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

    //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

In HomePage class we extend the LoadableComponent class and instantiate the BasePage class in the constructor. In this way, we can use BasePage class’s methods. Also, we implement load() and isLoaded() method’s of the LoadableComponent class. Finally, we define page related functions.

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

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

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

    //We need to check that the basePage has been loaded.
    @Override
    public 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);
    }

}

LoginPage Class

In HomePage class we extend the LoadableComponent class and instantiate the BasePage class in the constructor. In this way, we can use BasePage class’s methods. Also, we implement load() and isLoaded() method’s of the LoadableComponent class. Finally, we define page related functions.

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 basePage;
    private final String loginURL = "https://www.n11.com/giris-yap";

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

    //*********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 basePage at load method
    @Override
    protected void load() {
        basePage.page.homePage().goToLoginPage();
    }

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

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

    //Verify Username Condition
    public LoginPage ThenIVerifyLoginUserName(String expectedText) {
        wait.until(ExpectedConditions.visibilityOfElementLocated(errorMessageUsernameBy));
        Assert.assertEquals(basePage.readText(errorMessageUsernameBy), expectedText);
        return this;
    }

    //Verify Password Condition
    public LoginPage ThenIVerifyLoginPassword(String expectedText) {
        wait.until(ExpectedConditions.visibilityOfElementLocated(errorMessagePasswordBy));
        Assert.assertEquals(basePage.readText(errorMessagePasswordBy), expectedText);
        return this;
    }
}

Now, lets go on with test classes.

BaseTest Class

It is the base class of all tests. We define global test setups in this class. For thread-safe parallel test execution, we use ThreadLocal concurrent map. Also, we instantiate page class to generate page classes.

package tests;

import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.annotations.*;
import pages.Page;
import utilities.ThreadLocalDriver;

public class BaseTest {
    protected WebDriverWait wait;
    protected Page page;

    @BeforeMethod(description = "Class Level Setup!")
    @Parameters(value = {"browser"})
    public void globalSetup (@Optional String browser) {
        //Set ThreadLocal Driver
        ThreadLocalDriver.setDriver(browser);

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

        //Maximize Window
        //ThreadLocalDriver.getDriver().manage().window().maximize();

        //Page Generator instance
        page = new Page ();
    }

    @AfterClass(description = "Class Level Teardown!")
    public void globalTearDown() {
        ThreadLocalDriver.getDriver().quit();
    }
}

LoginTests Class

In LoginTests class, we do not need to instantiate the pages as you see in the code and also we can get the login page by using LoadableComponent class’s get() method. When we need to reach the page methods, we use the page class to reach them easily.

package tests;

import org.testng.annotations.Test;

public class LoginTests extends BaseTest {

    private String wrongUsername = "[email protected]";
    private String wrongPassword = "11122233444";

    @Test (description="Invalid Login Scenario with wrong username and password.")
    public void invalidLoginTest_InvalidUserNameInvalidPassword () {
        page.loginPage().get()
                .WhenILoginToN11(wrongUsername, wrongPassword)
                .ThenIVerifyLoginPassword(("E-posta adresiniz veya şifreniz hatalı"));
    }

    @Test (description="Invalid Login Scenario with empty username and password.")
    public void invalidLoginTest_EmptyUserEmptyPassword ()  {
        page.loginPage().get()
                .WhenILoginToN11("","")
                .ThenIVerifyLoginUserName("Lütfen e-posta adresinizi girin.")
                .ThenIVerifyLoginPassword("Bu alanın doldurulması zorunludur.");
    }
}

ExtentManager is used for ExtentReports.

OptionsManager is used for DesiredCapabilities settings.

ThreadLocalDriver is used for thread-safe parallel execution.

You can reach all codes in projects GitHub page. I don’t want to explain that parts in this article. I explained these classes in other articles.

Project GitHub Page: https://github.com/swtestacademy/pagegenerator-loadablecomponent-pom

[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

16 thoughts on “PageGenerator and LoadableComponent for Better Page Object Model”

  1. This is nice looking. I’m using a POM model to where my “visitpage” has an overloaded method that also initializes the page with a “webdriver” so there’s no need to initialize the page. I’m thinking this might be a good way, but I need to look at the benefits of migrating over to this.

    Reply
    • Thank you Andrew, I tried to instantiate the pages one time for each driver instance. Maybe there are better approaches and solutions based on POM. If your tactic is more feasible, I would be very happy to hear that. Thank you so much for your comment.

      Reply
      • My way is most likely not efficient, thus why I’m thinking of migrating over to a new method like this might benefit our framework.
        My POM is a generic one, there’s a constructor that initializes the page, however, there’s also a static visit page and a nonstatic one, the static one initializes the page then goes to the page. Any other method flows into each other using “return this;” or “return new DiffPage(driver);”. I also implemented interfaces that act more like another class using defaults.

        Reply
  2. Superb Explanation will implement in my project this is very helpful once again thanks a lot hope this kind of articles we expect more from @Onur

    Reply
  3. Hi Onur, I am getting java.lang.reflect.InvocationTargetException while executing return constructor.newInstance(driver); on calling instantiateNewPage in Page class. Not sure why, still debugging.

    Reply
  4. I think a ThreadLocal variable which manages page objects will be much simple to work along with generic Factory pattern. Something is like:

    public class PageManager {

    private PageFactory pageFactory;

    public static ThreadLocal localVar;

    public abstract get(Class pageModel) {}
    }


    public class PageFactory {
    private WebDriver driver;

    public PageFactory(WebDriver driver) {
    this.driver = driver;

    public getPage(Class pageModel) {

    }
    }

    Reply

Leave a Comment

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