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