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

Replace Table#row_count and Table#column_column with an ElementCollection#count

Given a table:

<table>
  <tr>
    <td>Row 1 - Column 1</td>
    <td>Row 1 - Column 2</td>
  </tr>
  <tr>
    <td>Row 2 - Column 1</td>
    <td>Row 2 - Column 2</td>
  </tr>
  <tr>
    <td>Row 3 - Column 1</td>
    <td>Row 3 - Column 2</td>
  </tr>
</table>

You might want to check the number of rows and/or columns.

Obsolete Method

In Watir-Classic you may have retrieved these values using the row_count and column_count methods.

browser.table.row_count
#=> 3
 
browser.table.column_count
#=> 2

However, these methods will give an error in Watir-Webdriver:

browser.table.row_count
#=> NoMethodError
 
browser.table.column_count
#=> NoMethodError

Preferred Method

For Watir-Webdriver, you will need to count the tr and td elements using an elemnent collection:

browser.table.trs.count
#=> 3
 
browser.table.tr.tds.count
#=> 2

Note that this will return all tr and td elements in the table. Watir-Classic’s method ignored the nested tables, therefore, for nested tables, the equivalent Watir-Webdriver method will need to use the rows and cells methods:

browser.table.rows.count
#=> 3
 
browser.table.row.cells.count
#=> 2
Posted in Watir, Watir Migration, Watir-Classic | Leave a comment

Watir 6.0 is the end of the Watir metagem

The Watir gem started out as driver for IE through the OLE protocol. In 2012, version 4.0.0 changed Watir into a metagem that, based on the desired browser, used either Watir-Classic (the original implementation) or Watir-Webdriver (the Selenium backed implementation that supported additional browsers). The soon to be released Watir 6.0 will continue the evolution to being just Watir-Webdriver.

You can find out more about Watir 6.0 from:

What does this mean for Watir-Classic users?

Anyone using:

require 'watir'

Must update to using:

require 'watir-classic'

This is another reminder that support for Watir-Classic is limited. If possible, you should switch to Watir-Webdriver.

What does this mean for Watir-Webdriver users?

Anyone using:

require 'watir-webdriver'

Will get a deprecation warning unless they switch to:

require 'watir'

The default browser will be changed from Firefox to Chrome. This means that anyone using the default browser to start Firefox:

browser = Watir::Browser.new 

Will now need to explicitly specify Firefox:

browser = Watir::Browser.new :firefox
Posted in Watir, Watir-Classic, Watir-Webdriver | Leave a comment

Toggle a checkbox

Problem

Checking a checkbox is as simple as:

browser.checkbox.set

Clearing a checkbox is just as easy:

browser.checkbox.clear

What if you want to toggle the state of the checkbox – ie check it if its unchecked or uncheck it if it is checked?

Solution

The set? method returns the current state of the checkbox, therefore the negation is the state of the checkbox once toggled:

browser.checkbox.set?
#=> true
!browser.checkbox.set?
#=> false

We could write an if statement based on the set? method. However, we can find a simpler solution by passing a parameter to the set:

  • true – sets the checkbox
  • false – clears the checkbox

Putting it all together, we can toggle the checkbox by:

checkbox = browser.checkbox
checkbox.set(!checkbox.set?)
Posted in Watir, Watir-Classic, Watir-Webdriver | Tagged | 16 Comments

Unhiding the overflow:hidden in DevExtreme select lists

Problem

When Dev Silver asked me how to automate a DevExtreme select list, it seemed like a simple task. It looked like any other set of elements styled as a select list.

# Go to the page
browser = Watir::Browser.new
browser.goto('js.devexpress.com/Demos/WidgetsGallery/#demo/editors-select_box-overview')
iframe = browser.iframe(id: 'demo-frame')

# Find the dropdown
dropdown = iframe.div(id: 'products-simple')
dropdown.wait_until_present

# Open the list of dropdown options
dropdown.div(class: 'dx-dropdowneditor-button').click
list_container = iframe.div(class: 'dx-dropdownlist-popup-wrapper').div(class: 'dx-scrollable-container')
list_container.wait_until_present

# Click a list item
list_container.div(text: 'SuperLED 50').click

It worked! Well… at least for some options. Clicking some of the other options resulted in an exception:

list_container.div(text: 'Projector Plus').click
#=> Selenium::WebDriver::Error::ElementNotVisibleError

What was going on?

Solution

After investigating the DevExtreme HTML, I was able to narrow the problematic HTML to the following:

<div style="overflow:hidden; height:59px; width:200px; border:1px solid black;" class="dx-scrollable-container">
  <div style="line-height:20px" class="dx-list-item">HD Video Player</div>
  <div style="line-height:20px" class="dx-list-item">SuperPlasma 50</div>
  <div style="line-height:20px" class="dx-list-item">SuperLED 50</div>
  <div style="line-height:20px" class="dx-list-item">Projector Plus</div>
  <div style="line-height:20px" class="dx-list-item">ExcelRemote IR</div>
</div>

Which gets rendered as:

HD Video Player
SuperPlasma 50
SuperLED 50
Projector Plus
ExcelRemote IR

 

Notice that the outer div element has a style of overflow:hidden. This means that any content that does not fit within the element’s dimensions will be hidden to the user. There is no scrollbar for the user to see the hidden content. In other words, a user can see the first 3 list items, but not the last 2. WebDriver mimics the user – it can only see and click the first 3 items. The last 2 items are not visible and therefore cannot be interacted with.

browser.divs(class: 'dx-list-item').map(&:visible?)
#=> [true, true, true, false, false]

To interact with the overflowed/hidden items, they must first be made visible. The actual DevExtreme control does provide another element that mimics the scrollbar behaviour. However, with the control relying on mouseovers, it seemed a nuisance to automate. Instead, I chose to mimic the scrolling via a JQuery script written by James on StackOverflow:

list_item = list_container.div(text: 'Projector Plus')
script = '$(arguments[0]).scrollTop($(arguments[1]).offset().top - $(arguments[0]).offset().top + $(arguments[0]).scrollTop());'
iframe.execute_script(script, list_container, list_item)
list_item.click
Posted in Watir, Watir-Webdriver | Tagged , | 2 Comments

Translating a headers attribute to text

Problem

I was struggling to determine a cell’s headers associated via the headers attribute. The table was large with auto-generated ids, which created headers attributes like:

ctl00_ctl00_ctl00_MasterContent_MainContent_ReviewInputDetailsContent_MarginingCertificateGrid_GR_ctl01_GDH
ctl00_ctl00_ctl00_MasterContent_MainContent_ReviewInputDetailsContent_MarginingCertificateGrid_GR_ctl01_SR_ctl01_SDC
ctl00_ctl00_ctl00_MasterContent_MainContent_ReviewInputDetailsContent_MarginingCertificateGrid_GR_ctl01_SR_ctl01_FDR_ctl00_FDC
ctl00_ctl00_ctl00_MasterContent_MainContent_ReviewInputDetailsContent_MarginingCertificateGrid_GR_ctl01_SR_ctl01_LGR_ctl00_LGTDC

Finding each of the header cells became tedious, so I turned to Watir for a solution.

As a simplified example, let us consider the following table from the WCAG examples:

<table>
  <tr>
    <th rowspan="2" id="h">Homework</th>
    <th colspan="3" id="e">Exams</th>
    <th colspan="3" id="p">Projects</th>
  </tr>
  <tr>
    <th id="e1" headers="e">1</th>
    <th id="e2" headers="e">2</th>
    <th id="ef" headers="e">Final</th>
    <th id="p1" headers="p">1</th>
    <th id="p2" headers="p">2</th>
    <th id="pf" headers="p">Final</th>
  </tr>
  <tr>
    <td headers="h">15%</td>
    <td headers="e e1">15%</td>
    <td headers="e e2">15%</td>
    <td headers="e ef">20%</td>
    <td headers="p p1">10%</td>
    <td headers="p p2">10%</td>
    <td headers="p pf">15%</td>
  </tr>
</table>

From inspection, we can see that the headers attribute “e e2” will be read as “Exams 2”. How can we programmatically determine this?

Solution – Watir-Classic

The headers attribute is a space-deliminated list of element ids. We can split the attribute value into a list of ids and then locate each of the referenced elements:

require 'watir-classic'
browser = Watir::Browser.attach(:url, //)

headers = 'e e2'

headers.split(' ')
  .map { |header| browser.element(id: header, tag_name: ['th', 'td']).text }
  .join(' ')
#=> "Exams 2"

Note that this will find the cells anywhere on the page. If you are concerned about the header cell possibly being in another table, you can restrict the search scope to a specific table.

Solution – Watir-Webdriver

As the purpose of this script was to assist manual testing, it was written in Watir-Classic to leverage attaching to browsers. If you want to get the headers in Watir-Webdriver, the code is a little different. This is due to the different values supported by the :tag_name locator. Watir-Classic does not support regular expressions, which is a known bug, while Watir-Webdriver does not support arrays.

When searching multiple tag names, you can use a regular expression:

headers = 'e e2'

headers.split(' ')
  .map { |header| browser.element(id: header, tag_name: /^(th|td)$/).text }
  .join(' ')
#=> "Exams 2"
Posted in Watir, Watir-Classic, Watir-Webdriver | Tagged | Leave a comment

Returning Watir element from execute_script

Question

I noticed the following Stack Overflow answer by Jeff Price yesterday:

One thing we have done at times when a query becomes oppressive is to get out of the selenium/watir loop altogether and ask the browser to execute some javascript to grab the elements we want. Obviously this has some limitations especially if you need to use watir on the object you get back (you can’t). But if you are looking for something in particular, you can’t get much faster. I am assuming you are using jQuery, but in reality the JavaScript can be arbitrary and whatever you need.

The answer seemed to suggest that it is not possible to get a Watir::Element from the DOM element returned by execute_script. This is not how I remember it, but it has been a long time since I tried doing this.

What is the current behaviour?

Answer – Watir-Webdriver

From a code review of the Watir::Browser, Watir will take the value returned from Selenium-WebDriver’s execute_script method and, if a Selenium::WebDriver::Element, convert it to a Watir object:

def execute_script(script, *args)
  args.map! { |e| e.kind_of?(Watir::Element) ? e.wd : e }
  returned = @driver.execute_script(script, *args)

  wrap_elements_in(returned)
end

def wrap_elements_in(obj)
  case obj
  when Selenium::WebDriver::Element
    wrap_element(obj)
  when Array
    obj.map { |e| wrap_elements_in(e) }
  when Hash
    obj.each { |k,v| obj[k] = wrap_elements_in(v) }

    obj
  else
    obj
  end
end

def wrap_element(element)
  Watir.element_class_for(element.tag_name.downcase).new(self, element: element)
end

This suggests that Watir’s execute_script can return a Watir::Element. Let us see if this is true.

Assume that the page being worked on is:

<html>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
  </head>
  <body>
    <div>
      <span id="the_span">span text</span>
      <input type="text" id="the_text_field">
    </div>
  </body>
</html>

When using JavaScript to return the DOM element for the span, it can be seen that Watir creates the corresponding Watir::Element – ie a Watir::Span:

e = browser.execute_script('return document.getElementById("the_span");')
p e.class
#=> Watir::Span
p e.text
#=> "span text"

When the same is performed for the text field input, the results are not as great. A Watir::Input object is returned, which means that methods such as set will not be available. The code appears to be selecting the object type based on just the tag name (presumably a bug). At any rate, the desired Watir::TextField object can be retrieved by calling the to_subtype method.

e = browser.execute_script('return document.getElementById("the_text_field");')
p e.class
#=> Watir::Input

# Switch to the subtype to get the type specific methods
e = e.to_subtype
p e.class
#=> Watir::TextField
e.set('value')

Watir elements are also created when using jQuery to locate the element.

# The value returned by jQuery() will be an array of Watir elements
e = browser.execute_script('return jQuery("#the_span");')
p e.class
#=> Array
p e.first.class
#=> Watir::Span
p e.first.text
#=> "span text"

Answer – Watir-Classic

From the code in the page-container, it does not look like there is anything to return a Watir object.

def execute_script(source)
  result = nil
  begin
    source = with_json2_if_needed source
    result = document.parentWindow.eval(source)
  rescue WIN32OLERuntimeError, NoMethodError #if eval fails we need to use execScript(source.to_s) which does not return a value, hence the workaround
    escaped_src = source.gsub(/\r?\n/, "\\n").gsub("'", "\\\\'")
    wrapper = "_watir_helper_div_#{SecureRandom.uuid}"
    cmd = "var e = document.createElement('DIV'); e.style.display='none'; e.id='#{wrapper}'; e.innerHTML = eval('#{escaped_src}'); document.body.appendChild(e);"
    document.parentWindow.execScript(cmd)
    result = document.getElementById(wrapper).innerHTML
  end

  MultiJson.load(result)["value"] rescue nil
end

In fact, trying to return the DOM element results in an exception:

e = browser.execute_script('return document.getElementById("the_span");')
#=> WIN32OLERuntimeError: (in OLE method `execScript': )
#=>     OLE error code:80020101 in <Unknown>
#=>       Could not complete the operation due to error 80020101.
#=>     HRESULT error code:0x80020009
#=>       Exception occurred.
#=>         from C:/Ruby193/lib/ruby/gems/1.9.1/gems/watir-classic-4.1.0/lib/watir-classic/page-container.rb:31:in `method_missing'
#=>         from C:/Ruby193/lib/ruby/gems/1.9.1/gems/watir-classic-4.1.0/lib/watir-classic/page-container.rb:31:in `rescue in execute_script'
#=>         from C:/Ruby193/lib/ruby/gems/1.9.1/gems/watir-classic-4.1.0/lib/watir-classic/page-container.rb:24:in `execute_script'
#=>         from (irb):3
#=>         from C:/Ruby193/bin/irb:12:in `<main>'

However, with a little bit more work, you can directly interact with the OLE objects to get the desired results:

ole_object = browser.document.getElementById("the_span")
e = browser.span(ole_object: ole_object)
p e.text
#=> "span text"

Or when using jQuery:

ole_object = browser.document.parentWindow.eval('jQuery("#the_span")[0];')
e = browser.span(ole_object: ole_object)
p e.text
#=> "span text"

Conclusion

It is possible to take the DOM element returned from a script execution and turn it into a Watir object.

Posted in Watir, Watir-Classic, Watir-Webdriver | Tagged | Leave a comment