The many code paths of the :id locator

Problem

While investigating the changes needed for Pull Request #662, I noticed that the :id locator had quite a few code paths. For example, the locator could be:

  • Directly passed on to Selenium-WebDriver
  • Incorporated into an XPath expression
  • Compared while iterating over a set of elements

On top of that, there were multiple places that used each of these approaches. There were 2 different methods for iterating elements, with their usage based on if there was a single Webdriver locator vs multiple locators. There were also 2 different places that passed the locator directly to Selenium. One was when it was the only locator, the other was as a “short-circuit“.

Are all of these code paths really necessary?

Setup

As a first pass to understanding the necessity of these code paths, I decided to look at the performance for different locators. The factors investigated included:

  • Number of locators – 1 (#element method), 2 (#div method, which is 2 locators due to the tag name)
  • Locator type – :id, :xpath, :css
  • Locator value – String, simple Regexp, complex Regexp
  • Browser – Chrome, Firefox, IE11

The benchmark script used was:

require 'watir'
require 'benchmark'

browser = Watir::Browser.new :chrome # or :firefox or :ie
browser.goto 'file://path\to\test.htm'

iterations = 100
Benchmark.bm(29) do |bm|
  bm.report('warm up') do # the first couple of calls are always slower in Firefox
    iterations.times { browser.element(id: 'last').exists? }
  end

  bm.report('element(id: String)') do
    iterations.times { browser.element(id: 'last').exists? }
  end

  bm.report('element(id: ConvertableRegexp)') do
    iterations.times { browser.element(id: /last/).exists? }
  end

  bm.report('element(id: ComplexRegexp)') do
    iterations.times { browser.element(id: /last|missing/).exists? }
  end

  bm.report('element(xpath: String)') do
    iterations.times { browser.element(xpath: ".//*[@id='last']").exists? }
  end  

  bm.report('element(css: String)') do
    iterations.times { browser.element(css: "#last").exists? }
  end  

  bm.report('div(id: String)') do
    iterations.times { browser.div(id: 'last').exists? }
  end

  bm.report('div(id: ConvertableRegexp)') do
    iterations.times { browser.div(id: /last/).exists? }
  end  

  bm.report('div(id: ComplexRegexp)') do
    iterations.times { browser.div(id: /last|missing/).exists? }
  end    

  bm.report('div(xpath: String)') do
    iterations.times { browser.div(xpath: ".//div[@id='last']").exists? }
  end  

  bm.report('div(css: String)') do
    iterations.times { browser.div(css: "div#last").exists? }
  end  	
end

Which was run against a very simple page:

<html>
  <head></head>
  <body>
    <div id="first">text</div>
    <div>text</div>
    <div>text</div>
    <div>text</div>
    <div>text</div>
    <div>text</div>
    <div>text</div>
    <div>text</div>
    <div>text</div>
    <div id="last">text</div>
  </body>
</html>

Results

Chrome:

                                    user     system          real
element(id: String)             0.000000   0.000000   (  3.125152)
element(id: ConvertableRegexp)  0.000000   0.000000   ( 23.825110)
element(id: ComplexRegexp)      0.000000   0.000000   ( 23.570522)
element(xpath: String)          0.000000   0.000000   (  4.734666)
element(css: String)            0.000000   0.000000   (  4.718936)
div(id: String)                 0.016000   0.000000   (  4.715831)
div(id: ConvertableRegexp)      0.000000   0.000000   (  4.734731)
div(id: ComplexRegexp)          0.000000   0.000000   ( 18.971499)
div(xpath: String)              0.000000   0.016000   (  4.714069)
div(css: String)                0.000000   0.000000   (  4.719363)

Firefox:

                                    user     system          real
element(id: String)             0.000000   0.000000   (  1.702996)
element(id: ConvertableRegexp)  0.000000   0.000000   ( 43.959463)
element(id: ComplexRegexp)      0.000000   0.000000   ( 43.709797)
element(xpath: String)          0.000000   0.000000   (  3.312703)
element(css: String)            0.000000   0.000000   (  3.281463)
div(id: String)                 0.000000   0.000000   (  3.140972)
div(id: ConvertableRegexp)      0.000000   0.000000   (  4.859860)
div(id: ComplexRegexp)          0.000000   0.000000   ( 34.114425)
div(xpath: String)              0.000000   0.000000   (  3.218661)
div(css: String)                0.000000   0.000000   (  3.203543)

IE11:

                                    user     system          real
element(id: String)             0.000000   0.000000   ( 26.331459)
element(id: ConvertableRegexp)  0.000000   0.000000   (280.249377)
element(id: ComplexRegexp)      0.015000   0.000000   (283.995062)
element(xpath: String)          0.000000   0.000000   ( 26.831583)
element(css: String)            0.000000   0.000000   ( 38.602937)
div(id: String)                 0.000000   0.000000   ( 37.646597)
div(id: ConvertableRegexp)      0.000000   0.000000   ( 37.833313)
div(id: ComplexRegexp)          0.391000   0.125000   (224.993691)
div(xpath: String)              0.000000   0.000000   ( 25.425635)
div(css: String)                0.000000   0.000000   ( 35.176564)

To better understand the performance results, it helps to know what wire calls were made. Unfortunately, they don’t fit nicely in a blog post, so please check out the gist of wire calls.

Conclusion

This initial investigation suggests that the extra code paths are not necessary:

  • div(id: ComplexRegexp) is faster than element(id: ComplexRegexp). The wire calls show that #element iterates through all elements on the page including the html, head and body tags where as #div can iterate a subset. In theory, removing the #element specific iterator would simplify the code without impacting performance.
  • div(id: ConvertableRegexp) is faster than element(id: ConvertableRegexp). This is due to a single XPath call being made instead of iterating elements. Again, removing the #element specific iterator would be beneficial.
  • div(xpath: String) is comparable, if not faster than using div(id: String). This suggests that the :id “short-circuit” used by div(id: String) is not neccessary. The short-circuit needs to make 2 wire calls – one to get the element by id and another to validate the tag name. This validation is a performance hit.

There are a couple of other tweaks that can be made and things to keep in mind:

  • In Firefox, div(id: ConvertableRegexp) is slower than a plain XPath or CSS-selector. The wire calls, suggest that there are extra validations, which I would expect is unnecessary.
  • In IE11, XPath locators are faster than CSS-selectors. For W3C drivers, Selenium converts :id locators to :css. This means that div(id: String) would be faster if it went through Watir’s XPath builder instead of short-circuiting.
  • While element(id: String) is the fastest approach in Chrome/Firefox, I do not think the same will be true with the above changes.

I am curious to make these changes and see how the results change.

Advertisements
Posted in Watir | Tagged , , , | Leave a comment

Text field performance – #set vs #set!

Problem

While investigating an issue for TextField#set!, I noticed that the method was:

def set!(input_value)
  set input_value[0]
  element_call { execute_js(:setValue, @element, input_value[0..-2]) }
  append(input_value[-1])
  raise Watir::Exception::Error, "#set! value does not match expected input" unless value == input_value
end

Notice that it calls the normal #set for the first character, as well as another call for the middle of the value, the end and validation. At a minimum that’s 3 extra wire calls. More wire calls equals slower performance.

At what point is using #set! actually faster than just #set?

Conclusion

For most cases, using #set is likely sufficient. If you need to optimize performance:

  • Use #set for shorter values and #set! for longer values.
  • The browser has a significant impact on what a “short” vs “long” value is. For Chrome “short” is ~30 characters, IE11 is ~80 characters and Firefox is ~200 characters.

Setup

The following script was used to benchmark the methods. Basically the script uses each method to set a text field with values of different lengths.

require 'watir'
require 'benchmark'

# The first scenario always seems to take longer.
# The 15 character scenario is just there to absorb the outlier.
value_lengths = [15, 1, 10, 20, 30, 40, 50, 60, 70, 80, 90,
  100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]
iterations = 50

[:chrome, :firefox, :ie].each do |b|
  browser = Watir::Browser.new b
  puts browser.name
  browser.goto 'bit.ly/watir-webdriver-demo'

  Benchmark.bm(27) do |bm|
    value_lengths.each do |chars|
      value = 'a' * chars
      field = browser.text_field(id: 'entry_1000000')
      field.id # force locate

      bm.report("#{chars} chars - #set") do
        iterations.times { field.set(value) }
      end

      bm.report("#{chars} chars - #set!") do
        iterations.times { field.set!(value) }
      end
    end
  end

  browser.close
end

Results

Due to working on this benchmark over a couple of days, I happened to use 3 different machines. The machines seemed to have an impact on the results – eg one machine took twice as long as another machine. While the actual values differed across machines, the relative trending/patterns seemed consistent for a given machine. The values below are from a single machine.

The raw data (total execution time for 50 iterations):

  • Chrome
                              user     system      total        real
    15 chars - #set       0.140000   0.031000   0.171000 ( 12.774118)
    15 chars - #set!      0.140000   0.078000   0.218000 ( 18.878440)
    1 chars - #set        0.110000   0.015000   0.125000 (  8.081138)
    1 chars - #set!       0.249000   0.141000   0.390000 ( 18.239944)
    10 chars - #set       0.125000   0.093000   0.218000 ( 11.127267)
    10 chars - #set!      0.296000   0.141000   0.437000 ( 19.303918)
    20 chars - #set       0.110000   0.015000   0.125000 ( 15.537791)
    20 chars - #set!      0.171000   0.125000   0.296000 ( 19.298714)
    30 chars - #set       0.125000   0.063000   0.188000 ( 17.663151)
    30 chars - #set!      0.125000   0.156000   0.281000 ( 17.932947)
    40 chars - #set       0.140000   0.000000   0.140000 ( 18.630426)
    40 chars - #set!      0.109000   0.124000   0.233000 ( 16.829911)
    50 chars - #set       0.047000   0.047000   0.094000 ( 22.844721)
    50 chars - #set!      0.156000   0.078000   0.234000 ( 17.055359)
    60 chars - #set       0.047000   0.031000   0.078000 ( 24.631807)
    60 chars - #set!      0.140000   0.078000   0.218000 ( 16.928176)
    70 chars - #set       0.094000   0.063000   0.157000 ( 27.157706)
    70 chars - #set!      0.250000   0.078000   0.328000 ( 16.582010)
    80 chars - #set       0.078000   0.047000   0.125000 ( 29.898149)
    80 chars - #set!      0.140000   0.125000   0.265000 ( 17.438833)
    90 chars - #set       0.125000   0.062000   0.187000 ( 32.817954)
    90 chars - #set!      0.156000   0.094000   0.250000 ( 17.871170)
    100 chars - #set      0.078000   0.125000   0.203000 ( 37.087482)
    100 chars - #set!     0.203000   0.156000   0.359000 ( 17.116646)
    200 chars - #set      0.171000   0.093000   0.264000 ( 72.201890)
    200 chars - #set!     0.063000   0.110000   0.173000 ( 17.913061)
    300 chars - #set      0.265000   0.124000   0.389000 (107.456812)
    300 chars - #set!     0.281000   0.203000   0.484000 ( 19.793376)
    400 chars - #set      0.218000   0.219000   0.437000 (138.737188)
    400 chars - #set!     0.218000   0.124000   0.342000 ( 17.895441)
    500 chars - #set      0.172000   0.172000   0.344000 (173.578494)
    500 chars - #set!     0.265000   0.109000   0.374000 ( 18.025635)
    600 chars - #set      0.265000   0.187000   0.452000 (215.553506)
    600 chars - #set!     0.234000   0.250000   0.484000 ( 17.540175)
    700 chars - #set      0.234000   0.203000   0.437000 (258.045478)
    700 chars - #set!     0.250000   0.078000   0.328000 ( 17.831776)
    800 chars - #set      0.218000   0.374000   0.592000 (305.438888)
    800 chars - #set!     0.266000   0.109000   0.375000 ( 17.437788)
    900 chars - #set      0.358000   0.266000   0.624000 (355.288330)
    900 chars - #set!     0.203000   0.124000   0.327000 ( 18.202421)
    1000 chars - #set     0.297000   0.297000   0.594000 (408.735169)
    1000 chars - #set!    0.280000   0.093000   0.373000 ( 18.468718)
    
  • Firefox
                              user     system      total        real
    15 chars - #set       0.343000   0.187000   0.530000 (  7.469105)
    15 chars - #set!      0.686000   0.405000   1.091000 ( 11.048614)
    1 chars - #set        0.390000   0.188000   0.578000 (  4.787335)
    1 chars - #set!       0.702000   0.312000   1.014000 ( 10.953899)
    10 chars - #set       0.265000   0.203000   0.468000 (  5.024884)
    10 chars - #set!      0.858000   0.281000   1.139000 ( 11.050294)
    20 chars - #set       0.328000   0.140000   0.468000 (  5.590159)
    20 chars - #set!      0.858000   0.359000   1.217000 ( 12.095065)
    30 chars - #set       0.421000   0.156000   0.577000 (  6.954347)
    30 chars - #set!      0.920000   0.452000   1.372000 ( 11.968912)
    40 chars - #set       0.265000   0.188000   0.453000 (  6.644976)
    40 chars - #set!      1.154000   0.249000   1.403000 ( 11.515816)
    50 chars - #set       0.375000   0.172000   0.547000 (  6.968870)
    50 chars - #set!      0.920000   0.390000   1.310000 ( 11.550612)
    60 chars - #set       0.515000   0.171000   0.686000 (  8.126478)
    60 chars - #set!      1.201000   0.453000   1.654000 ( 13.393321)
    70 chars - #set       0.327000   0.109000   0.436000 (  7.842220)
    70 chars - #set!      0.952000   0.374000   1.326000 ( 12.060087)
    80 chars - #set       0.515000   0.110000   0.625000 (  8.157458)
    80 chars - #set!      0.842000   0.358000   1.200000 ( 11.051940)
    90 chars - #set       0.296000   0.219000   0.515000 (  8.991014)
    90 chars - #set!      1.170000   0.390000   1.560000 ( 12.287642)
    100 chars - #set      0.437000   0.156000   0.593000 (  9.277474)
    100 chars - #set!     0.952000   0.390000   1.342000 ( 13.137452)
    200 chars - #set      0.296000   0.234000   0.530000 ( 12.114465)
    200 chars - #set!     0.983000   0.515000   1.498000 ( 12.445034)
    300 chars - #set      0.359000   0.109000   0.468000 ( 16.051603)
    300 chars - #set!     0.889000   0.359000   1.248000 ( 12.314791)
    400 chars - #set      0.359000   0.203000   0.562000 ( 19.885181)
    400 chars - #set!     0.936000   0.483000   1.419000 ( 14.016303)
    500 chars - #set      0.359000   0.156000   0.515000 ( 21.436203)
    500 chars - #set!     0.936000   0.312000   1.248000 ( 12.877110)
    600 chars - #set      0.312000   0.063000   0.375000 ( 27.064266)
    600 chars - #set!     0.842000   0.280000   1.122000 ( 11.879354)
    700 chars - #set      0.421000   0.110000   0.531000 ( 30.558957)
    700 chars - #set!     0.749000   0.280000   1.029000 ( 13.257108)
    800 chars - #set      0.328000   0.125000   0.453000 ( 32.094866)
    800 chars - #set!     0.951000   0.375000   1.326000 ( 13.150200)
    900 chars - #set      0.499000   0.109000   0.608000 ( 35.937065)
    900 chars - #set!     0.874000   0.421000   1.295000 ( 12.472492)
    1000 chars - #set     0.327000   0.140000   0.467000 ( 39.199762)
    1000 chars - #set!    0.874000   0.297000   1.171000 ( 11.981587)
    
  • IE11
                              user     system      total        real
    15 chars - #set       0.015000   0.000000   0.015000 ( 30.232037)
    15 chars - #set!      0.000000   0.015000   0.015000 ( 60.386750)
    1 chars - #set        0.016000   0.016000   0.032000 ( 28.533165)
    1 chars - #set!       0.015000   0.015000   0.030000 ( 60.728970)
    10 chars - #set       0.032000   0.032000   0.064000 ( 30.186125)
    10 chars - #set!      0.046000   0.000000   0.046000 ( 61.104927)
    20 chars - #set       0.032000   0.015000   0.047000 ( 30.278858)
    20 chars - #set!      0.046000   0.000000   0.046000 ( 55.895414)
    30 chars - #set       0.047000   0.031000   0.078000 ( 35.396704)
    30 chars - #set!      0.047000   0.032000   0.079000 ( 58.093593)
    40 chars - #set       0.047000   0.046000   0.093000 ( 40.107238)
    40 chars - #set!      0.015000   0.016000   0.031000 ( 58.421772)
    50 chars - #set       0.000000   0.000000   0.000000 ( 44.507613)
    50 chars - #set!      0.016000   0.000000   0.016000 ( 54.085531)
    60 chars - #set       0.062000   0.016000   0.078000 ( 50.736921)
    60 chars - #set!      0.032000   0.062000   0.094000 ( 59.026181)
    70 chars - #set       0.078000   0.031000   0.109000 ( 52.462982)
    70 chars - #set!      0.062000   0.016000   0.078000 ( 58.203642)
    80 chars - #set       0.094000   0.062000   0.156000 ( 56.439571)
    80 chars - #set!      0.000000   0.000000   0.000000 ( 57.065291)
    90 chars - #set       0.078000   0.047000   0.125000 ( 61.698241)
    90 chars - #set!      0.000000   0.031000   0.031000 ( 58.921369)
    100 chars - #set      0.031000   0.016000   0.047000 ( 69.029235)
    100 chars - #set!     0.015000   0.015000   0.030000 ( 58.827357)
    200 chars - #set      0.047000   0.094000   0.141000 (105.689615)
    200 chars - #set!     0.016000   0.016000   0.032000 ( 57.267913)
    300 chars - #set      0.093000   0.046000   0.139000 (151.164081)
    300 chars - #set!     0.047000   0.016000   0.063000 ( 56.144656)
    400 chars - #set      0.109000   0.109000   0.218000 (196.513294)
    400 chars - #set!     0.016000   0.031000   0.047000 ( 55.644743)
    500 chars - #set      0.203000   0.188000   0.391000 (241.784579)
    500 chars - #set!     0.062000   0.046000   0.108000 ( 57.954614)
    600 chars - #set      0.265000   0.141000   0.406000 (278.289007)
    600 chars - #set!     0.125000   0.047000   0.172000 ( 58.749861)
    700 chars - #set      0.219000   0.140000   0.359000 (331.874462)
    700 chars - #set!     0.078000   0.047000   0.125000 ( 64.521360)
    800 chars - #set      0.156000   0.203000   0.359000 (377.504898)
    800 chars - #set!     0.031000   0.062000   0.093000 ( 64.584817)
    900 chars - #set      0.140000   0.078000   0.218000 (370.951837)
    900 chars - #set!     0.063000   0.062000   0.125000 ( 59.779334)
    1000 chars - #set     0.390000   0.063000   0.453000 (463.118516)
    1000 chars - #set!    0.031000   0.031000   0.062000 ( 60.605189)
    

The graph below shows the average execution time for each method. Only the data up to 300 characters has been plotted. The 300-1000 character range just continues the same trend.

performance of #set vs #set!

The performance of the #set method had a direct correlation with the characters entered. For Chrome/IE11, the difference in inputting 1 character vs 1000 characters was ~8 seconds. Firefox had a measly degradation of 0.7 seconds.

In contrast, #set!, for a given browser, had a constant performance across the character range. Firefox was the fastest at 0.25s. Chrome was close behind at 0.35s. IE11 was a slow third at 1.18s.

The trends resulted in an intersection of the two methods at 30 characters for Chrome, 80 characters for IE11 and 200 characters for Firefox. In other words, this is the point where #set! becomes faster than #set.

Posted in Watir | Tagged , , , | Leave a comment

Reproducing the compound class error

Problem

A Stack Overflow question by BIGJOHN mentioned getting a “compound class names not permitted error.” The usage of the compound class was obvious:

$website.element(:class => "gs-c-promo-heading nw-o-link gs-o-bullet__text gs-o-faux-block-link__overlay-link gel-pica-bold gs-u-pl@xs")

The question is, where is this exception coming from? Compound class names, while on the list to be deprecated in v7.0, should still be supported in the current v6.8.4.

Solution

Using a simple page:

<html>
  <body>
    <div class="a b">text</div>
  </body>
</html>

The exception was not seen by my typical command:

browser.div(class: 'a b').exists?
#=> true

It was not until I noticed that the question used element instead of div that I was able to reproduce the exception:

p browser.element(class: 'a b').exists?
#=> invalid selector: Compound class names not permitted (Selenium::WebDriver::Error::InvalidSelectorError)

It turns out that none of the new locator strategies work when using element:

# Array with multiple classes (bad)
browser.div(class: ['a', 'b']).exists?
#=> true
browser.element(class: ['a', 'b']).exists?
#=> false

# Boolean (bad)
browser.div(class: true).exists?
#=> true
browser.element(class: true).exists?
#=> false

# String with single class (good)
browser.div(class: 'a').exists?
#=> true
browser.element(class: 'a').exists?
#=> true

# Regexp (good)
browser.div(class: /a b/).exists?
#=> true
browser.element(class: /a b/).exists?
#=> true

Taking a closer look at the exceptions stacktrace, I traced the problem back to the following method in watir/locators/element/locator.rb:171.

def wd_find_first_by(how, what)
  if what.is_a? String
    locate_element(how, what)
  else
    all_elements.find { |element| fetch_value(element, how) =~ what }
  end
end

This method is called whenever:

  • The locator is directly supported by Selenium – eg :class, :id, etc.
  • There is a single locator strategy

If the what is a String, the locator is directly sent to Selenium. As Selenium does not support compound classes, an exception will occur. Only #element fails because it is the only way to have single locator strategy. In comparison, the tag specific element methods will have 2 strategies – :class and :tag_name:

browser.div(class: 'a b').inspect
#=> <Watir::Div: located: false; {:class=>\"a b\", :tag_name=>\"div\"}>
browser.element(class: 'a b').inspect
#=> <Watir::HTMLElement: located: false; {:class=>\"a b\"}>

When the what is one of the new types, Array or Boolean, the else branch is triggered. fetch_value basically returns attribute values, which will rarely match the what. As a result, an element is never found.

I am unsure why we handle the webdriver locators differently. Maybe there are some performance gains? I will have to leave that as an investigation for another time. For now, Issue 658 has been opened to track this bug.

Posted in Watir | Tagged , | Leave a comment

Attach to a manually opened Chrome browser using debuggerAddress

Problem

Watir-Classic can attach to manually opened browsers. This is a great benefit when using Watir to aid manual testing efforts. It is also helpful when developing automated tests. However, for the longest time, this was not possible with Watir(-Webdriver).

The ChromeOptions page mentions a debuggerAddress option:

An address of a Chrome debugger server to connect to, in the form of , e.g. ‘127.0.0.1:38947’

Can we use this option to attach to manually opened browsers?

Solution

From Issue 710, I was able to attach to a browser using the following steps:

  1. Close all Chrome browsers
  2. Manually start Chrome using chrome.exe --remote-debugging-port=8181 (any port number will do, but it must be used to open the first window)
  3. In the Watir script, initialize the browser using the debuggerAddress option with the same port:
    require 'watir'
    
    browser = Watir::Browser.new(
      :chrome, 
      'chromeOptions' => {'debuggerAddress': '127.0.0.1:8181'})
    

Limitations

Unfortunately, ChromeDriver mentions that there are some limitations. For example, maximizing the browser is not supported:

browser.window.maximize
#=> Selenium::WebDriver::Error::UnknownError: 
#=>   unknown error: operation is unsupported with remote debugging

While I need to investigate which commands are not supported, it looks promising. At least the manual testing scripts tested so far, which include navigation and form filling, work.

Posted in Watir, Watir-Webdriver | Tagged , , | 1 Comment

Locate by classes in a specific order

Problem

In HTML, the class attribute is a space-separated list of classes for the element. The order of classes does not matter when applying style sheets. For example, in the following page, all 3 div elements would look the same even though their class value is different.

<html>
  <body>
    <div class="a b">1</div> 
    <div class="b a">2</div>
    <div class="a b">3</div>
  </body>
</html>

For many years, Watir’s :class locator has been order dependent when using multiple classes:

# Only matches the exact order "a" and then "b"
browser.divs(class: 'a b').map(&:text)
#=> ["1", "3"]

# Only matches the exact order "b" and then "a"
browser.divs(class: 'b a').map(&:text)
#=> ["2"]

Watir 6.5 introduced the ability to be order independent. This is done by providing an Array of classes:

browser.divs(class: ['a', 'b']).map(&:text)
#=> ["1", "2", "3"]

browser.divs(class: ['b', 'a']).map(&:text)
#=> ["1", "2", "3"]

For backwards compatibility, the decision was to leave the existing order dependent behaviour for String values as-is. However, a deprecation warning will be generated:

browser.divs(class: 'a b').map(&:text)
#=> 2017-08-16 21:50:17 WARN Watir [DEPRECATION] 
#=>   Using the :class locator to locate multiple classes with 
#=>   a String value is deprecated. Use Array instead.

Hopefully no one has to deal with a class order requirement, but if you do, what can you do to prevent the deprecation warning?

Solution

The new handling for Array values still builds the XPath the same as when a String value is used. The only difference is that the Array value will “and” the items together. With only a single item in the Array, the behaviour is the same as using a String. As well, the Array is not checked for items with spaces – ie is not subject to the deprecation warning.

This means that the order dependent classes can simply be passed as one of the Array elements:

browser.divs(class: ['a b']).map(&:text)
#=> ["1", "3"]
	
browser.divs(class: ['b a']).map(&:text)
#=> ["2"]

The XPath generated for the deprecated approach and this workaround only differ by an unimportant set of brackets:

# Using browser.divs(class: 'a b')
{"using":"xpath","value":".//div[contains(concat(' ', @class, ' '), ' a b ')]"}

# Using browser.divs(class: ['a b'])
{"using":"xpath","value":".//div[(contains(concat(' ', @class, ' '), ' a b '))]"}
Posted in Watir | Tagged , | Leave a comment

Set styled radio buttons and checkboxes that have hidden or overlapped the input field

Problem

There seems to be an upward trend in applications using label elements to style radio buttons and checkboxes. This can cause issues when Watir tries to set the field. For example:

These issues can be reproduced on the following page:

<html>
  <body>
    <div>  
      <input type="checkbox" id="overlapped_checkbox" style="position:absolute;">
      <label for="overlapped_checkbox" style="background-color:white; position:relative;">Label for overlap<label>
    </div>      
    <div>
      <label for="hidden_radio">Label for hidden</label> 
      <input type="radio" id="hidden_radio" style="display:none;">    
    </div>
  </body>
</html>

With the following Watir code:

browser.checkbox(id: 'overlapped_checkbox').set
#=> Selenium::WebDriver::Error::UnknownError:
#=>   Element is not clickable at point (18, 17). 
#=>   Other element would receive the click: <label for="overlapped_checkbox" style="background-color:white; position:relative;">...</label>

browser.radio(id: 'hidden_radio').set
#=> Watir::Exception::UnknownObjectException:
#=>   element located, but timed out after 30 seconds, 
#=>   waiting for true condition on #<Watir::Radio: located: true; {:id=>"hidden_radio", :tag_name=>"input", :type=>"radio"}> 

How do you get Watir to set these fields?

Solution

With these styled controls, a user never actually interacts with the input field. They only see and click the label element, which might happen to include an image of an enabled/disabled field. This works because the browser transfers clicks on the label element to clicks on the input field.

Watir needs to do the same thing as a user – ie click the label element instead:

p browser.checkbox(id: 'overlapped_checkbox').checked?
#=> false
browser.label(for: 'overlapped_checkbox').click
p browser.checkbox(id: 'overlapped_checkbox').checked?
#=> true

p browser.radio(id: 'hidden_radio').set?
#=> false
browser.label(for: 'hidden_radio').click
p browser.radio(id: 'hidden_radio').set?
#=> true
Posted in Watir, Watir-Webdriver | Tagged , | Leave a comment

Determine the feature directory in Cucumber hooks

Problem

Today’s Stack Overflow question, “Is it possible to know from which folder cucumber scenario is running?” by Den Silver, was looking to apply hooks based on a feature file’s folder. While I can see the merit of this approach, using tags, as suggested by Jeff Price in a similar question, might be a better choice.

At any rate, I was curious. Can you determine a feature file’s location?

Solution

It is actually quite simple once you know that there is a #location method:

Before do |scenario|
  scenario.location.file
  #=> "features/mobile/test.feature"
end

Finding the Solution

Providing the solution is great, but sometimes I wonder if explaining how to find the solution would be useful. For those that are interested, here is how I got to the solution.

I knew that the basic form of a hook looked like:

Before do |scenario|
end

A scenario is part of a feature, so the first step was to see what methods scenario had. To do that, one needs to know its class:

puts scenario.class
#=> Cucumber::RunningTestCase::Scenario

Looking through the Scenario class documentation at RubyDoc.info, none of the methods seemed related to a feature. Then I noticed that the #name method, which I had used before, was not mentioned. The method still worked, so how was it defined?

Following the docs “Defined in”, the source code for the class can be found on GitHub. The Scenario is a SimpleDelegator, which means that undefined methods, such as #name, would be sent to another object – specifically @test_case:

class Scenario < SimpleDelegator
  def initialize(test_case, result = Core::Test::Result::Unknown.new)
    @test_case = test_case
    @result = result
    super test_case
  end

@test_case could provide other useful methods, so I needed to determine what class it was:

puts scenario.instance_variable_get(:@test_case).class
#=> Cucumber::Core::Test::Case

Unfortunately, this class was not defined in the Cucumber RubyDoc or in the source code pages previously used. Looking at the gems installed by Cucumber, I noticed that there was also a Cucumber-Core gem. Browsing the Cucumber-Core docs, I found the class. There was the #name method that I was looking for. As well, it had a potentially useful method called #location:

puts scenario.location
#=> "features/mobile/test.feature"

Great. I just needed to split the path to get the sub-directory (as the question just wanted “mobile”):

puts scenario.location.split(File::SEPARATOR)[1]
#=> NoMethodError: undefined method `split' for #<Cucumber::Core::Ast::Location::Precise>

Oh. Apparently the returned value was not just a String. Exploring the Ast::Location::Precise documentation, I found the needed #file method:

puts scenario.location.file.split(File::SEPARATOR)[1]	
#=> "mobile"
Posted in Cucumber | Tagged , | Leave a comment