It’s been far too long since there has been any new content on this space. This actually wasn’t even the article I intended on being published next, but so it goes.
Obviously all of the examples on this site thus far have been written in Java, mainly due to experience with it on our team. However I have seen so many different bindings/DSLs for Selenium, that it has gotten me curious. What tools are you using? Is it the language you use in your day to day work? Is it a scripting language? Is it something else entirely?
We have looked at geb + spock, phantomjs + ghostdriver, watir + watirwebdriver, and at the native implementations (Java, C#, Ruby, & Python). What other tools are you looking at for your testing purposes? Have you worked with any of these? What do you like/dislike about them? What do you feel is lacking in this space?
Often times while testing you’ll want to ensure that you are testing your system in isolation. We find that our tests are often much slower due to third party integrations, it might be a social media provider or it could be a third party analytics provider. These items are outside of your control, and you may wish to avoid their effects while testing.
How can we do this? Pull in the browsermob-proxy project into your tests (we’d love to hear your comments on how this can be achieved with other language bindings). With this addition you now have easy ways to ensure your third party providers will not interfere with your testing. You might wish to run these tests with and without the proxy enabled to get an idea of how your end-users will perceive the performance of your site. With all of this being said, it is worth keeping these artifacts somewhere, and running eventually deriving some statistics from your data sets.
So how do we do this? First we need to setup our proxy server, this is as simple as adding the proxy to your class path, and some setup code. Remember we are using TestNG in this case below.
public class ExampleProxyTest {
private ProxyServer server;
private WebDriver driver;
@BeforeClass
public void setup() throws Exception {
server = new ProxyServer(9000); //Any high numbered port should work fine
server.start();
for (final String s : getHostsToBlackList()) {
server.blacklistRequests(s, 200); //Send a 200 when the host matches
}
final DesiredCapabilities dc = new DesiredCapabilities();
dc.setCapability(CapabilityType.PROXY, server.seleniumProxy());
driver = new FirefoxDriver(dc);
}
private List<String> hostsToBlackList() {
final List<String> hosts = new ArrayList<String>();
hosts.add("http://.*\\.fbcdn.net/.*");
hosts.add("http://.*\\.facebook.com/.*");
hosts.add("http://.*\\.plusone.google.com/.*");
hosts.add("http://.*\\.twitter.com/.*");
hosts.add("http://.*\\.twimg.com/.*");
hosts.add("http://.*\\.pinterest.com/.*");
return hosts;
}
}
As you can see we have blocked out a number of Social Media sites. This list should be modified for your needs obviously. However, now it’s as simple as opening your site, and noticing the speed increases.
@Test
public void testThirdPartyBlocks() {
server.newHar("example.har");
driver.get("http://example.com");
try {
server.getHar().writeTo(new File("example.har"));
} catch (IOException ioe) { }
}
That’s it. You should see a nice increase in testing speed now, you could also test to see how your application works / looks when your JavaScript fails to load, or a myriad of other things. Don’t forget to close the driver, and shut down the proxy:
@AfterClass
public void shutdown() {
driver.quit();
server.stop();
}
If you hadn’t seen our post on changing browsers without changing code, it’s worth noting that we did not show you that we also have the proxy in a profile. If that profile is turned on than the driver we return has the proxy enabled. It will be nice once the cloud providers can allow us to use this functionality on their sites.
Until next time, enjoy the Thanksgiving holiday if you’re celebrating it.
We’ve finally added the code to github. It’s pretty raw (not commented), pull requests will certainly be considered.
I stated in our last post that we would be putting our code up on GitHub soon… I apologize for that not happening, but I really wanted to get this out in the open before we added our bits to GitHub. As usual that did not happen in a timely manner. For this I apologize.
We are introducing two concepts from the Spring Framework in this post. Spring is a bolt on product for Java development, which is typically used in enterprise applications. We will be using two new features of the framework. Namely, Java configuration and profiles. To ease this transition, I am switching from JUnit tests to TestNG. This is due to JUnit @Before & @After methods needing to be static. Spring cannot inject static values.
Below is our updated class (I’ve removed all but one of the tests to shorten the post):
@ContextConfiguration(classes = { Firefox.class, InternetExplorer.class, Chrome.class })
public class JQueryHomePageTest extends AbstractTestNGSpringContextTests {
@Resource
private WebDriver driver;
private JQueryHomePage homePage;
@BeforeClass
public void setup() {
homePage = new JQueryHomePage(driver);
}
@Test
public void getText() {
//Chrome and Firefox will include a \n in the text representation, whereas IE will not
final String text = StringUtils.replaceEach(homePage.getTagline().getText(), new String[] { "\r", "\n" }, new String[] { " ", " " });
Assert.assertTrue("jQuery is a new kind of JavaScript Library.".equalsIgnoreCase(text));
}
@AfterClass
public void shutdown() {
driver.quit();
}
}
Now we need to setup our configuration files. Here is what our Firefox class looks like:
@Configuration
@Profile("firefox")
public class Firefox {
@Bean
public WebDriver driver() {
return new FirefoxDriver();
}
}
And the Chrome class:
@Configuration
@Profile({ "chrome", "default" })
public class Chrome {
@Bean
public WebDriver driver() {
System.setProperty("webdriver.chrome.driver", "chromedriver.exe");
return new ChromeDriver();
}
}
And the InternetExplorer class:
@Configuration
@Profile("ie")
public class InternetExplorer {
@Bean
public WebDriver driver() {
System.setProperty("webdriver.ie.driver", "IEDriverServer.exe");
return new InternetExplorerDriver();
}
}
If we run the test with no system parameters, we will utilize Chrome as our browser. If we add the following parameter to our run configuration “-Dspring.profiles.active=ie” we will run with Internet Explorer. If we add “-Dspring.profiles.active=firefox”, we will run with Firefox. This is a nice and easy way to modify our tests to run in a different browser, without having to modify code. Hopefully this is easily extensible for your environment. Need to run against Sauce Labs or Testing Bot? Create a profile(s) for each browser. Need to run against an internal grid? Create a profile(s) for each browser. If you can get this baked into your continuous integration server, you should feel pretty confident that your tests are behaving as expected in all browsers.
I’m sorry for the delay between these posts, but sometimes time gets away from us. When we look at our sites, we realized that they are very modular — look even at this site, we have a center column and a sidebar. We needed a way to verify that our modules were showing up in these correct areas, which led us to some initial frustration. Without storing your locator as static items you couldn’t do the following:
@FindBy(id = "jq-intro") private WebElement introDiv; @FindBy(css = "#jq-intro h2") private WebElement introText; //Ensure that this is located within the introDiv
That wasn’t good enough; we wanted to enforce these types of rules, and yell out if items were located where they were unexpected (without having to use our jQuery locator or create a css locator based off the original). So today we are going to show you a slight spin-off of what we created to get around this limitation. This will again be a code heavy post, to keep it “short” there are no comments as in the past. This should be posted to github before long with some documentation / comments. I’ll update the blog once that has happened.
So let’s start (as usual) with a new annotation. We don’t allow for the $ here, as it seems unneeded — if you’re reverting to jQuery to find the item, it seems like that would be the proper locator to use. This will be the annotation that let’s us know that we should look inside of another WebElement:
public @interface ChildOf {
String fieldName();
How how();
String using() default = "";
}
So let’s take the above example, and use our new annotation:
@FindBy(id = "jq-intro") private WebElement introDiv; @ChildOf(fieldName = "introDivWithDefault", how = How.TAG_NAME, using = "h2") private WebElement introText;
Now we have to setup the plumbing for our PageFactory to find these items. To do this, we first need a ChildElementLocatorFactory:
public class ChildElementLocatorFactory implements ElementLocatorFactory {
final Map<String, List<WebElement>> parentCandidates;
public ChildElementLocatorFactory(final Map<String, List<WebElement>> parentCandidates) {
this.parentCandidates = parentCandidates;
}
@Override
public ElementLocator createLocator(final Field field) {
if (field.isAnnotationPresent(ChildOf.class)) {
return new ChildElementLocator(parentCandidates, field);
}
return null; //When the factory returns null, the PageFactory doesn't look for the item
}
}
We also need to create our ElementLocator.
public class ChildElementLocator implements ElementLocator {
private final List<WebElement> parentCandidates;
private final How how;
private final String using;
public ChildElementLocator(final Map<String, List<WebElement>> parentCandidates, final Field field) {
final ChildOf childOf = field.getAnnotation(ChildOf.class);
this.how = childOf.how();
if (How.ID_OR_NAME.equals(how)) {
this.using = field.getName();
} else {
this.using = childOf.using();
}
if (!parentCandidates.containsKey(childOf.fieldName())) {
throw new NoSuchElementException("No parents with the field name '" + childOf.fieldName()
+ "' were located. Unable to locate element with: " + how + " using: " + using);
}
this.parentCandidates = parentCandidates.get(childOf.fieldName());
}
@Override
public WebElement findElement() {
NoSuchElementException finalException = null;
for (final WebElement candidate : parentCandidates) {
try {
final WebElement e = candidate.findElement(buildBy());
e.isDisplayed();
return e;
} catch (final NoSuchElementException nsee) {
finalException = nsee;
}
}
throw finalException;
}
@Override
public List<WebElement> findElements() {
for (final WebElement candidate : parentCandidates) {
final List<WebElement> elements = candidate.findElements(buildBy());
if (elements.size() > 0) {
return elements;
}
}
return Collections.emptyList();
}
private By buildBy() {
//Implementation basically same as Annotations#buildByFromLongFindBy from Selenium code
//Not showing here to shorten post
}
}
And with that we need to update our PageFactory. This is long and uncommented, but again should be up on github soon.
public class CustomPageFactory {
public static void initElements(final WebDriver driver, final Object page) {
final Map<String, List<WebElement>> locatedFields = initElements(new CustomElementLocatorFactory(driver), page);
initElements(new ChildElementLocatorFactory(locatedFields), page);
}
public static void initElements(final ChildElementLocatorFactory factory, final Object page) {
PageFactory.initElements(new DefaultFieldDecorator(factory), page);
}
public static Map<String, List<WebElement>> initElements(final CustomElementLocatorFactory factory, final Object page) {
return initElements(new CustomFieldDecorator(factory), page);
}
public static Map<String, List<WebElement>> initElements(final CustomFieldDecorator decorator, final Object page) {
Map<String, List<WebElement>> locatedFields = new HashMap<>();
Class<?> proxyIn = page.getClass();
while (proxyIn != Object.class) {
locatedFields = mergeValues(locatedFields, proxyFields(decorator, page, proxyIn));
proxyIn = proxyIn.getSuperclass();
}
return locatedFields;
}
private static Map<String, List<WebElement>> mergeValues(final Map<String, List<WebElement>> foundInSubclasses,
final Map<String, List<WebElement>> currentlyLocated) {
if (foundInSubclasses.entrySet().size() == 0) {
return currentlyLocated;
}
for (final Map.Entry<String, List<WebElement>> entry : foundInSubclasses.entrySet()) {
if (currentlyLocated.containsKey(entry.getKey())) {
final List<WebElement> locatedInSubclasses = entry.getValue();
final List<WebElement> justFound = currentlyLocated.get(entry.getKey());
locatedInSubclasses.addAll(justFound);
} else {
entry.setValue(currentlyLocated.get(entry.getKey()));
}
}
return foundInSubclasses;
}
private static Map<String, List<WebElement>> proxyFields(final FieldDecorator decorator, final Object page, final Class<?> proxyIn) {
final Map<String, List<WebElement>> locatedFields = new HashMap<>();
final Field[] fields = proxyIn.getDeclaredFields();
for (final Field field : fields) {
final Object value = decorator.decorate(page.getClass().getClassLoader(), field);
if (value != null) {
try {
field.setAccessible(true);
field.set(page, value);
if (locatedFields.containsKey(field.getName())) {
if (!(value instanceof List)) {
locatedFields.get(field.getName()).add((WebElement) value);
}
} else {
if (!(value instanceof List)) {
locatedFields.put(field.getName(), new ArrayList<WebElement>(Arrays.asList((WebElement) value)));
}
}
} catch (final IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
return locatedFields;
}
}
Now we can add the following to our JQueryHomePageTest:
@Test
public void getText() {
Assert.assertThat("jQuery\nis a new kind of JavaScript Library.",
Matchers.equalToIgnoringCase(homePage.getTagline().getText()));
}
One thing to note: this will work with inheritance (on fields with the same name) as long as you annotate child elements with @OptionalElement.
In yesterday’s post we discussed ways to allow Selenium to better handle items which may or may not have been on the page. We ended the post with something that might be considered a bit of a nuisance when attempting to utilize one of these non-located elements. Let’s take a look at the two tests we ended the post with:
@Test
public void ensureNullValue() {
Assert.assertNull("A null value is expected for optional elements which do not exist on the page.", homePage.getOptionalIntroDivWithDefault());
}
@Test(expected = NoSuchElementException.class)
public void ensureNoSuchElementExceptionThrown() {
homePage.getOptionalIntroDivWithDefault().getText();
}
Unfortunately the way proxies work in Java our proposed solution yesterday will not allow both of these two tests to pass. So, how can we ensure that the item is not located and retain the information from the NoSuchElementException?
Well, the easiest way is to create our own implementation of a WebElement. Here’s a portion of the class below:
public class MissingWebElement implements WebElement {
private final NoSuchElementException nsee;
public MissingWebElement(final NoSuchElementException nsee) {
this.nsee = nsee;
}
@Override
public void click() {
throw nsee;
}
//Throw the NoSuchElementException for all methods the interface requires us to implement
}
It’s okay, you can admit it; you thought we were going to name that class CustomWebElement didn’t you?
Now we need to modify the CustomFieldDecorator to return a MissingWebElement when we have an optional item that was not located. We need to simply replace our decorate method from yesterday’s post, to what is shown below:
@Override
public Object decorate(final ClassLoader loader, final Field field) {
if (!(WebElement.class.isAssignableFrom(field.getType()) || isDecoratableList(field))) {
return null;
}
final CustomElementLocator locator = (CustomElementLocator) factory.createLocator(field);
if (locator == null) {
return null;
}
if (WebElement.class.isAssignableFrom(field.getType())) {
final WebElement proxy = proxyForLocator(loader, locator);
try {
proxy.isDisplayed();
return proxy;
} catch (final NoSuchElementException nsee) {
if (!locator.isOptionalElement()) {
throw nsee;
}
return new MissingWebElement(nsee);
}
} else if (List.class.isAssignableFrom(field.getType())) {
return proxyForListLocator(loader, locator);
} else {
return null;
}
}
We now instruct our test to expect to receive a MissingWebElement:
@Test
public void ensureMissingElement() {
Assert.assertTrue("A MissingWebElement is expected for optional elements which do not exist on the page.", homePage.getOptionalIntroDivWithDefault() instanceof MissingWebElement);
}
One of the criticisms (from A Smattering of Selenium 107) of the PageObject pattern default PageFactory is that we are never certain if an item is on the page until attempting to utilize the item. In today’s post, we will address this shortcoming. This will be another code-heavy post.
We needed to know immediately if an item was not on a page, if it was a required field; to do that we decided to further extend our PageFactory (as seen in Custom Locators Parts 1 & 2). We decided to keep this information in the metadata of the fields, just as was done with the FindBy annotation. Here is the new annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface OptionalElement {
}
This is simply a marker annotation, we only utilize it when we have an Optional Item, which is why we didn’t have a boolean value here to designate whether or not the item was required. We default any field without this as required.
To utilize this functionality, we are now blowing away the usage of the DefaultFieldDecorator, so here is the updated version of our CustomFieldDecorator. Note: this is almost a verbatim copy of the DefaultFieldDecorator. We simply check to see if an item is on the page as soon as we finish loading the page, and we had to modify the proxyForListLocator to allow for our JQueryLocator to work properly.
public class CustomFieldDecorator extends DefaultFieldDecorator implements FieldDecorator {
public CustomFieldDecorator(final CustomElementLocatorFactory factory) {
super(factory);
}
@Override
public Object decorate(final ClassLoader loader, final Field field) {
if (!(WebElement.class.isAssignableFrom(field.getType()) || isDecoratableList(field))) {
return null;
}
final CustomElementLocator locator = (CustomElementLocator) factory.createLocator(field);
if (locator == null) {
return null;
}
if (WebElement.class.isAssignableFrom(field.getType())) {
final WebElement proxy = proxyForLocator(loader, locator);
try {
proxy.isDisplayed(); //This will break your initialization if the element is not located & the item is not optional
return proxy;
} catch (final NoSuchElementException nsee) {
if (!locator.isOptionalElement()) {
throw nsee;
}
return null;
}
} else if (List.class.isAssignableFrom(field.getType())) {
return proxyForListLocator(loader, locator);
} else {
return null;
}
}
private boolean isDecoratableList(final Field field) {
if (!List.class.isAssignableFrom(field.getType())) {
return false;
}
// Type erasure in Java isn't complete. Attempt to discover the generic type of the list.
final Type genericType = field.getGenericType();
if (!(genericType instanceof ParameterizedType)) {
return false;
}
final Type listType = ((ParameterizedType) genericType).getActualTypeArguments()[0];
if (!WebElement.class.equals(listType)) {
return false;
}
if (field.getAnnotation(FindBy.class) == null && field.getAnnotation(FindBys.class) == null
&& field.getAnnotation(JQueryLocator.class) == null) {
return false;
}
return true;
}
}
We will now add a field to our JQueryHomePage which will never be found and test it. Here is the field:
@FindBy(id = "jq-intro_DOES_NOT_EXIST") @OptionalElement private WebElement optionalIntroDivWithDefault;
And here is our new test, which will now pass.
@Test
public void ensureNullValue() {
Assert.assertNull("A null value is expected for optional elements which do not exist on the page.",
homePage.getOptionalIntroDivWithDefault());
}
We can now receive null for our WebElements which we expected to be proxied. This makes us pretty happy, however we think we can go a step further. For example, a lot of context is lost in the following example:
@Test(expected = NoSuchElementException.class)
public void ensureNoSuchElementExceptionThrown() {
homePage.getOptionalIntroDivWithDefault().getText();
}
We will receive a NullPointerException in this case, instead of our desired NoSuchElementException. In our next post we hope to fix this aggravation.
Through discussions between the QA group and developers at our corporation, it has become apparent that there are terminology and ideological differences between these two groups. This is compounded when an automation group begins testing. What exactly should the automation group work on? Who should be writing unit tests? What exactly are unit tests?! I will attempt to answer some of these questions, at least from the automation perspective. I would love to hear feedback on this, so please comment with your thoughts.
What is a unit test? According to Wikipedia, it’s “a method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine if they are fit for use.” So… what does that mean? To me, a unit test is a test that tests a small module or class that ALSO can be run in isolation. A unit test is a test that mocks interactions with outside services, so that it can be run without network access, database access, etc.
OK, that seems pretty straight-forward. What then is a test that tests some small module or class but also hits external or internal services? This would be an integration test. The test is testing the INTEGRATION of the components, by testing their interaction.
So with those definitions out of the way, who should write unit tests? The developers of the code in question should. It also isn’t out of the question that they should be responsible for some integration testing. After all, how can they be sure that their code works as expected without seeing how it interacts with the rest of the system?
Well, if developers are to write unit tests AND integration tests, what should the QA automation group be doing? The obvious and not very helpful answer is, automating QA tasks. One example would be ensuring that some module on a web page got rendered correctly using a tool like Selenium. This module could be static content, or it could be populated by service endpoints. Tests through the front-end test essentially ALL parts of an application, from the database, to web services, to whatever caching procedure is in place, and potentially any CDN in place. Provided these tests are modular and well-designed, code reuse is high and benefit is high, too. If the tests run on a CI server whenever a build is performed, in addition to the unit and integration tests written by developers, many issues will be exposed before the code is ever deployed. Then, QA is free to hand test the things that aren’t easily covered via automated interactions with Selenium.
That all sounds fine and good, but what about tests that aren’t quite covered by the above? Well, the demarcation isn’t black and white, between QA automation and developers. This is when you will have to meet with the disparate groups, and come to an agreement on what makes the most sense in your organization. The first step is to get everyone on the same page and make sure the definitions between the groups are the same, and hopefully this post will prove helpful in that regard.
Now that we’ve developed our “custom” PageFactory, we need to create create a couple of more classes before our annotation is fully usable. The first of which is the CustomElementLocatorFactory. It will be responsible for telling the PageFactory to use our CustomElementLocator, which we will go over in just a minute. Here is the implementation for our Factory method:
public class CustomElementLocatorFactory implements ElementLocatorFactory {
private final WebDriver driver;
public CustomElementLocatorFactory(final WebDriver driver) {
this.driver = driver;
}
@Override
public ElementLocator createLocator(final Field field) {
if (field.isAnnotationPresent(JQueryLocator.class)) {
return new CustomElementLocator((JavascriptExecutor) driver, field);
} else {
return new DefaultElementLocator(driver, field);
}
}
}
Once we’ve completed our CustomElementLocator we are good to go. Here’s the sample implementation:
public class CustomElementLocator implements ElementLocator {
private final JavascriptExecutor driver;
private final JQueryLocator jql;
public CustomElementLocator(final JavascriptExecutor driver, final Field field) {
this.driver = driver;
this.jql = field.getAnnotation(JQueryLocator.class);
}
@Override
public WebElement findElement() {
return (WebElement) driver.executeScript("return $" + jql.$() + "[0];");
}
@SuppressWarnings("unchecked") @Override
public List<WebElement> findElements() {
return (List<WebElement>) driver.executeScript("return $" + jql.$());
}
}
We now can utilize the new annotation in any of our Page Objects, and have it work with our custom PageFactory. Here is a sample PageObject:
public class JQueryHomePage {
private final WebDriver driver;
@JQueryLocator($ = "('#jq-intro')")
private WebElement introDivWithJQuery;
@FindBy(id = "jq-intro")
private WebElement introDivWithDefault;
public JQueryHomePage(final WebDriver driver) {
this.driver = driver;
driver.get("http://jquery.com/");
CustomPageFactory.initElements(driver, this);
}
public WebElement getIntroDivWithJQuery() {
return introDivWithJQuery;
}
public WebElement getIntroDivWithDefault() {
return introDivWithDefault;
}
}
And here is a simple class to test that things are working as expected:
public class JQueryHomePageTest {
private static WebDriver driver;
private static JQueryHomePage homePage;
@BeforeClass
public static void setup() {
driver = new FirefoxDriver();
homePage = new JQueryHomePage(driver);
}
@Test
public void ensureItemsAreSame() {
Assert.assertThat(homePage.getIntroDivWithJQuery().equals(homePage.getIntroDivWithDefault()), Matchers.is(true));
}
@AfterClass
public static void shutdown() {
driver.quit();
}
}
I’ll first state that I love the PageObject pattern, but I’ll admit there is one thing I’d like to see done differently to make it a bit more extensible. Getting the browser to locate the vast majority of items is actually quite simple, but there will come a time when you may need another way to get an element on the screen. In our tests we really want to avoid using XPATH, so we have removed it from nearly all of our code; this is mostly due to speed concerns (although to be fair I have not profiled the solution against XPATH), however it is also avoided because it more closely ties our code to how the page currently exists in absolute form. Let’s take a look at how we would typically find an element on one of our pages:
@FindBy(id = "jq-intro") private WebElement defaultIntroDiv;
I wish, that the How enum in this case was responsible for implementing the ElementLocator interface. This would allow us to modify the @FindBy annotation to utilize an ElementLocator instead of the How enum. We could then create new implementations quite easily, as we would really only need to create one concrete implementation (this would require some changes to core of selenium). As it stands, multiple classes were needed to extend this functionality.
I’m surprised I haven’t seen much out there about custom PageFactory implementations (I feel this is mostly due to the majority of our needs being met out of the box). It became apparent that if we were going to avoid using XPath, we would need to utilize jQuery in some places. Since all of our pages already include jQuery, this was quite straight-forward. The example you are about to see could be made a bit more robust, by adding in jQuery to the page if it isn’t detected, but I’ll leave that as an exercise for the reader since it’s beyond our needs.
The first thing we did was create a new annotation.
public @interface JQueryLocator {
String $();
}
We deliberately broke the best practice on annotations, that single value annotations should have a single attribute name value, to clearly trigger our minds to use jQuery syntax on our elements. We can now annotate our fields in the following manner:
@JQueryLocator($ = "('#jq-intro')")
private WebElement jQueryIntroDiv;
We now need to tell Selenium how to locate these items. The easiest way to do this is to create the following classes: CustomPageFactory, CustomElementLocatorFactory, and CustomFieldDecorator (you can name them what you want).
The first is the CustomPageFactory, we need to make it aware of how to find the elements with jQuery, we follow a similar pattern with our selenium tests, whereby we call the PageFactory in the constructor. This allows us to keep our custom PageFactory quite short. In fact, it can be written in four lines:
public class CustomPageFactory {
public static void initElements(final WebDriver driver, final Object page) {
initElements(new CustomElementLocatorFactory(driver), page);
}
public static void initElements(final ElementLocatorFactory factory, final Object page) {
PageFactory.initElements(new CustomFieldDecorator(factory), page);
}
}
public class CustomFieldDecorator extends DefaultFieldDecorator implements FieldDecorator {
public CustomFieldDecorator(final CustomElementLocatorFactory factory) {
super(factory);
}
@Override
public Object decorate(final ClassLoader loader, final Field field) {
if (!(WebElement.class.isAssignableFrom(field.getType()) || isDecoratableList(field))) {
return null;
}
final CustomElementLocator locator = (CustomElementLocator) factory.createLocator(field);
if (locator == null) {
return null;
}
if (WebElement.class.isAssignableFrom(field.getType())) {
final WebElement proxy = proxyForLocator(loader, locator);
try {
proxy.isDisplayed();
return proxy;
} catch (final NoSuchElementException nsee) {
if (!locator.isOptionalElement()) {
throw nsee;
}
return new MissingWebElement(nsee);
}
} else if (List.class.isAssignableFrom(field.getType())) {
return proxyForListLocator(loader, locator);
} else {
return null;
}
}
private boolean isDecoratableList(final Field field) {
if (!List.class.isAssignableFrom(field.getType())) {
return false;
}
// Type erasure in Java isn't complete. Attempt to discover the generic type of the list.
final Type genericType = field.getGenericType();
if (!(genericType instanceof ParameterizedType)) {
return false;
}
final Type listType = ((ParameterizedType) genericType).getActualTypeArguments()[0];
if (!WebElement.class.equals(listType)) {
return false;
}
if (field.getAnnotation(FindBy.class) == null && field.getAnnotation(FindBys.class) == null && field.getAnnotation(JQueryLocator.class) == null) {
return false;
}
return true;
}
@Override
protected WebElement proxyForLocator(final ClassLoader loader, final ElementLocator locator) {
final InvocationHandler handler = new CustomLocatingElementHandler((CustomElementLocator) locator);
WebElement proxy;
proxy = (WebElement) Proxy.newProxyInstance(loader, new Class[] { WebElement.class, WrapsElement.class, Locatable.class }, handler);
return proxy;
}
}
You can find the implementation of the MissingWebElement here. In our next post we will show how to create the CustomElementLocatorFactory and the CustomFieldDecorators to use in conjunction with this PageFactory.
