Selenium synchronization with custom events

At work we’ve been using selenium webdriver to enable automated browser testing as part of our regression suite. One of the most difficult parts we’ve had is maintaining DOM synchronization. Selenium handles apps built around page loads extremely well, however more interactive applications reload the page less frequently, favouring incremental updates and dynamic page contents. This complicates webdriver automation as you have to wait for the UI to finish updating. Some developers will use time.sleep() to solve this problem, but quickly find it unreliable, slow and error prone. The smart guys at sauce labs recommend using latches to solve synchronization issues. While latches are a good way to solve synchronization issues, they require adding specific global variables to client side code. From my experience, code that isn’t actually used in the application rots and becomes difficult to maintain over time.

Many of the interactive applications I’ve worked on have used custom events to help decouple application concerns and notify various components that work has been completed. Since custom events are often an integral part of an application, it would be ideal if selenium could re-use those events as well. It turns out that it is pretty simple. We’re using python for our webdriver code, but a similar approach can be used with any webdriver scripting language.

Show Plain Text
  1. # Assuming driver is an instace of selenium.webdriver.Firefox
  2.  
  3. # Define a context manager to keep testing scripts
  4. # simple and short.
  5. class DomEventManager(object):
  6.     def __init__(self, driver, event):
  7.         self.driver = driver
  8.         self.event = event
  9.         self.toggle = 'selenium_wait_' + event.replace('.', '_')
  10.  
  11.     def __enter__(self):
  12.         self.driver.execute_script(
  13.             'window.selenium_event_callback = function () { window.' +
  14.                 self.toggle + ' = true; };')
  15.         )
  16.         self.driver.execute_script(
  17.         '$(document).on("' + self.event + '", window.selenium_event_callback);')
  18.  
  19.     def __exit__(self, exc_type, exc_value, traceback):
  20.         for i in range(30):
  21.             if self.driver.execute_script('return window.' + self.toggle + ' === true;')
  22.                 self.driver.execute_script('$(document).off(window.selenium_event_callback);')
  23.                 self.driver.execute_script('delete window.' + self.toggle)
  24.                 self.driver.execute_script('delete window.selenium_event_callback;')
  25.                 return
  26.             time.sleep(0.5)

The above defines a python context manager to encapsulate the DOM event binding and unbinding. This lets testing scripts stay simple as all the event management is abstracted. Using this context manager would look something like:

Show Plain Text
  1. # These methods are in a test case class that has self.driver defined.
  2. def dom_event(event):
  3.     return DomEventManager(self.driver, event)
  4.  
  5. def test_something(self):
  6.     self.load_page('/')
  7.     with self.dom_event('dashboard.reload'):
  8.         self.driver.find_element_by_css_selector('#reload').click()
  9.     el = self.driver.find_element_by_css_selector('#recent-events h2')
  10.     assert el.text == 'Overview'

In the above code the with block won’t exit until the dashboard.reload event has fired. Once the event has fired the block will exit, and execution will continue.

Using DOM events offers the same benefits as using latches, but makes maintenance easier as often times applications are already using custom events to synchronize UI state.

Comments

Comments are not open at this time.