I have found a very interesting post about how @FindBy works
and how to use FieldDecorator in Selenium (WebDriver) based tests: http://habrahabr.ru/post/134462/.
The author of the post is Роман Оразмагомедов (Roman
Orazmagomedof).
Here I am giving more explanation on how to use FieldDecorator. Also I will be showing the extended version of the original implementation with additional
functionality which will allow waiting for a decorated field to be ready by using ExpectedCondition interface.
Setting Objectives
Most illustrations of Selenium page object pattern are using
WebElement interface to define the pages’ fields:
public class APageObject {
@FindBy(id="fieldOne_id")
WebElement fieldOne;
@FindBy(xpath="fieldTwo_xpath")
WebElement fieldTwo;
<RESTO OF THE Page IMPLEMENTATION>
}
I would like:
a) A page to be a more generic container with the ability to
combine several forms together.
b) To use plain java objects instead of WebElement interface to declare
fields on a page.
c) To have a simple way to determine if an element
on a page is ready to be used or not.
For example:
public class PageObject {
private APageForm formA;
<OTHER FORMS DECLARATIONS >
public void init(final WebDriver driver) {
this.driver = driver;
formA = new APageForm());
PageFactory.initElements(new SomeDecorator(driver),
formA);
<OTHER FORMS INITIALIZATION>
}
<THE REST OF the PAGE
IMPLEMENTATION>
}
Where APageForm looks similar to a APageObject, but with a little
difference – each field in the form is defined by a dedicated java class.
public class APageForm {
@FindBy(id="fieldOne_id")
FieldOne fieldOne;
@FindBy(xpath="fieldTwo_xpath")
FieldTwo fieldTwo;
<REST OF THE FORM
IMPLEMENTATION>
}
There are two more important points to remember:
a)
This approach should use Selenium ExpectedCondition;
b)
This approach should help to
separate code between “data delivery” and “data assertion”.
1. Element
public interface Element {
public boolean isVisible();
public void click();
public
ExpectedCondition<WebElement> isReady();
}
This interface should be extended for more complex elements
like button, link, label etc. For example:
public interface TextField extends Element {
public TextField
clear();
public TextField
enterText(String text);
public
ExpectedCondition<WebElement> isReady();
}
Each element should provide isReady() to avoid usage of the
Thread.sleep().
Every implementation of an element should extend
AbstractElement class:
public abstract class AbstractElement implements Element {
protected WebElement wrappedElement;
protected AbstractElement (final WebElement el) {
this.wrappedElement = el;
}
@Override
public boolean isVisible() {
return wrappedElement.isDisplayed();
}
@Override
public void click() {
wrappedElement.click();
}
public abstract
ExpectedCondition<WebElement> isReady();
}
For example:
public class
ApplicationTextField extends AbstractElement implements TextField {
public
ApplicationTextField(final WebElement el) {
super(el);
}
@Override
public TextField
clear() {
wrappedElement.clear();
return this;
}
@Override
public TextField
enterText(String text) {
char[] letters =
text.toCharArray();
for (char c: letters) {
wrappedElement.sendKeys(Character.toString(c));
// because it
is typing too fast...
try {
Thread.sleep(70);
} catch
(InterruptedException e) {
e.printStackTrace();
}
}
return this;
}
@Override
public
ExpectedCondition<WebElement> isReady() {
return
ExpectedConditions.elementToBeClickable(wrappedElement);
}
}
The following interface describes an element factory:
public interface ElementFactory {
public <E extends Element> E
create(Class<E> containerClass, WebElement wrappedElement);
}
the implementation of the element factory is:
public class
DefaultElementFactory implements ElementFactory {
@Override
public <E extends Element> E
create(final Class<E> elementClass,
final WebElement
wrappedElement) {
E element;
try {
element =
findImplementingClass(elementClass)
.getDeclaredConstructor(WebElement.class)
.newInstance(wrappedElement);
}
catch
(InstantiationException e) { throw new
RuntimeException(e);}
catch
(IllegalAccessException e) { throw new
RuntimeException(e);}
catch
(IllegalArgumentException e) {throw new
RuntimeException(e);}
catch
(InvocationTargetException e) {throw new
RuntimeException(e);}
catch
(NoSuchMethodException e) { throw new
RuntimeException(e);}
catch
(SecurityException e) {throw new RuntimeException(e);}
return element;
}
private <E extends Element>
Class<? extends E> findImplementingClass (final Class<E>
elementClass) {
String pack =
elementClass.getPackage().getName();
String className =
elementClass.getSimpleName();
String interfaceClassName = pack+"."+className;
Properties impls = TestingProperties.getTestingProperties().getImplementations();
if (impls == null) throw new
RuntimeException("Implementations are not loaded");
String implClassName =
impls.getProperty(interfaceClassName);
if (implClassName
== null) throw new
RuntimeException("No implementation found for interface "+interfaceClassName);
try {
return (Class<? extends E>) Class.forName(implClassName);
} catch
(ClassNotFoundException e) {
throw new
RuntimeException("Unable to load class for "+implClassName,e);
}
}
}
The factory reads a property file to use a desired
implementation for an element:
com.qamation.web.elements.Button = tests.application.elements.ApplicationButton
com.qamation.web.elements.Link = tests.application.elements.ApplicationLink
com.qamation.web.elements.TextField
= tests.application.elements.ApplicationTextField
com.qamation.web.elements.Label=tests.application.elements.ApplicationLabel
The element factory is going to be used by an implementation
of the FieldDecorator interface. I will discuss this below.
At this point the elements' part coverage is completed. Here is
the summary:
Each element is described by an interface which is extending Element
interface.
Every element’s implementation extends the AbstractElement class
and completes isReady(), along with other required methods.
Desired element’s implementation should be defined in a property file.
The element factory will instantiate an element and pass it
to the PageFactory.initElement() via the decorator.
It seems complicated at first.
It becomes very convenient to create and use simple elements
to model complex forms and pages.
2. Container.
A container is a facility to keep elements and other
containers together in order to model complex web forms and pages.
The container structure is similar to the element however it's
simpler.
A container is defined by an interface:
public interface Container {
public void init(WebElement
wrappedElement);
public
ExpectedCondition<Boolean> isReady(WebDriverWait wait);
}
The container has its AbstractContainer base class:
public abstract class
AbstractContainer implements Container{
private WebElement wrappedElement;
@Override
public void init(WebElement
wrappedElement) {
this.wrappedElement =
wrappedElement;
}
public abstract ExpectedCondition<Boolean>
isReady(final WebDriverWait wait);
}
It is important to pay attention to the container’s init method:
method’s parameter is an instance of the WebElement interface.
Similar to an element, a container should implement
the isReady() method. The difference is in the return type:
ExpectedCondition<Boolean>.
The “Ready” condition of a container depends on the combination of
the elements included into the container.
It is logical to combine several conditions into one using
Boolean type.
Here is an example of a container:
public class LoginContainer extends
AbstractContainer{
@FindBy(id="Email")
private TextField username;
@FindBy(id="Passwd" )
private TextField password;
@FindBy(id="signIn")
private Button submitButton;
public void login(final String username,
final String password)
{
this.username.clear().enterText(username);
this.password.clear().enterText(password);
this.submitButton.press();
}
@Override
public
ExpectedCondition<Boolean> isReady(final WebDriverWait
wait) {
return new ExpectedCondition<Boolean>()
{
@Override
public Boolean apply(final WebDriver
driver) {
ExpectedCondition
isUserNameFieldReady = username.isReady();
ExpectedCondition isPasswordFieldReady = password.isReady();
ExpectedCondition isSubmitButtonReady = submitButton.isReady();
try {
wait.until(isUserNameFieldReady);
wait.until(isPasswordFieldReady);
wait.until(isSubmitButtonReady);
return new Boolean(true);
}
catch
(TimeoutException ex) {
return new Boolean(false);
}
}
};
}
}
Container factory defined by an interface:
public interface ContainerFactory
{
public <C extends Container> C
create(Class<C> wrappingClass, WebElement wrappedElement);
}
container factory's implementation is much simpler than element’s
factory:
public class
DefaultContainerFactory implements ContainerFactory {
@Override
public <C extends Container> C
create(final Class<C> wrappingClass,
final WebElement
wrappedElement) {
C container;
try {
container =
wrappingClass.newInstance();
}
catch (InstantiationException e){throw new
RuntimeException(e);}
catch (IllegalAccessException e){throw new
RuntimeException(e);}
container.init(wrappedElement);
return container;
}
}
Here is a short summary for the container:
A container is used to combine elements and other containers
into one unit.
A container's implementation should extend from the AbstructContainer
class. It should implement isReady() and other methods required by the
container.
A container will be instantiated and passed to the
PageFactory.initElement() by the container factory through a decorator.
3. Page
A page is a bridge between a WebDriver instance and
containers. A Page helps to decouple WebDriver from testing activities, test
data provisioning and test results verification.
A Page is defined by an interface, similar to the Container:
public interface Page {
public void init(WebDriver
driver);
}
The difference between a container and a page is in the init():
public abstract class AbstractPage implements Page {
protected WebDriver driver;
@Override
public void init(WebDriver
driver) {
this.driver = driver;
}
}
Page’s init method takes a WebDriver instance as a
parameter.
A page implementation should extend the AbstractPage class.
For example, a simple gmail page:
public interface GMailPage extends Page {
public NewEmail
startNewEmail();
}
public class DefaultGMailPage
extends AbstractPage implements GMailPage {
private
LeftMenueContainer leftMenue;
public void init(final WebDriver
driver) {
this.driver = driver;
leftMenue = new
LeftMenueContainer();
PageFactory.initElements(new
DefaultWebDecorator(driver), leftMenue);
WebDriverWait wait = new
WebDriverWait(driver,TestingProperties.getTestingProperties().getTimeOutGeneral());
ExpectedCondition<Boolean>
isEmailFormReady = leftMenue.isReady(wait);
wait.until(isEmailFormReady);
}
@Override
public NewEmail
startNewEmail() {
leftMenue.pressCompose();
NewEmailWindowContainer newEmail =
new
NewEmailWindowContainer();
PageFactory.initElements(new
DefaultWebDecorator(driver), newEmail);
WebDriverWait wait = new WebDriverWait(driver,TestingProperties.getTestingProperties().getTimeOutGeneral());
ExpectedCondition<Boolean>
isNewEmailReady=newEmail.isReady(wait);
wait.until(isNewEmailReady);
return newEmail;
}
}
Components summary:
Element -> AbstractElement -> Element’s
Emplementations -> Element Factory
Container -> AbstractContainer -> Container Factory
Page -> AbstractPage.
4. Decorator
The constructions described above become alive when
PageFactory.initElements() calls the provided decorator.
A basic implementations is already exists –
DefaultFieldDecorator. Lets use it.
public class
DefaultWebDecorator extends DefaultFieldDecorator {
private ElementFactory elementFactory = new
DefaultElementFactory();
private ContainerFactory
containerFactory = new
DefaultContainerFactory();
public
DefaultWebDecorator(SearchContext context) {
super(new
DefaultElementLocatorFactory(context));
}
@Override
public Object
decorate(ClassLoader classLoader, Field field) {
ElementLocator locator = factory.createLocator(field);
WebElement wrappedElement =
proxyForLocator(classLoader, locator);
if (Container.class.isAssignableFrom(field.getType()))
{
return decorateContainer(field,
wrappedElement);
}
if (Element.class.isAssignableFrom(field.getType()))
{
return
decorateElement(field, wrappedElement);
}
return super.decorate(classLoader,
field);
}
private Object
decorateContainer(final Field field, final WebElement wrappedElement) {
Container container = containerFactory.create((Class<?
extends Container>)field.getType(),
wrappedElement);
PageFactory.initElements(new
DefaultWebDecorator(wrappedElement), container);
return container;
}
private Object
decorateElement(final Field field, final WebElement wrappedElement) {
Element element = elementFactory.create((Class<?
extends Element>)field.getType(),
wrappedElement);
return element;
}
}
Note that the decorateContainer() does not exit until all
sub elements and containers are not initialized.
Now, let’s look at a simple test that presses Compose button
on the gmail page and checks if a new email window appears on the screen:
public class NewEmailTest {
private WebDriver driver;
@BeforeTest
public void setUp() {
driver = new FirefoxDriver();
driver.manage().window().maximize();
}
@AfterTest
public void tearDown() {
driver.close();
}
@Test (dataProvider = "inputAndOutput",
dataProviderClass = com.qamation.data.provider.TestDataProvider.class)
public void
startNewEmailTest(DataBlock data) {
DefaultHomePage homePage = new
DefaultHomePage();
driver.manage().deleteAllCookies();
driver.get(data.getInput()[0]);
homePage.init(driver);
NewEmail newEmail =
homePage.signIn().login(data.getInput()[1],
data.getInput()[2]).startNewEmail();
for (String[] sa :
data.getExpectedResults()) {
WebElement el = driver.findElement(By.xpath(sa[0]));
Assert.assertTrue(el.isDisplayed());
}
}
}
When running the test from Eclipse, the following VM
arguments need to be used:
-DpropertiesFile=testing.properties