What is the problem

It is a common case that a fragment of a page appears on multiple pages. This can be a header, a toolbar, a footer or any block of elements related to each other. These parts can be called sections, panels, blocks, components or by any other name the test developers want. I will refer to these from now on as panels.

To reduce code duplicates the panels can be implemented in separated objects and added to the required page objects as private fields, accessing them with get methods. Finding web elements with the FindBy annotation require the full locator (because locators cannot be concatenated) and default implementation with the LoadableComponent get method. In most cases the locator contains the locator of the panel and the locator of the element in order to be sure to get the given element. This also lead to code duplication.

Original page object

Let’s assume we have this Base page object with webdriver wait, sleep and another wait method. In order to initialize the web elements with @FindBy annotations the PageFactory.initElements() method is used. The webdriver will search for a element throughout the DOM. The this.get() method will call the matching isLoaded() and load() methods in the end of the constructor. This base page is fine for regular page objects. Later we will see how we can improve this code to make it a fine choice for panel objects also.

public class BasePageObjectV01 extends LoadableComponent BasePageObjectV01{

    protected static Logger logger = LoggerFactory.getLogger(BasePage.class);
    protected WebDriver driver;
    protected WebDriverWait webDriverWait;

    public BasePageObjectV01(WebDriver driver) {
        this.driver = driver;
        webDriverWait = new WebDriverWait(driver, 30);
        PageFactory.initElements(driver, this);
        this.get();
    }

    @Override
    public void isLoaded() {}

    @Override
    protected void load() {}

    public WebElement waitForElementToAppear(WebElement webElement, String errorMessage) {
        return webDriverWait.withMessage(errorMessage).until(ExpectedConditions.visibilityOf(webElement));
    }

    public void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

Add root element

To avoid code duplication driver and webdriver wait are initialized in a private method and two new constructors are added: one with additional locator and another one with additional web element. The key difference is that the PageFactory.initElements() is called with parameter “new DefaultElementLocatorFactory(root)”, so the initialization of web elements will execute under the given root web element, not through the root of the page.

 

package com.example.panelobjects.base;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.LoadableComponent;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class BasePageObjectV02 extends LoadableComponent<BasePageObjectV02>{

	protected static Logger logger = LoggerFactory.getLogger(BasePageObjectV03.class);
	protected WebDriver driver;
	protected WebDriverWait webDriverWait;

	public BasePageObjectV02(WebDriver driver) {
		init(driver);
		PageFactory.initElements(driver, this);
		this.get();
	}

	public BasePageObjectV02(WebDriver driver, By root) {
		init(driver);
		WebElement rootelement = driver.findElement(root);
		waitForElementToAppear(rootelement, "The root element should be present");
		PageFactory.initElements(new DefaultElementLocatorFactory(rootelement), this);
		this.get();
	}

	public BasePageObjectV02(WebDriver driver, WebElement root) {
		init(driver);
		waitForElementToAppear(root, "The root element should be present");
		PageFactory.initElements(new DefaultElementLocatorFactory(root), this);
		this.get();
	}

	private void init(WebDriver driver) {
		this.driver = driver;
		webDriverWait = new WebDriverWait(driver, 30);
	}

	@Override
	public void isLoaded() {

	}

	@Override
	protected void load() {

	}

	public WebElement waitForElementToAppear(WebElement webElement, String errorMessage) {
		return webDriverWait.withMessage(errorMessage).until(ExpectedConditions.visibilityOf(webElement));
	}

	public void sleep(long millis) {
		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

Fixing wait issues

The previous implementation had some unexpected wait issues. For example we have panels with same root element but different child elements. When the panel disappears from the page (but it is still present at the DOM) and the new panel appears it can lead to a stale element reference exception. The driver tries to find a element from the previous status of the page the wrong root element, which will cause timeout exception.

To resolve this an optional boolean parameter was added to the constructor. First it will verify that (in a short period of time) the root element would disappear. If the element disappears we search for a new root element.

This constructor takes significantly more time (1 – 2 second) than the one without disappearance verification. If we have a lot of panel objects, the test run can increase up to 30sec, so we should have possibility to skip this verification.

 

package com.example.panelobjects.base;

import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.LoadableComponent;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class BasePageObjectV03 extends LoadableComponent<BasePageObjectV03> {

    protected static Logger logger = LoggerFactory.getLogger(BasePageObjectV03.class);
    protected WebDriver driver;
    protected WebDriverWait webDriverWait;

    public BasePageObjectV03(WebDriver driver) {
        init(driver);
        PageFactory.initElements(driver, this);
        this.get();
    }

    public BasePageObjectV03(WebDriver driver, By root, boolean wait) {
		init(driver);
		WebElement rootElement = driver.findElement(root);
		if(wait && waitForElementToDisappear(rootElement)) {
			rootElement = driver.findElement(root);
		}
		initElementsUnderRoot(rootElement);
    }

    public BasePageObjectV03(WebDriver driver, By root) {
        this(driver, root, true);
    }

    public BasePageObjectV03(WebDriver driver, WebElement root) {
        init(driver);
        initElementsUnderRoot(root);
    }

    private void init(WebDriver driver) {
        this.driver = driver;
        webDriverWait = new WebDriverWait(driver, 30);
    }

    private void initElementsUnderRoot(WebElement root) {
        waitForElementToAppear(root, "The root element should be present");
        PageFactory.initElements(new DefaultElementLocatorFactory(root), this);
        this.get();
    }

    @Override
    public void isLoaded() { }

    @Override
    protected void load() { }
    
    public WebElement waitForElementToAppear(WebElement webElement, String errorMessage) {
        return webDriverWait.withMessage(errorMessage).until(ExpectedConditions.visibilityOf(webElement));
    }
    
    public boolean waitForElementToDisappear(WebElement rootelement) {
        int count = 5;
        while (count > 0) {
            try {
                if (rootelement.isDisplayed()) {
                    sleep(100);
                } else {
                    // Disappeared
                    return true;
                }
            } catch (StaleElementReferenceException | NoSuchElementException e) {
                // Disappeared
                return true;
            }
            count--;
        }
        return false;
    }
    
    public void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Unlock panel depth

The previous implementation is able to initialize web elements under the root element. Sub panel initialization is possible only with given root web element as parentRoot and the sub-panel root element. But still the previously mentioned wait issue can ruin this solution. Now a new constructor has been implemented to allow initialization of page object with parent web element and with relative path to the root element.

 

package com.example.panelobjects.base;

import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.LoadableComponent;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class BasePageObjectV04 extends LoadableComponent<BasePageObjectV04> {

	protected static Logger logger = LoggerFactory.getLogger(BasePageObjectV04.class);
	protected WebDriver driver;
	protected WebDriverWait webDriverWait;

	public BasePageObjectV04(WebDriver driver) {
		init(driver);
		PageFactory.initElements(driver, this);
		this.get();
	}

	public BasePageObjectV04(WebDriver driver, By root, WebElement parentRoot, boolean wait) {
		init(driver);
		WebElement rootElement = parentRoot.findElement(root);
		if(wait && waitForElementToDisappear(rootElement)) {
			rootElement = parentRoot.findElement(root);
		}
		initElementsUnderRoot(rootElement);
	}

	public BasePageObjectV04(WebDriver driver, By root, boolean wait) {
		this(driver, root, driver.findElement(By.xpath("/html")), wait);
	}

	public BasePageObjectV04(WebDriver driver, By root) {
		this(driver, root, true);
	}

	public BasePageObjectV04(WebDriver driver, WebElement root) {
		init(driver);
		initElementsUnderRoot(root);
	}

	private void init(WebDriver driver) {
		this.driver = driver;
		webDriverWait = new WebDriverWait(driver, 30);
	}

	private void initElementsUnderRoot(WebElement root) {
		waitForElementToAppear(root, "The root element should be present");
		PageFactory.initElements(new DefaultElementLocatorFactory(root), this);
		this.get();
	}

	@Override
	public void isLoaded() { }

	@Override
	protected void load() { }

	public WebElement waitForElementToAppear(WebElement webElement, String errorMessage) {
		return webDriverWait.withMessage(errorMessage).until(ExpectedConditions.visibilityOf(webElement));
	}

	public boolean waitForElementToDisappear(WebElement rootelement) {
		int count = 5;
		while (count > 0) {
			try {
				if (rootelement.isDisplayed()) {
					sleep(100);
				} else {
					// Disappeared
					return true;
				}
			} catch (StaleElementReferenceException | NoSuchElementException e) {
				// Disappeared
				return true;
			}
			count--;
		}
		return false;
	}

	public void sleep(long millis) {
		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}