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!

Did I select the right element?

Time and time again, I will find unexpected behavior in my tests (either from an error, or the wrong text being displayed, etc), and I need to fallback to determining if I’m even using the correct element.

Two very handy methods that I created allow me to debug whether I have selected the right element. They are highlight and html:

highlight:

This method allows me put a temporary bright yellow border around the element in question. This gives me a visual clue of which element is selected when I look at the browser.

  def highlight element, time = 3
    orig_style = element.attribute("style")
    @driver.execute_script("arguments[0].setAttribute(arguments[1], arguments[2])", element, "style", "border: 2px solid yellow; color: yellow; font-weight: bold;")
    if time > 0
      sleep time
      @driver.execute_script("arguments[0].setAttribute(arguments[1], arguments[2])", element, "style", orig_style)
    end
  end
  some_field = @driver.find_element(:css => "input.text") #=> <Selenium::WebDriver::Element ... >
  highlight some_field #=> nil (But look at the browser, and you should see the element get highlighted for three seconds)
  highlight some_field, 10 #=> nil (But look at the browser, and you should see the element get highlighted for ten seconds)
  highlight some_field, 0 #=> nil (But look at the browser, and you should see the element get highlighted, and stay highlighted)

html:

This method allows me to see the actual HTML code of the element in question. I can use it to compare to the DOM of the actual page if I still don’t “see” the element on the page.

  def html element
    @driver.execute_script("var f = document.createElement('div').appendChild(arguments[0].cloneNode(true)); return f.parentNode.innerHTML", element)
  end
  some_div = @driver.find_element(:css => "div.something") #=> <Selenium::WebDriver::Element ... >
  puts html(some_div) #=> "<div class=\"something\" ... </div>"

———————–

If you wanted to get really clever, you could open up the Selenium::WebDriver::Element module directly and add those methods in directly:

module Selenium::WebDriver
  Element.module_eval do
    def highlight time = 3
      orig_style = self.attribute("style")
      @driver.execute_script("arguments[0].setAttribute(arguments[1], arguments[2])", self, "style", "border: 2px solid yellow; color: yellow; font-weight: bold;")
      if time > 0
        sleep time
        @driver.execute_script("arguments[0].setAttribute(arguments[1], arguments[2])", self, "style", orig_style)
      end
    end
    def html element
      @driver.execute_script("var f = document.createElement('div').appendChild(arguments[0].cloneNode(true)); return f.parentNode.innerHTML", self)
    end
  end
end
some_field.highlight #=> nil (But look at the browser, and you should see the element get highlighted for three seconds)
some_div.html #=> "<div class=\"something\" ... </div>"

Using both of these methods in conjunction, I can always figure out which element I have selected during my interactive coding process.

Solving window.onbeforeunload nasty prompts

One of our tests that we run continuously is an iteration over all the pages in our control room app, checking to ensure the page loads without any error keywords. On a particular set of pages, there are Javascript window.onbeforeunload event listeners that generate the nasty prompts that want the user to verify they do indeed want to leave the page (presumably without saving their work).While looking into the application code, I notice that window.onbeforeunload gets set after every autoSave (application) call, which of course happens at random/timed intervals as well as when certain events are fired.

My first attempts at addressing this issue looked something like this:

@driver.execute_script("window.onbeforeunload = undefined;")
@driver.find_element(:css => "a.unload_prompt").click
## Wait for next page ... hope that a race condition doesn't occur

This worked most of the time. But one out of ten or fifteen times, the race condition would occur where the autoSave js call would occur just after I cleared the window.onbeforeunload and just before the element actually got clicked. This re-set the unload prompt, and caused the extremely frustrating

Selenium::WebDriver::Error::UnhandledAlertError: Modal dialog present

error to appear, failing my control room tests from that point on. 😦

If you are one of the many users struggling with this, never fear. There is hope!

It all centers around the brilliance of what click returns. If we are just clicking on a regular link that either takes us to a page or executes some javascript, we get this response:

pry> @driver.find_element(:css => ".homepage a").click
=> "ok"

If we click a link that shows this window.onbeforeunload prompt:

pry> @driver.find_element(:css => "a.unload_prompt").click
=> "Are you sure you would like to leave this page?"

Aha! Webdriver is nice enough to detect that this unload is going to throw a nasty prompt and sends it back through the JSON Wire to be returned to the Ruby call to click. So how does this solve our issues? We can just get and accept the alert to continue on in the page!

pry> @driver.switch_to.alert.text
=> "Are you sure you would like to leave this page?"
pry> @driver.switch_to.alert.accept
=> ""

Thus, we can update our code to conditionally take care of the alert if the click returns anything other than an ok response:

click_resp = @driver.find_element(:css => "a.unload_prompt").click
@driver.switch_to.alert.accept unless click_resp.eql?("ok")
## Continue on to the next page, etc...

And that is how you solve this nasty problem!