Element location after auto-scrolling by #click

Problem

An exception will occur when the driver tries to click a location outside of the viewport. From my experience with Element#click, I’ve never run into the exception for elements outside the viewport. There is clearly some auto-scrolling that occurs before clicking.

When I added Element#obscured?, I believed that if the element was outside of the viewport, it would always be scrolled to the top. However, a Slack discussion suggested that this was not true (or at least has changed).

So what auto-scrolling happens today?

Answer

The scrolling depends on multiple factors:

  • Browser being used
  • Location of the element being clicked relative to the viewport
  • Whether click modifiers (eg :control) are used

The below table summarizes where the element is positioned (within the viewport) after clicking it:

BrowserClick ModifiersElement Below ViewportElement In ViewportElememnt Above Viewport
Chrome/Edge v95Without modifiersBottomUnchangedTop
Chrome/Edge v95With modifiersBottomUnchangedBottom
Firefox v94With modifiersBottomUnchangedBottom
Firefox v94With modifiersOut Of Bounds Error (no scrolling)UnchangedOut Of Bounds Error (no scrolling)

This was observed using the following script (Watir v7.0.0.beta2, Selenium-WebDriver v4.0.0.beta4):

def test_click(browser, link, relative_to, modifier)
  out_of_bounds = ''
  begin
    link.click(*modifier)
  rescue Selenium::WebDriver::Error::ElementClickInterceptedError
    # scrolled but covered by floating header/footer
  rescue Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
    out_of_bounds = "(MoveTargetOutOfBoundsError)"
  end
  puts "after clicking element #{relative_to} viewport: #{out_of_bounds} #{browser.execute_script('return arguments[0].getBoundingClientRect().top;', link)}"
end

[:chrome, :edge, :firefox].each do |browser_type|
  browser = Watir::Browser.new browser_type

  # any tall page where the link isn't in the viewport when scrolled to top or bottom
  browser.goto 'https://jkotests.wordpress.com/2013/04/10/directions-analogy-for-locator-strategies/'
  link = browser.link(text: 'Automation')

  browser.execute_script('arguments[0].removeAttribute("href");', link) # prevent navigation
  puts "starting: #{browser.execute_script('return arguments[0].getBoundingClientRect().top;', link)}"

  [[], [:control]].each do |modifier|
    puts "using #{browser_type} - modifiers #{modifier}"

    browser.scroll.to(:top)
    test_click(browser, link, 'below', modifier)

    link.scroll.to(:center)
    test_click(browser, link, 'in centre of', modifier)        

    browser.scroll.to(:bottom)
    test_click(browser, link, 'above', modifier)    

    puts
  end

  browser.close
  puts
end

Which had the output:

starting: 1617
using chrome - modifiers []
after clicking element below viewport:  693
after clicking element in centre of viewport:  374
after clicking element above viewport:  0

using chrome - modifiers [:control]
after clicking element below viewport:  693
after clicking element in centre of viewport:  374
after clicking element above viewport:  693


starting: 1617
using edge - modifiers []
after clicking element below viewport:  662
after clicking element in centre of viewport:  358
after clicking element above viewport:  0

using edge - modifiers [:control]
after clicking element below viewport:  662
after clicking element in centre of viewport:  358
after clicking element above viewport:  662


starting: 1657
using firefox - modifiers []
after clicking element below viewport:  708
after clicking element in centre of viewport:  381
after clicking element above viewport:  708

using firefox - modifiers [:control]
after clicking element below viewport: (MoveTargetOutOfBoundsError) 1657
after clicking element in centre of viewport:  381
after clicking element above viewport: (MoveTargetOutOfBoundsError) -1889
Advertisement
Posted in Uncategorized, Watir | Tagged , | Leave a comment

Restore access to ChromeDriver 75 console logs

Problem

I recently wrote the following code to pull the console logs from Chrome:

caps = Selenium::WebDriver::Remote::Capabilities.chrome('loggingPrefs' => {browser: 'ALL'})
browser = Watir::Browser.new :chrome, desired_capabilities: caps

browser.execute_script('console.log("test");')

puts browser.driver.manage.logs.get(:browser)

However, it started throwing an exception a couple of days ago:

#=> /selenium-webdriver-3.142.3/lib/selenium/webdriver/common/logs.rb:32:in `get': 
#=>   undefined method `log' for # (NoMethodError)

What is going on?

Answer

The problem stems from an upgrade to ChromeDriver 75.0.3770.8, where the release notes state “the most noticeable change is ChromeDriver now runs in W3C standard compliant mode by default.” This results in a different Selenium bridge being used:

  • ChromeDriver 74.0.3729.6 uses the Selenium::WebDriver::Remote::OSS::Bridge
  • ChromeDriver 75.0.3770.90 uses the Selenium::WebDriver::Remote::W3C::Bridge

From Selenium Issue 5127, this logging is not defined in W3C. No surprise then that the W3C::Bridge does not have have the logging methods that the OSS::Bridge had.

The ChromeDriver release notes also say, “renamed capability loggingPrefs to goog:loggingPrefs, as required by W3C standard.” This gives hope that the logging still exists; we just need to make it available. What if we just copy the OSS::Bridge functionality – particularly the #log method that is undefined and the associated command? I came up with the following patch:

module Selenium
  module WebDriver
    module Chrome
      module Bridge
        COMMANDS = remove_const(:COMMANDS).dup
        COMMANDS[:get_log] = [:post, 'session/:session_id/log']
        COMMANDS.freeze
        
        def log(type)
          data = execute :get_log, {}, {type: type.to_s}

          Array(data).map do |l|
            begin
              LogEntry.new l.fetch('level', 'UNKNOWN'), l.fetch('timestamp'), l.fetch('message')
            rescue KeyError
              next
            end
          end
        end
      end
    end
  end
end

To make this work, the capability also needs to be renamed from “loggingPrefs” to “goog:loggingPrefs”:

caps = Selenium::WebDriver::Remote::Capabilities.chrome('goog:loggingPrefs' => {browser: 'ALL'})
browser = Watir::Browser.new :chrome, desired_capabilities: caps

browser.execute_script('console.log("test");')

puts browser.driver.manage.logs.get(:browser)
#=> INFO 2019-06-13 21:48:03 -0400: console-ap

Not ideal to monkey patch, but good enough as a short-term fix. I’ll be keeping an eye on Issue 7270 to see if a better solution becomes available.

Posted in Selenium-Webdriver, Watir | Leave a comment

Clearing the way to checking clickability via Element#obscured?

Problem

When clicking an element, you are presented with an error like:

(Chrome)

Selenium::WebDriver::Error::UnknownError: unknown error: Element <button style=”width: 100px; margin: 40px 0;” id=”obscured”>…</button> is not clickable at point (58, 476). Other element would receive the click: < div style=”position: absolute; height: 100px; width: 100px; background: rgba(255,0,0,.5); margin-left: 40px; margin-top: -120px;”></div>

(Firefox)

Selenium::WebDriver::Error::ElementClickInterceptedError: Element <button id=”obscured”> is not clickable at point (58,480.16668701171875) because another element <div> obscures it

This means that there is an element on top of the element you are trying to click. A real user would not be able to click the element without doing some sort of action – eg waiting a couple of seconds for a temporary spinner to go away or scrolling the page so the element is no longer under a floating header.

Since a real user cannot do this action, the WebDriver spec has a step for checking this in element click:

If element’s container is obscured by another element, return error with error code element click intercepted.

Where obscured is defined as:

An element is obscured if the pointer-interactable paint tree at its center point is empty, or the first element in this tree is not an inclusive descendant of itself.

Over the past year, there have been a number of discussions (eg #532, #534) which partially boil down to – can we detect obscuring?

Example

The following page shows the common problem. An overlay is displayed when the page is first loaded, but eventually disappears after some asynchronous process complets.

<html>
  <head>
    <style>
      #overlay {
        position: fixed;
        display: block;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: rgba(0,0,0,0.5);
        z-index: 2;
        cursor: pointer;
      }
    </style>
  </head>
  <body>
    <div id="overlay"></div>
    <a href="#">my link</a>
    <script>
      setTimeout(function() {
        var e = document.getElementById("overlay");
        e.parentNode.removeChild(e);
      }, 5000);
    </script>
  </body>
</html> 

Clicking the link, before the overlay disappears, will result in the exception:

browser.link.click
#=> Selenium::WebDriver::Error::UnknownError

Solution

This came up in Stack Overflow recently and I think I found a possible solution. JavaScript has a elementFromPoint method, which returns the topmost element at the specified coordinates. We could check if the topmost element, at the point we are clicking, is the element we want to click. If it is the same or one of its descendants, then it’s not obscured and we won’t get an exception clicking it. If it is a different element, then it is obscured and we’ll get the exception.

This could be added to Watir as:

module Watir
  class Element
    def obscured?
      element_from_point_js = 'return document.elementFromPoint(arguments[0], arguments[1]);'
      element_at_point = execute_script(element_from_point_js, *center)
      ![self, elements.to_a].flatten.include?(element_at_point)
    end
  end
end

Instead of having to click-rescue-retry the clicking of temporarily obscured elements, you would be able to do:

element.wait_while(&:obscured?).click

I think this would solve at least the basic scenarios. There are likely some more complex scenarios that need to be considered/handled – ie there are some steps in the WebDriver spec that I do not fully understand. This is a good start, so I will put together a pull request.

Posted in Watir | Tagged | 3 Comments

Selecting another option. Fast.

Problem

I was looking at the following Watir code to randomly select a different dropdown option:

select = browser.select_list
candidates = select.options.map(&:text)
candidates.delete(select.selected_options.first.text)
select.select(candidates.sample)

Short and simple… but with poor performance. How can we make this faster?

Answer

Most of these commands are necessary. We cannot get away from finding the select list, retrieving the options and selecting an option. However, we do not need to compare the options using their text. While a human would do this, Watir has other means.

Each Watir::Element (technically the underlying Selenium::WebDriver::Element object) stores an unique reference ID, which is generated by the driver. This ID will be consistent regardless of how the element is located.

browser.select.wd.ref
#=> "0.715404377030527-1"
browser.selects.first.wd.ref
#=> "0.715404377030527-1"
browser.select(id: 'my_select').wd.ref
#=> "0.715404377030527-1"

When Watir checks if 2 elements are the same, this reference ID is what is compared. As the ID is stored in the object, no calls to the browser are needed to check it. Using these IDs for comparison, we can eliminate the text retrievals:

select = browser.select_list
candidates = select.options.to_a - select.selected_options
candidates.sample.select

Performance Comparison

Given a small dropdown with 3 options:

<html>
  <body>
    <select id="my_select">
      <option value="a" selected>Option A</option>
      <option value="b">Option B</option>      
      <option value="c">Option C</option>        
    </select>
  </body>
</html>

We can see from the Selenium Statistics gem that the performance increases when the text usage is removed:

Method Wire Calls Execution Time (s)
Comparing text 25 0.5
Comparing reference ID 9 0.2

Not only is the reference ID approach faster, it is also scalable. The below results are for a select list with 20 options. While the text approach gets slower, the reference ID approach remains constant.

Method Wire Calls Execution Time (s)
Comparing text 42 0.9
Comparing reference ID 9 0.2

 

Posted in Watir | Tagged , | Leave a comment

One-liner to wait until clicked element is removed

Problem

Buttons/links that trigger JavaScript processing before navigating to another page can cause timing issues. Watir, specifically Webdriver, does not know it needs to wait for the navigation. This can result in Watir continuing the execution even before the navigation starts. One way to combat this is by waiting for an element to go stale. Stale means the element is no longer on the page, which is a good indication that the page has navigated else where.

Using Element#stale? requires that you have located the element before the navigation starts. Note that locating is triggered when you interact with the element – eg click. I have typically done:

btn = browser.button(id: 'some_id')
btn.click
btn.wait_until(&:stale?)

Writing these 3 lines is tedious. Can it be shorter?

Solution

By using Object#tap we can make this a one-liner:

browser.button(id: 'some_id').tap(&:click).wait_until(&:stale?)
Posted in Uncategorized, Watir | Tagged , | 2 Comments

Identify the wire calls being sent

Identify the wire calls being sent

Problem

Knowing which wire calls are being sent to the driver/browser can be useful in understanding Watir’s behaviour, especially with performance issues.

How do you find out what wire calls are being made?

Answer

To output the wire calls being made, simply set Selenium’s logger level to :info:

Selenium::WebDriver.logger.level = :info

If you want to stop outputting the wire calls, you can reset the level back to the default:

Selenium::WebDriver.logger.level = :warn

Note that while we are updating Selenium::WebDriver, this solution also applies to Watir.

Example

As an example, we can look at @niartseoj’s performance question on Stack Overflow, “best option for returning visible list_items in a large collection of hidden elements”. The question had a simple page:

<div id="queue-body">
  <ul id="queue-list">
    <li class="message" style="display:none;">oculto</li>
    <li class="message" style="display:none;">oculto</li>
    <li class="message" style="display:none;">oculto</li>
    <li class="message">vidljiv</li>
    <li class="message">vidljiv</li>
    <li class="message">vidljiv</li>
    <li class="message">vidljiv</li>
  </ul>
</div>

Where using the below line to find all visible list items took a longer time than expected. For me the command took 1.26 seconds. Note that their actual use case was a list of 300 list items, which would make the problem worse.

browser.ul(id: 'queue-list').lis.find_all(&:visible?)

If we set the logger level to :info before calling this command, we can see that quite a few wire calls are made. There are 10 calls to find the list items and then another 14 to check which are displayed. More importantly, there were 3 wire calls made per list item – 1 to validate the tag name, 1 to check for existence and 1 to check visibility.

Selenium::WebDriver.logger.level = :info
browser.ul(id: 'queue-list').lis.find_all(&:visible?)

# Wire calls made to find the list items
# 22:42:31 Selenium -> POST session/931/element
# 22:42:31 Selenium    >>> http://127.0.0.1:9515/session/931/element | {"using":"id","value":"queue-list"}
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":{"ELEMENT":"0.0867-1"}}
# 22:42:31 Selenium -> GET session/931/element/0.0867-1/name
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":"ul"}
# 22:42:31 Selenium -> POST session/931/element/0.0867-1/elements
# 22:42:31 Selenium    >>> http://127.0.0.1:9515/session/931/element/0.0867-1/elements | {"using":"tag name","value":"li"}
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":[{"ELEMENT":"0.0867-2"},{"ELEMENT":"0.0867-3"},{"ELEMENT":"0.0867-4"},{"ELEMENT":"0.0867-5"},{"ELEMENT":"0.0867-6"},{"ELEMENT":"0.0867-7"},{"ELEMENT":"0.0867-8"}]}
# 22:42:31 Selenium -> GET session/931/element/0.0867-2/name
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":"li"}
# 22:42:31 Selenium -> GET session/931/element/0.0867-3/name
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":"li"}
# 22:42:31 Selenium -> GET session/931/element/0.0867-4/name
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":"li"}
# 22:42:31 Selenium -> GET session/931/element/0.0867-5/name
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":"li"}
# 22:42:31 Selenium -> GET session/931/element/0.0867-6/name
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":"li"}
# 22:42:31 Selenium -> GET session/931/element/0.0867-7/name
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":"li"}
# 22:42:31 Selenium -> GET session/931/element/0.0867-8/name
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":"li"}

# Wire calls made to check if the item is visible
# 22:42:31 Selenium -> GET session/931/element/0.0867-2/enabled
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":true}
# 22:42:31 Selenium -> GET session/931/element/0.0867-2/displayed
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":false}
# 22:42:31 Selenium -> GET session/931/element/0.0867-3/enabled
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":true}
# 22:42:31 Selenium -> GET session/931/element/0.0867-3/displayed
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":false}
# 22:42:31 Selenium -> GET session/931/element/0.0867-4/enabled
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":true}
# 22:42:31 Selenium -> GET session/931/element/0.0867-4/displayed
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":false}
# 22:42:31 Selenium -> GET session/931/element/0.0867-5/enabled
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":true}
# 22:42:31 Selenium -> GET session/931/element/0.0867-5/displayed
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":true}
# 22:42:31 Selenium -> GET session/931/element/0.0867-6/enabled
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":true}
# 22:42:31 Selenium -> GET session/931/element/0.0867-6/displayed
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":true}
# 22:42:31 Selenium -> GET session/931/element/0.0867-7/enabled
# 22:42:31 Selenium <- {"sessionId":"931","status":0,"value":true}
# 22:42:31 Selenium -> GET session/931/element/0.0867-7/displayed
# 22:42:32 Selenium <- {"sessionId":"931","status":0,"value":true}
# 22:42:32 Selenium -> GET session/931/element/0.0867-8/enabled
# 22:42:32 Selenium <- {"sessionId":"931","status":0,"value":true}
# 22:42:32 Selenium -> GET session/931/element/0.0867-8/displayed
# 22:42:32 Selenium <- {"sessionId":"931","status":0,"value":true}

My proposed answer was to check for the style attribute rather than checking visibility. This brought the execution time down to 0.14 seconds.

Selenium::WebDriver.logger.level = :info
browser.ul(id: 'queue-list').lis(style: false)

# Wire calls
# 22:53:43 Selenium -> POST session/f12/element
# 22:53:43 Selenium    >>> http://127.0.0.1:9515/session/f12/element | {"using":"id","value":"queue-list"}
# 22:53:44 Selenium <- {"sessionId":"f12","status":0,"value":{"ELEMENT":"0.308-1"}}
# 22:53:44 Selenium -> GET session/f12/element/0.308-1/name
# 22:53:44 Selenium <- {"sessionId":"f12","status":0,"value":"ul"}
# 22:53:44 Selenium -> POST session/f12/element/0.308-1/elements
# 22:53:44 Selenium    >>> http://127.0.0.1:9515/session/f12/element/0.308-1/elements | {"using":"xpath","value":".//li[not(@style)]"}
# 22:53:44 Selenium <- {"sessionId":"f12","status":0,"value":[{"ELEMENT":"0.308-2"},{"ELEMENT":"0.308-3"},{"ELEMENT":"0.308-4"},{"ELEMENT":"0.308-5"}]}
# 22:53:44 Selenium -> GET session/f12/element/0.308-2/name
# 22:53:44 Selenium <- {"sessionId":"f12","status":0,"value":"li"}
# 22:53:44 Selenium -> GET session/f12/element/0.308-3/name
# 22:53:44 Selenium <- {"sessionId":"f12","status":0,"value":"li"}
# 22:53:44 Selenium -> GET session/f12/element/0.308-4/name
# 22:53:44 Selenium <- {"sessionId":"f12","status":0,"value":"li"}
# 22:53:44 Selenium -> GET session/f12/element/0.308-5/name
# 22:53:44 Selenium <- {"sessionId":"f12","status":0,"value":"li"}

Inspecting the wire calls, we can better understand why there is a performance improvement. Watir no longer needs to validate the tag name of the hidden items – they are excluded by the generated XPath. The 2 wire calls made for checking existence and visibility were also eliminated.

Posted in Selenium-Webdriver, Watir | Tagged , | 3 Comments

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.

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 , , | 2 Comments