dinsdag 9 december 2008

WatiN roadmap

Today I received a question on the mailing list about the status of WatiN 2.0. I thought I publish my response here as well.

No release date(s) yet, but I can report about the progress and speculate about a roadmap:

  • Currently the development code (a continuation of the 1.3 code base) is refactored to add support for testing with different browsers (IE, FireFox for now). I estimate I'm at 60% of the changes that need to be done to get the basic stuff to work.
  • Next is the integration of the firefox specific code from the 2.0 CTP version into the development code. I estimate that I will go for a first integration around the end of this month.

So my current guesstimate is that a beta or final release of 2.0 will be earliest at the end of q1 2009.

In the meantime Edward Wilde will update the current 2.0 CTP version to add support for FireFox 3.x. We haven't spoken about a release date be hopefully this refresh will be available early next year.

I will also release WatiN 1.3.1 in December with some important bug fixes.

So nothing final yet but I hope this gives you (all) a feel of what we are aiming for.

WatiN Find.ByDefault explained

My next post will be Part III of Supporting custom elements with WatiN. I got so excited about the possibilities that I started working on a wrapper for the Ra-Ajax controls as a show case. It is becoming very COOL and its fun to create as well. But as I said in my next post I'll show you more.

This post is all about a new feature of WatiN 1.3 which might not be that discoverable so I felt it needed some promotion.

Find.ByDefault

When you want to automate some action against an element on a web page, WatiN offers you different types of elements (TextField, Button, etc) to find an element. Each of these offer overloads accepting something to find the element by. For example, if you want to find a TextField by its id ("firstName_Id" in this example) you would typically have this code:

var firstNameField = ie.TextField("firstName_Id");

Which is the same as:

var firstNameField = ie.TextField(Find.ByDefault("firstName_Id"));

And with the default Settings applied, it is the same as:

var firstNameField = ie.TextField(Find.ById("firstName_Id"));

In this post I'll show you how you can change the implementation and behavior of Find.ByDefault() by creating your own default factory class and assigning it to Settings.DefaultFinderFactory .

Make Find.ByName the default

If your site makes heavy use of the name attribute on elements (instead of ids) and you use these to find the elements in your tests, you would typically write code like:

var firstNameField = ie.TextField(Find.ByName("firstName_Name"));

Lets make Find.ByName() the default so your code will look like this:

var firstNameField = ie.TextField("firstName_Name");

To make this work we need to create a new class which implements IFindByDefaultFactory. In the implementation of the ByDefault(string) method we can simply return Find.ByName(value). This is the implementation of the class:

public class FindByNameFactory : IFindByDefaultFactory
{
public BaseConstraint ByDefault(string value)
{
  return Find.ByName(value);
}
    public BaseConstraint ByDefault(Regex value)
{
  return Find.ByName(value);
}
}

The last thing we need to do is register this new factory class and Find.ByName will be the new default:

Settings.FindByDefaultFactory = new FindByNameFactory();

Handle ASP ids with

Another very useful way we could leverage this new feature is dealing with ASP.Net ids. When you automate a website which is build with ASP.Net webforms you probably have to deal with very long ids. By default ASP.Net creates unique ids by concatenating all of the ids of the container controls a control is in. For instance if a TextBox to enter a first name is placed within a tab control container and this is placed in a placeholder, the resulting id could be something like "plc_Main$tab_Details$txt_FirstName". With WatiN you have several ways to find this TextField by id.

Option 1: Using the full Id
The simplest option to find the control is to use the full id. Example:

ie.TextField("plc_Main$tab_Details$txt_FirstName").TypeText("Jeroen");

There are two disadvantages to this approach:
- It relies heavily on the ids of the controls the ASP:TextBox control is placed in. If you remove/rename or add another container to the stack, the TextField won't be found cause the id is changed.
- The longer the id gets the less readable this line of code becomes. Although you can (should in my opinion) abstract the way you find your controls by using a Page model, this still is massy.

Option 2: Using a regular expression
The second option is to find the control by using the overload on TextField which accepts a regular expression. In the following example we create a Regex which matches TextFields ending (hence the $ sign) with "txt_FirstName" :

ie.TextField(new Regex("txt_FirstName$")).TypeText("Jeroen");

Option 3: Make WatiN handle ASP ids by itself

public class FindByAspIdFactory : IFindByDefaultFactory
{
public BaseConstraint ByDefault(string value)
{
  return Find.ById(new Regex(value + "$"));
}
    public BaseConstraint ByDefault(Regex value)
{
  return Find.ById(value);
}
}

Register this new factory class:

Settings.FindByDefaultFactory = new FindByAspIdFactory();

And you can find controls with ASP ids like:

ie.TextField("txt_FirstName").TypeText("Jeroen");

Using (magic) prefixes

If you don't like to always use a regular expression, like we did in the previous example, you could implement something like the Incisif automation framework offers. They choose to put a prefix in front of the ASP id. So only if the specific prefix (a:) is in the id a lookup with a Regex will be done to find the element. Example:

ie.TextField("a:txt_FirstName").TypeText

and the Factory implementation:

public class FindByAspIdFactory : IFindByDefaultFactory
{

public BaseConstraint ByDefault(string value)
{
  if (value.StartsWith("a:")
  {
   Regex regex = new Regex(string.format("(.*\${0}$)|(.*_{0}$)", value));
   return Find.ById(regex);
  }
  return Find.ById(value);


public BaseConstraint ByDefault(Regex value)
{
  return Find.ById(value);
}
}

Other possible uses

Put the attribute you want to match with in front of the value

ie.TextField("id:txt_FirstName").TypeText
ie.TextField("name:txt_FirstName").TypeText

A ruby like notation to express a regular expression (starting and ending with a forward slash) instead of having new Regex() all over your code:

ie.TextField("/txt_FirstName$/").TypeText

And I'm sure you can come up with many more possible uses.

Enjoy!