Highlighting the element before any clicks: A foray into the AbstractEventListener

Last week I wrote a post about how you can create a highlight method to help you visually find an element that you have selected. In the comments section, wiiniRyan suggested implementing the highlight method before all clicks occur using one of the Abstract Event Listeners hooks provided in the Selenium::WebDriver::Support module. Only thing was, he couldn’t figure out how to use the hooks according to his comments, and suggested that I might be able to provide some insight.

His comments intrigued me, so I did a little bit of research and hands on testing to figure out how these hooks work.

I looked at the documentation, but didn’t see anything real obvious about how to hook into them. My first attempts simply tried to subclass the AbstractEventListeners class, but that did no good, since the driver had no way to interface with the overridden hooks.

It wasn’t until I read a small crucial line after the #for method call on the main WebDriver documentation page that I figured out how these hooks worked:

One special argument is not passed on to the bridges, :listener. You can pass a listener for this option to get notified of WebDriver events. The passed object must respond to #call or implement the methods from AbstractEventListener

Essentially, you need to create a class with either every method hook defined, or use the generic #call method to delegate the hooks as they occur.

# Either
class AbstractDefineEveryMethodTest
  def before_click *args
    # .. do stuff
  end
  def before_#... rest of the hooks defined
end
# The downside to the above is that every single method
# hook needs to be defined, or else things will blow up
# when the hook tries to be called.
# Or, more efficiently
class AbstractHandleCallMethodTest
  def call *args
    # only need to deal with the hooks you are interested in
    case args.first
    when :before_click
      puts "Clicking #{args[1]}"
      # .. do stuff
    when :before_find
      puts "Finding #{args[1]} => #{args[2]}"
      # .. do other stuff
    end
  end
end

After creating this class, then we just create a new instance of the class and pass it into the initialization of our WebDriver browser:

ael = AbstractHandleCallMethodTest.new
driver = Selenium::WebDriver.for :firefox, :listener => ael

So, now when you find an element and click it, you will see the following output:

driver.find_element(:css => "h1").click # =>
Finding css selector => h1
Clicking #<Selenium::WebDriver::Element:0x000000022f6528>
=> "ok"

So, its a simple matter of adding the highlight method to the AbstractEventListener and using the #call method to call highlight when we want to.

class HighlightAbstractTest
  def highlight element, driver
    orig_style = element.attribute("style")
    driver.execute_script("arguments[0].setAttribute(arguments[1], arguments[2])", element, "style", "border: 2px solid yellow; color: yellow;")
    sleep 1
    driver.execute_script("arguments[0].setAttribute(arguments[1], arguments[2])", element, "style", orig_style)
  end
  def call *args
    case args.first
    when :before_click
      highlight args[1], args[2]
    end
  end
end
hat = HighlightAbstractTest.new
driver = Selenium::WebDriver.for :firefox, :listener => hat
driver.find_element(:css => "h1").click # => Be sure to watch in your browser window for the highlight to appear!
driver.find_elements(:css => "input[type='button']").each{|b| b.click } # => Be sure to watch in your browser window for the highlight to appear!

A couple words of caution:

First, if you are going to define each hook instead of using the recommenended #call method, be sure you define each and every hook inside your class. Otherwise, if that hook attempts to be executed, you will get a nasty error message about not being able to find the method.

Second, not all of the hooks provide the same parameters. In fact, the only hooks to include the element and driver, which are needed for the highlight method to work are the before_click, before_change_value_of (still trying to figure out when that one is triggered), after_click, and after_change_value_of. The other hooks have different parameters according to their respective function, which can be found by RTFM. Don’t get burned when you expect before_find to pass the whole element as a parameter. And I wouldn’t recommend trying to get the actual element inside this hook with the by and what parameters, lest you create an infinite loop.

AbstractEventListener seems like an interesting idea, but is a little bit complicated to set up. Also, I am not sure what other use cases might come from implementing these hooks. If you have some ideas, please share them in the comments!