I found a very interesting post about how @FindBy works and how to use FieldDecorator tests in Selenium (WebDriver): http://habrahabr.ru/post/134462/ .
The author of the message is Roman Orazmagomedov (Roman Orazmagomedof).
Here I give more explanations on how to use FieldDecorator. I will also show an extended version of the original implementation with additional functionality that allows you to wait until the decorated field is ready using the ExpectedCondition interface.
Setup Tasks
Most Selenium page object template illustrations use the WebElement interface to define page fields:
public class APageObject { @FindBy(id="fieldOne_id") WebElement fieldOne; @FindBy(xpath="fieldTwo_xpath") WebElement fieldTwo; <RESTO OF THE Page IMPLEMENTATION> }
I would like to:
a) A page that will be a more general container with the ability to combine several forms together.
b) Use simple Java objects instead of the WebElement interface to declare fields on the page.
c) To have an easy way to determine if an element on a page is ready to use or not.
For instance:
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 is similar to APageObject, but with a slight difference - each field in the form is determined by the selected 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:
a) This approach should use the Selenium ExpectedCondition;
b) This approach should help to separate the code between โdata deliveryโ and โdata validationโ.
This interface should be extended to more complex elements such as button, link, shortcut, etc. For instance:
public interface TextField extends Element { public TextField clear(); public TextField enterText(String text); public ExpectedCondition<WebElement> isReady(); }
Each element must provide isReady () to avoid using Thread.sleep ().
Each element implementation must extend the 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 instance:
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));
The following interface describes the factory element:
public interface ElementFactory { public <E extends Element> E create(Class<E> containerClass, WebElement wrappedElement); }
factory element implementation:
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); } } }
factory reads the properties file to use the desired implementation for the 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 factory element will be used by the implementation of the FieldDecorator interface. I will talk about this below.
At this point, the coverage of parts of the elements is completed. Here is a summary:
Each element is described by an interface that extends the Element interface.
Each element implementation extends the AbstractElement class and completes isReady () along with other necessary methods.
The implementation of the desired elements must be defined in the properties file.
The factory element will instantiate the element and pass it to PageFactory.initElement () through the decorator.
It seems complicated at first.
It is very convenient to create and use simple elements for modeling complex forms and pages.
- Container.
A container is a tool for storing elements and other containers for modeling complex web forms and pages.
The structure of the container is similar to an element, but simpler.
The container is defined by the interface:
public interface Container { public void init(WebElement wrappedElement); public ExpectedCondition<Boolean> isReady(WebDriverWait wait); }
The container has an 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 init method: the method parameter is an instance of the WebElement interface.
Like an element, a container must implement the isReady () method. The difference is in the type of the return value: ExpectedCondition.
The Ready condition of the container depends on the combination of elements included in the container.
It is logical to combine several conditions into one using the Boolean type.
Here is an example 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); } } }; } }
The factory container defined by the interface:
public interface ContainerFactory { public <C extends Container> C create(Class<C> wrappingClass, WebElement wrappedElement); }
container factory implementation is much simpler than factory elements:
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 quick overview for the container:
A container is used to combine elements and other containers into one block.
The container implementation must pass from the AbstructContainer class. It should implement isReady () and other methods required by the container.
The container will be created and transferred to the PageFactory.initElement () file by the factory container through the decorator.
- Page
The page is the bridge between the WebDriver instance and the containers. The page helps separate WebDriver from testing, validating test data, and validating test results.
The page is defined by an interface similar to the container:
public interface Page { public void init(WebDriver driver); }
The difference between the container and the page is in init ():
public abstract class AbstractPage implements Page { protected WebDriver driver; @Override public void init(WebDriver driver) { this.driver = driver; } }
The init method of the page takes an instance of WebDriver as a parameter.
The implementation of the page 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; } }
Component Summary:
Element โ Absolute Element โ Element Emulation โ Factory Element
Container โ Abstract Container โ Factory Container
Page โ AbstractPage.
- decorator
The constructs described above become alive when PageFactory.initElements () calls the provided decorator.
Major implementations already exist - 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 decorateContainer () does not end until all auxiliary elements and containers are initialized.
Now let's look at a simple test that clicks the Create button on the gmail page and checks to see 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 starting a test from Eclipse, you must use the following VM arguments:
-DpropertiesFile = testing.properties
Sources and some more articles about QA and QA Automation can be found here http://qamation.blogspot.com