woensdag 12 november 2008

Supporting custom elements with WatiN, Part II

12 Nov 2008: Updated section about creating an extension method.

In part I of this series I explained why you should create wrappers and how you could create a basic wrapper for your custom or third party controls. In this post I will show you

  • how to make use of the deferred find execution
  • how to use extension methods to make it all look integrated with WatiN

What is Deferred Find Execution

Deferred Find Execution in WatiN means that the search for an element in the current web page is done the first time you get a property value, set a property value or call a method on that element. An example:

using(IE ie = new IE())
{
TextField name = ie.TextField("name");

ie.GoTo(new Uri(Util.HtmlTestBaseURI, "EnterYourName.htm"));
name.TypeText("Jeroen");
}

The first line will open a new Internet Explorer instance showing you an empty page (about:blank). Next we set the name variable to point to an input element of type text with the id 'name'. If WatiN did an immediate lookup it would throw an ElementNotFound exception cause these elements aren't on the page shown in the browser. But WatiN doesn't throw an exception, it actually returns an instance of TextField for which the find isn't executed yet. Then the browser navigates to a page with this name element on it. When calling TypeText on the TextField instance, WatiN will search for (and find) the input element with id 'name' before executing the TypeText action. So finding of the name element is deferred until the code actually needs a reference to that element.

WatiN heavily relies on this behavior so it seems like a good idea to play with the rules.

Implementing deferred find execution

To make this work for the DateTimeElement an element finder class needs to be passed to the base class of DateTimeElement. Lets replace the current constructor, which accepts a Table instance, with one that calls the base constructor accepting an INativeElementFinder instance. For readability I created a private method CreateElementFinder which does the actual creation of the INativeElementFinder instance. The base constructor also needs a DomContainer instance which can be provided by our IE instance in our test code. Here is the reviced code for DateTimeElement:

public class DateTimeElement : Table
{
public DateTimeElement(DomContainer domContainer, string id) :
base(domContainer, CreateElementFinder(domContainer, id)) { }

public void TypeDateTime(DateTime value)
{
TextField(new Regex("^date$")).TypeText(value.ToString("dd-MM-yyyy"));
TextField(new Regex("^time$")).TypeText(value.ToString("hh:mm:ss"));
}

private static INativeElementFinder CreateElementFinder(DomContainer domContainer,
string id)
{
return domContainer.NativeBrowser.CreateElementFinder(ElementTags,
Find.ById(id),
domContainer);
}
}

And here is how the code in our test looks like:

DateTimeElement element = new DateTimeElement(ie, "datetime2");
element.TypeDateTime(new DateTime(2008, 11, 1, 12, 00, 00));

All in all a small code change for a big change in behavior.

Introducing CustomElementFinderHelper

In the above code I choose to create the ElementFinder instance inside the DateTimeElement class. I did this because my focus was on introducing deferred find execution (always make small steps). But if we are going to create more wrappers for our custom or third party controls, this CreateElementFinder method seems to be needed in all of them. So lets do some refactoring and move this method into a new class which will be passed into the constructor of DateTimeElement. Here is the new class CustomElementFinderHelper:

public class CustomElementFinderHelper
{
private readonly DomContainer _domContainer;
private readonly BaseConstraint _constraint;

public CustomElementFinderHelper(DomContainer domContainer,
BaseConstraint constraint)
{
_domContainer = domContainer;
_constraint = constraint;
}

public DomContainer DomContainer
{
get { return _domContainer; }
}

public INativeElementFinder CreateElementFinder(ArrayList supportedTags)
{
return _domContainer.NativeBrowser.CreateElementFinder(supportedTags,
_constraint,
_domContainer);
}
}

And this is our cleaned up DateTimeElement code:

public class DateTimeElement : Table
{
public DateTimeElement(CustomElementFinderHelper helper) :
base(helper.DomContainer, helper.CreateElementFinder(ElementTags)) { }

public void TypeDateTime(DateTime value)
{
TextField(new Regex("^date$")).TypeText(value.ToString("dd-MM-yyyy"));
TextField(new Regex("^time$")).TypeText(value.ToString("hh:mm:ss"));
}
}

And to top things of, the extension method

Our last refactoring has turned our testing code into an unreadable mess:

CustomElementFinderHelper helper = new CustomElementFinderHelper(ie, Find.ById
("datetime2"));
DateTimeElement element = new DateTimeElement(helper);
element.TypeDateTime(new DateTime(2008, 11, 1, 12, 00, 00));

The intention is completely lost and there is stuff we need to do over and over again if we use a DateTimeElement in our test automation. Again refactoring to a method will help, but wouldn't it be nice if we could use the same syntax in our tests as we use to find for instance a TextField. So we need to extend the IE class in some way.

One option would be to create a new sub class of the IE class, add a new method to find the DateTimeElement and use the sub class in all our tests.

A new and simpeler solution to get the same intellisense support and readable code in your tests, is the use of an extension method. We do have several options for our extension. We could use the IE class to extend. But a better candidate would be the DomContainer (inheritted by IE) so that this extension method is also available on the HtmlDialog class (for html popup dialogs). With our current implementation of CustomElementFinderHelper extending DomContainer seems to be the logical choice. So lets create a static DateTimeElementExtensions class with an extension method on DomContainer:

public static DateTimeElement DateTimeElement(this DomContainer domContainer,
string id)
{
CustomElementFinderHelper helper = new CustomElementFinderHelper(domContainer,
Find.ById(id));
return new DateTimeElement(helper);
}

And thanks to the magic of extension methods the code in our test now looks like:

ie.DateTimeElement("datetime2").TypeDateTime(new DateTime(2008, 11, 1, 12, 00, 00));

This refactoring cleans up the code big time and makes it look like DateTimeElement is part of the WatiN API itself. Ain't that sweat.... :-)

If your still using C# 2.0, first go to your manager and (again) explain why you need to move to C#3.0 (and it's a breeze!). If the answer (again) is NO than remove the this keyword from the method signature and you can call it like this:

DateTimeElementExtensions.DateTimeElement(ie, "datetime2").TypeDateTime(new
DateTime(2008, 11, 1, 12, 00,00));

Almost there

And now you want to access a DateTimeElement as a child from a Frame, Div or Table element..... That is a no go with the current solution.

In part III I will show you how we can fix this by making some changes to the CustomElementFinderHelper and again the magic of extension methods. But this time on one of WatiN's interfaces.

vrijdag 7 november 2008

Supporting custom elements with WatiN, part I

When using WatiN it is easy to automate simple actions on standard elements in the browser. But when you try to automate (complex) controls of control vendors like Telerik, Infragistics or even your own custom controls, things get to start ugly quickly. Wouldn't it be nice if we could automate these controls in the same way we do automate a TextField ?

In this blog post and the next one, I while show you how.

The html code to automate

Lets start small and simple. The following HTML snippet is created by a (fictitious) custom date time control. For one of our (again fictitious) tests we need to set the values of the date and time input elements.

<table id="datetime2">
    <tr>
        <td>
            <input type="hidden" id="original_date" value="01-01-2007" />
            <input type="text" id="date" value="01-01-2007" />
        </td>
        <td>
            <input type="hidden" id="original_time" value="10:00:00" />
            <input type="text" id="time" value="10:00:00" />
        </td>
    </tr>
</table>

As you can see the html for this date time control contains two input fields, one for the date and one for the time. There are also two hidden input elements containing the original values of the input fields.

The simple way

To automate setting the date and time the following code will do the job nicely.

IE ie = new IE("http://www.examples.watin.net/datetime.html");

ie.Table("datetime2").TextField(new Regex("^date$")).TypeText("01-11-2008");
ie.Table("datetime2").TextField(new Regex("^time$")).TypeText("12:00:00");

So what's wrong with this code you might wonder. It basically comes down to being code and logic that isn't reusable. To name two issues:

  • To get the correct TextField for the date or time, you need to remember (or find out the hard way) the correct regular expression. Otherwise you might end up setting the value of the hidden input element which will give you a nice exception (and then you remember... and I can tell).
  • The formatting of the date and time needs to be in this specific format for the control to work.

We could refactor this into a method, but still. WatiN offers you all kinds of objects that wrap html elements so it feels more natural for this date time control to be wrapped as well.

Basic wrapping

So lets create a wrapper. I will use Table as the base for our new element class since Table seems to be the container element for the control.

public class DateTimeElement : Table
{
    public DateTimeElement(Table table) : base(table) { }

    public void TypeDateTime(DateTime value)
    {
        TextField(new Regex("^date$")).TypeText(value.ToString("dd-MM-yyyy"));
        TextField(new Regex("^time$")).TypeText(value.ToString("hh:mm:ss"));
    }
}

We can use this new DateTimeElement class in our tests like this:

DateTimeElement element = new DateTimeElement(ie.Table("datetime2"));
element.TypeDateTime(new DateTime(2008, 11, 1, 12, 00, 00));

In my opinion the readability and intention of this code is way better then our first approach. And it also encapsulate the knowledge of the regular expression and the specific formatting of the date and time.

So are we happy now. Not completely. I think the syntax of creating the new DateTimeElement can be improved cause it feels weird to pass in a table element instance into the constructor. Another, not visible, drawback of passing in an element is that it doesn't use WatiN's deferred execution to find the table element on the page. In more complicated wrappers this might lead to unexpected behavior. For now I will spare you the details.

Stay tuned

In part II I will show you how you can get deferred find execution by providing a small helper class and making some changes to the DateTimeElement constructor.

To top things off I will use extension methods (C# 3.0 only) to give you this end result in you test code:

ie.DateTimeElement("datetime2").TypeDateTime(new DateTime(2008, 11, 1, 12, 00, 00));

Stay tuned....

Tags van Technorati: