Wednesday, April 7, 2010

Watir 102: Javascripting XPCOM to remove session cookies in a FireWatir script

Yesterday (see previous entry) I was playing with FireWatir and found that I needed to remove session cookies to ensure a clean starting point for my script. XPCOM allowed me to read back cookies and it seemed very likely that it would also facilitate deletion.

The Mozilla Developer Center cookies examples were very close to what I needed. nsICookieManager.remove seemed perfect. In a FireWatir script it looked like what I needed to read/write cookies was something like this:

def removeCookie(brwsr, host, name, path)
   cmd = "Components.classes['@mozilla.org/cookiemanager;1']"
   cmd += ".getService(Components.interfaces.nsICookieManager)"
   cmd += ".remove(\"" + host + "\", \"" + name + "\", \"" + path + "\", false)";
   puts "Removing cookie " + name + " from " + host + " for path " + path
   result = brwsr.execute_script "#{cmd}\n"   
end

def readCookies(brwsr, site)
   cmd = "var uri = Components.classes['@mozilla.org/network/io-service;1'].getService(Components.interfaces.nsIIOService)"
   cmd += ".newURI(\"" + site + "\", null, null);"
   cmd += "Components.classes['@mozilla.org/cookieService;1']"
   cmd += ".getService(Components.interfaces.nsICookieService)"
   cmd += ".getCookieString(uri, null);"
   result = brwsr.execute_script "#{cmd}\n"   
end

In my initial attempts to use this I passed in "http://myhost" and although the method succeeded subsequent logging of the updated cookie string showed no change at all. After some fumbling around I discovered a detailed example of manipulating cookies through javascript in an extension. The main difference in their example was no protocol:// in front of the host/domain name. After that it worked perfectly. A simple example follows:

require "watir"

protocol = "http";
host = "mytesthost";
initial_path = "/somethingfuntotest"

site = protocol + "://" + host

def url(brwsr)
   return brwsr.js_eval("document.location.toString();")
end

def readCookies(brwsr, site)
   cmd = "var uri = Components.classes['@mozilla.org/network/io-service;1'].getService(Components.interfaces.nsIIOService)"
   cmd += ".newURI(\"" + site + "\", null, null);"
   cmd += "Components.classes['@mozilla.org/cookieService;1']"
   cmd += ".getService(Components.interfaces.nsICookieService)"
   cmd += ".getCookieString(uri, null);"
   result = brwsr.execute_script "#{cmd}\n"   
end

#Ref https://developer.mozilla.org/en/nsICookieManager
#Ref http://www.springenwerk.com/2007/07/using-cookies-with-firefox-extensions.html
#Usage removeCookie(brwsr, "myhostname", "SessionIdCookieName", "/")
#Note that passing http://myhostname for host does not appear to work
def removeCookie(brwsr, host, name, path)
   cmd = "Components.classes['@mozilla.org/cookiemanager;1']"
   cmd += ".getService(Components.interfaces.nsICookieManager)"
   cmd += ".remove(\"" + host + "\", \"" + name + "\", \"" + path + "\", false)";
   puts "Removing cookie " + name + " from " + host + " for path " + path
   result = brwsr.execute_script "#{cmd}\n"   
end

Watir::Browser.default = "firefox"
brwsr = Watir::Browser.new

#May have a session id
puts "Cookies for " + site + ": " + readCookies(brwsr, site)
removeCookie(brwsr, host, "JSESSIONID", "/")
#Session id is now gone ... yay!
puts "Cookies for " + site + ": " + readCookies(brwsr, site)

Tuesday, April 6, 2010

Watir 101: Javascripting XPCOM to read cookies in a FireWatir script

Today I ran into the need to automate a web UI built primarily of html and javascript. HtmlUnit based solutions seemed nice but Watir seemed like an ideal solution as it would necessarily produce the exact same results a real user would experience.

First step was to get setup, following the instructions at http://watir.com/installation/#win. Despite my complete lack of experience with both Ruby and Watir things went pretty smoothly. My setup sequence was:

  1. Install Ruby (http://rubyforge.org/frs/download.php/29263/ruby186-26.exe)
  2. At new cmdprompt:
    1. We need a new command prompt because one open from before the Ruby install will not notice the updated environment variables and thus will not have ruby on PATH
    2. Note that the second command is slow as hell (at least it was for me)
      gem update --system
      gem install watir
      
  3. Install FF JSSH plugin; ref http://watir.com/installation/#win

  4. Poke around a couple of helpful documents to try to learn the basics

  5. Run scripts similar to ruby my_script.rb

Scripting the basic interaction went pretty smoothly. Obtaining a Firefox browser and jumping around is every bit as straight forward as you might hope. Obtaining the current url is slightly more problemmatic than one might help but luckily others have solved this problem already. Here is an example of a simple script:
require "watir"

site = "http://my.test.host"
initial_path = "/blah"

def url(brwsr)
   return brwsr.js_eval("document.location.toString();")
end

Watir::Browser.default = "firefox"

puts "Requesting a new " + Watir::Browser.default
brwsr = Watir::Browser.new

puts "Loading " + site + initial_path
brwsr.goto site + initial_path
puts "At " + url(brwsr)

puts "Selecting and navigating forward"
brwsr.radio(:name, "inputRadio").set
brwsr.button(:name, "Continue").click
puts "At " + url(brwsr)

The only real problem with basic scripting functionality I ran into (as yet unresolved) is that if something takes longer than Watir likes to wait (say the browser is slow to pop or a page is slow to respond) the script gets ahead of the browser and gets severely confused.

So now for the first real problem I ran into: Cookies. The application I want to test is session-aware so ultimately I will want to nuke cookies at the beginning of the test. First step, lets see if we can get cookie information back. After a bit of searching it turns out manipulating cookies from Watir is less simple than one might hope. For IE people seem to simply use Ruby to delete the files out of the filesystem (see FAQ). For Firefox we can alter cookies via XPCOM, Mozilla even has an example.

This is all well and good but how can we execute javascript against XPCOM objects from Ruby code running FireWatir?? Luckily I found a great series of articles that were almost what I wanted:

The only problem was that the example called for rebuilding some of the code ("I opened up the Firewatir::Firefox class and added...") for FireWatir. This would definitely be stretching my fledgling Ruby/Watir skills so I went looking for a simpler approach.

The browser object for Firefox (FireWatir::Firefox) exposes the wonderful execute_script method. The description "Executes the given JavaScript string" sounds pretty promising. A quick check of the source firefox.rb file showed that the method is a thin wrapper around the same JSSH interaction sequence that is used in the http://sticklebackplastic.com articles. This all seemed almost too good to be true so I decided to see if it actually worked the way it appeared. Luckily it works just as you would hope; the following example shows Ruby code reading cookies for whatever domain it happens to be interested in:

require "watir"

site = "http://my.test.host"
initial_path = "/blah"

def prepare(brwsr, site)
   puts "Preparing..."
   cmd = "var uri = Components.classes['@mozilla.org/network/io-service;1'].getService(Components.interfaces.nsIIOService)"
   cmd += ".newURI(\"" + site + "\", null, null);"
   cmd += "Components.classes['@mozilla.org/cookieService;1']"
   cmd += ".getService(Components.interfaces.nsICookieService)"
   cmd += ".getCookieString(uri, null);"
   result = brwsr.execute_script "#{cmd}\n"
   
   puts "Cookies for " + site + ": " + result
end

Watir::Browser.default = "firefox"

puts "Requesting a new " + Watir::Browser.default
brwsr = Watir::Browser.new

showcookie(brwsr, site)
showcookie(brwsr, "http://www.bing.com")
Hopefully a similar mechanism will allow me to alter cookies but that's as far as I've made it so far. Seems quite promising! And there are lots of cool names (Watir, Ruby, JSSH, XPCOM, ...) involved, always a plus!

I was very impressed that starting with no knowledge of Ruby or Watir I was able to get everything installed and create a functional proof of concept script (albeit one very low on validation or error checks) for a semi-complex sequence (add item to cart and checkout) in a couple of hours! If I can get cookie removal working to ensure a clean start state this could grow to be a very valuable verification tool for our developers.