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.

Advertisements
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

Finding visible/hidden elements with the new :visible locator

Watir 6.0 introduces a new locator – :visible. This allows you to specify whether or not the element can be seen by the user.

Usage

Consider the following page where the first div is visible and first span element is hidden:

<html>
  <body>
    <div id="visible_div">can be seen by users</div>
    <div id="hidden_div" style="display:none;">cannot be seen by users</div>

    <span id="hidden_span" style="display:none;">cannot be seen by users</span>
    <span id="visible_span">can be seen by users</span>
  </body>
</html>

By default, Watir locates the first matching element regardless of visibility:

browser.div.id
#=> "visible_div"

browser.span.id
#=> "hidden_span"

By adding visible: true, the first matching visible element is returned:

browser.div(visible: true).id
#=> "visible_div"

browser.span(visible: true).id
#=> "visible_span"

By adding visible: false, the first matching hidden element is returned:

browser.div(visible: false).id
#=> "hidden_div"

browser.span(visible: false).id
#=> "hidden_span"

Not available for collections

In at least version 6.0.2, the :visible locator is not available for collections. Hopefully this is just an oversight and will be added in future releases – Issue 499.

browser.divs(visible: true).count
#=> C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/xpath_support.rb:5:in `escape': undefined method `include?' for true:TrueClass (NoMethodError)
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/locators/element/selector_builder/xpath.rb:48:in `equal_pair'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/locators/element/selector_builder/xpath.rb:32:in `block in attribute_expression'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/locators/element/selector_builder/xpath.rb:28:in `each'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/locators/element/selector_builder/xpath.rb:28:in `map'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/locators/element/selector_builder/xpath.rb:28:in `attribute_expression'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/locators/element/selector_builder/xpath.rb:18:in `build'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/locators/element/selector_builder.rb:122:in `build_xpath'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/locators/element/selector_builder.rb:103:in `build_wd_selector'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/locators/element/selector_builder.rb:49:in `build'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/locators/element/locator.rb:140:in `find_all_by_multiple'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/locators/element/locator.rb:60:in `locate_all'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/element_collection.rb:98:in `elements'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/element_collection.rb:84:in `to_a'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/element_collection.rb:28:in `each'
#=>   from watir-webdriver.rb:98:in `count'
#=>   from watir-webdriver.rb:98:in `<main>'

Performance impact

For the locator to work, Watir iterates through the elements to check their visibility. This can be seen in the Watir::Locators::Element::Locator. A wire call, el.displayed? is made for each potentially matching element:

elements = @query_scope.wd.find_elements(how, what)
elements = elements.select { |el| visible == el.displayed? } unless visible.nil?
elements[idx] unless elements.nil?

In other words, a simple call like browser.div(visible: true).id will need to check every single div on the page. Large number of elements, means slower execution. The following shows the increasing execution time based on the number of elements on the page:

Executed 10 Times 1 div 10 divs 50 divs 100 divs
browser.div.id 0.966645 1.011956 0.943130 0.931430
browser.div(visible: true).id 1.107269 2.511261 8.877038 17.534622

You may want to consider:

  • Only using :visible when necessary – ie as a tie-breaker for a locator that matches both a visible and hidden element.
  • When using :visible, use other locators to limit the potential matches (wire calls).

Does not change interaction rules

Note that this is just a locator. It does not change the fact that the element is hidden and therefore cannot be interacted with.

browser.div(visible: false).click
#=> C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/elements/element.rb:528:in `rescue in wait_for_present': element located, but timed out after 30 seconds, waiting for true condition on {:visible=>false, :tag_name=>"div"} (Watir::Exception::UnknownObjectException)
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/elements/element.rb:518:in `wait_for_present'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/elements/element.rb:535:in `wait_for_enabled'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/elements/element.rb:658:in `element_call'
#=>   from C:/Ruby23/lib/ruby/gems/2.3.0/gems/watir-6.0.2/lib/watir/elements/element.rb:114:in `click'
#=>   from watir-webdriver.rb:99:in `<main>'
Posted in Watir | Tagged , | Leave a comment

ParallelTests::RSpec::RuntimeLogger ignores first process

Problem

Inspired by a recent meeting with the team at Loblaws Digital, I have been looking at tools to improve the execution of my regression suite. To address an ever growing execution time, I turned to the ParallelTests gem. This gem offered the ability to group RSpec examples, which could then be run in parallel on multiple machines.

Using the default grouping by file size, resulted in a huge disparity in processing times. One machine finished in an hour, while another took almost 2 hours. Not particularly surprising given that these are UI tests.

There is an option to group tests by runtime. For this, ParallelTests needs a runtime history for each spec file. This can be generated by using the ParallelTests::RSpec::RuntimeLogger:

parallel_rspec spec -o "--format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log"

Unfortunately, this produces an empty log when using a single process. With multiple processes, test times were logged, but not for the first process. What is going on? How do we get the run times for the first (or only) process?

Solution

Digging into the ParallelTests::RSpec::RuntimeLogger, you can see that the log file is written by:

def start_dump(*args)
  return unless ENV['TEST_ENV_NUMBER'] #only record when running in parallel
  # TODO: Figure out why sometimes time can be less than 0
  lock_output do
    @example_times.each do |file, time|
      relative_path = file.sub(/^#{Regexp.escape Dir.pwd}\//,'').sub(/^\.\//, "")
      @output.puts "#{relative_path}:#{time > 0 ? time : 0}"
    end
  end
  @output.flush
end

Inspecting the ENV['TEST_ENV_NUMBER'] you will get “nil” for the first process and “2” for the second process. Due to the “nil”, the first line’s return will execute, skipping the logging. I do not understand why you would ever want to use this formatter and skip logging, but fine, let’s ignore that. Where does the ENV['TEST_ENV_NUMBER'] get set?

Looking through the repository, you can see the ParallelTests::Test::Runner.execute_command creates a set of values for later use as environment variables:

def execute_command(cmd, process_number, num_processes, options)
  env = (options[:env] || {}).merge(
    "TEST_ENV_NUMBER" => test_env_number(process_number, options),
    "PARALLEL_TEST_GROUPS" => num_processes
  )
  cmd = "nice #{cmd}" if options[:nice]
  cmd = "#{cmd} 2>&1" if options[:combine_stderr]
  puts cmd if options[:verbose]

  execute_command_and_capture_output(env, cmd, options[:serialize_stdout])
end

More importantly, the TEST_ENV_NUMBER value is defined by:

def test_env_number(process_number, options={})
  if process_number == 0 && !options[:first_is_1]
    ''
  else
    process_number + 1
  end
end

For the first process, an empty String is returned. When it gets used in execute_command_and_capture_output, the setting of the environment variable is done with:

"(SET \"#{k}=#{v}\")"

Note this means the environment variable is set using SET TEST_ENV_NUMBER= not SET TEST_ENV_NUMBER="". This is why ENV['TEST_ENV_NUMBER'] is “nil”.

The next question is what is the options[:first_is_1]? Can we set that value so that test_env_number returns the process_number + 1 instead? This takes us to the ParallelTests::CLI. Here we can see that the option is set to true when using the “–first-is-1” flag.

In the end, this long stroll through the code tells us that we can fix the logging by running:

parallel_rspec spec --first-is-1 -o "--format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log"
Posted in RSpec | Tagged | 1 Comment

Finding the next empty text field

Problem

As I continued to investigate making Watir-Classic tests compatible with Watir-Webdriver, I noticed one test that failed to input the next empty row of a text field grid. The grid could start empty or it could start with rows pre-populated with saved values:

<html>
  <body>
    <table>
      <tr>
        <td><input type="text" id="name_01" value="existing name"></td>
        <td><input type="text" id="value_01" value="existing value"></td>
      </tr>
      <tr>
        <td><input type="text" id="name_02"></td>
        <td><input type="text" id="value_02"></td>
      </tr>
      <tr>
        <td><input type="text" id="name_03"></td>
        <td><input type="text" id="value_03"></td>
      </tr>      
    </table>
  </body>
</html>

The test looked for the first row with an empty field and inputted it, repeating if multiple rows of data were needed. The Watir-Classic code boiled down to:

next_field = browser.text_field(id: /name/, value: '')
p next_field.id
#=> "name_02"

next_field.set('some text')
next_field = browser.text_field(id: /name/, value: '')
p next_field.id
#=> "name_03"

Sadly, the same code failed with Watir (previously named Watir-Webdriver):

next_field = browser.text_field(id: /name/, value: '')
p next_field.id
#=> unable to locate element, using {:id=>/name/, :value=>"", :tag_name=>"input or textarea", :type=>"(any text type)"} (Watir::Exception::UnknownObjectException)

Solution

The problem is that Watir’s XPath builder will look for an element that has the value attribute with an empty string value. Unfortunately, that is not the same as our element, which does not have any value attribute. We could address this by writing our own XPath locator that checks for the absence of the value attribute (or wait for the functionality to be added to Watir in Issue 345):

next_field = browser.text_field(xpath: './/input[@type="text"][not(@value)]')
p next_field.id
#=> "name_02"

next_field.set('some text')
next_field = browser.text_field(xpath: './/input[@type="text"][not(@value)]')
p next_field.id
#=> "name_02"

This only works for the first row after the pre-populated rows. As seen by the attempt to input the second blank row, Watir grabs the same row, which is no longer empty. I recently learned on Stack Overflow from Tom Walpole, that this is due to the value property being set by the set method, but not the value attribute. Selenium-WebDriver only checks the attribute, which as we have not changed, results in the same row being returned each time.

Another option would be to exploit the fact that Watir will check the value property when it cannot build an XPath expression. Locators using non-trivial regular expressions, such as that for our empty string /^$/, will trigger this behaviour.

next_field = browser.text_field(id: /name/, value: /^$/)
p next_field.id
#=> "name_02"

next_field.set('some text')
next_field = browser.text_field(id: /name/, value: /^$/)
p next_field.id
#=> "name_03"

As you can see, we got the desired result. Note that this may break in future releases. Finding ways to convert regular expressions into XPath provides an improvement boost in locators. While Watir currently only optimizes a couple of scenarios, there is the possibility that more will be done. Those changes could result in the above code once again checking the value property. If you want to avoid that risk, you may want to explicitly check the property:

next_field = browser.text_fields(id: /name/).find { |t| t.value.empty? }
p next_field.id
#=> "name_02"

next_field.set('some text')
next_field = browser.text_fields(id: /name/).find { |t| t.value.empty? }
p next_field.id
#=> "name_03"
Posted in Watir, Watir Migration, Watir-Webdriver | Tagged , , | Leave a comment