Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

selenium scroll element into (center of) view

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.

like image 318
mango Avatar asked Nov 23 '13 20:11

mango


2 Answers

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);
like image 52
Prakash Saravanan Avatar answered Sep 17 '22 11:09

Prakash Saravanan


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
like image 23
Ben Amos Avatar answered Sep 17 '22 11:09

Ben Amos