Wednesday, February 9, 2011

Web Application Testing in Ruby: How to wait in Watir?

Web Application Testing in Ruby: How to wait in Watir?

1 - New Windows
Navigating to a new page with the existing IE window will not require you to define any special behavior. However, if you perform an action that brings up a secondary page (pop-up) you will need to define some code to wait on it's existence. Luckily, if you can deal with pop-ups with the "attach" method, there is already an implicit wait in the attach method. If you have a modal dialog box or other windows related dialog box (Security Alert Box, Login, File Download, etc.) you will need to create your own wait methods. See this section for more info.

2 - Objects that depend on Javascript to show up
Sometimes IE thinks it's done before everything is loaded. Unfortunately, IE#wait doesn't always work here because it exits whenever IE thinks it's done. So, if this is your problem, you will need to tell Watir to wait. There are several ways to solve this, here are two.

If you want to wait forever, consider the following:

sleep 1 until ie.text.include? "new results are loaded"
This code will keep sleeping until the text "new results are loaded" appears on the page. However, you may not want to wait forever. Using the following code will wait; for the text "new results are loaded" to appear on the page OR 60 seconds, whichever comes first.

wait_until {ie.text.include? "new results are loaded"}
Sometimes you need to wait for something to happen in the Application under test before you interact with it. Sleep statements are hardcoded and lock you down into a certain number of seconds before moving through your test. To avoid that, we've written a polling mechanism in the latest versions of Watir - the wait_until method.

An example might be that you're loading the Google home page and for some reason it's taking time to load. Here's a basic contrived script with a sleep statement.

require 'watir'

browser = Watir::Browser.start('http://www.google.com')
sleep 5 # we need to wait for the page to load and on a subjective basis I've chosen 5 seconds which works on my machine
browser.text_field(:name, 'q').set('ruby poignant')
....
Unfortunately the sleep is hardcoded and doesn't work for anyone else on my team who have slower network connections, my connection has gotten faster, but it still waits for 5 seconds before setting the text field.

Watir has a Watir::Waiter class with a wait_until method that can poll for a certain condition to return true before continuing on or erroring out. By default it checks the condition every half second up until 60 seconds. So I rewrite my code to look like this:

require 'watir'
browser = Watir::Browser.start('http://www.google.com')
# In this case all I care about is the text field existing, you could
# check title, text, anything you're expecting before continuing
Watir::Waiter::wait_until { browser.text_field(:name, 'q').exists? }
browser.text_field(:name, 'q').set('ruby poignant')
...
It now works for me with a half second delay, but also works for the other members of my team who have network delays up to a minute. If you're considering using sleep, use wait_until instead. It will make your test code more resilient to timing issues in those cases where you really need to use it.

3 - Handling asynchronous javascript / AJAX
if you include asynchronous JS in your definition of done, you'll need to code around that specifically. There's an open feature request to have Watir's wait method track XHRs and timers that are launched when the page is loaded, and wait for them as well. That may be tricky to do though, and with an all-volunteer project, it really depends on someone making the time to do it.

In lieu of that, one option is having the application keep track if XHRs and timers that it kicks off, and setting the some value to true when they are all complete. For example:

def wait_until_loaded(timeout = 30)
start_time = Time.now
until (however_your_application_reports_its_loaded == 'true') do
sleep 0.1
if Time.now - start_time> timeout
raise RuntimeError, "Timed out after #{timeout} seconds"
end
end
end
A simpler option is:

tries = 0
until browser.link(:text, /link_to_wait_for/).exists? do
sleep 0.5
tries += 1
end
browser.link(:text, /link_to_wait_for/).click
end
Another option is to retry until timeout or no exception is raised.

def rescue_wait_retry(exception = Watir::Exception::UnknownObjectException, times = 10, seconds = 2, &block)
begin
return yield
rescue exception => e
puts "Caught #{exception}: #{e}. Sleeping #{seconds} seconds." if $DEBUG
sleep(seconds)
@ie.wait
if (times -= 1)> 0
puts "Retrying... #{times} times left" if $DEBUG
retry
end
end
yield
end

@ie.link(:url, %r|confirmdelete\.action\?educationId|).click #This starts some ajax stuff
rescue_wait_retry { @ie.button(:id, 'form_0').click }
Not sure about this one but this is a good introduction to the problem.

Also, from the thread "[wtr-general] SUMMARY: Enable/disable JS support in IE"

Q - How to check from watir code whether web page was reloaded or it was updated dynamically withJS.
A - Not so easy to implement for HCOM application due to a lot of ajax stuff. The following code should work when JS is disabled, plus we can pass additional condition to check here (e.g. waiter for object appearance on the page). Note: $ie is global IE instance in our framework. Also I think that we need to use something like click_no_wait method to reload the page, otherwise IE.wait() method won't let us do the check until page is ready. What I was asking here is how to check that page is reloading rather than just some elements on the page are updated with JS.

class Page
def loading?(additionalCondition = false)
if $ie.ie.busy || ($ie.ie.readyState != 4) || additionalCondition
true
else
false
end
end
end
4 - Yourself while you watch a script execute
Maybe you are watching the script execute and you just want to slow things down for troubleshooting reasons. Of course, there is always the venerable (and oft overused) "sleep" method. Using "sleep( x )" will cause the script to pause for x seconds.

If, however, you are annoyed by the slowness of your script, you can always make it faster:

ie = Watir::IE.new
ie.speed = :fast

or

ie = Watir::IE.new
ie.speed = :zippy
See this for more information.

click_no_wait
Information here about what this method is all about.

Continuing testing if IE refuses to load a page
When IE.attach does not find a window it throws a NoMatchingWindowFoundException. You can catch the exception instead of letting watir exit your tests. At this point you can build some 'recovery' if you need it or simply continue or exit your test explicitly after logging some test data.

require "watir"

begin
@ie = Watir::IE.attach(:url , /Webpage Dialog/) #let's say the window does not exist on the desktop
rescue Watir::Exception::NoMatchingWindowFoundException => e
puts "Failed to find new pop up window! #{e}"
# you can now recover or exit or continue
end
When IE.goto is invoked it implicitly waits for the page to load and runs any registered @error_checkers. If you don't have them added to browser then you can explicitly call check_for_http_errors. On HTTP errors encountered the Exception will be thrown. You can rescue the exception and continue with your test and recover or just exit after logging some test data.

require 'watir'
@ie = Watir::IE.new #start new browser window
@ie.goto 'http://asdf asdf.com' #navigation to this page will load Can Not Find Server page #notice the space in url string

begin
@ie.check_for_http_error # will throw Exception
rescue Watir::Exception::NavigationException => e #catch the Exception
puts "Page did not load: #{e}" # => 'Cannot find server or DNS Error'
# you can now recover or exit or just continue
end

# OR you can register error_checker which will be called when goto is invoked
require 'watir/contrib/page_checker'
@ie.add_checker(PageCheckers::NAVIGATION_CHECKER)

begin
@ie.goto "http://marekj.com/asdfasdfasdfa"
rescue Watir::Exception::NavigationException => e #catch the Exception
puts "Page did not load: #{e}" # => 'HTTP 404 - File not found'
# you can now recover or exit or just continue
end

No comments:

Post a Comment