Let us implement the autocomplete functional test for Google search page, when you type, o,r,a,c,l,e, it popup some matching words or phrases in the flyout area and you can select the one you are looking for.
There are 5 actions in autocomplete,
Find the input box
Send keys one by one
Try to find the suggestion you are looking for in the suggestion area
Click the suggested item
Explicitly wait for it
The input field for us to enter criteria is named "q", so you can use By.name("q") to find it and type the characters of Oracle one by one into it and wait for "Oracle" appears in the selection area and click it.
It is difficult to locate the suggestion items since there is no obvious selection criteria to find them.
You can use Firebug to find its absolute xpath, to get some idea but it can be used in the tests since the structure of the page may change and it will not work,
/html/body/table/tbody/tr/td[2]/table/tbody/tr/td/div/table/tbody/tr/td/span
If you are not familiar with xpath, you can use complex search to find a container first and from the container, find the element. This autocomplete suggestion is inside a table with class "gssb_c", so we can
findElement of the table first, and the
findElements of all the span elements inside that table and
find the one with the text "oracle" you try to type in,
// ☹ this is a bad example, please don't follow the style.
try {
WebElement table = webDriver.findElement(By.className("gssb_c"));
List<WebElement> spans = table.findElements(By.tagName("span"));
WebElement oracle = find(spans,
new Predicate<WebElement>() {
@Override
public boolean apply(WebElement span) {
return span.getText().equals("oracle");
}
});
oracle.click();
} catch (NoSuchElementException e) {
e.printStackTrace();
}
This is not the complete logic, it is just the part to find the "oracle" from the suggestion list, it is already very verbose.
Selenium capsules, a new Selenium framework, uses a
locator pattern to abstract the element locating logic to make the test code cleaner, in the autoCompleteUsingLocator test from following code block.
Note, () -> className("gssb_c") is a lambda expression introduced in Java 8.
// ☺ This is a good example.
new ElementTryLocator<AbstractPage>(() -> className("gssb_c"))
.and(new ElementsLocator<>(SPAN))
.and(new FirstMatch<>(TEXT.and(new IsStringEqual("oracle")))));
It is a little bit cleaner, but still very complex.
After rewriting the search using By xpath, it only has one findElement method call, which is much better than the navigational locating strategy, so you can greatly simplify the test if you know how to use xpath.
// ☹ this is a bad example, please don't follow the style.
try {
WebElement oracle = webDriver.findElement(
By.xpath("//table[contains(concat(' ', @class, ' '), 'gssb_c')]/descendant::span[text()='oracle']");
oracle.click();
} catch (NoSuchElementException e) {
e.printStackTrace();
}
But it is still not clean if the By.xpath call appears multiple times in different tests,
By.xpath("//table[contains(concat(' ', @class, ' '), 'gssb_c')]/descendant::span[text()='oracle']");
By using Selenium Capsules framework, you can actually put this xpath string literal inside an enum and use the enum in the tests,
// ☺ This is a good example.
import org.openqa.selenium.By;
import java.util.function.Supplier;
import static org.openqa.selenium.By.xpath;
public enum Xpath implements Supplier<By> {
DIV_CONTAINER_ID("//div[@id='container']"),
ORACLE_AUTOCOMPLETE("//table[contains(concat(' ', @class, ' '), 'gssb_c')]/descendant::span[text()='oracle']"),
QUANTITY("//div[@id='ys_cartInfo']/descendant::input[@name='cartDS.shoppingcart_ROW0_m_orderItemVector_ROW0_m_quantity']");
private final By by;
private Xpath(String id) {
this.by = xpath(id);
}
@Override
public By get() {
return by;
}
@Override
public String toString() {
return by.toString();
}
}
Now the test can be rewritten as simple as one line of code,
// ☺ This is a good example.
@Test
public void autoCompleteUsingXpath() {
googlePage.autocomplete(Q, "oracle", new ElementTryLocator<>(ORACLE_AUTOCOMPLETE));
}
Even you don't want to use a framework, putting locating strategy in enum can still simplify code, unlike the above code, you can just use ORACLE_AUTOCOMPLETE as a parameter, you need to call get() method to get a By object, but it is still cleaner than using xpath string literals directly in the code,
// ☺ This is an OK example.
try {
WebElement oracle = webDriver.findElement(ORACLE_AUTOCOMPLETE.get());
oracle.click();
} catch (NoSuchElementException e) {
e.printStackTrace();
}
versus original,
// ☹ this is a bad example, please don't follow the style.
try {
WebElement oracle = webDriver.findElement(
By.xpath("//table[contains(concat(' ', @class, ' '), 'gssb_c')]/descendant::span[text()='oracle']");
oracle.click();
} catch (NoSuchElementException e) {
e.printStackTrace();
}
Now let us have a comparison between the tests written with and without using framework,
1. autoCompeleteUsingSelenium doesn't use framework, it uses Selenium directly.
2. autoCompleteUsingXpath uses framework,
Cohesively, all By selectors can be organized into one package, selectors to encourage people reuse existing definitions, the name of that package should be bysuppliers, I found out selectors is a more meaningful name since that's the purpose of those classes.
By Selector Suppliers and all its member classes are enum types implementing Supplier
interface to provide a by selector for Selenium WebDriver or WebElement, this is explained in this blog entry, functional selenium