Saturday, December 8, 2012

Floating Elements in WebDriver

One of the tricky parts of using WebDriver is the design philosophy.  The framework is designed to emulate user behavior, but occasionally fails to match normal human intelligence.  I'll be posting a couple of articles on examples of typical problems when using Selenium WebDriver and suggestions for how to overcome them.  In my examples, I'll showing code that works in ChromeDriver (since I work on a Mac, and Firefox doesn't parallelize as well due to ephemeral port exhaustion).

The first problem: floating elements.  Floating banners, footers, advertisements, or information boxes are somewhat common on sites that want to provide a more dynamic or unified interface.  Unfortunately, when a floating element covers a target element that you wish to interact with.  Browser drivers are pretty adept at detecting when an element is off-page.  In these cases, the drivers automatically scrolls to make the element visible (at least, as of v2.26.0 of Selenium)... but only visible enough to click. If the element is below the bottom of the visible window, then it will automatically be scrolled into view at the bottom of the window.  In this case, it can often be obscured behind a floating footer.

If an element is covered by a floating element, a WebDriverException is generated when the driver tries to click on it; generally something like the following:
Element is not clickable at point (355, 389.5). Other element would receive the click: <a class="ui-corner-all" href="http://www.blogger.com/blogger.g?blogID=9129951650727084756" tabindex="-1">...</a>
There are a few possible strategies one could use at this point:
  • Scroll to the given element whenever you encounter the problem.  This has the disadvantage of adding lots of scrolling to your code, with no guarantee that you'll solve for all cases - if you run your test in a smaller window, new scripts might break, for example.
  • Remove floaters before you start testing, using JavaScript or custom versions of the pages.  This method has the disadvantage of altering the system under test.
  • Use a custom extension of WebElement that catches the error and responds appropriately.  This solution can work globally, without altering the system under test, but does mean that you'll need to find a way to provide these elements without manually converting every element that comes out of a PageFactory or WebDriver, by extending WebDriver, for example.
Here's a sample ScrollingRemoteWebElement that shows a possible fix.  In this case, the WebElement responds to the exception by trying to scroll the element to the center of the page, if possible.

public class ScrollingRemoteWebElement extends RemoteWebElement {

    private static final String VISIBILITY_EXCEPTION = "Element is not clickable at point";

    public ScrollingRemoteWebElement(RemoteWebElement initializingElement) {
        super();
        this.setId(initializingElement.getId());
        this.setParent((RemoteWebDriver) initializingElement.getWrappedDriver());
        this.setFileDetector(parent.getFileDetector());
    }

    @Override
    protected Response execute(String command, Map<String, ?> parameters) {
        try {
            return super.execute(command, parameters);
        } catch (WebDriverException webDriverException) {
            if (isVisibilityProblem(webDriverException)) {
                scrollToElement();
                return super.execute(command, parameters);
            } else {
                throw webDriverException;
            }
        }
    }

    private boolean isVisibilityProblem(WebDriverException webDriverException) {
        String message = webDriverException.getMessage();
        return (message != null
                && message.contains(VISIBILITY_EXCEPTION));
    }

    private void scrollToElement() {
        Integer scrollHeight = getHeightPlacingElementInWindowCenter();
        String script = "window.scrollTo(0," + scrollHeight + ")";
        parent.executeScript(script);
    }

    private Integer getHeightPlacingElementInWindowCenter() {
        Dimension windowSize = parent.manage().window().getSize();
        Integer halfWindow = windowSize.getHeight() / 2;
        Integer scrollHeight = this.getLocation().getY() - halfWindow;
        return (scrollHeight > 0) ? scrollHeight : 0;
    }
}

This element could be improved by adding scrolling on the x-axis to center element before retrying the initial command, but this shows the general strategy. It doesn't solve for elements floating in the center of the page, but one hopes that such an annoyingly screen-grabbing element will not be present one's system under test.

As always, I welcome feedback, especially better solutions than those I've scraped together.