When an element is out of view with selenium and one tries to interact with it, selenium will usually scroll the element into view first implicitly. This is great except that what is annoying is that it usually puts in the element just enough into view. What I mean is that if the element is below the window, it will scroll down enough just till when the element is just bordering the edge of the window.
Usually this is fine, but when working on a web site with borders around it, this will lead to numerous of these kinds of errors
Selenium::WebDriver::Error::UnknownError:
unknown error: Element is not clickable at point (438, 747). Other element would receive the click: <body>...</body>
Because usually the border of the web page is over it, but will try to click the element anyway. Is there anyway handle this? perhaps to automatically move elements to the center of the screen when out of view? I am thinking along the lines monkey-patching via ruby.
This should work in order to scroll element into center of view:
WebElement element = driver.findElement(By.xxx("xxxx"));
String scrollElementIntoMiddle = "var viewPortHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);"
+ "var elementTop = arguments[0].getBoundingClientRect().top;"
+ "window.scrollBy(0, elementTop-(viewPortHeight/2));";
((JavascriptExecutor) driver).executeScript(scrollElementIntoMiddle, element);
Yes, it is possible to automatically scroll the browser such that any element we interact with gets centered in the window. I have a working example below, written and tested in ruby using selenium-webdriver-2.41.0 and Firefox 28.
Full disclosure: You might have to edit parts of your code slightly to get this to work properly. Explanations follow.
Selenium::WebDriver::Mouse.class_eval do
# Since automatic centering of elements can be time-expensive, we disable
# this behavior by default and allow it to be enabled as-needed.
self.class_variable_set(:@@keep_elements_centered, false)
def self.keep_elements_centered=(enable)
self.class_variable_set(:@@keep_elements_centered, enable)
end
def self.keep_elements_centered
self.class_variable_get(:@@keep_elements_centered)
end
# Uses javascript to attempt to scroll the desired element as close to the
# center of the window as possible. Does nothing if the element is already
# more-or-less centered.
def scroll_to_center(element)
element_scrolled_center_x = element.location_once_scrolled_into_view.x + element.size.width / 2
element_scrolled_center_y = element.location_once_scrolled_into_view.y + element.size.height / 2
window_pos = @bridge.getWindowPosition
window_size = @bridge.getWindowSize
window_center_x = window_pos[:x] + window_size[:width] / 2
window_center_y = window_pos[:y] + window_size[:height] / 2
scroll_x = element_scrolled_center_x - window_center_x
scroll_y = element_scrolled_center_y - window_center_y
return if scroll_x.abs < window_size[:width] / 4 && scroll_y.abs < window_size[:height] / 4
@bridge.executeScript("window.scrollBy(#{scroll_x}, #{scroll_y})", "");
sleep(0.5)
end
# Create a new reference to the existing function so we can re-use it.
alias_method :base_move_to, :move_to
# After Selenium does its own mouse motion and scrolling, do ours.
def move_to(element, right_by = nil, down_by = nil)
base_move_to(element, right_by, down_by)
scroll_to_center(element) if self.class.keep_elements_centered
end
end
Recommended usage:
Enable automatic centering at the start of any code segments where elements are commonly off-screen, then disable it afterward.
NOTE: This code does not seem to work with chained actions. Example:
driver.action.move_to(element).click.perform
The scrolling fix doesn't seem to update the click
position. In the above example, it would click on the element's pre-scroll position, generating a mis-click.
Why move_to
?
I chose move_to
because most mouse-based actions make use of it, and Selenium's existing "scroll into view" behavior occurs during this step. This particular patch shouldn't work for any mouse interactions that don't call move_to
at some level, nor do I expect it to work with any keyboard interactions, but a similar approach should work, in theory, if you wrap the right functions.
Why sleep
?
I'm not actually sure why a sleep
command is needed after scrolling via executeScript
. With my particular setup, I am able to remove the sleep
command and it still works. Similar examples from other developers across the 'net include sleep
commands with delays ranging from 0.1 to 3 seconds. As a wild guess, I would say this is being done for cross-compatibility reasons.
What if I don't want to monkey-patch?
The ideal solution would be, as you suggested, to change Selenium's "scroll into view" behavior, but I believe this behavior is controlled by code outside of the selenium-webdriver gem. I traced the code all the way to the Bridge
before the trail went cold.
For the monkey-patch averse, the scroll_to_center
method works fine as a standalone method with a few substitutions, where driver
is your Selenium::WebDriver::Driver
instance:
driver.manage.window.position
instead of
@bridge.getWindowPosition
driver.manage.window.size
instead of
@bridge.getWindowSize
driver.execute_script
instead of
@bridge.executeScript
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With