ExpectedConditions is somewhat everyone uses with their classic approach to look up elements with Wait
-like classes. However when you implement your tests in Page Object design pattern, there is another mechanism that goes to the foreground. In this article we’ll see what ExpectedConditions
is, why its design is arguable, look at the example case that implements Page Object design pattern and finally implement conditional waiting for the page object elements using the approach Selenium supports for Page Object out of the box.
Example description: Page Object test which expects the elements to meet certain conditions
Assume that you have a regular Page Object class that most of the people would use to describe, say, a menu of https://webelement.click/en/welcome. Let’s take a look at that:
package click.webelement.pagefactory.conditions; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.PageFactory; public class MenuPage { @FindBy(xpath = ".//li[1]/a") WebElement main; @FindBy(xpath = ".//li[2]/a") WebElement aboutMe; public MenuPage(WebDriver driver){ PageFactory.initElements(driver, this); } public MenuPage switchToMain(){ main.click(); return this; } public MenuPage switchToAboutMe(){ aboutMe.click(); return this; } }
What can we see here? Quite a standard stuff, right? Fields which are described as WebElement
, and methods which access the fields in some way. The problem is that such the design does not allow to wait for the particular state of the element. For example if the element is under control of JavaScript it might change the state after a certain time and we might want to consider the element ready for interaction only after it has taken the mentioned state.
In a regular approach (not Page Object one) we would use Wait
and ExpectedConditions
classes. However for Page Object design which has a native support in Selenium there is another mechanism intended to address such things.
What is ExpectedConditions and when do we use it
Basically, ExpectedConditions
is an utility class that provides preset objects which implement Function
interface. When you define such an object you have to specify a type that would be taken as a parameter of a function and a type that would be returned by a function. In the implementation that is delivered with Selenium Java bindings, each condition in ExpectedConditions
class is a Function
that always takes WebDriver
as the parameter and returns a type that is specific for a particular condition. The example of usage could look like the following:
private void doClick(){ Wait<WebDriver> waiter = new FluentWait<>(driver); WebElement element = waiter .until( ExpectedConditions .visibilityOfElementLocated(By.xpath(".//some-element")) ); element.click(); }
Thus the main arguable point of ExpectedConditions
class is that while waiters like FluentWait
support any sort of the parameter that would be accepted by a conditional function (you are even not limited with any Selenium-related things - you can just wait for any sort of event), the conditions which are pre-implemented within ExpectedConditions
class are limited with WebDriver
implementation. The controversy of the design used can even be seen in such "conditions" of ExpectedConditions
class like:
public static ExpectedCondition<Boolean> attributeToBeNotEmpty(final WebElement element, final String attribute) { return driver -> getAttributeOrCssValue(element, attribute).isPresent(); }
Above you can see that the driver reference is not even used in lambda expression. It is just ignored. And there are quite a lot of methods like the shown one. This is what I call freaky design.
Using waiters in page object that is created with PageFactory.init()
As it has been mentioned in introduction, Selenium has a special mechanism of waiting for element to take a certain condition in page object. To understand that lets take a quick look at the overall process of creating a page with PageFactory
:
-
There are several
init()
methods inPageFactory
class. Each is used to inject fields to your page object. One can either use more easyinit()
so that it will cascadingly call other init methods with some default values, or use more complexinit()
with some custom non-default settings. -
One of the core concepts of that approach is so called
ElementLocator
. Objects which implement this interface are used by Selenium to bind the logic of lookup to a particular field of your page class. -
There are two default implementations of mentioned interface:
DefaultElementLocator
andAjaxElementLocator
.-
DefaultElementLocator
introduces some simple lookup logic. Basically it just delegates all the logic to aSearchContext
that is currently used (either aWebDriver
orWebElement
). However it supports element caching so having that option enabled would force Selenium to use already located element rather than re-lookup it on the page (this is not always a good idea since you may go into stale element issue). -
AjaxElementLocator
extendsDefaultElementLocator
. It is designed in the way to re-try lookup if the element was not located. This is what we’re going to take a closer look at.
-
-
There is also a
LocatorFactory
playing important role. This interface defines a factory that is to returnElementLocator
for a particular field of your page class. We’re going to implement something very trivial for our factory. It will be creating the same locators for all the fields. However in your case you will probably want to return different locators depending on, say, a value of your custom annotation that you are planning to annotate your fields with.
Having all these points considered let’s move to locator implementation.
Customizing AjaxElementLocator for our purpose
The implementation of AjaxElementLocator
wraps the lookup logic of parent class into retry loop but it considers the element is ready for use as soon as it appears in DOM. This is the behavior that we would like change. Luckily Selenium developers have foreseen the need of such extension. There is the method that is intended to address checking the condition. Its default implementation is
protected boolean isElementUsable(WebElement element) { return true; }
As you have noticed, the default implementation always returns true
which means that once the element is located there is no any additional check performed. This is what we’re going to fix in particular. Since the change is really tiny, we’ll not be creating a separate class for that. Instead we’ll create our custom implementation of ElementLocatorFactory
and extend the AjaxElementLocator
right in its create
method.
Since we’re testing our conditions within the method that overrides
isElementUsable
it will look organic to build our class aroundWebElement
rather than aWebDriver
like it happens in originalExpectedCondition
. This is why in the beginning I noticed that the existing condition implementation does not really fit Page Object construction in Selenium in the best way.
package click.webelement.pagefactory.conditions; import java.util.function.Function; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.pagefactory.AjaxElementLocator; import org.openqa.selenium.support.pagefactory.ElementLocator; import org.openqa.selenium.support.pagefactory.ElementLocatorFactory; import java.lang.reflect.Field; public class AjaxConditionalLocatorFactory implements ElementLocatorFactory { final SearchContext searchContext; final int timeOutInSeconds; final Function<WebElement, Boolean> condition; public AjaxConditionalLocatorFactory(SearchContext searchContext, int timeOutInSeconds, Function<WebElement, Boolean> condition) { this.searchContext = searchContext; this.timeOutInSeconds = timeOutInSeconds; this.condition = condition; } @Override public ElementLocator createLocator(Field field) { return new AjaxElementLocator(searchContext, field, timeOutInSeconds) { @Override protected boolean isElementUsable(WebElement element) { return condition.apply(element); } }; } }
Here are few remarkable points regarding this class:
-
Since implementation of
AjaxElementLocator
requiresSearchContext
,Field
andint
as parameters we need to supply them to our factory.Field
is required according to the interface contract and will be provided by Selenium framework. However other stuff is under our own responsibility so that we have to set up it in our constructor. -
Since we are going to implement condition check logic, I suggest to use the same approach as they use in
ExpectedConditions
. A condition (in both our and their approaches) is an object that implementsFunction<T, R>
interface whereT
is a type of parameter the function accepts, andR
is a type of result that the function returns. As it is seen from my implementation, our conditions would takeWebElement
as a parameter and would returnBoolean
value. -
Function
interface defines the onlyapply()
method. So that our overriddenisElementUsable
method would just delegate the check to the condition object.
Implement our custom conditions
Let’s implement couple of conditions. Much like in ExpectedConditions
class we’ll prepare a class that would contain static methods returning Function<WebElement, Boolean>
. Check the sample implementation:
package click.webelement.pagefactory.conditions; import org.openqa.selenium.WebElement; import java.util.function.Function; import java.util.regex.Pattern; public class MyConditions { public static Function<WebElement, Boolean> elementIsVisibleAndEnabled(){ return webElement -> webElement.isDisplayed() && webElement.isEnabled(); } public static Function<WebElement, Boolean> elementTextCorrespondsToAPattern(final Pattern p){ return webElement -> { String elementText = webElement.getText(); if(p.matcher(elementText).matches()){ return true; }else{ return false; } }; } }
As you can see, here we have two conditions implemented (just as an example). First would check if the element is not just in DOM but also visible and enabled, and the second one performs more complex check: it tests if the element’s text corresponds to a specified pattern. Having this example you can easily add your own checks.
So we now need to take two more steps to have the puzzle solved: amend our page class constructor to make it constructed with our custom factory and finally write some sample test class.
Make your page class be constructed with custom ElementLocatorFactory
Now we have to change our page class a bit. Instead of using PageFactory.init(WebDriver, Object)
method we’re going to use PageFactory.init(ElementLocatorFactory, Object)
which is really more flexible and will fit our design. This is how our new constructor would look like:
public MenuPage(SearchContext searchContext, Function<WebElement, Boolean> condition){ PageFactory.initElements( new AjaxConditionalLocatorFactory( searchContext, 10, condition ), this ); }
Here we changed several things in our original constructor. First of all we changed the parameters set. Instead of WebDriver
we now take SearchContext
so that we can instantiate our page not only against the whole driver but also against any implementation of SearchContext
. For example it could be a WebElement
. Second is that we also take a condition object that would define the logic of how we consider the element is ready.
In this example we use quite a simple architecture that implies that our conditions accept only
WebElement
parameter (after all we’re implementing the conditions which are to be used within our overriddenisElementUsable
method) and that all the elements of the page instance are handled by the same readiness condition. If you need to have your architecture more flexible, I would suggest to introduce your custom annotation that would define which conditions are to be used for a particular page field and then improveAjaxConditionalLocatorFactory
that would be using that field’s annotation instead of taking the condition as a parameter.
We also provide some timeout (this parameter is defined in AjaxElementLocator
which we extend by our custom element locator class) that would be used to stop looking for the element at some point and consider it unavailable eventually.
Final step: implement a test
Now we have everything prepared for a test. We have our custom ElementLocatorFactory
that produces custom ElementLocator
that extends AjaxElementLocator
having isElementUsable(WebElement element)
method overridden with the logic that is defined in our custom conditions. Let’s now look at how the test class that would use all we have prepared could look like:
package click.webelement.pagefactory.conditions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openqa.selenium.WebDriver; import org.openqa.selenium.firefox.FirefoxDriver; import java.util.regex.Pattern; public class PageFactoryConditionsTest { WebDriver driver; @BeforeAll public static void globalSetup() { System.setProperty("webdriver.gecko.driver", "/home/alexey/Desktop/Dev/webdrivers/geckodriver"); } @BeforeEach public void setUp() { driver = new FirefoxDriver(); } @Test public void testCapitalizedTest() { driver.get("https://webelement.click/en/welcome"); MenuPage menuPage = new MenuPage( driver, MyConditions.elementTextCorrespondsToAPattern(Pattern.compile("^[A-Z]{1}.*")) ); menuPage.switchToAboutMe(); menuPage.switchToMain(); } @AfterEach public void tearDown() { if (driver != null) { driver.quit(); } } }
What is this test doing? Basically it tests if a menu item starts from capitalized latin letter. We can assume that we have a web page that is somehow "prettified" by JavaScript (like it happens on my site with code snippet stylization) after all elements have been loaded. Thus we’re considering the menu item is ready as soon as it is properly styled. This test demonstrates such the case.
Well, now you know the end-to-end flow of how to implement the waiting mechanism for Page Object design pattern with PageFactory
harness like it was originally designed by Selenium developers. This does not mean there are no other ways to address such problem. However I believe the described approach is most flexible and self-descriptive.
If you still have the questions please send them to me using this form. I will amend the article according to your feedback.