Watir needs to remove the Watir#always_locate feature toggle. Most people don’t use it and most of those who do will be unlikely to notice any differences at this point. This post is an overview of how Selenium and Watir view elements differently, how that has affected Watir’s code, and what Watir needs to do to address it.

To follow along with the code at home:

$ gem install watir -v 6.0.0.beta3
$ irb
require 'watir'
browser = Watir::Browser.new
driver = browser.driver
browser.goto 'google.com'

Watir vs Selenium

These projects have traditionally had a fundamental difference in the concept of what an element is.

  • In Selenium an element is a specific object on a specific page that hasn’t changed.
  • In Watir an element is whichever object is found at the location of the provided selector.

Watir stores the location or “address” of an element in a selector variable:

wtr_element = browser.link(id: 'gb_70') 
wtr_element.inspect 
# => #<Watir::Anchor:0x..fcb27d0ea9591d6f4 located=false selector={:id=>\"gb_70\", :tag_name=>\"a\"}

Alternately, Selenium WebDriver does not store the locator for an element, but maintains a reference to the object in the DOM (underlying page code) as provided by the driver:

sel_element = driver.find_element(id: 'gb_70') 
sel_element.inspect 
# => #<Selenium::WebDriver::Element:0x529e9f4e6cb7faf4 id=\"0.2516871485244032-1\">

When the DOM changes (e.g. the page is refreshed), these object references go “stale.” Attempting to interact with a stale element in Selenium throws an error:

browser.refresh
sel_element.click
# => Selenium::WebDriver::Error::StaleElementReferenceError: stale element reference: element is not attached to the page document

Attempting to interact with a stale element in Watir results in different behaviors based on how the the always_locate toggle is set.

History of #always_locate

In the original version of Watir (watir-classic), there was no notion of a stale element. The desired element was always located before every action based on the selector that is provided.

When Watir was first implemented with WebDriver, it adopted the Selenium idea of what an element is. For various (good) reasons, Watir does a lot of work when it locates an element. As such, there is a performance benefit to only looking it up once, storing the reference id from the driver, and then using that for all future interactions.

As people began converting their test suites from using watir-classic to watir-webdriver, they began to see these Watir::Exception::UnknownObjectException errors as a result of stale elements, because their tests were written assuming that the DOM could change without needing to redefine their elements. Rather than restoring backward compatibility completely, the Watir#always_locate toggle was created. The default value (true) was backward compatible with the original Watir, but it could be set to false to allow users to consider elements the same way Selenium does (and reap the performance boost).

Fortunately, caching elements and relocating them do not have to be all-or-nothing. The default always_locate behavior was updated last year to cache the element and then make a single call to verify that it is not stale before using it, rather than the multiple calls that go into locating it from scratch every time. Essentially the default “always locate” is currently “locate when necessary.” This provided a significant performance improvement for the default behavior.

Inconsistencies

Considering Watir elements in the Selenium fashion results in some unfortunate inconsistencies:

Watir.always_locate = false 

A stale element will return false for #exists?, but only the first time it is called:

wtr_element.exists? # => true
browser.refresh
wtr_element.exists? # => false
wtr_element.exists? # => true

Or in the case of taking an action on an element:

wtr_element.exists? # => true
browser.refresh
wtr_element.text # => Watir::Exception::UnknownObjectException
wtr_element.text # => 'Sign In'

Oddly, this also means that there is no functional distinction between wait_while and wait_until:

wtr_element.exists? # => true
browser.refresh
wtr_element.wait_while_present
wtr_element.text # => 'Sign In'
wtr_element.exists? # => true
browser.refresh
wtr_element.wait_until_present
wtr_element.text # => 'Sign In'

The Solution

Because the performance difference between the options is now negligible, there is no significant benefit to maintaining two separate notions of what an element is. Watir should only implement its original approach. That means that #exists? will always be true if an element with the provided selector is on the page.

To assist those relying on the Selenium approach to elements, Watir needs to implement a public #stale? method. This method could only be called on previously located elements and would return true if the driver shows that the associated reference id is no longer attached to the DOM.

This means #stale? would return true regardless of how many times it is called:

wtr_element = browser.link(id: 'gb_70')
wtr_element.exists? # => true
browser.refresh
wtr_element.stale? # => true
wtr_element.stale? # => true
wtr_element.stale? # => true

In code that uses explicit waits to check for DOM changes (as in the examples in the previous section), rather than using #wait_while_present or #wait_until_present, it would need to be changed to #wait_until_stale:

wtr_element.exists? # => true
browser.refresh
wtr_element.wait_until_stale
wtr_element.exists? # => true

It might also be worth noting that there is a major anti-pattern that can be used with the Selenium approach that would also need to be addressed. If the test code takes an action on an element that might be stale and rescues the exception to use for flow control (yes, I’ve seen this in actual test code), then it will need to be changed as well:

take_action_that_might_change_dom
begin
  element_that_might_go_stale.click
  take_action_on_unchanged_dom
rescue Watir::Exception::UnknownObjectException
  take_action_on_changed_dom
end

This should be handled the same way as in the previous example, by explicitly waiting for the condition that needs to be met:

take_action_that_might_change_dom
if element_that_might_go_stale.stale?
  take_action_on_changed_dom
else
  take_action_on_unchanged_dom
end

Conclusion

Watir needs to standardize on its original definition of what an element is. This will significantly reduce both complexity in the code and testing time. It shouldn’t impact many people, and for those whom it does, it should be relatively painless to swap out “stale” for “exist” or “present” in various places of their test suites.

No comments

You today

Comments are closed