Wrapping WebElement 1
Basic Wrapping
December 08, 2012I'm starting a series called "Wrapping WebElement" that takes you through my process of creating selophane,
a library that wraps WebElements
with functionality. Work on the library is ongoing, with the codebase opening up by New Year's Day. But documentation and testing must come before a public release.
We start with the basics of wrapping a WebElement
.
Wrapping is a simple way to provide functionality. Selenium WebDriver comes with a class called Select which provides functionality common to drop down select/option/optiongroup HTML tags. It works by taking in its constructor a WebElement
and returning functionality similar to the way a dropdown works.
Their implementation has two defects:
- Even though they do provide functionality of a dropdown, they lose the functionality of being able to access the underlying
WebElement
. Of course, you still have theWebElement
, so you can still interact directly. - Their
PageFactory
can't generate alternative classes forWebElement
s because it compares everything to the basicWebElement
interface. It always results in aWebElement
, but it's the one created by your current WebDriver, not our customized class.
Select is also a one-off case in the selenium project, unfortunately. They don't provide many wrappers, and it leads to a bit of frustration when you are limited to the basic WebElement
commands for each HTML tag in your project.
This section may look a bit trivial and like so much Java cruft to you, but it really does help to fix the public interface in place. WebDriver really only understands things in the case where they can be cast to one of its interfaces. So goes the form of this implementation.
We need to establish a few new interfaces before we get into this exercise. I don't do it to be a jerk, but because of the way WebDriver treats them versus implementations. Some thinking went into this first interface, it's a real doozy. OK, it's a placeholder.
public interface Element extends WebElement, WrapsElement, Locatable {
boolean elementWired();
}
(This is named 'Element' because it's shorter than WebElement
, and we're in a 'domain' of 'web' by the nature of the library.)
Just so we don't end up repeating ourselves a thousand times over, it's good to have a base implementation to extend. It will cover the base cases, which is all of the functionality of WebElement
.
public class ElementImpl implements Element {
private final WebElement element;
public ElementImpl(final WebElement element) {
this.element = element;
}
@Override
public void click() {
element.click();
}
@Override
public void sendKeys(CharSequence... keysToSend) {
element.sendKeys(keysToSend);
}
@Override
public void etcetera() {
// there's a lot of wrapping going on in this file. See the WebDriver
// interface for details.
}
}
Here, we simply use the Delegation Pattern to let the inner
WebElement do the work. We can always disable functionality as needed, but the WebElement interface requires these
methods to be implemented.
A good example of a wrapped WebElement
is the check-box. Checkboxes should do the following (written in
interface form):
public interface CheckBox extends Element {
void toggle();
void check();
void uncheck();
boolean isChecked();
}
The verbs to use on a checkbox are easy to understand. The implementation follows. As you will see,
extending ElementImpl
reduces code in each new element.
public class CheckBoxImpl extends ElementImpl implements CheckBox {
public CheckBoxImpl(WebElement element) {
super(element);
}
public void toggle() {
getWrappedElement().click();
}
public void check() {
if (!isChecked()) {
toggle();
}
}
public void uncheck() {
if (isChecked()) {
toggle();
}
}
public boolean isChecked() {
return getWrappedElement().isSelected();
}
}
The implementation is pretty short. It only adds the features of a checkbox, and little more is needed. An example follows of this (primitive) functionality:
public class SimpleForm {
@FindBy(id = "checkbox")
WebElement checkBox;
@Test
public void simple() {
WebDriver driver = new FirefoxDriver();
PageFactory.initElements(driver, this);
PageLoader.get(driver, "forms.html");
CheckBox wrappedCheckBox = new CheckBoxImpl(checkBox);
Assert.assertFalse(wrappedCheckBox.isChecked());
wrappedCheckBox.check();
Assert.assertTrue(wrappedCheckBox.isChecked());
driver.close();
}
}
This is clearly more cumbersome than it needs to be:
- You still have to find elements as class
WebElement
. There's no factory for this. - You need to wrap things yourself.
Conclusion
Wrapping a WebElement
adds appropriate domain methods to an object, but this approach is inefficient. We now need
to automate creation of the wrapped objects so we don't have to hand-craft every relation. This is ugly:
Checkbox cb = new CheckBox(checkBoxWebElement);
cb.check();
What we really want is this:
// in the page object:
@FindBy(id='checkbox1')
public Checkbox cb;
// in test code:
page.cb.check();
Part 2 of this series discusses exactly how to get there.