I was recently automating an internal sales tool used at Yell when I came across this particular problem. I needed to hover over the navigation bar and then click on one of the options that drops down (see Play.com for a working example).
This requires use of Selenium’s Advanced User Interactions API – which was new to me – but this was only the start of my problems as we will discover.
The scenario
We have some HTML that looks like this:
Note: this probably isn’t syntactically correct but you can see that the navigation bar is an unordered list of unordered lists. The dropdown menu is initially hidden from view (display:none).
<div id="navBar">
<ul>
<li>
<a href="http://customer"> My Customers </a>
<ul id="dropDownMenu" style="display:none;">
<li>
<a id="searchCustomer" href="http://searchcustomer">Search</a>
</li>
…
Once you hover over the main link, the HTML transforms, exposing the hidden links (display:block):
<div id="navBar">
<ul>
<li>
<a href="http://customer"> My Customers </a>;
<ul id="dropDownMenu" style="display:block;">
<li>
<a id="searchCustomer" href="http://searchcustomer">Search</a>
</li>
…
Start creating a solution
I wanted to solve this problem and at the same time create a helper method for the framework that was flexible, powerful and reusable. The method declaration would look something like this:
public void clickHiddenMenuBarItem(String menuXpath, String menuItemXpath, long timeout)
In the interest of best practice, I try to ensure that all tests make frequent use of helper methods which typically include targeted explicit waits. That way tests are much more readable with the complexity hidden away and it also reduces the likelihood of testers making those simple but frustrating programming errors.
So clickHiddenMenuBarItem as the name implies, would click on a hidden menu bar link which must be revealed by hovering over the menu link first.
So let’s build two explicit waits for the main link and the hidden link (note: the latter is hidden from the user and Selenium’s direct interactions but you can still verify its existence).
NB: due to my setup (I use Spring to run my code), the Selenium commands are missing the prefix driver.. Similarly, you would need to use driver instead of webDriver() for your WebDriver instance (or whatever you named it).
public void clickHiddenMenuBarItem(final String menuXpath, final String menuItemXpath, long timeout) {
// Explicit wait for the menu
WebElement menu = (new WebDriverWait(webDriver(), timeout)).until(new ExpectedCondition() {
public WebElement apply(WebDriver d) {
return d.findElement(By.xpath(menuXpath));
}
});
// Explicit wait for the menu option
WebElement menuOption = (new WebDriverWait(webDriver(), timeout)).until(new ExpectedCondition() {
public WebElement apply(WebDriver d) {
return d.findElement(By.xpath(menuItemXpath));
}
});
}
These are your standard explicit waits as defined by the documentation. As each wait makes use of an inner class we must declare the xpath arguments as final.
You might want to extend the piece of code above to check the visibility of the elements, as this helped me to diagnose problems later on:
System.out.println("Menu displayed: " + menu.isDisplayed());
System.out.println("Menu option displayed: " + menuOption.isDisplayed());
This should return true and false respectively.
Introducing the Advanced User Interactions API
Now that piece of code is working fine, we can move onto making use of the Advanced User Interactions API. This is where I got a little stuck and where isDisplayed() came in handy.
According to the documentation you should be able to do something like this:
// Configure the action Actions builder = new Actions(webDriver()); builder.moveToElement(menu) .click(menuOption); // Get the action Action clickMenuOption = builder.build(); // Perform the action clickMenuOption.perform();
This compiled fine for me, ran without errors but nothing seemed to happen. The mouse didn’t hover on the menu, even when I stepped through it using the debugger. So I tried a variant on this, which is available from the same link above:
// Configure the action Actions builder = new Actions(webDriver()); // Get and build the action Action clickMenuOption = builder.moveToElement(menu) .click(menuOption) .build(); // Perform the action clickMenuOption.perform();
Again nothing was happening. After some searching around I found that someone had had a related problem. Essentially, you need to perform the click() outside of the action chain. So here’s the code:
Actions builder = new Actions(webDriver()); builder.moveToElement(menu).perform(); menuOption.click();
I tried this…and it almost worked. The mouse certainly hovered on the link but the code fell over as there is a tiny (<500ms) delay where the menu option becomes 'un-hidden'. To achieve this diagnosis I put the earlier isDisplayed() code along with a small static wait in-between builder.moveToElement(menu).perform() and menuOption.click()
Now I knew what the problem was I needed to find a neat and tidy way of structuring this wait.
Using the ExpectedConditions class
This question of a neat and tidy wait had me stumped for some time. I knew that you could use an explicit wait whilst waiting for the existence of an element – but I had already proved that in my previous code. I needed a way of somehow combining the visibility of an element with a wait (and importantly, elegantly).
An implicit wait for 1 or 2 seconds worked – manage().timeouts().implicitlyWait(1, TimeUnit.SECONDS) but it is bad practise to combine explicit and implicit waits. Besides, the implicit wait would live for the life of the WebDriver instance which is unacceptable. I almost resorted to Thread.sleep(500) to achieve the desired effect. Finally, I found the Selenium ExpectedConditions class. I needed to know when, after hovering on the menu, the menu option became visible and clickable – and elementToBeClickable was a perfect match. So simply put that condition as part of a WebDriverWait statement like so:
Actions builder = new Actions(webDriver()); builder.moveToElement(menu).perform(); // hover new WebDriverWait(webDriver(), 10).until(ExpectedConditions.elementToBeClickable(By.xpath(menuItemXpath))); menuOption.click();
Test the solution
This code worked for my application but when I tested it on Play.com – which has a very similar navigation bar – it didn’t work. I had a hunch that my code was failing due to being “too slow” in identifying when the hidden element was clickable and acting upon it.
There are two possible solutions:
- Use a different WebDriverWait constructor to specify the element polling interval (measured in milliseconds).
- Use a FluentWait which works similarly to WebDriverWait (which in fact extends FluentWait) but gives you a little more flexibility.
new WebDriverWait(webDriver(), 10, 50).until(ExpectedConditions.elementToBeClickable(By.xpath(menuItemXpath)));
new FluentWait(webDriver()) .withTimeout(timeout, TimeUnit.SECONDS) .pollingEvery(50, TimeUnit.MILLISECONDS) .ignoring(NoSuchElementException.class) .until(ExpectedConditions.elementToBeClickable(By.xpath(menuItemXpath)));
Whilst the FluentWait offers more flexibility – in particular the ability to choose the WebDriver exception to ignore – in this case I opted for the more finely-tuned WebDriverWait as it achieved what I needed in one line of code.
Full code
/**
* Click a hidden menu bar item (AKA hover and click) using a configurable wait.
*
* @param menuXpath
* The xpath of the main menu parent item.
* @param menuItemXpath
* The xpath of the hidden/drop-down menu item.
* @param timeout
* The wait timeout (10 is recommended).
*/
public void clickHiddenMenuBarItem(final String menuXpath, final String menuItemXpath, long timeout) {
// Explicit wait for up to 10 seconds for the menu
WebElement menu = (new WebDriverWait(webDriver(), timeout)).until(new ExpectedCondition<WebElement>() {
public WebElement apply(WebDriver d) {
return d.findElement(By.xpath(menuXpath));
}
});
// Explicit wait for up to 10 seconds for the menu option
WebElement menuOption = (new WebDriverWait(webDriver(), timeout)).until(new ExpectedCondition<WebElement>() {
public WebElement apply(WebDriver d) {
return d.findElement(By.xpath(menuItemXpath));
}
});
/*
* Use the Advanced User Interactions API to perform a hover and click.
* A WebDriverWait utilising elementToBeClickable() is required between the hover and click.
*/
Actions builder = new Actions(webDriver());
builder.moveToElement(menu).perform(); // hover
new WebDriverWait(webDriver(), 10, 50).until(ExpectedConditions.elementToBeClickable(By.xpath(menuItemXpath)));
menuOption.click();
}