October 16, 2006

Extending Watir to Support Additional Elements

Last week my colleague Jim Matthews and I added support for map and area elements to Watir. Watir users often ask how they can get Watir to support new elements, such as list items (”<li>“), so I thought I would walk through our new code.

Here’s the code for map:

 module Watir
   class Map < NonControlElement
     TAG = 'MAP'
   module Container
     def map(how, what)
       return Map.new(self, how, what)

First, we created the Map class. All we had to do was specify the tag that indicates a map in HTML (”<MAP>“). The rest of the behavior for the class is inherited from NonControlElement.

Second, we created the map method that users will use. This is added to the Container module. Modules are used to allow methods to be added to different classes. In Watir, most of the classes include Container: the IE class, the Frame class, and all the element classes (include NonControlElement). Therefore this method can be called as ie.map(), or @ie.frame().map() or even ie.table().cell().map().

This method is simply a wrapper around a call to create an instance of the Map class that we just created. The standard element constructor needs not only the “how” and the “what” but also a reference to the containing object. This is ie in the case of ie.map(), but a cell in the case of ie.table().cell().map(). It is the Container object, and in Ruby self tells us what the current object is (because its class included the Container module).

We write basically the same code for the area element. As demonstrated by Jim’s tests, this is all the code necessary to get the behavior you expect Watir to provide. You can reference maps and areas by :name, :url, :id, or :alt. You can even use multiple attributes. You can reference an area in a map. And you get an exists? method that returns true or false depending on whether the referenced element exists on the page, a click method (for clicking), and name, url, id and alt methods that you can use to check the attributes of the referenced element. All of that comes in through inheritance.

I’ve been slow to document how to extend Watir like this for one reason. I thought this was too complicated. It seemed to me that this basic pattern was repeated enough times that we should just replace it with something like this:

  Watir.define_element :map

Ruby certainly makes it pretty easy to define powerful functions like this. The define_element method could create the class the add a method to the container class. This is sometimes the right thing to do. It is central to the Don’t Repeat Yourself philosophy.

But from working with Jim and experimenting with Rails, I’ve learned that this ultra-condensed approach undermines understandabality. By using effectively six lines of code instead of one, we also provide a framework that can be expanded further.

Suppose we wanted ie.map(:name, 'map1').area(:alt, 'Texas').accesskey to return the character that highlights the area. The “verbose” code makes it pretty clear where we would have to define this method:

 module Watir
   class Area < NonControlElement
     TAG = 'AREA'
     def accesskey
       // more code here...
Posted by bret at 12:30 PM | Comments (1)